Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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.

Blame · internal/storage/storage.go

Line attribution on main · 159 line(s)

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 }