package router import ( "bytes" stdContext "context" "fmt" "html" "html/template" "io" "io/ioutil" "net/http" "net/url" "os" "path" "path/filepath" "regexp" "sort" "strings" "sync" "time" "github.com/kataras/iris/v12/context" ) const indexName = "/index.html" // DirListFunc is the function signature for customizing directory and file listing. // See `DirList` and `DirListRich` functions for its implementations. type DirListFunc func(ctx *context.Context, dirOptions DirOptions, dirName string, dir http.File) error // Attachments options for files to be downloaded and saved locally by the client. // See `DirOptions`. type Attachments struct { // Set to true to enable the files to be downloaded and // saved locally by the client, instead of serving the file. Enable bool // Options to send files with a limit of bytes sent per second. Limit float64 Burst int // Use this function to change the sent filename. NameFunc func(systemName string) (attachmentName string) } // DirCacheOptions holds the options for the cached file system. // See `DirOptions`structure for more. type DirCacheOptions struct { // Enable or disable cache. Enable bool // Minimum content size for compression in bytes. CompressMinSize int64 // Ignore compress files that match this pattern. CompressIgnore *regexp.Regexp // The available sever's encodings to be negotiated with the client's needs, // common values: gzip, br. Encodings []string // If greater than zero then prints information about cached files to the stdout. // If it's 1 then it prints only the total cached and after-compression reduced file sizes // If it's 2 then it prints it per file too. Verbose uint8 } // DirOptions contains the settings that `FileServer` can use to serve files. // See `DefaultDirOptions`. type DirOptions struct { // Defaults to "/index.html", if request path is ending with **/*/$IndexName // then it redirects to **/*(/) which another handler is handling it, // that another handler, called index handler, is auto-registered by the framework // if end developer does not managed to handle it by hand. IndexName string // PushTargets filenames (map's value) to // be served without additional client's requests (HTTP/2 Push) // when a specific request path (map's key WITHOUT prefix) // is requested and it's not a directory (it's an `IndexFile`). // // Example: // "/": { // "favicon.ico", // "js/main.js", // "css/main.css", // } PushTargets map[string][]string // PushTargetsRegexp like `PushTargets` but accepts regexp which // is compared against all files under a directory (recursively). // The `IndexName` should be set. // // Example: // "/": regexp.MustCompile("((.*).js|(.*).css|(.*).ico)$") // See `iris.MatchCommonAssets` too. PushTargetsRegexp map[string]*regexp.Regexp // Cache to enable in-memory cache and pre-compress files. Cache DirCacheOptions // When files should served under compression. Compress bool // List the files inside the current requested directory if `IndexName` not found. ShowList bool // If `ShowList` is true then this function will be used instead // of the default one to show the list of files of a current requested directory(dir). // See `DirListRich` package-level function too. DirList DirListFunc // Files downloaded and saved locally. Attachments Attachments // Optional validator that loops through each requested resource. AssetValidator func(ctx *context.Context, name string) bool } // DefaultDirOptions holds the default settings for `FileServer`. var DefaultDirOptions = DirOptions{ IndexName: indexName, PushTargets: make(map[string][]string), PushTargetsRegexp: make(map[string]*regexp.Regexp), Cache: DirCacheOptions{ // Disable by-default. Enable: false, // Don't compress files smaller than 300 bytes. CompressMinSize: 300, // Gzip, deflate, br(brotli), snappy. Encodings: context.AllEncodings, // Log to the stdout (no iris logger) the total reduced file size. Verbose: 1, }, Compress: true, ShowList: false, DirList: DirListRich(DirListRichOptions{ Tmpl: DirListRichTemplate, TmplName: "dirlist", }), Attachments: Attachments{ Enable: false, Limit: 0, Burst: 0, }, AssetValidator: nil, } // FileServer returns a Handler which serves files from a specific file system. // The first parameter is the file system, // if it's a `http.Dir` the files should be located near the executable program. // The second parameter is the settings that the caller can use to customize the behavior. // // See `Party#HandleDir` too. // Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/file-server func FileServer(fs http.FileSystem, options DirOptions) context.Handler { if fs == nil { panic("FileServer: fs is nil. The fs parameter should point to a file system of physical system directory or to an embedded one") } // Make sure index name starts with a slash. if options.IndexName != "" { options.IndexName = prefix(options.IndexName, "/") } // Make sure PushTarget's paths are in the proper form. for path, filenames := range options.PushTargets { for idx, filename := range filenames { filenames[idx] = filepath.ToSlash(filename) } options.PushTargets[path] = filenames } if !options.Attachments.Enable { // make sure rate limiting is not used when attachments are not. options.Attachments.Limit = 0 options.Attachments.Burst = 0 } plainStatusCode := func(ctx *context.Context, statusCode int) { if writer, ok := ctx.ResponseWriter().(*context.CompressResponseWriter); ok { writer.Disabled = true } ctx.StatusCode(statusCode) } dirList := options.DirList if dirList == nil { dirList = DirList } open := fsOpener(fs, options.Cache) // We only need its opener, the "fs" is NOT used below. h := func(ctx *context.Context) { r := ctx.Request() name := prefix(r.URL.Path, "/") r.URL.Path = name f, err := open(name, r) if err != nil { plainStatusCode(ctx, http.StatusNotFound) return } defer f.Close() info, err := f.Stat() if err != nil { plainStatusCode(ctx, http.StatusNotFound) return } var indexFound bool // use contents of index.html for directory, if present if info.IsDir() && options.IndexName != "" { // Note that, in contrast of the default net/http mechanism; // here different handlers may serve the indexes // if manually then this will block will never fire, // if index handler are automatically registered by the framework // then this block will be fired on indexes because the static site routes are registered using the static route's handler. // // End-developers must have the chance to register different logic and middlewares // to an index file, useful on Single Page Applications. index := strings.TrimSuffix(name, "/") + options.IndexName fIndex, err := open(index, r) if err == nil { defer fIndex.Close() infoIndex, err := fIndex.Stat() if err == nil { indexFound = true f = fIndex info = infoIndex } } } // Still a directory? (we didn't find an index.html file) if info.IsDir() { if !options.ShowList { plainStatusCode(ctx, http.StatusNotFound) return } if modified, err := ctx.CheckIfModifiedSince(info.ModTime()); !modified && err == nil { ctx.WriteNotModified() ctx.StatusCode(http.StatusNotModified) ctx.Next() return } ctx.SetLastModified(info.ModTime()) err = dirList(ctx, options, info.Name(), f) if err != nil { ctx.Application().Logger().Errorf("FileServer: dirList: %v", err) plainStatusCode(ctx, http.StatusInternalServerError) return } ctx.Next() return } // index requested, send a moved permanently status // and navigate back to the route without the index suffix. if strings.HasSuffix(name, options.IndexName) { localRedirect(ctx, "./") return } if options.AssetValidator != nil { if !options.AssetValidator(ctx, name) { errCode := ctx.GetStatusCode() if ctx.ResponseWriter().Written() <= context.StatusCodeWritten { // if nothing written as body from the AssetValidator but 200 status code(which is the default), // then we assume that the end-developer just returned false expecting this to be not found. if errCode == http.StatusOK { errCode = http.StatusNotFound } plainStatusCode(ctx, errCode) } return } } // try to find and send the correct content type based on the filename // and the binary data inside "f". detectOrWriteContentType(ctx, info.Name(), f) // if not index file and attachments should be force-sent: if !indexFound && options.Attachments.Enable { destName := info.Name() // diposition := "attachment" if nameFunc := options.Attachments.NameFunc; nameFunc != nil { destName = nameFunc(destName) } ctx.ResponseWriter().Header().Set(context.ContentDispositionHeaderKey, "attachment;filename="+destName) } // the encoding saved from the negotiation. encoding, isCached := getFileEncoding(f) if isCached { // if it's cached and its settings didnt allow this file to be compressed // then don't try to compress it on the fly, even if the options.Compress was set to true. if encoding != "" { if ctx.ResponseWriter().Header().Get(context.ContentEncodingHeaderKey) != "" { // disable any compression writer if that header exist, // note that, we don't directly check for CompressResponseWriter type // because it may be a ResponseRecorder. ctx.CompressWriter(false) } // Set the response header we need, the data are already compressed. context.AddCompressHeaders(ctx.ResponseWriter().Header(), encoding) } } else if options.Compress { ctx.CompressWriter(true) } if indexFound && !options.Attachments.Enable { if indexAssets, ok := options.PushTargets[name]; ok { if pusher, ok := ctx.ResponseWriter().Naive().(http.Pusher); ok { var pushOpts *http.PushOptions if encoding != "" { pushOpts = &http.PushOptions{Header: r.Header} } for _, indexAsset := range indexAssets { if indexAsset[0] != '/' { // it's relative path. indexAsset = path.Join(r.RequestURI, indexAsset) } if err = pusher.Push(indexAsset, pushOpts); err != nil { break } } } } if regex, ok := options.PushTargetsRegexp[r.URL.Path]; ok { if pusher, ok := ctx.ResponseWriter().Naive().(http.Pusher); ok { var pushOpts *http.PushOptions if encoding != "" { pushOpts = &http.PushOptions{Header: r.Header} } prefixURL := strings.TrimSuffix(r.RequestURI, name) names, err := findNames(fs, name) if err == nil { for _, indexAsset := range names { // it's an index file, do not pushed that. if strings.HasSuffix(prefix(indexAsset, "/"), options.IndexName) { continue } // match using relative path (without the first '/' slash) // to keep consistency between the `PushTargets` behavior if regex.MatchString(indexAsset) { // println("Regex Matched: " + indexAsset) if err = pusher.Push(path.Join(prefixURL, indexAsset), pushOpts); err != nil { break } } } } } } } // If limit is 0 then same as ServeContent. ctx.ServeContentWithRate(f, info.Name(), info.ModTime(), options.Attachments.Limit, options.Attachments.Burst) if serveCode := ctx.GetStatusCode(); context.StatusCodeNotSuccessful(serveCode) { plainStatusCode(ctx, serveCode) return } ctx.Next() // fire any middleware, if any. } return h } // StripPrefix returns a handler that serves HTTP requests // by removing the given prefix from the request URL's Path // and invoking the handler h. StripPrefix handles a // request for a path that doesn't begin with prefix by // replying with an HTTP 404 not found error. // // Usage: // fileserver := FileServer("./static_files", DirOptions {...}) // h := StripPrefix("/static", fileserver) // app.Get("/static/{file:path}", h) // app.Head("/static/{file:path}", h) func StripPrefix(prefix string, h context.Handler) context.Handler { if prefix == "" { return h } // here we separate the path from the subdomain (if any), we care only for the path // fixes a bug when serving static files via a subdomain canonicalPrefix := prefix if dotWSlashIdx := strings.Index(canonicalPrefix, SubdomainPrefix); dotWSlashIdx > 0 { canonicalPrefix = canonicalPrefix[dotWSlashIdx+1:] } canonicalPrefix = toWebPath(canonicalPrefix) return func(ctx *context.Context) { if p := strings.TrimPrefix(ctx.Request().URL.Path, canonicalPrefix); len(p) < len(ctx.Request().URL.Path) { ctx.Request().URL.Path = p h(ctx) } else { ctx.NotFound() } } } func toWebPath(systemPath string) string { // winos slash to slash webpath := strings.Replace(systemPath, "\\", "/", -1) // double slashes to single webpath = strings.Replace(webpath, "//", "/", -1) return webpath } // Abs calls filepath.Abs but ignores the error and // returns the original value if any error occurred. func Abs(path string) string { absPath, err := filepath.Abs(path) if err != nil { return path } return absPath } // The algorithm uses at most sniffLen bytes to make its decision. const sniffLen = 512 func detectOrWriteContentType(ctx *context.Context, name string, content io.ReadSeeker) (string, error) { // If Content-Type isn't set, use the file's extension to find it, but // if the Content-Type is unset explicitly, do not sniff the type. ctypes, haveType := ctx.ResponseWriter().Header()["Content-Type"] var ctype string if !haveType { ctype = TypeByExtension(filepath.Ext(name)) if ctype == "" { // read a chunk to decide between utf-8 text and binary var buf [sniffLen]byte n, _ := io.ReadFull(content, buf[:]) ctype = http.DetectContentType(buf[:n]) _, err := content.Seek(0, io.SeekStart) // rewind to output whole file if err != nil { return "", err } } ctx.ContentType(ctype) } else if len(ctypes) > 0 { ctype = ctypes[0] } return ctype, nil } // localRedirect gives a Moved Permanently response. // It does not convert relative paths to absolute paths like Redirect does. func localRedirect(ctx *context.Context, newPath string) { if q := ctx.Request().URL.RawQuery; q != "" { newPath += "?" + q } ctx.Header("Location", newPath) ctx.StatusCode(http.StatusMovedPermanently) } // DirectoryExists returns true if a directory(or file) exists, otherwise false func DirectoryExists(dir string) bool { if _, err := os.Stat(dir); os.IsNotExist(err) { return false } return true } // Instead of path.Base(filepath.ToSlash(s)) // let's do something like that, it is faster // (used to list directories on serve-time too): func toBaseName(s string) string { n := len(s) - 1 for i := n; i >= 0; i-- { if c := s[i]; c == '/' || c == '\\' { if i == n { // "s" ends with a slash, remove it and retry. return toBaseName(s[:n]) } return s[i+1:] // return the rest, trimming the slash. } } return s } // DirList is a `DirListFunc` which renders directories and files in html, but plain, mode. // See `DirListRich` for more. func DirList(ctx *context.Context, dirOptions DirOptions, dirName string, dir http.File) error { dirs, err := dir.Readdir(-1) if err != nil { return err } sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() }) ctx.ContentType(context.ContentHTMLHeaderValue) _, err = ctx.WriteString("
\n")
	if err != nil {
		return err
	}

	for _, d := range dirs {
		name := toBaseName(d.Name())

		upath := path.Join(ctx.Request().RequestURI, name)
		url := url.URL{Path: upath}

		downloadAttr := ""
		if dirOptions.Attachments.Enable && !d.IsDir() {
			downloadAttr = " download" // fixes chrome Resource interpreted, other browsers will just ignore this  attribute.
		}

		viewName := name
		if d.IsDir() {
			viewName += "/"
		}

		// name may contain '?' or '#', which must be escaped to remain
		// part of the URL path, and not indicate the start of a query
		// string or fragment.
		_, err = ctx.Writef("%s\n", url.String(), downloadAttr, html.EscapeString(viewName))
		if err != nil {
			return err
		}
	}
	_, err = ctx.WriteString("
\n") return err } // DirListRichOptions the options for the `DirListRich` helper function. type DirListRichOptions struct { // If not nil then this template's "dirlist" is used to render the listing page. Tmpl *template.Template // If not empty then this view file is used to render the listing page. // The view should be registered with `Application.RegisterView`. // E.g. "dirlist.html" TmplName string } // DirListRich is a `DirListFunc` which can be passed to `DirOptions.DirList` field // to override the default file listing appearance. // See `DirListRichTemplate` to modify the template, if necessary. func DirListRich(opts ...DirListRichOptions) DirListFunc { var options DirListRichOptions if len(opts) > 0 { options = opts[0] } if options.TmplName == "" && options.Tmpl == nil { options.Tmpl = DirListRichTemplate } return func(ctx *context.Context, dirOptions DirOptions, dirName string, dir http.File) error { dirs, err := dir.Readdir(-1) if err != nil { return err } sortBy := ctx.URLParam("sort") switch sortBy { case "name": sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() }) case "size": sort.Slice(dirs, func(i, j int) bool { return dirs[i].Size() < dirs[j].Size() }) default: sort.Slice(dirs, func(i, j int) bool { return dirs[i].ModTime().After(dirs[j].ModTime()) }) } pageData := listPageData{ Title: fmt.Sprintf("List of %d files", len(dirs)), Files: make([]fileInfoData, 0, len(dirs)), } for _, d := range dirs { name := toBaseName(d.Name()) upath := path.Join(ctx.Request().RequestURI, name) url := url.URL{Path: upath} viewName := name if d.IsDir() { viewName += "/" } shouldDownload := dirOptions.Attachments.Enable && !d.IsDir() pageData.Files = append(pageData.Files, fileInfoData{ Info: d, ModTime: d.ModTime().UTC().Format(http.TimeFormat), Path: url.String(), RelPath: path.Join(ctx.Path(), name), Name: html.EscapeString(viewName), Download: shouldDownload, }) } if options.TmplName != "" { return ctx.View(options.TmplName, pageData) } return options.Tmpl.ExecuteTemplate(ctx, "dirlist", pageData) } } type ( listPageData struct { Title string // the document's title. Files []fileInfoData } fileInfoData struct { Info os.FileInfo ModTime string // format-ed time. Path string // the request path. RelPath string // file path without the system directory itself (we are not exposing it to the user). Name string // the html-escaped name. Download bool // the file should be downloaded (attachment instead of inline view). } ) // FormatBytes returns a string representation of the "b" bytes. func FormatBytes(b int64) string { const unit = 1000 if b < unit { return fmt.Sprintf("%d B", b) } div, exp := int64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp]) } // DirListRichTemplate is the html template the `DirListRich` function is using to render // the directories and files. var DirListRichTemplate = template.Must(template.New("dirlist"). Funcs(template.FuncMap{ "formatBytes": FormatBytes, }).Parse(` {{.Title}} {{ range $idx, $file := .Files }} {{ if $file.Download }} {{ else }} {{ end }} {{ if $file.Info.IsDir }} {{ else }} {{ end }} {{ end }}
# Name Size
{{ $idx }}{{ $file.Name }}{{ $file.Name }}Dir{{ formatBytes $file.Info.Size }}
`)) // fsOpener returns the file system opener, cached one or the original based on the options Enable field. func fsOpener(fs http.FileSystem, options DirCacheOptions) func(name string, r *http.Request) (http.File, error) { if !options.Enable { // if it's not enabled return the opener original one. return func(name string, _ *http.Request) (http.File, error) { return fs.Open(name) } } c, err := cache(fs, options) if err != nil { panic(err) } return c.Ropen } // cache returns a http.FileSystem which serves in-memory cached (compressed) files. // Look `Verbose` function to print out information while in development status. func cache(fs http.FileSystem, options DirCacheOptions) (*cacheFS, error) { start := time.Now() names, err := findNames(fs, "/") if err != nil { return nil, err } sort.Slice(names, func(i, j int) bool { return strings.Count(names[j], "/") > strings.Count(names[i], "/") }) dirs, err := findDirs(fs, names) if err != nil { return nil, err } files, err := cacheFiles(stdContext.Background(), fs, names, options.Encodings, options.CompressMinSize, options.CompressIgnore) if err != nil { return nil, err } ttc := time.Since(start) c := &cacheFS{dirs: dirs, files: files, algs: options.Encodings} go logCacheFS(c, ttc, len(names), options.Verbose) return c, nil } func logCacheFS(fs *cacheFS, ttc time.Duration, n int, level uint8) { if level == 0 { return } var ( totalLength int64 totalCompressedLength = make(map[string]int64) totalCompressedContents int64 ) for name, f := range fs.files { uncompressed := f.algs[""] totalLength += int64(len(uncompressed)) if level == 2 { fmt.Printf("%s (%s)\n", name, FormatBytes(int64(len(uncompressed)))) } for alg, contents := range f.algs { if alg == "" { continue } totalCompressedContents++ if len(alg) < 7 { alg += strings.Repeat(" ", 7-len(alg)) } totalCompressedLength[alg] += int64(len(contents)) if level == 2 { fmt.Printf("%s (%s)\n", alg, FormatBytes(int64(len(contents)))) } } } fmt.Printf("Time to complete the compression and caching of [%d/%d] files: %s\n", totalCompressedContents/int64(len(fs.algs)), n, ttc) fmt.Printf("Total size reduced from %s to:\n", FormatBytes(totalLength)) for alg, length := range totalCompressedLength { // https://en.wikipedia.org/wiki/Data_compression_ratio reducedRatio := 1 - float64(length)/float64(totalLength) fmt.Printf("%s (%s) [%.2f%%]\n", alg, FormatBytes(length), reducedRatio*100) } } type cacheFS struct { dirs map[string]*dir files fileMap algs []string } var _ http.FileSystem = (*cacheFS)(nil) // Open returns the http.File based on "name". // If file, it always returns a cached file of uncompressed data. // See `Ropen` too. func (c *cacheFS) Open(name string) (http.File, error) { // we always fetch with the sep, // as http requests will do, // and the filename's info.Name() is always base // and without separator prefix // (keep note, we need that fileInfo // wrapper because go-bindata's Name originally // returns the fullname while the http.Dir returns the basename). if name == "" || name[0] != '/' { name = "/" + name } if d, ok := c.dirs[name]; ok { return d, nil } if f, ok := c.files[name]; ok { return f.Get("") } return nil, os.ErrNotExist } // Ropen returns the http.File based on "name". // If file, it negotiates the content encoding, // based on the given algorithms, and // returns the cached file with compressed data, // if the encoding was empty then it // returns the cached file with its original, uncompressed data. // // A check of `GetEncoding(file)` is required to set // response headers. // // Note: We don't require a response writer to set the headers // because the caller of this method may stop the operation // before file's contents are written to the client. func (c *cacheFS) Ropen(name string, r *http.Request) (http.File, error) { if name == "" || name[0] != '/' { name = "/" + name } if d, ok := c.dirs[name]; ok { return d, nil } if f, ok := c.files[name]; ok { encoding, _ := context.GetEncoding(r, c.algs) return f.Get(encoding) } return nil, os.ErrNotExist } // getFileEncoding returns the encoding of an http.File. // If the "f" file was created by a `Cache` call then // it returns the content encoding that this file was cached with. // It returns empty string for files that // were too small or ignored to be compressed. // // It also reports whether the "f" is a cached file or not. func getFileEncoding(f http.File) (string, bool) { if f == nil { return "", false } ff, ok := f.(*file) if !ok { return "", false } return ff.alg, true } // type fileMap map[string] /* path */ map[string] /*compression alg or empty for original */ []byte /*contents */ type fileMap map[string]*file func cacheFiles(ctx stdContext.Context, fs http.FileSystem, names []string, compressAlgs []string, compressMinSize int64, compressIgnore *regexp.Regexp) (fileMap, error) { ctx, cancel := stdContext.WithCancel(ctx) defer cancel() list := make(fileMap, len(names)) mutex := new(sync.Mutex) cache := func(name string) error { f, err := fs.Open(name) if err != nil { return err } inf, err := f.Stat() if err != nil { f.Close() return err } fi := newFileInfo(path.Base(name), inf.Mode(), inf.ModTime()) contents, err := ioutil.ReadAll(f) f.Close() if err != nil { return err } algs := make(map[string][]byte, len(compressAlgs)+1) algs[""] = contents // original contents. mutex.Lock() list[name] = newFile(name, fi, algs) mutex.Unlock() if compressMinSize > 0 && compressMinSize > int64(len(contents)) { return nil } if compressIgnore != nil && compressIgnore.MatchString(name) { return nil } // Note: // We can fire a new goroutine for each compression of the same file // but this will have an impact on CPU cost if // thousands of files running 4 compressions at the same time, // so, unless requested keep it as it's. buf := new(bytes.Buffer) for _, alg := range compressAlgs { // stop all compressions if at least one file failed to. select { case <-ctx.Done(): return ctx.Err() default: break } if alg == "brotli" { alg = "br" } w, err := context.NewCompressWriter(buf, strings.ToLower(alg), -1) if err != nil { return err } _, err = w.Write(contents) w.Close() if err != nil { return err } bs := buf.Bytes() dest := make([]byte, len(bs)) copy(dest, bs) algs[alg] = dest buf.Reset() } return nil } var ( err error wg sync.WaitGroup errOnce sync.Once ) for _, name := range names { wg.Add(1) go func(name string) { defer wg.Done() if fnErr := cache(name); fnErr != nil { errOnce.Do(func() { err = fnErr cancel() }) } }(name) } wg.Wait() return list, err } type cacheStoreFile interface { Get(compressionAlgorithm string) (http.File, error) } type file struct { io.ReadSeeker // nil on cache store and filled on file Get. algs map[string][]byte // non empty for store and nil for files. alg string // empty for cache store, filled with the compression algorithm of this file (useful to decompress). name string baseName string info os.FileInfo } var _ http.File = (*file)(nil) var _ cacheStoreFile = (*file)(nil) func newFile(name string, fi os.FileInfo, algs map[string][]byte) *file { return &file{ name: name, baseName: path.Base(name), info: fi, algs: algs, } } func (f *file) Close() error { return nil } func (f *file) Readdir(count int) ([]os.FileInfo, error) { return nil, os.ErrNotExist } func (f *file) Stat() (os.FileInfo, error) { return f.info, nil } // Get returns a new http.File to be served. // Caller should check if a specific http.File has this method as well. func (f *file) Get(alg string) (http.File, error) { // The "alg" can be empty for non-compressed file contents. // We don't need a new structure. if contents, ok := f.algs[alg]; ok { return &file{ name: f.name, baseName: f.baseName, info: f.info, alg: alg, ReadSeeker: bytes.NewReader(contents), }, nil } // When client accept compression but cached contents are not compressed, // e.g. file too small or ignored one. return f.Get("") } func findNames(fs http.FileSystem, name string) ([]string, error) { f, err := fs.Open(name) if err != nil { return nil, err } defer f.Close() fi, err := f.Stat() if err != nil { return nil, err } if !fi.IsDir() { return []string{name}, nil } fileinfos, err := f.Readdir(-1) if err != nil { return nil, err } files := make([]string, 0) for _, info := range fileinfos { // Note: // go-bindata has absolute names with os.Separator, // http.Dir the basename. filename := toBaseName(info.Name()) fullname := path.Join(name, filename) if fullname == name { // prevent looping through itself when fs is cacheFS. continue } rfiles, err := findNames(fs, fullname) if err != nil { return nil, err } files = append(files, rfiles...) } return files, nil } type fileInfo struct { baseName string modTime time.Time isDir bool mode os.FileMode } var _ os.FileInfo = (*fileInfo)(nil) func newFileInfo(baseName string, mode os.FileMode, modTime time.Time) *fileInfo { return &fileInfo{ baseName: baseName, modTime: modTime, mode: mode, isDir: mode == os.ModeDir, } } func (fi *fileInfo) Close() error { return nil } func (fi *fileInfo) Name() string { return fi.baseName } func (fi *fileInfo) Mode() os.FileMode { return fi.mode } func (fi *fileInfo) ModTime() time.Time { return fi.modTime } func (fi *fileInfo) IsDir() bool { return fi.isDir } func (fi *fileInfo) Size() int64 { return 0 } func (fi *fileInfo) Sys() interface{} { return fi } type dir struct { os.FileInfo // *fileInfo io.ReadSeeker // nil name string // fullname, for any case. baseName string children []os.FileInfo // a slice of *fileInfo } var _ os.FileInfo = (*dir)(nil) var _ http.File = (*dir)(nil) func (d *dir) Close() error { return nil } func (d *dir) Name() string { return d.baseName } func (d *dir) Stat() (os.FileInfo, error) { return d.FileInfo, nil } func (d *dir) Readdir(count int) ([]os.FileInfo, error) { return d.children, nil } func newDir(fi os.FileInfo, fullname string) *dir { baseName := path.Base(fullname) return &dir{ FileInfo: newFileInfo(baseName, os.ModeDir, fi.ModTime()), name: fullname, baseName: baseName, } } var _ http.File = (*dir)(nil) // returns unorderded map of directories both reclusive and flat. func findDirs(fs http.FileSystem, names []string) (map[string]*dir, error) { dirs := make(map[string]*dir, 0) for _, name := range names { f, err := fs.Open(name) if err != nil { return nil, err } inf, err := f.Stat() if err != nil { return nil, err } dirName := path.Dir(name) d, ok := dirs[dirName] if !ok { fi := newFileInfo(path.Base(dirName), os.ModeDir, inf.ModTime()) d = newDir(fi, dirName) dirs[dirName] = d } fi := newFileInfo(path.Base(name), inf.Mode(), inf.ModTime()) // Add the directory file info (=this dir) to the parent one, // so `ShowList` can render sub-directories of this dir. parentName := path.Dir(dirName) if parent, hasParent := dirs[parentName]; hasParent { parent.children = append(parent.children, d) } d.children = append(d.children, fi) } return dirs, nil }