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