package main
import (
"bytes"
"crypto/rand"
"embed"
"encoding/base64"
"encoding/json"
"fmt"
"html/template"
"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
host string
}
type Metadata struct {
ContentType string `json:"contentType"`
}
//go:embed form.html result.html
var static embed.FS
var tmpl = template.Must(template.ParseFS(static, "*.html"))
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) createFile(rd io.Reader, ext string, mimeType string) (string, error) {
f, name, err := s.createRandomFilename(ext)
if err != nil {
return "", fmt.Errorf("createRandomFilename(): %w", err)
}
defer f.Sync()
defer f.Close()
if _, err := io.Copy(f, rd); err != nil {
return "", fmt.Errorf("io.Copy(): %w", err)
}
m := Metadata{
ContentType: mimeType,
}
if err = s.writeMetadata(name, &m); err != nil {
return "", fmt.Errorf("writeMetadata(): %w", err)
}
return name, nil
}
func (s *Server) postText(r *http.Request) (string, error) {
var ext string
text := r.Form.Get("text")
if text == "" {
return "", nil
}
mimeType := r.Form.Get("text_mimetype")
rd := io.Reader(strings.NewReader(text))
if mimeType == "text/plain" {
ext = ".txt"
} else if mimeType == "auto" {
var err error
mimeType, ext, rd, err = detectMimetype(rd)
if err != nil {
mimeType = DefaultMimeType
}
}
return s.createFile(rd, ext, mimeType)
}
func (s *Server) postFile(r *http.Request) (string, error) {
var ext string
var rd io.Reader
mimeType := r.Form.Get("file_mimetype")
file, _, err := r.FormFile("file")
if err != nil {
return "", err
}
defer file.Close()
if mimeType == "auto" {
mimeType, ext, rd, err = detectMimetype(file)
if err != nil {
log.Printf("Failed to detect mimetype. defaulting '%s': %s", DefaultMimeType, err)
mimeType = DefaultMimeType
}
} else {
rd = file
}
return s.createFile(rd, ext, mimeType)
}
func (s *Server) formPost(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(10 << 23) // 83MB
var textLink string
var fileLink string
textName, err := s.postText(r)
if err != nil {
log.Print("Error text upload: ", err)
} else if textName != "" {
log.Printf("Created posting: %s", textName)
textLink = fmt.Sprintf("%s/%s", s.host, textName)
}
fileName, err := s.postFile(r)
if err != nil {
log.Print("Error file upload: ", err)
} else if fileName != "" {
log.Printf("Created posting: %s", fileName)
fileLink = fmt.Sprintf("%s/%s", s.host, fileName)
}
if fileName == "" && textName == "" {
w.WriteHeader(http.StatusInternalServerError)
return
}
tmplData := map[string]string{
"TextLink": textLink,
"FileLink": fileLink,
}
if err := tmpl.ExecuteTemplate(w, "result.html", tmplData); 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 "text/x-url":
location, err := os.ReadFile(filename)
if err != nil {
log.Print("read(): ", err)
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Add("location", string(location))
w.WriteHeader(http.StatusFound)
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 {
mime := mimetype.Lookup(mimeType)
if mime != nil {
ext = mime.Extension()
}
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/%s\n", s.host, name)
}
func (s *Server) indexGet(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if path == "/" {
w.Header().Add("content-type", "text/html; charset=UTF-8")
if err := tmpl.ExecuteTemplate(w, "form.html", nil); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
} else {
s.servePosting(w, r, filepath.Base(path))
}
}
func (s *Server) Post(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
s.formPost(w, r)
default:
w.WriteHeader(http.StatusBadRequest)
}
}
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
host 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.StringVarP(&host, "host", "h", "", "The host url, e.g. http://127.0.0.1")
flag.Parse()
datadir, err := filepath.Abs(data)
if err != nil {
log.Fatal(err)
}
s := &Server{
data: datadir,
host: host,
}
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)
}
}