├── .gitignore ├── .dockerignore ├── .gitlab-ci.yml ├── vendor └── vendor.json ├── templating.go ├── Makefile ├── README.md ├── index.html.erb ├── LICENSE ├── Dockerfile ├── helpers.go ├── assets └── index.html ├── main.go ├── handler.go └── bindata.go /.gitignore: -------------------------------------------------------------------------------- 1 | builds 2 | vendor/*/ 3 | public 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | builds 3 | vendor 4 | !vendor/vendor.json 5 | LICENSE 6 | README.md 7 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: docker:stable 2 | 3 | variables: 4 | DOCKER_DRIVER: overlay2 5 | 6 | services: 7 | - docker:dind 8 | 9 | pages: 10 | script: 11 | - apk add --update make ruby 12 | - make website 13 | artifacts: 14 | paths: 15 | - public 16 | -------------------------------------------------------------------------------- /vendor/vendor.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "", 3 | "ignore": "test", 4 | "package": [ 5 | { 6 | "checksumSHA1": "RqDlQ3BLWYnTsXF/cXNMx5rvG5Q=", 7 | "path": "github.com/dustin/go-humanize", 8 | "revision": "bd88f87ad3a420f7bcf05e90566fd1ceb351fa7f" 9 | } 10 | ], 11 | "rootPath": "gitlab.com/dAnjou/goup" 12 | } 13 | -------------------------------------------------------------------------------- /templating.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/dustin/go-humanize" 5 | "html/template" 6 | "os" 7 | ) 8 | 9 | type context struct { 10 | IsRoot bool 11 | Upload bool 12 | DirEntries []os.FileInfo 13 | SortOrder map[string]string 14 | } 15 | 16 | var ( 17 | assetIndexHTML, err = Asset("assets/index.html") 18 | defaultTemplate = string(assetIndexHTML) 19 | funcMap = template.FuncMap{ 20 | "size": func(b int64) string { 21 | return humanize.Bytes(uint64(b)) 22 | }, 23 | //"time": humanize.Time, 24 | } 25 | tmpl = template.Must(template.New("index").Funcs(funcMap).Parse(defaultTemplate)) 26 | ) 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOUP_IMAGE ?= goup 2 | GOUP_PORT ?= 4000 3 | GOUP_DIR ?= $(PWD) 4 | 5 | image: 6 | docker build --tag $(GOUP_IMAGE) . 7 | 8 | builds: image 9 | GOUP_CONTAINER=$$(docker create $(GOUP_IMAGE)) && \ 10 | docker cp $$GOUP_CONTAINER:/builds . ; \ 11 | docker rm $$GOUP_CONTAINER 12 | 13 | run: image 14 | docker run --rm -v $(GOUP_DIR):/data -p $(GOUP_PORT):4000 $(GOUP_IMAGE) 15 | 16 | website: builds 17 | rm -rf public && mkdir public 18 | cp -a builds public && rm public/builds/goup 19 | export GOUP_VERSION=$$(builds/goup -version 2>&1) && \ 20 | export GOUP_USAGE=$$(builds/goup -help 2>&1) && \ 21 | export GOUP_BUILDS=$$(ls -1 public/builds) && \ 22 | erb index.html.erb > public/index.html 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Goup 2 | ==== 3 | 4 | Goup is a small one-binary file server written in Go which also allows to upload 5 | files. You can run it standalone or deploy it as FastCGI application. 6 | 7 | ## Installation for Go users 8 | 9 | `go get -u gitlab.com/dAnjou/goup` 10 | 11 | ## Usage 12 | 13 | Usage of goup: 14 | -addr="0.0.0.0:4000": listen on this address 15 | -auth="": comma-separated list of what will be protected by HTTP Basic authentication (index,download,upload) 16 | -dir=".": directory for storing and serving files 17 | -index="": serve this file if it exists in the current directory instead of a listing 18 | -mode="http": run either standalone (http) or as FCGI application (fcgi) 19 | -noupload=false: enable or disable uploads 20 | -password="": password for HTTP Basic authentication (-auth needs to be set) 21 | -user="": user for HTTP Basic authentication (-auth needs to be set) 22 | -v=false: verbose output (no output at all by default) 23 | -version=false: show version and exit 24 | -------------------------------------------------------------------------------- /index.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Goup 8 | 24 | 25 | 26 |

Goup <%= ENV['GOUP_VERSION'] %>

27 |

Builds

28 | 33 |

Usage

34 |
<%= ENV['GOUP_USAGE'] %>
35 | 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Max Ludwig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1-alpine 2 | 3 | ENV version 0.2.2 4 | 5 | # install govendor and fpm 6 | RUN apk add --no-cache --quiet \ 7 | ruby \ 8 | ruby-dev \ 9 | gcc \ 10 | libffi-dev \ 11 | make \ 12 | libc-dev \ 13 | rpm \ 14 | govendor \ 15 | git \ 16 | tar \ 17 | && gem install --quiet --no-ri --no-rdoc fpm 18 | 19 | # create volume for files and directories to be served by Goup 20 | RUN mkdir /data 21 | VOLUME /data 22 | 23 | # build Goup 24 | ENV tarball_name goup_${version}_64-bit 25 | RUN mkdir /${tarball_name} 26 | RUN mkdir /go/src/goup 27 | WORKDIR /go/src/goup 28 | COPY . /go/src/goup 29 | RUN govendor sync 30 | ENV GOOS linux 31 | ENV GOARCH amd64 32 | ENV CGO_ENABLED 0 33 | RUN go build -o /${tarball_name}/goup -ldflags "-X main.VERSION=${version}" -v . 34 | 35 | # create packages 36 | RUN mkdir /builds 37 | WORKDIR /builds 38 | RUN cp /${tarball_name}/goup . 39 | ENV PATH /builds:${PATH} 40 | RUN tar -czf ${tarball_name}.tar.gz /${tarball_name}/goup 41 | RUN fpm -s dir -t deb --name goup --version ${version} /${tarball_name}/goup=/usr/local/bin/goup 42 | RUN fpm -s dir -t rpm --name goup --version ${version} /${tarball_name}/goup=/usr/local/bin/goup 43 | 44 | EXPOSE 4000 45 | 46 | CMD ["goup", "-dir", "/data"] 47 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "sort" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | asc = false 11 | desc = true 12 | sortOrderMap = map[string]string{ 13 | "mode": "asc", 14 | "time": "asc", 15 | "size": "asc", 16 | "name": "asc", 17 | } 18 | ) 19 | 20 | type sortable struct { 21 | Infos *[]os.FileInfo 22 | Key string 23 | Order bool 24 | } 25 | 26 | func xnor(a, b bool) bool { return !((a || b) && (!a || !b)) } 27 | 28 | func (s sortable) Len() int { return len(*s.Infos) } 29 | func (s sortable) Less(i, j int) bool { 30 | switch s.Key { 31 | case "mode": 32 | return xnor((*s.Infos)[i].Mode() > (*s.Infos)[j].Mode(), s.Order) 33 | case "time": 34 | return xnor((*s.Infos)[i].ModTime().After((*s.Infos)[j].ModTime()), s.Order) 35 | case "size": 36 | return xnor((*s.Infos)[i].Size() > (*s.Infos)[j].Size(), s.Order) 37 | default: 38 | return xnor((*s.Infos)[i].Name() > (*s.Infos)[j].Name(), s.Order) 39 | } 40 | } 41 | func (s sortable) Swap(i, j int) { (*s.Infos)[i], (*s.Infos)[j] = (*s.Infos)[j], (*s.Infos)[i] } 42 | 43 | func readDir(dirname string, sortby string, order bool) ([]os.FileInfo, error) { 44 | f, err := os.Open(dirname) 45 | if err != nil { 46 | return nil, err 47 | } 48 | list, err := f.Readdir(-1) 49 | f.Close() 50 | if err != nil { 51 | return nil, err 52 | } 53 | sort.Sort(sortable{&list, sortby, order}) 54 | return list, nil 55 | } 56 | 57 | func isProtected(level string, auth string) bool { 58 | levels := strings.Split(auth, ",") 59 | for _, x := range levels { 60 | if x == level { 61 | return true 62 | } 63 | } 64 | return false 65 | } 66 | -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | goup 3 | 4 | 5 | 12 | {{ if .Upload }} 13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 |
24 | {{ end }} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {{ if not .IsRoot }} 33 | 34 | 35 | 36 | 37 | {{ end }} 38 | {{ range $e := .DirEntries }} 39 | 40 | 41 | 42 | {{ if $e.IsDir }} 43 | 44 | 45 | {{ else }} 46 | 47 | 48 | {{ end }} 49 | 50 | {{ end }} 51 |
modelast modifiedsizename
..
{{ $e.Mode }}{{ $e.ModTime.Format "2006-01-02 15:04:05" }}{{ $e.Name }}/{{ size $e.Size }}{{ $e.Name }}
52 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "net/http/fcgi" 10 | "os" 11 | ) 12 | 13 | var ( 14 | noupload = false 15 | user string 16 | password string 17 | auth string 18 | dir = "." 19 | mode = "http" 20 | index string 21 | // VERSION application version 22 | VERSION string 23 | ) 24 | 25 | func main() { 26 | flag.Usage = func() { 27 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 28 | flag.PrintDefaults() 29 | } 30 | address := flag.String("addr", "0.0.0.0:4000", "listen on this address") 31 | flag.BoolVar(&noupload, "noupload", noupload, "enable or disable uploads") 32 | flag.StringVar(&user, "user", user, "user for HTTP Basic authentication (-auth needs to be set)") 33 | flag.StringVar(&password, "password", password, "password for HTTP Basic authentication (-auth needs to be set)") 34 | flag.StringVar(&auth, "auth", auth, "comma-separated list of what will be protected by HTTP Basic authentication (index,download,upload)") 35 | flag.StringVar(&dir, "dir", dir, "directory for storing and serving files") 36 | flag.StringVar(&mode, "mode", mode, "run either standalone (http) or as FCGI application (fcgi)") 37 | flag.StringVar(&index, "index", index, "serve this file if it exists in the current directory instead of a listing") 38 | verbose := flag.Bool("v", false, "verbose output (no output at all by default)") 39 | version := flag.Bool("version", false, "show version and exit") 40 | flag.Parse() 41 | 42 | if *version { 43 | fmt.Println(VERSION) 44 | return 45 | } 46 | 47 | log.SetOutput(ioutil.Discard) 48 | if *verbose { 49 | log.SetOutput(os.Stdout) 50 | flag.VisitAll(func(f *flag.Flag) { 51 | log.Printf("SETTINGS: %s = %s", f.Name, f.Value) 52 | }) 53 | } 54 | 55 | http.HandleFunc("/", handler) 56 | 57 | switch mode { 58 | case "http": 59 | log.Fatal(http.ListenAndServe(*address, nil)) 60 | case "fcgi": 61 | log.Fatal(fcgi.Serve(nil, nil)) 62 | default: 63 | log.Fatalf("Unknown mode '%s'!", mode) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "os" 8 | "path" 9 | "strings" 10 | ) 11 | 12 | func handler(w http.ResponseWriter, r *http.Request) { 13 | urlPath := path.Clean(r.URL.Path) 14 | localPath := path.Join(dir, urlPath) 15 | switch r.Method { 16 | case "POST": 17 | if noupload { 18 | http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) 19 | return 20 | } 21 | if isProtected("upload", auth) { 22 | u, p, ok := r.BasicAuth() 23 | if !ok || user != u || password != p { 24 | w.Header().Set("WWW-Authenticate", "Basic") 25 | http.Error(w, "Unauthorized", http.StatusUnauthorized) 26 | return 27 | } 28 | } 29 | if h := r.Header.Get("Content-Type"); !strings.HasPrefix(h, "multipart/form-data") { 30 | folder := path.Clean(r.PostFormValue("folder")) 31 | if folder != "" && folder != "/" { 32 | switch err := os.Mkdir(path.Join(localPath, folder), 0750).(type) { 33 | case *os.PathError: 34 | http.Error(w, err.Op+" "+path.Join(urlPath, folder)+": "+ 35 | err.Err.Error(), 500) 36 | return 37 | case error: 38 | http.Error(w, err.Error(), 500) 39 | return 40 | } 41 | } 42 | http.Redirect(w, r, path.Join(urlPath, folder), 302) 43 | return 44 | } 45 | body, err := r.MultipartReader() 46 | if err != nil { 47 | http.Error(w, err.Error(), 500) 48 | return 49 | } 50 | for part, err := body.NextPart(); err == nil; part, err = body.NextPart() { 51 | formName := part.FormName() 52 | if formName != "file" { 53 | log.Printf("Skipping '%s'", formName) 54 | continue 55 | } 56 | log.Printf("Handling '%s'", formName) 57 | destFile, err := os.Create(path.Join(localPath, part.FileName())) 58 | if err != nil { 59 | http.Error(w, err.Error(), 500) 60 | return 61 | } 62 | defer destFile.Close() 63 | if _, err := io.Copy(destFile, part); err != nil { 64 | http.Error(w, err.Error(), 500) 65 | return 66 | } 67 | } 68 | http.Redirect(w, r, urlPath, 302) 69 | case "GET": 70 | if isProtected("index", auth) { 71 | u, p, ok := r.BasicAuth() 72 | if !ok || user != u || password != p { 73 | w.Header().Set("WWW-Authenticate", "Basic") 74 | http.Error(w, "Unauthorized", http.StatusUnauthorized) 75 | return 76 | } 77 | } 78 | entryInfo, err := os.Stat(localPath) 79 | if err != nil { 80 | log.Printf("ERROR: os.Stat('%s')", localPath) 81 | http.Error(w, err.Error(), 500) 82 | return 83 | } 84 | if entryInfo.IsDir() && !strings.HasSuffix(r.URL.Path, "/") { 85 | http.Redirect(w, r, r.URL.Path+"/", 302) 86 | return 87 | } 88 | if entryInfo.IsDir() && index != "" { 89 | if fi, err := os.Stat(path.Join(localPath, index)); err == nil { 90 | if !fi.IsDir() { 91 | http.Redirect(w, r, path.Join(urlPath, index), 302) 92 | return 93 | } 94 | } 95 | } 96 | if entryInfo.IsDir() { 97 | sortKey := r.URL.Query().Get("key") 98 | sortOrder := asc 99 | switch r.URL.Query().Get("order") { 100 | case "asc": 101 | sortOrder = asc 102 | sortOrderMap[sortKey] = "desc" 103 | case "desc": 104 | sortOrder = desc 105 | sortOrderMap[sortKey] = "asc" 106 | } 107 | entries, err := readDir(localPath, sortKey, sortOrder) 108 | if err != nil { 109 | log.Printf("ERROR: ReadDir('%s')", localPath) 110 | http.Error(w, err.Error(), 500) 111 | return 112 | } 113 | ctx := context{urlPath == "/", !noupload, entries, sortOrderMap} 114 | if err := tmpl.Execute(w, ctx); err != nil { 115 | log.Println("ERROR: Executing template") 116 | http.Error(w, err.Error(), 500) 117 | return 118 | } 119 | } else { 120 | if isProtected("download", auth) { 121 | u, p, ok := r.BasicAuth() 122 | if !ok || user != u || password != p { 123 | w.Header().Set("WWW-Authenticate", "Basic") 124 | http.Error(w, "Unauthorized", http.StatusUnauthorized) 125 | return 126 | } 127 | } 128 | f, err := os.Open(localPath) 129 | if err != nil { 130 | log.Printf("ERROR: os.Open('%s')", localPath) 131 | http.Error(w, err.Error(), 500) 132 | return 133 | } 134 | defer f.Close() 135 | log.Printf("Serving '%s'", localPath) 136 | http.ServeContent(w, r, entryInfo.Name(), entryInfo.ModTime(), f) 137 | } 138 | default: 139 | http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) 140 | } 141 | return 142 | } 143 | -------------------------------------------------------------------------------- /bindata.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bindata. DO NOT EDIT. 2 | // sources: 3 | // assets/index.html 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "compress/gzip" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | func bindataRead(data []byte, name string) ([]byte, error) { 19 | gz, err := gzip.NewReader(bytes.NewBuffer(data)) 20 | if err != nil { 21 | return nil, fmt.Errorf("Read %q: %v", name, err) 22 | } 23 | 24 | var buf bytes.Buffer 25 | _, err = io.Copy(&buf, gz) 26 | clErr := gz.Close() 27 | 28 | if err != nil { 29 | return nil, fmt.Errorf("Read %q: %v", name, err) 30 | } 31 | if clErr != nil { 32 | return nil, err 33 | } 34 | 35 | return buf.Bytes(), nil 36 | } 37 | 38 | type asset struct { 39 | bytes []byte 40 | info os.FileInfo 41 | } 42 | 43 | type bindataFileInfo struct { 44 | name string 45 | size int64 46 | mode os.FileMode 47 | modTime time.Time 48 | } 49 | 50 | func (fi bindataFileInfo) Name() string { 51 | return fi.name 52 | } 53 | func (fi bindataFileInfo) Size() int64 { 54 | return fi.size 55 | } 56 | func (fi bindataFileInfo) Mode() os.FileMode { 57 | return fi.mode 58 | } 59 | func (fi bindataFileInfo) ModTime() time.Time { 60 | return fi.modTime 61 | } 62 | func (fi bindataFileInfo) IsDir() bool { 63 | return false 64 | } 65 | func (fi bindataFileInfo) Sys() interface{} { 66 | return nil 67 | } 68 | 69 | var _assetsIndexHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x53\xc1\x6e\xdb\x30\x0c\x3d\xd7\x5f\xc1\x19\xc1\x7c\x8a\x9c\x16\xeb\x30\xb8\xb2\x77\x58\x3b\xa0\x87\xad\xc3\xda\x1d\x76\x54\x2d\x3a\x16\x26\x4b\x86\xc4\xa4\xcd\x8c\xfc\xfb\x20\x39\x6b\xdd\xa4\x45\xb7\xdb\xa3\x48\xbe\x47\x3e\xda\xfc\xcd\xf9\xd5\xa7\x9b\x9f\xdf\x2e\xa0\xa5\x4e\x57\x09\x27\x45\x1a\xab\xa5\x5d\xf5\x3c\x1f\x71\xc2\x3b\x24\x01\x75\x2b\x9c\x47\x2a\xb3\x15\x35\xf3\x0f\xd9\xdf\x67\x23\x3a\x2c\xd3\xb5\xc2\xbb\xde\x3a\x4a\xa1\xb6\x86\xd0\x50\x99\xde\x29\x49\x6d\x29\x71\xad\x6a\x9c\xc7\x20\xad\x12\xee\x69\xa3\x11\x68\xd3\x63\x99\x12\xde\x53\x5e\x7b\x9f\x56\x09\x89\x5b\x8d\x30\x40\x63\x0d\xcd\x1b\xd1\x29\xbd\x29\xa0\xb3\xc6\xfa\x5e\xd4\x78\x06\xdb\x84\x24\x0c\x10\x3a\xe6\x42\xab\xa5\x29\xc0\xa9\x65\x4b\x67\xd0\x0b\x29\x95\x59\x16\xb0\x80\xe3\xd3\xfe\x7e\x2c\x65\x61\xaa\xbd\x7a\x8d\x0d\xed\xb2\x12\x35\x52\xc8\xaf\x95\x57\xb7\x4a\x2b\xda\x14\xd0\x2a\x29\xd1\xc4\x0a\x57\xb4\x76\x8d\x0e\x5e\x2a\x8d\x58\xc7\xb1\x78\x1e\x37\xaa\x92\x61\x00\xd5\x00\xfb\xd1\x6b\x2b\x24\x6c\xb7\x09\x97\x6a\x5d\x25\x47\xbc\xb1\xae\x03\x51\x93\xb2\xa6\xcc\x58\x06\x1d\x52\x6b\x65\x99\xf5\xd6\x53\x06\x68\xea\x68\x46\xd6\xad\x34\xa9\x5e\x38\xca\x43\xc3\x5c\x0a\x12\x59\x95\x1c\x1d\x71\x65\xfa\x15\x8d\x8e\x65\x8d\xd2\x98\x8d\x96\x8f\xf8\xa0\xc2\xaf\x6e\x3b\x45\xe1\x9d\x47\xa2\x57\x27\x38\x60\x08\x9e\x3d\x68\x58\x2d\xd1\x65\xd0\x6b\x51\x63\x1b\x83\x87\xc7\x7f\x90\xe6\x79\xf4\x80\xb7\x2e\xfa\x83\x66\x34\x26\x1e\x3b\x94\x91\x8b\x24\xd4\x56\x5c\x40\xeb\xb0\x29\x53\xf6\xf1\x17\x6e\xca\xce\x4a\x7c\x6b\x5d\x90\x1b\x06\x60\xd7\xd6\xd1\x55\x88\x58\x48\xc0\x76\x9b\x56\x01\xf0\x5c\x54\x3c\xa7\xf6\x05\x12\x52\xdd\xf3\x24\x21\x11\x49\xb4\xf0\x04\x9d\x95\xaa\x51\x28\x5f\x61\xf3\xea\xf7\xf3\x6c\x21\x11\xd9\x02\x78\x85\x24\xb8\xfa\x2c\x49\xfc\x5e\x03\x49\x00\x13\x12\x9e\x47\x8f\xc6\x8f\xcb\x58\x02\x76\xe9\xbf\x5b\x4b\xc1\xc7\x47\xff\x64\x28\x97\xd5\x21\x18\xb3\x50\x6b\xe1\x7d\x99\x06\xee\x74\x32\x12\x4b\x2b\xc6\x76\x62\xf2\x89\xd8\xee\x52\x01\x3a\x61\x96\x08\x33\x84\xa2\x04\x76\xae\xdc\x85\x21\xa7\xd0\xef\x0f\x30\x0c\x30\x43\xf6\x65\x3c\xcf\x44\xfb\x31\x71\xa3\x3a\x64\x9f\xad\xeb\x04\x41\x7a\xb2\x58\xbc\x9f\x2f\x8e\xe7\x8b\x13\x38\x3e\x2d\x16\xef\x8a\xc5\x69\x3a\x69\x1c\xf7\x9d\x21\xbb\xf4\xe7\xca\x45\xad\xc9\x9e\xbb\xe0\x85\xb5\x46\xc1\xaf\xa3\xa1\x79\x5a\x3d\x8d\x27\xeb\xc6\x45\xb5\xc7\x09\xfd\x30\x40\x3c\xe7\x0c\xd9\xf5\x78\xd6\xff\x15\xdc\xd3\xdb\x97\xdb\xf9\x7a\xe0\x34\xcf\x77\x3f\xc5\x9f\x00\x00\x00\xff\xff\x43\x28\x22\x0d\x90\x05\x00\x00") 70 | 71 | func assetsIndexHtmlBytes() ([]byte, error) { 72 | return bindataRead( 73 | _assetsIndexHtml, 74 | "assets/index.html", 75 | ) 76 | } 77 | 78 | func assetsIndexHtml() (*asset, error) { 79 | bytes, err := assetsIndexHtmlBytes() 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | info := bindataFileInfo{name: "assets/index.html", size: 1424, mode: os.FileMode(436), modTime: time.Unix(1528887806, 0)} 85 | a := &asset{bytes: bytes, info: info} 86 | return a, nil 87 | } 88 | 89 | // Asset loads and returns the asset for the given name. 90 | // It returns an error if the asset could not be found or 91 | // could not be loaded. 92 | func Asset(name string) ([]byte, error) { 93 | cannonicalName := strings.Replace(name, "\\", "/", -1) 94 | if f, ok := _bindata[cannonicalName]; ok { 95 | a, err := f() 96 | if err != nil { 97 | return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) 98 | } 99 | return a.bytes, nil 100 | } 101 | return nil, fmt.Errorf("Asset %s not found", name) 102 | } 103 | 104 | // MustAsset is like Asset but panics when Asset would return an error. 105 | // It simplifies safe initialization of global variables. 106 | func MustAsset(name string) []byte { 107 | a, err := Asset(name) 108 | if err != nil { 109 | panic("asset: Asset(" + name + "): " + err.Error()) 110 | } 111 | 112 | return a 113 | } 114 | 115 | // AssetInfo loads and returns the asset info for the given name. 116 | // It returns an error if the asset could not be found or 117 | // could not be loaded. 118 | func AssetInfo(name string) (os.FileInfo, error) { 119 | cannonicalName := strings.Replace(name, "\\", "/", -1) 120 | if f, ok := _bindata[cannonicalName]; ok { 121 | a, err := f() 122 | if err != nil { 123 | return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) 124 | } 125 | return a.info, nil 126 | } 127 | return nil, fmt.Errorf("AssetInfo %s not found", name) 128 | } 129 | 130 | // AssetNames returns the names of the assets. 131 | func AssetNames() []string { 132 | names := make([]string, 0, len(_bindata)) 133 | for name := range _bindata { 134 | names = append(names, name) 135 | } 136 | return names 137 | } 138 | 139 | // _bindata is a table, holding each asset generator, mapped to its name. 140 | var _bindata = map[string]func() (*asset, error){ 141 | "assets/index.html": assetsIndexHtml, 142 | } 143 | 144 | // AssetDir returns the file names below a certain 145 | // directory embedded in the file by go-bindata. 146 | // For example if you run go-bindata on data/... and data contains the 147 | // following hierarchy: 148 | // data/ 149 | // foo.txt 150 | // img/ 151 | // a.png 152 | // b.png 153 | // then AssetDir("data") would return []string{"foo.txt", "img"} 154 | // AssetDir("data/img") would return []string{"a.png", "b.png"} 155 | // AssetDir("foo.txt") and AssetDir("notexist") would return an error 156 | // AssetDir("") will return []string{"data"}. 157 | func AssetDir(name string) ([]string, error) { 158 | node := _bintree 159 | if len(name) != 0 { 160 | cannonicalName := strings.Replace(name, "\\", "/", -1) 161 | pathList := strings.Split(cannonicalName, "/") 162 | for _, p := range pathList { 163 | node = node.Children[p] 164 | if node == nil { 165 | return nil, fmt.Errorf("Asset %s not found", name) 166 | } 167 | } 168 | } 169 | if node.Func != nil { 170 | return nil, fmt.Errorf("Asset %s not found", name) 171 | } 172 | rv := make([]string, 0, len(node.Children)) 173 | for childName := range node.Children { 174 | rv = append(rv, childName) 175 | } 176 | return rv, nil 177 | } 178 | 179 | type bintree struct { 180 | Func func() (*asset, error) 181 | Children map[string]*bintree 182 | } 183 | var _bintree = &bintree{nil, map[string]*bintree{ 184 | "assets": &bintree{nil, map[string]*bintree{ 185 | "index.html": &bintree{assetsIndexHtml, map[string]*bintree{}}, 186 | }}, 187 | }} 188 | 189 | // RestoreAsset restores an asset under the given directory 190 | func RestoreAsset(dir, name string) error { 191 | data, err := Asset(name) 192 | if err != nil { 193 | return err 194 | } 195 | info, err := AssetInfo(name) 196 | if err != nil { 197 | return err 198 | } 199 | err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) 200 | if err != nil { 201 | return err 202 | } 203 | err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) 204 | if err != nil { 205 | return err 206 | } 207 | err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) 208 | if err != nil { 209 | return err 210 | } 211 | return nil 212 | } 213 | 214 | // RestoreAssets restores an asset under the given directory recursively 215 | func RestoreAssets(dir, name string) error { 216 | children, err := AssetDir(name) 217 | // File 218 | if err != nil { 219 | return RestoreAsset(dir, name) 220 | } 221 | // Dir 222 | for _, child := range children { 223 | err = RestoreAssets(dir, filepath.Join(name, child)) 224 | if err != nil { 225 | return err 226 | } 227 | } 228 | return nil 229 | } 230 | 231 | func _filePath(dir, name string) string { 232 | cannonicalName := strings.Replace(name, "\\", "/", -1) 233 | return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) 234 | } 235 | 236 | --------------------------------------------------------------------------------