1package 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}
168