package selfupdate import ( "encoding/json" "fmt" "log" "os" "path/filepath" ) const stateFileName = "update-state.json" // UpdateState tracks the last update attempt. Persisted to disk as audit log. type UpdateState struct { Status string `json:"status"` // "pending", "success", "failed" PreviousVersion string `json:"previous_version"` PreviousImage string `json:"previous_image"` TargetVersion string `json:"target_version"` TargetImage string `json:"target_image"` InitiatedAt string `json:"initiated_at"` // RFC3339 InitiatedBy string `json:"initiated_by"` // "manual" or "auto" CompletedAt string `json:"completed_at,omitempty"` Error string `json:"error,omitempty"` } // LoadState reads the update state file. Returns nil, nil if file doesn't exist. func LoadState(dataDir string) (*UpdateState, error) { path := filepath.Join(dataDir, stateFileName) data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, fmt.Errorf("reading state file: %w", err) } var state UpdateState if err := json.Unmarshal(data, &state); err != nil { return nil, fmt.Errorf("parsing state file: %w", err) } return &state, nil } // SaveState writes the state file atomically (write to .tmp, then rename). func SaveState(dataDir string, state *UpdateState) error { path := filepath.Join(dataDir, stateFileName) tmpPath := path + ".tmp" data, err := json.MarshalIndent(state, "", " ") if err != nil { return fmt.Errorf("marshaling state: %w", err) } if err := os.WriteFile(tmpPath, data, 0644); err != nil { return fmt.Errorf("writing temp state file: %w", err) } if err := os.Rename(tmpPath, path); err != nil { return fmt.Errorf("renaming state file: %w", err) } return nil } // ClearState removes the state file. Used for cleanup. func ClearState(dataDir string, logger *log.Logger) { path := filepath.Join(dataDir, stateFileName) if err := os.Remove(path); err != nil && !os.IsNotExist(err) { logger.Printf("[WARN] Failed to clear update state file: %v", err) } }