55 "context"
66 "encoding/json"
77 "fmt"
8+ "io"
89 "os"
910 "path"
1011 "path/filepath"
@@ -13,11 +14,18 @@ import (
1314 "strings"
1415 "time"
1516
17+ "github.com/github/git-bundle-server/internal/common"
1618 "github.com/github/git-bundle-server/internal/core"
1719 "github.com/github/git-bundle-server/internal/git"
1820 "github.com/github/git-bundle-server/internal/log"
1921)
2022
23+ const (
24+ BundleListJsonFilename string = "bundle-list.json"
25+ BundleListFilename string = "bundle-list"
26+ RepoBundleListFilename string = "repo-bundle-list"
27+ )
28+
2129type BundleHeader struct {
2230 Version int64
2331
@@ -30,15 +38,21 @@ type BundleHeader struct {
3038}
3139
3240type Bundle struct {
33- URI string
34- Filename string
41+ // The absolute path to the bundle from the root of the bundle web server,
42+ // typically '/org/route/filename'.
43+ URI string
44+
45+ // The absolute path to the bundle on disk
46+ Filename string
47+
48+ // The creation token used in Git's 'creationToken' heuristic
3549 CreationToken int64
3650}
3751
3852func NewBundle (repo * core.Repository , timestamp int64 ) Bundle {
3953 bundleName := fmt .Sprintf ("bundle-%d.bundle" , timestamp )
4054 return Bundle {
41- URI : path .Join ("." , bundleName ),
55+ URI : path .Join ("/" , repo . Route , bundleName ),
4256 Filename : filepath .Join (repo .WebDir , bundleName ),
4357 CreationToken : timestamp ,
4458 }
@@ -76,17 +90,20 @@ type BundleProvider interface {
7690}
7791
7892type bundleProvider struct {
79- logger log.TraceLogger
80- gitHelper git.GitHelper
93+ logger log.TraceLogger
94+ fileSystem common.FileSystem
95+ gitHelper git.GitHelper
8196}
8297
8398func NewBundleProvider (
8499 l log.TraceLogger ,
100+ fs common.FileSystem ,
85101 g git.GitHelper ,
86102) BundleProvider {
87103 return & bundleProvider {
88- logger : l ,
89- gitHelper : g ,
104+ logger : l ,
105+ fileSystem : fs ,
106+ gitHelper : g ,
90107 }
91108}
92109
@@ -115,78 +132,128 @@ func (b *bundleProvider) CreateSingletonList(ctx context.Context, bundle Bundle)
115132 return & list
116133}
117134
118- // Given a BundleList
135+ // Given a BundleList, write the bundle list content to the web directory.
119136func (b * bundleProvider ) WriteBundleList (ctx context.Context , list * BundleList , repo * core.Repository ) error {
120137 //lint:ignore SA4006 always override the ctx with the result from 'Region()'
121138 ctx , exitRegion := b .logger .Region (ctx , "bundles" , "write_bundle_list" )
122139 defer exitRegion ()
123140
124- listFile := repo .WebDir + "/bundle-list"
125- jsonFile := repo .RepoDir + "/bundle-list.json"
126-
127- // TODO: Formalize lockfile concept.
128- f , err := os .OpenFile (listFile + ".lock" , os .O_WRONLY | os .O_CREATE , 0o600 )
129- if err != nil {
130- return fmt .Errorf ("failure to open file: %w" , err )
141+ var listLockFile , repoListLockFile , jsonLockFile common.LockFile
142+ rollbackAll := func () {
143+ if listLockFile != nil {
144+ listLockFile .Rollback ()
145+ }
146+ if repoListLockFile != nil {
147+ repoListLockFile .Rollback ()
148+ }
149+ if jsonLockFile != nil {
150+ jsonLockFile .Rollback ()
151+ }
131152 }
132153
133- out := bufio .NewWriter (f )
134-
135- fmt .Fprintf (
136- out , "[bundle]\n \t version = %d\n \t mode = %s\n \n " ,
137- list .Version , list .Mode )
138-
154+ // Write the bundle list files: one for requests with a trailing slash
155+ // (where the relative bundle paths are '<bundlefile>'), one for requests
156+ // without a trailing slash (where the relative bundle paths are
157+ // '<repo>/<bundlefile>').
139158 keys := list .sortedCreationTokens ()
159+ writeListFile := func (f io.Writer , requestUri string ) error {
160+ out := bufio .NewWriter (f )
161+ defer out .Flush ()
140162
141- for _ , token := range keys {
142- bundle := list .Bundles [token ]
143163 fmt .Fprintf (
144- out , "[bundle \" %d\" ]\n \t uri = %s\n \t creationToken = %d\n \n " ,
145- token , bundle .URI , token )
164+ out , "[bundle]\n \t version = %d\n \t mode = %s\n \n " ,
165+ list .Version , list .Mode )
166+
167+ uriBase := path .Dir (requestUri ) + "/"
168+ for _ , token := range keys {
169+ bundle := list .Bundles [token ]
170+
171+ // Get the URI relative to the bundle server root
172+ uri := strings .TrimPrefix (bundle .URI , uriBase )
173+ if uri == bundle .URI {
174+ panic ("error resolving bundle URI paths" )
175+ }
176+
177+ fmt .Fprintf (
178+ out , "[bundle \" %d\" ]\n \t uri = %s\n \t creationToken = %d\n \n " ,
179+ token , uri , token )
180+ }
181+ return nil
146182 }
147183
148- out .Flush ()
149- err = f .Close ()
184+ listLockFile , err := b .fileSystem .WriteLockFileFunc (
185+ filepath .Join (repo .WebDir , BundleListFilename ),
186+ func (f io.Writer ) error {
187+ return writeListFile (f , path .Join ("/" , repo .Route )+ "/" )
188+ },
189+ )
150190 if err != nil {
151- return fmt .Errorf ("failed to close lock file: %w" , err )
191+ rollbackAll ()
192+ return err
152193 }
153194
154- f , err = os .OpenFile (jsonFile + ".lock" , os .O_WRONLY | os .O_CREATE , 0o600 )
195+ repoListLockFile , err = b .fileSystem .WriteLockFileFunc (
196+ filepath .Join (repo .WebDir , RepoBundleListFilename ),
197+ func (f io.Writer ) error {
198+ return writeListFile (f , path .Join ("/" , repo .Route ))
199+ },
200+ )
155201 if err != nil {
156- return fmt .Errorf ("failed to open JSON file: %w" , err )
202+ rollbackAll ()
203+ return err
157204 }
158205
159- data , jsonErr := json .Marshal (list )
160- if jsonErr != nil {
161- return fmt .Errorf ("failed to convert list to JSON: %w" , err )
206+ // Write the (internal-use) JSON representation of the bundle list
207+ jsonLockFile , err = b .fileSystem .WriteLockFileFunc (
208+ filepath .Join (repo .RepoDir , BundleListJsonFilename ),
209+ func (f io.Writer ) error {
210+ data , err := json .Marshal (list )
211+ if err != nil {
212+ return fmt .Errorf ("failed to convert list to JSON: %w" , err )
213+ }
214+
215+ written := 0
216+ for written < len (data ) {
217+ n , writeErr := f .Write (data [written :])
218+ if writeErr != nil {
219+ return fmt .Errorf ("failed to write JSON: %w" , err )
220+ }
221+ written += n
222+ }
223+
224+ return nil
225+ },
226+ )
227+ if err != nil {
228+ rollbackAll ()
229+ return err
162230 }
163231
164- written := 0
165- for written < len (data ) {
166- n , writeErr := f .Write (data [written :])
167- if writeErr != nil {
168- return fmt .Errorf ("failed to write JSON: %w" , err )
169- }
170- written += n
232+ // Commit all lockfiles
233+ err = jsonLockFile .Commit ()
234+ if err != nil {
235+ return fmt .Errorf ("failed to rename JSON file: %w" , err )
171236 }
172237
173- f .Sync ()
174- f .Close ()
238+ err = listLockFile .Commit ()
239+ if err != nil {
240+ return fmt .Errorf ("failed to rename bundle list file: %w" , err )
241+ }
175242
176- renameErr := os . Rename ( jsonFile + ".lock" , jsonFile )
177- if renameErr != nil {
178- return fmt .Errorf ("failed to rename JSON file: %w" , renameErr )
243+ err = repoListLockFile . Commit ( )
244+ if err != nil {
245+ return fmt .Errorf ("failed to rename repo-level bundle list file: %w" , err )
179246 }
180247
181- return os . Rename ( listFile + ".lock" , listFile )
248+ return nil
182249}
183250
184251func (b * bundleProvider ) GetBundleList (ctx context.Context , repo * core.Repository ) (* BundleList , error ) {
185252 //lint:ignore SA4006 always override the ctx with the result from 'Region()'
186253 ctx , exitRegion := b .logger .Region (ctx , "bundles" , "get_bundle_list" )
187254 defer exitRegion ()
188255
189- jsonFile := repo .RepoDir + "/bundle-list.json"
256+ jsonFile := filepath . Join ( repo .RepoDir , BundleListJsonFilename )
190257
191258 reader , err := os .Open (jsonFile )
192259 if err != nil {
0 commit comments