5
5
"context"
6
6
"encoding/json"
7
7
"fmt"
8
+ "io"
8
9
"os"
9
10
"path"
10
11
"path/filepath"
@@ -13,11 +14,18 @@ import (
13
14
"strings"
14
15
"time"
15
16
17
+ "github.com/github/git-bundle-server/internal/common"
16
18
"github.com/github/git-bundle-server/internal/core"
17
19
"github.com/github/git-bundle-server/internal/git"
18
20
"github.com/github/git-bundle-server/internal/log"
19
21
)
20
22
23
+ const (
24
+ BundleListJsonFilename string = "bundle-list.json"
25
+ BundleListFilename string = "bundle-list"
26
+ RepoBundleListFilename string = "repo-bundle-list"
27
+ )
28
+
21
29
type BundleHeader struct {
22
30
Version int64
23
31
@@ -30,15 +38,21 @@ type BundleHeader struct {
30
38
}
31
39
32
40
type 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
35
49
CreationToken int64
36
50
}
37
51
38
52
func NewBundle (repo * core.Repository , timestamp int64 ) Bundle {
39
53
bundleName := fmt .Sprintf ("bundle-%d.bundle" , timestamp )
40
54
return Bundle {
41
- URI : path .Join ("." , bundleName ),
55
+ URI : path .Join ("/" , repo . Route , bundleName ),
42
56
Filename : filepath .Join (repo .WebDir , bundleName ),
43
57
CreationToken : timestamp ,
44
58
}
@@ -76,17 +90,20 @@ type BundleProvider interface {
76
90
}
77
91
78
92
type bundleProvider struct {
79
- logger log.TraceLogger
80
- gitHelper git.GitHelper
93
+ logger log.TraceLogger
94
+ fileSystem common.FileSystem
95
+ gitHelper git.GitHelper
81
96
}
82
97
83
98
func NewBundleProvider (
84
99
l log.TraceLogger ,
100
+ fs common.FileSystem ,
85
101
g git.GitHelper ,
86
102
) BundleProvider {
87
103
return & bundleProvider {
88
- logger : l ,
89
- gitHelper : g ,
104
+ logger : l ,
105
+ fileSystem : fs ,
106
+ gitHelper : g ,
90
107
}
91
108
}
92
109
@@ -115,78 +132,128 @@ func (b *bundleProvider) CreateSingletonList(ctx context.Context, bundle Bundle)
115
132
return & list
116
133
}
117
134
118
- // Given a BundleList
135
+ // Given a BundleList, write the bundle list content to the web directory.
119
136
func (b * bundleProvider ) WriteBundleList (ctx context.Context , list * BundleList , repo * core.Repository ) error {
120
137
//lint:ignore SA4006 always override the ctx with the result from 'Region()'
121
138
ctx , exitRegion := b .logger .Region (ctx , "bundles" , "write_bundle_list" )
122
139
defer exitRegion ()
123
140
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
+ }
131
152
}
132
153
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>').
139
158
keys := list .sortedCreationTokens ()
159
+ writeListFile := func (f io.Writer , requestUri string ) error {
160
+ out := bufio .NewWriter (f )
161
+ defer out .Flush ()
140
162
141
- for _ , token := range keys {
142
- bundle := list .Bundles [token ]
143
163
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
146
182
}
147
183
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
+ )
150
190
if err != nil {
151
- return fmt .Errorf ("failed to close lock file: %w" , err )
191
+ rollbackAll ()
192
+ return err
152
193
}
153
194
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
+ )
155
201
if err != nil {
156
- return fmt .Errorf ("failed to open JSON file: %w" , err )
202
+ rollbackAll ()
203
+ return err
157
204
}
158
205
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
162
230
}
163
231
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 )
171
236
}
172
237
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
+ }
175
242
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 )
179
246
}
180
247
181
- return os . Rename ( listFile + ".lock" , listFile )
248
+ return nil
182
249
}
183
250
184
251
func (b * bundleProvider ) GetBundleList (ctx context.Context , repo * core.Repository ) (* BundleList , error ) {
185
252
//lint:ignore SA4006 always override the ctx with the result from 'Region()'
186
253
ctx , exitRegion := b .logger .Region (ctx , "bundles" , "get_bundle_list" )
187
254
defer exitRegion ()
188
255
189
- jsonFile := repo .RepoDir + "/bundle-list.json"
256
+ jsonFile := filepath . Join ( repo .RepoDir , BundleListJsonFilename )
190
257
191
258
reader , err := os .Open (jsonFile )
192
259
if err != nil {
0 commit comments