1package vfsgen
2
3import (
4 "bytes"
5 "compress/gzip"
6 "errors"
7 "fmt"
8 "io"
9 "io/ioutil"
10 "net/http"
11 "os"
12 pathpkg "path"
13 "sort"
14 "strconv"
15 "text/template"
16 "time"
17
18 "github.com/shurcooL/httpfs/vfsutil"
19)
20
21// Generate Go code that statically implements input filesystem,
22// write the output to a file specified in opt.
23func Generate(input http.FileSystem, opt Options) error {
24 opt.fillMissing()
25
26 // Use an in-memory buffer to generate the entire output.
27 buf := new(bytes.Buffer)
28
29 err := t.ExecuteTemplate(buf, "Header", opt)
30 if err != nil {
31 return err
32 }
33
34 var toc toc
35 err = findAndWriteFiles(buf, input, &toc)
36 if err != nil {
37 return err
38 }
39
40 err = t.ExecuteTemplate(buf, "DirEntries", toc.dirs)
41 if err != nil {
42 return err
43 }
44
45 err = t.ExecuteTemplate(buf, "Trailer", toc)
46 if err != nil {
47 return err
48 }
49
50 // Write output file (all at once).
51 fmt.Println("writing", opt.Filename)
52 err = ioutil.WriteFile(opt.Filename, buf.Bytes(), 0644)
53 return err
54}
55
56type toc struct {
57 dirs []*dirInfo
58
59 HasCompressedFile bool // There's at least one compressedFile.
60 HasFile bool // There's at least one uncompressed file.
61}
62
63// fileInfo is a definition of a file.
64type fileInfo struct {
65 Path string
66 Name string
67 ModTime time.Time
68 UncompressedSize int64
69}
70
71// dirInfo is a definition of a directory.
72type dirInfo struct {
73 Path string
74 Name string
75 ModTime time.Time
76 Entries []string
77}
78
79// findAndWriteFiles recursively finds all the file paths in the given directory tree.
80// They are added to the given map as keys. Values will be safe function names
81// for each file, which will be used when generating the output code.
82func findAndWriteFiles(buf *bytes.Buffer, fs http.FileSystem, toc *toc) error {
83 walkFn := func(path string, fi os.FileInfo, r io.ReadSeeker, err error) error {
84 if err != nil {
85 // Consider all errors reading the input filesystem as fatal.
86 return err
87 }
88
89 switch fi.IsDir() {
90 case false:
91 file := &fileInfo{
92 Path: path,
93 Name: pathpkg.Base(path),
94 ModTime: fi.ModTime().UTC(),
95 UncompressedSize: fi.Size(),
96 }
97
98 marker := buf.Len()
99
100 // Write CompressedFileInfo.
101 err = writeCompressedFileInfo(buf, file, r)
102 switch err {
103 default:
104 return err
105 case nil:
106 toc.HasCompressedFile = true
107 // If compressed file is not smaller than original, revert and write original file.
108 case errCompressedNotSmaller:
109 _, err = r.Seek(0, io.SeekStart)
110 if err != nil {
111 return err
112 }
113
114 buf.Truncate(marker)
115
116 // Write FileInfo.
117 err = writeFileInfo(buf, file, r)
118 if err != nil {
119 return err
120 }
121 toc.HasFile = true
122 }
123 case true:
124 entries, err := readDirPaths(fs, path)
125 if err != nil {
126 return err
127 }
128
129 dir := &dirInfo{
130 Path: path,
131 Name: pathpkg.Base(path),
132 ModTime: fi.ModTime().UTC(),
133 Entries: entries,
134 }
135
136 toc.dirs = append(toc.dirs, dir)
137
138 // Write DirInfo.
139 err = t.ExecuteTemplate(buf, "DirInfo", dir)
140 if err != nil {
141 return err
142 }
143 }
144
145 return nil
146 }
147
148 err := vfsutil.WalkFiles(fs, "/", walkFn)
149 return err
150}
151
152// readDirPaths reads the directory named by dirname and returns
153// a sorted list of directory paths.
154func readDirPaths(fs http.FileSystem, dirname string) ([]string, error) {
155 fis, err := vfsutil.ReadDir(fs, dirname)
156 if err != nil {
157 return nil, err
158 }
159 paths := make([]string, len(fis))
160 for i := range fis {
161 paths[i] = pathpkg.Join(dirname, fis[i].Name())
162 }
163 sort.Strings(paths)
164 return paths, nil
165}
166
167// writeCompressedFileInfo writes CompressedFileInfo.
168// It returns errCompressedNotSmaller if compressed file is not smaller than original.
169func writeCompressedFileInfo(w io.Writer, file *fileInfo, r io.Reader) error {
170 err := t.ExecuteTemplate(w, "CompressedFileInfo-Before", file)
171 if err != nil {
172 return err
173 }
174 sw := &stringWriter{Writer: w}
175 gw := gzip.NewWriter(sw)
176 _, err = io.Copy(gw, r)
177 if err != nil {
178 return err
179 }
180 err = gw.Close()
181 if err != nil {
182 return err
183 }
184 if sw.N >= file.UncompressedSize {
185 return errCompressedNotSmaller
186 }
187 err = t.ExecuteTemplate(w, "CompressedFileInfo-After", file)
188 return err
189}
190
191var errCompressedNotSmaller = errors.New("compressed file is not smaller than original")
192
193// Write FileInfo.
194func writeFileInfo(w io.Writer, file *fileInfo, r io.Reader) error {
195 err := t.ExecuteTemplate(w, "FileInfo-Before", file)
196 if err != nil {
197 return err
198 }
199 sw := &stringWriter{Writer: w}
200 _, err = io.Copy(sw, r)
201 if err != nil {
202 return err
203 }
204 err = t.ExecuteTemplate(w, "FileInfo-After", file)
205 return err
206}
207
208var t = template.Must(template.New("").Funcs(template.FuncMap{
209 "quote": strconv.Quote,
210 "comment": func(s string) (string, error) {
211 var buf bytes.Buffer
212 cw := &commentWriter{W: &buf}
213 _, err := io.WriteString(cw, s)
214 if err != nil {
215 return "", err
216 }
217 err = cw.Close()
218 return buf.String(), err
219 },
220}).Parse(`{{define "Header"}}// Code generated by vfsgen; DO NOT EDIT.
221
222{{with .BuildTags}}// +build {{.}}
223
224{{end}}package {{.PackageName}}
225
226import (
227 "bytes"
228 "compress/gzip"
229 "fmt"
230 "io"
231 "io/ioutil"
232 "net/http"
233 "os"
234 pathpkg "path"
235 "time"
236)
237
238{{comment .VariableComment}}
239var {{.VariableName}} = func() http.FileSystem {
240 fs := vfsgen۰FS{
241{{end}}
242
243
244
245{{define "CompressedFileInfo-Before"}} {{quote .Path}}: &vfsgen۰CompressedFileInfo{
246 name: {{quote .Name}},
247 modTime: {{template "Time" .ModTime}},
248 uncompressedSize: {{.UncompressedSize}},
249{{/* This blank line separating compressedContent is neccessary to prevent potential gofmt issues. See issue #19. */}}
250 compressedContent: []byte("{{end}}{{define "CompressedFileInfo-After"}}"),
251 },
252{{end}}
253
254
255
256{{define "FileInfo-Before"}} {{quote .Path}}: &vfsgen۰FileInfo{
257 name: {{quote .Name}},
258 modTime: {{template "Time" .ModTime}},
259 content: []byte("{{end}}{{define "FileInfo-After"}}"),
260 },
261{{end}}
262
263
264
265{{define "DirInfo"}} {{quote .Path}}: &vfsgen۰DirInfo{
266 name: {{quote .Name}},
267 modTime: {{template "Time" .ModTime}},
268 },
269{{end}}
270
271
272
273{{define "DirEntries"}} }
274{{range .}}{{if .Entries}} fs[{{quote .Path}}].(*vfsgen۰DirInfo).entries = []os.FileInfo{{"{"}}{{range .Entries}}
275 fs[{{quote .}}].(os.FileInfo),{{end}}
276 }
277{{end}}{{end}}
278 return fs
279}()
280{{end}}
281
282
283
284{{define "Trailer"}}
285type vfsgen۰FS map[string]interface{}
286
287func (fs vfsgen۰FS) Open(path string) (http.File, error) {
288 path = pathpkg.Clean("/" + path)
289 f, ok := fs[path]
290 if !ok {
291 return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
292 }
293
294 switch f := f.(type) {{"{"}}{{if .HasCompressedFile}}
295 case *vfsgen۰CompressedFileInfo:
296 gr, err := gzip.NewReader(bytes.NewReader(f.compressedContent))
297 if err != nil {
298 // This should never happen because we generate the gzip bytes such that they are always valid.
299 panic("unexpected error reading own gzip compressed bytes: " + err.Error())
300 }
301 return &vfsgen۰CompressedFile{
302 vfsgen۰CompressedFileInfo: f,
303 gr: gr,
304 }, nil{{end}}{{if .HasFile}}
305 case *vfsgen۰FileInfo:
306 return &vfsgen۰File{
307 vfsgen۰FileInfo: f,
308 Reader: bytes.NewReader(f.content),
309 }, nil{{end}}
310 case *vfsgen۰DirInfo:
311 return &vfsgen۰Dir{
312 vfsgen۰DirInfo: f,
313 }, nil
314 default:
315 // This should never happen because we generate only the above types.
316 panic(fmt.Sprintf("unexpected type %T", f))
317 }
318}
319{{if .HasCompressedFile}}
320// vfsgen۰CompressedFileInfo is a static definition of a gzip compressed file.
321type vfsgen۰CompressedFileInfo struct {
322 name string
323 modTime time.Time
324 compressedContent []byte
325 uncompressedSize int64
326}
327
328func (f *vfsgen۰CompressedFileInfo) Readdir(count int) ([]os.FileInfo, error) {
329 return nil, fmt.Errorf("cannot Readdir from file %s", f.name)
330}
331func (f *vfsgen۰CompressedFileInfo) Stat() (os.FileInfo, error) { return f, nil }
332
333func (f *vfsgen۰CompressedFileInfo) GzipBytes() []byte {
334 return f.compressedContent
335}
336
337func (f *vfsgen۰CompressedFileInfo) Name() string { return f.name }
338func (f *vfsgen۰CompressedFileInfo) Size() int64 { return f.uncompressedSize }
339func (f *vfsgen۰CompressedFileInfo) Mode() os.FileMode { return 0444 }
340func (f *vfsgen۰CompressedFileInfo) ModTime() time.Time { return f.modTime }
341func (f *vfsgen۰CompressedFileInfo) IsDir() bool { return false }
342func (f *vfsgen۰CompressedFileInfo) Sys() interface{} { return nil }
343
344// vfsgen۰CompressedFile is an opened compressedFile instance.
345type vfsgen۰CompressedFile struct {
346 *vfsgen۰CompressedFileInfo
347 gr *gzip.Reader
348 grPos int64 // Actual gr uncompressed position.
349 seekPos int64 // Seek uncompressed position.
350}
351
352func (f *vfsgen۰CompressedFile) Read(p []byte) (n int, err error) {
353 if f.grPos > f.seekPos {
354 // Rewind to beginning.
355 err = f.gr.Reset(bytes.NewReader(f.compressedContent))
356 if err != nil {
357 return 0, err
358 }
359 f.grPos = 0
360 }
361 if f.grPos < f.seekPos {
362 // Fast-forward.
363 _, err = io.CopyN(ioutil.Discard, f.gr, f.seekPos-f.grPos)
364 if err != nil {
365 return 0, err
366 }
367 f.grPos = f.seekPos
368 }
369 n, err = f.gr.Read(p)
370 f.grPos += int64(n)
371 f.seekPos = f.grPos
372 return n, err
373}
374func (f *vfsgen۰CompressedFile) Seek(offset int64, whence int) (int64, error) {
375 switch whence {
376 case io.SeekStart:
377 f.seekPos = 0 + offset
378 case io.SeekCurrent:
379 f.seekPos += offset
380 case io.SeekEnd:
381 f.seekPos = f.uncompressedSize + offset
382 default:
383 panic(fmt.Errorf("invalid whence value: %v", whence))
384 }
385 return f.seekPos, nil
386}
387func (f *vfsgen۰CompressedFile) Close() error {
388 return f.gr.Close()
389}
390{{else}}
391// We already imported "compress/gzip" and "io/ioutil", but ended up not using them. Avoid unused import error.
392var _ = gzip.Reader{}
393var _ = ioutil.Discard
394{{end}}{{if .HasFile}}
395// vfsgen۰FileInfo is a static definition of an uncompressed file (because it's not worth gzip compressing).
396type vfsgen۰FileInfo struct {
397 name string
398 modTime time.Time
399 content []byte
400}
401
402func (f *vfsgen۰FileInfo) Readdir(count int) ([]os.FileInfo, error) {
403 return nil, fmt.Errorf("cannot Readdir from file %s", f.name)
404}
405func (f *vfsgen۰FileInfo) Stat() (os.FileInfo, error) { return f, nil }
406
407func (f *vfsgen۰FileInfo) NotWorthGzipCompressing() {}
408
409func (f *vfsgen۰FileInfo) Name() string { return f.name }
410func (f *vfsgen۰FileInfo) Size() int64 { return int64(len(f.content)) }
411func (f *vfsgen۰FileInfo) Mode() os.FileMode { return 0444 }
412func (f *vfsgen۰FileInfo) ModTime() time.Time { return f.modTime }
413func (f *vfsgen۰FileInfo) IsDir() bool { return false }
414func (f *vfsgen۰FileInfo) Sys() interface{} { return nil }
415
416// vfsgen۰File is an opened file instance.
417type vfsgen۰File struct {
418 *vfsgen۰FileInfo
419 *bytes.Reader
420}
421
422func (f *vfsgen۰File) Close() error {
423 return nil
424}
425{{else if not .HasCompressedFile}}
426// We already imported "bytes", but ended up not using it. Avoid unused import error.
427var _ = bytes.Reader{}
428{{end}}
429// vfsgen۰DirInfo is a static definition of a directory.
430type vfsgen۰DirInfo struct {
431 name string
432 modTime time.Time
433 entries []os.FileInfo
434}
435
436func (d *vfsgen۰DirInfo) Read([]byte) (int, error) {
437 return 0, fmt.Errorf("cannot Read from directory %s", d.name)
438}
439func (d *vfsgen۰DirInfo) Close() error { return nil }
440func (d *vfsgen۰DirInfo) Stat() (os.FileInfo, error) { return d, nil }
441
442func (d *vfsgen۰DirInfo) Name() string { return d.name }
443func (d *vfsgen۰DirInfo) Size() int64 { return 0 }
444func (d *vfsgen۰DirInfo) Mode() os.FileMode { return 0755 | os.ModeDir }
445func (d *vfsgen۰DirInfo) ModTime() time.Time { return d.modTime }
446func (d *vfsgen۰DirInfo) IsDir() bool { return true }
447func (d *vfsgen۰DirInfo) Sys() interface{} { return nil }
448
449// vfsgen۰Dir is an opened dir instance.
450type vfsgen۰Dir struct {
451 *vfsgen۰DirInfo
452 pos int // Position within entries for Seek and Readdir.
453}
454
455func (d *vfsgen۰Dir) Seek(offset int64, whence int) (int64, error) {
456 if offset == 0 && whence == io.SeekStart {
457 d.pos = 0
458 return 0, nil
459 }
460 return 0, fmt.Errorf("unsupported Seek in directory %s", d.name)
461}
462
463func (d *vfsgen۰Dir) Readdir(count int) ([]os.FileInfo, error) {
464 if d.pos >= len(d.entries) && count > 0 {
465 return nil, io.EOF
466 }
467 if count <= 0 || count > len(d.entries)-d.pos {
468 count = len(d.entries) - d.pos
469 }
470 e := d.entries[d.pos : d.pos+count]
471 d.pos += count
472 return e, nil
473}
474{{end}}
475
476
477
478{{define "Time"}}
479{{- if .IsZero -}}
480 time.Time{}
481{{- else -}}
482 time.Date({{.Year}}, {{printf "%d" .Month}}, {{.Day}}, {{.Hour}}, {{.Minute}}, {{.Second}}, {{.Nanosecond}}, time.UTC)
483{{- end -}}
484{{end}}
485`))