summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/config.go46
-rw-r--r--config_example.json10
-rw-r--r--controller/app.go39
-rw-r--r--controller/controller.go8
-rw-r--r--controller/file.go79
-rw-r--r--controller/index.go13
-rw-r--r--controller/template/404.html7
-rw-r--r--controller/template/base.html49
-rw-r--r--controller/template/index.html28
-rw-r--r--controller/template/uploaded.html10
-rw-r--r--go.mod3
-rw-r--r--main.go66
-rw-r--r--storage/file.go21
-rw-r--r--storage/storage.go57
14 files changed, 436 insertions, 0 deletions
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000..691e756
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,46 @@
+package config
+
+import (
+ "encoding/json"
+ "io"
+ "os"
+)
+
+type Configuration struct {
+ Http struct {
+ ListenAddress string `json:"listen"`
+ BaseAddress string `json:"base"`
+ } `json:"http"`
+ Authentication struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ } `json:"auth"`
+}
+
+func Must(config Configuration, err error) Configuration {
+ if err != nil {
+ panic(err)
+ }
+
+ return config
+}
+
+func Open(filename string) (Configuration, error) {
+ file, err := os.Open(filename)
+ if err != nil {
+ return Configuration{}, err
+ }
+
+ buf, err := io.ReadAll(file)
+ if err != nil {
+ return Configuration{}, err
+ }
+
+ config := Configuration{}
+
+ if err := json.Unmarshal(buf, &config); err != nil {
+ return Configuration{}, err
+ } else {
+ return config, nil
+ }
+}
diff --git a/config_example.json b/config_example.json
new file mode 100644
index 0000000..99eff96
--- /dev/null
+++ b/config_example.json
@@ -0,0 +1,10 @@
+{
+ "http": {
+ "listen": "127.0.0.1:8080",
+ "base": "http://127.0.0.1:8080"
+ },
+ "auth": {
+ "username": "jwolff",
+ "password": "changeme"
+ }
+}
diff --git a/controller/app.go b/controller/app.go
new file mode 100644
index 0000000..f145ce2
--- /dev/null
+++ b/controller/app.go
@@ -0,0 +1,39 @@
+package controller
+
+import (
+ "embed"
+ "html/template"
+ "net/http"
+
+ "drop.janw.name/config"
+ "drop.janw.name/storage"
+)
+
+//go:embed template/*
+var templateFs embed.FS
+
+type App struct {
+ storage *storage.Storage
+ tmpl *template.Template
+ config config.Configuration
+}
+
+func NewApp(configFilename string) *App {
+ return &App{
+ storage: storage.NewStorage(),
+ tmpl: template.Must(template.ParseFS(templateFs, "template/*.html")),
+ config: config.Must(config.Open(configFilename)),
+ }
+}
+
+func (app App) requireAuth(res http.ResponseWriter, req *http.Request) bool {
+ username, password, ok := req.BasicAuth()
+ if ok && username == app.config.Authentication.Username && password == app.config.Authentication.Password {
+ return true
+ }
+
+ res.Header().Add("WWW-Authenticate", "Basic realm=\"Authentication Required\", charset=\"UTF-8\"")
+ res.WriteHeader(http.StatusUnauthorized)
+
+ return false
+}
diff --git a/controller/controller.go b/controller/controller.go
new file mode 100644
index 0000000..1f28562
--- /dev/null
+++ b/controller/controller.go
@@ -0,0 +1,8 @@
+package controller
+
+import "net/http"
+
+type Controller struct {
+ GET http.HandlerFunc
+ POST http.HandlerFunc
+}
diff --git a/controller/file.go b/controller/file.go
new file mode 100644
index 0000000..53121d5
--- /dev/null
+++ b/controller/file.go
@@ -0,0 +1,79 @@
+package controller
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+
+ "drop.janw.name/storage"
+)
+
+const MAX_FILESIZE = (1 << 19) * 100
+
+func (app App) FilePost(res http.ResponseWriter, req *http.Request) {
+ if !app.requireAuth(res, req) {
+ return
+ }
+
+ if err := req.ParseMultipartForm(MAX_FILESIZE); err != nil {
+ panic(err)
+ }
+
+ formFile, formFileHeader, err := req.FormFile("file")
+ if err != nil {
+ panic(err)
+ }
+
+ protected := req.FormValue("protect") == "on"
+
+ fileData, err := io.ReadAll(formFile)
+ if err != nil {
+ panic(err)
+ }
+
+ file := storage.File{
+ Protected: protected,
+ Filename: formFileHeader.Filename,
+ Data: fileData,
+ AvailableUntil: time.Now().Add(time.Hour * 2),
+ }
+
+ key, err := app.storage.Put(file)
+ if err != nil {
+ panic(err)
+ }
+
+ app.tmpl.ExecuteTemplate(res, "uploaded", struct {
+ DownloadURL string
+ }{
+ DownloadURL: fmt.Sprintf("%s/%s", app.config.Http.BaseAddress, key),
+ })
+}
+
+func (app App) FileGet(res http.ResponseWriter, req *http.Request) {
+ fileId := req.PathValue("file")
+
+ file, _ := app.storage.Get(fileId)
+
+ if file == nil || !file.IsAvailable() {
+ if !app.requireAuth(res, req) {
+ return
+ }
+
+ res.WriteHeader(http.StatusNotFound)
+ app.tmpl.ExecuteTemplate(res, "404", nil)
+ return
+ }
+
+ if file.Protected && !app.requireAuth(res, req) {
+ return
+ }
+
+ res.Header().Add("Content-Type", "application/octet-stream")
+ res.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", file.Filename))
+
+ if _, err := res.Write(file.Data); err != nil {
+ panic(err)
+ }
+}
diff --git a/controller/index.go b/controller/index.go
new file mode 100644
index 0000000..6f927ed
--- /dev/null
+++ b/controller/index.go
@@ -0,0 +1,13 @@
+package controller
+
+import (
+ "net/http"
+)
+
+func (app App) Index(res http.ResponseWriter, req *http.Request) {
+ if !app.requireAuth(res, req) {
+ return
+ }
+
+ app.tmpl.ExecuteTemplate(res, "index", nil)
+}
diff --git a/controller/template/404.html b/controller/template/404.html
new file mode 100644
index 0000000..fa19547
--- /dev/null
+++ b/controller/template/404.html
@@ -0,0 +1,7 @@
+{{define "404"}}
+{{template "header" .}}
+<main>
+ <h1>no such file</h1>
+</main>
+{{template "footer" .}}
+{{end}}
diff --git a/controller/template/base.html b/controller/template/base.html
new file mode 100644
index 0000000..c91245d
--- /dev/null
+++ b/controller/template/base.html
@@ -0,0 +1,49 @@
+{{define "header"}}
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>drop.janw.name</title>
+ <meta charset="UTF-8">
+ <meta name="author" content="Jan Wolff">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+ <link href="data:," rel="icon">
+ <style>
+ html, body {
+ padding: 0;
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ }
+
+ body {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: #ebebeb;
+ font-family: sans-serif;
+ }
+
+ main {
+ padding: 2rem;
+ border-radius: 2rem;
+ background: #fefefe;
+ }
+
+ form {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ progress {
+ width: 100%;
+ }
+ </style>
+ </head>
+ <body>
+{{end}}
+
+{{define "footer"}}
+ </body>
+</html>
+{{end}}
diff --git a/controller/template/index.html b/controller/template/index.html
new file mode 100644
index 0000000..f50be97
--- /dev/null
+++ b/controller/template/index.html
@@ -0,0 +1,28 @@
+{{define "index"}}
+{{template "header" .}}
+<main>
+ <h1>drop.janw.name</h1>
+ <form method="POST" action="/" enctype="multipart/form-data">
+ <div>
+ <label for="file">
+ File:
+ </label>
+ <input id="file" type="file" name="file">
+ </div>
+ <div>
+ <label for="protect">
+ Password Protect:
+ </label>
+ <input id="protect" type="checkbox" name="protect">
+ </div>
+ <div>
+ <label for="until">
+ Available Until:
+ </label>
+ <input id="until" type="datetime-local" name="until">
+ </div>
+ <input type="submit" id="upload" value="Upload">
+ </form>
+</main>
+{{template "footer" .}}
+{{end}}
diff --git a/controller/template/uploaded.html b/controller/template/uploaded.html
new file mode 100644
index 0000000..1324082
--- /dev/null
+++ b/controller/template/uploaded.html
@@ -0,0 +1,10 @@
+{{define "uploaded"}}
+{{template "header" .}}
+<main>
+ <h1>File uploaded successfully</h1>
+ <p>
+ <a href="{{.DownloadURL}}">{{.DownloadURL}}</a>
+ </p>
+</main>
+{{template "footer" .}}
+{{end}}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..031d11d
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module drop.janw.name
+
+go 1.24.7
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..3800de3
--- /dev/null
+++ b/main.go
@@ -0,0 +1,66 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "net/http"
+ "runtime/debug"
+
+ "drop.janw.name/controller"
+)
+
+var CONFIG_FILENAME string
+
+func registerHandler(pattern string, controller controller.Controller) {
+ http.HandleFunc(pattern, func(res http.ResponseWriter, req *http.Request) {
+ defer func() {
+ err := recover()
+ if err != nil {
+ res.WriteHeader(http.StatusInternalServerError)
+ fmt.Fprintln(res, "internal server error")
+ fmt.Fprintln(res, err)
+ fmt.Fprintln(res, string(debug.Stack()))
+ }
+ }()
+
+ var methodFunc http.HandlerFunc
+
+ switch req.Method {
+ case http.MethodGet:
+ methodFunc = controller.GET
+ case http.MethodPost:
+ methodFunc = controller.POST
+ }
+
+ log.Println(req.Method, req.RemoteAddr, req.RequestURI)
+
+ if methodFunc != nil {
+ methodFunc(res, req)
+ } else {
+ res.WriteHeader(http.StatusMethodNotAllowed)
+ fmt.Fprintln(res, "method not allowed")
+ }
+ })
+}
+
+func init() {
+ flag.StringVar(&CONFIG_FILENAME, "c", "config_example.json", "path to the configuration file")
+ flag.Parse()
+}
+
+func main() {
+ app := controller.NewApp(CONFIG_FILENAME)
+
+ registerHandler("/", controller.Controller{
+ GET: app.Index,
+ POST: app.FilePost,
+ })
+
+ registerHandler("/{file}", controller.Controller{
+ GET: app.FileGet,
+ })
+
+ log.Println(fmt.Sprintf("serving on http://%s\n", "127.0.0.1:8080"))
+ log.Panicln(http.ListenAndServe("127.0.0.1:8080", nil))
+}
diff --git a/storage/file.go b/storage/file.go
new file mode 100644
index 0000000..6fa5070
--- /dev/null
+++ b/storage/file.go
@@ -0,0 +1,21 @@
+package storage
+
+import (
+ "bytes"
+ "time"
+)
+
+type File struct {
+ Protected bool
+ Filename string
+ Data []byte
+ AvailableUntil time.Time
+}
+
+func (f File) IsAvailable() bool {
+ return time.Now().Before(f.AvailableUntil)
+}
+
+func (f File) Reader() *bytes.Reader {
+ return bytes.NewReader(f.Data)
+}
diff --git a/storage/storage.go b/storage/storage.go
new file mode 100644
index 0000000..6dc4a37
--- /dev/null
+++ b/storage/storage.go
@@ -0,0 +1,57 @@
+package storage
+
+import (
+ "crypto/rand"
+ "errors"
+ "math/big"
+ "strings"
+)
+
+var ErrNoSuchFile = errors.New("no such file")
+
+type Storage struct {
+ files map[string]File
+}
+
+func NewStorage() *Storage {
+ return &Storage{
+ files: make(map[string]File),
+ }
+}
+
+func generateKey(length uint) (string, error) {
+ key := make([]string, 0, 0)
+ alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789abcdefghkmnpqrstuvwxyz"
+
+ for i := uint(0); i < length; i++ {
+ n, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphabet))))
+
+ if err != nil {
+ return "", err
+ }
+
+ key = append(key, string(alphabet[n.Int64()]))
+ }
+
+ return strings.Join(key, ""), nil
+}
+
+func (s *Storage) Get(key string) (*File, error) {
+ if file, ok := s.files[key]; ok {
+ return &file, nil
+ } else {
+ return nil, ErrNoSuchFile
+ }
+}
+
+func (s *Storage) Put(file File) (string, error) {
+ key, err := generateKey(8)
+
+ if err != nil {
+ return "", err
+ }
+
+ s.files[key] = file
+
+ return key, nil
+}