Skip to content

Commit 27be538

Browse files
committedJan 7, 2025
This is a somewhat decent state of the source code
1 parent d8ccd87 commit 27be538

19 files changed

+414
-334
lines changed
 

‎cmd/bluebell/main.go

+30-10
Original file line numberDiff line numberDiff line change
@@ -88,47 +88,67 @@ func main() {
8888
}
8989
}()
9090

91-
db, err := bluebell.InitDB(config.DB)
91+
db, err := bluebell.InitDB(filepath.Join(wd, config.DB))
9292
if err != nil {
9393
logger.Logger.Fatal("Failed to open database", "err", err)
9494
return
9595
}
9696

9797
// Listen must be called before Ready
98-
ln, err := upg.Listen("tcp", config.Listen)
98+
lnHttp, err := upg.Listen("tcp", config.Listen.Http)
99+
if err != nil {
100+
logger.Logger.Fatal("Listen failed", "err", err)
101+
}
102+
103+
lnApi, err := upg.Listen("tcp", config.Listen.Api)
99104
if err != nil {
100105
logger.Logger.Fatal("Listen failed", "err", err)
101106
}
102107

103108
uploadHandler := upload.New(sitesFs, db)
104-
serveHandler := serve.New(sitesFs, db, config.Domain)
109+
serveHandler := serve.New(sitesFs, db)
105110

106111
router := httprouter.New()
107-
router.POST("/u/:site", uploadHandler.Handle)
108-
router.GET("/*filepath", serveHandler.Handle)
112+
router.POST("/u/:site/:branch", uploadHandler.Handle)
109113
router.POST("/sites/:host", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
110114

111115
})
112116
router.DELETE("/sites/:host", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
113117

114118
})
115119

116-
server := &http.Server{
117-
Handler: router,
120+
serverHttp := &http.Server{
121+
Handler: serveHandler,
118122
ReadTimeout: 1 * time.Minute,
119123
ReadHeaderTimeout: 1 * time.Minute,
120124
WriteTimeout: 1 * time.Minute,
121125
IdleTimeout: 1 * time.Minute,
122126
MaxHeaderBytes: 4 * humanize.MiByte,
123127
}
124-
logger.Logger.Info("HTTP server listening on", "addr", config.Listen)
128+
logger.Logger.Info("HTTP server listening on", "addr", config.Listen.Http)
125129
go func() {
126-
err := server.Serve(ln)
130+
err := serverHttp.Serve(lnHttp)
127131
if !errors.Is(err, http.ErrServerClosed) {
128132
logger.Logger.Fatal("Serve failed", "err", err)
129133
}
130134
}()
131135

136+
serverApi := &http.Server{
137+
Handler: router,
138+
ReadTimeout: 1 * time.Minute,
139+
ReadHeaderTimeout: 1 * time.Minute,
140+
WriteTimeout: 1 * time.Minute,
141+
IdleTimeout: 1 * time.Minute,
142+
MaxHeaderBytes: 4 * humanize.MiByte,
143+
}
144+
logger.Logger.Info("API server listening on", "addr", config.Listen.Api)
145+
go func() {
146+
err := serverApi.Serve(lnApi)
147+
if !errors.Is(err, http.ErrServerClosed) {
148+
logger.Logger.Fatal("API Serve failed", "err", err)
149+
}
150+
}()
151+
132152
logger.Logger.Info("Ready")
133153
if err := upg.Ready(); err != nil {
134154
panic(err)
@@ -140,5 +160,5 @@ func main() {
140160
os.Exit(1)
141161
})
142162

143-
server.Shutdown(context.Background())
163+
serverHttp.Shutdown(context.Background())
144164
}

‎conf/conf.go

+5-20
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,11 @@
11
package conf
22

33
type Conf struct {
4-
Listen string `yaml:"listen"`
5-
DB string `yaml:"db"`
6-
Domain string `yaml:"domain"`
4+
Listen ListenConf `yaml:"listen"`
5+
DB string `yaml:"db"`
76
}
87

9-
func SlugFromDomain(domain string) string {
10-
a := []byte(domain)
11-
for i := range a {
12-
switch {
13-
case a[i] == '-':
14-
// skip
15-
case a[i] >= 'A' && a[i] <= 'Z':
16-
a[i] += 32
17-
case a[i] >= 'a' && a[i] <= 'z':
18-
// skip
19-
case a[i] >= '0' && a[i] <= '9':
20-
// skip
21-
default:
22-
a[i] = '-'
23-
}
24-
}
25-
return string(a)
8+
type ListenConf struct {
9+
Http string `yaml:"http"`
10+
Api string `yaml:"api"`
2611
}

‎conf/conf_test.go

-70
This file was deleted.

‎conf/test-sites.yml

-2
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
CREATE TABLE sites
22
(
3-
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
4-
slug TEXT(8) NOT NULL,
3+
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
54
domain TEXT NOT NULL,
6-
token TEXT NOT NULL,
7-
enable BOOL NOT NULL
5+
token TEXT NOT NULL
6+
);
7+
8+
CREATE TABLE branches
9+
(
10+
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
11+
domain TEXT NOT NULL,
12+
branch TEXT NOT NULL,
13+
last_update DATETIME NOT NULL,
14+
enable BOOLEAN NOT NULL
815
);

‎database/models.go

+12-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎database/queries/sites.sql

+15-12
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
1-
-- name: GetSiteBySlug :one
1+
-- name: GetSiteByDomain :one
22
SELECT *
33
FROM sites
4-
WHERE slug = ?
4+
WHERE domain = ?
55
LIMIT 1;
66

7-
-- name: GetSiteByDomain :one
8-
SELECT *
9-
FROM sites
7+
-- name: GetLastUpdatedByDomainBranch :one
8+
SELECT last_update
9+
FROM branches
1010
WHERE domain = ?
11+
AND branch = ?
12+
AND enable = true
1113
LIMIT 1;
1214

13-
-- name: EnableDomain :exec
14-
INSERT INTO sites (slug, domain, token)
15-
VALUES (?, ?, ?);
15+
-- name: AddSiteDomain :exec
16+
INSERT INTO sites (domain, token)
17+
VALUES (?, ?);
1618

17-
-- name: DeleteDomain :exec
18-
UPDATE sites
19-
SET enable = false
20-
WHERE domain = ?;
19+
-- name: SetDomainBranchEnabled :exec
20+
UPDATE branches
21+
SET enable = ?
22+
WHERE domain = ?
23+
AND branch = ?;

‎database/sites.sql.go

+41-39
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎go.mod

+4-6
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,28 @@ require (
88
github.com/dustin/go-humanize v1.0.1
99
github.com/golang-migrate/migrate/v4 v4.18.1
1010
github.com/julienschmidt/httprouter v1.3.0
11-
github.com/mrmelon54/trie v0.0.3
1211
github.com/spf13/afero v1.11.0
13-
github.com/stretchr/testify v1.9.0
12+
github.com/stretchr/testify v1.10.0
1413
gopkg.in/yaml.v3 v3.0.1
1514
)
1615

1716
require (
18-
filippo.io/edwards25519 v1.1.0 // indirect
1917
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
2018
github.com/charmbracelet/lipgloss v1.0.0 // indirect
2119
github.com/charmbracelet/x/ansi v0.6.0 // indirect
2220
github.com/davecgh/go-spew v1.1.1 // indirect
2321
github.com/go-logfmt/logfmt v0.6.0 // indirect
24-
github.com/go-sql-driver/mysql v1.8.1 // indirect
2522
github.com/hashicorp/errwrap v1.1.0 // indirect
2623
github.com/hashicorp/go-multierror v1.1.1 // indirect
2724
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
2825
github.com/mattn/go-isatty v0.0.20 // indirect
2926
github.com/mattn/go-runewidth v0.0.16 // indirect
27+
github.com/mattn/go-sqlite3 v1.14.24 // indirect
3028
github.com/muesli/termenv v0.15.2 // indirect
3129
github.com/pmezard/go-difflib v1.0.0 // indirect
3230
github.com/rivo/uniseg v0.4.7 // indirect
3331
go.uber.org/atomic v1.11.0 // indirect
34-
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect
35-
golang.org/x/sys v0.28.0 // indirect
32+
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 // indirect
33+
golang.org/x/sys v0.29.0 // indirect
3634
golang.org/x/text v0.21.0 // indirect
3735
)

‎go.sum

+8-54
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
2-
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
3-
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
4-
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
5-
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
6-
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
71
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
82
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
93
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
@@ -16,30 +10,10 @@ github.com/cloudflare/tableflip v1.2.3 h1:8I+B99QnnEWPHOY3fWipwVKxS70LGgUsslG7CS
1610
github.com/cloudflare/tableflip v1.2.3/go.mod h1:P4gRehmV6Z2bY5ao5ml9Pd8u6kuEnlB37pUFMmv7j2E=
1711
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1812
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
19-
github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0=
20-
github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs=
21-
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
22-
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
23-
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
24-
github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
25-
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
26-
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
27-
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
28-
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
2913
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
3014
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
31-
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
32-
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
3315
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
3416
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
35-
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
36-
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
37-
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
38-
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
39-
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
40-
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
41-
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
42-
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
4317
github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y=
4418
github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks=
4519
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -57,47 +31,27 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
5731
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
5832
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
5933
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
60-
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
61-
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
62-
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
63-
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
64-
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
65-
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
66-
github.com/mrmelon54/trie v0.0.3 h1:wZmws84FiGNBZJ00garLyQ2EQhtx0SipVoV7fK8+kZE=
67-
github.com/mrmelon54/trie v0.0.3/go.mod h1:d3hl0YUBSWR3XN4S9BDLkGVzLT4VgwP2mZkBJM6uFpw=
34+
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
35+
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
6836
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
6937
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
70-
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
71-
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
72-
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
73-
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
74-
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
75-
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
7638
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
7739
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7840
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
7941
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
8042
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
8143
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
8244
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
83-
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
84-
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
85-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
86-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
87-
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
88-
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
89-
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
90-
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
91-
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
92-
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
45+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
46+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
9347
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
9448
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
95-
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
96-
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
49+
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588=
50+
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
9751
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
9852
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
99-
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
100-
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
53+
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
54+
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
10155
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
10256
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
10357
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

‎identifier.sqlite

16 KB
Binary file not shown.

‎initdb.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"errors"
77
"github.com/1f349/bluebell/database"
88
"github.com/golang-migrate/migrate/v4"
9-
"github.com/golang-migrate/migrate/v4/database/mysql"
9+
"github.com/golang-migrate/migrate/v4/database/sqlite3"
1010
"github.com/golang-migrate/migrate/v4/source/iofs"
1111
)
1212

@@ -18,15 +18,15 @@ func InitDB(p string) (*database.Queries, error) {
1818
if err != nil {
1919
return nil, err
2020
}
21-
dbOpen, err := sql.Open("mysql", p)
21+
dbOpen, err := sql.Open("sqlite3", p)
2222
if err != nil {
2323
return nil, err
2424
}
25-
dbDrv, err := mysql.WithInstance(dbOpen, &mysql.Config{})
25+
dbDrv, err := sqlite3.WithInstance(dbOpen, &sqlite3.Config{})
2626
if err != nil {
2727
return nil, err
2828
}
29-
mig, err := migrate.NewWithInstance("iofs", migDrv, "mysql", dbDrv)
29+
mig, err := migrate.NewWithInstance("iofs", migDrv, "sqlite3", dbDrv)
3030
if err != nil {
3131
return nil, err
3232
}

‎serve/missing-branch.go.html

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<html lang="en">
2+
<head><title>{{.Host}}</title></head>
3+
<body>
4+
<center>
5+
<h1>{{.Host}}</h1>
6+
<p>The requested beta is not available</p>
7+
<hr>
8+
<a href="/__bluebell-switch-beta?reset">Revert to main website</a>
9+
</center>
10+
</body>
11+
</html>

‎serve/serve.go

+116-34
Original file line numberDiff line numberDiff line change
@@ -2,90 +2,172 @@ package serve
22

33
import (
44
"context"
5-
"github.com/1f349/bluebell/conf"
5+
_ "embed"
66
"github.com/1f349/bluebell/database"
7-
"github.com/julienschmidt/httprouter"
7+
"github.com/1f349/bluebell/logger"
88
"github.com/spf13/afero"
9-
"io"
9+
"html/template"
1010
"net"
1111
"net/http"
1212
"os"
1313
"path"
1414
"path/filepath"
15+
"strconv"
1516
"strings"
17+
"time"
1618
)
1719

1820
var (
19-
indexBranches = []string{
20-
"main",
21-
"master",
22-
}
21+
//go:embed missing-branch.go.html
22+
missingBranchHtml string
23+
missingBranchTemplate = template.Must(template.New("missingBranchHtml").Parse(missingBranchHtml))
24+
2325
indexFiles = []func(p string) string{
24-
func(p string) string { return path.Join(p, "index.html") },
25-
func(p string) string { return p + ".html" },
2626
func(p string) string { return p },
27+
func(p string) string { return p + ".html" },
28+
func(p string) string { return path.Join(p, "index.html") },
2729
}
2830
)
2931

32+
func isInvalidIndexPath(p string) bool {
33+
switch p {
34+
case ".", ".html":
35+
return true
36+
}
37+
return false
38+
}
39+
40+
const (
41+
BetaCookieName = "__bluebell-site-beta"
42+
BetaSwitchPath = "/__bluebell-switch-beta"
43+
BetaExpiry = 24 * time.Hour
44+
45+
NoCacheQuery = "/?__bluebell-no-cache="
46+
)
47+
3048
type sitesQueries interface {
31-
GetSiteByDomain(ctx context.Context, domain string) (database.Site, error)
49+
GetLastUpdatedByDomainBranch(ctx context.Context, params database.GetLastUpdatedByDomainBranchParams) (time.Time, error)
3250
}
3351

34-
func New(storage afero.Fs, db sitesQueries, domain string) *Handler {
35-
return &Handler{storage, db, domain}
52+
func New(storage afero.Fs, db sitesQueries) *Handler {
53+
return &Handler{storage, db}
3654
}
3755

3856
type Handler struct {
3957
storageFs afero.Fs
4058
db sitesQueries
41-
domain string
4259
}
4360

44-
func (h *Handler) Handle(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {
61+
func cacheBuster(rw http.ResponseWriter, req *http.Request) {
62+
header := rw.Header()
63+
header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
64+
header.Set("Pragma", "no-cache")
65+
header.Set("Expires", "0")
66+
http.Redirect(rw, req, NoCacheQuery+strconv.FormatInt(time.Now().Unix(), 16), http.StatusFound)
67+
}
68+
69+
func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
4570
host, _, err := net.SplitHostPort(req.Host)
4671
if err != nil {
47-
http.Error(rw, "Bad Gateway", http.StatusBadGateway)
48-
return
72+
host = req.Host
4973
}
50-
site, ok := strings.CutSuffix(host, "."+h.domain)
51-
if !ok {
52-
http.Error(rw, "Bad Gateway", http.StatusBadGateway)
74+
75+
// detect beta switch path
76+
if req.URL.Path == BetaSwitchPath {
77+
q := req.URL.Query()
78+
79+
// init cookie
80+
baseCookie := &http.Cookie{
81+
Name: BetaCookieName,
82+
Path: "/",
83+
HttpOnly: true,
84+
SameSite: http.SameSiteLaxMode,
85+
}
86+
87+
// reset beta
88+
if q.Has("reset") {
89+
baseCookie.MaxAge = -1
90+
http.SetCookie(rw, baseCookie)
91+
cacheBuster(rw, req)
92+
return
93+
}
94+
95+
// set beta branch
96+
baseCookie.Value = q.Get("branch")
97+
baseCookie.Expires = time.Now().Add(BetaExpiry)
98+
http.SetCookie(rw, baseCookie)
99+
cacheBuster(rw, req)
53100
return
54101
}
55-
site = conf.SlugFromDomain(site)
56-
branch := req.URL.User.Username()
57-
if branch == "" {
58-
for _, i := range indexBranches {
59-
if h.tryServePath(rw, site, i, req.URL.Path) {
60-
return
61-
}
62-
}
63-
} else if h.tryServePath(rw, site, branch, req.URL.Path) {
102+
103+
// read the beta cookie
104+
branchCookie, _ := req.Cookie(BetaCookieName)
105+
var branch = "@"
106+
if branchCookie != nil {
107+
branch += branchCookie.Value
108+
}
109+
110+
updated, err := h.db.GetLastUpdatedByDomainBranch(req.Context(), database.GetLastUpdatedByDomainBranchParams{Domain: host, Branch: branch})
111+
if err != nil {
112+
rw.WriteHeader(http.StatusMisdirectedRequest)
113+
_ = missingBranchTemplate.Execute(rw, struct{ Host string }{host})
114+
logger.Logger.Debug("Branch is not available", "host", host, "branch", branch, "err", err)
64115
return
65116
}
117+
118+
if h.tryServePath(rw, req, host, branch, updated, req.URL.Path) {
119+
return // page has been served
120+
}
121+
122+
// tryServePath found no matching files
66123
http.Error(rw, "404 Not Found", http.StatusNotFound)
124+
logger.Logger.Debug("No matching file was found")
67125
}
68126

69-
func (h *Handler) tryServePath(rw http.ResponseWriter, site, branch, p string) bool {
127+
// tryServePath attempts to find a valid path from the indexFiles list
128+
func (h *Handler) tryServePath(rw http.ResponseWriter, req *http.Request, site, branch string, updated time.Time, p string) bool {
70129
for _, i := range indexFiles {
71-
if h.tryServeFile(rw, site, branch, i(p)) {
130+
// skip invalid paths "." and ".html"
131+
p2 := path.Clean(i(p))
132+
if isInvalidIndexPath(p2) {
133+
continue
134+
}
135+
136+
if h.tryServeFile(rw, req, site, branch, updated, p2) {
72137
return true
73138
}
74139
}
75140
return false
76141
}
77142

78-
func (h *Handler) tryServeFile(rw http.ResponseWriter, site, branch, p string) bool {
143+
// tryServeFile attempts to serve the content of a file if the file can be found
144+
//
145+
// If a matching file can be found or an internal error has occurred then the return value is true to prevent further changes to the response.
146+
//
147+
// If branch == "@" then time based caching is enabled for subsequent page loads. Otherwise, time based caching is disabled to prevent stale beta content from being cached.
148+
func (h *Handler) tryServeFile(rw http.ResponseWriter, req *http.Request, site, branch string, updated time.Time, p string) bool {
79149
// prevent path traversal
80150
if strings.Contains(site, "..") || strings.Contains(branch, "..") || strings.Contains(p, "..") {
81151
http.Error(rw, "400 Bad Request", http.StatusBadRequest)
82152
return true
83153
}
84-
open, err := h.storageFs.Open(filepath.Join(site, branch, p))
154+
155+
servePath := filepath.Join(site, branch, p)
156+
logger.Logger.Debug("Serving file", "full", servePath, "site", site, "branch", branch, "file", p)
157+
open, err := h.storageFs.Open(servePath)
85158
switch {
86159
case err == nil:
87-
rw.WriteHeader(http.StatusOK)
88-
_, _ = io.Copy(rw, open)
160+
// ignore directories
161+
stat, err := open.Stat()
162+
if err != nil || stat.IsDir() {
163+
return false
164+
}
165+
166+
// disable timed cache for non-main branches
167+
if branch != "@" {
168+
updated = time.Time{}
169+
}
170+
http.ServeContent(rw, req, p, updated, open)
89171
case os.IsNotExist(err):
90172
// check next path
91173
return false

‎serve/serve_test.go

+59-19
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,73 @@
11
package serve
22

33
import (
4-
"github.com/1f349/bluebell/conf"
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
"github.com/1f349/bluebell/database"
8+
"github.com/1f349/bluebell/logger"
9+
"github.com/charmbracelet/log"
510
"github.com/spf13/afero"
11+
"github.com/stretchr/testify/assert"
12+
"io"
13+
"net/http"
14+
"net/http/httptest"
15+
"os"
16+
"path/filepath"
617
"testing"
18+
"time"
719
)
820

9-
func makeConfig(f afero.Fs) (*conf.Conf, error) {
10-
c := conf.New(f)
11-
return c, c.Load()
21+
func init() {
22+
logger.Logger.SetLevel(log.DebugLevel)
1223
}
1324

14-
func TestName(t *testing.T) {
15-
f := afero.NewMemMapFs()
16-
h := &Handler{
17-
storageFs: f,
18-
conf: conf.Testable([]conf.SiteConf{
19-
{Domain: "example.com", Token: "abcd1234"},
20-
}),
25+
type fakeServeDB struct {
26+
branch string
27+
}
28+
29+
func (f *fakeServeDB) GetLastUpdatedByDomainBranch(_ context.Context, params database.GetLastUpdatedByDomainBranchParams) (time.Time, error) {
30+
if params.Domain == "example.com" && params.Branch == "@"+f.branch {
31+
return time.Now(), nil
2132
}
22-
h.findSiteBranchSubdomain("example-com-test")
23-
site, branch := h.findSiteBranch("example-com_test")
33+
return time.Time{}, sql.ErrNoRows
2434
}
2535

26-
func TestHandler_Handle(t *testing.T) {
27-
f := afero.NewMemMapFs()
28-
h := &Handler{
29-
storageFs: f,
30-
conf: &conf.Conf{},
36+
func TestHandler_ServeHTTP(t *testing.T) {
37+
for _, branch := range []string{"", "test", "dev"} {
38+
t.Run(branch+" branch", func(t *testing.T) {
39+
serveTest(t, "example.com", branch, "example.com/@"+branch+"/index.html")
40+
serveTest(t, "example.com/hello-world", branch, "example.com/@"+branch+"/hello-world.html")
41+
serveTest(t, "example.com/hello-world", branch, "example.com/@"+branch+"/hello-world/index.html")
42+
serveTest(t, "example.com/hello-world", branch, "example.com/@"+branch+"/hello-world")
43+
})
3144
}
32-
h.Handle()
45+
}
46+
47+
func serveTest(t *testing.T, address string, branch string, name string) {
48+
t.Run(fmt.Sprintf("serveTest \"%s\" (%s) -> \"%s\"", address, branch, name), func(t *testing.T) {
49+
fs := afero.NewMemMapFs()
50+
assert.NoError(t, fs.MkdirAll(filepath.Dir(name), os.ModePerm))
51+
assert.NoError(t, afero.WriteFile(fs, name, []byte("Hello World\n"), 0666))
52+
h := New(fs, &fakeServeDB{branch: branch})
53+
54+
//goland:noinspection HttpUrlsUsage
55+
const httpPrefix = "http://"
56+
req := httptest.NewRequest(http.MethodPost, httpPrefix+address, nil)
57+
if branch != "" {
58+
req.AddCookie(&http.Cookie{
59+
Name: "__bluebell-site-beta",
60+
Value: branch,
61+
})
62+
}
63+
rec := httptest.NewRecorder()
64+
h.ServeHTTP(rec, req)
65+
66+
res := rec.Result()
67+
assert.Equal(t, http.StatusOK, res.StatusCode)
68+
assert.NotNil(t, res.Body)
69+
all, err := io.ReadAll(res.Body)
70+
assert.NoError(t, err)
71+
assert.Equal(t, "Hello World\n", string(all))
72+
})
3373
}

‎sqlc.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
version: "2"
22
sql:
3-
- engine: mysql
3+
- engine: sqlite
44
queries: database/queries
55
schema: database/migrations
66
gen:

‎upload/test-sites.yml

-2
This file was deleted.

‎upload/upload.go

+27-12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"archive/tar"
55
"compress/gzip"
66
"context"
7+
"crypto/subtle"
8+
"errors"
79
"fmt"
810
"github.com/1f349/bluebell/database"
911
"github.com/dustin/go-humanize"
@@ -12,13 +14,17 @@ import (
1214
"io"
1315
"io/fs"
1416
"net/http"
15-
"os"
1617
"path/filepath"
18+
"slices"
1719
"strings"
1820
)
1921

22+
var indexBranches = []string{
23+
"main",
24+
"master",
25+
}
26+
2027
type sitesQueries interface {
21-
GetSiteBySlug(ctx context.Context, slug string) (database.Site, error)
2228
GetSiteByDomain(ctx context.Context, domain string) (database.Site, error)
2329
}
2430

@@ -33,19 +39,19 @@ type Handler struct {
3339
db sitesQueries
3440
}
3541

36-
func (h *Handler) Handle(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {
37-
q := req.URL.Query()
38-
site := q.Get("site")
39-
branch := q.Get("branch")
42+
func (h *Handler) Handle(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
43+
site := params.ByName("site")
44+
branch := params.ByName("branch")
4045

4146
site = strings.ReplaceAll(site, "*", "")
4247

43-
siteConf, err := h.db.GetSiteByDomain(req.Context(), "*"+site)
48+
siteConf, err := h.db.GetSiteByDomain(req.Context(), site)
4449
if err != nil {
4550
http.Error(rw, "", http.StatusNotFound)
4651
return
4752
}
48-
if "Bearer "+siteConf.Token != req.Header.Get("Authorization") {
53+
token, ok := strings.CutPrefix(req.Header.Get("Authorization"), "Bearer ")
54+
if !ok || subtle.ConstantTimeCompare([]byte(token), []byte(siteConf.Token)) == 0 {
4955
http.Error(rw, "403 Forbidden", http.StatusForbidden)
5056
return
5157
}
@@ -58,7 +64,7 @@ func (h *Handler) Handle(rw http.ResponseWriter, req *http.Request, _ httprouter
5864

5965
// if file is bigger than 1GiB
6066
if fileHeader.Size > maxFileSize {
61-
http.Error(rw, "File too big", http.StatusBadRequest)
67+
http.Error(rw, "File too big", http.StatusInsufficientStorage)
6268
return
6369
}
6470

@@ -72,9 +78,18 @@ func (h *Handler) Handle(rw http.ResponseWriter, req *http.Request, _ httprouter
7278
}
7379

7480
func (h *Handler) extractTarGzUpload(fileData io.Reader, site, branch string) error {
75-
siteBranchPath := filepath.Join(site, branch)
76-
err := h.storageFs.Rename(siteBranchPath, siteBranchPath+".old")
77-
if err != nil && !os.IsNotExist(err) {
81+
if slices.Contains(indexBranches, branch) {
82+
branch = ""
83+
}
84+
siteBranchPath := filepath.Join(site, "@"+branch)
85+
86+
err := h.storageFs.RemoveAll(siteBranchPath + ".old")
87+
if err != nil && !errors.Is(err, fs.ErrNotExist) {
88+
return fmt.Errorf("failed to remove old site branch %s: %w", siteBranchPath, err)
89+
}
90+
91+
err = h.storageFs.Rename(siteBranchPath, siteBranchPath+".old")
92+
if err != nil && !errors.Is(err, fs.ErrNotExist) {
7893
return fmt.Errorf("failed to save an old copy of the site: %w", err)
7994
}
8095

‎upload/upload_test.go

+70-41
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package upload
22

33
import (
44
"bytes"
5+
"context"
6+
"database/sql"
57
_ "embed"
6-
"github.com/1f349/bluebell/conf"
8+
"fmt"
9+
"github.com/1f349/bluebell/database"
710
"github.com/julienschmidt/httprouter"
811
"github.com/spf13/afero"
912
"github.com/stretchr/testify/assert"
@@ -17,65 +20,91 @@ import (
1720
var (
1821
//go:embed test-archive.tar.gz
1922
testArchiveTarGz []byte
20-
//go:embed test-sites.yml
21-
testSitesYml []byte
2223
)
2324

24-
func assertUploadedFile(t *testing.T, fs afero.Fs) {
25+
func assertUploadedFile(t *testing.T, fs afero.Fs, branch string) {
26+
switch branch {
27+
case "main", "master":
28+
branch = ""
29+
}
30+
2531
// check uploaded file exists
26-
stat, err := fs.Stat("example.com/main/test.txt")
32+
stat, err := fs.Stat("example.com/@" + branch + "/test.txt")
2733
assert.NoError(t, err)
2834
assert.False(t, stat.IsDir())
2935
assert.Equal(t, int64(13), stat.Size())
3036

3137
// check contents
32-
o, err := fs.Open("example.com/main/test.txt")
38+
o, err := fs.Open("example.com/@" + branch + "/test.txt")
3339
assert.NoError(t, err)
3440
all, err := io.ReadAll(o)
3541
assert.NoError(t, err)
3642
assert.Equal(t, "Hello world!\n", string(all))
3743
}
3844

45+
type fakeUploadDB struct {
46+
}
47+
48+
func (f *fakeUploadDB) GetSiteByDomain(_ context.Context, domain string) (database.Site, error) {
49+
if domain == "example.com" {
50+
return database.Site{
51+
ID: 1,
52+
Domain: "example.com",
53+
Token: "abcd1234",
54+
}, nil
55+
}
56+
return database.Site{}, sql.ErrNoRows
57+
}
58+
3959
func TestHandler_Handle(t *testing.T) {
40-
f := afero.NewMemMapFs()
41-
conf := conf.New(f)
42-
h := &Handler{f, conf}
43-
create, err := f.Create("sites.yml")
44-
assert.NoError(t, err)
45-
_, err = create.Write(testSitesYml)
46-
assert.NoError(t, err)
47-
assert.NoError(t, create.Close())
48-
assert.NoError(t, conf.Load())
60+
fs := afero.NewMemMapFs()
61+
h := New(fs, new(fakeUploadDB))
4962

50-
mpBuf := new(bytes.Buffer)
51-
mp := multipart.NewWriter(mpBuf)
52-
file, err := mp.CreateFormFile("upload", "test-archive.tar.gz")
53-
assert.NoError(t, err)
54-
_, err = file.Write(testArchiveTarGz)
55-
assert.NoError(t, err)
56-
assert.NoError(t, mp.Close())
57-
req, err := http.NewRequest(http.MethodPost, "https://example.com/u?site=example.com&branch=main", mpBuf)
58-
assert.NoError(t, err)
59-
req.Header.Set("Authorization", "Bearer abcd1234")
60-
req.Header.Set("Content-Type", mp.FormDataContentType())
61-
rec := httptest.NewRecorder()
62-
h.Handle(rec, req, httprouter.Params{})
63-
res := rec.Result()
64-
assert.Equal(t, http.StatusAccepted, res.StatusCode)
65-
assert.NotNil(t, res.Body)
66-
all, err := io.ReadAll(res.Body)
67-
assert.NoError(t, err)
68-
assert.Equal(t, "", string(all))
63+
r := httprouter.New()
64+
r.POST("/u/:site/:branch", h.Handle)
65+
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
66+
panic("Not Found")
67+
})
68+
69+
for _, branch := range []string{"main", "test", "dev"} {
70+
t.Run(branch+" branch", func(t *testing.T) {
71+
mpBuf := new(bytes.Buffer)
72+
mp := multipart.NewWriter(mpBuf)
73+
file, err := mp.CreateFormFile("upload", "test-archive.tar.gz")
74+
assert.NoError(t, err)
75+
_, err = file.Write(testArchiveTarGz)
76+
assert.NoError(t, err)
77+
assert.NoError(t, mp.Close())
6978

70-
assertUploadedFile(t, f)
79+
req := httptest.NewRequest(http.MethodPost, "https://example.com/u/example.com/"+branch, mpBuf)
80+
req.Header.Set("Authorization", "Bearer abcd1234")
81+
req.Header.Set("Content-Type", mp.FormDataContentType())
82+
rec := httptest.NewRecorder()
83+
r.ServeHTTP(rec, req)
84+
85+
res := rec.Result()
86+
assert.Equal(t, http.StatusAccepted, res.StatusCode)
87+
assert.NotNil(t, res.Body)
88+
all, err := io.ReadAll(res.Body)
89+
assert.NoError(t, err)
90+
assert.Equal(t, "", string(all))
91+
92+
fmt.Println(fs)
93+
94+
assertUploadedFile(t, fs, branch)
95+
})
96+
}
7197
}
7298

7399
func TestHandler_extractTarGzUpload(t *testing.T) {
74-
fs := afero.NewMemMapFs()
75-
conf := conf.New(fs)
76-
h := &Handler{fs, conf}
77-
buffer := bytes.NewBuffer(testArchiveTarGz)
78-
assert.NoError(t, h.extractTarGzUpload(buffer, "example.com", "main"))
100+
for _, branch := range []string{"main", "test", "dev"} {
101+
t.Run(branch+" branch", func(t *testing.T) {
102+
fs := afero.NewMemMapFs()
103+
h := New(fs, nil)
104+
buffer := bytes.NewBuffer(testArchiveTarGz)
105+
assert.NoError(t, h.extractTarGzUpload(buffer, "example.com", branch))
79106

80-
assertUploadedFile(t, fs)
107+
assertUploadedFile(t, fs, branch)
108+
})
109+
}
81110
}

0 commit comments

Comments
 (0)
Please sign in to comment.