Keep working on this

This commit is contained in:
Melon 2023-08-21 00:27:54 +01:00
parent c727ac594f
commit a431e506ec
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
11 changed files with 410 additions and 25 deletions

View File

@ -3,8 +3,12 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"github.com/1f349/site-hosting/config"
"github.com/1f349/site-hosting/serve"
"github.com/1f349/site-hosting/upload" "github.com/1f349/site-hosting/upload"
"github.com/MrMelon54/exit-reload"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/spf13/afero"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -32,10 +36,15 @@ func main() {
log.Fatal("[SiteHosting] Failed to stat storage path, does the directory exist? Error: ", err) 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 := httprouter.New()
router.POST("/u/:site", uploadHandler.Handle) router.POST("/u/:site", uploadHandler.Handle)
router.GET("/", serveHandler.Handle)
srv := &http.Server{ srv := &http.Server{
Addr: listenFlag, 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) scReload := make(chan os.Signal, 1)
signal.Notify(scReload, syscall.SIGHUP) signal.Notify(scReload, syscall.SIGHUP)
go func() {
for {
select {
case <-exitSig:
case <-scReload:
}
}
}()
// Wait for exit signal // Wait for exit signal
sc := make(chan os.Signal, 1) sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
<-sc <-sc
close(exitSig)
fmt.Println() fmt.Println()
// Stop server // Stop server

92
config/config.go Normal file
View 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
View 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
View File

@ -0,0 +1,2 @@
- domain: example.com
token: abcd1234

10
go.mod
View File

@ -3,14 +3,16 @@ module github.com/1f349/site-hosting
go 1.20 go 1.20
require ( 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/julienschmidt/httprouter v1.3.0
github.com/spf13/afero v1.9.5 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 ( 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 github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.8 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
) )

16
go.sum
View File

@ -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= 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/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/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/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/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 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-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-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/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.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.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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 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/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.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.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.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.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/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= 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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/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.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.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-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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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/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/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.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.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-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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/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
View 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
View 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
View File

@ -0,0 +1,2 @@
- domain: example.com
token: abcd1234

View File

@ -4,26 +4,39 @@ import (
"archive/tar" "archive/tar"
"compress/gzip" "compress/gzip"
"fmt" "fmt"
"github.com/1f349/site-hosting/config"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/spf13/afero" "github.com/spf13/afero"
"io" "io"
"io/fs"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
) )
func New(storagePath string) *Handler { func New(storage afero.Fs, conf *config.Config) *Handler {
fs := afero.NewBasePathFs(afero.NewOsFs(), storagePath) return &Handler{storage, conf}
return &Handler{fs}
} }
type Handler struct { type Handler struct {
storageFs afero.Fs storageFs afero.Fs
conf *config.Config
} }
func (h *Handler) Handle(rw http.ResponseWriter, req *http.Request, params httprouter.Params) { func (h *Handler) Handle(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
site := params.ByName("site") q := req.URL.Query()
branch := req.URL.Query().Get("branch") 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") fileData, fileHeader, err := req.FormFile("upload")
if err != nil { 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 { 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 // decompress gzip wrapper
gzipReader, err := gzip.NewReader(fileData) gzipReader, err := gzip.NewReader(fileData)
@ -60,18 +83,19 @@ func (h *Handler) extractTarGzUpload(fileData io.Reader, site, branch string) er
for { for {
next, err := tarReader.Next() next, err := tarReader.Next()
if err == io.EOF { if err == io.EOF {
// finished reading tar, exit now
break break
} }
if err != nil { if err != nil {
return fmt.Errorf("invalid tar archive: %w", err) 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 { if err != nil {
return fmt.Errorf("failed to make directory tree: %w", err) 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 { if err != nil {
return fmt.Errorf("failed to create output file: '%s': %w", next.Name, err) return fmt.Errorf("failed to create output file: '%s': %w", next.Name, err)
} }

View File

@ -3,6 +3,7 @@ package upload
import ( import (
"bytes" "bytes"
_ "embed" _ "embed"
"github.com/1f349/site-hosting/config"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -13,8 +14,12 @@ import (
"testing" "testing"
) )
var (
//go:embed test-archive.tar.gz //go:embed test-archive.tar.gz
var testArchiveTarGz []byte 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) {
// check uploaded file exists // check uploaded file exists
@ -32,8 +37,16 @@ func assertUploadedFile(t *testing.T, fs afero.Fs) {
} }
func TestHandler_Handle(t *testing.T) { func TestHandler_Handle(t *testing.T) {
fs := afero.NewMemMapFs() f := afero.NewMemMapFs()
h := &Handler{fs} 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) mpBuf := new(bytes.Buffer)
mp := multipart.NewWriter(mpBuf) mp := multipart.NewWriter(mpBuf)
file, err := mp.CreateFormFile("upload", "test-archive.tar.gz") file, err := mp.CreateFormFile("upload", "test-archive.tar.gz")
@ -41,11 +54,12 @@ func TestHandler_Handle(t *testing.T) {
_, err = file.Write(testArchiveTarGz) _, err = file.Write(testArchiveTarGz)
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, mp.Close()) 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) assert.NoError(t, err)
req.Header.Set("Authorization", "Bearer abcd1234")
req.Header.Set("Content-Type", mp.FormDataContentType()) req.Header.Set("Content-Type", mp.FormDataContentType())
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
h.Handle(rec, req, httprouter.Params{{Key: "site", Value: "example.com"}}) h.Handle(rec, req, httprouter.Params{})
res := rec.Result() res := rec.Result()
assert.Equal(t, http.StatusAccepted, res.StatusCode) assert.Equal(t, http.StatusAccepted, res.StatusCode)
assert.NotNil(t, res.Body) assert.NotNil(t, res.Body)
@ -53,12 +67,13 @@ func TestHandler_Handle(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "", string(all)) assert.Equal(t, "", string(all))
assertUploadedFile(t, fs) assertUploadedFile(t, f)
} }
func TestHandler_extractTarGzUpload(t *testing.T) { func TestHandler_extractTarGzUpload(t *testing.T) {
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
h := &Handler{fs} conf := config.New(fs)
h := &Handler{fs, conf}
buffer := bytes.NewBuffer(testArchiveTarGz) buffer := bytes.NewBuffer(testArchiveTarGz)
assert.NoError(t, h.extractTarGzUpload(buffer, "example.com", "main")) assert.NoError(t, h.extractTarGzUpload(buffer, "example.com", "main"))