package files import ( "crypto/md5" //nolint:gosec "crypto/sha1" //nolint:gosec "crypto/sha256" "crypto/sha512" "encoding/hex" "hash" "io" "log" "mime" "net/http" "os" "path" "path/filepath" "strings" "time" "github.com/spf13/afero" "github.com/filebrowser/filebrowser/v2/errors" "github.com/filebrowser/filebrowser/v2/rules" ) // FileInfo describes a file. type FileInfo struct { *Listing Fs afero.Fs `json:"-"` Path string `json:"path"` Name string `json:"name"` Size int64 `json:"size"` Extension string `json:"extension"` ModTime time.Time `json:"modified"` Mode os.FileMode `json:"mode"` IsDir bool `json:"isDir"` Type string `json:"type"` Subtitles []string `json:"subtitles,omitempty"` Content string `json:"content,omitempty"` Checksums map[string]string `json:"checksums,omitempty"` Token string `json:"token,omitempty"` } // FileOptions are the options when getting a file info. type FileOptions struct { Fs afero.Fs Path string Modify bool Expand bool ReadHeader bool Token string Checker rules.Checker } // NewFileInfo creates a File object from a path and a given user. This File // object will be automatically filled depending on if it is a directory // or a file. If it's a video file, it will also detect any subtitles. func NewFileInfo(opts FileOptions) (*FileInfo, error) { if !opts.Checker.Check(opts.Path) { return nil, os.ErrPermission } info, err := opts.Fs.Stat(opts.Path) if err != nil { return nil, err } file := &FileInfo{ Fs: opts.Fs, Path: opts.Path, Name: info.Name(), ModTime: info.ModTime(), Mode: info.Mode(), IsDir: info.IsDir(), Size: info.Size(), Extension: filepath.Ext(info.Name()), Token: opts.Token, } if opts.Expand { if file.IsDir { if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet return nil, err } return file, nil } err = file.detectType(opts.Modify, true, true) if err != nil { return nil, err } } return file, err } // Checksum checksums a given File for a given User, using a specific // algorithm. The checksums data is saved on File object. func (i *FileInfo) Checksum(algo string) error { if i.IsDir { return errors.ErrIsDirectory } if i.Checksums == nil { i.Checksums = map[string]string{} } reader, err := i.Fs.Open(i.Path) if err != nil { return err } defer reader.Close() var h hash.Hash //nolint:gosec switch algo { case "md5": h = md5.New() case "sha1": h = sha1.New() case "sha256": h = sha256.New() case "sha512": h = sha512.New() default: return errors.ErrInvalidOption } _, err = io.Copy(h, reader) if err != nil { return err } i.Checksums[algo] = hex.EncodeToString(h.Sum(nil)) return nil } //nolint:goconst //TODO: use constants func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error { if IsNamedPipe(i.Mode) { i.Type = "blob" return nil } // failing to detect the type should not return error. // imagine the situation where a file in a dir with thousands // of files couldn't be opened: we'd have immediately // a 500 even though it doesn't matter. So we just log it. var buffer []byte mimetype := mime.TypeByExtension(i.Extension) if mimetype == "" && readHeader { buffer = i.readFirstBytes() mimetype = http.DetectContentType(buffer) } switch { case strings.HasPrefix(mimetype, "video"): i.Type = "video" i.detectSubtitles() return nil case strings.HasPrefix(mimetype, "audio"): i.Type = "audio" return nil case strings.HasPrefix(mimetype, "image"): i.Type = "image" return nil case (strings.HasPrefix(mimetype, "text") || (len(buffer) > 0 && !isBinary(buffer))) && i.Size <= 10*1024*1024: // 10 MB i.Type = "text" if !modify { i.Type = "textImmutable" } if saveContent { afs := &afero.Afero{Fs: i.Fs} content, err := afs.ReadFile(i.Path) if err != nil { return err } i.Content = string(content) } return nil default: i.Type = "blob" } return nil } func (i *FileInfo) readFirstBytes() []byte { reader, err := i.Fs.Open(i.Path) if err != nil { log.Print(err) i.Type = "blob" return nil } defer reader.Close() buffer := make([]byte, 512) n, err := reader.Read(buffer) if err != nil && err != io.EOF { log.Print(err) i.Type = "blob" return nil } return buffer[:n] } func (i *FileInfo) detectSubtitles() { if i.Type != "video" { return } i.Subtitles = []string{} ext := filepath.Ext(i.Path) // TODO: detect multiple languages. Base.Lang.vtt fPath := strings.TrimSuffix(i.Path, ext) + ".vtt" if _, err := i.Fs.Stat(fPath); err == nil { i.Subtitles = append(i.Subtitles, fPath) } } func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error { afs := &afero.Afero{Fs: i.Fs} dir, err := afs.ReadDir(i.Path) if err != nil { return err } listing := &Listing{ Items: []*FileInfo{}, NumDirs: 0, NumFiles: 0, } for _, f := range dir { name := f.Name() fPath := path.Join(i.Path, name) if !checker.Check(fPath) { continue } if IsSymlink(f.Mode()) { // It's a symbolic link. We try to follow it. If it doesn't work, // we stay with the link information instead of the target's. info, err := i.Fs.Stat(fPath) if err == nil { f = info } } file := &FileInfo{ Fs: i.Fs, Name: name, Size: f.Size(), ModTime: f.ModTime(), Mode: f.Mode(), IsDir: f.IsDir(), Extension: filepath.Ext(name), Path: fPath, } if file.IsDir { listing.NumDirs++ } else { listing.NumFiles++ err := file.detectType(true, false, readHeader) if err != nil { return err } } listing.Items = append(listing.Items, file) } i.Listing = listing return nil }