Initial commit
This commit is contained in:
commit
73515f2183
7
go.mod
Normal file
7
go.mod
Normal file
@ -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
|
4
go.sum
Normal file
4
go.sum
Normal file
@ -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=
|
14
minipaste.go
Normal file
14
minipaste.go
Normal file
@ -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()
|
||||||
|
}
|
12
server/responses.go
Normal file
12
server/responses.go
Normal file
@ -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"`
|
||||||
|
}
|
147
server/server.go
Normal file
147
server/server.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
46
store/memory.go
Normal file
46
store/memory.go
Normal file
@ -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
|
||||||
|
}
|
12
store/memory_test.go
Normal file
12
store/memory_test.go
Normal file
@ -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)
|
||||||
|
}
|
14
store/store.go
Normal file
14
store/store.go
Normal file
@ -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)
|
||||||
|
}
|
45
store/store_test.go
Normal file
45
store/store_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user