summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarin Ivanov <[email protected]>2025-06-13 16:09:50 +0300
committerMarin Ivanov <[email protected]>2025-06-13 16:09:50 +0300
commit3cb88faf5d952596b1efa89833fe6e7415be0513 (patch)
treed7ba66d7cf9a2889fc8dd128e843199e19fa9bec
parent3255b4b862bfc52e62ebb92e94dcf71261aeffb6 (diff)
v2
-rw-r--r--go.mod3
-rw-r--r--go.sum4
-rw-r--r--main.go200
-rw-r--r--post.html31
4 files changed, 195 insertions, 43 deletions
diff --git a/go.mod b/go.mod
index 8786350..174a249 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index 655ea97..00daa1d 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/main.go b/main.go
index 315f0d2..63d7b4f 100644
--- a/main.go
+++ b/main.go
@@ -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