Skip to content

Commit aef1250

Browse files
committed
bundles: write bundle lists for different URIs
The bundle list for a given 'git-bundle-web-server' is intended to be served from the route 'http[s]://<host>:<port>/<org>/<repo>', with or without a trailing slash. The bundles listed are provided as paths relative to '/<org>/<repo>'. However, these relative paths only work when there's a trailing slash after '<repo>' in the URI; otherwise '<repo>' is interpreted as a file in directory '<org>'. For example: - URI: https://localhost:8080/git/git/, bundle relpath: bundle-1.bundle -> https://localhost:8080/git/git/bundle-1.bundle - URI: https://localhost:8080/git/git, bundle relpath: bundle-2.bundle -> https://localhost:8080/git/bundle-2.bundle This behavior is baked into Git, and is consistent with RFC 1808 [1]. We still want to be able to serve the bundle list when no trailing slash is given, though, so write out a second bundle list that prepends '<repo>' to the bundle filename. In a later commit, we will update the web server to serve this file's contents when appropriate, fixing the routing bug. As part of this refactor of 'BundleProvider.WriteBundleList()', switch to using 'FileSystem.WriteLockFileFunc()' to write lock files. Doing so allows us to mock filesystem writes and unit test 'WriteBundleList()', which is done here as well. [1] https://www.rfc-editor.org/rfc/rfc1808#section-4 Signed-off-by: Victoria Dye <[email protected]>
1 parent 7e70af8 commit aef1250

File tree

3 files changed

+384
-47
lines changed

3 files changed

+384
-47
lines changed

internal/bundles/bundles.go

Lines changed: 114 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
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+
2129
type BundleHeader struct {
2230
Version int64
2331

@@ -30,15 +38,21 @@ type BundleHeader struct {
3038
}
3139

3240
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
3549
CreationToken int64
3650
}
3751

3852
func 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

7892
type bundleProvider struct {
79-
logger log.TraceLogger
80-
gitHelper git.GitHelper
93+
logger log.TraceLogger
94+
fileSystem common.FileSystem
95+
gitHelper git.GitHelper
8196
}
8297

8398
func 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.
119136
func (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\tversion = %d\n\tmode = %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\turi = %s\n\tcreationToken = %d\n\n",
145-
token, bundle.URI, token)
164+
out, "[bundle]\n\tversion = %d\n\tmode = %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\turi = %s\n\tcreationToken = %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

184251
func (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

Comments
 (0)