blob: 04f6d7a39d9c12da5ebe7f83eaa48336aa48d610 [file] [log] [blame]
Matthias Andreas Benkard832a54e2019-01-29 09:27:38 +01001package assetfs
2
3import (
4 "bytes"
5 "errors"
6 "io"
7 "io/ioutil"
8 "net/http"
9 "os"
10 "path"
11 "path/filepath"
12 "strings"
13 "time"
14)
15
16var (
17 defaultFileTimestamp = time.Now()
18)
19
20// FakeFile implements os.FileInfo interface for a given path and size
21type FakeFile struct {
22 // Path is the path of this file
23 Path string
24 // Dir marks of the path is a directory
25 Dir bool
26 // Len is the length of the fake file, zero if it is a directory
27 Len int64
28 // Timestamp is the ModTime of this file
29 Timestamp time.Time
30}
31
32func (f *FakeFile) Name() string {
33 _, name := filepath.Split(f.Path)
34 return name
35}
36
37func (f *FakeFile) Mode() os.FileMode {
38 mode := os.FileMode(0644)
39 if f.Dir {
40 return mode | os.ModeDir
41 }
42 return mode
43}
44
45func (f *FakeFile) ModTime() time.Time {
46 return f.Timestamp
47}
48
49func (f *FakeFile) Size() int64 {
50 return f.Len
51}
52
53func (f *FakeFile) IsDir() bool {
54 return f.Mode().IsDir()
55}
56
57func (f *FakeFile) Sys() interface{} {
58 return nil
59}
60
61// AssetFile implements http.File interface for a no-directory file with content
62type AssetFile struct {
63 *bytes.Reader
64 io.Closer
65 FakeFile
66}
67
68func NewAssetFile(name string, content []byte, timestamp time.Time) *AssetFile {
69 if timestamp.IsZero() {
70 timestamp = defaultFileTimestamp
71 }
72 return &AssetFile{
73 bytes.NewReader(content),
74 ioutil.NopCloser(nil),
75 FakeFile{name, false, int64(len(content)), timestamp}}
76}
77
78func (f *AssetFile) Readdir(count int) ([]os.FileInfo, error) {
79 return nil, errors.New("not a directory")
80}
81
82func (f *AssetFile) Size() int64 {
83 return f.FakeFile.Size()
84}
85
86func (f *AssetFile) Stat() (os.FileInfo, error) {
87 return f, nil
88}
89
90// AssetDirectory implements http.File interface for a directory
91type AssetDirectory struct {
92 AssetFile
93 ChildrenRead int
94 Children []os.FileInfo
95}
96
97func NewAssetDirectory(name string, children []string, fs *AssetFS) *AssetDirectory {
98 fileinfos := make([]os.FileInfo, 0, len(children))
99 for _, child := range children {
100 _, err := fs.AssetDir(filepath.Join(name, child))
101 fileinfos = append(fileinfos, &FakeFile{child, err == nil, 0, time.Time{}})
102 }
103 return &AssetDirectory{
104 AssetFile{
105 bytes.NewReader(nil),
106 ioutil.NopCloser(nil),
107 FakeFile{name, true, 0, time.Time{}},
108 },
109 0,
110 fileinfos}
111}
112
113func (f *AssetDirectory) Readdir(count int) ([]os.FileInfo, error) {
114 if count <= 0 {
115 return f.Children, nil
116 }
117 if f.ChildrenRead+count > len(f.Children) {
118 count = len(f.Children) - f.ChildrenRead
119 }
120 rv := f.Children[f.ChildrenRead : f.ChildrenRead+count]
121 f.ChildrenRead += count
122 return rv, nil
123}
124
125func (f *AssetDirectory) Stat() (os.FileInfo, error) {
126 return f, nil
127}
128
129// AssetFS implements http.FileSystem, allowing
130// embedded files to be served from net/http package.
131type AssetFS struct {
132 // Asset should return content of file in path if exists
133 Asset func(path string) ([]byte, error)
134 // AssetDir should return list of files in the path
135 AssetDir func(path string) ([]string, error)
136 // AssetInfo should return the info of file in path if exists
137 AssetInfo func(path string) (os.FileInfo, error)
138 // Prefix would be prepended to http requests
139 Prefix string
140}
141
142func (fs *AssetFS) Open(name string) (http.File, error) {
143 name = path.Join(fs.Prefix, name)
144 if len(name) > 0 && name[0] == '/' {
145 name = name[1:]
146 }
147 if b, err := fs.Asset(name); err == nil {
148 timestamp := defaultFileTimestamp
149 if fs.AssetInfo != nil {
150 if info, err := fs.AssetInfo(name); err == nil {
151 timestamp = info.ModTime()
152 }
153 }
154 return NewAssetFile(name, b, timestamp), nil
155 }
156 if children, err := fs.AssetDir(name); err == nil {
157 return NewAssetDirectory(name, children, fs), nil
158 } else {
159 // If the error is not found, return an error that will
160 // result in a 404 error. Otherwise the server returns
161 // a 500 error for files not found.
162 if strings.Contains(err.Error(), "not found") {
163 return nil, os.ErrNotExist
164 }
165 return nil, err
166 }
167}