mirror of
https://github.com/1f349/site-hosting.git
synced 2024-11-25 05:01:38 +00:00
Start adding project code
This commit is contained in:
parent
3046ff86c1
commit
3367bd2d09
74
cmd/site-hosting/main.go
Normal file
74
cmd/site-hosting/main.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"github.com/1f349/site-hosting/upload"
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
listenFlag string
|
||||||
|
storageFlag string
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.StringVar(&listenFlag, "listen", "", "Address to listen on")
|
||||||
|
flag.StringVar(&storageFlag, "storage", "", "Path site files are stored in")
|
||||||
|
if listenFlag == "" {
|
||||||
|
log.Fatal("[SiteHosting] Missing listen flag")
|
||||||
|
}
|
||||||
|
if storageFlag == "" {
|
||||||
|
log.Fatal("[SiteHosting] Missing storage flag")
|
||||||
|
}
|
||||||
|
_, err := os.Stat(storageFlag)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("[SiteHosting] Failed to stat storage path, does the directory exist? Error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadHandler := upload.New(storageFlag)
|
||||||
|
|
||||||
|
router := httprouter.New()
|
||||||
|
router.POST("/u/:site", uploadHandler.Handle)
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: listenFlag,
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
log.Printf("[SiteHosting] Starting server on: '%s'\n", srv.Addr)
|
||||||
|
go func() {
|
||||||
|
err := srv.ListenAndServe()
|
||||||
|
if err != nil {
|
||||||
|
if err == http.ErrServerClosed {
|
||||||
|
log.Printf("[SiteHosting] The http server shutdown successfully\n")
|
||||||
|
} else {
|
||||||
|
log.Printf("[SiteHosting] Error trying to host the http server: %s\n", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
scReload := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(scReload, syscall.SIGHUP)
|
||||||
|
|
||||||
|
// Wait for exit signal
|
||||||
|
sc := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
|
||||||
|
<-sc
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Stop server
|
||||||
|
log.Printf("[SiteHosting] Stopping...")
|
||||||
|
n := time.Now()
|
||||||
|
|
||||||
|
// close http server
|
||||||
|
_ = srv.Close()
|
||||||
|
|
||||||
|
log.Printf("[SiteHosting] Took '%s' to shutdown\n", time.Now().Sub(n))
|
||||||
|
log.Println("[SiteHosting] Goodbye")
|
||||||
|
}
|
15
test.yml
Normal file
15
test.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
on: [push, pull_request]
|
||||||
|
name: Test
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.20.x]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- run: go build ./cmd/site-hosting/
|
||||||
|
- run: go test ./...
|
BIN
upload/test-archive.tar.gz
Normal file
BIN
upload/test-archive.tar.gz
Normal file
Binary file not shown.
85
upload/upload.go
Normal file
85
upload/upload.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package upload
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(storagePath string) *Handler {
|
||||||
|
fs := afero.NewBasePathFs(afero.NewOsFs(), storagePath)
|
||||||
|
return &Handler{fs}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
storageFs afero.Fs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Handle(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||||
|
site := params.ByName("site")
|
||||||
|
branch := req.URL.Query().Get("branch")
|
||||||
|
|
||||||
|
fileData, fileHeader, err := req.FormFile("upload")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, "Missing file upload", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if file is bigger than 1GiB
|
||||||
|
if fileHeader.Size > 1074000000 {
|
||||||
|
http.Error(rw, "File too big", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.extractTarGzUpload(fileData, site, branch)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, fmt.Sprintf("Invalid upload: %s", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rw.WriteHeader(http.StatusAccepted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) extractTarGzUpload(fileData io.Reader, site, branch string) error {
|
||||||
|
storeFs := afero.NewBasePathFs(h.storageFs, filepath.Join(site, branch))
|
||||||
|
|
||||||
|
// decompress gzip wrapper
|
||||||
|
gzipReader, err := gzip.NewReader(fileData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid gzip file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse tar encoding
|
||||||
|
tarReader := tar.NewReader(gzipReader)
|
||||||
|
for {
|
||||||
|
next, err := tarReader.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid tar archive: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = storeFs.MkdirAll(filepath.Dir(next.Name), os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to make directory tree: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
create, err := storeFs.Create(next.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create output file: '%s': %w", next.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(create, tarReader)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to copy from archive to output file: '%s': %w", next.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
66
upload/upload_test.go
Normal file
66
upload/upload_test.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package upload
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
_ "embed"
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed test-archive.tar.gz
|
||||||
|
var testArchiveTarGz []byte
|
||||||
|
|
||||||
|
func assertUploadedFile(t *testing.T, fs afero.Fs) {
|
||||||
|
// check uploaded file exists
|
||||||
|
stat, err := fs.Stat("example.com/main/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")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
all, err := io.ReadAll(o)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "Hello world!\n", string(all))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_Handle(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
h := &Handler{fs}
|
||||||
|
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/example.com?branch=main", mpBuf)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.Header.Set("Content-Type", mp.FormDataContentType())
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.Handle(rec, req, httprouter.Params{{Key: "site", Value: "example.com"}})
|
||||||
|
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))
|
||||||
|
|
||||||
|
assertUploadedFile(t, fs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_extractTarGzUpload(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
h := &Handler{fs}
|
||||||
|
buffer := bytes.NewBuffer(testArchiveTarGz)
|
||||||
|
assert.NoError(t, h.extractTarGzUpload(buffer, "example.com", "main"))
|
||||||
|
|
||||||
|
assertUploadedFile(t, fs)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user