Browse Source

pare che fa

boyska 6 years ago
commit
c640ec47ac

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+rice-box.go
+*.yaml

+ 63 - 0
README.md

@@ -0,0 +1,63 @@
+MEGAROR upload
+-----------------
+
+Un uploader fatto apposta per ondarossa
+
+Cose principali:
+
+* pesca l'utente da degli header HTTP (sì, il server proxy deve fare le cose giuste)
+* autorizza delle cose in base a dei file di permessi semplici. Esempio /megaror/spot -> tizio@ondarossa.info,
+    *.ondarossa.net, tipa@ondarossa.info
+* associa path tipo /megaror/redattor@/boyska a  https://megaror.ondarossa.info/redattor@/boyska. Ovvero gli
+    dai un view-url-base e un view-path-base e lui compone le cose giuste.
+* fa graceful degradation così non killa i trasferimenti in corso
+* logga decentemente
+* (da approfondire) scarica da URL, ma con possibilità di filtrare quali url sì e quali no (ie: solo archive.org)
+* (da approfondire) manda mail quando un file viene caricato
+
+Configurazione. Un file YAML in cui l'unità di base è lo "share". Ogni share ha
+```
+type Share struct {
+Name OBBLIGATORIO
+Dir OBBLIGATORIA
+Description
+Authorized lista di glob di utenti autorizzati.
+SizeLimit esempio 20M
+ExtList lista di estensioni esempio ogg,oga,mp3,aac
+    }
+```
+
+```
+type Global struct {
+    Excluded []Glob
+    ViewURLBase string
+    ViewPathBase string
+}
+```
+
+Sezione globale: si possono escludere alcune persone globalmente dal servizio. Esempio: molesto@ondarossa.info, worm@ondarossa.info. Se un utente è bloccato globalmente ma è dentro l'Authorized di un certo share, è autorizzato.
+
+```
+type UserHome {
+    PathPrefix string
+    Create  bool
+    }
+func (*uh UserHome) GetShare(username string) (Share)
+```
+
+Share per-utente: si può creare una share "dinamica" per ogni utente (home) tipo /megaror/Redazione/redattor@/%h
+Se c'è una share con nome uguale a home_tizio@ondarossa.info si possono cambiare le impostazioni per quella
+home. Ovvero una share statica sovrascrive una share dinamica.
+Ad esempio per dire che home_leditanellapresa@ondarossa.info è scrivibile anche da nerd@ondarossa.info si può
+creare la share esplicitamente e mettere tutti i desiderati (incluso il "proprietario", non c'è automatismo)
+dentro Authorized
+Un utente bloccato globalmente non ha la home, a meno che la sezione non sia creata manualmente e l'utente
+venga autorizzato.
+
+```
+type URLFetcher struct {
+    Enabled bool
+    AllowedURLs []Glob
+    SizeLimit ???
+}
+```

+ 38 - 0
cmd/megauploader/main.go

@@ -0,0 +1,38 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"net/http"
+	"os"
+
+	"git.lattuga.net/boyska/megauploader"
+)
+
+func main() {
+	dump := flag.Bool("dump", false, "Do not run; dump the configuration as it is loaded instead")
+	cfgfile := flag.String("cfg", "/etc/megauploader/config.yaml", "Location of the configuration file")
+	addr := flag.String("addr", "localhost:8000", "Listen address")
+	flag.Parse()
+
+	cfg, err := megauploader.ParseConf(*cfgfile)
+	if err != nil {
+		fmt.Fprintln(os.Stderr, err)
+		os.Exit(1)
+	}
+	if errs := cfg.Validate(); len(errs) > 0 {
+		fmt.Fprintln(os.Stderr, "Configuration errors:")
+		for _, e := range errs {
+			fmt.Fprintln(os.Stderr, " -", e)
+		}
+		os.Exit(1)
+	}
+	if *dump {
+		fmt.Println(cfg.Pretty())
+		os.Exit(0)
+	}
+
+	mu := megauploader.MegaUploader{Conf: cfg}
+	mu.SetupRoutes()
+	http.ListenAndServe(*addr, nil)
+}

+ 304 - 0
conf.go

@@ -0,0 +1,304 @@
+package megauploader
+
+import (
+	"fmt"
+	"io"
+	"net/url"
+	"os"
+	"path"
+	"path/filepath"
+	"sort"
+	"strings"
+
+	"github.com/c2h5oh/datasize"
+	"github.com/pkg/errors"
+	glob "github.com/ryanuber/go-glob"
+)
+
+// helpers {{{
+
+// Globs is an array of globs with convenience methods
+type Globs []string
+
+// Has check if any of the  Glob matches with needle
+func (gs Globs) Has(needle string) bool {
+	for _, g := range gs {
+		if glob.Glob(g, needle) {
+			return true
+		}
+	}
+	return false
+}
+
+// ExtensionList is a list of extensions; you MUST NOT include the dot. That is, it must be
+//      {"mp3", "ogg", "aac"}
+// not
+//      {".docx", ".odt"}
+type ExtensionList []string
+
+// Matches check if the provided filename matches any of the extension
+func (el *ExtensionList) Matches(needle string) bool {
+	if len(*el) == 0 {
+		return true
+	}
+	n := strings.ToLower(needle)
+	for _, ext := range *el {
+		if strings.HasSuffix(n, fmt.Sprintf(".%s", strings.ToLower(ext))) {
+			return true
+		}
+	}
+	return false
+}
+
+// }}}
+
+// Config contains the whole configuration
+type Config struct {
+	Global   Global    `yaml:"global"`
+	UserHome *UserHome `yaml:"userhome"`
+	Shares   []Share   `yaml:"shares"`
+}
+
+// Pretty returns a pretty-printed representation of the Config data
+func (c Config) Pretty() string {
+	return fmt.Sprintf("GLOBAL\n%+v\nHOME\n%+v\nSHARES\n%+v\n", c.Global, *(c.UserHome), c.Shares)
+}
+
+// Validate checks the config for blocking inconsistencies
+//
+// while at the moment this does few checks, expect it to act on the conservative side
+//
+// returns a slice of errors; so check with
+//     if errs := conf.Validate(); len(errs) != 0
+func (c Config) Validate() []error {
+	errs := make([]error, 0)
+	names := make([]string, len(c.Shares))
+	if c.Shares != nil {
+		for i, s := range c.Shares {
+			names[i] = s.Name
+		}
+		sort.Strings(names)
+		i := 0
+		for i < len(names)-1 {
+			if names[i] == names[i+1] {
+				errs = append(errs, fmt.Errorf("Duplicate '%s' shares", names[i]))
+			}
+			i++
+		}
+
+		for i, s := range c.Shares {
+			// shareRef is a human-understandable,
+			// 1-indexed reference to this share
+			shareRef := fmt.Sprintf("Share %d(%s)", i+1, s.Name)
+			if s.Name == "" {
+				errs = append(errs, fmt.Errorf("%s has no name", shareRef))
+			}
+			if s.Dir == "" {
+				errs = append(errs, fmt.Errorf("%s has no dir", shareRef))
+			} else {
+				var fi os.FileInfo
+				var err error
+				if fi, err = os.Stat(s.Dir); err != nil {
+					errs = append(errs, fmt.Errorf("error accessing dir %s for %s", s.Dir, shareRef))
+				} else {
+					if !fi.IsDir() {
+						errs = append(errs, fmt.Errorf("dir %s for %s is not a dir", s.Dir, shareRef))
+					}
+				}
+			}
+		}
+	}
+
+	if c.UserHome != nil && c.UserHome.BasePath == "" {
+		errs = append(errs, errors.New("UserHome requires a BasePath: fill it or omit it"))
+	}
+
+	if (c.Global.ViewPathBase == "") != (c.Global.ViewURLBase == "") {
+		errs = append(errs, errors.New("viewpathbase and viewurlbase must be both set, or none"))
+	}
+
+	return errs
+}
+
+// Global is the global section of the configuration
+type Global struct {
+	Excluded     Globs  `yaml:"excluded"`
+	ViewURLBase  string `yaml:"viewurlbase"`
+	ViewPathBase string `yaml:"viewpathbase"`
+}
+
+// UserHome specifies rules for generating home shares on the fly
+type UserHome struct {
+	BasePath string `yaml:"basepath"`
+	Create   bool   `yaml:"create"`
+}
+
+// A Share is a directory where users can upload files
+type Share struct {
+	Name        string            `yaml:"name"`
+	Dir         string            `yaml:"dir"`
+	Description string            `yaml:"description"`
+	Authorized  Globs             `yaml:"authorized"`
+	SizeLimit   datasize.ByteSize `yaml:"sizelimit"`
+	Extensions  ExtensionList     `yaml:"extensions"`
+}
+
+// IsAuthorized checks whether username is autorized to access the Share
+func (s Share) IsAuthorized(username string) bool {
+	return s.Authorized.Has(username)
+}
+
+// Upload let you create a new file in the share
+//
+// If a file with the same name exists, Upload will try to "increment"
+// it. If no valid filename is found, an error will be returned.
+//
+// FIXME: distinguish between validation errors and server errors
+func (s Share) Upload(buf io.Reader, filename string) (string, error) {
+	if !s.Extensions.Matches(filename) {
+		return "", fmt.Errorf("Extension not admitted for this share")
+	}
+	fpath := filepath.Join(s.Dir, filename)
+	incrementable := newFileIncrement(fpath)
+	var dst *os.File
+	var err error
+	var realFileName string
+
+	try := 0
+	for try < 10 {
+		realFileName = incrementable.Increment(try)
+		dst, err = os.OpenFile(realFileName,
+			os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
+		if err == nil {
+			break
+		}
+		try++
+	}
+	if try == 10 {
+		return "", fmt.Errorf("Can't create file %s", filename)
+	}
+	defer dst.Close()
+	_, err = io.Copy(dst, buf)
+	return realFileName, nil
+}
+
+// GetViewURL retrieves the view URL for the specified path
+func (g *Global) GetViewURL(dir string) (url.URL, error) {
+	rel, err := filepath.Rel(g.ViewPathBase, dir)
+	if err != nil {
+		return url.URL{}, errors.Wrapf(err, "Error getting view URL for '%s'", dir)
+	}
+	if strings.HasPrefix(rel, "../") {
+		return url.URL{}, fmt.Errorf("Error getting view URL for '%s'", dir)
+	}
+	u, err := url.Parse(g.ViewURLBase)
+	if err != nil {
+		return url.URL{}, fmt.Errorf("Invalid view URL %s", g.ViewURLBase)
+	}
+	u.Path = path.Join(u.Path, rel)
+	return *u, nil
+}
+
+// GetShareURL retrieves the view URL for the share identified by sharename
+func (c *Config) GetShareURL(sharename string) (url.URL, error) {
+	if c.Global.ViewPathBase == "" || c.Global.ViewURLBase == "" {
+		return url.URL{}, errors.New("View URL not configured")
+	}
+	s, err := c.GetShare(sharename)
+	if err != nil {
+		return url.URL{}, err
+	}
+	return c.Global.GetViewURL(s.Dir)
+}
+
+// GetAuthShares returns an array of all the shares the user is authorized to use, including its home.
+func (c *Config) GetAuthShares(username string) []Share {
+	shares := make([]Share, 0)
+	for _, s := range c.Shares {
+		if s.IsAuthorized(username) {
+			shares = append(shares, s)
+		}
+	}
+	home, err := c.GetHome(username)
+	if err == nil {
+		shares = append(shares, *home)
+	}
+	return shares
+}
+
+// GetAuthShare returns a valid share for a certain user.
+//
+// It includes checking for permissions and dynamically-generated home.
+//
+// FIXME: can't tell 'unauthorized' from 'not found'
+func (c *Config) GetAuthShare(sharename string, username string) (Share, error) {
+	share, err := c.GetShare(sharename)
+	if err == nil {
+		if share.IsAuthorized(username) {
+			return share, nil
+		}
+		return Share{}, errors.New("Unauthorized")
+	}
+	home, homeerr := c.GetHome(username)
+	if homeerr != nil || home.Name != sharename {
+		return Share{}, errors.New("Cannot find home")
+	}
+	// assuming user is always authorized on its dynamically-generated home
+	return *home, nil
+}
+
+func (c *Config) GetShare(name string) (Share, error) {
+	for _, share := range c.Shares {
+		if share.Name == name {
+			return share, nil
+		}
+	}
+	return Share{}, errors.New("share not found")
+}
+
+// GetHome returns the home share for a user, if available
+// It implements authorization check
+func (c *Config) GetHome(username string) (*Share, error) {
+	// explicit share gets precedence
+	sharename := fmt.Sprintf("home_%s", username)
+	share, err := c.GetShare(sharename)
+	if err == nil {
+		if share.IsAuthorized(username) {
+			return &share, nil
+		}
+		return nil, fmt.Errorf("Home share for %s is forbidden to its owner", username)
+	}
+	if c.Global.Excluded.Has(username) {
+		return nil, fmt.Errorf("User %s has no home", username)
+	}
+	s := new(Share)
+	s.Name = sharename
+	s.Authorized = Globs{username}
+	s.Dir = filepath.Join(c.UserHome.BasePath, username)
+	stat, err := os.Stat(s.Dir)
+	if err != nil {
+		if os.IsNotExist(err) { //doesn't exist
+			if c.UserHome.Create {
+				os.Mkdir(s.Dir, os.ModeDir)
+			} else {
+				return nil, fmt.Errorf("User %s home is non-existent (and we won't create it)", username)
+			}
+		} else {
+			return nil, errors.Wrapf(err, "access to %s'shome failed", username)
+		}
+	} else {
+		if !stat.IsDir() {
+			return nil, fmt.Errorf("%s's home exists but is not a directory", username)
+		}
+	}
+	s.Description = fmt.Sprintf("%s's home", username)
+
+	return s, nil
+}
+
+// URLFetcher is a WIP section of the config to define rules for fetching URLs instead of uploading
+type URLFetcher struct {
+	Enabled     bool
+	AllowedURLs Globs
+	SizeLimit   datasize.ByteSize
+}

+ 38 - 0
conf_test.go

@@ -0,0 +1,38 @@
+package megauploader
+
+import "testing"
+
+type TestCase struct {
+	G Global
+	P string // path
+	E string // expected
+}
+
+var good []TestCase = []TestCase{
+	// simplest case
+	TestCase{G: Global{ViewPathBase: "/", ViewURLBase: "http://localhost/"}, P: "/", E: "http://localhost/"},
+	// prefix == path
+	TestCase{G: Global{ViewPathBase: "/asd/", ViewURLBase: "http://localhost/"}, P: "/asd/", E: "http://localhost/"},
+	// path has something more
+	TestCase{G: Global{ViewPathBase: "/asd/", ViewURLBase: "http://localhost/"}, P: "/asd/foo", E: "http://localhost/foo"},
+	// URL has a path
+	TestCase{G: Global{ViewPathBase: "/asd/", ViewURLBase: "http://localhost/bla/"}, P: "/asd/foo", E: "http://localhost/bla/foo"},
+	// removing slashes does not affect the result
+	TestCase{G: Global{ViewPathBase: "/asd", ViewURLBase: "http://localhost/bla"}, P: "/asd/foo", E: "http://localhost/bla/foo"},
+	// URL has port
+	TestCase{G: Global{ViewPathBase: "/asd/", ViewURLBase: "http://localhost:1234/"}, P: "/asd/foo", E: "http://localhost:1234/foo"},
+	// URL has params
+	TestCase{G: Global{ViewPathBase: "/asd/", ViewURLBase: "http://localhost:1234/?k=v"}, P: "/asd/foo", E: "http://localhost:1234/foo?k=v"},
+}
+
+func TestViewURL(t *testing.T) {
+	for _, test := range good {
+		val, err := test.G.GetShareURL(test.P)
+		if err != nil {
+			t.Error(err)
+		}
+		if val != test.E {
+			t.Fail()
+		}
+	}
+}

+ 26 - 0
confload.go

@@ -0,0 +1,26 @@
+package megauploader
+
+import (
+	"io/ioutil"
+	"os"
+
+	"github.com/pkg/errors"
+
+	yaml "gopkg.in/yaml.v2"
+)
+
+// ParseConf will read and load a config file
+func ParseConf(filename string) (Config, error) {
+	buf, err := os.Open(filename)
+	if err != nil {
+		return Config{}, errors.Wrap(err, "Error opening config file")
+	}
+	defer buf.Close()
+	content, err := ioutil.ReadAll(buf)
+	if err != nil {
+		return Config{}, errors.Wrap(err, "Error reading config file")
+	}
+	var c Config
+	err = yaml.UnmarshalStrict(content, &c)
+	return c, err
+}

+ 18 - 0
doc.go

@@ -0,0 +1,18 @@
+// megauploader is a very opinionated uploader. It is focused on very simple authorization schemes and the
+// idea of multiple "shares". A share is basically a directory where users can put things. Each share can list
+// authorized users (wildcards supported).
+//
+// There's also some support for "dynamic" share. MegaUploader has a native concept of user "home": a
+// directory that is dynamically calculated for each user. There are both global settings and possibility of
+// overriding dynamic calculation
+//
+// Upload is supported, while view is not. MegaUploader relies on a separate "file browse" URL.
+//
+// Security
+//
+// Absolutely do *not* expose it publicly; in fact, for authentication it relies on a proxy that will
+// set the X-Forwarded-User accordingly. This is nice if you want to reuse your webserver authentication
+// modules without rewriting them for this application
+// Requests that do not have X-Forwarded-User are an error; configure your webserver properly to avoid that a
+// malicious user can forge that header!
+package megauploader

+ 27 - 0
fileutils.go

@@ -0,0 +1,27 @@
+package megauploader
+
+import (
+	"fmt"
+	"strings"
+)
+
+type fileIncrement struct {
+	pre  string
+	post string
+}
+
+func (fi fileIncrement) Increment(i int) string {
+	if i == 0 {
+		return fmt.Sprintf("%s%s", fi.pre, fi.post)
+	}
+	return fmt.Sprintf("%s-%d%s", fi.pre, i, fi.post)
+}
+
+func newFileIncrement(fn string) fileIncrement {
+	idx := strings.Index(fn, ".")
+	if idx != -1 {
+		return fileIncrement{pre: fn[0:idx], post: fn[idx:]}
+	} else {
+		return fileIncrement{pre: fn[0:], post: ""}
+	}
+}

+ 31 - 0
fileutils_test.go

@@ -0,0 +1,31 @@
+package megauploader
+
+import "testing"
+
+func TestNoExtZero(t *testing.T) {
+	if newFileIncrement("foo").Increment(0) != "foo" {
+		t.Fail()
+	}
+}
+func TestNoExtMore(t *testing.T) {
+	if newFileIncrement("foo").Increment(1) != "foo-1" {
+		t.Fail()
+	}
+	if newFileIncrement("foo").Increment(2) != "foo-2" {
+		t.Fail()
+	}
+}
+
+func TestExtZero(t *testing.T) {
+	if newFileIncrement("foo.ogg").Increment(0) != "foo.ogg" {
+		t.Fail()
+	}
+}
+func TestExtMore(t *testing.T) {
+	if newFileIncrement("foo.ogg").Increment(1) != "foo-1.ogg" {
+		t.Fail()
+	}
+	if newFileIncrement("foo.ogg").Increment(2) != "foo-2.ogg" {
+		t.Fail()
+	}
+}

+ 129 - 0
http.go

@@ -0,0 +1,129 @@
+package megauploader
+
+//go:generate rice embed-go
+
+// this file contains all the web-related code for MegaUploader
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+
+	rice "github.com/GeertJohan/go.rice"
+	"github.com/c2h5oh/datasize"
+)
+
+// MegaUploader acts as controller for all the application. Since this is inherently a web-focused
+// application, it will include http utils
+type MegaUploader struct {
+	Conf Config
+}
+
+// SetupRoutes adds API routes
+func (mu *MegaUploader) SetupRoutes() {
+	http.HandleFunc("/", requireUserMiddleware(mu.home))
+	http.HandleFunc("/upload/", requireUserMiddleware(mu.uploadUI))
+	static := rice.MustFindBox("res/static")
+	http.HandleFunc("/static/", requireUserMiddleware(
+		http.StripPrefix("/static/", http.FileServer(static.HTTPBox())).ServeHTTP,
+	))
+	http.HandleFunc("/api/share", requireUserMiddleware(mu.listShares))
+	http.HandleFunc("/api/share/", requireUserMiddleware(mu.getShare))
+	http.HandleFunc("/api/upload/", requireUserMiddleware(mu.upload))
+}
+
+func getUser(r *http.Request) (string, error) {
+	user := r.Header.Get("X-Forwarded-User")
+	if user == "" {
+		return "", fmt.Errorf("User not set")
+	}
+	return user, nil
+}
+
+func requireUserMiddleware(inner func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		_, err := getUser(r)
+		if err != nil {
+			http.Error(w, "Only logged-in users are authorized", http.StatusUnauthorized)
+			return
+		}
+		inner(w, r)
+	}
+}
+
+func (mu *MegaUploader) listShares(w http.ResponseWriter, r *http.Request) {
+	user, _ := getUser(r) // user is set: checked by middleware
+	shares := mu.Conf.GetAuthShares(user)
+	serialized, err := json.Marshal(shares)
+	if err != nil {
+		http.Error(w, err.Error(), 500)
+		return
+	}
+	w.Header().Set("Content-type", "application/json")
+	w.Write(serialized)
+}
+
+func (mu *MegaUploader) getShare(w http.ResponseWriter, r *http.Request) {
+	sharename := r.URL.Path[strings.LastIndexByte(r.URL.Path, '/')+1:]
+	share, err := mu.Conf.GetShare(sharename)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Share '%s' not found: %s", sharename, err), http.StatusNotFound)
+		return
+	}
+	user, _ := getUser(r) // user is set: checked by middleware
+	if !share.IsAuthorized(user) {
+		http.Error(w, "You are not authorized to this share", http.StatusForbidden)
+		return
+	}
+	serialized, err := json.Marshal(share)
+	if err != nil {
+		http.Error(w, err.Error(), 500)
+		return
+	}
+	w.Header().Set("Content-type", "application/json")
+	w.Write(serialized)
+}
+
+// on success, redirect to view URL
+func (mu *MegaUploader) upload(w http.ResponseWriter, r *http.Request) {
+	user, _ := getUser(r) // user is set: checked by middleware
+	sharename := r.URL.Path[strings.LastIndexByte(r.URL.Path, '/')+1:]
+
+	share, err := mu.Conf.GetAuthShare(sharename, user)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Share '%s' not found: %s", sharename, err), http.StatusNotFound)
+		return
+	}
+	sizelimit := uint64(20 * datasize.MB)
+	if share.SizeLimit.Bytes() > 0 {
+		sizelimit = share.SizeLimit.Bytes()
+	}
+	err = r.ParseMultipartForm(int64(sizelimit))
+	if err != nil {
+		http.Error(w, "Bad request", http.StatusBadRequest)
+		return
+	}
+	file, header, err := r.FormFile("file")
+	if err != nil {
+		http.Error(w, "No file uploaded", http.StatusBadRequest)
+		return
+	}
+	fname, err := share.Upload(file, filepath.Base(header.Filename))
+	if err != nil {
+		fmt.Fprintln(os.Stderr, err)
+		http.Error(w, "Error uploading", http.StatusInternalServerError)
+		return
+	}
+	fname = filepath.Base(fname)
+	u, err := mu.Conf.GetShareURL(sharename)
+	if err != nil {
+		w.Write([]byte(fname))
+		return
+	}
+	u.Path = path.Join(u.Path, fname)
+	w.Write([]byte(u.String()))
+}

+ 79 - 0
httpui.go

@@ -0,0 +1,79 @@
+package megauploader
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"html/template"
+	"net/http"
+	"net/url"
+	"strings"
+
+	rice "github.com/GeertJohan/go.rice"
+)
+
+//go:generate rice embed-go
+
+// loadTmpl return the required template
+// It is provided to centralize handling of templates; it will care about
+// riceboxes and other quirks that templates might need
+func (mu *MegaUploader) loadTmpl(tmplname string) (*template.Template, error) {
+	str := rice.MustFindBox("res/templates").MustString(tmplname)
+	tmpl := template.Must(
+		template.New(strings.Split(tmplname, ".")[0]).Funcs(template.FuncMap{
+			"pretty": func(data interface{}) string {
+				var out bytes.Buffer
+				enc := json.NewEncoder(&out)
+				enc.SetIndent("", "    ")
+				err := enc.Encode(data)
+				if err != nil {
+					panic(err)
+				}
+				return out.String()
+			},
+		}).Parse(str),
+	)
+	str = rice.MustFindBox("res/templates").MustString("base.html.tmpl")
+	tmpl = template.Must(tmpl.Parse(str))
+	return tmpl, nil
+}
+
+func (mu *MegaUploader) home(w http.ResponseWriter, r *http.Request) {
+	tmpl, err := mu.loadTmpl("index.html.tmpl")
+	if err != nil {
+		http.Error(w, err.Error(), 500)
+		return
+	}
+	user, _ := getUser(r)
+	shares := mu.Conf.GetAuthShares(user)
+	w.Header().Set("Content-Type", "text/html")
+	tmpl.Execute(w, map[string]interface{}{
+		"Shares": shares,
+	})
+}
+
+func (mu *MegaUploader) uploadUI(w http.ResponseWriter, r *http.Request) {
+	user, _ := getUser(r) // user is set: checked by middleware
+	sharename := r.URL.Path[strings.LastIndexByte(r.URL.Path, '/')+1:]
+
+	share, err := mu.Conf.GetAuthShare(sharename, user)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("Share '%s' not found", sharename), http.StatusNotFound)
+		return
+	}
+
+	tmpl, err := mu.loadTmpl("upload.html.tmpl")
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	viewurl, err := mu.Conf.GetShareURL(sharename)
+	if err != nil {
+		viewurl = url.URL{}
+	}
+	w.Header().Set("Content-Type", "text/html")
+	tmpl.Execute(w, map[string]interface{}{
+		"Share":   share,
+		"ViewURL": viewurl.String(),
+	})
+}

File diff suppressed because it is too large
+ 0 - 0
res/static/dropzone/dropzone.min.css


File diff suppressed because it is too large
+ 0 - 0
res/static/dropzone/dropzone.min.js


+ 14 - 0
res/templates/base.html.tmpl

@@ -0,0 +1,14 @@
+<!doctype html>
+<html>
+    <head>
+        {{block "head" .}}
+            <meta name="generator" value="megauploader" />
+        {{end}}
+        {{block "styles" .}}{{end}}
+    </head>
+    <body>
+        {{template "body" .}}
+        {{block "scripts" .}}{{end}}
+    </body>
+</html>
+

+ 21 - 0
res/templates/index.html.tmpl

@@ -0,0 +1,21 @@
+{{define "body"}}
+    <h1>MegaUploader</h1>
+
+    Carica un file
+
+    <ul>
+        {{range .Shares}}
+            <li><span class="share-link">
+                    <a href="/upload/{{.Name}}">{{.Name}}</a>
+                </span>
+                <span class="share-description">{{.Description}}</span>
+                {{ if ne .SizeLimit.Bytes 0 }}
+                    <span class="share-maxsize">
+                        (Max {{.SizeLimit.HR}})</span>
+                {{else}}
+                    <span class="share-maxsize"></span>
+                {{end}}
+            </li>
+        {{end}}
+    </ul>
+{{end}}

+ 46 - 0
res/templates/upload.html.tmpl

@@ -0,0 +1,46 @@
+{{define "body"}}
+    <h1>MegaUploader - {{.Share.Name}}</h1>
+
+    <div>Carica un file su {{.Share.Name}} 
+        {{if .ViewURL}}(<a href="{{.ViewURL}}">Guarda cosa c'è già</a>){{end}}
+        </div>
+    <div>Ricorda:
+        <ul>
+            <li>{{if ne .Share.SizeLimit 0}}
+                Max {{.Share.SizeLimit.HR}}
+            {{end}}
+            </li>
+            {{if ne (len .Share.Extensions) 0}}
+                <li>Solo file di tipo
+                    {{range .Share.Extensions}}
+                        <tt>{{.}}</tt>,
+                    {{end}}
+                </li>
+            {{end}}
+        </ul>
+    </div>
+    <pre id="responses"></pre>
+    <form id="uploadForm" class="dropzone" method="POST" action="/api/upload/{{.Share.Name}}">
+        <noscript>
+            <input type="file" name="file" />
+        </noscript>
+    </form>
+
+    <!-- TODO: area per trascinare file -->
+{{end}}
+
+{{define "scripts"}}
+    <script type="text/javascript" src="/static/dropzone/dropzone.min.js"></script>
+    <script type="text/javascript">
+Dropzone.options.uploadForm = {
+    init: function() {
+        this.on("success", function(file, response) {
+            document.getElementById("responses").textContent += response + "\n";
+        });
+    }
+};
+    </script>
+{{end}}
+{{define "styles"}}
+    <link href="/static/dropzone/dropzone.min.css" rel="stylesheet" />
+{{end}}

Some files were not shown because too many files changed in this diff