From 3367bd2d0964dafb120220ba81a9664d308a863c Mon Sep 17 00:00:00 2001 From: MrMelon54 Date: Sat, 22 Jul 2023 00:59:45 +0100 Subject: [PATCH] Start adding project code --- cmd/site-hosting/main.go | 74 ++++++++++++++++++++++++++++++++ test.yml | 15 +++++++ upload/test-archive.tar.gz | Bin 0 -> 136 bytes upload/upload.go | 85 +++++++++++++++++++++++++++++++++++++ upload/upload_test.go | 66 ++++++++++++++++++++++++++++ 5 files changed, 240 insertions(+) create mode 100644 cmd/site-hosting/main.go create mode 100644 test.yml create mode 100644 upload/test-archive.tar.gz create mode 100644 upload/upload.go create mode 100644 upload/upload_test.go diff --git a/cmd/site-hosting/main.go b/cmd/site-hosting/main.go new file mode 100644 index 0000000..77e049c --- /dev/null +++ b/cmd/site-hosting/main.go @@ -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") +} diff --git a/test.yml b/test.yml new file mode 100644 index 0000000..2c86d73 --- /dev/null +++ b/test.yml @@ -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 ./... diff --git a/upload/test-archive.tar.gz b/upload/test-archive.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..e02d5b598ce4b0e2a01f25e3c85e26c558134d55 GIT binary patch literal 136 zcmb2|=3oE==C|hzxtI(^S{{a1cU3T+ES@}FyMp&%NhI@b|1UqZlpg>MxP7^k6tE<`k`^x{7 gvAJv8i|5};-23aRB*Zc(dEolGq^-tJ7&I6d06Hr 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 +} diff --git a/upload/upload_test.go b/upload/upload_test.go new file mode 100644 index 0000000..a0308be --- /dev/null +++ b/upload/upload_test.go @@ -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) +}