| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | package files | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							| 
									
										
										
										
											2020-05-31 23:12:36 +00:00
										 |  |  | 	"crypto/md5"  //nolint:gosec
 | 
					
						
							|  |  |  | 	"crypto/sha1" //nolint:gosec
 | 
					
						
							| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | 	"crypto/sha256" | 
					
						
							|  |  |  | 	"crypto/sha512" | 
					
						
							|  |  |  | 	"encoding/hex" | 
					
						
							|  |  |  | 	"hash" | 
					
						
							|  |  |  | 	"io" | 
					
						
							| 
									
										
										
										
											2019-01-06 12:59:53 +00:00
										 |  |  | 	"log" | 
					
						
							| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | 	"mime" | 
					
						
							|  |  |  | 	"net/http" | 
					
						
							|  |  |  | 	"os" | 
					
						
							|  |  |  | 	"path" | 
					
						
							|  |  |  | 	"path/filepath" | 
					
						
							|  |  |  | 	"strings" | 
					
						
							|  |  |  | 	"time" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-31 23:12:36 +00:00
										 |  |  | 	"github.com/spf13/afero" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | 	"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"` | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // FileOptions are the options when getting a file info.
 | 
					
						
							|  |  |  | type FileOptions struct { | 
					
						
							|  |  |  | 	Fs      afero.Fs | 
					
						
							|  |  |  | 	Path    string | 
					
						
							|  |  |  | 	Modify  bool | 
					
						
							|  |  |  | 	Expand  bool | 
					
						
							|  |  |  | 	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()), | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if opts.Expand { | 
					
						
							|  |  |  | 		if file.IsDir { | 
					
						
							| 
									
										
										
										
											2020-05-31 23:12:36 +00:00
										 |  |  | 			if err := file.readListing(opts.Checker); err != nil { //nolint:shadow
 | 
					
						
							|  |  |  | 				return nil, err | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			return file, nil | 
					
						
							| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-01-06 09:24:09 +00:00
										 |  |  | 		err = file.detectType(opts.Modify, true) | 
					
						
							| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | 		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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-31 23:12:36 +00:00
										 |  |  | 	//nolint:gosec
 | 
					
						
							| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | 	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 | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-31 23:12:36 +00:00
										 |  |  | //nolint:goconst
 | 
					
						
							|  |  |  | //TODO: use constants
 | 
					
						
							| 
									
										
										
										
											2019-01-06 09:24:09 +00:00
										 |  |  | func (i *FileInfo) detectType(modify, saveContent bool) error { | 
					
						
							| 
									
										
										
										
											2019-01-06 12:59:53 +00:00
										 |  |  | 	// 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.
 | 
					
						
							| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | 	reader, err := i.Fs.Open(i.Path) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2019-01-06 12:59:53 +00:00
										 |  |  | 		log.Print(err) | 
					
						
							|  |  |  | 		i.Type = "blob" | 
					
						
							|  |  |  | 		return nil | 
					
						
							| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	defer reader.Close() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	buffer := make([]byte, 512) | 
					
						
							|  |  |  | 	n, err := reader.Read(buffer) | 
					
						
							|  |  |  | 	if err != nil && err != io.EOF { | 
					
						
							| 
									
										
										
										
											2019-01-06 12:59:53 +00:00
										 |  |  | 		log.Print(err) | 
					
						
							|  |  |  | 		i.Type = "blob" | 
					
						
							|  |  |  | 		return nil | 
					
						
							| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	mimetype := mime.TypeByExtension(i.Extension) | 
					
						
							|  |  |  | 	if mimetype == "" { | 
					
						
							|  |  |  | 		mimetype = http.DetectContentType(buffer[:n]) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	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 | 
					
						
							| 
									
										
										
										
											2019-05-07 18:02:12 +00:00
										 |  |  | 	case isBinary(buffer[:n], n) || i.Size > 10*1024*1024: // 10 MB
 | 
					
						
							| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | 		i.Type = "blob" | 
					
						
							|  |  |  | 		return nil | 
					
						
							|  |  |  | 	default: | 
					
						
							|  |  |  | 		i.Type = "text" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if !modify { | 
					
						
							|  |  |  | 			i.Type = "textImmutable" | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-01-06 09:24:09 +00:00
										 |  |  | 		if saveContent { | 
					
						
							|  |  |  | 			afs := &afero.Afero{Fs: i.Fs} | 
					
						
							|  |  |  | 			content, err := afs.ReadFile(i.Path) | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				return err | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			i.Content = string(content) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (i *FileInfo) detectSubtitles() { | 
					
						
							|  |  |  | 	if i.Type != "video" { | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	i.Subtitles = []string{} | 
					
						
							|  |  |  | 	ext := filepath.Ext(i.Path) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// TODO: detect multiple languages. Base.Lang.vtt
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-31 23:12:36 +00:00
										 |  |  | 	fPath := strings.TrimSuffix(i.Path, ext) + ".vtt" | 
					
						
							|  |  |  | 	if _, err := i.Fs.Stat(fPath); err == nil { | 
					
						
							|  |  |  | 		i.Subtitles = append(i.Subtitles, fPath) | 
					
						
							| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (i *FileInfo) readListing(checker rules.Checker) 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() | 
					
						
							| 
									
										
										
										
											2020-05-31 23:12:36 +00:00
										 |  |  | 		fPath := path.Join(i.Path, name) | 
					
						
							| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-31 23:12:36 +00:00
										 |  |  | 		if !checker.Check(fPath) { | 
					
						
							| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if strings.HasPrefix(f.Mode().String(), "L") { | 
					
						
							|  |  |  | 			// It's a symbolic link. We try to follow it. If it doesn't work,
 | 
					
						
							|  |  |  | 			// we stay with the link information instead if the target's.
 | 
					
						
							| 
									
										
										
										
											2020-05-31 23:12:36 +00:00
										 |  |  | 			info, err := i.Fs.Stat(fPath) | 
					
						
							| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | 			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), | 
					
						
							| 
									
										
										
										
											2020-05-31 23:12:36 +00:00
										 |  |  | 			Path:      fPath, | 
					
						
							| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if file.IsDir { | 
					
						
							|  |  |  | 			listing.NumDirs++ | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			listing.NumFiles++ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-01-06 09:24:09 +00:00
										 |  |  | 			err := file.detectType(true, false) | 
					
						
							| 
									
										
										
										
											2019-01-05 22:44:33 +00:00
										 |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				return err | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		listing.Items = append(listing.Items, file) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	i.Listing = listing | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } |