vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys
keenan · 1h ago
4b8c9e1d
| 1 | // Package storage measures the instance's data directory and the filesystem it |
| 2 | // lives on. It is deliberately read-only: the status page reports what is there, |
| 3 | // and the sweep decisions stay in forge.GC. |
| 4 | package storage |
| 5 | |
| 6 | import ( |
| 7 | "errors" |
| 8 | "io/fs" |
| 9 | "path/filepath" |
| 10 | "strings" |
| 11 | "syscall" |
| 12 | "time" |
| 13 | ) |
| 14 | |
| 15 | // Category is one slice of the data directory's disk use. Bytes counts allocated |
| 16 | // blocks, not apparent file size, so a sparse file does not claim space it does |
| 17 | // not use. A hard link is charged once, to the first name the walk sees. |
| 18 | type Category struct { |
| 19 | Name string |
| 20 | Bytes int64 |
| 21 | Files int |
| 22 | } |
| 23 | |
| 24 | // Disk is the filesystem behind the data directory. Available is what an |
| 25 | // unprivileged process can still write, which is the number an operator cares |
| 26 | // about; Used is the whole filesystem's usage, not just Vole's. |
| 27 | type Disk struct { |
| 28 | Total int64 |
| 29 | Used int64 |
| 30 | Available int64 |
| 31 | } |
| 32 | |
| 33 | // Stats is one snapshot. DiskErr is set when the operating system cannot report |
| 34 | // filesystem capacity; the directory walk can still succeed on its own. |
| 35 | type Stats struct { |
| 36 | DataDir string |
| 37 | Bytes int64 |
| 38 | Files int |
| 39 | Categories []Category |
| 40 | Disk Disk |
| 41 | DiskErr string |
| 42 | Measured time.Time |
| 43 | } |
| 44 | |
| 45 | // Measure walks root, deduplicates hard links, and asks the filesystem for its |
| 46 | // free space. |
| 47 | func Measure(root string) (Stats, error) { |
| 48 | st := Stats{DataDir: root, Measured: time.Now().UTC()} |
| 49 | if disk, err := diskUsage(root); err != nil { |
| 50 | st.DiskErr = err.Error() |
| 51 | } else { |
| 52 | st.Disk = disk |
| 53 | } |
| 54 | |
| 55 | // Every category is allocated a slot up front so the template can render a |
| 56 | // stable order even when an instance has never used one of them. |
| 57 | order := []string{"Repositories", "LFS objects", "Databases", "Queue", "ACME", "Other"} |
| 58 | cats := make(map[string]*Category, len(order)) |
| 59 | for _, name := range order { |
| 60 | cats[name] = &Category{Name: name} |
| 61 | } |
| 62 | seen := map[fileKey]bool{} |
| 63 | err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { |
| 64 | if err != nil { |
| 65 | if errors.Is(err, fs.ErrNotExist) { |
| 66 | return nil |
| 67 | } |
| 68 | return err |
| 69 | } |
| 70 | if d.IsDir() || !d.Type().IsRegular() { |
| 71 | return nil |
| 72 | } |
| 73 | info, err := d.Info() |
| 74 | if err != nil { |
| 75 | if errors.Is(err, fs.ErrNotExist) { |
| 76 | return nil |
| 77 | } |
| 78 | return err |
| 79 | } |
| 80 | key, linked := fileKeyFor(info) |
| 81 | if linked { |
| 82 | if seen[key] { |
| 83 | return nil |
| 84 | } |
| 85 | seen[key] = true |
| 86 | } |
| 87 | n := info.Size() |
| 88 | if st, ok := info.Sys().(*syscall.Stat_t); ok && st.Blocks > 0 { |
| 89 | // Blocks is in 512-byte units, regardless of the filesystem's block |
| 90 | // size. It is the allocation the file actually costs. |
| 91 | n = st.Blocks * 512 |
| 92 | } |
| 93 | name := category(path, root) |
| 94 | cats[name].Bytes += n |
| 95 | cats[name].Files++ |
| 96 | st.Bytes += n |
| 97 | st.Files++ |
| 98 | return nil |
| 99 | }) |
| 100 | if err != nil { |
| 101 | return st, err |
| 102 | } |
| 103 | for _, name := range order { |
| 104 | st.Categories = append(st.Categories, *cats[name]) |
| 105 | } |
| 106 | return st, nil |
| 107 | } |
| 108 | |
| 109 | type fileKey struct { |
| 110 | dev uint64 |
| 111 | ino uint64 |
| 112 | } |
| 113 | |
| 114 | func fileKeyFor(info fs.FileInfo) (fileKey, bool) { |
| 115 | st, ok := info.Sys().(*syscall.Stat_t) |
| 116 | if !ok { |
| 117 | return fileKey{}, false |
| 118 | } |
| 119 | return fileKey{dev: uint64(st.Dev), ino: st.Ino}, true |
| 120 | } |
| 121 | |
| 122 | func category(path, root string) string { |
| 123 | rel, err := filepath.Rel(root, path) |
| 124 | if err != nil { |
| 125 | return "Other" |
| 126 | } |
| 127 | rel = filepath.ToSlash(rel) |
| 128 | top := rel |
| 129 | if i := strings.IndexByte(rel, '/'); i >= 0 { |
| 130 | top = rel[:i] |
| 131 | } |
| 132 | switch top { |
| 133 | case "repos": |
| 134 | return "Repositories" |
| 135 | case "lfs": |
| 136 | return "LFS objects" |
| 137 | case "queue": |
| 138 | return "Queue" |
| 139 | case "acme": |
| 140 | return "ACME" |
| 141 | } |
| 142 | // SQLite keeps its main file and WAL/SHM siblings at the top of data_dir. |
| 143 | if strings.HasPrefix(top, "users.db") || strings.HasPrefix(top, "index.db") || strings.HasPrefix(top, "runs.db") || |
| 144 | strings.HasSuffix(top, ".db") || strings.HasSuffix(top, ".db-wal") || strings.HasSuffix(top, ".db-shm") { |
| 145 | return "Databases" |
| 146 | } |
| 147 | return "Other" |
| 148 | } |
| 149 | |
| 150 | func diskUsage(root string) (Disk, error) { |
| 151 | var st syscall.Statfs_t |
| 152 | if err := syscall.Statfs(root, &st); err != nil { |
| 153 | return Disk{}, err |
| 154 | } |
| 155 | total := int64(st.Blocks) * st.Bsize |
| 156 | free := int64(st.Bfree) * st.Bsize |
| 157 | available := int64(st.Bavail) * st.Bsize |
| 158 | return Disk{Total: total, Used: total - free, Available: available}, nil |
| 159 | } |
| 160 |