package opslog import ( "encoding/json" "fmt" "io" "os" "path/filepath" "strings" "time" ) // LogEntry is the detailed record written to .jsonl files and flushed to the DB. type LogEntry struct { IterationID int64 `json:"iteration_id"` DryRun bool `json:"dry_run"` Operation string `json:"operation"` Filepath string `json:"filepath"` SizeBefore int64 `json:"size_before"` SizeAfter int64 `json:"size_after"` MD5Before string `json:"md5_before"` MD5After string `json:"md5_after"` StartedAt time.Time `json:"started_at"` EndedAt time.Time `json:"ended_at"` Status string `json:"status"` ErrorMessage string `json:"error_message"` } // OpRecord describes a single file operation performed during a sync pass. type OpRecord struct { IterID int64 `json:"iter_id"` RelPath string `json:"rel_path"` Action string `json:"action"` // create | update | delete | skip | fail Bytes int64 `json:"bytes,omitempty"` ErrMsg string `json:"error,omitempty"` At time.Time `json:"at"` Owner string `json:"owner,omitempty"` } // Writer serialises OpRecords as newline-delimited JSON. type Writer struct { file *os.File // non-nil when opened via Open enc *json.Encoder } // NewWriter wraps any io.Writer (useful for testing). func NewWriter(w io.Writer) *Writer { return &Writer{enc: json.NewEncoder(w)} } // Open creates logDir if needed and opens a new .jsonl log file named after // pair, direction and the current UTC timestamp. func Open(logDir, pair, direction string) (*Writer, error) { if err := os.MkdirAll(logDir, 0o755); err != nil { return nil, fmt.Errorf("opslog: create dir: %w", err) } ts := strings.ReplaceAll(time.Now().UTC().Format(time.RFC3339), ":", "-") name := fmt.Sprintf("opslog-%s-%s-%s.jsonl", pair, direction, ts) f, err := os.Create(filepath.Join(logDir, name)) if err != nil { return nil, fmt.Errorf("opslog: open file: %w", err) } return &Writer{file: f, enc: json.NewEncoder(f)}, nil } // WriteOp stamps rec with the current UTC time and encodes it as JSON. func (w *Writer) WriteOp(rec OpRecord) error { rec.At = time.Now().UTC() return w.enc.Encode(rec) } // Close closes the underlying file if the Writer was created with Open. func (w *Writer) Close() error { if w.file != nil { return w.file.Close() } return nil } // Path returns the log file path when the Writer was created with Open. func (w *Writer) Path() string { if w.file != nil { return w.file.Name() } return "" }