From c640ec47ac759836734bcd98f8040c2b88fca4e4 Mon Sep 17 00:00:00 2001 From: boyska Date: Sat, 23 Sep 2017 19:46:58 +0200 Subject: [PATCH] pare che fa --- .gitignore | 2 + README.md | 63 ++++++ cmd/megauploader/main.go | 38 ++++ conf.go | 304 +++++++++++++++++++++++++++ conf_test.go | 38 ++++ confload.go | 26 +++ doc.go | 18 ++ fileutils.go | 27 +++ fileutils_test.go | 31 +++ http.go | 129 ++++++++++++ httpui.go | 79 +++++++ res/static/dropzone/dropzone.min.css | 1 + res/static/dropzone/dropzone.min.js | 2 + res/templates/base.html.tmpl | 14 ++ res/templates/index.html.tmpl | 21 ++ res/templates/upload.html.tmpl | 46 ++++ 16 files changed, 839 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cmd/megauploader/main.go create mode 100644 conf.go create mode 100644 conf_test.go create mode 100644 confload.go create mode 100644 doc.go create mode 100644 fileutils.go create mode 100644 fileutils_test.go create mode 100644 http.go create mode 100644 httpui.go create mode 100644 res/static/dropzone/dropzone.min.css create mode 100644 res/static/dropzone/dropzone.min.js create mode 100644 res/templates/base.html.tmpl create mode 100644 res/templates/index.html.tmpl create mode 100644 res/templates/upload.html.tmpl diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c1edef5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +rice-box.go +*.yaml diff --git a/README.md b/README.md new file mode 100644 index 0000000..a3e3109 --- /dev/null +++ b/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 ??? +} +``` diff --git a/cmd/megauploader/main.go b/cmd/megauploader/main.go new file mode 100644 index 0000000..625b3ee --- /dev/null +++ b/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) +} diff --git a/conf.go b/conf.go new file mode 100644 index 0000000..70378a9 --- /dev/null +++ b/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 +} diff --git a/conf_test.go b/conf_test.go new file mode 100644 index 0000000..385e304 --- /dev/null +++ b/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() + } + } +} diff --git a/confload.go b/confload.go new file mode 100644 index 0000000..d5b83f9 --- /dev/null +++ b/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 +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..f7b63fe --- /dev/null +++ b/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 diff --git a/fileutils.go b/fileutils.go new file mode 100644 index 0000000..26acc8a --- /dev/null +++ b/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: ""} + } +} diff --git a/fileutils_test.go b/fileutils_test.go new file mode 100644 index 0000000..92ce61c --- /dev/null +++ b/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() + } +} diff --git a/http.go b/http.go new file mode 100644 index 0000000..9b85283 --- /dev/null +++ b/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())) +} diff --git a/httpui.go b/httpui.go new file mode 100644 index 0000000..cb8c8cf --- /dev/null +++ b/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(), + }) +} diff --git a/res/static/dropzone/dropzone.min.css b/res/static/dropzone/dropzone.min.css new file mode 100644 index 0000000..d04515e --- /dev/null +++ b/res/static/dropzone/dropzone.min.css @@ -0,0 +1 @@ +@-webkit-keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%, 70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@-moz-keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%, 70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%, 70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@-webkit-keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@-moz-keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@-webkit-keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}@-moz-keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}@keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}.dropzone,.dropzone *{box-sizing:border-box}.dropzone{min-height:150px;border:2px solid rgba(0,0,0,0.3);background:white;padding:20px 20px}.dropzone.dz-clickable{cursor:pointer}.dropzone.dz-clickable *{cursor:default}.dropzone.dz-clickable .dz-message,.dropzone.dz-clickable .dz-message *{cursor:pointer}.dropzone.dz-started .dz-message{display:none}.dropzone.dz-drag-hover{border-style:solid}.dropzone.dz-drag-hover .dz-message{opacity:0.5}.dropzone .dz-message{text-align:center;margin:2em 0}.dropzone .dz-preview{position:relative;display:inline-block;vertical-align:top;margin:16px;min-height:100px}.dropzone .dz-preview:hover{z-index:1000}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview.dz-file-preview .dz-image{border-radius:20px;background:#999;background:linear-gradient(to bottom, #eee, #ddd)}.dropzone .dz-preview.dz-file-preview .dz-details{opacity:1}.dropzone .dz-preview.dz-image-preview{background:white}.dropzone .dz-preview.dz-image-preview .dz-details{-webkit-transition:opacity 0.2s linear;-moz-transition:opacity 0.2s linear;-ms-transition:opacity 0.2s linear;-o-transition:opacity 0.2s linear;transition:opacity 0.2s linear}.dropzone .dz-preview .dz-remove{font-size:14px;text-align:center;display:block;cursor:pointer;border:none}.dropzone .dz-preview .dz-remove:hover{text-decoration:underline}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview .dz-details{z-index:20;position:absolute;top:0;left:0;opacity:0;font-size:13px;min-width:100%;max-width:100%;padding:2em 1em;text-align:center;color:rgba(0,0,0,0.9);line-height:150%}.dropzone .dz-preview .dz-details .dz-size{margin-bottom:1em;font-size:16px}.dropzone .dz-preview .dz-details .dz-filename{white-space:nowrap}.dropzone .dz-preview .dz-details .dz-filename:hover span{border:1px solid rgba(200,200,200,0.8);background-color:rgba(255,255,255,0.8)}.dropzone .dz-preview .dz-details .dz-filename:not(:hover){overflow:hidden;text-overflow:ellipsis}.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span{border:1px solid transparent}.dropzone .dz-preview .dz-details .dz-filename span,.dropzone .dz-preview .dz-details .dz-size span{background-color:rgba(255,255,255,0.4);padding:0 0.4em;border-radius:3px}.dropzone .dz-preview:hover .dz-image img{-webkit-transform:scale(1.05, 1.05);-moz-transform:scale(1.05, 1.05);-ms-transform:scale(1.05, 1.05);-o-transform:scale(1.05, 1.05);transform:scale(1.05, 1.05);-webkit-filter:blur(8px);filter:blur(8px)}.dropzone .dz-preview .dz-image{border-radius:20px;overflow:hidden;width:120px;height:120px;position:relative;display:block;z-index:10}.dropzone .dz-preview .dz-image img{display:block}.dropzone .dz-preview.dz-success .dz-success-mark{-webkit-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-moz-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-ms-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-o-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview.dz-error .dz-error-mark{opacity:1;-webkit-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-moz-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-ms-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-o-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview .dz-success-mark,.dropzone .dz-preview .dz-error-mark{pointer-events:none;opacity:0;z-index:500;position:absolute;display:block;top:50%;left:50%;margin-left:-27px;margin-top:-27px}.dropzone .dz-preview .dz-success-mark svg,.dropzone .dz-preview .dz-error-mark svg{display:block;width:54px;height:54px}.dropzone .dz-preview.dz-processing .dz-progress{opacity:1;-webkit-transition:all 0.2s linear;-moz-transition:all 0.2s linear;-ms-transition:all 0.2s linear;-o-transition:all 0.2s linear;transition:all 0.2s linear}.dropzone .dz-preview.dz-complete .dz-progress{opacity:0;-webkit-transition:opacity 0.4s ease-in;-moz-transition:opacity 0.4s ease-in;-ms-transition:opacity 0.4s ease-in;-o-transition:opacity 0.4s ease-in;transition:opacity 0.4s ease-in}.dropzone .dz-preview:not(.dz-processing) .dz-progress{-webkit-animation:pulse 6s ease infinite;-moz-animation:pulse 6s ease infinite;-ms-animation:pulse 6s ease infinite;-o-animation:pulse 6s ease infinite;animation:pulse 6s ease infinite}.dropzone .dz-preview .dz-progress{opacity:1;z-index:1000;pointer-events:none;position:absolute;height:16px;left:50%;top:50%;margin-top:-8px;width:80px;margin-left:-40px;background:rgba(255,255,255,0.9);-webkit-transform:scale(1);border-radius:8px;overflow:hidden}.dropzone .dz-preview .dz-progress .dz-upload{background:#333;background:linear-gradient(to bottom, #666, #444);position:absolute;top:0;left:0;bottom:0;width:0;-webkit-transition:width 300ms ease-in-out;-moz-transition:width 300ms ease-in-out;-ms-transition:width 300ms ease-in-out;-o-transition:width 300ms ease-in-out;transition:width 300ms ease-in-out}.dropzone .dz-preview.dz-error .dz-error-message{display:block}.dropzone .dz-preview.dz-error:hover .dz-error-message{opacity:1;pointer-events:auto}.dropzone .dz-preview .dz-error-message{pointer-events:none;z-index:1000;position:absolute;display:block;display:none;opacity:0;-webkit-transition:opacity 0.3s ease;-moz-transition:opacity 0.3s ease;-ms-transition:opacity 0.3s ease;-o-transition:opacity 0.3s ease;transition:opacity 0.3s ease;border-radius:8px;font-size:13px;top:130px;left:-10px;width:140px;background:#be2626;background:linear-gradient(to bottom, #be2626, #a92222);padding:0.5em 1.2em;color:white}.dropzone .dz-preview .dz-error-message:after{content:'';position:absolute;top:-6px;left:64px;width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #be2626} diff --git a/res/static/dropzone/dropzone.min.js b/res/static/dropzone/dropzone.min.js new file mode 100644 index 0000000..3e1da61 --- /dev/null +++ b/res/static/dropzone/dropzone.min.js @@ -0,0 +1,2 @@ +(function(){var a,b,c,d,e,f,g,h,i,j=[].slice,k=function(a,b){function c(){this.constructor=a}for(var d in b)l.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},l={}.hasOwnProperty;h=function(){},b=function(){function a(){}return a.prototype.addEventListener=a.prototype.on,a.prototype.on=function(a,b){return this._callbacks=this._callbacks||{},this._callbacks[a]||(this._callbacks[a]=[]),this._callbacks[a].push(b),this},a.prototype.emit=function(){var a,b,c,d,e,f;if(d=arguments[0],a=2<=arguments.length?j.call(arguments,1):[],this._callbacks=this._callbacks||{},c=this._callbacks[d])for(e=0,f=c.length;e'),this.element.appendChild(e)),g=e.getElementsByTagName("span")[0],g&&(null!=g.textContent?g.textContent=this.options.dictFallbackMessage:null!=g.innerText&&(g.innerText=this.options.dictFallbackMessage)),this.element.appendChild(this.getFallbackForm())},resize:function(a,b,c,d){var e,f,g;if(e={srcX:0,srcY:0,srcWidth:a.width,srcHeight:a.height},f=a.width/a.height,null==b&&null==c?(b=e.srcWidth,c=e.srcHeight):null==b?b=c*f:null==c&&(c=b/f),b=Math.min(b,e.srcWidth),c=Math.min(c,e.srcHeight),g=b/c,e.srcWidth>b||e.srcHeight>c)if("crop"===d)f>g?(e.srcHeight=a.height,e.srcWidth=e.srcHeight*g):(e.srcWidth=a.width,e.srcHeight=e.srcWidth/g);else{if("contain"!==d)throw new Error("Unknown resizeMethod '"+d+"'");f>g?c=b/f:b=c*f}return e.srcX=(a.width-e.srcWidth)/2,e.srcY=(a.height-e.srcHeight)/2,e.trgWidth=b,e.trgHeight=c,e},transformFile:function(a,b){return(this.options.resizeWidth||this.options.resizeHeight)&&a.type.match(/image.*/)?this.resizeImage(a,this.options.resizeWidth,this.options.resizeHeight,this.options.resizeMethod,b):b(a)},previewTemplate:'
\n
\n
\n
\n
\n
\n
\n
\n
\n \n Check\n \n \n \n \n \n
\n
\n \n Error\n \n \n \n \n \n \n \n
\n
',drop:function(a){return this.element.classList.remove("dz-drag-hover")},dragstart:h,dragend:function(a){return this.element.classList.remove("dz-drag-hover")},dragenter:function(a){return this.element.classList.add("dz-drag-hover")},dragover:function(a){return this.element.classList.add("dz-drag-hover")},dragleave:function(a){return this.element.classList.remove("dz-drag-hover")},paste:h,reset:function(){return this.element.classList.remove("dz-started")},addedfile:function(a){var b,c,e,f,g,h,i,j,k,l,m,n,o;if(this.element===this.previewsContainer&&this.element.classList.add("dz-started"),this.previewsContainer){for(a.previewElement=d.createElement(this.options.previewTemplate.trim()),a.previewTemplate=a.previewElement,this.previewsContainer.appendChild(a.previewElement),j=a.previewElement.querySelectorAll("[data-dz-name]"),b=0,f=j.length;b'+this.options.dictRemoveFile+""),a.previewElement.appendChild(a._removeLink)),m=function(b){return function(c){return c.preventDefault(),c.stopPropagation(),a.status===d.UPLOADING?d.confirm(b.options.dictCancelUploadConfirmation,function(){return b.removeFile(a)}):b.options.dictRemoveFileConfirmation?d.confirm(b.options.dictRemoveFileConfirmation,function(){return b.removeFile(a)}):b.removeFile(a)}}(this),l=a.previewElement.querySelectorAll("[data-dz-remove]"),o=[],e=0,h=l.length;e'+this.options.dictDefaultMessage+"")),this.clickableElements.length&&(h=function(a){return function(){return a.hiddenFileInput&&a.hiddenFileInput.parentNode.removeChild(a.hiddenFileInput),a.hiddenFileInput=document.createElement("input"),a.hiddenFileInput.setAttribute("type","file"),(null==a.options.maxFiles||a.options.maxFiles>1)&&a.hiddenFileInput.setAttribute("multiple","multiple"),a.hiddenFileInput.className="dz-hidden-input",null!=a.options.acceptedFiles&&a.hiddenFileInput.setAttribute("accept",a.options.acceptedFiles),null!=a.options.capture&&a.hiddenFileInput.setAttribute("capture",a.options.capture),a.hiddenFileInput.style.visibility="hidden",a.hiddenFileInput.style.position="absolute",a.hiddenFileInput.style.top="0",a.hiddenFileInput.style.left="0",a.hiddenFileInput.style.height="0",a.hiddenFileInput.style.width="0",document.querySelector(a.options.hiddenInputContainer).appendChild(a.hiddenFileInput),a.hiddenFileInput.addEventListener("change",function(){var b,c,d,e;if(c=a.hiddenFileInput.files,c.length)for(d=0,e=c.length;d',this.options.dictFallbackText&&(c+="

"+this.options.dictFallbackText+"

"),c+='',b=d.createElement(c),"FORM"!==this.element.tagName?(e=d.createElement('
'),e.appendChild(b)):(this.element.setAttribute("enctype","multipart/form-data"),this.element.setAttribute("method",this.options.method)),null!=e?e:b)},d.prototype.getExistingFallback=function(){var a,b,c,d,e,f;for(b=function(a){var b,c,d;for(c=0,d=a.length;c0){for(i=["tb","gb","mb","kb","b"],c=d=0,e=i.length;d=b){f=a/Math.pow(this.options.filesizeBase,4-c),g=h;break}f=Math.round(10*f)/10}return""+f+" "+this.options.dictFileSizeUnits[g]},d.prototype._updateMaxFilesReachedClass=function(){return null!=this.options.maxFiles&&this.getAcceptedFiles().length>=this.options.maxFiles?(this.getAcceptedFiles().length===this.options.maxFiles&&this.emit("maxfilesreached",this.files),this.element.classList.add("dz-max-files-reached")):this.element.classList.remove("dz-max-files-reached")},d.prototype.drop=function(a){var b,c;a.dataTransfer&&(this.emit("drop",a),b=a.dataTransfer.files,this.emit("addedfiles",b),b.length&&(c=a.dataTransfer.items,c&&c.length&&null!=c[0].webkitGetAsEntry?this._addFilesFromItems(c):this.handleFiles(b)))},d.prototype.paste=function(a){var b,c;if(null!=(null!=a&&null!=(c=a.clipboardData)?c.items:void 0))return this.emit("paste",a),b=a.clipboardData.items,b.length?this._addFilesFromItems(b):void 0},d.prototype.handleFiles=function(a){var b,c,d,e;for(e=[],c=0,d=a.length;c0){for(f=0,g=c.length;f1024*this.options.maxFilesize*1024?b(this.options.dictFileTooBig.replace("{{filesize}}",Math.round(a.size/1024/10.24)/100).replace("{{maxFilesize}}",this.options.maxFilesize)):d.isValidFile(a,this.options.acceptedFiles)?null!=this.options.maxFiles&&this.getAcceptedFiles().length>=this.options.maxFiles?(b(this.options.dictMaxFilesExceeded.replace("{{maxFiles}}",this.options.maxFiles)),this.emit("maxfilesexceeded",a)):this.options.accept.call(this,a,b):b(this.options.dictInvalidFileType)},d.prototype.addFile=function(a){return a.upload={progress:0,total:a.size,bytesSent:0,filename:this._renameFile(a)},this.files.push(a),a.status=d.ADDED,this.emit("addedfile",a),this._enqueueThumbnail(a),this.accept(a,function(b){return function(c){return c?(a.accepted=!1,b._errorProcessing([a],c)):(a.accepted=!0,b.options.autoQueue&&b.enqueueFile(a)),b._updateMaxFilesReachedClass()}}(this))},d.prototype.enqueueFiles=function(a){var b,c,d;for(c=0,d=a.length;c4&&(j.width=p.trgHeight,j.height=p.trgWidth),e){case 2:k.translate(j.width,0),k.scale(-1,1);break;case 3:k.translate(j.width,j.height),k.rotate(Math.PI);break;case 4:k.translate(0,j.height),k.scale(1,-1);break;case 5:k.rotate(.5*Math.PI),k.scale(1,-1);break;case 6:k.rotate(.5*Math.PI),k.translate(0,-j.height);break;case 7:k.rotate(.5*Math.PI),k.translate(j.width,-j.height),k.scale(-1,1);break;case 8:k.rotate(-.5*Math.PI),k.translate(-j.width,0)}if(g(k,i,null!=(l=p.srcX)?l:0,null!=(m=p.srcY)?m:0,p.srcWidth,p.srcHeight,null!=(n=p.trgX)?n:0,null!=(o=p.trgY)?o:0,p.trgWidth,p.trgHeight),q=j.toDataURL("image/png"),null!=f)return f(q,j)})}}(this),null!=f&&(i.onerror=f),i.src=a.dataURL},d.prototype.processQueue=function(){var a,b,c,d;if(b=this.options.parallelUploads,c=this.getUploadingFiles().length,a=c,!(c>=b)&&(d=this.getQueuedFiles(),d.length>0)){if(this.options.uploadMultiple)return this.processFiles(d.slice(0,b-c));for(;a=I;m=0<=I?++A:--A)c=function(c){return function(d,e,f){return function(d){if(h.append(e,d,f),++b===a.length)return c.submitRequest(O,h,a)}}}(this),K.push(this.options.transformFile.call(this,a[m],c(a[m],this._getParamName(m),a[m].upload.filename)));return K},d.prototype.submitRequest=function(a,b,c){return a.send(b)},d.prototype._finished=function(a,b,c){var e,f,g;for(f=0,g=a.length;f=h;d=0<=h?++f:--f)e[d]=c.charCodeAt(d);return new Blob([b],{type:g})},i=function(a,b){var c,d,e,f;for(f=[],d=0,e=a.length;dj;)b=e[4*(h-1)+3],0===b?f=h:j=h,h=f+j>>1;return i=h/g,0===i?1:i},g=function(a,b,c,d,e,g,h,i,j,k){var l;return l=f(b),a.drawImage(b,c,d,e,g,h,i,j,k/l)},c=function(){function a(){}return a.KEY_STR="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",a.encode64=function(a){var b,c,d,e,f,g,h,i,j;for(j="",b=void 0,c=void 0,d="",e=void 0,f=void 0,g=void 0,h="",i=0;;)if(b=a[i++],c=a[i++],d=a[i++],e=b>>2,f=(3&b)<<4|c>>4,g=(15&c)<<2|d>>6,h=63&d,isNaN(c)?g=h=64:isNaN(d)&&(h=64),j=j+this.KEY_STR.charAt(e)+this.KEY_STR.charAt(f)+this.KEY_STR.charAt(g)+this.KEY_STR.charAt(h),b=c=d="",e=f=g=h="",!(ia.length)break}return f},a.decode64=function(a){var b,c,d,e,f,g,h,i,j,k;for("",d=void 0,e=void 0,f="",g=void 0,h=void 0,i=void 0,j="",k=0,c=[],b=/[^A-Za-z0-9\+\/\=]/g,b.exec(a)&&console.warning("There were invalid base64 characters in the input text.\nValid base64 characters are A-Z, a-z, 0-9, '+', '/',and '='\nExpect errors in decoding."),a=a.replace(/[^A-Za-z0-9\+\/\=]/g,"");;)if(g=this.KEY_STR.indexOf(a.charAt(k++)),h=this.KEY_STR.indexOf(a.charAt(k++)),i=this.KEY_STR.indexOf(a.charAt(k++)),j=this.KEY_STR.indexOf(a.charAt(k++)),d=g<<2|h>>4,e=(15&h)<<4|i>>2,f=(3&i)<<6|j,c.push(d),64!==i&&c.push(e),64!==j&&c.push(f),d=e=f="",g=h=i=j="",!(k + + + {{block "head" .}} + + {{end}} + {{block "styles" .}}{{end}} + + + {{template "body" .}} + {{block "scripts" .}}{{end}} + + + diff --git a/res/templates/index.html.tmpl b/res/templates/index.html.tmpl new file mode 100644 index 0000000..f3475e6 --- /dev/null +++ b/res/templates/index.html.tmpl @@ -0,0 +1,21 @@ +{{define "body"}} +

MegaUploader

+ + Carica un file + +
    + {{range .Shares}} +
  • + + {{ if ne .SizeLimit.Bytes 0 }} + + {{else}} + + {{end}} +
  • + {{end}} +
+{{end}} diff --git a/res/templates/upload.html.tmpl b/res/templates/upload.html.tmpl new file mode 100644 index 0000000..511de81 --- /dev/null +++ b/res/templates/upload.html.tmpl @@ -0,0 +1,46 @@ +{{define "body"}} +

MegaUploader - {{.Share.Name}}

+ +
Carica un file su {{.Share.Name}} + {{if .ViewURL}}(Guarda cosa c'è già){{end}} +
+
Ricorda: +
    +
  • {{if ne .Share.SizeLimit 0}} + Max {{.Share.SizeLimit.HR}} + {{end}} +
  • + {{if ne (len .Share.Extensions) 0}} +
  • Solo file di tipo + {{range .Share.Extensions}} + {{.}}, + {{end}} +
  • + {{end}} +
+
+

+    
+ +
+ + +{{end}} + +{{define "scripts"}} + + +{{end}} +{{define "styles"}} + +{{end}}