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.
|
4506a4d3
Keenan Nemetz
14d ago
|
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 | } |