package opslog import ( "bufio" "database/sql" "encoding/json" "fmt" "os" "path/filepath" "time" "github.com/vandachevici/ha-sync/internal/db" ) // FlushAll decodes every *.jsonl file in logDir, groups entries by // iteration_id, and inserts each group via db.BulkInsertOperations. // Successfully flushed files are deleted; files that fail are left in place. // The last error encountered is returned (nil if all files succeeded). func FlushAll(logDir string, sqlDB *sql.DB) error { files, err := filepath.Glob(filepath.Join(logDir, "*.jsonl")) if err != nil { return fmt.Errorf("opslog: glob: %w", err) } var lastErr error for _, path := range files { if err := flushFile(path, sqlDB); err != nil { lastErr = err continue } _ = os.Remove(path) } return lastErr } // iterGroup holds the entries that share the same iteration_id, all of which // must also share the same dry_run flag (set when the iteration was started). type iterGroup struct { dryRun bool entries []LogEntry } func flushFile(path string, sqlDB *sql.DB) error { f, err := os.Open(path) if err != nil { return fmt.Errorf("opslog: open %s: %w", path, err) } defer f.Close() groups := make(map[int64]*iterGroup) scanner := bufio.NewScanner(f) for scanner.Scan() { var entry LogEntry if err := json.Unmarshal(scanner.Bytes(), &entry); err != nil { return fmt.Errorf("opslog: decode %s: %w", path, err) } g, ok := groups[entry.IterationID] if !ok { g = &iterGroup{dryRun: entry.DryRun} groups[entry.IterationID] = g } g.entries = append(g.entries, entry) } if err := scanner.Err(); err != nil { return fmt.Errorf("opslog: scan %s: %w", path, err) } for iterID, g := range groups { ops := make([]db.OpRecord, len(g.entries)) for i, e := range g.entries { ops[i] = db.OpRecord{ Operation: e.Operation, Filepath: e.Filepath, SizeBefore: e.SizeBefore, SizeAfter: e.SizeAfter, MD5Before: e.MD5Before, MD5After: e.MD5After, StartedAt: e.StartedAt, EndedAt: e.EndedAt, Status: e.Status, ErrorMessage: e.ErrorMessage, } } if err := db.BulkInsertOperations(sqlDB, iterID, g.dryRun, ops); err != nil { return fmt.Errorf("opslog: insert iteration %d from %s: %w", iterID, path, err) } } return nil } // CleanOld removes *.jsonl files in logDir whose modification time is older // than retainDays days. Files that cannot be stat'd are skipped silently. func CleanOld(logDir string, retainDays int) error { files, err := filepath.Glob(filepath.Join(logDir, "*.jsonl")) if err != nil { return fmt.Errorf("opslog: glob: %w", err) } cutoff := time.Now().AddDate(0, 0, -retainDays) for _, path := range files { info, err := os.Stat(path) if err != nil { continue } if info.ModTime().Before(cutoff) { _ = os.Remove(path) } } return nil }