diff options
| author | Marin Ivanov <[email protected]> | 2025-06-13 16:09:50 +0300 |
|---|---|---|
| committer | Marin Ivanov <[email protected]> | 2025-06-13 16:09:50 +0300 |
| commit | 3cb88faf5d952596b1efa89833fe6e7415be0513 (patch) | |
| tree | d7ba66d7cf9a2889fc8dd128e843199e19fa9bec | |
| parent | 3255b4b862bfc52e62ebb92e94dcf71261aeffb6 (diff) | |
v2
| -rw-r--r-- | go.mod | 3 | ||||
| -rw-r--r-- | go.sum | 4 | ||||
| -rw-r--r-- | main.go | 200 | ||||
| -rw-r--r-- | post.html | 31 |
4 files changed, 195 insertions, 43 deletions
@@ -3,6 +3,9 @@ module go.metala.org/gloginki go 1.24.1 require ( + github.com/gabriel-vasile/mimetype v1.4.9 github.com/robert-nix/ansihtml v1.0.1 github.com/spf13/pflag v1.0.6 ) + +require golang.org/x/net v0.39.0 // indirect @@ -1,5 +1,7 @@ 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/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robert-nix/ansihtml v1.0.1 h1:VTiyQ6/+AxSJoSSLsMecnkh8i0ZqOEdiRl/odOc64fc= @@ -9,6 +11,8 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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= @@ -1,7 +1,9 @@ package main import ( + "bytes" "crypto/rand" + "embed" "encoding/base64" "encoding/json" "fmt" @@ -12,12 +14,14 @@ import ( "path/filepath" "strings" + "github.com/gabriel-vasile/mimetype" "github.com/robert-nix/ansihtml" flag "github.com/spf13/pflag" ) const ( - MetadataExt = ".metadata" + MetadataExt = ".metadata" + DefaultMimeType = "application/octet-stream" ) type Server struct { @@ -28,6 +32,33 @@ type Metadata struct { ContentType string `json:"contentType"` } +//go:embed post.html +var static embed.FS + +func detectMimetype(input io.Reader) (mimeType string, fileext string, recycled io.Reader, err error) { + header := bytes.NewBuffer(nil) + mtype, err := mimetype.DetectReader(io.TeeReader(input, header)) + recycled = io.MultiReader(header, input) + return mtype.String(), mtype.Extension(), recycled, err +} + +func serveAnsiHtml(w http.ResponseWriter, filename string) { + f, err := os.Open(filename) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Add("content-type", "text/html") + data, err := io.ReadAll(f) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + data = ansihtml.ConvertToHTML(data) + data = []byte(fmt.Sprintf("<pre>%s</pre>", strings.ReplaceAll(string(data), "\n", "<br>"))) + w.Write(data) +} + func writeMetadata(filename string, m *Metadata) error { f, err := os.OpenFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644) if err != nil { @@ -50,68 +81,91 @@ func readMetadata(filename string) (*Metadata, error) { return &m, nil } -func (s *Server) Post(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - return - } +func (s *Server) writeMetadata(name string, m *Metadata) error { + filename := filepath.Join(s.data, name+MetadataExt) + return writeMetadata(filename, m) +} + +func (s *Server) createRandomFilename(ext string) (*os.File, string, error) { var random [16]byte _, err := io.ReadFull(rand.Reader, random[:]) if err != nil { - log.Print("io.ReadFull(rand.Reader): ", err) - w.WriteHeader(http.StatusInternalServerError) - return + return nil, "", err } - name := base64.RawURLEncoding.EncodeToString(random[:]) + name := base64.RawURLEncoding.EncodeToString(random[:]) + ext filename := filepath.Join(s.data, name) f, err := os.OpenFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644) if err != nil { - log.Print("OpenFile(): ", err) + return nil, "", err + } + return f, name, nil +} + +func (s *Server) formPost(w http.ResponseWriter, r *http.Request) { + r.ParseMultipartForm(10 << 20) // 10MB + + mimeType := r.Form.Get("mimetype") + ext := "" + var datard io.Reader + text := r.Form.Get("text") + if strings.TrimSpace(text) == "" { + file, _, err := r.FormFile("file") + if err != nil { + log.Print("Error file upload: ", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + defer file.Close() + if mimeType == "auto" { + mimeType, ext, datard, err = detectMimetype(file) + if err != nil { + log.Printf("Failed to detect mimetype. defaulting '%s': %s", DefaultMimeType, err) + mimeType = DefaultMimeType + } + } else { + datard = file + } + } else { + datard = strings.NewReader(text) + if mimeType == "auto" { + mimeType = "text/plain" + } + } + + f, name, err := s.createRandomFilename(ext) + if err != nil { + log.Print("createRandomFilename(): ", err) w.WriteHeader(http.StatusInternalServerError) - return } + defer f.Sync() defer f.Close() - if _, err = io.Copy(f, r.Body); err != nil { + if _, err := io.Copy(f, datard); err != nil { log.Print("io.Copy(): ", err) w.WriteHeader(http.StatusInternalServerError) return } m := Metadata{ - ContentType: r.Header.Get("content-type"), + ContentType: mimeType, } - if err = writeMetadata(filename+MetadataExt, &m); err != nil { + if err = s.writeMetadata(name, &m); err != nil { log.Print("writeMetadata(): ", err) w.WriteHeader(http.StatusInternalServerError) return } - w.WriteHeader(http.StatusCreated) - fmt.Fprintf(w, "/%s\n", name) + log.Printf("Created posting: %s", name) + w.Header().Add("location", fmt.Sprintf("/%s\n", name)) + w.WriteHeader(http.StatusFound) } -func serveAnsiHtml(w http.ResponseWriter, filename string) { - f, err := os.Open(filename) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - w.Header().Add("content-type", "text/html") - data, err := io.ReadAll(f) - if err != nil { +func (s *Server) formGet(w http.ResponseWriter, r *http.Request) { + f, _ := static.Open("post.html") + if _, err := io.Copy(w, f); err != nil { w.WriteHeader(http.StatusInternalServerError) - return } - data = ansihtml.ConvertToHTML(data) - data = []byte(fmt.Sprintf("<pre>%s</pre>", strings.ReplaceAll(string(data), "\n", "<br>"))) - w.Write(data) } -func (s *Server) Get(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path - if path == "/" { - w.Header().Add("content-type", "text/plain; charset=UTF-8") - w.Write([]byte("Bin-ки и глогинки")) - return - } - filename := filepath.Join(s.data, path) +func (s *Server) servePosting(w http.ResponseWriter, r *http.Request, name string) { + filename := filepath.Join(s.data, name) m, err := readMetadata(filename + MetadataExt) if err != nil { log.Print("readMetadata(): ", err) @@ -131,12 +185,69 @@ func (s *Server) Get(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, filename) } -func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (s *Server) indexPost(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + return + } + var rd io.Reader + mimeType := r.Header.Get("content-type") + ext := "" + if mimeType == "" { + mimeType, ext, rd, _ = detectMimetype(r.Body) + } + f, name, err := s.createRandomFilename(ext) + if err != nil { + log.Print("createRandomFilename(): ", err) + w.WriteHeader(http.StatusInternalServerError) + } + defer f.Sync() + defer f.Close() + if _, err = io.Copy(f, rd); err != nil { + log.Print("io.Copy(): ", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + m := Metadata{ + ContentType: mimeType, + } + if err = s.writeMetadata(name, &m); err != nil { + log.Print("writeMetadata(): ", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + log.Printf("Created posting: %s", name) + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, "/%s\n", name) +} + +func (s *Server) indexGet(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + if path == "/" { + w.Header().Add("content-type", "text/plain; charset=UTF-8") + w.Write([]byte("Bin-ки и глогинки.")) + } else { + s.servePosting(w, r, filepath.Base(path)) + } +} + +func (s *Server) Post(w http.ResponseWriter, r *http.Request) { switch r.Method { - case "POST": - s.Post(w, r) - case "GET": - s.Get(w, r) + case http.MethodGet: + s.formGet(w, r) + case http.MethodPost: + s.formPost(w, r) + default: + } +} + +func (s *Server) Index(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + s.indexGet(w, r) + case http.MethodPost: + s.indexPost(w, r) + default: + w.WriteHeader(http.StatusMethodNotAllowed) } } @@ -156,8 +267,11 @@ func main() { s := &Server{ data: datadir, } + mux := http.NewServeMux() + mux.HandleFunc("/post", s.Post) + mux.HandleFunc("/", s.Index) log.Printf("Listening at '%s'...", addr) - if err := http.ListenAndServe(addr, s); err != nil { + if err := http.ListenAndServe(addr, mux); err != nil { log.Fatal(err) } } diff --git a/post.html b/post.html new file mode 100644 index 0000000..3ed8f1b --- /dev/null +++ b/post.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <meta http-equiv="X-UA-Compatible" content="ie=edge" /> + <title>Gloginki</title> + </head> + <body> + <form + enctype="multipart/form-data" + action="/post" + method="post" + > + <fieldset> + <legend>Text Posting</legend> + <textarea name="text" style="width:100%;min-height:300px;"></textarea> + </fieldset> + <fieldset> + <legend>File Upload</legend> + <input type="file" name="file" /> + </fieldset> + <fieldset> + <legend>Submit</legend> + <label for="mimetype">Mime-type:</label> + <input type="text" name="mimetype" value="auto" /> + <input type="submit" value="Post" /> + </fieldset> + </form> + </body> +</html>
\ No newline at end of file |
