pare che fa

This commit is contained in:
boyska 2017-09-23 19:46:58 +02:00
commit c640ec47ac
16 changed files with 839 additions and 0 deletions

2
.gitignore vendored Normal file
View file

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

63
README.md Normal file
View file

@ -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
cmd/megauploader/main.go Normal file
View file

@ -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
conf.go Normal file
View file

@ -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
conf_test.go Normal file
View file

@ -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
confload.go Normal file
View file

@ -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
doc.go Normal file
View file

@ -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
fileutils.go Normal file
View file

@ -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
fileutils_test.go Normal file
View file

@ -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
http.go Normal file
View file

@ -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
httpui.go Normal file
View file

@ -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(),
})
}

1
res/static/dropzone/dropzone.min.css vendored Normal file

File diff suppressed because one or more lines are too long

2
res/static/dropzone/dropzone.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -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>

View file

@ -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}}

View file

@ -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}}