commit 73515f218353b07b9015688e7b82a37bb6ccde8f Author: Torjus HÃ¥kestad Date: Wed Apr 20 02:37:27 2022 +0200 Initial commit diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4ca4878 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module git.t-juice.club/torjus/minipaste + +go 1.18 + +require github.com/google/uuid v1.3.0 + +require github.com/go-chi/chi/v5 v5.0.7 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aff816d --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/minipaste.go b/minipaste.go new file mode 100644 index 0000000..096f589 --- /dev/null +++ b/minipaste.go @@ -0,0 +1,14 @@ +package main + +import ( + "git.t-juice.club/torjus/minipaste/server" + "git.t-juice.club/torjus/minipaste/store" +) + +func main() { + s := store.NewMemoryStore() + server := server.NewServer(s) + + server.Addr = ":8080" + server.ListenAndServe() +} diff --git a/server/responses.go b/server/responses.go new file mode 100644 index 0000000..45b90da --- /dev/null +++ b/server/responses.go @@ -0,0 +1,12 @@ +package server + +type ResponseIndex struct { + Name string `json:"name"` + Version string `json:"version"` + GoVersion string `json:"go_version"` + Commit string `json:"commit"` +} + +type ResponseAPIPost struct { + ID string `json:"id"` +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..e53b883 --- /dev/null +++ b/server/server.go @@ -0,0 +1,147 @@ +package server + +import ( + "encoding/json" + "io" + "log" + "mime" + "mime/multipart" + "net/http" + "runtime" + "runtime/debug" + "strings" + + "git.t-juice.club/torjus/minipaste/store" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" +) + +const Version = "v0.1.0" + +type Server struct { + store store.Store + http.Server +} + +func NewServer(s store.Store) *Server { + srv := &Server{store: s} + r := chi.NewRouter() + + r.Use(middleware.RealIP) + r.Use(middleware.Recoverer) + r.Use(middleware.Logger) + + r.Get("/", srv.HandlerIndexGet) + r.Route("/api", func(r chi.Router) { + r.Get("/{id}", srv.HandlerAPIGet) + r.Post("/", srv.HandlerAPIPost) + }) + + srv.Handler = r + + return srv +} + +func (s *Server) HandlerIndexGet(w http.ResponseWriter, r *http.Request) { + bi, ok := debug.ReadBuildInfo() + if !ok { + panic("not ok") + } + var commitHash string + for i := range bi.Settings { + if bi.Settings[i].Key == "vcs.revision" { + commitHash = bi.Settings[i].Value + } + } + resp := &ResponseIndex{ + Name: "minipaste", + Version: "v0.0.0", + Commit: commitHash, + GoVersion: runtime.Version(), + } + encoder := json.NewEncoder(w) + if err := encoder.Encode(resp); err != nil { + log.Panicf("Error encoding response: %s", err) + } +} + +func (s *Server) HandlerAPIPost(w http.ResponseWriter, r *http.Request) { + // Check if multipart + mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + log.Panicf("Error parsing media type: %s", err) + } + log.Printf("mt: %s", mediaType) + + if strings.HasPrefix(mediaType, "multipart/") { + // Is multipart + mr := multipart.NewReader(r.Body, params["boundary"]) + for { + p, err := mr.NextPart() + if err == io.EOF { + break + } + if err != nil { + log.Fatal(err) + } + defer p.Close() + + id, err := s.store.Add(p) + if err != nil { + log.Panicf("Error storing file: %s", err) + } + log.Printf("Stored file with id %s", id) + + resp := &ResponseAPIPost{ID: id} + encoder := json.NewEncoder(w) + + if err := encoder.Encode(resp); err != nil { + log.Panicf("Error encoding response: %s", err) + } + } + } else { + // Is not multipart + if mediaType == "application/x-www-form-urlencoded" { + if err := r.ParseForm(); err != nil { + log.Printf("Error parsing form: %s", err) + } + for key := range r.Form { + file, _, err := r.FormFile(key) + if err != nil { + log.Panicf("Error parsing formfile: %s", err) + } + defer file.Close() + + id, err := s.store.Add(file) + if err != nil { + log.Panicf("Error storing file: %s", err) + } + log.Printf("Stored file with id %s", id) + + resp := &ResponseAPIPost{ID: id} + encoder := json.NewEncoder(w) + + if err := encoder.Encode(resp); err != nil { + log.Panicf("Error encoding response: %s", err) + } + } + } + } +} + +func (s *Server) HandlerAPIGet(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + if id == "" { + log.Panicf("Missing id") + } + + reader, err := s.store.Get(id) + if err != nil { + log.Panicf("No such file") + } + defer reader.Close() + + if _, err := io.Copy(w, reader); err != nil { + log.Panicf("Error writing to client: %s", err) + } +} diff --git a/store/memory.go b/store/memory.go new file mode 100644 index 0000000..d4e2351 --- /dev/null +++ b/store/memory.go @@ -0,0 +1,46 @@ +package store + +import ( + "bytes" + "io" + "io/ioutil" + + "github.com/google/uuid" +) + +type MemoryStore struct { + data map[string][]byte +} + +func NewMemoryStore() *MemoryStore { + return &MemoryStore{data: map[string][]byte{}} +} + +func (s *MemoryStore) Add(r io.Reader) (string, error) { + id := uuid.Must(uuid.NewRandom()).String() + + data, err := ioutil.ReadAll(r) + if err != nil { + return "", err + } + + s.data[id] = data + return id, nil +} + +func (s *MemoryStore) Delete(id string) error { + delete(s.data, id) + return nil +} + +func (s *MemoryStore) Get(id string) (io.ReadCloser, error) { + data, ok := s.data[id] + if !ok { + return nil, ErrNoSuchItem + } + br := bytes.NewReader(data) + + r := io.NopCloser(br) + + return r, nil +} diff --git a/store/memory_test.go b/store/memory_test.go new file mode 100644 index 0000000..4ff9b27 --- /dev/null +++ b/store/memory_test.go @@ -0,0 +1,12 @@ +package store_test + +import ( + "testing" + + "git.t-juice.club/torjus/minipaste/store" +) + +func TestMemoryStore(t *testing.T) { + s := store.NewMemoryStore() + testStore(s, t) +} diff --git a/store/store.go b/store/store.go new file mode 100644 index 0000000..c4482d6 --- /dev/null +++ b/store/store.go @@ -0,0 +1,14 @@ +package store + +import ( + "errors" + "io" +) + +var ErrNoSuchItem = errors.New("no such item") + +type Store interface { + Add(r io.Reader) (string, error) + Delete(id string) error + Get(id string) (io.ReadCloser, error) +} diff --git a/store/store_test.go b/store/store_test.go new file mode 100644 index 0000000..c03d182 --- /dev/null +++ b/store/store_test.go @@ -0,0 +1,45 @@ +package store_test + +import ( + "io" + "strings" + "testing" + + "git.t-juice.club/torjus/minipaste/store" +) + +func testStore(s store.Store, t *testing.T) { + t.Run("TestSimple", func(t *testing.T) { + expected := "this is some text" + sr := strings.NewReader(expected) + + // Add + id, err := s.Add(sr) + if err != nil { + t.Fatalf("Error when adding: %s", err) + } + if id == "" { + t.Fatalf("Blank ID returned") + } + + // Get + data, err := s.Get(id) + if err != nil { + t.Fatalf("Error getting data: %s", err) + } + + var sb strings.Builder + + if _, err := io.Copy(&sb, data); err != nil { + t.Fatalf("Error reading returned data: %s", err) + } + + if sb.String() != expected { + t.Fatalf("Returned data does not match expected") + } + + if err := s.Delete(id); err != nil { + t.Fatalf("Error deleting: %s", err) + } + }) +}