feat(cli): separate online reports from offline diagnosis
Ask the running daemon for storage usage and keep local SQL in the offline path. Private snapshots leave the installation untouched.
100 unmodified lines1011021031041051061071081093 unmodified lines100 unmodified lines } return found.Kind == daemon.Running, err }, DeleteSessions: application.DeleteSessions, OnlineReport: application.StorageReport, } workspace, err := os.Getwd() if err != nil { workspace = "."3 unmodified lines12345678910111213141516171819202122package app
import ( "context"
"albedo/cli/internal/daemon")
// StorageReport attaches to a running daemon without launching or upgrading it.// A nil report means discovery found no running daemon.func (s *Service) StorageReport(ctx context.Context) (*daemon.StorageReport, error) { conn, err := s.Existing(ctx) if err != nil || conn == nil { return nil, err } defer conn.HTTPClient().CloseIdleConnections() report, err := daemon.GetStorageReport(ctx, conn) if err != nil { return nil, err } return &report, nil}5 unmodified lines6789101112131415151617181920171819212223242526272829303132333435243637383940414243444582 unmodified lines5 unmodified lines "os/signal" "strings" "syscall"
"albedo/cli/internal/daemon" "albedo/cli/internal/storage" "github.com/spf13/cobra")
func newStorage(deps Dependencies) *cobra.Command { var asJSON, sessions bool var asJSON, sessions, offline bool command := &cobra.Command{Use: "storage", Short: "show disk usage or clean up selected data", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { var preview *daemon.StorageReport var err error if !offline { preview, err := deps.Storage.Report(cmd.Context()) if err != nil { return err preview, err = deps.Application.StorageReport(cmd.Context()) if err != nil { return err } } if preview == nil { report, reportErr := deps.Storage.OfflineReport(cmd.Context()) if reportErr != nil { return reportErr } preview = &report } if asJSON { return writeJSON(cmd.OutOrStdout(), preview, true) } if err := storagePrint(cmd.OutOrStdout(), preview, sessions); err != nil { if err := storagePrint(cmd.OutOrStdout(), *preview, sessions); err != nil { return err } return storageHint(cmd.OutOrStdout()) }} command.Flags().BoolVar(&offline, "offline", false, "inspect local storage without contacting the daemon") command.Flags().BoolVar(&asJSON, "json", false, "print the full storage report as JSON") command.Flags().BoolVar(&sessions, "sessions", false, "show estimated sizes by session") command.MarkFlagsMutuallyExclusive("json", "sessions") command.AddCommand(newPrune(deps))82 unmodified lines5 unmodified lines67891010111213148 unmodified lines232425262727282930318 unmodified lines4041424344444546474811 unmodified lines5 unmodified lines "io" "slices" "strings"
"albedo/cli/internal/storage" "albedo/cli/internal/daemon")
func storageSize(bytes int64) string { if bytes < 1024 {8 unmodified lines } return ""}
func storagePrint(w io.Writer, p storage.Preview, sessions bool) error {func storagePrint(w io.Writer, p daemon.StorageReport, sessions bool) error { var text strings.Builder fmt.Fprintf(&text, "Database: %s · %s of unused space can be reclaimed with --vacuum\n", storageSize(p.Database), storageSize(p.DB.FreePages*p.DB.PageSize)) fmt.Fprintf(&text, "Shared images: %s (each image is stored once)\n", storageSize(p.DB.Images)) fmt.Fprintf(&text, "Backups: %s · %d recent migration backups (%s) are kept for 30 days, including with --all\n",8 unmodified lines backups += f.Bytes } fmt.Fprintf(&text, "Available to clean up: %d unused Python state files (%s), %d old backups (%s)\n", len(p.OldKernels), storageSize(kernels), len(p.OldBackups), storageSize(backups)) if sessions { slices.SortFunc(p.DB.Sessions, func(a, b storage.Session) int { return cmp.Compare(b.Bytes, a.Bytes) }) slices.SortFunc(p.DB.Sessions, func(a, b daemon.StorageSession) int { return cmp.Compare(b.Bytes, a.Bytes) }) for _, s := range p.DB.Sessions { fmt.Fprintf(&text, " %s %s\n", storageSize(s.Bytes), s.ID) } }11 unmodified lines123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110// Controlled peers return unsupported or malformed reports, and cancellation// can interrupt a read. None of these failures may fall back to local storage.package cli
import ( "bytes" "context" "errors" "io" "net" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "time"
"albedo/cli/internal/app" "albedo/cli/internal/daemon" "albedo/cli/internal/storage")
func TestStorageCommandKeepsOnlineFailuresWithoutLocalFallback(t *testing.T) { for _, scenario := range []struct { name string capabilities, body string }{ {name: "unsupported", capabilities: `[]`}, {name: "malformed", capabilities: `["storage_report"]`, body: `{"database":12}`}, } { t.Run(scenario.name, func(t *testing.T) { home := filepath.Join(t.TempDir(), "unreadable") if err := os.WriteFile(home, []byte("not a directory"), 0600); err != nil { t.Fatal(err) } reportCalls := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/health" { _, _ = w.Write([]byte(`{"ok":true,"version":2,"capabilities":` + scenario.capabilities + `}`)) return } reportCalls++ _, _ = w.Write([]byte(scenario.body)) })) defer server.Close() conn := daemon.NewConnection(daemon.ConnectionSnapshot{Port: server.Listener.Addr().(*net.TCPAddr).Port}, nil) defer conn.HTTPClient().CloseIdleConnections() application := &app.Service{Connect: func(context.Context) (*daemon.Connection, error) { t.Fatal("unexpected launch"); return nil, nil }, Existing: func(context.Context) (*daemon.Connection, error) { return conn, nil }} var output bytes.Buffer err := Execute(t.Context(), []string{"storage", "--json"}, Dependencies{Application: application, Storage: &storage.Service{Home: home, Now: time.Now}}, Streams{In: bytes.NewReader(nil), Out: &output, Err: io.Discard}) if err == nil || output.Len() != 0 { t.Fatalf("failure delivered a report: %v %s", err, output.Bytes()) } switch scenario.name { case "unsupported": if _, ok := errors.AsType[*daemon.UpgradeRequiredError](err); !ok { t.Fatalf("lost capability failure: %v", err) } case "malformed": if _, ok := errors.AsType[*daemon.ProtocolError](err); !ok { t.Fatalf("lost protocol failure: %v", err) } } if scenario.name == "unsupported" && reportCalls != 0 { t.Fatalf("dispatched unsupported report %d times", reportCalls) } }) }}
func TestStorageCommandCancellationDoesNotInspectLocalStorage(t *testing.T) { started := make(chan struct{}) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/health" { _, _ = w.Write([]byte(`{"ok":true,"version":2,"capabilities":["storage_report"]}`)) return } close(started) <-r.Context().Done() })) defer server.Close() conn := daemon.NewConnection(daemon.ConnectionSnapshot{Port: server.Listener.Addr().(*net.TCPAddr).Port}, nil) defer conn.HTTPClient().CloseIdleConnections() home := filepath.Join(t.TempDir(), "not-a-directory") if err := os.WriteFile(home, []byte("unreadable storage"), 0600); err != nil { t.Fatal(err) } application := &app.Service{Existing: func(ctx context.Context) (*daemon.Connection, error) { return conn, ctx.Err() }} ctx, cancel := context.WithCancel(t.Context()) defer cancel() done := make(chan error, 1) var output bytes.Buffer go func() { done <- Execute(ctx, []string{"storage", "--json"}, Dependencies{Application: application, Storage: &storage.Service{Home: home, Now: time.Now}}, Streams{In: bytes.NewReader(nil), Out: &output, Err: io.Discard}) }() select { case <-started: case <-time.After(5 * time.Second): t.Fatal("report request never started") } cancel() select { case err := <-done: if !errors.Is(err, context.Canceled) || output.Len() != 0 { t.Fatalf("cancellation lost or returned a report: %v %s", err, output.Bytes()) } case <-time.After(5 * time.Second): t.Fatal("storage report did not stop after cancellation") }}1234567893 unmodified lines1314151617181920217 unmodified lines293031323133343534363738394012 unmodified lines535455565556575758596015 unmodified lines767778798081828384858687888990919293948195969798991008 unmodified lines1091101111129811311411511611711811912012112212312412512612712812916 unmodified lines1461471481491291501511521531543 unmodified lines1581591601611411621631641651669 unmodified lines17617717817918018118218318418518610 unmodified lines// Package storage inspects local usage and applies approved, revalidated cleanup plans.package storage
import ( "albedo/cli/internal/daemon" "context" "errors" "fmt" "os"3 unmodified lines)
// Service keeps offline inspection separate from daemon-backed session deletion.type Service struct { OnlineReport func(context.Context) (*daemon.StorageReport, error) Now func() time.Time Running func() (bool, error) DeleteSessions func(context.Context, []string) error Home string7 unmodified linestype CleanupPlan struct { files map[string]os.FileInfo home string options CleanupOptions preview Preview preview daemon.StorageReport}
func (p CleanupPlan) Preview() Preview {func (p CleanupPlan) Preview() daemon.StorageReport { result := p.preview result.DB.Sessions = slices.Clone(result.DB.Sessions) result.OldKernels = slices.Clone(result.OldKernels) result.OldBackups = slices.Clone(result.OldBackups)12 unmodified lines}
var storageID = regexp.MustCompile(`^[0-9a-f]{32}$`)
func (s *Service) Report(ctx context.Context) (Preview, error) { return storageSnapshot(ctx, s.Home, s.Now())}func (s *Service) PlanCleanup(ctx context.Context, options CleanupOptions) (CleanupPlan, error) { var plan CleanupPlan options.Sessions = slices.Clone(options.Sessions) for i, id := range options.Sessions {15 unmodified lines } if len(options.Sessions) > 0 && (options.OldKernels || options.Backups || options.Vacuum) { return plan, errors.New("delete sessions and clean up local files in separate commands; session deletion needs Albedo running, file cleanup needs it stopped") } var preview daemon.StorageReport var err error if len(options.Sessions) > 0 { if s.OnlineReport == nil { return plan, errors.New("session deletion needs a running daemon; start Albedo, then try again") } online, reportErr := s.OnlineReport(ctx) if reportErr != nil { return plan, reportErr } if online == nil { return plan, errors.New("session deletion needs a running daemon; start Albedo, then try again") } preview = *online } else { preview, err := s.Report(ctx) preview, err = s.OfflineReport(ctx) } if err != nil { return plan, err } if len(options.Sessions) == 0 {8 unmodified lines return plan, errors.New("cleanup needs Albedo stopped; no files have been removed; when its work has finished, run albedo daemon --stop, then try again") } } else { for _, id := range options.Sessions { if !slices.ContainsFunc(preview.DB.Sessions, func(session Session) bool { return session.ID == id }) { if !slices.ContainsFunc(preview.DB.Sessions, func(session daemon.StorageSession) bool { return session.ID == id }) { return plan, fmt.Errorf("no session has the ID %s; use the full ID from albedo sessions", id) } } } plan = CleanupPlan{preview: preview, options: options, home: s.Home, files: make(map[string]os.FileInfo)} if len(options.Sessions) > 0 { return plan, nil } for _, file := range append(slices.Clone(preview.OldKernels), preview.OldBackups...) { if err := ctx.Err(); err != nil { return CleanupPlan{}, err } info, err := os.Lstat(file.Path) if err != nil { return CleanupPlan{}, err }16 unmodified lines } if running { return result, errors.New("daemon started while you were confirming cleanup; no files have been removed; stop Albedo and try again") } var files []File var files []daemon.StorageFile if options.OldKernels { files = append(files, preview.OldKernels...) } if options.Backups {3 unmodified lines if err != nil { return result, err } defer helper.close() fresh, err := s.Report(ctx) fresh, err := s.OfflineReport(ctx) if err != nil { if ctx.Err() != nil { return result, ctx.Err() }9 unmodified lines return result, errors.New("backups changed after the preview; no backups have been removed; run cleanup again to review the new list") } // Recheck all approved candidates before deleting any, including replacements of equal size. for _, file := range files { if err := ctx.Err(); err != nil { return result, err } info, err := os.Lstat(file.Path) if err != nil { return result, err }10 unmodified lines123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134"""Read known storage layouts without creating, migrating, or modifying SQLite."""
from contextlib import closing
import jsonimport sqlite3import shutilimport statimport sysfrom pathlib import Path
def columns(db, table, required): found = {row[1] for row in db.execute(f"PRAGMA table_info({table})")} if not required <= found: missing = ", ".join(sorted(required - found)) raise ValueError( f"unsupported storage layout: {table} lacks {missing}; " "use a compatible daemon to inspect or upgrade this database" ) return found
def source_identity(path): try: info = path.lstat() except FileNotFoundError: return None if not stat.S_ISREG(info.st_mode): raise ValueError(f"offline storage snapshot requires a regular file: {path}") return info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns
def inspect(path, directory): sources = [path, Path(str(path) + "-wal")] before = [source_identity(source) for source in sources] if before[0] is None: raise ValueError( "database disappeared before offline inspection; retry the report" ) # SQLite may create SHM while reading WAL, even in read-only mode. Keep that # side effect in a private snapshot and never build an index beside the source. snapshot = Path(directory) / "albedo.sqlite" for source, identity, target in zip( sources, before, [snapshot, Path(str(snapshot) + "-wal")] ): if identity is not None: shutil.copyfile(source, target) if [source_identity(source) for source in sources] != before: raise ValueError( "storage changed while copying the offline snapshot; retry the report or query the running daemon" ) with closing(sqlite3.connect(snapshot.as_uri() + "?mode=ro", uri=True)) as db: db.execute("PRAGMA query_only=ON") tables = { row[0] for row in db.execute("SELECT name FROM sqlite_master WHERE type='table'") } # Existing databases must identify sessions and their durable transcript. session_columns = columns(db, "sessions", {"id"}) columns(db, "transcript", {"session", "payload"}) # This projection supports pre-pinned sessions and both TEXT and BLOB # image rows; startup migration markers need not be applied or rewritten. pinned = ( "COALESCE(length(pinned_context),0)" if "pinned_context" in session_columns else "0" ) sessions = [ {"id": session_id, "bytes": size} for session_id, size in db.execute( f"SELECT id, {pinned} FROM sessions ORDER BY id" ) ] identities = [session["id"] for session in sessions] if any( not isinstance(identity, str) or not identity for identity in identities ): raise ValueError( "unsupported storage layout: invalid session identity; use a compatible daemon to inspect this database" ) if len(set(identities)) != len(identities): raise ValueError( "unsupported storage layout: duplicate session identities; use a compatible daemon to inspect this database" ) sizes = dict( db.execute( "SELECT session, COALESCE(sum(length(payload)),0) " "FROM transcript GROUP BY session" ) ) for session in sessions: session["bytes"] += sizes.get(session["id"], 0) if "cell_traces" in tables: columns(db, "cell_traces", {"id", "payload"}) if "cells" in tables: columns(db, "cells", {"id", "session", "source", "payload"}) traces = ( "LEFT JOIN cell_traces t ON t.id=c.id" if "cell_traces" in tables else "" ) trace_bytes = "COALESCE(length(t.payload),0)" if traces else "0" sizes = dict( db.execute( "SELECT c.session, COALESCE(sum(length(c.source) + " "COALESCE(length(c.payload),0) + " + trace_bytes + "),0) FROM cells c " + traces + " GROUP BY c.session" ) ) for session in sessions: session["bytes"] += sizes.get(session["id"], 0) images = 0 if "images" in tables: columns(db, "images", {"data"}) images = db.execute( "SELECT COALESCE(sum(length(data)),0) FROM images" ).fetchone()[0] return { "sessions": sessions, "images": images, "free_pages": db.execute("PRAGMA freelist_count").fetchone()[0], "page_size": db.execute("PRAGMA page_size").fetchone()[0], }
if __name__ == "__main__": try: print(json.dumps(inspect(Path(sys.argv[1]), sys.argv[2]))) except (sqlite3.Error, ValueError, OSError) as error: sys.exit(str(error))1234567820 unmodified lines2930313232333435363749 unmodified lines8788899090919293949528 unmodified linespackage storage
import ( "albedo/cli/internal/daemon" "bytes" "context" _ "embed" "encoding/json"20 unmodified lines Result *CleanupResult `json:"result"` Ready bool `json:"ready"`}
func startMaintenance(ctx context.Context, home string, files []File) (*maintenance, error) {func startMaintenance(ctx context.Context, home string, files []daemon.StorageFile) (*maintenance, error) { helper := &maintenance{command: exec.CommandContext(ctx, "python3", "-c", maintenanceScript, home)} helper.command.Stderr = &helper.stderr input, err := helper.command.StdinPipe() if err != nil {49 unmodified lines } return fmt.Errorf("storage maintenance: %w: %s", err, strings.TrimSpace(m.stderr.String()))}
func (m *maintenance) apply(ctx context.Context, vacuum bool, fresh Preview) (CleanupResult, error) {func (m *maintenance) apply(ctx context.Context, vacuum bool, fresh daemon.StorageReport) (CleanupResult, error) { var result CleanupResult err := json.NewEncoder(m.input).Encode(struct { Apply bool `json:"apply"` Vacuum bool `json:"vacuum"`28 unmodified lines1 unmodified line234567891017 unmodified lines282930313132333435364 unmodified lines41424344444546474748495051525354555556575859606 unmodified lines67686970707172737475767677787879808181828384858639 unmodified lines1 unmodified line// E2E cannot pause Go's apply call between helper capture and authorization.package storage
import ( "albedo/cli/internal/daemon" "context" "errors" "os" "path/filepath"17 unmodified lines t.Fatal(err) } ctx, cancel := context.WithCancel(t.Context()) defer cancel() helper, err := startMaintenance(ctx, home, []File{{Path: path, Bytes: 4}}) helper, err := startMaintenance(ctx, home, []daemon.StorageFile{{Path: path, Bytes: 4}}) if err != nil { t.Fatal(err) } defer helper.close()4 unmodified lines t.Fatal(acquireErr) } switch ending { case "success": _, err = helper.apply(ctx, false, Preview{}) _, err = helper.apply(ctx, false, daemon.StorageReport{}) case "cancel": cancel() _, err = helper.apply(ctx, false, Preview{}) _, err = helper.apply(ctx, false, daemon.StorageReport{}) if !errors.Is(err, context.Canceled) { t.Fatalf("lost cancellation: %v", err) } case "helper killed": if err = helper.command.Process.Kill(); err != nil { t.Fatal(err) } _, err = helper.apply(ctx, false, Preview{}) _, err = helper.apply(ctx, false, daemon.StorageReport{}) case "input EOF": if err = helper.input.Close(); err != nil { t.Fatal(err) }6 unmodified lines } if err = os.Rename(replacement, path); err != nil { t.Fatal(err) } _, err = helper.apply(ctx, false, Preview{}) _, err = helper.apply(ctx, false, daemon.StorageReport{}) case "deletion failure": if err = os.Chmod(directory, 0500); err != nil { t.Fatal(err) } defer os.Chmod(directory, 0700) _, err = helper.apply(ctx, false, Preview{}) _, err = helper.apply(ctx, false, daemon.StorageReport{}) case "vacuum failure": _, err = helper.apply(ctx, true, Preview{Database: 4096, DB: Database{FreePages: 1}}) _, err = helper.apply(ctx, true, daemon.StorageReport{Database: 4096, DB: daemon.StorageDatabase{FreePages: 1}}) case "no free pages": var result CleanupResult result, err = helper.apply(ctx, true, Preview{Database: 4096}) result, err = helper.apply(ctx, true, daemon.StorageReport{Database: 4096}) if !result.VacuumSkipped || result.Vacuumed { t.Fatalf("unexpected vacuum result: %+v", result) } }39 unmodified lines123456789102 unmodified lines131415161516171817181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980812021222324252627282930313233343536373839404114 unmodified lines5657585910560616210863646566674 unmodified lines727374751217677787980818283841 unmodified line86878889132909192939495138139969798991001011021031041053 unmodified lines10911011111215111311411511611711811912012112212312412512612712812913013116116213213313413513613713813914016914114214314414514617514714814915015110 unmodified lines1621631641651661671681691701711729 unmodified linespackage storage
import ( "albedo/cli/internal/daemon" "context" _ "embed" "encoding/json" "errors" "fmt" "os"2 unmodified lines "strings" "time")
type Session struct { ID string `json:"id"` Bytes int64 `json:"bytes"`}//go:embed diagnostic.pyvar inspectStorage string
type Database struct { Sessions []Session `json:"sessions"` Images int64 `json:"images"` FreePages int64 `json:"free_pages"` PageSize int64 `json:"page_size"`}
type File struct { Path string `json:"path"` Bytes int64 `json:"bytes"`}
type Preview struct { OldKernels []File `json:"old_kernels"` OldBackups []File `json:"old_backups"` DB Database `json:"db"` Database int64 `json:"database"` WAL int64 `json:"wal"` Kernels int64 `json:"kernels"` Backups int64 `json:"backups"` Other int64 `json:"other"` RecentBackups int64 `json:"recent_backups"` RecentBackupCount int `json:"recent_backup_count"`}
// Use Python's bundled sqlite3 instead of introducing a second SQLite engine.// The read-only URI does not create a missing database or journal.const inspectStorage = `import json, sqlite3, sysfrom pathlib import Pathp = Path(sys.argv[1])if not p.is_file(): print(json.dumps({'sessions': [], 'images': 0, 'free_pages': 0, 'page_size': 0})) sys.exit(0)db = sqlite3.connect(p.as_uri() + '?mode=ro', uri=True)db.execute('PRAGMA query_only=ON')tables = {row[0] for row in db.execute("SELECT name FROM sqlite_master WHERE type='table'")}sessions = []if 'sessions' in tables: for sid, pinned in db.execute('SELECT id, length(pinned_context) FROM sessions'): sessions.append({'id': sid, 'bytes': pinned or 0})if 'transcript' in tables: sizes = dict(db.execute('SELECT session, COALESCE(sum(length(payload)),0) FROM transcript GROUP BY session')) for session in sessions: session['bytes'] += sizes.get(session['id'], 0)if 'cells' in tables: traces = 'LEFT JOIN cell_traces t ON t.id=c.id' if 'cell_traces' in tables else '' trace_bytes = 'COALESCE(length(t.payload),0)' if traces else '0' sql = ('SELECT c.session, COALESCE(sum(length(c.source) + ' 'COALESCE(length(c.payload),0) + ' + trace_bytes + '),0) ' 'FROM cells c ' + traces + ' GROUP BY c.session') sizes = dict(db.execute(sql)) for session in sessions: session['bytes'] += sizes.get(session['id'], 0)images = db.execute('SELECT COALESCE(sum(length(data)),0) FROM images').fetchone()[0] if 'images' in tables else 0print(json.dumps({'sessions': sessions, 'images': images, 'free_pages': db.execute('PRAGMA freelist_count').fetchone()[0], 'page_size': db.execute('PRAGMA page_size').fetchone()[0]}))`
func sqliteStorage(ctx context.Context, path string) (Database, error) { var db Database out, err := exec.CommandContext(ctx, "python3", "-c", inspectStorage, path).CombinedOutput()func sqliteStorage(ctx context.Context, path string) (daemon.StorageDatabase, error) { db := daemon.StorageDatabase{Sessions: []daemon.StorageSession{}} if err := ctx.Err(); err != nil { return db, err } scratch, err := os.MkdirTemp("", "albedo-storage-") if err != nil { return db, fmt.Errorf("create private offline storage snapshot: %w", err) } defer os.RemoveAll(scratch) out, err := exec.CommandContext(ctx, "python3", "-c", inspectStorage, path, scratch).CombinedOutput() if ctx.Err() != nil { return db, ctx.Err() } if errors.Is(err, exec.ErrNotFound) { return db, errors.New("offline storage diagnostics require Python 3; install Python or query a running daemon without --offline") } if err != nil { return db, fmt.Errorf("read disk usage from the database: %w: %s", err, strings.TrimSpace(string(out))) } if err := json.Unmarshal(out, &db); err != nil { return db, fmt.Errorf("decode the database disk usage report: %w", err)14 unmodified lines } return info.Size(), nil}
func storageFiles(dir string, eligible func(string, os.FileInfo) bool) (int64, []File, error) {func storageFiles(ctx context.Context, dir string, eligible func(string, os.FileInfo) bool) (int64, []daemon.StorageFile, error) { stat, err := os.Lstat(dir) if errors.Is(err, os.ErrNotExist) { return 0, nil, nil return 0, []daemon.StorageFile{}, nil } if err != nil { return 0, nil, err }4 unmodified lines if err != nil { return 0, nil, err } var total int64 var candidates []File candidates := []daemon.StorageFile{} for _, entry := range entries { if err := ctx.Err(); err != nil { return 0, nil, err } info, err := entry.Info() if err != nil { return 0, nil, err }1 unmodified line continue } total += info.Size() if eligible(entry.Name(), info) { candidates = append(candidates, File{filepath.Join(dir, entry.Name()), info.Size()}) candidates = append(candidates, daemon.StorageFile{Path: filepath.Join(dir, entry.Name()), Bytes: info.Size()}) } } return total, candidates, nil}
func storageSnapshot(ctx context.Context, home string, now time.Time) (Preview, error) { var p Previewfunc (s *Service) OfflineReport(ctx context.Context) (daemon.StorageReport, error) { home, now := s.Home, s.Now() if err := ctx.Err(); err != nil { return daemon.StorageReport{}, err } p := daemon.StorageReport{OldKernels: []daemon.StorageFile{}, OldBackups: []daemon.StorageFile{}, DB: daemon.StorageDatabase{Sessions: []daemon.StorageSession{}}} root, err := os.Lstat(home) if errors.Is(err, os.ErrNotExist) { return p, nil }3 unmodified lines if !root.IsDir() { return p, fmt.Errorf("ALBEDO_HOME must be a directory: %s", home) } dbPath := filepath.Join(home, "albedo.sqlite") if p.Database, err = regularSize(dbPath); err != nil { databaseInfo, err := os.Lstat(dbPath) databaseExists := err == nil if err != nil && !errors.Is(err, os.ErrNotExist) { return p, err } if databaseExists { if !databaseInfo.Mode().IsRegular() { return p, fmt.Errorf("expected a file, but found a different kind of entry: %s", dbPath) } p.Database = databaseInfo.Size() } for _, suffix := range []string{"-wal", "-shm"} { size, sizeErr := regularSize(dbPath + suffix) if sizeErr != nil { return p, sizeErr } p.WAL += size } if databaseExists { if p.DB, err = sqliteStorage(ctx, dbPath); err != nil { return p, err if p.DB, err = sqliteStorage(ctx, dbPath); err != nil { return p, err } } known := make(map[string]bool) for _, session := range p.DB.Sessions { known[session.ID] = true } cutoff := now.Add(-30 * 24 * time.Hour) p.Kernels, p.OldKernels, err = storageFiles(filepath.Join(home, "kernels"), func(name string, info os.FileInfo) bool { p.Kernels, p.OldKernels, err = storageFiles(ctx, filepath.Join(home, "kernels"), func(name string, info os.FileInfo) bool { return strings.HasSuffix(name, ".state") && !known[strings.TrimSuffix(name, ".state")] && info.ModTime().Before(cutoff) }) if err != nil { return p, err } p.Backups, p.OldBackups, err = storageFiles(filepath.Join(home, "backups"), func(name string, info os.FileInfo) bool { p.Backups, p.OldBackups, err = storageFiles(ctx, filepath.Join(home, "backups"), func(name string, info os.FileInfo) bool { if !strings.HasPrefix(name, "albedo-before-image-store-") || !strings.HasSuffix(name, ".sqlite") { return false } if info.ModTime().Before(cutoff) {10 unmodified lines if err != nil { return p, err } for _, entry := range entries { if err := ctx.Err(); err != nil { return p, err } if entry.Name() == "kernels" || entry.Name() == "backups" || entry.Name() == "albedo.sqlite" || entry.Name() == "albedo.sqlite-wal" || entry.Name() == "albedo.sqlite-shm" { continue } info, err := entry.Info()9 unmodified lines