package main
import (
"bytes"
"crypto/rand"
"embed"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gabriel-vasile/mimetype"
"github.com/robert-nix/ansihtml"
flag "github.com/spf13/pflag"
)
const (
MetadataExt = ".metadata"
DefaultMimeType = "application/octet-stream"
)
type Server struct {
data string
}
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("
%s
", strings.ReplaceAll(string(data), "\n", "
")))
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 {
return err
}
j := json.NewEncoder(f)
return j.Encode(m)
}
func readMetadata(filename string) (*Metadata, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
var m Metadata
j := json.NewDecoder(f)
if err := j.Decode(&m); err != nil {
return nil, err
}
return &m, nil
}
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 {
return nil, "", err
}
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 {
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)
}
defer f.Sync()
defer f.Close()
if _, err := io.Copy(f, datard); 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.Header().Add("location", fmt.Sprintf("/%s\n", name))
w.WriteHeader(http.StatusFound)
}
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)
}
}
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)
w.WriteHeader(http.StatusNotFound)
return
}
h := w.Header()
switch m.ContentType {
case "text/x-ansi":
serveAnsiHtml(w, filename)
return
case "":
m.ContentType = "application/octet-stream"
default:
}
h.Add("content-type", m.ContentType)
http.ServeFile(w, r, filename)
}
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)
} else {
rd = 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 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)
}
}
func main() {
var (
addr string
data string
)
flag.StringVarP(&addr, "addr", "a", "0.0.0.0:9000", "The address to bind to")
flag.StringVarP(&data, "data-path", "d", "./data", "The path to the data")
flag.Parse()
datadir, err := filepath.Abs(data)
if err != nil {
log.Fatal(err)
}
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, mux); err != nil {
log.Fatal(err)
}
}