├── .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 |
29 | <% for @build in ENV['GOUP_BUILDS'].split("\n") %>
30 | - <%= @build %>
31 | <% end %>
32 |
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 |
18 |
22 |
23 |
24 | {{ end }}
25 |
26 |
27 | | mode |
28 | last modified |
29 | size |
30 | name |
31 |
32 | {{ if not .IsRoot }}
33 |
34 | | | |
35 | .. |
36 |
37 | {{ end }}
38 | {{ range $e := .DirEntries }}
39 |
40 | | {{ $e.Mode }} |
41 | {{ $e.ModTime.Format "2006-01-02 15:04:05" }} |
42 | {{ if $e.IsDir }}
43 | |
44 | {{ $e.Name }}/ |
45 | {{ else }}
46 | {{ size $e.Size }} |
47 | {{ $e.Name }} |
48 | {{ end }}
49 |
50 | {{ end }}
51 |
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 |
--------------------------------------------------------------------------------