diff --git a/.gitignore b/.gitignore index bf658ff3e..07a6678cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ downloads cloud-torrent.json +tmp diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index e9f37ab70..5e3494201 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -53,7 +53,7 @@ }, { "ImportPath": "github.com/jpillora/opts", - "Rev": "019b00bcdc616f881fc1f669d040aea7c74c295d" + "Rev": "c5abde5b29df260a64d212596657d491dd80c7b0" }, { "ImportPath": "github.com/jpillora/requestlog", @@ -80,6 +80,10 @@ "ImportPath": "github.com/ryszard/goskiplist/skiplist", "Rev": "2dfbae5fcf46374f166f8969cb07e167f1be6273" }, + { + "ImportPath": "github.com/skratchdot/open-golang/open", + "Rev": "c8748311a7528d0ba7330d302adbc5a677ef9c9e" + }, { "ImportPath": "github.com/spacemonkeygo/monotime", "Rev": "7a1ce30a80e7104dbb48fed297f42a8dea1d91c5" diff --git a/Godeps/_workspace/src/github.com/jpillora/opts/README.md b/Godeps/_workspace/src/github.com/jpillora/opts/README.md index 2b6b583e9..1ab5e1718 100644 --- a/Godeps/_workspace/src/github.com/jpillora/opts/README.md +++ b/Godeps/_workspace/src/github.com/jpillora/opts/README.md @@ -4,37 +4,70 @@ [![GoDoc](https://godoc.org/github.com/jpillora/opts?status.svg)](https://godoc.org/github.com/jpillora/opts) -### Overview - -Command-line parsing should be easy. We shouldn't be forced to keep our configuration in sync with our command-line flags. `opts` attempts solve this with the goal of being as low friction as possible: +Command-line parsing should be easy. Use configuration structs: ``` go -opts.Parse(&foo) +package main + +import ( + "fmt" + + "github.com/jpillora/opts" +) + +func main() { + config := struct { + File string `help:"file to load"` + Lines int `help:"number of lines to show"` + }{} + opts.Parse(&config) + fmt.Println(config) +} +``` + +``` +$ go run main.go -f foo -l 12 +{foo 12} ``` +### Features (with examples) + +* Easy to use ([simple](example/simple/)) +* Promotes separation of CLI code and library code ([separation](example/separation/)) +* Automatically generated `--help` text via struct tags `help:"Foo bar"` ([help](example/help/)) +* Subcommands by nesting structs ([subcmds](example/subcmds/)) +* Default values by modifying the struct prior to `Parse()` ([defaults](example/defaults/)) +* Default values from a JSON config file, unmarshalled via your config struct ([config](example/config/)) +* Default values from environment, defined by your field names ([env](example/env/)) +* Infers program name from package name (and optional repository link) +* Extensible via `flag.Value` ([customtypes](example/customtypes/)) +* Customizable help text by modifying the default templates ([customhelp](example/customhelp/)) + +### Overview + Internally, `opts` creates `flag.FlagSet`s from your configuration structs using `pkg/reflect`. So, given the following program: ``` go -type FooConfig struct { +type Config struct { Alpha string `help:"a string"` Bravo int `help:"an int"` Charlie bool `help:"a bool"` Delta time.Duration `help:"a duration"` } -foo := FooConfig{ +c := Config{ Bravo: 42, Delta: 2 * time.Minute, } -opts.Parse(&foo) +opts.Parse(&c) ``` `opts` would *approximately* perform: ``` go -foo := FooConfig{} -set := flag.NewFlagSet("FooConfig") +foo := Config{} +set := flag.NewFlagSet("Config") set.StringVar(&foo.Alpha, "", "a string") set.IntVar(&foo.Bravo, 42, "an int") set.BoolVar(&foo.Charlie, false, "a bool") @@ -58,76 +91,19 @@ $ ./foo --help ``` -### Features (with examples) - -* Easy to use ([simple](example/simple/)) -* Promotes separation of CLI code and library code ([separation](example/separation/)) -* Automatically generated `--help` text via struct tags `help:"Foo bar"` ([help](example/help/)) -* Subcommands by nesting structs ([subcmds](example/subcmds/)) -* Default values by modifying the struct prior to `Parse()` ([defaults](example/defaults/)) -* Default values from a JSON config file, unmarshalled via your config struct ([config](example/config/)) -* Default values from environment, defined by your field names ([env](example/env/)) -* Infers program name from package name (and optional repository link) -* Extensible via `flag.Value` ([customtypes](example/customtypes/)) -* Customizable help text by modifying the default templates ([customhelp](example/customhelp/)) - -### [Simple Example](example/simple) - -``` go -package main - -import ( - "fmt" - - "github.com/jpillora/opts" -) - -type Config struct { - Foo string - Bar string -} - -func main() { - c := Config{} - opts.Parse(&c) - fmt.Println(c.Foo) - fmt.Println(c.Bar) -} -``` - -``` -$ ./myprog --foo hello --bar world -hello -world -``` - -``` plain -$ ./myprog --help - - Usage: myprog [options] - - Options: - --foo, -f - --bar, -b - --help, -h - -``` - ### All Examples -#### See all [example](example/)s here +--- -### Struct Tag API +#### See all [example](example/)s here -`opts` relies on struct tags to "compile" your flag set. Since there are defaults in all cases however, `opts` use any struct as as a flag set, even with no struct tags defined. A struct field can contain any number of struct tag properties. These come in the form: +--- -``` -A int `foo:"bar" ping:"pong"` -``` +### Struct Tag API -Below are the various properties available: +#### **Common tags** -#### **Common properties** +These tags are usable across all `type`s: * `name` - Name is used to display the field in the help text (defaults to the field name converted to lowercase and dashes) * `help` - Help is used to describe the field (defaults to "") @@ -135,21 +111,21 @@ Below are the various properties available: #### `type` defaults -Each field **must** have a `type`. By default a struct field will be assigned a `type` depending on the field type: +All fields will have a `type`. By default a struct field will be assigned a `type` depending on its field type: -| Field Type | Opt Type | -| ------------- |:-------------:| -| int | opt | -| string | opt | -| bool | opt | -| flag.Value | opt | -| time.Duration | opt | -| []string | arglist | -| struct | subcmd | +| Field Type | Default `type` | Valid `type`s | +| ------------- |:-------------:|:-------------------:| +| int | opt | opt, arg | +| string | opt | opt, arg, cmdname | +| bool | opt | opt, arg | +| flag.Value | opt | opt, arg | +| time.Duration | opt | opt, arg | +| []string | arglist | arglist | +| struct | subcmd | subcmd, embedded | -This default assignment can be overruled with a `type` struct tags. For example you could set a string struct field to be an `arg` field with `type:"arg"`. +This default assignment can be overridden with a `type` struct tag. For example you could set a string struct field to be an `arg` field with `type:"arg"`. -#### `type` list and type specific properties +#### `type` specific properties * **`opt`** diff --git a/Godeps/_workspace/src/github.com/jpillora/opts/example/intro/intro.go b/Godeps/_workspace/src/github.com/jpillora/opts/example/intro/intro.go new file mode 100644 index 000000000..d70aea939 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jpillora/opts/example/intro/intro.go @@ -0,0 +1,16 @@ +package main + +import ( + "fmt" + + "github.com/jpillora/opts" +) + +func main() { + config := struct { + File string `help:"file to load"` + Lines int `help:"number of lines to show"` + }{} + opts.Parse(&config) + fmt.Println(config) +} diff --git a/Godeps/_workspace/src/github.com/jpillora/opts/help.go b/Godeps/_workspace/src/github.com/jpillora/opts/opts_help.go similarity index 100% rename from Godeps/_workspace/src/github.com/jpillora/opts/help.go rename to Godeps/_workspace/src/github.com/jpillora/opts/opts_help.go diff --git a/Godeps/_workspace/src/github.com/jpillora/opts/util.go b/Godeps/_workspace/src/github.com/jpillora/opts/util.go index 4b2b53ea3..49790aaf9 100644 --- a/Godeps/_workspace/src/github.com/jpillora/opts/util.go +++ b/Godeps/_workspace/src/github.com/jpillora/opts/util.go @@ -4,12 +4,10 @@ import ( "bytes" "strconv" "strings" + "unicode" + "unicode/utf8" ) -func camel2dash(s string) string { - return strings.ToLower(s) -} - func camel2const(s string) string { b := bytes.Buffer{} var c rune @@ -73,3 +71,91 @@ func constrain(str string, width int) string { } return strings.Join(words, " ") } + +//borrowed from https://github.com/huandu/xstrings/blob/master/convert.go#L77 +func camel2dash(str string) string { + if len(str) == 0 { + return "" + } + + buf := &bytes.Buffer{} + var prev, r0, r1 rune + var size int + + r0 = '-' + + for len(str) > 0 { + prev = r0 + r0, size = utf8.DecodeRuneInString(str) + str = str[size:] + + switch { + case r0 == utf8.RuneError: + buf.WriteByte(byte(str[0])) + + case unicode.IsUpper(r0): + if prev != '-' { + buf.WriteRune('-') + } + + buf.WriteRune(unicode.ToLower(r0)) + + if len(str) == 0 { + break + } + + r0, size = utf8.DecodeRuneInString(str) + str = str[size:] + + if !unicode.IsUpper(r0) { + buf.WriteRune(r0) + break + } + + // find next non-upper-case character and insert `_` properly. + // it's designed to convert `HTTPServer` to `http_server`. + // if there are more than 2 adjacent upper case characters in a word, + // treat them as an abbreviation plus a normal word. + for len(str) > 0 { + r1 = r0 + r0, size = utf8.DecodeRuneInString(str) + str = str[size:] + + if r0 == utf8.RuneError { + buf.WriteRune(unicode.ToLower(r1)) + buf.WriteByte(byte(str[0])) + break + } + + if !unicode.IsUpper(r0) { + if r0 == '-' || r0 == ' ' || r0 == '_' { + r0 = '-' + buf.WriteRune(unicode.ToLower(r1)) + } else { + buf.WriteRune('-') + buf.WriteRune(unicode.ToLower(r1)) + buf.WriteRune(r0) + } + + break + } + + buf.WriteRune(unicode.ToLower(r1)) + } + + if len(str) == 0 || r0 == '-' { + buf.WriteRune(unicode.ToLower(r0)) + break + } + + default: + if r0 == ' ' || r0 == '_' { + r0 = '-' + } + + buf.WriteRune(r0) + } + } + + return buf.String() +} diff --git a/Godeps/_workspace/src/github.com/skratchdot/open-golang/open/exec.go b/Godeps/_workspace/src/github.com/skratchdot/open-golang/open/exec.go new file mode 100644 index 000000000..1b0e71368 --- /dev/null +++ b/Godeps/_workspace/src/github.com/skratchdot/open-golang/open/exec.go @@ -0,0 +1,18 @@ +// +build !windows,!darwin + +package open + +import ( + "os/exec" +) + +// http://sources.debian.net/src/xdg-utils/1.1.0~rc1%2Bgit20111210-7.1/scripts/xdg-open/ +// http://sources.debian.net/src/xdg-utils/1.1.0~rc1%2Bgit20111210-7.1/scripts/xdg-mime/ + +func open(input string) *exec.Cmd { + return exec.Command("xdg-open", input) +} + +func openWith(input string, appName string) *exec.Cmd { + return exec.Command(appName, input) +} diff --git a/Godeps/_workspace/src/github.com/skratchdot/open-golang/open/exec_darwin.go b/Godeps/_workspace/src/github.com/skratchdot/open-golang/open/exec_darwin.go new file mode 100644 index 000000000..16160e6f0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/skratchdot/open-golang/open/exec_darwin.go @@ -0,0 +1,15 @@ +// +build darwin + +package open + +import ( + "os/exec" +) + +func open(input string) *exec.Cmd { + return exec.Command("open", input) +} + +func openWith(input string, appName string) *exec.Cmd { + return exec.Command("open", "-a", appName, input) +} diff --git a/Godeps/_workspace/src/github.com/skratchdot/open-golang/open/exec_windows.go b/Godeps/_workspace/src/github.com/skratchdot/open-golang/open/exec_windows.go new file mode 100644 index 000000000..3ad667dfd --- /dev/null +++ b/Godeps/_workspace/src/github.com/skratchdot/open-golang/open/exec_windows.go @@ -0,0 +1,28 @@ +// +build windows + +package open + +import ( + "os" + "os/exec" + "path/filepath" + "strings" +) + +var ( + cmd = "url.dll,FileProtocolHandler" + runDll32 = filepath.Join(os.Getenv("SYSTEMROOT"), "System32", "rundll32.exe") +) + +func cleaninput(input string) string { + r := strings.NewReplacer("&", "^&") + return r.Replace(input) +} + +func open(input string) *exec.Cmd { + return exec.Command(runDll32, cmd, cleaninput(input)) +} + +func openWith(input string, appName string) *exec.Cmd { + return exec.Command("cmd", "/C", "start", "", appName, cleaninput(input)) +} diff --git a/Godeps/_workspace/src/github.com/skratchdot/open-golang/open/open.go b/Godeps/_workspace/src/github.com/skratchdot/open-golang/open/open.go new file mode 100644 index 000000000..b1f648ff5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/skratchdot/open-golang/open/open.go @@ -0,0 +1,50 @@ +/* + + Open a file, directory, or URI using the OS's default + application for that object type. Optionally, you can + specify an application to use. + + This is a proxy for the following commands: + + OSX: "open" + Windows: "start" + Linux/Other: "xdg-open" + + This is a golang port of the node.js module: https://github.com/pwnall/node-open + +*/ +package open + +/* + Open a file, directory, or URI using the OS's default + application for that object type. Wait for the open + command to complete. +*/ +func Run(input string) error { + return open(input).Run() +} + +/* + Open a file, directory, or URI using the OS's default + application for that object type. Don't wait for the + open command to complete. +*/ +func Start(input string) error { + return open(input).Start() +} + +/* + Open a file, directory, or URI using the specified application. + Wait for the open command to complete. +*/ +func RunWith(input string, appName string) error { + return openWith(input, appName).Run() +} + +/* + Open a file, directory, or URI using the specified application. + Don't wait for the open command to complete. +*/ +func StartWith(input string, appName string) error { + return openWith(input, appName).Start() +} diff --git a/Godeps/_workspace/src/github.com/skratchdot/open-golang/open/open_test.go b/Godeps/_workspace/src/github.com/skratchdot/open-golang/open/open_test.go new file mode 100644 index 000000000..5db2da27c --- /dev/null +++ b/Godeps/_workspace/src/github.com/skratchdot/open-golang/open/open_test.go @@ -0,0 +1,70 @@ +package open + +import "testing" + +func TestRun(t *testing.T) { + // shouldn't error + input := "https://google.com/" + err := Run(input) + if err != nil { + t.Errorf("open.Run(\"%s\") threw an error: %s", input, err) + } + + // should error + input = "xxxxxxxxxxxxxxx" + err = Run(input) + if err == nil { + t.Errorf("Run(\"%s\") did not throw an error as expected", input) + } +} + +func TestStart(t *testing.T) { + // shouldn't error + input := "https://google.com/" + err := Start(input) + if err != nil { + t.Errorf("open.Start(\"%s\") threw an error: %s", input, err) + } + + // shouldn't error + input = "xxxxxxxxxxxxxxx" + err = Start(input) + if err != nil { + t.Errorf("open.Start(\"%s\") shouldn't even fail on invalid input: %s", input, err) + } +} + +func TestRunWith(t *testing.T) { + // shouldn't error + input := "https://google.com/" + app := "firefox" + err := RunWith(input, app) + if err != nil { + t.Errorf("open.RunWith(\"%s\", \"%s\") threw an error: %s", input, app, err) + } + + // should error + app = "xxxxxxxxxxxxxxx" + err = RunWith(input, app) + if err == nil { + t.Errorf("RunWith(\"%s\", \"%s\") did not throw an error as expected", input, app) + } +} + +func TestStartWith(t *testing.T) { + // shouldn't error + input := "https://google.com/" + app := "firefox" + err := StartWith(input, app) + if err != nil { + t.Errorf("open.StartWith(\"%s\", \"%s\") threw an error: %s", input, app, err) + } + + // shouldn't error + input = "[]" + err = StartWith(input, app) + if err != nil { + t.Errorf("StartWith(\"%s\", \"%s\") shouldn't even fail on invalid input: %s", input, app, err) + } + +} diff --git a/README.md b/README.md index 96be10060..49ef0527a 100644 --- a/README.md +++ b/README.md @@ -40,16 +40,20 @@ $ cloud-torrent --help Usage: cloud-torrent [options] Options: - --port, -p Listening port (default 3000, env PORT) - --host, -h Listening interface (default all) - --auth, -a Optional basic auth (in form user:password) (env AUTH) - --configpath, -c Configuration file path (default cloud-torrent.json) - --log, -l Enable request logging + --title, -t Title of this instance (default Cloud Torrent, env TITLE) + --port, -p Listening port (default 3000, env PORT) + --host, -h Listening interface (default all) + --auth, -a Optional basic auth in form 'user:password' (env AUTH) + --config-path, -c Configuration file path (default cloud-torrent.json) + --key-path, -k TLS Key file path + --cert-path, -r TLS Certicate file path + --log, -l Enable request logging + --open, -o Open now with your default browser --help --version, -v Version: - 0.8.1 + 0.8.2 Read more: https://github.com/jpillora/cloud-torrent @@ -62,7 +66,7 @@ This project is the version 2 rewrite of the original [Node version](https://git ![overview](https://docs.google.com/drawings/d/1ekyeGiehwQRyi6YfFA4_tQaaEpUaS8qihwJ-s3FT_VU/pub?w=606&h=305) -Credits to @anacrolix for https://github.com/anacrolix/torrent +Credits to `anacrolix` for https://github.com/anacrolix/torrent #### MIT License diff --git a/app.json b/app.json index 92fd64e68..fa2aae11b 100644 --- a/app.json +++ b/app.json @@ -3,6 +3,10 @@ "website": "https://github.com/jpillora/cloud-torrent", "repository": "https://github.com/jpillora/cloud-torrent", "env": { + "TITLE": { + "description": "Optionally set the title of this instance (defaults to 'Cloud Torrent')", + "required": false + }, "AUTH": { "description": "Optional HTTP authentication in the form user:password", "required": false diff --git a/main.go b/main.go index 683a126a0..f80957c43 100644 --- a/main.go +++ b/main.go @@ -11,14 +11,16 @@ var VERSION = "0.0.0-src" //set with ldflags func main() { s := server.Server{ + Title: "Cloud Torrent", Port: 3000, ConfigPath: "cloud-torrent.json", } - opts.New(&s). - Version(VERSION). - PkgRepo(). - Parse() + o := opts.New(&s) + o.Version(VERSION) + o.PkgRepo() + o.LineWidth = 96 + o.Parse() if err := s.Run(VERSION); err != nil { log.Fatal(err) diff --git a/server/server.go b/server/server.go index 1376182b1..58cd2dee0 100644 --- a/server/server.go +++ b/server/server.go @@ -9,7 +9,6 @@ import ( "os" "path/filepath" "runtime" - "strconv" "strings" "sync" "time" @@ -19,16 +18,21 @@ import ( "github.com/jpillora/go-realtime" "github.com/jpillora/requestlog" "github.com/jpillora/scraper/scraper" + "github.com/skratchdot/open-golang/open" ) //Server is the "State" portion of the diagram type Server struct { //config + Title string `help:"Title of this instance" env:"TITLE"` Port int `help:"Listening port" env:"PORT"` Host string `help:"Listening interface (default all)"` - Auth string `help:"Optional basic auth (in form user:password)" env:"AUTH"` + Auth string `help:"Optional basic auth in form 'user:password'" env:"AUTH"` ConfigPath string `help:"Configuration file path"` + KeyPath string `help:"TLS Key file path"` + CertPath string `help:"TLS Certicate file path" short:"r"` Log bool `help:"Enable request logging"` + Open bool `help:"Open now with your default browser"` //http handlers files, static http.Handler scraper *scraper.Handler @@ -46,6 +50,7 @@ type Server struct { Torrents map[string]*engine.Torrent Users map[string]*realtime.User Stats struct { + Title string Version string Runtime string Uptime time.Time @@ -53,7 +58,18 @@ type Server struct { } } -func (s *Server) init() error { +func (s *Server) Run(version string) error { + + tls := s.CertPath != "" || s.KeyPath != "" //poor man's XOR + if tls && (s.CertPath == "" || s.KeyPath == "") { + return fmt.Errorf("You must provide both key and cert paths") + } + + s.state.Stats.Title = s.Title + s.state.Stats.Version = version + s.state.Stats.Runtime = strings.TrimPrefix(runtime.Version(), "go") + s.state.Stats.Uptime = time.Now() + //init maps s.state.Users = map[string]*realtime.User{} //will use a the local embed/ dir if it exists, otherwise will use the hardcoded embedded binaries @@ -116,8 +132,38 @@ func (s *Server) init() error { } }() - //ready - return nil + host := s.Host + if host == "" { + host = "0.0.0.0" + } + addr := fmt.Sprintf("%s:%d", host, s.Port) + proto := "http" + if tls { + proto += "s" + } + log.Printf("Listening at %s://%s", proto, addr) + + if s.Open { + openhost := host + if openhost == "0.0.0.0" { + openhost = "localhost" + } + go func() { + time.Sleep(1 * time.Second) + open.Run(fmt.Sprintf("%s://%s:%d", proto, openhost, s.Port)) + }() + } + + h := http.Handler(http.HandlerFunc(s.handle)) + if s.Log { + h = requestlog.Wrap(h) + } + + if tls { + return http.ListenAndServeTLS(addr, s.CertPath, s.KeyPath, h) + } else { + return http.ListenAndServe(addr, h) + } } func (s *Server) reconfigure(c engine.Config) error { @@ -136,26 +182,6 @@ func (s *Server) reconfigure(c engine.Config) error { return nil } -func (s *Server) Run(version string) error { - - s.state.Stats.Version = version - s.state.Stats.Runtime = strings.TrimPrefix(runtime.Version(), "go") - s.state.Stats.Uptime = time.Now() - - if err := s.init(); err != nil { - return err - } - // TODO if Open { - // cross platform open - https://github.com/skratchdot/open-golang - // } - h := http.Handler(http.HandlerFunc(s.handle)) - if s.Log { - h = requestlog.Wrap(h) - } - log.Printf("Listening on %d...", s.Port) - return http.ListenAndServe(s.Host+":"+strconv.Itoa(s.Port), h) -} - func (s *Server) handle(w http.ResponseWriter, r *http.Request) { //basic auth if s.Auth != "" { diff --git a/server/server_files.go b/server/server_files.go index f4d354dcc..8b5b896f1 100644 --- a/server/server_files.go +++ b/server/server_files.go @@ -34,17 +34,29 @@ func (s *Server) listFiles() *fsNode { func (s *Server) serveFiles(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/download/") { + url := strings.TrimPrefix(r.URL.Path, "/download/") //dldir is absolute dldir := s.state.Config.DownloadDirectory - file := filepath.Join(dldir, strings.TrimPrefix(r.URL.Path, "/download/")) + file := filepath.Join(dldir, url) //only allow fetches/deletes inside the dl dir if !strings.HasPrefix(file, dldir) || dldir == file { http.Error(w, "Nice try\n"+dldir+"\n"+file, http.StatusBadRequest) return } + info, err := os.Stat(file) + if err != nil { + http.Error(w, "File stat error: "+err.Error(), http.StatusBadRequest) + return + } switch r.Method { case "GET": - http.ServeFile(w, r, file) + f, err := os.Open(file) + if err != nil { + http.Error(w, "File open error: "+err.Error(), http.StatusBadRequest) + return + } + http.ServeContent(w, r, info.Name(), info.ModTime(), f) + f.Close() case "DELETE": if err := os.RemoveAll(file); err != nil { http.Error(w, "Delete failed: "+err.Error(), http.StatusInternalServerError) diff --git a/static/files.go b/static/files.go index 6763d0d9d..28566df7c 100644 --- a/static/files.go +++ b/static/files.go @@ -462,7 +462,7 @@ func filesCssThemesDefaultAssetsImagesFlagsPng() (*asset, error) { return a, nil } -var _filesIndexHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x9c\x57\x5b\x4f\xe3\xb8\x17\x7f\xee\x7c\x0a\x13\xfd\x25\x66\x24\x52\x0f\xa3\xff\x53\x49\x22\xcd\x32\x88\x5d\x2d\x2b\x24\x60\xf7\x65\x34\x5a\xb9\xc9\x69\x62\x70\xec\xac\xed\xb4\x74\xab\x7e\xf7\x3d\x76\x2e\x4d\x2f\x40\x19\x1e\xda\xe6\xf8\x5c\x7e\xe7\xf6\x8b\x89\x0a\x5b\x8a\xe4\xc3\x28\x2a\x80\x65\xf8\x3d\x8a\x2c\xb7\x02\x92\x4b\xa1\xea\x8c\x3c\x28\xad\x41\xda\x88\x36\xc2\x0f\x04\xff\x46\x51\x09\x96\x11\xc9\x4a\x88\x83\x39\x87\x45\xa5\xb4\x0d\x48\xaa\xa4\x45\xd5\x38\x58\xf0\xcc\x16\x71\x06\x73\x9e\x42\xe8\x1f\xce\x08\x97\xdc\x72\x26\x42\x93\x32\x01\xf1\xf9\xf8\xf3\x19\x29\xd9\x33\x2f\xeb\x72\x28\xaa\x0d\x68\xff\xcc\xa6\x28\x92\x2a\xf0\x78\x04\x97\x4f\x44\x83\x88\x03\x63\x97\x02\x4c\x01\x80\xe1\xec\xb2\xc2\xf0\x16\x9e\x2d\x4d\x8d\x09\x48\xa1\x61\x16\x07\xf8\x93\xde\x30\xab\xfc\xc7\xd8\x1d\xfc\x94\x0b\x03\x25\x93\x96\xa7\xe3\x92\xcb\x9f\xf7\xc2\xaa\xea\x90\x31\xc7\x4a\xf5\x6a\xae\xca\xe1\x8c\xcd\x9d\x70\x5c\xc9\xbc\xf3\xc7\x4b\x96\x03\x7d\x0e\x1b\x65\xea\x5d\xf8\xa8\xbb\xf1\x92\xef\x32\x0f\xd1\x0d\x7b\xfa\xb1\xca\xb8\xa9\x04\x5b\x4e\x88\x54\x12\x2e\xd6\x11\xf5\x16\xae\xb9\xb4\xed\x6e\x34\x55\xd9\x92\xa0\x05\x62\x8b\x03\xfc\xc0\xbe\x09\x66\x4c\xf3\x3b\x89\x32\x3e\x27\x9d\xbf\xee\x24\x45\x24\x4d\x0e\xed\x29\x47\xe0\x27\x88\x4b\x42\x6a\x21\xeb\x3d\xb4\x92\x70\xc1\xb4\xe4\x32\x27\x35\xc7\xb6\xcf\x41\xa3\x0e\x71\x5f\x1c\x1b\x8b\x5d\x37\xd6\x61\x21\x29\xce\x0a\x68\xc2\x04\xcf\x25\x2a\x18\xc8\x4b\x94\xf8\x38\x4d\xa0\xd6\x2b\x7a\x71\xb9\xfa\xf1\x62\x5c\x82\x6e\x55\x8a\xf3\x81\x46\x1f\xc7\xb9\xee\x54\x46\xdf\xb8\xe9\x51\x7a\x1b\x5a\x9c\x1f\x6d\x1c\xf1\x4e\x45\xe3\x11\xa2\x2c\xac\xcf\xca\x37\x24\x89\x28\x4f\xb6\x5d\x52\xc4\xec\x8b\xd4\xff\x38\x09\x43\xf2\xf0\xdb\xc3\xcd\x15\x89\x0f\xfd\x91\x30\xec\x8b\xda\x46\xf2\x4b\xd6\xe5\xf7\xa5\xc5\xc1\xda\x51\x29\xac\xad\xcc\x84\xd2\x9c\xdb\xa2\x9e\x8e\x53\x55\xd2\xc7\x8a\x0b\xa1\x34\xa3\xcd\x18\xd9\x66\x59\x03\x6f\x37\xb2\x4c\xe7\x80\xeb\xf8\xf7\x54\x30\xf9\xd4\x0a\x5d\xf7\x70\x15\xe3\x40\xa8\x5c\xfd\xaa\x30\x71\x12\x93\x19\x13\x06\x36\x0a\xa5\xc2\x3d\x74\x47\xdb\x5a\x56\xd7\xbb\x4a\x02\xd8\x1c\x0e\xf9\x6a\xa0\x0f\x6a\x38\x15\x35\x34\xa5\x6b\xc6\xcb\x0b\x57\x1e\xf6\xe4\xa4\xb7\x3f\x23\x4d\x72\x13\xd2\x8b\xd6\x4d\xad\xc9\x16\x1d\x35\x85\xa1\xac\x6b\xc1\x97\xbd\xa9\x31\x96\xd9\xda\x6c\x5a\x89\x41\x4d\xa1\x16\xae\x99\xff\xd4\x60\x2c\x96\xaf\x96\x96\x24\xe4\x73\x9b\xd2\x66\x1c\x52\xae\xd3\x5a\x30\x8d\x18\x58\xe6\x3a\xae\xa1\x02\x66\x77\x1a\xdf\x3a\x4d\x05\x4f\x9f\xe2\xe0\x7f\x5a\x29\xe7\x53\xce\x78\x3e\x86\x8c\x5b\xac\xc4\xc9\x9e\xf0\x62\x2b\xf9\x5c\x03\xc8\x09\xd9\xd3\x5a\xbf\x8c\x08\xe9\xd1\x55\xb9\xa9\xa3\xdf\xed\x38\x98\xe1\x5e\x84\x86\xff\x0b\x13\x72\xfe\xff\xea\xf9\xe2\x75\x88\xaa\x94\x7c\x07\x60\x2f\xba\xd8\x74\xf7\x20\xc6\x5e\xf1\x15\x84\xc8\x5a\x12\xec\x7b\x10\x1e\x2a\xfc\xf6\xb2\xed\xa1\x22\x2d\xac\x7e\xb9\xcf\xb0\x47\xd9\x84\x6c\x38\x89\xac\x87\x2b\x7a\x70\x33\xaf\x6f\x6e\x7f\xf9\x7a\x43\xae\xee\xee\x6e\xef\xc8\x1f\x57\xf7\xf7\x5f\xaf\x8f\x58\xd4\x96\xfd\x40\xeb\x60\x80\x1c\x1f\x15\xe6\x0e\xc6\x74\x54\x39\xc8\x0c\x87\xdc\x6c\xcd\xfe\xa0\x1d\x68\x88\x8d\x90\xb5\x10\x03\xbc\x83\x31\xee\x18\xe9\xca\x05\xe8\xf1\x8f\xa2\x2a\x59\xad\xd0\x76\x8d\x14\x5f\x0d\x52\xf3\x2f\x09\x2c\x00\x57\x72\x40\xca\x38\x58\x4d\x64\xec\x83\x56\x42\xb8\xc5\xbe\xf4\xe2\xcb\x5e\xe2\x15\xb8\x4c\x45\x9d\x01\x31\x3a\x8d\x83\x53\x0b\x25\xbe\x4c\x2c\xd0\x76\x36\xdd\x3d\xe1\xd4\xc1\x6c\x23\x24\x07\xa2\xb9\x11\xd9\x8b\x75\x8b\xc2\xe3\x22\xf9\x09\x3b\x26\x4e\xcb\x74\x66\x2f\x56\x4b\x10\xe6\xb8\x78\x9d\x9b\xa3\x62\x66\x6a\x21\x1d\x25\xec\x07\xfd\xd6\x9d\x1c\x17\xb5\x77\x74\x20\xac\x8b\x3b\xc3\xc9\x00\xdd\x34\xda\x54\x4c\x26\x03\xc6\x3f\xe6\x6d\x80\x57\x88\x6d\xea\x4f\xba\x13\x47\x99\x47\xb9\xd8\x79\xa1\xec\x39\xdc\x3a\x76\x5e\xdd\x0b\xde\xb8\x62\xad\x56\xc4\x71\x2f\x8c\xef\xf1\xd3\x8c\xff\x6a\xc5\xeb\xf5\x28\xc4\x29\xf5\xe9\xf4\x89\x75\xdb\x24\xeb\xf2\x77\x58\x9a\x8f\x8d\xe1\x9f\xc8\x72\xe6\x13\x52\xf3\x79\x80\x53\x4e\x0e\x1e\xae\xd7\xfe\xae\x68\x36\x14\x70\xc0\x7d\x72\xad\x76\xe1\xdc\x21\xe9\xf3\x12\x88\x5b\x9c\x4e\x39\xa2\x5d\xc1\x7d\xd3\x53\xcd\x2b\xdb\xb4\xec\xd1\xd0\x39\xc8\x4c\x69\x8a\xef\x0c\xbd\x0c\x8d\xd5\x48\x4a\xe3\x47\xe3\x5b\xe6\x15\x93\x97\x6d\x98\xcc\x1d\x9b\xf9\x2b\xe4\x91\x26\xa5\x72\x77\xa0\xb7\x2d\xa8\x06\x26\x5c\x22\x2f\xa9\x25\x0b\x8e\x0e\x17\x63\xbc\xd4\x21\xbf\x74\x0f\x1d\x20\x95\xd5\x02\x3e\x9e\xe2\xe1\xe9\x19\xf9\xfe\xe3\xd3\xc5\x6b\xd0\x9a\xed\x1f\xcc\xfb\x9b\xc9\xb8\x2d\x7e\x8f\x7e\xb7\x85\xef\xb1\xe9\x77\xe8\x3d\x46\xb5\xe5\xc2\xbc\xa9\xd5\xdd\xfd\xc3\xb4\x80\xf4\x69\xaa\x9e\xdf\xb4\xd0\xf5\x5e\xbf\x1a\x42\x8e\xa8\xbb\x6e\x27\x1f\xf0\x8a\xe2\xff\xc9\xfa\x2f\x00\x00\xff\xff\x57\x41\x00\x9d\x6c\x0d\x00\x00") +var _filesIndexHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x9c\x57\x5b\x6f\xdb\xb6\x17\x7f\x76\x3e\x05\x23\xfc\x81\xb4\x40\x64\x35\xc5\xff\xc9\x91\x04\x74\x6d\x90\x0d\xcb\x10\x20\xc9\xf6\x52\x14\x03\x2d\x1d\x4b\x4c\x28\x52\x23\x29\x3b\x9e\xa1\xef\xbe\x43\xea\x62\xd9\x72\x12\xa7\x7e\x90\xad\x73\xfd\x9d\x2b\xe9\x30\x37\x05\x27\x22\xf3\x69\x59\x46\x1e\x3e\x3c\x12\x9f\x4c\xc2\x1c\x68\x8a\xdf\x93\xd0\x30\xc3\xc1\xf2\xe7\x4c\xa4\x91\xa7\x0d\x35\x30\xbd\xc7\xa7\x9e\x3e\x58\x96\x17\x7f\xe5\xb2\x4a\xc9\x83\x54\x0a\x84\x09\x03\xa7\x10\x9f\x10\xfc\x4c\xc2\x02\x0c\x25\x82\x16\x10\x79\x4b\x06\xab\x52\x2a\xe3\x91\x44\x0a\x83\xa2\x91\xb7\x62\xa9\xc9\xa3\x14\x96\x2c\x01\xdf\xbd\x9c\x13\x26\x98\x61\x94\xfb\x3a\xa1\x1c\xa2\x8b\xe9\xa7\x73\x52\xd0\x67\x56\x54\xc5\x90\x54\x69\x50\xee\x9d\xce\x91\x24\xa4\xe7\xb0\x72\x26\x9e\x88\x02\x6e\x61\xae\x39\xe8\x1c\x00\xdd\x99\x75\x89\xee\x0d\x3c\x9b\x20\xd1\xda\x23\xb9\x82\x45\xe4\xe1\xcf\xe0\x86\x1a\xe9\x1e\x53\xcb\xf8\x29\x13\x1a\x0a\x2a\x0c\x4b\xa6\x05\x13\x3f\x6f\x05\xd3\x7e\x48\x99\x61\xa6\x7a\x31\x9b\x65\x7f\x41\x97\x96\x38\x2d\x45\xd6\xd9\x63\x05\xcd\x20\x78\xf6\x1b\xe1\xc0\x99\x70\x5e\xf7\xfd\xc5\xdf\xb1\x8a\x68\x86\x3e\xfd\xd8\xa4\x4c\x97\x9c\xae\x67\x44\x48\x01\x97\x75\x18\x38\x0d\x5b\xf8\xa0\xad\x7c\x38\x97\xe9\x9a\x24\x9c\x6a\xdd\xb4\x45\x1c\xa6\x6c\x49\x3a\x13\x1d\x27\x41\xe7\x0d\xec\x96\xcb\x10\xeb\x29\x42\x11\x90\x18\x48\xbd\x5e\xae\xa1\xf8\x2b\xaa\x04\x13\x19\xa9\x18\x56\x7a\x09\x0a\x65\x88\xfd\x62\x58\x4b\x2c\xb4\x36\xd6\x3d\x49\xb0\x3d\x40\x11\xca\x59\x26\x50\x40\x43\x56\x20\xc5\xf9\x69\x1c\xb5\x56\xd1\x8a\x0d\xcf\x75\x14\x65\x02\x54\x2b\x92\x5f\x0c\x24\x7a\x3f\xd6\x74\x27\x32\xf9\xc6\x74\x8f\xd2\xe9\x04\xf9\xc5\xd1\xca\x21\xeb\x44\x14\xb2\x10\x65\x6e\x5c\x54\xae\x06\x71\x18\xb0\x78\xd7\x64\x80\x98\x5d\x92\xfa\x1f\xa7\xbe\x4f\x1e\x7e\x7b\xb8\xb9\x22\xd1\xa1\x0f\xf1\xfd\x3e\xa9\xad\x27\xd3\x4c\x5b\x03\xf1\x73\x8b\x83\xb6\xdd\x91\x1b\x53\xea\x59\x10\x64\xcc\xe4\xd5\x7c\x9a\xc8\x22\x78\x2c\x19\xe7\x52\xd1\xa0\xe9\x1c\xd3\xcc\xa7\xe7\xf4\x26\x86\xaa\x0c\x70\x02\xff\x9e\x73\x2a\x9e\x5a\xa2\xad\x1e\x4e\x5f\xe4\x71\x99\xc9\x5f\x25\x06\x4e\x22\xb2\xa0\x5c\xc3\x56\xa0\x90\x38\x7a\x96\xb5\x2b\x65\x54\xb5\x2f\xc4\x81\x2e\xe1\x90\xad\x06\xfa\x20\x87\x73\x5e\x41\x93\xba\xa6\xbd\x1c\x71\xe3\x60\xcf\x4e\x7b\xfd\x73\xd2\x04\x37\x23\x3d\xa9\x6e\x72\x4d\xc8\x66\x43\x46\x8b\x89\xd4\x75\x93\xa3\x80\x76\xd5\xf8\x3c\x6a\x20\xab\x55\xe9\x6d\x55\xd1\xbf\xce\xe5\xca\xd6\xf5\x9f\x0a\xb4\xc1\x4c\x56\xc2\x90\x98\x7c\x6a\xa3\xdb\x76\x46\xc2\x54\x52\x71\xaa\x10\x0e\x4d\x6d\xf1\x15\x94\x40\xcd\x5e\x0f\xb4\x46\x13\xce\x92\xa7\xc8\xfb\x9f\x92\xd2\xda\x14\x0b\x96\x4d\x21\x65\x06\x93\x72\x3a\x22\x5e\xee\xe4\x21\x53\x00\x62\x46\x46\x52\xf5\xcb\x88\x70\x39\xda\x84\x37\x29\x75\x93\x1d\x79\x0b\x1c\x11\x5f\xb3\x7f\x61\x46\x2e\xfe\x5f\x3e\x5f\xbe\x0e\x51\x16\x82\xed\x01\xec\x49\x97\xdb\x42\x1f\xc4\xd8\x0b\xbe\x82\x10\x77\x96\x00\xf3\x1e\x84\x87\x12\xbf\x3b\x77\x23\x54\xa4\x85\xd5\xcf\xf9\x39\xd6\x28\x9d\x91\xed\x7a\x22\xf5\x70\x5a\x0f\x0e\xe9\xf5\xcd\xed\x2f\x5f\x6e\xc8\xd5\xdd\xdd\xed\x1d\xf9\xe3\xea\xfe\xfe\xcb\xf5\x11\x33\xdb\x2e\x42\x50\xca\x1b\x20\xc7\x57\x89\xb1\x83\xd6\xdd\xd6\x1c\x44\x86\xfd\xae\x77\xc6\x60\x50\x0e\x54\xc4\x42\x88\x8a\xf3\x01\xde\x41\x1b\x77\xcb\xe9\xca\x3a\xe8\xf1\x4f\xc2\x32\xde\x6c\x50\xb7\xc6\x05\x5f\x0e\x42\x73\x47\x04\x26\x80\x49\x31\xd8\xcf\xd8\x58\x8d\x67\xac\x83\x92\x9c\xdb\x19\xff\xea\xc8\x5f\x7b\x8a\x13\x60\x22\xe1\x55\x0a\x44\xab\x24\xf2\xce\x0c\x14\x78\x94\x18\x08\xda\xde\xb4\xf7\x89\x33\x0b\xb3\xf5\x10\x1f\xf0\x66\x5b\x64\xe4\xeb\x16\x89\xc7\x79\x72\x1d\x76\x8c\x9f\x76\xe9\xe9\x91\xaf\xf6\xb6\xa2\x8f\xf3\xd7\x99\x39\xca\x67\x2a\x57\xc2\xae\x84\xb1\xd3\x6f\x1d\xe7\x38\xaf\xbd\xa1\x03\x6e\xad\xdf\x05\x76\x06\xa8\xa6\xd0\xba\xa4\x22\x1e\x2c\xff\x63\x0e\x06\xbc\x40\xec\x9e\x02\x71\xc7\xb1\x2b\xf3\x28\x13\x7b\x67\xcb\xc8\xe0\x0e\xdb\x5a\xb5\x67\xbd\xb6\xc9\xda\xdb\xd8\x7f\xb5\xe4\xba\x9e\xf8\xd8\xa5\x2e\x9c\x3e\xb0\x6e\x9a\x44\x55\xfc\x0e\x6b\xfd\xa1\x51\xfc\x13\xb7\x9c\xfe\x88\xab\xf9\xc2\xc3\x2e\x27\x07\x99\x75\xed\x6e\x8a\x7a\xbb\x02\x0e\x98\x8f\xc7\x91\x4a\x84\x9f\x4d\xa5\xca\xc6\x21\x5d\x4b\x17\xc7\x1e\xfe\x3b\x3c\x25\x58\x61\xcf\x9c\xad\xf5\x30\xe8\x2a\xe4\xba\x24\x51\xac\x34\x4d\x8d\x1f\x75\xb0\x04\x91\x4a\x15\xe0\x21\xa3\xd6\xbe\x36\x0a\xb7\xd8\xf4\x51\xbb\x1a\x3b\xc1\xf8\x65\x1d\x84\x66\xd7\x9f\xbb\x71\x1e\xa9\x52\x48\x7b\x7f\x7a\x5b\x23\x50\x40\xb9\x0d\xe4\x25\xb1\x78\x85\xff\x02\xe4\x6a\x8a\x17\x42\x5c\x48\xdd\x4b\x07\x48\xa6\x15\x87\x0f\x67\xc8\x3c\x3b\x27\xdf\x7f\x7c\xbc\x7c\x0d\x5a\xb3\x2e\x06\x03\xf2\x66\x30\x76\xec\xdf\x23\xdf\x8d\xed\x7b\x74\xfa\xa1\x7b\x8f\x52\x65\x18\xd7\x6f\x4a\x75\x7f\x15\xfc\x24\x87\xe4\x69\x2e\x9f\xdf\xd4\x50\xd5\xa8\x5e\xcd\x06\x0f\x03\x7b\x3b\x8f\x4f\xf0\x4e\x83\xab\x21\x3e\xf9\x2f\x00\x00\xff\xff\x99\x67\x76\x9e\xc5\x0d\x00\x00") func filesIndexHtmlBytes() ([]byte, error) { return bindataRead( @@ -477,7 +477,7 @@ func filesIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "files/index.html", size: 3436, mode: os.FileMode(420), modTime: time.Unix(1442247411, 0)} + info := bindataFileInfo{name: "files/index.html", size: 3525, mode: os.FileMode(420), modTime: time.Unix(1442673631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -517,7 +517,7 @@ func filesJsDownloadsControllerJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "files/js/downloads-controller.js", size: 2301, mode: os.FileMode(420), modTime: time.Unix(1442236444, 0)} + info := bindataFileInfo{name: "files/js/downloads-controller.js", size: 2301, mode: os.FileMode(420), modTime: time.Unix(1442672875, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -737,7 +737,7 @@ func filesTemplateDownloadsHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "files/template/downloads.html", size: 401, mode: os.FileMode(420), modTime: time.Unix(1442242094, 0)} + info := bindataFileInfo{name: "files/template/downloads.html", size: 401, mode: os.FileMode(420), modTime: time.Unix(1442672870, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/static/files/index.html b/static/files/index.html index 9ee5e89b2..789106c57 100644 --- a/static/files/index.html +++ b/static/files/index.html @@ -1,6 +1,6 @@ - + - Cloud Torrent + Cloud Torrent @@ -8,7 +8,7 @@ -
+

@@ -27,7 +27,7 @@

ng-init="logoHover = false" ng-mouseover="logoHover = true" ng-mouseleave="logoHover = false"> - Cloud Torrent + {{ state.Stats.Title }}

@@ -57,7 +57,7 @@

github.com/jpillora/cloud-torrent version {{ state.Stats.Version }} - {{ numKeys(state.Users) }} users connected - - Go {{ state.Stats.Runtime }} + Go {{ state.Stats.Runtime }}