Index: trunk/COPYING
===================================================================
--- trunk/COPYING	(revision 65)
+++ trunk/COPYING	(revision 65)
@@ -0,0 +1,14 @@
+Copyright (c) 2021 Willy Goiffon <contact@z3bra.org>
+Copyright (c) 2023-present Izuru Yakumo <postmaster@chaotic.ninja>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
Index: trunk/LICENSE
===================================================================
--- trunk/LICENSE	(revision 64)
+++ trunk/LICENSE	(revision 65)
@@ -1,13 +1,1 @@
-Copyright (c) 2021 Willy Goiffon <contact@z3bra.org>
-
-Permission to use, copy, modify, and/or distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
-OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-PERFORMANCE OF THIS SOFTWARE.
+link COPYING
Index: trunk/Makefile
===================================================================
--- trunk/Makefile	(revision 65)
+++ trunk/Makefile	(revision 65)
@@ -0,0 +1,25 @@
+GO ?= go
+GOFLAGS ?= -v -ldflags "-w -X `go list`.Version=${VERSION} -X `go list`.Commit=${COMMIT} -X `go list`.Build=${BUILD}"
+CGO ?= 0
+
+VERSION = `git describe --abbrev=0 --tags 2>/dev/null || echo "$VERSION"`
+COMMIT = `git rev-parse --short HEAD || echo "$COMMIT"`
+BRANCH = `git rev-parse --abbrev-ref HEAD`
+BUILD = `git show -s --pretty=format:%cI`
+
+PREFIX ?= /usr/local
+
+all: marisa marisa-trash
+
+marisa:
+	CGO_ENABLED=${CGO} go build ${GOFLAGS} ./cmd/marisa
+marisa-trash:
+	CGO_ENABLED=${CGO} go build ${GOFLAGS} ./cmd/marisa-trash
+clean:
+	rm -f marisa marisa-trash
+install:
+	install -Dm0755 marisa ${PREFIX}/bin/marisa
+	install -Dm0755 marisa-trash ${PREFIX}/bin/marisa-trash
+	install -Dm0644 marisa.1 ${PREFIX}/share/man/man1/marisa.1
+	install -Dm0644 marisa.conf.5 ${PREFIX}/share/man/man5/marisa.conf.5
+.PHONY: marisa marisa-trash
Index: trunk/README
===================================================================
--- trunk/README	(revision 64)
+++ 	(revision )
@@ -1,38 +1,0 @@
-partage
-=======
-HTTP based File upload system.
-
-![screenshot](https://z3bra.org/partage/screenshot.png)
-
-Features
---------
-+ Link expiration
-+ Mimetype support
-+ Random filenames
-+ Multiple file uploads
-+ Javascript not needed
-+ Privilege drop
-+ chroot(2) support
-+ FastCGI support
-
-Usage
------
-Refer to the partage(1) manual page for details and examples.
-
-	partage [-v] [-f partage.conf]
-
-Configuration is done through its configuration file, partage.conf(5).
-The format is that of the INI file format.
-
-Uploading files is done via PUT and POST requests. Multiple files can
-be sent via POST requests.
-
-	curl -T file.png http://domain.tld
-	curl -F file=file.png -F expiry=3600 http://domain.tld
-
-Installation
-------------
-Edit the `config.mk` file to match your setup, then run the following:
-
-        $ mk
-        # mk install
Index: trunk/README.md
===================================================================
--- trunk/README.md	(revision 65)
+++ trunk/README.md	(revision 65)
@@ -0,0 +1,36 @@
+marisa
+======
+HTTP based File upload system.
+
+Features
+--------
++ Link expiration
++ Mimetype support
++ Random filenames
++ Multiple file uploads
++ Javascript not needed
++ Privilege drop
++ chroot(2) support
++ FastCGI support
+
+Usage
+-----
+Refer to the marisa(1) manual page for details and examples.
+
+	marisa [-v] [-f marisa.conf]
+
+Configuration is done through its configuration file, marisa.conf(5).
+The format is that of the INI file format.
+
+Uploading files is done via PUT and POST requests. Multiple files can
+be sent via POST requests.
+
+	curl -T file.png http://domain.tld
+	curl -F file=file.png -F expiry=3600 http://domain.tld
+
+Installation
+------------
+Edit the `config.mk` file to match your setup, then run the following:
+
+        $ (b)make
+        # (b)make install
Index: trunk/cmd/marisa-trash/main.go
===================================================================
--- trunk/cmd/marisa-trash/main.go	(revision 65)
+++ trunk/cmd/marisa-trash/main.go	(revision 65)
@@ -0,0 +1,101 @@
+package main
+
+import (
+	"log"
+	"flag"
+	"os"
+	"time"
+	"path/filepath"
+	"encoding/json"
+	
+	"github.com/dustin/go-humanize"
+)
+
+type metadata struct {
+	Filename string
+	Size int64
+	Expiry int64
+}
+
+var conf struct {
+	filepath string
+	metapath string
+}
+
+var verbose bool
+var count int64
+var deleted int64
+var size int64
+
+func readmeta(filename string) (metadata, error) {
+	j, err := os.ReadFile(filename)
+	if err != nil {
+		return metadata{}, err
+	}
+
+	var meta metadata
+	err = json.Unmarshal(j, &meta)
+	if err != nil {
+		return metadata{}, err
+	}
+
+	return meta, nil
+}
+
+func checkexpiry(path string, info os.FileInfo, err error) error {
+	if filepath.Ext(path) != ".json" {
+		return nil
+	}
+	meta, err := readmeta(path)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+
+	count++
+
+	now := time.Now().Unix()
+	if verbose {
+		log.Printf("now: %s, expiry: %s\n", now, meta.Expiry);
+	}
+
+	if meta.Expiry > 0 && now >= meta.Expiry {
+		if verbose {
+			expiration :=  humanize.Time(time.Unix(meta.Expiry, 0))
+			log.Printf("%s/%s: expired %s\n", conf.filepath, meta.Filename, expiration)
+		}
+		if err = os.Remove(conf.filepath + "/" + meta.Filename); err != nil {
+			log.Fatal(err)
+		}
+		if err = os.Remove(path); err != nil {
+			log.Fatal(err)
+		}
+		deleted++
+		return nil
+	} else {
+		if verbose {
+			expiration :=  humanize.Time(time.Unix(meta.Expiry, 0))
+			log.Printf("%s/%s: expire in %s\n", conf.filepath, meta.Filename, expiration)
+		}
+		size += meta.Size
+	}
+
+	return nil
+}
+
+func main() {
+	flag.BoolVar(&verbose,         "v", false, "Verbose logging")
+	flag.StringVar(&conf.filepath, "f", "./files", "Directory containing files")
+	flag.StringVar(&conf.metapath, "m", "./meta", "Directory containing metadata")
+
+	flag.Parse()
+
+	err := filepath.Walk(conf.metapath, checkexpiry)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	if verbose && count > 0 {
+		log.Printf("%d/%d file(s) deleted (remaining: %s)", deleted, count, humanize.IBytes(uint64(size)))
+	}
+}
Index: trunk/cmd/marisa/main.go
===================================================================
--- trunk/cmd/marisa/main.go	(revision 65)
+++ trunk/cmd/marisa/main.go	(revision 65)
@@ -0,0 +1,423 @@
+package main
+
+import (
+	"encoding/json"
+	"flag"
+	"fmt"
+	"html/template"
+	"io"
+	"io/ioutil"
+	"log"
+	"net"
+	"net/http"
+	"net/http/fcgi"
+	"os"
+	"os/signal"
+	"os/user"
+	"path"
+	"path/filepath"
+	"strconv"
+	"syscall"
+	"time"
+
+	"github.com/dustin/go-humanize"
+	"gopkg.in/ini.v1"
+	"marisa.chaotic.ninja/marisa"
+)
+
+type templatedata struct {
+	Links   []string
+	Size    string
+	Maxsize string
+}
+
+type metadata struct {
+	Filename string
+	Size     int64
+	Expiry   int64
+}
+
+var conf struct {
+	user     string
+	group    string
+	chroot   string
+	listen   string
+	baseuri  string
+	rootdir  string
+	tmplpath string
+	filepath string
+	metapath string
+	filectx  string
+	maxsize  int64
+	expiry   int64
+}
+
+var verbose bool
+
+func writefile(f *os.File, s io.ReadCloser, contentlength int64) error {
+	buffer := make([]byte, 4096)
+	eof := false
+	sz := int64(0)
+
+	defer f.Sync()
+
+	for !eof {
+		n, err := s.Read(buffer)
+		if err != nil && err != io.EOF {
+			return err
+		} else if err == io.EOF {
+			eof = true
+		}
+
+		/* ensure we don't write more than expected */
+		r := int64(n)
+		if sz+r > contentlength {
+			r = contentlength - sz
+			eof = true
+		}
+
+		_, err = f.Write(buffer[:r])
+		if err != nil {
+			return err
+		}
+		sz += r
+	}
+
+	return nil
+}
+
+func writemeta(filename string, expiry int64) error {
+
+	f, _ := os.Open(filename)
+	stat, _ := f.Stat()
+	size := stat.Size()
+	f.Close()
+
+	if expiry < 0 {
+		expiry = conf.expiry
+	}
+
+	meta := metadata{
+		Filename: filepath.Base(filename),
+		Size:     size,
+		Expiry:   time.Now().Unix() + expiry,
+	}
+
+	if verbose {
+		log.Printf("Saving metadata for %s in %s", meta.Filename, conf.metapath+"/"+meta.Filename+".json")
+	}
+
+	f, err := os.Create(conf.metapath + "/" + meta.Filename + ".json")
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	j, err := json.Marshal(meta)
+	if err != nil {
+		return err
+	}
+
+	_, err = f.Write(j)
+
+	return err
+}
+
+func servetemplate(w http.ResponseWriter, f string, d templatedata) {
+	t, err := template.ParseFiles(conf.tmplpath + "/" + f)
+	if err != nil {
+		http.Error(w, "Internal error", http.StatusInternalServerError)
+		return
+	}
+
+	if verbose {
+		log.Printf("Serving template %s", t.Name())
+	}
+
+	err = t.Execute(w, d)
+	if err != nil {
+		fmt.Println(err)
+	}
+}
+
+func uploaderPut(w http.ResponseWriter, r *http.Request) {
+	/* limit upload size */
+	if r.ContentLength > conf.maxsize {
+		http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
+	}
+
+	tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(r.URL.Path))
+	f, err := os.Create(tmp.Name())
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	defer f.Close()
+
+	if verbose {
+		log.Printf("Writing %d bytes to %s", r.ContentLength, tmp.Name())
+	}
+
+	if err = writefile(f, r.Body, r.ContentLength); err != nil {
+		http.Error(w, "Internal error", http.StatusInternalServerError)
+		defer os.Remove(tmp.Name())
+		return
+	}
+	writemeta(tmp.Name(), conf.expiry)
+
+	resp := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
+	w.Write([]byte(resp + "\r\n"))
+}
+
+func uploaderPost(w http.ResponseWriter, r *http.Request) {
+	/* read 32Mb at a time */
+	r.ParseMultipartForm(32 << 20)
+
+	links := []string{}
+	for _, h := range r.MultipartForm.File["file"] {
+		if h.Size > conf.maxsize {
+			http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
+			return
+		}
+
+		post, err := h.Open()
+		if err != nil {
+			http.Error(w, "Internal error", http.StatusInternalServerError)
+			return
+		}
+		defer post.Close()
+
+		tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(h.Filename))
+		f, err := os.Create(tmp.Name())
+		if err != nil {
+			http.Error(w, "Internal error", http.StatusInternalServerError)
+			return
+		}
+		defer f.Close()
+
+		if err = writefile(f, post, h.Size); err != nil {
+			http.Error(w, "Internal error", http.StatusInternalServerError)
+			defer os.Remove(tmp.Name())
+			return
+		}
+
+		expiry, err := strconv.Atoi(r.PostFormValue("expiry"))
+		if err != nil || expiry < 0 {
+			expiry = int(conf.expiry)
+		}
+		writemeta(tmp.Name(), int64(expiry))
+
+		link := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
+		links = append(links, link)
+	}
+
+	switch r.PostFormValue("output") {
+	case "html":
+		data := templatedata{
+			Maxsize: humanize.IBytes(uint64(conf.maxsize)),
+			Links:   links,
+		}
+		servetemplate(w, "/index.html", data)
+	case "json":
+		data, _ := json.Marshal(links)
+		w.Write(data)
+	default:
+		for _, link := range links {
+			w.Write([]byte(link + "\r\n"))
+		}
+	}
+}
+
+func uploaderGet(w http.ResponseWriter, r *http.Request) {
+	// r.URL.Path is sanitized regarding "." and ".."
+	filename := r.URL.Path
+	if r.URL.Path == "/" || r.URL.Path == "/index.html" {
+		data := templatedata{Maxsize: humanize.IBytes(uint64(conf.maxsize))}
+		servetemplate(w, "/index.html", data)
+		return
+	}
+
+	if verbose {
+		log.Printf("Serving file %s", conf.rootdir+filename)
+	}
+
+	http.ServeFile(w, r, conf.rootdir+filename)
+}
+
+func uploaderDelete(w http.ResponseWriter, r *http.Request) {
+	// r.URL.Path is sanitized regarding "." and ".."
+	filename := r.URL.Path
+	filepath := conf.filepath + filename
+
+	if verbose {
+		log.Printf("Deleting file %s", filepath)
+	}
+
+	f, err := os.Open(filepath)
+	if err != nil {
+		http.NotFound(w, r)
+		return
+	}
+	f.Close()
+
+	// Force file expiration
+	writemeta(filepath, 0)
+	w.WriteHeader(http.StatusNoContent)
+}
+
+func uploader(w http.ResponseWriter, r *http.Request) {
+	if verbose {
+		log.Printf("%s: <%s> %s %s %s", r.Host, r.RemoteAddr, r.Method, r.RequestURI, r.Proto)
+	}
+
+	switch r.Method {
+	case "DELETE":
+		uploaderDelete(w, r)
+	case "POST":
+		uploaderPost(w, r)
+	case "PUT":
+		uploaderPut(w, r)
+	case "GET":
+		uploaderGet(w, r)
+	}
+}
+
+func parseconfig(file string) error {
+	cfg, err := ini.Load(file)
+	if err != nil {
+		return err
+	}
+
+	conf.listen = cfg.Section("").Key("listen").String()
+	conf.user = cfg.Section("").Key("user").String()
+	conf.group = cfg.Section("").Key("group").String()
+	conf.baseuri = cfg.Section("").Key("baseuri").String()
+	conf.filepath = cfg.Section("").Key("filepath").String()
+	conf.metapath = cfg.Section("").Key("metapath").String()
+	conf.filectx = cfg.Section("").Key("filectx").String()
+	conf.rootdir = cfg.Section("").Key("rootdir").String()
+	conf.chroot = cfg.Section("").Key("chroot").String()
+	conf.tmplpath = cfg.Section("").Key("tmplpath").String()
+	conf.maxsize, _ = cfg.Section("").Key("maxsize").Int64()
+	conf.expiry, _ = cfg.Section("").Key("expiry").Int64()
+
+	return nil
+}
+
+func usergroupids(username string, groupname string) (int, int, error) {
+	u, err := user.Lookup(username)
+	if err != nil {
+		return -1, -1, err
+	}
+
+	uid, _ := strconv.Atoi(u.Uid)
+	gid, _ := strconv.Atoi(u.Gid)
+
+	if conf.group != "" {
+		g, err := user.LookupGroup(groupname)
+		if err != nil {
+			return uid, -1, err
+		}
+		gid, _ = strconv.Atoi(g.Gid)
+	}
+
+	return uid, gid, nil
+}
+
+func main() {
+	var err error
+	var configfile string
+	var listener net.Listener
+
+	/* default values */
+	conf.listen = "127.0.0.1:8080"
+	conf.baseuri = "http://127.0.0.1:8080"
+	conf.rootdir = "static"
+	conf.tmplpath = "templates"
+	conf.filepath = "files"
+	conf.metapath = "meta"
+	conf.filectx = "/f/"
+	conf.maxsize = 34359738368
+	conf.expiry = 86400
+
+	flag.StringVar(&configfile, "f", "", "Configuration file")
+	flag.BoolVar(&verbose, "v", false, "Verbose logging")
+	flag.Parse()
+
+	if configfile != "" {
+		if verbose {
+			log.Printf("Reading configuration %s", configfile)
+		}
+		parseconfig(configfile)
+	}
+
+	if conf.chroot != "" {
+		if verbose {
+			log.Printf("Changing root to %s", conf.chroot)
+		}
+		syscall.Chroot(conf.chroot)
+	}
+
+	if conf.listen[0] == '/' {
+		/* Remove any stale socket */
+		os.Remove(conf.listen)
+		if listener, err = net.Listen("unix", conf.listen); err != nil {
+			log.Fatal(err)
+		}
+		defer listener.Close()
+
+		/*
+		 * Ensure unix socket is removed on exit.
+		 * Note: this might not work when dropping privileges…
+		 */
+		defer os.Remove(conf.listen)
+		sigs := make(chan os.Signal, 1)
+		signal.Notify(sigs, os.Interrupt, os.Kill, syscall.SIGTERM)
+		go func() {
+			_ = <-sigs
+			listener.Close()
+			if err = os.Remove(conf.listen); err != nil {
+				log.Fatal(err)
+			}
+			os.Exit(0)
+		}()
+	} else {
+		if listener, err = net.Listen("tcp", conf.listen); err != nil {
+			log.Fatal(err)
+		}
+		defer listener.Close()
+	}
+
+	if conf.user != "" {
+		if verbose {
+			log.Printf("Dropping privileges to %s", conf.user)
+		}
+		uid, gid, err := usergroupids(conf.user, conf.group)
+		if err != nil {
+			log.Fatal(err)
+		}
+
+		if listener.Addr().Network() == "unix" {
+			os.Chown(conf.listen, uid, gid)
+		}
+
+		syscall.Setuid(uid)
+		syscall.Setgid(gid)
+	}
+
+	http.HandleFunc("/", uploader)
+	http.Handle(conf.filectx, http.StripPrefix(conf.filectx, http.FileServer(http.Dir(conf.filepath))))
+
+	if verbose {
+		log.Printf("Starting marisa %v\n", marisa.FullVersion())
+		log.Printf("Listening on %s", conf.listen)
+	}
+
+	if listener.Addr().Network() == "unix" {
+		err = fcgi.Serve(listener, nil)
+		log.Fatal(err) /* NOTREACHED */
+	}
+
+	err = http.Serve(listener, nil)
+	log.Fatal(err) /* NOTREACHED */
+}
Index: trunk/example/marisa.conf
===================================================================
--- trunk/example/marisa.conf	(revision 65)
+++ trunk/example/marisa.conf	(revision 65)
@@ -0,0 +1,36 @@
+# TCP or unix Socket to listen on.
+# When unix sockets are used, the content will be served over FastCGI.
+#listen = /var/run/marisa-fcgi.sock
+listen = 127.0.0.1:9000
+
+# Base to use when constructing URI to files uploaded.
+# The full URI must be specified, in the form SCHEME://HOST[:PORT]
+baseuri = http://127.0.0.1:9000
+
+# Drop privilege to the user and group specified.
+# When only the user is specified, the default group of the user will
+# be used.
+#user = www
+#group = daemon
+
+# Change the root directory to the following directory.
+# When a chroot is set, all path must be given according to the chroot.
+# Note: the configuration file is read before chrooting.
+#chroot = /var/www
+
+# Path to the different path used by the server. Must take into account
+# the chroot if set.
+rootdir = static
+tmplpath = templates
+filepath = files
+metapath = meta
+
+# URI context that files will be served on
+filectx = /f/
+
+# Maximum per-file upload size (in bytes)
+maxsize = 536870912 # 512Mib
+
+# Default expiration time (in seconds). An expiration time of 0 seconds
+# means no expiration.
+expiry = 86400 # 24 hours
Index: trunk/example/partage.conf
===================================================================
--- trunk/example/partage.conf	(revision 64)
+++ 	(revision )
@@ -1,36 +1,0 @@
-# TCP or unix Socket to listen on.
-# When unix sockets are used, the content will be served over FastCGI.
-#listen = /var/run/partage-fcgi.sock
-listen = 127.0.0.1:9000
-
-# Base to use when constructing URI to files uploaded.
-# The full URI must be specified, in the form SCHEME://HOST[:PORT]
-baseuri = http://127.0.0.1:9000
-
-# Drop privilege to the user and group specified.
-# When only the user is specified, the default group of the user will
-# be used.
-#user = www
-#group = daemon
-
-# Change the root directory to the following directory.
-# When a chroot is set, all path must be given according to the chroot.
-# Note: the configuration file is read before chrooting.
-#chroot = /var/www
-
-# Path to the different path used by the server. Must take into account
-# the chroot if set.
-rootdir = example/static
-tmplpath = example/templates
-filepath = example/files
-metapath = example/meta
-
-# URI context that files will be served on
-filectx = /f/
-
-# Maximum per-file upload size (in bytes)
-maxsize = 536870912 # 512Mib
-
-# Default expiration time (in seconds). An expiration time of 0 seconds
-# means no expiration.
-expiry = 86400 # 24 hours
Index: trunk/example/static/marisa_98.css
===================================================================
--- trunk/example/static/marisa_98.css	(revision 65)
+++ trunk/example/static/marisa_98.css	(revision 65)
@@ -0,0 +1,90 @@
+body {
+    padding: 5%;
+    margin: auto;
+    max-width: 540px;
+    font-family: sans-serif;
+    font-size: 1.5rem;
+    text-align: center;
+    background-color: #550000;
+    color: #ff4444;
+}
+
+header {
+    display: flex;
+    flex-direction: column;
+    flex-wrap: wrap-reverse;
+    align-items: center;
+    align-content: center;
+}
+
+section {
+    display: flex;
+    justify-content: flex-end;
+    font-size: initial;
+}
+
+section#formsettings > * {
+    margin-top: 20px;
+    margin-left: 20px;
+}
+
+img#logo {
+    height: 100%;
+    max-height: 30vh;
+}
+
+h1 {
+    font-size: 4.0rem;
+}
+
+#uploads {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+}
+
+#uploads > ul {
+    list-style: none;
+    text-align: left;
+    padding: 0;
+}
+
+#uploads > button {
+    align-self: flex-end;
+    margin-right: 10%;
+}
+
+.dropzone {
+    padding-top: 60px;
+    padding-bottom: 60px;
+    border: 2px dashed #888888;
+    border-radius: 8px;
+    text-align: center;
+    margin: auto;
+    color: #888888;
+}
+
+.dropzone.dragover {
+    color: #222222;
+    border-color: #222222;
+}
+
+/* font attributes are not inherited by default */
+input, input::file-selector-button {
+    text-align: inherit;
+    font-family: inherit;
+    font-size: inherit;
+}
+
+@media (min-aspect-ratio: 18/9) {
+    header {
+	flex-direction: row;
+    }
+    h1 { font-size: 3rem; margin-right: 10px; }
+    img#logo {
+	height: 50%;
+	max-height: 20vh;
+	order: 2;
+    }
+}
Index: trunk/example/static/partage.css
===================================================================
--- trunk/example/static/partage.css	(revision 64)
+++ 	(revision )
@@ -1,106 +1,0 @@
-body {
-	padding: 5%;
-	margin: auto;
-	max-width: 540px;
-	font-family: serif;
-	font-size: 1.5rem;
-	text-align: center;
-	background-color: #eeeeee;
-	color: #222222;
-}
-
-header {
-	display: flex;
-	flex-direction: column;
-	flex-wrap: wrap-reverse;
-	align-items: center;
-	align-content: center;
-}
-
-section {
-	display: flex;
-	justify-content: flex-end;
-	font-size: initial;
-}
-
-section#formsettings > * {
-	margin-top: 20px;
-	margin-left: 20px;
-}
-
-img#logo {
-	height: 100%;
-	max-height: 30vh;
-}
-
-h1 {
-	font-size: 4.0rem;
-}
-
-#uploads {
-	display: flex;
-	flex-direction: column;
-	align-items: center;
-	justify-content: center;
-}
-
-#uploads > ul {
-	list-style: none;
-	text-align: left;
-	padding: 0;
-}
-
-#uploads > button {
-	align-self: flex-end;
-	margin-right: 10%;
-}
-
-.dropzone {
-	padding-top: 60px;
-	padding-bottom: 60px;
-	border: 2px dashed #888888;
-	border-radius: 8px;
-	text-align: center;
-	margin: auto;
-	color: #888888;
-}
-
-.dropzone.dragover {
-	color: #222222;
-	border-color: #222222;
-}
-
-/* font attributes are not inherited by default */
-input, input::file-selector-button {
-	text-align: inherit;
-	font-family: inherit;
-	font-size: inherit;
-}
-
-@media (min-aspect-ratio: 18/9) {
-	header {
-		flex-direction: row;
-	}
-	h1 { font-size: 3rem; margin-right: 10px; }
-	img#logo {
-		height: 50%;
-		max-height: 20vh;
-		order: 2;
-	}
-}
-
-@media (prefers-color-scheme: light) {
-	a { color: black; }
-	body {
-		color: #222222;
-		background-color: #eeeeee;
-	}
-}
-
-@media (prefers-color-scheme: dark) {
-	a { color: white; }
-	body {
-		color: #eeeeee;
-		background-color: #222222;
-	}
-}
Index: trunk/example/templates/index.html
===================================================================
--- trunk/example/templates/index.html	(revision 64)
+++ trunk/example/templates/index.html	(revision 65)
@@ -1,42 +1,42 @@
 <!DOCTYPE HTML>
 <html>
-<head>
-	<meta charset="utf-8">
-	<meta name="author" content="z3bra">
-	<meta name="robots" content="noindex,nofollow" />
-	<meta name="viewport" content="width=device-width">
-	<link rel="stylesheet" type="text/css" href="/partage.css" />
-	<link rel="icon" type="image/ico" href="/favicon.ico" />
-	<title>Partage</title>
-</head>
-<body>
-	<header>
-		<img id="logo" src="/partage.png" />
-		<h1>partage</h1>
-	</header>
-	<form enctype="multipart/form-data" method="post">
-		<div id="dropzone"></div>
-		<div id="fallbackform" class="dropzone">
-			<input id="filebox" name="file" type="file" multiple/>
-			<input id="output" name="output" type="hidden" value='html' />
-			<input type="submit" value="Upload"/>
-		</div>
-		<section id="formsettings">
-			<label for="expiry"> Destroy after </label>
-			<select id="expiry" name="expiry">
-		                <option value="900"> 15 minutes </option>
-		                <option value="3600"> 1 hour </option>
-		                <option value="28800"> 8 hours </option>
-		                <option value="86400" selected> 1 day </option>
-		                <option value="604800"> 1 week </option>
-			</select>
-		</section>
-	</form>
-	<p>File size limited to {{.Maxsize}}.</p>
-	<div id="uploads">{{if .Links}}
-		<ul>
-	        {{range .Links}}<li><a href="{{.}}">{{.}}</a></li>{{end}}
-		</ul>
-        {{end}}</div>
-</body>
+  <head>
+    <meta charset="utf-8">
+    <meta name="author" content="z3bra, Izuru Yakumo">
+    <meta name="robots" content="noindex,nofollow" >
+    <meta name="viewport" content="width=device-width">
+    <link rel="stylesheet" type="text/css" href="/marisa_98.css" >
+    <link rel="icon" href="/marisa.png">
+    <title>Marisa</title>
+  </head>
+  <body>
+    <header>
+      <img id="logo" src="/marisa.png" >
+      <h1>marisa</h1>
+    </header>
+    <form enctype="multipart/form-data" method="post">
+      <div id="dropzone"></div>
+      <div id="fallbackform" class="dropzone">
+	<input id="filebox" name="file" type="file" multiple>
+	<input id="output" name="output" type="hidden" value='html' >
+	<input type="submit" value="Upload">
+      </div>
+      <section id="formsettings">
+	<label for="expiry"> Destroy after </label>
+	<select id="expiry" name="expiry">
+	  <option value="900"> 15 minutes </option>
+	  <option value="3600"> 1 hour </option>
+	  <option value="28800"> 8 hours </option>
+	  <option value="86400" selected> 1 day </option>
+	  <option value="604800"> 1 week </option>
+	</select>
+      </section>
+    </form>
+    <p>File size limited to {{.Maxsize}}.</p>
+    <div id="uploads">{{if .Links}}
+      <ul>
+	{{range .Links}}<li><a href="{{.}}">{{.}}</a></li>{{end}}
+      </ul>
+      {{end}}</div>
+  </body>
 </html>
Index: trunk/go.mod
===================================================================
--- trunk/go.mod	(revision 64)
+++ trunk/go.mod	(revision 65)
@@ -1,9 +1,10 @@
-module git.z3bra.org/partage
+module marisa.chaotic.ninja/marisa
 
 go 1.17
 
 require (
-	github.com/dustin/go-humanize v1.0.0 // indirect
-	github.com/vharitonsky/iniflags v0.0.0-20180513140207-a33cd0b5f3de // indirect
-	gopkg.in/ini.v1 v1.63.2 // indirect
+	github.com/dustin/go-humanize v1.0.0
+	gopkg.in/ini.v1 v1.63.2
 )
+
+require github.com/stretchr/testify v1.8.4 // indirect
Index: trunk/go.sum
===================================================================
--- trunk/go.sum	(revision 65)
+++ trunk/go.sum	(revision 65)
@@ -0,0 +1,20 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c=
+gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Index: trunk/marisa-trash.1
===================================================================
--- trunk/marisa-trash.1	(revision 65)
+++ trunk/marisa-trash.1	(revision 65)
@@ -0,0 +1,41 @@
+.Dd 2021-11-03
+.Dt PARTAGE-TRASH 1
+.Os POSIX.1-2017
+.Sh NAME
+.Nm partage-trash
+.Nd Purge expired share files
+.Sh SYNOPSIS
+.Nm partage-trash
+.Op Fl v
+.Op Fl f Ar files
+.Op Fl m Ar metadata
+.Sh DESCRIPTION
+Upon each run,
+.Nm
+will check expiration times for files in the
+.Pa metadata
+directory, and delete the according file in the 
+.Pa files
+directory if the expiration time has passed.
+.Pp
+.Nm
+is best run as a
+.Xr cron 8
+job, as the same user as the
+.Xr partage 1
+daemon.
+.Bl -tag -width Ds
+.It Fl v
+Turn on verbose logging to
+.Pa stderr
+.It Fl f Ar files
+Set the location of actual files to
+.Pa files
+.It Fl m Ar metadata
+Lookup metadata files in directory
+.Pa metadata
+.El
+.Sh SEE ALSO
+.Xr partage 1
+.Sh AUTHOR
+.An Willy Goiffon Aq Mt dev@z3bra.org
Index: trunk/marisa.1
===================================================================
--- trunk/marisa.1	(revision 65)
+++ trunk/marisa.1	(revision 65)
@@ -0,0 +1,33 @@
+.Dd 2021-11-03
+.Dt PARTAGE 1
+.Os POSIX.1-2017
+.Sh NAME
+.Nm partage
+.Nd HTTP based file upload system
+.Sh SYNOPSIS
+.Nm partage
+.Op Fl v
+.Op Fl f Ar file
+.Sh DESCRIPTION
+.Nm
+is an HTTP server that permits temporary file uploads using PUT and
+POST requests.
+.Pp
+Files uploaded are saved in a single directory and given random names
+while retaining their original extension.
+A configurable expiration time is set for each file, that can be used
+to cleanup expired files thanks to
+.Xr partage-trash 1 .
+.Bl -tag -width Ds
+.It Fl v
+Turn on verbose logging to
+.Pa stderr
+.It Fl f Ar file
+Load configuration from
+.Pa file
+.El
+.Sh SEE ALSO
+.Xr partage-trash 1 ,
+.Xr partage.conf 5
+.Sh AUTHOR
+.An Willy Goiffon Aq Mt dev@z3bra.org
Index: trunk/marisa.conf.5
===================================================================
--- trunk/marisa.conf.5	(revision 65)
+++ trunk/marisa.conf.5	(revision 65)
@@ -0,0 +1,91 @@
+.Dd 2021-11-03
+.Dt PARTAGE.CONF 5
+.Os POSIX.1-2017
+.Sh NAME
+.Nm partage.conf
+.Nd partage configuration file format
+.Sh DESCRIPTION
+.Nm
+is the configuration file for the HTTP file sharing system,
+.Xr partage 1 .
+.Sh CONFIGURATION
+Here are the settings that can be set:
+.Bl -tag -width Ds
+.It Ic listen Ar socket
+Have the program listen on
+.Ar socket .
+This socket can be specified either as a TCP socket:
+.Ar host:port
+or as a Unix socket:
+.Ar /path/to/partage.sock .
+When using Unix sockets, the program will serve content using the
+.Em FastCGI
+protocol.
+.It Ic user Ar user
+Username that the program will drop privileges to upon startup. When
+using Unix sockets, the owner of the socket will be changed to this user.
+.It Ic group Ar group
+Group that the program will drop privileges to upon startup (require that
+.Ic user
+is set). When using Unix sockets, the owner group of the socket will be
+changed to this group.
+.It Ic chroot Pa dir
+Directory to chroot into upon startup. When specified, all other path
+must be set within the chroot directory.
+.It Ic baseuri Ar uri
+Base URI to use when constructing hyper links.
+.It Ic rootdir Pa dir
+Directory containing static files.
+.It Ic tmplpath Pa dir
+Directory containing template files.
+.It Ic filepath Pa dir
+Directory where uploaded files must be written to.
+.It Ic metapath Pa dir
+Directory where metadata for uploaded files will be saved.
+.It Ic filectx Pa context
+URI context to use for serving files.
+.It Ic maxsize Ar size
+Maximum size per file to accept for uploads.
+.It Ic expiry Ar time
+Default expiration time to set for uploads.
+.El
+.Sh EXAMPLE
+Configuration suitable for use with
+.Xr httpd 8
+using fastcgi:
+.Bd -literal -offset indent
+listen      = /run/partage.sock
+baseuri     = https://domain.tld
+user        = www
+group       = daemon
+chroot      = /var/www
+rootdir     = /htdocs/static
+filepath    = /htdocs/files
+metapath    = /htdocs/meta
+tmplpath    = /htdocs/templates
+filectx     = /d/
+maxsize     = 10737418240 # 10 Gib
+expiry      = 86400       # 24 hours
+.Ed
+
+Mathing
+.Xr httpd.conf 5
+configuration:
+.Bd -literal -offset indent
+server "domain.tld" {
+	listen on * tls port 443
+	connection { max request body 10737418240 }
+	location "*" {
+		fastcgi socket "/run/partage.sock"
+	}
+}
+types { include "/usr/share/misc/mime.types" }
+.Ed
+
+.Sh SEE ALSO
+.Xr partage 1 ,
+.Xr partage-trash 1 ,
+.Xr httpd 8,
+.Xr httpd.conf 5
+.Sh AUTHORS
+.An Willy Goiffon Aq Mt dev@z3bra.org
Index: trunk/mkfile
===================================================================
--- trunk/mkfile	(revision 64)
+++ 	(revision )
@@ -1,30 +1,0 @@
-<config.mk
-
-all:V: partage partage-trash/partage-trash
-
-%: %.go
-	$GO build -o $stem $stem.go
-
-clean:V:
-	rm -f partage partage-trash/partage-trash
-
-install:V: partage partage-trash/partage-trash
-	mkdir -p ${DESTDIR}${PREFIX}/bin
-	cp partage ${DESTDIR}${PREFIX}/bin/partage
-	cp partage-trash/partage-trash ${DESTDIR}${PREFIX}/bin/partage-trash
-	chmod 755 ${DESTDIR}${PREFIX}/bin/partage
-	chmod 755 ${DESTDIR}${PREFIX}/bin/partage-trash
-	mkdir -p ${DESTDIR}${MANDIR}/man1
-	cp partage.1 ${DESTDIR}${MANDIR}/man1/partage.1
-	cp partage-trash/partage-trash.1 ${DESTDIR}${MANDIR}/man1/partage-trash.1
-	cp partage.conf.5 ${DESTDIR}${MANDIR}/man5/partage.conf.5
-	chmod 644 ${DESTDIR}${MANDIR}/man1/partage.1
-	chmod 644 ${DESTDIR}${MANDIR}/man1/partage-trash.1
-	chmod 644 ${DESTDIR}${MANDIR}/man5/partage.conf.5
-
-uninstall:V:
-	rm ${DESTDIR}${PREFIX}/bin/partage
-	rm ${DESTDIR}${PREFIX}/bin/partage-trash
-	rm ${DESTDIR}${MANDIR}/man1/partage.1
-	rm ${DESTDIR}${MANDIR}/man1/partage-trash.1
-	rm ${DESTDIR}${MANDIR}/man5/partage.conf.5
Index: trunk/partage-trash/partage-trash.1
===================================================================
--- trunk/partage-trash/partage-trash.1	(revision 64)
+++ 	(revision )
@@ -1,41 +1,0 @@
-.Dd 2021-11-03
-.Dt PARTAGE-TRASH 1
-.Os POSIX.1-2017
-.Sh NAME
-.Nm partage-trash
-.Nd Purge expired share files
-.Sh SYNOPSIS
-.Nm partage-trash
-.Op Fl v
-.Op Fl f Ar files
-.Op Fl m Ar metadata
-.Sh DESCRIPTION
-Upon each run,
-.Nm
-will check expiration times for files in the
-.Pa metadata
-directory, and delete the according file in the 
-.Pa files
-directory if the expiration time has passed.
-.Pp
-.Nm
-is best run as a
-.Xr cron 8
-job, as the same user as the
-.Xr partage 1
-daemon.
-.Bl -tag -width Ds
-.It Fl v
-Turn on verbose logging to
-.Pa stderr
-.It Fl f Ar files
-Set the location of actual files to
-.Pa files
-.It Fl m Ar metadata
-Lookup metadata files in directory
-.Pa metadata
-.El
-.Sh SEE ALSO
-.Xr partage 1
-.Sh AUTHOR
-.An Willy Goiffon Aq Mt dev@z3bra.org
Index: trunk/partage-trash/partage-trash.go
===================================================================
--- trunk/partage-trash/partage-trash.go	(revision 64)
+++ 	(revision )
@@ -1,101 +1,0 @@
-package main
-
-import (
-	"log"
-	"flag"
-	"os"
-	"time"
-	"path/filepath"
-	"encoding/json"
-	
-	"github.com/dustin/go-humanize"
-)
-
-type metadata struct {
-	Filename string
-	Size int64
-	Expiry int64
-}
-
-var conf struct {
-	filepath string
-	metapath string
-}
-
-var verbose bool
-var count int64
-var deleted int64
-var size int64
-
-func readmeta(filename string) (metadata, error) {
-	j, err := os.ReadFile(filename)
-	if err != nil {
-		return metadata{}, err
-	}
-
-	var meta metadata
-	err = json.Unmarshal(j, &meta)
-	if err != nil {
-		return metadata{}, err
-	}
-
-	return meta, nil
-}
-
-func checkexpiry(path string, info os.FileInfo, err error) error {
-	if filepath.Ext(path) != ".json" {
-		return nil
-	}
-	meta, err := readmeta(path)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-
-	count++
-
-	now := time.Now().Unix()
-	if verbose {
-		log.Printf("now: %s, expiry: %s\n", now, meta.Expiry);
-	}
-
-	if meta.Expiry > 0 && now >= meta.Expiry {
-		if verbose {
-			expiration :=  humanize.Time(time.Unix(meta.Expiry, 0))
-			log.Printf("%s/%s: expired %s\n", conf.filepath, meta.Filename, expiration)
-		}
-		if err = os.Remove(conf.filepath + "/" + meta.Filename); err != nil {
-			log.Fatal(err)
-		}
-		if err = os.Remove(path); err != nil {
-			log.Fatal(err)
-		}
-		deleted++
-		return nil
-	} else {
-		if verbose {
-			expiration :=  humanize.Time(time.Unix(meta.Expiry, 0))
-			log.Printf("%s/%s: expire in %s\n", conf.filepath, meta.Filename, expiration)
-		}
-		size += meta.Size
-	}
-
-	return nil
-}
-
-func main() {
-	flag.BoolVar(&verbose,         "v", false, "Verbose logging")
-	flag.StringVar(&conf.filepath, "f", "./files", "Directory containing files")
-	flag.StringVar(&conf.metapath, "m", "./meta", "Directory containing metadata")
-
-	flag.Parse()
-
-	err := filepath.Walk(conf.metapath, checkexpiry)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	if verbose && count > 0 {
-		log.Printf("%d/%d file(s) deleted (remaining: %s)", deleted, count, humanize.IBytes(uint64(size)))
-	}
-}
Index: trunk/partage.1
===================================================================
--- trunk/partage.1	(revision 64)
+++ 	(revision )
@@ -1,33 +1,0 @@
-.Dd 2021-11-03
-.Dt PARTAGE 1
-.Os POSIX.1-2017
-.Sh NAME
-.Nm partage
-.Nd HTTP based file upload system
-.Sh SYNOPSIS
-.Nm partage
-.Op Fl v
-.Op Fl f Ar file
-.Sh DESCRIPTION
-.Nm
-is an HTTP server that permits temporary file uploads using PUT and
-POST requests.
-.Pp
-Files uploaded are saved in a single directory and given random names
-while retaining their original extension.
-A configurable expiration time is set for each file, that can be used
-to cleanup expired files thanks to
-.Xr partage-trash 1 .
-.Bl -tag -width Ds
-.It Fl v
-Turn on verbose logging to
-.Pa stderr
-.It Fl f Ar file
-Load configuration from
-.Pa file
-.El
-.Sh SEE ALSO
-.Xr partage-trash 1 ,
-.Xr partage.conf 5
-.Sh AUTHOR
-.An Willy Goiffon Aq Mt dev@z3bra.org
Index: trunk/partage.conf.5
===================================================================
--- trunk/partage.conf.5	(revision 64)
+++ 	(revision )
@@ -1,91 +1,0 @@
-.Dd 2021-11-03
-.Dt PARTAGE.CONF 5
-.Os POSIX.1-2017
-.Sh NAME
-.Nm partage.conf
-.Nd partage configuration file format
-.Sh DESCRIPTION
-.Nm
-is the configuration file for the HTTP file sharing system,
-.Xr partage 1 .
-.Sh CONFIGURATION
-Here are the settings that can be set:
-.Bl -tag -width Ds
-.It Ic listen Ar socket
-Have the program listen on
-.Ar socket .
-This socket can be specified either as a TCP socket:
-.Ar host:port
-or as a Unix socket:
-.Ar /path/to/partage.sock .
-When using Unix sockets, the program will serve content using the
-.Em FastCGI
-protocol.
-.It Ic user Ar user
-Username that the program will drop privileges to upon startup. When
-using Unix sockets, the owner of the socket will be changed to this user.
-.It Ic group Ar group
-Group that the program will drop privileges to upon startup (require that
-.Ic user
-is set). When using Unix sockets, the owner group of the socket will be
-changed to this group.
-.It Ic chroot Pa dir
-Directory to chroot into upon startup. When specified, all other path
-must be set within the chroot directory.
-.It Ic baseuri Ar uri
-Base URI to use when constructing hyper links.
-.It Ic rootdir Pa dir
-Directory containing static files.
-.It Ic tmplpath Pa dir
-Directory containing template files.
-.It Ic filepath Pa dir
-Directory where uploaded files must be written to.
-.It Ic metapath Pa dir
-Directory where metadata for uploaded files will be saved.
-.It Ic filectx Pa context
-URI context to use for serving files.
-.It Ic maxsize Ar size
-Maximum size per file to accept for uploads.
-.It Ic expiry Ar time
-Default expiration time to set for uploads.
-.El
-.Sh EXAMPLE
-Configuration suitable for use with
-.Xr httpd 8
-using fastcgi:
-.Bd -literal -offset indent
-listen      = /run/partage.sock
-baseuri     = https://domain.tld
-user        = www
-group       = daemon
-chroot      = /var/www
-rootdir     = /htdocs/static
-filepath    = /htdocs/files
-metapath    = /htdocs/meta
-tmplpath    = /htdocs/templates
-filectx     = /d/
-maxsize     = 10737418240 # 10 Gib
-expiry      = 86400       # 24 hours
-.Ed
-
-Mathing
-.Xr httpd.conf 5
-configuration:
-.Bd -literal -offset indent
-server "domain.tld" {
-	listen on * tls port 443
-	connection { max request body 10737418240 }
-	location "*" {
-		fastcgi socket "/run/partage.sock"
-	}
-}
-types { include "/usr/share/misc/mime.types" }
-.Ed
-
-.Sh SEE ALSO
-.Xr partage 1 ,
-.Xr partage-trash 1 ,
-.Xr httpd 8,
-.Xr httpd.conf 5
-.Sh AUTHORS
-.An Willy Goiffon Aq Mt dev@z3bra.org
Index: trunk/partage.go
===================================================================
--- trunk/partage.go	(revision 64)
+++ 	(revision )
@@ -1,421 +1,0 @@
-package main
-
-import (
-	"encoding/json"
-	"flag"
-	"fmt"
-	"html/template"
-	"io"
-	"io/ioutil"
-	"log"
-	"net"
-	"net/http"
-	"net/http/fcgi"
-	"os"
-	"os/signal"
-	"os/user"
-	"path"
-	"path/filepath"
-	"strconv"
-	"syscall"
-	"time"
-
-	"github.com/dustin/go-humanize"
-	"gopkg.in/ini.v1"
-)
-
-type templatedata struct {
-	Links   []string
-	Size    string
-	Maxsize string
-}
-
-type metadata struct {
-	Filename string
-	Size     int64
-	Expiry   int64
-}
-
-var conf struct {
-	user     string
-	group    string
-	chroot   string
-	listen   string
-	baseuri  string
-	rootdir  string
-	tmplpath string
-	filepath string
-	metapath string
-	filectx  string
-	maxsize  int64
-	expiry   int64
-}
-
-var verbose bool
-
-func writefile(f *os.File, s io.ReadCloser, contentlength int64) error {
-	buffer := make([]byte, 4096)
-	eof := false
-	sz := int64(0)
-
-	defer f.Sync()
-
-	for !eof {
-		n, err := s.Read(buffer)
-		if err != nil && err != io.EOF {
-			return err
-		} else if err == io.EOF {
-			eof = true
-		}
-
-		/* ensure we don't write more than expected */
-		r := int64(n)
-		if sz+r > contentlength {
-			r = contentlength - sz
-			eof = true
-		}
-
-		_, err = f.Write(buffer[:r])
-		if err != nil {
-			return err
-		}
-		sz += r
-	}
-
-	return nil
-}
-
-func writemeta(filename string, expiry int64) error {
-
-	f, _ := os.Open(filename)
-	stat, _ := f.Stat()
-	size := stat.Size()
-	f.Close()
-
-	if expiry < 0 {
-		expiry = conf.expiry
-	}
-
-	meta := metadata{
-		Filename: filepath.Base(filename),
-		Size:     size,
-		Expiry:   time.Now().Unix() + expiry,
-	}
-
-	if verbose {
-		log.Printf("Saving metadata for %s in %s", meta.Filename, conf.metapath+"/"+meta.Filename+".json")
-	}
-
-	f, err := os.Create(conf.metapath + "/" + meta.Filename + ".json")
-	if err != nil {
-		return err
-	}
-	defer f.Close()
-
-	j, err := json.Marshal(meta)
-	if err != nil {
-		return err
-	}
-
-	_, err = f.Write(j)
-
-	return err
-}
-
-func servetemplate(w http.ResponseWriter, f string, d templatedata) {
-	t, err := template.ParseFiles(conf.tmplpath + "/" + f)
-	if err != nil {
-		http.Error(w, "Internal error", http.StatusInternalServerError)
-		return
-	}
-
-	if verbose {
-		log.Printf("Serving template %s", t.Name())
-	}
-
-	err = t.Execute(w, d)
-	if err != nil {
-		fmt.Println(err)
-	}
-}
-
-func uploaderPut(w http.ResponseWriter, r *http.Request) {
-	/* limit upload size */
-	if r.ContentLength > conf.maxsize {
-		http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
-	}
-
-	tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(r.URL.Path))
-	f, err := os.Create(tmp.Name())
-	if err != nil {
-		fmt.Println(err)
-		return
-	}
-	defer f.Close()
-
-	if verbose {
-		log.Printf("Writing %d bytes to %s", r.ContentLength, tmp.Name())
-	}
-
-	if err = writefile(f, r.Body, r.ContentLength); err != nil {
-		http.Error(w, "Internal error", http.StatusInternalServerError)
-		defer os.Remove(tmp.Name())
-		return
-	}
-	writemeta(tmp.Name(), conf.expiry)
-
-	resp := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
-	w.Write([]byte(resp + "\r\n"))
-}
-
-func uploaderPost(w http.ResponseWriter, r *http.Request) {
-	/* read 32Mb at a time */
-	r.ParseMultipartForm(32 << 20)
-
-	links := []string{}
-	for _, h := range r.MultipartForm.File["file"] {
-		if h.Size > conf.maxsize {
-			http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
-			return
-		}
-
-		post, err := h.Open()
-		if err != nil {
-			http.Error(w, "Internal error", http.StatusInternalServerError)
-			return
-		}
-		defer post.Close()
-
-		tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(h.Filename))
-		f, err := os.Create(tmp.Name())
-		if err != nil {
-			http.Error(w, "Internal error", http.StatusInternalServerError)
-			return
-		}
-		defer f.Close()
-
-		if err = writefile(f, post, h.Size); err != nil {
-			http.Error(w, "Internal error", http.StatusInternalServerError)
-			defer os.Remove(tmp.Name())
-			return
-		}
-
-		expiry, err := strconv.Atoi(r.PostFormValue("expiry"))
-		if err != nil || expiry < 0 {
-			expiry = int(conf.expiry)
-		}
-		writemeta(tmp.Name(), int64(expiry))
-
-		link := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
-		links = append(links, link)
-	}
-
-	switch r.PostFormValue("output") {
-	case "html":
-		data := templatedata{
-			Maxsize: humanize.IBytes(uint64(conf.maxsize)),
-			Links:   links,
-		}
-		servetemplate(w, "/index.html", data)
-	case "json":
-		data, _ := json.Marshal(links)
-		w.Write(data)
-	default:
-		for _, link := range links {
-			w.Write([]byte(link + "\r\n"))
-		}
-	}
-}
-
-func uploaderGet(w http.ResponseWriter, r *http.Request) {
-	// r.URL.Path is sanitized regarding "." and ".."
-	filename := r.URL.Path
-	if r.URL.Path == "/" || r.URL.Path == "/index.html" {
-		data := templatedata{Maxsize: humanize.IBytes(uint64(conf.maxsize))}
-		servetemplate(w, "/index.html", data)
-		return
-	}
-
-	if verbose {
-		log.Printf("Serving file %s", conf.rootdir+filename)
-	}
-
-	http.ServeFile(w, r, conf.rootdir+filename)
-}
-
-func uploaderDelete(w http.ResponseWriter, r *http.Request) {
-	// r.URL.Path is sanitized regarding "." and ".."
-	filename := r.URL.Path
-	filepath := conf.filepath + filename
-
-	if verbose {
-		log.Printf("Deleting file %s", filepath)
-	}
-
-	f, err := os.Open(filepath)
-	if err != nil {
-		http.NotFound(w, r)
-		return
-	}
-	f.Close()
-
-	// Force file expiration
-	writemeta(filepath, 0)
-	w.WriteHeader(http.StatusNoContent)
-}
-
-func uploader(w http.ResponseWriter, r *http.Request) {
-	if verbose {
-		log.Printf("%s: <%s> %s %s %s", r.Host, r.RemoteAddr, r.Method, r.RequestURI, r.Proto)
-	}
-
-	switch r.Method {
-	case "DELETE":
-		uploaderDelete(w, r)
-	case "POST":
-		uploaderPost(w, r)
-	case "PUT":
-		uploaderPut(w, r)
-	case "GET":
-		uploaderGet(w, r)
-	}
-}
-
-func parseconfig(file string) error {
-	cfg, err := ini.Load(file)
-	if err != nil {
-		return err
-	}
-
-	conf.listen = cfg.Section("").Key("listen").String()
-	conf.user = cfg.Section("").Key("user").String()
-	conf.group = cfg.Section("").Key("group").String()
-	conf.baseuri = cfg.Section("").Key("baseuri").String()
-	conf.filepath = cfg.Section("").Key("filepath").String()
-	conf.metapath = cfg.Section("").Key("metapath").String()
-	conf.filectx = cfg.Section("").Key("filectx").String()
-	conf.rootdir = cfg.Section("").Key("rootdir").String()
-	conf.chroot = cfg.Section("").Key("chroot").String()
-	conf.tmplpath = cfg.Section("").Key("tmplpath").String()
-	conf.maxsize, _ = cfg.Section("").Key("maxsize").Int64()
-	conf.expiry, _ = cfg.Section("").Key("expiry").Int64()
-
-	return nil
-}
-
-func usergroupids(username string, groupname string) (int, int, error) {
-	u, err := user.Lookup(username)
-	if err != nil {
-		return -1, -1, err
-	}
-
-	uid, _ := strconv.Atoi(u.Uid)
-	gid, _ := strconv.Atoi(u.Gid)
-
-	if conf.group != "" {
-		g, err := user.LookupGroup(groupname)
-		if err != nil {
-			return uid, -1, err
-		}
-		gid, _ = strconv.Atoi(g.Gid)
-	}
-
-	return uid, gid, nil
-}
-
-func main() {
-	var err error
-	var configfile string
-	var listener net.Listener
-
-	/* default values */
-	conf.listen = "0.0.0.0:8080"
-	conf.baseuri = "http://127.0.0.1:8080"
-	conf.rootdir = "static"
-	conf.tmplpath = "templates"
-	conf.filepath = "files"
-	conf.metapath = "meta"
-	conf.filectx = "/f/"
-	conf.maxsize = 34359738368
-	conf.expiry = 86400
-
-	flag.StringVar(&configfile, "f", "", "Configuration file")
-	flag.BoolVar(&verbose, "v", false, "Verbose logging")
-	flag.Parse()
-
-	if configfile != "" {
-		if verbose {
-			log.Printf("Reading configuration %s", configfile)
-		}
-		parseconfig(configfile)
-	}
-
-	if conf.chroot != "" {
-		if verbose {
-			log.Printf("Changing root to %s", conf.chroot)
-		}
-		syscall.Chroot(conf.chroot)
-	}
-
-	if conf.listen[0] == '/' {
-		/* Remove any stale socket */
-		os.Remove(conf.listen)
-		if listener, err = net.Listen("unix", conf.listen); err != nil {
-			log.Fatal(err)
-		}
-		defer listener.Close()
-
-		/*
-		 * Ensure unix socket is removed on exit.
-		 * Note: this might not work when dropping privileges…
-		 */
-		defer os.Remove(conf.listen)
-		sigs := make(chan os.Signal, 1)
-		signal.Notify(sigs, os.Interrupt, os.Kill, syscall.SIGTERM)
-		go func() {
-			_ = <-sigs
-			listener.Close()
-			if err = os.Remove(conf.listen); err != nil {
-				log.Fatal(err)
-			}
-			os.Exit(0)
-		}()
-	} else {
-		if listener, err = net.Listen("tcp", conf.listen); err != nil {
-			log.Fatal(err)
-		}
-		defer listener.Close()
-	}
-
-	if conf.user != "" {
-		if verbose {
-			log.Printf("Dropping privileges to %s", conf.user)
-		}
-		uid, gid, err := usergroupids(conf.user, conf.group)
-		if err != nil {
-			log.Fatal(err)
-		}
-
-		if listener.Addr().Network() == "unix" {
-			os.Chown(conf.listen, uid, gid)
-		}
-
-		syscall.Setuid(uid)
-		syscall.Setgid(gid)
-	}
-
-	http.HandleFunc("/", uploader)
-	http.Handle(conf.filectx, http.StripPrefix(conf.filectx, http.FileServer(http.Dir(conf.filepath))))
-
-	if verbose {
-		log.Printf("Listening on %s", conf.listen)
-	}
-
-	if listener.Addr().Network() == "unix" {
-		err = fcgi.Serve(listener, nil)
-		log.Fatal(err) /* NOTREACHED */
-	}
-
-	err = http.Serve(listener, nil)
-	log.Fatal(err) /* NOTREACHED */
-}
Index: trunk/version.go
===================================================================
--- trunk/version.go	(revision 65)
+++ trunk/version.go	(revision 65)
@@ -0,0 +1,18 @@
+package marisa
+
+import (
+	"fmt"
+)
+
+var (
+	// Version release version
+	Version = "0.0.1"
+
+	// Commit will be overwritten automatically by the build system
+	Commit = "HEAD"
+)
+
+// FullVersion display the full version and build
+func FullVersion() string {
+	return fmt.Sprintf("%s@%s", Version, Commit)
+}
