265 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			265 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Go
		
	
	
	
package files
 | 
						|
 | 
						|
import (
 | 
						|
	"crypto/md5"
 | 
						|
	"crypto/sha1"
 | 
						|
	"crypto/sha256"
 | 
						|
	"crypto/sha512"
 | 
						|
	"encoding/hex"
 | 
						|
	"hash"
 | 
						|
	"io"
 | 
						|
	"log"
 | 
						|
	"mime"
 | 
						|
	"net/http"
 | 
						|
	"os"
 | 
						|
	"path"
 | 
						|
	"path/filepath"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/filebrowser/filebrowser/v2/errors"
 | 
						|
	"github.com/filebrowser/filebrowser/v2/rules"
 | 
						|
	"github.com/spf13/afero"
 | 
						|
)
 | 
						|
 | 
						|
// 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 {
 | 
						|
			return file, file.readListing(opts.Checker)
 | 
						|
		}
 | 
						|
 | 
						|
		err = file.detectType(opts.Modify, 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
 | 
						|
 | 
						|
	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
 | 
						|
}
 | 
						|
 | 
						|
func (i *FileInfo) detectType(modify, saveContent bool) error {
 | 
						|
	// 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.
 | 
						|
	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
 | 
						|
	}
 | 
						|
 | 
						|
	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
 | 
						|
	case isBinary(string(buffer[:n])) || i.Size > 10*1024*1024: // 10 MB
 | 
						|
		i.Type = "blob"
 | 
						|
		return nil
 | 
						|
	default:
 | 
						|
		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
 | 
						|
}
 | 
						|
 | 
						|
func (i *FileInfo) detectSubtitles() {
 | 
						|
	if i.Type != "video" {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	i.Subtitles = []string{}
 | 
						|
	ext := filepath.Ext(i.Path)
 | 
						|
 | 
						|
	// TODO: detect multiple languages. Base.Lang.vtt
 | 
						|
 | 
						|
	path := strings.TrimSuffix(i.Path, ext) + ".vtt"
 | 
						|
	if _, err := i.Fs.Stat(path); err == nil {
 | 
						|
		i.Subtitles = append(i.Subtitles, path)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
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()
 | 
						|
		path := path.Join(i.Path, name)
 | 
						|
 | 
						|
		if !checker.Check(path) {
 | 
						|
			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.
 | 
						|
			info, err := i.Fs.Stat(path)
 | 
						|
			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:      path,
 | 
						|
		}
 | 
						|
 | 
						|
		if file.IsDir {
 | 
						|
			listing.NumDirs++
 | 
						|
		} else {
 | 
						|
			listing.NumFiles++
 | 
						|
 | 
						|
			err := file.detectType(true, false)
 | 
						|
			if err != nil {
 | 
						|
				return err
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		listing.Items = append(listing.Items, file)
 | 
						|
	}
 | 
						|
 | 
						|
	i.Listing = listing
 | 
						|
	return nil
 | 
						|
}
 |