okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffaufrefactor(cli): invoke daemon storage helpersf6da61d

main
5.1 KiB188 linesraw
1package storage
2
3import (
4	"albedo/cli/internal/daemon"
5	"bytes"
6	"context"
7	"encoding/json"
8	"errors"
9	"fmt"
10	"os"
11	"os/exec"
12	"path/filepath"
13	"strings"
14	"time"
15)
16
17func (s *Service) command(ctx context.Context, args ...string) (*exec.Cmd, error) {
18	if s.Command == nil {
19		return nil, errors.New("offline storage requires a local daemon executable; set ALBEDO_DAEMON or ALBEDO_ROOT, or query a running daemon without --offline")
20	}
21	return s.Command(ctx, args...)
22}
23
24func (s *Service) sqliteStorage(ctx context.Context, path string) (daemon.StorageDatabase, error) {
25	db := daemon.StorageDatabase{Sessions: []daemon.StorageSession{}}
26	if err := ctx.Err(); err != nil {
27		return db, err
28	}
29	scratch, err := os.MkdirTemp("", "albedo-storage-")
30	if err != nil {
31		return db, fmt.Errorf("create private offline storage snapshot: %w", err)
32	}
33	defer os.RemoveAll(scratch)
34	command, err := s.command(ctx, "storage", "inspect", path, scratch)
35	if err != nil {
36		return db, err
37	}
38	var stderr bytes.Buffer
39	command.Stderr = &stderr
40	out, err := command.Output()
41	if ctx.Err() != nil {
42		return db, ctx.Err()
43	}
44	if err != nil {
45		return db, fmt.Errorf("read disk usage from the database: %w: %s", err, strings.TrimSpace(stderr.String()))
46	}
47	if err := json.Unmarshal(out, &db); err != nil {
48		return db, fmt.Errorf("decode the database disk usage report: %w", err)
49	}
50	return db, nil
51}
52
53func regularSize(path string) (int64, error) {
54	info, err := os.Lstat(path)
55	if errors.Is(err, os.ErrNotExist) {
56		return 0, nil
57	}
58	if err != nil {
59		return 0, err
60	}
61	if !info.Mode().IsRegular() {
62		return 0, fmt.Errorf("expected a file, but found a different kind of entry: %s", path)
63	}
64	return info.Size(), nil
65}
66
67func storageFiles(ctx context.Context, dir string, eligible func(string, os.FileInfo) bool) (int64, []daemon.StorageFile, error) {
68	stat, err := os.Lstat(dir)
69	if errors.Is(err, os.ErrNotExist) {
70		return 0, []daemon.StorageFile{}, nil
71	}
72	if err != nil {
73		return 0, nil, err
74	}
75	if !stat.IsDir() {
76		return 0, nil, fmt.Errorf("expected a directory for storage: %s", dir)
77	}
78	entries, err := os.ReadDir(dir)
79	if err != nil {
80		return 0, nil, err
81	}
82	var total int64
83	candidates := []daemon.StorageFile{}
84	for _, entry := range entries {
85		if err := ctx.Err(); err != nil {
86			return 0, nil, err
87		}
88		info, err := entry.Info()
89		if err != nil {
90			return 0, nil, err
91		}
92		if !info.Mode().IsRegular() {
93			continue
94		}
95		total += info.Size()
96		if eligible(entry.Name(), info) {
97			candidates = append(candidates, daemon.StorageFile{Path: filepath.Join(dir, entry.Name()), Bytes: info.Size()})
98		}
99	}
100	return total, candidates, nil
101}
102
103func (s *Service) OfflineReport(ctx context.Context) (daemon.StorageReport, error) {
104	home, now := s.Home, s.Now()
105	if err := ctx.Err(); err != nil {
106		return daemon.StorageReport{}, err
107	}
108	p := daemon.StorageReport{OldKernels: []daemon.StorageFile{}, OldBackups: []daemon.StorageFile{}, DB: daemon.StorageDatabase{Sessions: []daemon.StorageSession{}}}
109	root, err := os.Lstat(home)
110	if errors.Is(err, os.ErrNotExist) {
111		return p, nil
112	}
113	if err != nil {
114		return p, err
115	}
116	if !root.IsDir() {
117		return p, fmt.Errorf("ALBEDO_HOME must be a directory: %s", home)
118	}
119	dbPath := filepath.Join(home, "albedo.sqlite")
120	databaseInfo, err := os.Lstat(dbPath)
121	databaseExists := err == nil
122	if err != nil && !errors.Is(err, os.ErrNotExist) {
123		return p, err
124	}
125	if databaseExists {
126		if !databaseInfo.Mode().IsRegular() {
127			return p, fmt.Errorf("expected a file, but found a different kind of entry: %s", dbPath)
128		}
129		p.Database = databaseInfo.Size()
130	}
131	for _, suffix := range []string{"-wal", "-shm"} {
132		size, sizeErr := regularSize(dbPath + suffix)
133		if sizeErr != nil {
134			return p, sizeErr
135		}
136		p.WAL += size
137	}
138	if databaseExists {
139		if p.DB, err = s.sqliteStorage(ctx, dbPath); err != nil {
140			return p, err
141		}
142	}
143	known := make(map[string]bool)
144	for _, session := range p.DB.Sessions {
145		known[session.ID] = true
146	}
147	cutoff := now.Add(-30 * 24 * time.Hour)
148	p.Kernels, p.OldKernels, err = storageFiles(ctx, filepath.Join(home, "kernels"), func(name string, info os.FileInfo) bool {
149		return strings.HasSuffix(name, ".state") && !known[strings.TrimSuffix(name, ".state")] && info.ModTime().Before(cutoff)
150	})
151	if err != nil {
152		return p, err
153	}
154	p.Backups, p.OldBackups, err = storageFiles(ctx, filepath.Join(home, "backups"), func(name string, info os.FileInfo) bool {
155		if !strings.HasPrefix(name, "albedo-before-image-store-") || !strings.HasSuffix(name, ".sqlite") {
156			return false
157		}
158		if info.ModTime().Before(cutoff) {
159			return true
160		}
161		p.RecentBackups += info.Size()
162		p.RecentBackupCount++
163		return false
164	})
165	if err != nil {
166		return p, err
167	}
168	entries, err := os.ReadDir(home)
169	if err != nil {
170		return p, err
171	}
172	for _, entry := range entries {
173		if err := ctx.Err(); err != nil {
174			return p, err
175		}
176		if entry.Name() == "kernels" || entry.Name() == "backups" || entry.Name() == "albedo.sqlite" || entry.Name() == "albedo.sqlite-wal" || entry.Name() == "albedo.sqlite-shm" {
177			continue
178		}
179		info, err := entry.Info()
180		if err != nil {
181			return p, err
182		}
183		if info.Mode().IsRegular() {
184			p.Other += info.Size()
185		}
186	}
187	return p, nil
188}