mirror of
https://github.com/filebrowser/filebrowser.git
synced 2025-05-08 19:22:57 +00:00

This changes allows to password protect shares. It works by: * Allowing to optionally pass a password when creating a share * If set, the password + salt that is configured via a new flag will be hashed via bcrypt and the hash stored together with the rest of the share * Additionally, a random 96 byte long token gets generated and stored as part of the share * When the backend retrieves an unauthenticated request for a share that has authentication configured, it will return a http 401 * The frontend detects this and will show a login prompt * The actual download links are protected via an url arg that contains the previously generated token. This allows us to avoid buffering the download in the browser and allows pasting the link without breaking it
288 lines
5.9 KiB
Go
288 lines
5.9 KiB
Go
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:shadow
|
|
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
|
|
}
|