mirror of
https://github.com/1f349/site-hosting.git
synced 2024-12-26 18:06:32 +00:00
Keep working on this
This commit is contained in:
parent
c727ac594f
commit
a431e506ec
@ -3,8 +3,12 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/1f349/site-hosting/config"
|
||||
"github.com/1f349/site-hosting/serve"
|
||||
"github.com/1f349/site-hosting/upload"
|
||||
"github.com/MrMelon54/exit-reload"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/spf13/afero"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -32,10 +36,15 @@ func main() {
|
||||
log.Fatal("[SiteHosting] Failed to stat storage path, does the directory exist? Error: ", err)
|
||||
}
|
||||
|
||||
uploadHandler := upload.New(storageFlag)
|
||||
storageFs := afero.NewBasePathFs(afero.NewOsFs(), storageFlag)
|
||||
liveConf := config.New(storageFs)
|
||||
|
||||
uploadHandler := upload.New(storageFs, liveConf)
|
||||
serveHandler := serve.New(storageFs, liveConf)
|
||||
|
||||
router := httprouter.New()
|
||||
router.POST("/u/:site", uploadHandler.Handle)
|
||||
router.GET("/", serveHandler.Handle)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: listenFlag,
|
||||
@ -53,13 +62,29 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
exit_reload.ExitReload("SiteHosting", func() {
|
||||
|
||||
}, func() {
|
||||
|
||||
})
|
||||
|
||||
exitSig := make(chan struct{}, 1)
|
||||
scReload := make(chan os.Signal, 1)
|
||||
signal.Notify(scReload, syscall.SIGHUP)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-exitSig:
|
||||
case <-scReload:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for exit signal
|
||||
sc := make(chan os.Signal, 1)
|
||||
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
|
||||
<-sc
|
||||
close(exitSig)
|
||||
fmt.Println()
|
||||
|
||||
// Stop server
|
||||
|
92
config/config.go
Normal file
92
config/config.go
Normal file
@ -0,0 +1,92 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/MrMelon54/trie"
|
||||
"github.com/spf13/afero"
|
||||
"gopkg.in/yaml.v3"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
fs afero.Fs
|
||||
l *sync.RWMutex
|
||||
m *trie.Trie[SiteConf]
|
||||
}
|
||||
|
||||
type SiteConf struct {
|
||||
Domain string `json:"domain"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func New(storageFs afero.Fs) *Config {
|
||||
return &Config{
|
||||
fs: storageFs,
|
||||
l: new(sync.RWMutex),
|
||||
m: trie.BuildFromMap(map[string]SiteConf{}),
|
||||
}
|
||||
}
|
||||
|
||||
func Testable(sites []SiteConf) *Config {
|
||||
c := &Config{}
|
||||
c.loadSlice(sites)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Config) Load() error {
|
||||
open, err := c.fs.Open("sites.yml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open sites.yml: %w", err)
|
||||
}
|
||||
var a []SiteConf
|
||||
err = yaml.NewDecoder(open).Decode(&a)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse yaml: %w", err)
|
||||
}
|
||||
|
||||
c.loadSlice(a)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) loadSlice(sites []SiteConf) {
|
||||
m := make(map[string]SiteConf, len(sites))
|
||||
|
||||
for _, i := range sites {
|
||||
m[c.slugFromDomain(i.Domain)] = i
|
||||
}
|
||||
|
||||
t := trie.BuildFromMap(m)
|
||||
|
||||
c.l.Lock()
|
||||
c.m = t
|
||||
c.l.Unlock()
|
||||
}
|
||||
|
||||
func (c *Config) 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)
|
||||
}
|
||||
|
||||
func (c *Config) Get(key string) (*SiteConf, int, bool) {
|
||||
return c.getInternal(c.slugFromDomain(key))
|
||||
}
|
||||
|
||||
func (c *Config) getInternal(key string) (*SiteConf, int, bool) {
|
||||
c.l.RLock()
|
||||
defer c.l.RUnlock()
|
||||
return c.m.SearchPrefixInString(key)
|
||||
}
|
70
config/config_test.go
Normal file
70
config/config_test.go
Normal file
@ -0,0 +1,70 @@
|
||||
package config
|
||||
|
||||
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 := &Config{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 := &Config{}
|
||||
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 := &Config{}
|
||||
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 := &Config{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)
|
||||
}
|
2
config/test-sites.yml
Normal file
2
config/test-sites.yml
Normal file
@ -0,0 +1,2 @@
|
||||
- domain: example.com
|
||||
token: abcd1234
|
10
go.mod
10
go.mod
@ -3,14 +3,16 @@ module github.com/1f349/site-hosting
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/MrMelon54/exit-reload v0.0.1
|
||||
github.com/MrMelon54/trie v0.0.2
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/spf13/afero v1.9.5
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
)
|
||||
|
16
go.sum
16
go.sum
@ -38,6 +38,10 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/MrMelon54/exit-reload v0.0.1 h1:sxHa59tNEQMcikwuX2+93lw6Vi1+R7oCRF8a0C3alXc=
|
||||
github.com/MrMelon54/exit-reload v0.0.1/go.mod h1:PLiSfmUzwdpTTQP3BBfUPhkqPwaIZjx0DuXBnM76Bug=
|
||||
github.com/MrMelon54/trie v0.0.2 h1:ZXWcX5ij62O9K4I/anuHmVg8L3tF0UGdlPceAASwKEY=
|
||||
github.com/MrMelon54/trie v0.0.2/go.mod h1:sGCGOcqb+DxSxvHgSOpbpkmA7mFZR47YDExy9OCbVZI=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
@ -46,8 +50,9 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@ -137,8 +142,9 @@ github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@ -283,8 +289,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@ -432,8 +439,9 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
112
serve/serve.go
Normal file
112
serve/serve.go
Normal file
@ -0,0 +1,112 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"github.com/1f349/site-hosting/config"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/spf13/afero"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
indexBranches = []string{
|
||||
"main",
|
||||
"master",
|
||||
}
|
||||
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 New(storage afero.Fs, conf *config.Config) *Handler {
|
||||
return &Handler{storage, conf}
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
storageFs afero.Fs
|
||||
conf *config.Config
|
||||
}
|
||||
|
||||
func (h *Handler) Handle(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
site, branch, subdomain, ok := h.findSiteBranchSubdomain(req.Host)
|
||||
if !ok {
|
||||
http.Error(rw, "Bad Gateway", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
if branch == "" {
|
||||
for _, i := range indexBranches {
|
||||
if h.tryServePath(rw, site, i, subdomain, req.URL.Path) {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if h.tryServePath(rw, site, branch, subdomain, req.URL.Path) {
|
||||
return
|
||||
}
|
||||
http.Error(rw, "404 Not Found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
func (h *Handler) findSiteBranchSubdomain(host string) (site, branch, subdomain string, ok bool) {
|
||||
var siteN int
|
||||
siteN, site = h.findSite(host)
|
||||
if site == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if host[siteN] != '-' {
|
||||
return
|
||||
}
|
||||
host = host[siteN+1:]
|
||||
|
||||
strings.LastIndexByte(host, '-')
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Handler) findSite(host string) (int, string) {
|
||||
siteVal, siteN, siteOk := h.conf.Get(host)
|
||||
if !siteOk || siteVal == nil {
|
||||
return -1, ""
|
||||
}
|
||||
|
||||
// so I used less than or equal here that's to prevent a bug where the prefix
|
||||
// found is longer than the string obviously that sounds impossible, and it is,
|
||||
// but I would rather the program not crash if some other bug allows this weird
|
||||
// event to happen
|
||||
if siteN <= len(host) {
|
||||
return -1, ""
|
||||
}
|
||||
return siteN, siteVal.Domain
|
||||
}
|
||||
|
||||
func (h *Handler) tryServePath(rw http.ResponseWriter, site, branch, subdomain, p string) bool {
|
||||
for _, i := range indexFiles {
|
||||
if h.tryServeFile(rw, site, branch, subdomain, i(p)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *Handler) tryServeFile(rw http.ResponseWriter, site, branch, subdomain, p string) bool {
|
||||
// if there is a subdomain then load files from inside the subdomain folder
|
||||
if subdomain != "" {
|
||||
p = filepath.Join("_subdomain", subdomain, p)
|
||||
}
|
||||
open, err := h.storageFs.Open(filepath.Join(site, branch, p))
|
||||
switch {
|
||||
case err == nil:
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
_, _ = io.Copy(rw, open)
|
||||
case os.IsNotExist(err):
|
||||
// check next path
|
||||
return false
|
||||
default:
|
||||
http.Error(rw, "500 Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
return true
|
||||
}
|
33
serve/serve_test.go
Normal file
33
serve/serve_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"github.com/1f349/site-hosting/config"
|
||||
"github.com/spf13/afero"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func makeConfig(f afero.Fs) (*config.Config, error) {
|
||||
c := config.New(f)
|
||||
return c, c.Load()
|
||||
}
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
f := afero.NewMemMapFs()
|
||||
h := &Handler{
|
||||
storageFs: f,
|
||||
conf: config.Testable([]config.SiteConf{
|
||||
{Domain: "example.com", Token: "abcd1234"},
|
||||
}),
|
||||
}
|
||||
h.findSiteBranchSubdomain("example-com-test")
|
||||
site, branch := h.findSiteBranch("example_com_test")
|
||||
}
|
||||
|
||||
func TestHandler_Handle(t *testing.T) {
|
||||
f := afero.NewMemMapFs()
|
||||
h := &Handler{
|
||||
storageFs: f,
|
||||
conf: &config.Config{},
|
||||
}
|
||||
h.Handle()
|
||||
}
|
2
upload/test-sites.yml
Normal file
2
upload/test-sites.yml
Normal file
@ -0,0 +1,2 @@
|
||||
- domain: example.com
|
||||
token: abcd1234
|
@ -4,26 +4,39 @@ import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"github.com/1f349/site-hosting/config"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/spf13/afero"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func New(storagePath string) *Handler {
|
||||
fs := afero.NewBasePathFs(afero.NewOsFs(), storagePath)
|
||||
return &Handler{fs}
|
||||
func New(storage afero.Fs, conf *config.Config) *Handler {
|
||||
return &Handler{storage, conf}
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
storageFs afero.Fs
|
||||
conf *config.Config
|
||||
}
|
||||
|
||||
func (h *Handler) Handle(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
site := params.ByName("site")
|
||||
branch := req.URL.Query().Get("branch")
|
||||
q := req.URL.Query()
|
||||
site := q.Get("site")
|
||||
branch := q.Get("branch")
|
||||
|
||||
siteConf, siteN, siteOk := h.conf.Get(site)
|
||||
if !siteOk || siteN != len(site) || siteConf == nil {
|
||||
http.Error(rw, "400 Bad Request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if "Bearer "+siteConf.Token != req.Header.Get("Authorization") {
|
||||
http.Error(rw, "403 Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
fileData, fileHeader, err := req.FormFile("upload")
|
||||
if err != nil {
|
||||
@ -47,7 +60,17 @@ func (h *Handler) Handle(rw http.ResponseWriter, req *http.Request, params httpr
|
||||
}
|
||||
|
||||
func (h *Handler) extractTarGzUpload(fileData io.Reader, site, branch string) error {
|
||||
storeFs := afero.NewBasePathFs(h.storageFs, filepath.Join(site, branch))
|
||||
siteBranchPath := filepath.Join(site, branch)
|
||||
err := h.storageFs.Rename(siteBranchPath, siteBranchPath+".old")
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to save an old copy of the site: %w", err)
|
||||
}
|
||||
|
||||
err = h.storageFs.MkdirAll(siteBranchPath, fs.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to make site directory: %w", err)
|
||||
}
|
||||
branchFs := afero.NewBasePathFs(h.storageFs, siteBranchPath)
|
||||
|
||||
// decompress gzip wrapper
|
||||
gzipReader, err := gzip.NewReader(fileData)
|
||||
@ -60,18 +83,19 @@ func (h *Handler) extractTarGzUpload(fileData io.Reader, site, branch string) er
|
||||
for {
|
||||
next, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
// finished reading tar, exit now
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid tar archive: %w", err)
|
||||
}
|
||||
|
||||
err = storeFs.MkdirAll(filepath.Dir(next.Name), os.ModePerm)
|
||||
err = branchFs.MkdirAll(filepath.Dir(next.Name), fs.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to make directory tree: %w", err)
|
||||
}
|
||||
|
||||
create, err := storeFs.Create(next.Name)
|
||||
create, err := branchFs.Create(next.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create output file: '%s': %w", next.Name, err)
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package upload
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"github.com/1f349/site-hosting/config"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -13,8 +14,12 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
//go:embed test-archive.tar.gz
|
||||
var testArchiveTarGz []byte
|
||||
var (
|
||||
//go:embed test-archive.tar.gz
|
||||
testArchiveTarGz []byte
|
||||
//go:embed test-sites.yml
|
||||
testSitesYml []byte
|
||||
)
|
||||
|
||||
func assertUploadedFile(t *testing.T, fs afero.Fs) {
|
||||
// check uploaded file exists
|
||||
@ -32,8 +37,16 @@ func assertUploadedFile(t *testing.T, fs afero.Fs) {
|
||||
}
|
||||
|
||||
func TestHandler_Handle(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
h := &Handler{fs}
|
||||
f := afero.NewMemMapFs()
|
||||
conf := config.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())
|
||||
|
||||
mpBuf := new(bytes.Buffer)
|
||||
mp := multipart.NewWriter(mpBuf)
|
||||
file, err := mp.CreateFormFile("upload", "test-archive.tar.gz")
|
||||
@ -41,11 +54,12 @@ func TestHandler_Handle(t *testing.T) {
|
||||
_, err = file.Write(testArchiveTarGz)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, mp.Close())
|
||||
req, err := http.NewRequest(http.MethodPost, "https://example.com/u/example.com?branch=main", mpBuf)
|
||||
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{{Key: "site", Value: "example.com"}})
|
||||
h.Handle(rec, req, httprouter.Params{})
|
||||
res := rec.Result()
|
||||
assert.Equal(t, http.StatusAccepted, res.StatusCode)
|
||||
assert.NotNil(t, res.Body)
|
||||
@ -53,12 +67,13 @@ func TestHandler_Handle(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", string(all))
|
||||
|
||||
assertUploadedFile(t, fs)
|
||||
assertUploadedFile(t, f)
|
||||
}
|
||||
|
||||
func TestHandler_extractTarGzUpload(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
h := &Handler{fs}
|
||||
conf := config.New(fs)
|
||||
h := &Handler{fs, conf}
|
||||
buffer := bytes.NewBuffer(testArchiveTarGz)
|
||||
assert.NoError(t, h.extractTarGzUpload(buffer, "example.com", "main"))
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user