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/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Phase 5: LFS objects, over SSH for writes and the proxy for reads

64ac5071a173e4e9ce8bc8d7170f9b0411bee52a · Keenan Nemetz · 18d ago · parent 35eaeccf

Phase 5: LFS objects, over SSH for writes and the proxy for reads

Large files leave the git object database and land in one content-addressed
store per instance. Reads are anonymous like clone; writes ride the SSH key
that already authenticates the push, so no token exists anywhere.

- internal/lfs: sha256-addressed store that verifies content and size before
  an object becomes visible; parser for the pointer format a client writes
- HTTP: POST .../info/lfs/objects/batch for downloads, GET .../objects/<oid>
  with the object id as ETag; upload over HTTP is refused and names SSH
- SSH: git-lfs-transfer speaking the pure SSH protocol (pkt-line), so
  `git lfs push` needs nothing but the key at the handshake
- pre-receive: a pointer whose object is missing, or whose size disagrees
  with the stored object, fails the push that introduced it; a binary over
  lfs_warn_bytes is accepted with a `git lfs track` suggestion. Checks run
  before the protected-branch gate, because an admin's approval bypass is
  about review, not about whether the commit can be checked out
- runs: a finished log becomes a content-addressed object in the same write
  that records the status, served at /r/<repo>/runs/<id>/log
- vole gc [--dry-run] [--grace]: marks pointer targets from every ref in
  every repository plus archived logs, sweeps the rest. Mark-and-sweep
  rather than reference counts: a counter that is one too low deletes an
  artifact, a walk over the refs cannot be wrong about what is reachable

internal/gc/gc.go +123

@@ -0,0 +1,123 @@
1 // Package gc sweeps the LFS store.
2 //
3 // The rule the sweep must never break: an object reachable from a live ref is
4 // kept. That is why nothing here trusts a counter — counters drift after a crash,
5 // a failed upload or an interrupted push, and a counter that is one too low
6 // deletes a release artifact. Instead the sweep marks what it can prove is
7 // reachable, and deletes only what remains.
8 //
9 // Two kinds of root:
10 //
11 // - Pointer files in any tree reachable from any ref, in any repository. A
12 // pointer names one object by sha256; that is the whole reference.
13 // - Archived CI logs recorded in the run queue. No git ref reaches them, so a
14 // sweep that only marked git would delete every finished log.
15 //
16 // Anything unmarked and younger than the grace period is kept: an upload that
17 // lands while the sweep is marking is not reachable yet, but is not garbage
18 // either.
19 package gc
20
21 import (
22 "fmt"
23 "time"
24
25 "github.com/rodent-software/vole/internal/gitx"
26 "github.com/rodent-software/vole/internal/lfs"
27 )
28
29 type Options struct {
30 // Repos are bare repository directories.
31 Repos []string
32 Store *lfs.Store
33 // LogOIDs are objects referenced by run records.
34 LogOIDs []string
35 // Grace protects recent objects that a concurrent upload has not made
36 // reachable yet.
37 Grace time.Duration
38 DryRun bool
39 Now time.Time
40 }
41
42 type Result struct {
43 Marked int
44 Swept int
45 SweptB int64
46 Within int // unmarked but inside the grace period
47 Scanned int
48 Left int64
49 Problems []string
50 }
51
52 // Run marks, then sweeps. A repository that cannot be scanned aborts the sweep:
53 // an unreadable ref means "I do not know what is reachable", and guessing costs
54 // somebody's data.
55 func Run(opts Options) (Result, error) {
56 var res Result
57 if opts.Store == nil {
58 return res, fmt.Errorf("gc: no store")
59 }
60 if opts.Now.IsZero() {
61 opts.Now = time.Now()
62 }
63 marked := map[string]bool{}
64 for _, oid := range opts.LogOIDs {
65 marked[oid] = true
66 }
67
68 for _, dir := range opts.Repos {
69 g := gitx.Open(dir)
70 blobs, err := g.AllBlobs()
71 if err != nil {
72 return res, fmt.Errorf("gc: scan %s: %w", dir, err)
73 }
74 for sha, size := range blobs {
75 if size > lfs.MaxPointerBytes {
76 continue // too big to be a pointer
77 }
78 head, err := g.BlobPrefix(sha, int(lfs.MaxPointerBytes))
79 if err != nil {
80 return res, fmt.Errorf("gc: read %s in %s: %w", sha, dir, err)
81 }
82 p, ok := lfs.ParsePointer(head)
83 if !ok {
84 continue
85 }
86 marked[p.OID] = true
87 }
88 }
89 res.Marked = len(marked)
90
91 err := opts.Store.Walk(func(oid string, size int64) error {
92 res.Scanned++
93 res.Left += size
94 if marked[oid] {
95 return nil
96 }
97 age, err := opts.Store.StoredAt(oid)
98 if err != nil {
99 res.Problems = append(res.Problems, fmt.Sprintf("%s: %v", oid, err))
100 return nil
101 }
102 if opts.Now.Sub(age) < opts.Grace {
103 res.Within++
104 return nil
105 }
106 if opts.DryRun {
107 res.Swept++
108 res.SweptB += size
109 return nil
110 }
111 if err := opts.Store.Remove(oid); err != nil {
112 res.Problems = append(res.Problems, fmt.Sprintf("%s: %v", oid, err))
113 return nil
114 }
115 res.Swept++
116 res.SweptB += size
117 return nil
118 })
119 if err != nil {
120 return res, fmt.Errorf("gc: sweep: %w", err)
121 }
122 return res, nil
123 }