From 27be538a06681a341685980d91cc6e983555c4f7 Mon Sep 17 00:00:00 2001 From: MrMelon54 Date: Sun, 5 Jan 2025 18:41:38 +0000 Subject: [PATCH] This is a somewhat decent state of the source code --- cmd/bluebell/main.go | 40 +++-- conf/conf.go | 25 +-- conf/conf_test.go | 70 -------- conf/test-sites.yml | 2 - .../migrations/20240810115057_init.up.sql | 15 +- database/models.go | 16 +- database/queries/sites.sql | 29 ++-- database/sites.sql.go | 86 +++++----- go.mod | 10 +- go.sum | 62 +------ identifier.sqlite | Bin 0 -> 16384 bytes initdb.go | 8 +- serve/missing-branch.go.html | 11 ++ serve/serve.go | 154 ++++++++++++++---- serve/serve_test.go | 80 ++++++--- sqlc.yaml | 2 +- upload/test-sites.yml | 2 - upload/upload.go | 39 +++-- upload/upload_test.go | 111 ++++++++----- 19 files changed, 421 insertions(+), 341 deletions(-) delete mode 100644 conf/conf_test.go delete mode 100644 conf/test-sites.yml create mode 100644 identifier.sqlite create mode 100644 serve/missing-branch.go.html delete mode 100644 upload/test-sites.yml diff --git a/cmd/bluebell/main.go b/cmd/bluebell/main.go index 74ccdb6..8fa68cd 100644 --- a/cmd/bluebell/main.go +++ b/cmd/bluebell/main.go @@ -88,24 +88,28 @@ func main() { } }() - db, err := bluebell.InitDB(config.DB) + db, err := bluebell.InitDB(filepath.Join(wd, config.DB)) if err != nil { logger.Logger.Fatal("Failed to open database", "err", err) return } // Listen must be called before Ready - ln, err := upg.Listen("tcp", config.Listen) + lnHttp, err := upg.Listen("tcp", config.Listen.Http) + if err != nil { + logger.Logger.Fatal("Listen failed", "err", err) + } + + lnApi, err := upg.Listen("tcp", config.Listen.Api) if err != nil { logger.Logger.Fatal("Listen failed", "err", err) } uploadHandler := upload.New(sitesFs, db) - serveHandler := serve.New(sitesFs, db, config.Domain) + serveHandler := serve.New(sitesFs, db) router := httprouter.New() - router.POST("/u/:site", uploadHandler.Handle) - router.GET("/*filepath", serveHandler.Handle) + router.POST("/u/:site/:branch", uploadHandler.Handle) router.POST("/sites/:host", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) { }) @@ -113,7 +117,23 @@ func main() { }) - server := &http.Server{ + serverHttp := &http.Server{ + Handler: serveHandler, + ReadTimeout: 1 * time.Minute, + ReadHeaderTimeout: 1 * time.Minute, + WriteTimeout: 1 * time.Minute, + IdleTimeout: 1 * time.Minute, + MaxHeaderBytes: 4 * humanize.MiByte, + } + logger.Logger.Info("HTTP server listening on", "addr", config.Listen.Http) + go func() { + err := serverHttp.Serve(lnHttp) + if !errors.Is(err, http.ErrServerClosed) { + logger.Logger.Fatal("Serve failed", "err", err) + } + }() + + serverApi := &http.Server{ Handler: router, ReadTimeout: 1 * time.Minute, ReadHeaderTimeout: 1 * time.Minute, @@ -121,11 +141,11 @@ func main() { IdleTimeout: 1 * time.Minute, MaxHeaderBytes: 4 * humanize.MiByte, } - logger.Logger.Info("HTTP server listening on", "addr", config.Listen) + logger.Logger.Info("API server listening on", "addr", config.Listen.Api) go func() { - err := server.Serve(ln) + err := serverApi.Serve(lnApi) if !errors.Is(err, http.ErrServerClosed) { - logger.Logger.Fatal("Serve failed", "err", err) + logger.Logger.Fatal("API Serve failed", "err", err) } }() @@ -140,5 +160,5 @@ func main() { os.Exit(1) }) - server.Shutdown(context.Background()) + serverHttp.Shutdown(context.Background()) } diff --git a/conf/conf.go b/conf/conf.go index d3314e2..a4b4ddf 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -1,26 +1,11 @@ package conf type Conf struct { - Listen string `yaml:"listen"` - DB string `yaml:"db"` - Domain string `yaml:"domain"` + Listen ListenConf `yaml:"listen"` + DB string `yaml:"db"` } -func SlugFromDomain(domain string) string { - a := []byte(domain) - for i := range a { - switch { - case a[i] == '-': - // skip - case a[i] >= 'A' && a[i] <= 'Z': - a[i] += 32 - case a[i] >= 'a' && a[i] <= 'z': - // skip - case a[i] >= '0' && a[i] <= '9': - // skip - default: - a[i] = '-' - } - } - return string(a) +type ListenConf struct { + Http string `yaml:"http"` + Api string `yaml:"api"` } diff --git a/conf/conf_test.go b/conf/conf_test.go deleted file mode 100644 index 7e4a75e..0000000 --- a/conf/conf_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package conf - -import ( - _ "embed" - "github.com/mrmelon54/trie" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" - "sync" - "testing" -) - -//go:embed test-sites.yml -var testSitesYml []byte - -func TestConfig_Load(t *testing.T) { - f := afero.NewMemMapFs() - create, err := f.Create("sites.yml") - assert.NoError(t, err) - _, err = create.Write(testSitesYml) - assert.NoError(t, err) - assert.NoError(t, create.Close()) - - c := New(f) - assert.NoError(t, c.Load()) - val, ok := c.m.GetByString("example-com") - assert.True(t, ok) - assert.Equal(t, SiteConf{Domain: "example.com", Token: "abcd1234"}, *val) -} - -func TestConfig_loadSlice(t *testing.T) { - c := &Conf{l: new(sync.RWMutex)} - c.loadSlice([]SiteConf{ - {Domain: "example.com", Token: "abcd1234"}, - }) - a, ok := c.m.GetByString("example-com") - assert.True(t, ok) - assert.Equal(t, SiteConf{Domain: "example.com", Token: "abcd1234"}, *a) -} - -func TestConfig_slugFromDomain(t *testing.T) { - c := &Conf{} - assert.Equal(t, "---------------", c.slugFromDomain("!\"#$%&'()*+,-./")) - assert.Equal(t, "0123456789", c.slugFromDomain("0123456789")) - assert.Equal(t, "-------", c.slugFromDomain(":;<=>?@")) - assert.Equal(t, "abcdefghijklmnopqrstuvwxyz", c.slugFromDomain("ABCDEFGHIJKLMNOPQRSTUVWXYZ")) - assert.Equal(t, "------", c.slugFromDomain("[\\]^_`")) - assert.Equal(t, "abcdefghijklmnopqrstuvwxyz", c.slugFromDomain("abcdefghijklmnopqrstuvwxyz")) - assert.Equal(t, "----", c.slugFromDomain("{|}~")) -} - -func FuzzConfig_slugFromDomain(f *testing.F) { - c := &Conf{} - f.Fuzz(func(t *testing.T, a string) { - b := c.slugFromDomain(a) - if len(a) != len(b) { - t.Fatalf("value '%s' (%d) did not match lengths with the output '%s' (%d)", a, len(a), b, len(b)) - } - }) -} - -func TestConfig_Get(t *testing.T) { - c := &Conf{l: new(sync.RWMutex), m: &trie.Trie[SiteConf]{}} - c.loadSlice([]SiteConf{ - {Domain: "example.com", Token: "abcd1234"}, - }) - val, n, ok := c.Get("example.com") - assert.True(t, ok) - assert.Equal(t, 11, n) - assert.Equal(t, SiteConf{Domain: "example.com", Token: "abcd1234"}, *val) -} diff --git a/conf/test-sites.yml b/conf/test-sites.yml deleted file mode 100644 index 496b918..0000000 --- a/conf/test-sites.yml +++ /dev/null @@ -1,2 +0,0 @@ -- domain: example.com - token: abcd1234 diff --git a/database/migrations/20240810115057_init.up.sql b/database/migrations/20240810115057_init.up.sql index 990fd13..1ae8865 100644 --- a/database/migrations/20240810115057_init.up.sql +++ b/database/migrations/20240810115057_init.up.sql @@ -1,8 +1,15 @@ CREATE TABLE sites ( - id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, - slug TEXT(8) NOT NULL, + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, domain TEXT NOT NULL, - token TEXT NOT NULL, - enable BOOL NOT NULL + token TEXT NOT NULL +); + +CREATE TABLE branches +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + domain TEXT NOT NULL, + branch TEXT NOT NULL, + last_update DATETIME NOT NULL, + enable BOOLEAN NOT NULL ); diff --git a/database/models.go b/database/models.go index 06cc095..d042aa2 100644 --- a/database/models.go +++ b/database/models.go @@ -4,12 +4,20 @@ package database -import () +import ( + "time" +) + +type Branch struct { + ID int64 `json:"id"` + Domain string `json:"domain"` + Branch string `json:"branch"` + LastUpdate time.Time `json:"last_update"` + Enable bool `json:"enable"` +} type Site struct { - ID int32 `json:"id"` - Slug string `json:"slug"` + ID int64 `json:"id"` Domain string `json:"domain"` Token string `json:"token"` - Enable bool `json:"enable"` } diff --git a/database/queries/sites.sql b/database/queries/sites.sql index 9e06154..edd85f5 100644 --- a/database/queries/sites.sql +++ b/database/queries/sites.sql @@ -1,20 +1,23 @@ --- name: GetSiteBySlug :one -SELECT * -FROM sites -WHERE slug = ? -LIMIT 1; - -- name: GetSiteByDomain :one SELECT * FROM sites WHERE domain = ? LIMIT 1; --- name: EnableDomain :exec -INSERT INTO sites (slug, domain, token) -VALUES (?, ?, ?); +-- name: GetLastUpdatedByDomainBranch :one +SELECT last_update +FROM branches +WHERE domain = ? + AND branch = ? + AND enable = true +LIMIT 1; --- name: DeleteDomain :exec -UPDATE sites -SET enable = false -WHERE domain = ?; +-- name: AddSiteDomain :exec +INSERT INTO sites (domain, token) +VALUES (?, ?); + +-- name: SetDomainBranchEnabled :exec +UPDATE branches +SET enable = ? +WHERE domain = ? + AND branch = ?; diff --git a/database/sites.sql.go b/database/sites.sql.go index 1fc97aa..961027e 100644 --- a/database/sites.sql.go +++ b/database/sites.sql.go @@ -7,37 +7,47 @@ package database import ( "context" + "time" ) -const deleteDomain = `-- name: DeleteDomain :exec -UPDATE sites -SET enable = false -WHERE domain = ? +const addSiteDomain = `-- name: AddSiteDomain :exec +INSERT INTO sites (domain, token) +VALUES (?, ?) ` -func (q *Queries) DeleteDomain(ctx context.Context, domain string) error { - _, err := q.db.ExecContext(ctx, deleteDomain, domain) - return err -} - -const enableDomain = `-- name: EnableDomain :exec -INSERT INTO sites (slug, domain, token) -VALUES (?, ?, ?) -` - -type EnableDomainParams struct { - Slug string `json:"slug"` +type AddSiteDomainParams struct { Domain string `json:"domain"` Token string `json:"token"` } -func (q *Queries) EnableDomain(ctx context.Context, arg EnableDomainParams) error { - _, err := q.db.ExecContext(ctx, enableDomain, arg.Slug, arg.Domain, arg.Token) +func (q *Queries) AddSiteDomain(ctx context.Context, arg AddSiteDomainParams) error { + _, err := q.db.ExecContext(ctx, addSiteDomain, arg.Domain, arg.Token) return err } +const getLastUpdatedByDomainBranch = `-- name: GetLastUpdatedByDomainBranch :one +SELECT last_update +FROM branches +WHERE domain = ? + AND branch = ? + AND enable = true +LIMIT 1 +` + +type GetLastUpdatedByDomainBranchParams struct { + Domain string `json:"domain"` + Branch string `json:"branch"` +} + +func (q *Queries) GetLastUpdatedByDomainBranch(ctx context.Context, arg GetLastUpdatedByDomainBranchParams) (time.Time, error) { + row := q.db.QueryRowContext(ctx, getLastUpdatedByDomainBranch, arg.Domain, arg.Branch) + var last_update time.Time + err := row.Scan(&last_update) + return last_update, err +} + const getSiteByDomain = `-- name: GetSiteByDomain :one -SELECT id, slug, domain, token, enable +SELECT id, domain, token FROM sites WHERE domain = ? LIMIT 1 @@ -46,32 +56,24 @@ LIMIT 1 func (q *Queries) GetSiteByDomain(ctx context.Context, domain string) (Site, error) { row := q.db.QueryRowContext(ctx, getSiteByDomain, domain) var i Site - err := row.Scan( - &i.ID, - &i.Slug, - &i.Domain, - &i.Token, - &i.Enable, - ) + err := row.Scan(&i.ID, &i.Domain, &i.Token) return i, err } -const getSiteBySlug = `-- name: GetSiteBySlug :one -SELECT id, slug, domain, token, enable -FROM sites -WHERE slug = ? -LIMIT 1 +const setDomainBranchEnabled = `-- name: SetDomainBranchEnabled :exec +UPDATE branches +SET enable = ? +WHERE domain = ? + AND branch = ? ` -func (q *Queries) GetSiteBySlug(ctx context.Context, slug string) (Site, error) { - row := q.db.QueryRowContext(ctx, getSiteBySlug, slug) - var i Site - err := row.Scan( - &i.ID, - &i.Slug, - &i.Domain, - &i.Token, - &i.Enable, - ) - return i, err +type SetDomainBranchEnabledParams struct { + Enable bool `json:"enable"` + Domain string `json:"domain"` + Branch string `json:"branch"` +} + +func (q *Queries) SetDomainBranchEnabled(ctx context.Context, arg SetDomainBranchEnabledParams) error { + _, err := q.db.ExecContext(ctx, setDomainBranchEnabled, arg.Enable, arg.Domain, arg.Branch) + return err } diff --git a/go.mod b/go.mod index d6a858e..6d9299e 100644 --- a/go.mod +++ b/go.mod @@ -8,30 +8,28 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/golang-migrate/migrate/v4 v4.18.1 github.com/julienschmidt/httprouter v1.3.0 - github.com/mrmelon54/trie v0.0.3 github.com/spf13/afero v1.11.0 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - filippo.io/edwards25519 v1.1.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/lipgloss v1.0.0 // indirect github.com/charmbracelet/x/ansi v0.6.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect - github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-sqlite3 v1.14.24 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect ) diff --git a/go.sum b/go.sum index ae0843a..8127b46 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,3 @@ -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 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 github.com/cloudflare/tableflip v1.2.3/go.mod h1:P4gRehmV6Z2bY5ao5ml9Pd8u6kuEnlB37pUFMmv7j2E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0= -github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= -github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= -github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -57,22 +31,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/mrmelon54/trie v0.0.3 h1:wZmws84FiGNBZJ00garLyQ2EQhtx0SipVoV7fK8+kZE= -github.com/mrmelon54/trie v0.0.3/go.mod h1:d3hl0YUBSWR3XN4S9BDLkGVzLT4VgwP2mZkBJM6uFpw= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -80,24 +42,16 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= -golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588= +golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/identifier.sqlite b/identifier.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..de9dac1a2f87ae8f55ee9b00ed2438b9c00c4324 GIT binary patch literal 16384 zcmeI#F;Buk6bJBYA(Avf985Zqyb%I%adffh#Wa={>ygMr3LKFLMQCwHC%?5<@K8Ds zM??OXwAc3Tz4rZJd5;g?B$YItFK1Com+Xug2D>I=jG5{vszZF$%uBt^##AEFlm-C^KmY;|fB*y_009U<00I#B7C`@xzXAjx009U<00Izz00bZa0SG|g_yxWI DcrB0w literal 0 HcmV?d00001 diff --git a/initdb.go b/initdb.go index 0d7d96e..b80f6d0 100644 --- a/initdb.go +++ b/initdb.go @@ -6,7 +6,7 @@ import ( "errors" "github.com/1f349/bluebell/database" "github.com/golang-migrate/migrate/v4" - "github.com/golang-migrate/migrate/v4/database/mysql" + "github.com/golang-migrate/migrate/v4/database/sqlite3" "github.com/golang-migrate/migrate/v4/source/iofs" ) @@ -18,15 +18,15 @@ func InitDB(p string) (*database.Queries, error) { if err != nil { return nil, err } - dbOpen, err := sql.Open("mysql", p) + dbOpen, err := sql.Open("sqlite3", p) if err != nil { return nil, err } - dbDrv, err := mysql.WithInstance(dbOpen, &mysql.Config{}) + dbDrv, err := sqlite3.WithInstance(dbOpen, &sqlite3.Config{}) if err != nil { return nil, err } - mig, err := migrate.NewWithInstance("iofs", migDrv, "mysql", dbDrv) + mig, err := migrate.NewWithInstance("iofs", migDrv, "sqlite3", dbDrv) if err != nil { return nil, err } diff --git a/serve/missing-branch.go.html b/serve/missing-branch.go.html new file mode 100644 index 0000000..4e9be3f --- /dev/null +++ b/serve/missing-branch.go.html @@ -0,0 +1,11 @@ + +{{.Host}} + +
+

{{.Host}}

+

The requested beta is not available

+
+ Revert to main website +
+ + diff --git a/serve/serve.go b/serve/serve.go index d16d843..fabdd96 100644 --- a/serve/serve.go +++ b/serve/serve.go @@ -2,90 +2,172 @@ package serve import ( "context" - "github.com/1f349/bluebell/conf" + _ "embed" "github.com/1f349/bluebell/database" - "github.com/julienschmidt/httprouter" + "github.com/1f349/bluebell/logger" "github.com/spf13/afero" - "io" + "html/template" "net" "net/http" "os" "path" "path/filepath" + "strconv" "strings" + "time" ) var ( - indexBranches = []string{ - "main", - "master", - } + //go:embed missing-branch.go.html + missingBranchHtml string + missingBranchTemplate = template.Must(template.New("missingBranchHtml").Parse(missingBranchHtml)) + indexFiles = []func(p string) string{ - func(p string) string { return path.Join(p, "index.html") }, - func(p string) string { return p + ".html" }, func(p string) string { return p }, + func(p string) string { return p + ".html" }, + func(p string) string { return path.Join(p, "index.html") }, } ) -type sitesQueries interface { - GetSiteByDomain(ctx context.Context, domain string) (database.Site, error) +func isInvalidIndexPath(p string) bool { + switch p { + case ".", ".html": + return true + } + return false } -func New(storage afero.Fs, db sitesQueries, domain string) *Handler { - return &Handler{storage, db, domain} +const ( + BetaCookieName = "__bluebell-site-beta" + BetaSwitchPath = "/__bluebell-switch-beta" + BetaExpiry = 24 * time.Hour + + NoCacheQuery = "/?__bluebell-no-cache=" +) + +type sitesQueries interface { + GetLastUpdatedByDomainBranch(ctx context.Context, params database.GetLastUpdatedByDomainBranchParams) (time.Time, error) +} + +func New(storage afero.Fs, db sitesQueries) *Handler { + return &Handler{storage, db} } type Handler struct { storageFs afero.Fs db sitesQueries - domain string } -func (h *Handler) Handle(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) { +func cacheBuster(rw http.ResponseWriter, req *http.Request) { + header := rw.Header() + header.Set("Cache-Control", "no-cache, no-store, must-revalidate") + header.Set("Pragma", "no-cache") + header.Set("Expires", "0") + http.Redirect(rw, req, NoCacheQuery+strconv.FormatInt(time.Now().Unix(), 16), http.StatusFound) +} + +func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { host, _, err := net.SplitHostPort(req.Host) if err != nil { - http.Error(rw, "Bad Gateway", http.StatusBadGateway) - return + host = req.Host } - site, ok := strings.CutSuffix(host, "."+h.domain) - if !ok { - http.Error(rw, "Bad Gateway", http.StatusBadGateway) - return - } - site = conf.SlugFromDomain(site) - branch := req.URL.User.Username() - if branch == "" { - for _, i := range indexBranches { - if h.tryServePath(rw, site, i, req.URL.Path) { - return - } + + // detect beta switch path + if req.URL.Path == BetaSwitchPath { + q := req.URL.Query() + + // init cookie + baseCookie := &http.Cookie{ + Name: BetaCookieName, + Path: "/", + HttpOnly: true, + SameSite: http.SameSiteLaxMode, } - } else if h.tryServePath(rw, site, branch, req.URL.Path) { + + // reset beta + if q.Has("reset") { + baseCookie.MaxAge = -1 + http.SetCookie(rw, baseCookie) + cacheBuster(rw, req) + return + } + + // set beta branch + baseCookie.Value = q.Get("branch") + baseCookie.Expires = time.Now().Add(BetaExpiry) + http.SetCookie(rw, baseCookie) + cacheBuster(rw, req) return } + + // read the beta cookie + branchCookie, _ := req.Cookie(BetaCookieName) + var branch = "@" + if branchCookie != nil { + branch += branchCookie.Value + } + + updated, err := h.db.GetLastUpdatedByDomainBranch(req.Context(), database.GetLastUpdatedByDomainBranchParams{Domain: host, Branch: branch}) + if err != nil { + rw.WriteHeader(http.StatusMisdirectedRequest) + _ = missingBranchTemplate.Execute(rw, struct{ Host string }{host}) + logger.Logger.Debug("Branch is not available", "host", host, "branch", branch, "err", err) + return + } + + if h.tryServePath(rw, req, host, branch, updated, req.URL.Path) { + return // page has been served + } + + // tryServePath found no matching files http.Error(rw, "404 Not Found", http.StatusNotFound) + logger.Logger.Debug("No matching file was found") } -func (h *Handler) tryServePath(rw http.ResponseWriter, site, branch, p string) bool { +// tryServePath attempts to find a valid path from the indexFiles list +func (h *Handler) tryServePath(rw http.ResponseWriter, req *http.Request, site, branch string, updated time.Time, p string) bool { for _, i := range indexFiles { - if h.tryServeFile(rw, site, branch, i(p)) { + // skip invalid paths "." and ".html" + p2 := path.Clean(i(p)) + if isInvalidIndexPath(p2) { + continue + } + + if h.tryServeFile(rw, req, site, branch, updated, p2) { return true } } return false } -func (h *Handler) tryServeFile(rw http.ResponseWriter, site, branch, p string) bool { +// tryServeFile attempts to serve the content of a file if the file can be found +// +// 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. +// +// 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. +func (h *Handler) tryServeFile(rw http.ResponseWriter, req *http.Request, site, branch string, updated time.Time, p string) bool { // prevent path traversal if strings.Contains(site, "..") || strings.Contains(branch, "..") || strings.Contains(p, "..") { http.Error(rw, "400 Bad Request", http.StatusBadRequest) return true } - open, err := h.storageFs.Open(filepath.Join(site, branch, p)) + + servePath := filepath.Join(site, branch, p) + logger.Logger.Debug("Serving file", "full", servePath, "site", site, "branch", branch, "file", p) + open, err := h.storageFs.Open(servePath) switch { case err == nil: - rw.WriteHeader(http.StatusOK) - _, _ = io.Copy(rw, open) + // ignore directories + stat, err := open.Stat() + if err != nil || stat.IsDir() { + return false + } + + // disable timed cache for non-main branches + if branch != "@" { + updated = time.Time{} + } + http.ServeContent(rw, req, p, updated, open) case os.IsNotExist(err): // check next path return false diff --git a/serve/serve_test.go b/serve/serve_test.go index e8cd2e7..44469a6 100644 --- a/serve/serve_test.go +++ b/serve/serve_test.go @@ -1,33 +1,73 @@ package serve import ( - "github.com/1f349/bluebell/conf" + "context" + "database/sql" + "fmt" + "github.com/1f349/bluebell/database" + "github.com/1f349/bluebell/logger" + "github.com/charmbracelet/log" "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" "testing" + "time" ) -func makeConfig(f afero.Fs) (*conf.Conf, error) { - c := conf.New(f) - return c, c.Load() +func init() { + logger.Logger.SetLevel(log.DebugLevel) } -func TestName(t *testing.T) { - f := afero.NewMemMapFs() - h := &Handler{ - storageFs: f, - conf: conf.Testable([]conf.SiteConf{ - {Domain: "example.com", Token: "abcd1234"}, - }), - } - h.findSiteBranchSubdomain("example-com-test") - site, branch := h.findSiteBranch("example-com_test") +type fakeServeDB struct { + branch string } -func TestHandler_Handle(t *testing.T) { - f := afero.NewMemMapFs() - h := &Handler{ - storageFs: f, - conf: &conf.Conf{}, +func (f *fakeServeDB) GetLastUpdatedByDomainBranch(_ context.Context, params database.GetLastUpdatedByDomainBranchParams) (time.Time, error) { + if params.Domain == "example.com" && params.Branch == "@"+f.branch { + return time.Now(), nil } - h.Handle() + return time.Time{}, sql.ErrNoRows +} + +func TestHandler_ServeHTTP(t *testing.T) { + for _, branch := range []string{"", "test", "dev"} { + t.Run(branch+" branch", func(t *testing.T) { + serveTest(t, "example.com", branch, "example.com/@"+branch+"/index.html") + serveTest(t, "example.com/hello-world", branch, "example.com/@"+branch+"/hello-world.html") + serveTest(t, "example.com/hello-world", branch, "example.com/@"+branch+"/hello-world/index.html") + serveTest(t, "example.com/hello-world", branch, "example.com/@"+branch+"/hello-world") + }) + } +} + +func serveTest(t *testing.T, address string, branch string, name string) { + t.Run(fmt.Sprintf("serveTest \"%s\" (%s) -> \"%s\"", address, branch, name), func(t *testing.T) { + fs := afero.NewMemMapFs() + assert.NoError(t, fs.MkdirAll(filepath.Dir(name), os.ModePerm)) + assert.NoError(t, afero.WriteFile(fs, name, []byte("Hello World\n"), 0666)) + h := New(fs, &fakeServeDB{branch: branch}) + + //goland:noinspection HttpUrlsUsage + const httpPrefix = "http://" + req := httptest.NewRequest(http.MethodPost, httpPrefix+address, nil) + if branch != "" { + req.AddCookie(&http.Cookie{ + Name: "__bluebell-site-beta", + Value: branch, + }) + } + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + + res := rec.Result() + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.NotNil(t, res.Body) + all, err := io.ReadAll(res.Body) + assert.NoError(t, err) + assert.Equal(t, "Hello World\n", string(all)) + }) } diff --git a/sqlc.yaml b/sqlc.yaml index 5aa4d05..cc777d1 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -1,6 +1,6 @@ version: "2" sql: - - engine: mysql + - engine: sqlite queries: database/queries schema: database/migrations gen: diff --git a/upload/test-sites.yml b/upload/test-sites.yml deleted file mode 100644 index 496b918..0000000 --- a/upload/test-sites.yml +++ /dev/null @@ -1,2 +0,0 @@ -- domain: example.com - token: abcd1234 diff --git a/upload/upload.go b/upload/upload.go index 49a4013..26169ce 100644 --- a/upload/upload.go +++ b/upload/upload.go @@ -4,6 +4,8 @@ import ( "archive/tar" "compress/gzip" "context" + "crypto/subtle" + "errors" "fmt" "github.com/1f349/bluebell/database" "github.com/dustin/go-humanize" @@ -12,13 +14,17 @@ import ( "io" "io/fs" "net/http" - "os" "path/filepath" + "slices" "strings" ) +var indexBranches = []string{ + "main", + "master", +} + type sitesQueries interface { - GetSiteBySlug(ctx context.Context, slug string) (database.Site, error) GetSiteByDomain(ctx context.Context, domain string) (database.Site, error) } @@ -33,19 +39,19 @@ type Handler struct { db sitesQueries } -func (h *Handler) Handle(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) { - q := req.URL.Query() - site := q.Get("site") - branch := q.Get("branch") +func (h *Handler) Handle(rw http.ResponseWriter, req *http.Request, params httprouter.Params) { + site := params.ByName("site") + branch := params.ByName("branch") site = strings.ReplaceAll(site, "*", "") - siteConf, err := h.db.GetSiteByDomain(req.Context(), "*"+site) + siteConf, err := h.db.GetSiteByDomain(req.Context(), site) if err != nil { http.Error(rw, "", http.StatusNotFound) return } - if "Bearer "+siteConf.Token != req.Header.Get("Authorization") { + token, ok := strings.CutPrefix(req.Header.Get("Authorization"), "Bearer ") + if !ok || subtle.ConstantTimeCompare([]byte(token), []byte(siteConf.Token)) == 0 { http.Error(rw, "403 Forbidden", http.StatusForbidden) return } @@ -58,7 +64,7 @@ func (h *Handler) Handle(rw http.ResponseWriter, req *http.Request, _ httprouter // if file is bigger than 1GiB if fileHeader.Size > maxFileSize { - http.Error(rw, "File too big", http.StatusBadRequest) + http.Error(rw, "File too big", http.StatusInsufficientStorage) return } @@ -72,9 +78,18 @@ func (h *Handler) Handle(rw http.ResponseWriter, req *http.Request, _ httprouter } func (h *Handler) extractTarGzUpload(fileData io.Reader, site, branch string) error { - siteBranchPath := filepath.Join(site, branch) - err := h.storageFs.Rename(siteBranchPath, siteBranchPath+".old") - if err != nil && !os.IsNotExist(err) { + if slices.Contains(indexBranches, branch) { + branch = "" + } + siteBranchPath := filepath.Join(site, "@"+branch) + + err := h.storageFs.RemoveAll(siteBranchPath + ".old") + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("failed to remove old site branch %s: %w", siteBranchPath, err) + } + + err = h.storageFs.Rename(siteBranchPath, siteBranchPath+".old") + if err != nil && !errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("failed to save an old copy of the site: %w", err) } diff --git a/upload/upload_test.go b/upload/upload_test.go index d3a5720..a874079 100644 --- a/upload/upload_test.go +++ b/upload/upload_test.go @@ -2,8 +2,11 @@ package upload import ( "bytes" + "context" + "database/sql" _ "embed" - "github.com/1f349/bluebell/conf" + "fmt" + "github.com/1f349/bluebell/database" "github.com/julienschmidt/httprouter" "github.com/spf13/afero" "github.com/stretchr/testify/assert" @@ -17,65 +20,91 @@ import ( var ( //go:embed test-archive.tar.gz testArchiveTarGz []byte - //go:embed test-sites.yml - testSitesYml []byte ) -func assertUploadedFile(t *testing.T, fs afero.Fs) { +func assertUploadedFile(t *testing.T, fs afero.Fs, branch string) { + switch branch { + case "main", "master": + branch = "" + } + // check uploaded file exists - stat, err := fs.Stat("example.com/main/test.txt") + stat, err := fs.Stat("example.com/@" + branch + "/test.txt") assert.NoError(t, err) assert.False(t, stat.IsDir()) assert.Equal(t, int64(13), stat.Size()) // check contents - o, err := fs.Open("example.com/main/test.txt") + o, err := fs.Open("example.com/@" + branch + "/test.txt") assert.NoError(t, err) all, err := io.ReadAll(o) assert.NoError(t, err) assert.Equal(t, "Hello world!\n", string(all)) } +type fakeUploadDB struct { +} + +func (f *fakeUploadDB) GetSiteByDomain(_ context.Context, domain string) (database.Site, error) { + if domain == "example.com" { + return database.Site{ + ID: 1, + Domain: "example.com", + Token: "abcd1234", + }, nil + } + return database.Site{}, sql.ErrNoRows +} + func TestHandler_Handle(t *testing.T) { - f := afero.NewMemMapFs() - conf := conf.New(f) - h := &Handler{f, conf} - create, err := f.Create("sites.yml") - assert.NoError(t, err) - _, err = create.Write(testSitesYml) - assert.NoError(t, err) - assert.NoError(t, create.Close()) - assert.NoError(t, conf.Load()) + fs := afero.NewMemMapFs() + h := New(fs, new(fakeUploadDB)) - mpBuf := new(bytes.Buffer) - mp := multipart.NewWriter(mpBuf) - file, err := mp.CreateFormFile("upload", "test-archive.tar.gz") - assert.NoError(t, err) - _, err = file.Write(testArchiveTarGz) - assert.NoError(t, err) - assert.NoError(t, mp.Close()) - req, err := http.NewRequest(http.MethodPost, "https://example.com/u?site=example.com&branch=main", mpBuf) - assert.NoError(t, err) - req.Header.Set("Authorization", "Bearer abcd1234") - req.Header.Set("Content-Type", mp.FormDataContentType()) - rec := httptest.NewRecorder() - h.Handle(rec, req, httprouter.Params{}) - res := rec.Result() - assert.Equal(t, http.StatusAccepted, res.StatusCode) - assert.NotNil(t, res.Body) - all, err := io.ReadAll(res.Body) - assert.NoError(t, err) - assert.Equal(t, "", string(all)) + r := httprouter.New() + r.POST("/u/:site/:branch", h.Handle) + r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + panic("Not Found") + }) - assertUploadedFile(t, f) + for _, branch := range []string{"main", "test", "dev"} { + t.Run(branch+" branch", func(t *testing.T) { + mpBuf := new(bytes.Buffer) + mp := multipart.NewWriter(mpBuf) + file, err := mp.CreateFormFile("upload", "test-archive.tar.gz") + assert.NoError(t, err) + _, err = file.Write(testArchiveTarGz) + assert.NoError(t, err) + assert.NoError(t, mp.Close()) + + req := httptest.NewRequest(http.MethodPost, "https://example.com/u/example.com/"+branch, mpBuf) + req.Header.Set("Authorization", "Bearer abcd1234") + req.Header.Set("Content-Type", mp.FormDataContentType()) + rec := httptest.NewRecorder() + r.ServeHTTP(rec, req) + + res := rec.Result() + assert.Equal(t, http.StatusAccepted, res.StatusCode) + assert.NotNil(t, res.Body) + all, err := io.ReadAll(res.Body) + assert.NoError(t, err) + assert.Equal(t, "", string(all)) + + fmt.Println(fs) + + assertUploadedFile(t, fs, branch) + }) + } } func TestHandler_extractTarGzUpload(t *testing.T) { - fs := afero.NewMemMapFs() - conf := conf.New(fs) - h := &Handler{fs, conf} - buffer := bytes.NewBuffer(testArchiveTarGz) - assert.NoError(t, h.extractTarGzUpload(buffer, "example.com", "main")) + for _, branch := range []string{"main", "test", "dev"} { + t.Run(branch+" branch", func(t *testing.T) { + fs := afero.NewMemMapFs() + h := New(fs, nil) + buffer := bytes.NewBuffer(testArchiveTarGz) + assert.NoError(t, h.extractTarGzUpload(buffer, "example.com", branch)) - assertUploadedFile(t, fs) + assertUploadedFile(t, fs, branch) + }) + } }