// Package debugbundle owns explicitly requested per-run debug bundles. package debugbundle import ( "fmt" "os" "path/filepath" "strings" "time" ) type Bundle struct { path, summaryRoot, traceRoot string createdAt time.Time } func Allocate(parent, runID string, startedAt time.Time) (*Bundle, error) { parent = strings.TrimSpace(parent) if parent == "" { return nil, fmt.Errorf("debug parent must not be empty") } if err := validateRunID(runID); err != nil { return nil, err } if err := os.MkdirAll(parent, 0o700); err != nil { return nil, fmt.Errorf("create debug parent %q: %w", parent, err) } path := filepath.Join(parent, runID) if err := os.Mkdir(path, 0o700); err != nil { if os.IsExist(err) { return nil, fmt.Errorf("debug bundle %q already exists", path) } return nil, fmt.Errorf("create debug bundle %q: %w", path, err) } summary, trace := filepath.Join(path, "summary"), filepath.Join(path, "trace") if err := os.Mkdir(summary, 0o700); err != nil { _ = os.Remove(path) return nil, fmt.Errorf("create debug summary %q: %w", summary, err) } if err := os.Mkdir(trace, 0o700); err != nil { _ = os.RemoveAll(path) return nil, fmt.Errorf("create debug trace %q: %w", trace, err) } return &Bundle{path: path, summaryRoot: summary, traceRoot: trace, createdAt: startedAt}, nil } func validateRunID(runID string) error { if runID == "" { return fmt.Errorf("debug run ID must not be empty") } if runID != strings.TrimSpace(runID) || strings.ContainsAny(runID, `/\\`) || filepath.IsAbs(runID) || filepath.Clean(runID) != runID || runID == "." || runID == ".." { return fmt.Errorf("debug run ID %q must be one safe path component", runID) } return nil } func (b *Bundle) Path() string { if b == nil { return "" } return b.path } func (b *Bundle) SummaryRoot() string { if b == nil { return "" } return b.summaryRoot } func (b *Bundle) TraceRoot() string { if b == nil { return "" } return b.traceRoot } func (b *Bundle) RunID() string { if b == nil { return "" } return filepath.Base(b.path) } func (b *Bundle) CreatedAt() time.Time { if b == nil { return time.Time{} } return b.createdAt } func (b *Bundle) Summary() *SummaryWriter { if b == nil { return nil } return &SummaryWriter{root: b.summaryRoot, runID: b.RunID(), createdAt: b.createdAt} }