Documentation
¶
Index ¶
- Constants
- Variables
- func ComputeChecksum(paths []string) (string, error)
- func CreateArchive(archivePath string, entries []ArchiveEntry) error
- func DeleteBackup(manifest Manifest) error
- func Prune(backupDir string, retentionCount int) ([]string, error)
- func RenameBackup(manifest Manifest, newDescription string) error
- func TogglePin(manifest Manifest) error
- func WriteManifest(path string, manifest Manifest) error
- type ArchiveEntry
- type BackupSource
- type Manifest
- type ManifestEntry
- type PathKind
- type RestoreService
- type Snapshotter
Constants ¶
const ArchiveFilename = "snapshot.tar.gz"
ArchiveFilename is the name of the compressed archive inside a backup directory.
const DefaultRetentionCount = 5
DefaultRetentionCount is the default number of unpinned backups to keep. Pinned backups do not count toward this limit and are never pruned.
const ManifestFilename = "manifest.json"
Variables ¶
var BackupRootFn = backupRoot
BackupRootFn is the function used to resolve the backup root directory. Package-level var for testability — swapped in tests to use a temp directory. Exported so tests in other packages (e.g. internal/update/upgrade) can override it.
var UserHomeDirFn = os.UserHomeDir
UserHomeDirFn is the function used to resolve the user's home directory. Package-level var for testability — swapped in tests to use a temp directory.
Functions ¶
func ComputeChecksum ¶
ComputeChecksum computes a composite SHA-256 checksum over the given file paths.
Algorithm:
- Filter to paths that exist and are regular files; skip others silently.
- Sort filtered paths lexicographically (deterministic ordering).
- For each path, compute SHA-256 of its contents.
- Concatenate all "path:hexhash\n" pairs into a single string.
- SHA-256 the concatenated result and return the hex-encoded digest.
If paths is empty or all paths are missing/non-regular, ComputeChecksum returns ("", nil) — callers treat an empty checksum as "no dedup possible".
Note: checksums include absolute file paths, so the same files at different paths (e.g., different home directories) will produce different checksums. This is intentional: dedup is per-machine, not cross-machine.
func CreateArchive ¶
func CreateArchive(archivePath string, entries []ArchiveEntry) error
CreateArchive writes a tar.gz archive at archivePath containing all entries. Each entry's content is read from SourcePath and stored under RelPath inside the archive. Intermediate directories in archivePath are created automatically.
The archive uses filepath.ToSlash for cross-platform tar path compatibility.
func DeleteBackup ¶
DeleteBackup removes the entire backup directory.
func Prune ¶
Prune deletes the oldest unpinned backups in backupDir, keeping at most retentionCount unpinned backups. Pinned backups are never deleted.
If retentionCount <= 0 the function is a no-op (unlimited retention).
Directories inside backupDir that do not contain a readable manifest.json are silently skipped and do not count toward the limit.
Deletion errors for individual backup directories are logged but do not abort the loop — remaining backups are still evaluated.
Returns the list of deleted backup IDs.
func RenameBackup ¶
RenameBackup updates the backup's Description field in the manifest file. This does not rename the directory — it updates the human-readable description.
func TogglePin ¶
TogglePin flips the Pinned field of the manifest and rewrites the manifest.json file inside the backup's RootDir. Pinned backups are excluded from retention pruning. Returns an error if RootDir is empty or the write fails.
func WriteManifest ¶
Types ¶
type ArchiveEntry ¶
type ArchiveEntry struct {
// RelPath is the relative path inside the archive (e.g. "files/.config/foo.json").
RelPath string
// SourcePath is the absolute path on disk (source when creating; destination when extracting).
SourcePath string
// Mode is the file permission bits.
Mode os.FileMode
}
ArchiveEntry describes a single file to include in a tar.gz archive (for creation) or a file that was extracted from one (for extraction).
func ExtractArchive ¶
func ExtractArchive(archivePath string, destDir string) ([]ArchiveEntry, error)
ExtractArchive extracts a tar.gz archive at archivePath into destDir. It returns the list of extracted entries (RelPath = path inside archive, SourcePath = absolute path of the extracted file inside destDir).
Path traversal protection: any entry whose RelPath contains ".." is rejected with an error and extraction is aborted.
type BackupSource ¶
type BackupSource string
BackupSource identifies what operation created a backup. New values may be added in future — consumers must handle unknown values gracefully.
const ( // BackupSourceInstall indicates the backup was created before an install run. BackupSourceInstall BackupSource = "install" // BackupSourceSync indicates the backup was created before a sync run. BackupSourceSync BackupSource = "sync" // BackupSourceUpgrade indicates the backup was created before an upgrade run. BackupSourceUpgrade BackupSource = "upgrade" // BackupSourceUninstall indicates the backup was created before an uninstall run. BackupSourceUninstall BackupSource = "uninstall" )
func (BackupSource) Label ¶
func (s BackupSource) Label() string
Label returns a human-readable string for the BackupSource. Unknown or empty sources return "unknown source" so old manifests display gracefully.
type Manifest ¶
type Manifest struct {
ID string `json:"id"`
CreatedAt time.Time `json:"created_at"`
RootDir string `json:"root_dir"`
Entries []ManifestEntry `json:"entries"`
// Source identifies what operation created this backup.
// Optional: omitted for backward-compatibility with old manifests.
Source BackupSource `json:"source,omitempty"`
// Description is a short human-readable note about the backup context.
// Optional: omitted for backward-compatibility with old manifests.
Description string `json:"description,omitempty"`
// FileCount is the number of files that existed and were actually snapshotted.
// Entries where Existed==false (files that did not exist at snapshot time) are
// not counted. Optional: omitted when zero for backward-compatibility.
FileCount int `json:"file_count,omitempty"`
// CreatedByVersion is the teicos version that created this backup.
// Optional: omitted when empty for backward-compatibility with old manifests.
CreatedByVersion string `json:"created_by_version,omitempty"`
// Pinned marks the backup as protected from retention pruning.
// Optional: omitted when false for backward-compatibility with old manifests.
Pinned bool `json:"pinned,omitempty"`
// Compressed indicates the backup files are stored as a tar.gz archive.
// Optional: omitted when false for backward-compatibility with old manifests.
Compressed bool `json:"compressed,omitempty"`
// Checksum is the SHA-256 composite hash of the snapshotted files, used for deduplication.
// Optional: omitted when empty for backward-compatibility with old manifests.
Checksum string `json:"checksum,omitempty"`
}
func DuplicateManifest ¶
DuplicateManifest returns the most recent manifest when its checksum matches newChecksum. It reports no match when the checksum is empty, no prior backup exists, or the latest manifest predates checksum-based deduplication. Directories without a manifest.json are silently skipped.
func ReadManifest ¶
func (Manifest) DisplayLabel ¶
DisplayLabel returns a human-readable label for the backup suitable for display in the CLI restore list and TUI backup screen. It combines the source label and the formatted creation timestamp, and appends the file count when known.
Old manifests without Source will show "unknown source" as a graceful fallback. Old manifests without FileCount will not show any file count.
type ManifestEntry ¶
type ManifestEntry struct {
OriginalPath string `json:"original_path"`
SnapshotPath string `json:"snapshot_path"`
Existed bool `json:"existed"`
Mode uint32 `json:"mode,omitempty"`
Kind PathKind `json:"kind,omitempty"`
// LinkTarget is the recorded symlink target. Only set when Kind
// is PathKindSymlinkDirectory. Restore validates that it is a safe
// relative path (no leading '/', no '..' segments that escape).
LinkTarget string `json:"link_target,omitempty"`
}
type PathKind ¶
type PathKind string
PathKind classifies a backed-up path for restore. The empty value is the legacy "unknown" state — persisted manifests written before this field existed carry it implicitly, and the restore path applies a safe compatibility policy (see RestoreService for details).
const ( // PathKindUnknown is the legacy default. Restore treats unknown // Existed==false entries as preserve-only (do not delete) so we // never destroy a path whose original type we cannot prove. PathKindUnknown PathKind = "" // PathKindRegularFile marks a regular file. Snapshotted into the // archive and restored by reading the archive entry. PathKindRegularFile PathKind = "regular" // PathKindDirectory marks an empty directory. Not archived. // Restore ensures the directory still exists; never deletes a // pre-existing directory even if the install/sync that produced // this snapshot created it. PathKindDirectory PathKind = "directory" // PathKindSymlinkDirectory marks a relative symlink to a // directory. Not archived. Restore validates LinkTarget is a safe // relative path and recreates the symlink if it is missing. PathKindSymlinkDirectory PathKind = "symlink_directory" )
type RestoreService ¶
type RestoreService struct {
Roots []string
}
RestoreService restores a backup manifest, writing back or removing files at their OriginalPath.
Roots restricts which directories a restore is allowed to write to or remove from. Every non-pre-existing manifest entry's OriginalPath must resolve under at least one Roots entry, or the restore refuses it.
When Roots is empty, Restore falls back to the single directory returned by UserHomeDirFn — the historical, backward-compatible behavior. This is the correct default for standalone restores (`gentle-ai restore <id>` and the TUI "restore from list" screen): the backup being restored may be arbitrarily old, and the workspace root that was in effect when it was created is not something the current process can safely rediscover on its own, so it falls back to the one directory it explicitly owns.
Callers that are still inside the same install/sync run that produced the manifest (e.g. pipeline rollback) know their own scope roots out of band and should set Roots explicitly — see rollbackRoots in internal/cli.
func (RestoreService) Restore ¶
func (s RestoreService) Restore(manifest Manifest) error
type Snapshotter ¶
type Snapshotter struct {
// contains filtered or unexported fields
}
func NewSnapshotter ¶
func NewSnapshotter() Snapshotter