filebrowser/backend/files/file.go

507 lines
12 KiB
Go
Raw Normal View History

package files
import (
2024-09-16 21:01:16 +00:00
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
2024-08-24 22:02:33 +00:00
"fmt"
"hash"
"io"
"mime"
"net/http"
"os"
2024-08-24 22:02:33 +00:00
"path/filepath"
2024-11-26 17:21:41 +00:00
"sort"
"strconv"
"strings"
"sync"
"time"
2024-02-10 00:13:02 +00:00
"unicode/utf8"
2023-06-15 01:08:09 +00:00
"github.com/gtsteffaniak/filebrowser/errors"
2024-11-21 00:15:30 +00:00
"github.com/gtsteffaniak/filebrowser/fileutils"
2024-08-24 22:02:33 +00:00
"github.com/gtsteffaniak/filebrowser/settings"
2024-11-21 00:15:30 +00:00
"github.com/gtsteffaniak/filebrowser/users"
2024-11-26 17:21:41 +00:00
"github.com/gtsteffaniak/filebrowser/utils"
)
var (
2024-10-07 22:44:53 +00:00
pathMutexes = make(map[string]*sync.Mutex)
pathMutexesMu sync.Mutex // Mutex to protect the pathMutexes map
)
2024-11-26 17:21:41 +00:00
type ItemInfo struct {
Name string `json:"name"`
Size int64 `json:"size"`
ModTime time.Time `json:"modified"`
Type string `json:"type"`
2024-10-07 22:44:53 +00:00
}
// FileInfo describes a file.
2024-10-07 22:44:53 +00:00
// reduced item is non-recursive reduced "Items", used to pass flat items array
type FileInfo struct {
2024-11-26 17:21:41 +00:00
ItemInfo
Files []ItemInfo `json:"files"`
Folders []ItemInfo `json:"folders"`
Path string `json:"path"`
}
// for efficiency, a response will be a pointer to the data
// extra calculated fields can be added here
type ExtendedFileInfo struct {
*FileInfo
Content string `json:"content,omitempty"`
Subtitles []string `json:"subtitles,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 {
2024-08-24 22:02:33 +00:00
Path string // realpath
2024-09-16 21:01:16 +00:00
IsDir bool
Modify bool
Expand bool
ReadHeader bool
2024-11-21 00:15:30 +00:00
Checker users.Checker
2021-04-23 12:04:02 +00:00
Content bool
}
2024-11-21 00:15:30 +00:00
func (f FileOptions) Components() (string, string) {
return filepath.Dir(f.Path), filepath.Base(f.Path)
}
2024-10-07 22:44:53 +00:00
2024-11-26 17:21:41 +00:00
func FileInfoFaster(opts FileOptions) (ExtendedFileInfo, error) {
2024-11-21 00:15:30 +00:00
index := GetIndex(rootPath)
opts.Path = index.makeIndexPath(opts.Path)
2024-11-26 17:21:41 +00:00
response := ExtendedFileInfo{}
// Lock access for the specific path
pathMutex := getMutex(opts.Path)
pathMutex.Lock()
defer pathMutex.Unlock()
if !opts.Checker.Check(opts.Path) {
2024-11-26 17:21:41 +00:00
return response, os.ErrPermission
}
2024-11-26 17:21:41 +00:00
2024-11-21 00:15:30 +00:00
_, isDir, err := GetRealPath(opts.Path)
if err != nil {
2024-11-26 17:21:41 +00:00
return response, err
2024-11-21 00:15:30 +00:00
}
opts.IsDir = isDir
2024-11-26 17:21:41 +00:00
// TODO : whats the best way to save trips to disk here?
// disabled using cache because its not clear if this is helping or hurting
2024-11-21 00:15:30 +00:00
// check if the file exists in the index
2024-11-26 17:21:41 +00:00
//info, exists := index.GetReducedMetadata(opts.Path, opts.IsDir)
//if exists {
// err := RefreshFileInfo(opts)
// if err != nil {
// return info, err
// }
// if opts.Content {
// content := ""
// content, err = getContent(opts.Path)
// if err != nil {
// return info, err
// }
// info.Content = content
// }
// return info, nil
//}
err = index.RefreshFileInfo(opts)
2024-09-16 21:01:16 +00:00
if err != nil {
2024-11-26 17:21:41 +00:00
return response, err
}
2024-11-26 17:21:41 +00:00
info, exists := index.GetReducedMetadata(opts.Path, opts.IsDir)
2024-11-21 00:15:30 +00:00
if !exists {
2024-11-26 17:21:41 +00:00
return response, err
}
2024-11-21 00:15:30 +00:00
if opts.Content {
content, err := getContent(opts.Path)
if err != nil {
2024-11-26 17:21:41 +00:00
return response, err
2024-11-21 00:15:30 +00:00
}
2024-11-26 17:21:41 +00:00
response.Content = content
2024-11-21 00:15:30 +00:00
}
2024-11-26 17:21:41 +00:00
response.FileInfo = info
return response, nil
}
// Checksum checksums a given File for a given User, using a specific
// algorithm. The checksums data is saved on File object.
2024-11-26 17:21:41 +00:00
func GetChecksum(fullPath, algo string) (map[string]string, error) {
subs := map[string]string{}
reader, err := os.Open(fullPath)
if err != nil {
2024-11-26 17:21:41 +00:00
return subs, err
}
defer reader.Close()
hashFuncs := map[string]hash.Hash{
"md5": md5.New(),
"sha1": sha1.New(),
"sha256": sha256.New(),
"sha512": sha512.New(),
}
h, ok := hashFuncs[algo]
if !ok {
2024-11-26 17:21:41 +00:00
return subs, errors.ErrInvalidOption
}
_, err = io.Copy(h, reader)
if err != nil {
2024-11-26 17:21:41 +00:00
return subs, err
}
2024-11-26 17:21:41 +00:00
subs[algo] = hex.EncodeToString(h.Sum(nil))
return subs, nil
}
// RealPath gets the real path for the file, resolving symlinks if supported.
func (i *FileInfo) RealPath() string {
2024-11-26 17:21:41 +00:00
realPath, _, _ := GetRealPath(rootPath, i.Path)
realPath, err := filepath.EvalSymlinks(realPath)
2024-08-24 22:02:33 +00:00
if err == nil {
return realPath
}
return i.Path
}
2024-09-16 21:01:16 +00:00
func GetRealPath(relativePath ...string) (string, bool, error) {
2024-08-24 22:02:33 +00:00
combined := []string{settings.Config.Server.Root}
for _, path := range relativePath {
combined = append(combined, strings.TrimPrefix(path, settings.Config.Server.Root))
}
joinedPath := filepath.Join(combined...)
2024-11-26 17:21:41 +00:00
isDir, _ := utils.RealPathCache.Get(joinedPath + ":isdir").(bool)
cached, ok := utils.RealPathCache.Get(joinedPath).(string)
if ok && cached != "" {
return cached, isDir, nil
}
2024-08-24 22:02:33 +00:00
// Convert relative path to absolute path
absolutePath, err := filepath.Abs(joinedPath)
if err != nil {
2024-11-21 00:15:30 +00:00
return absolutePath, false, fmt.Errorf("could not get real path: %v, %s", combined, err)
2024-08-24 22:02:33 +00:00
}
// Resolve symlinks and get the real path
2024-11-26 17:21:41 +00:00
realPath, isDir, err := resolveSymlinks(absolutePath)
if err == nil {
utils.RealPathCache.Set(joinedPath, realPath)
utils.RealPathCache.Set(joinedPath+":isdir", isDir)
}
return realPath, isDir, err
2024-08-24 22:02:33 +00:00
}
func DeleteFiles(absPath string, opts FileOptions) error {
err := os.RemoveAll(absPath)
if err != nil {
return err
}
2024-11-26 17:21:41 +00:00
index := GetIndex(rootPath)
err = index.RefreshFileInfo(opts)
2024-11-21 00:15:30 +00:00
if err != nil {
return err
}
return nil
}
func MoveResource(realsrc, realdst string, isSrcDir bool) error {
err := fileutils.MoveFile(realsrc, realdst)
if err != nil {
return err
}
2024-11-26 17:21:41 +00:00
index := GetIndex(rootPath)
2024-11-21 00:15:30 +00:00
// refresh info for source and dest
2024-11-26 17:21:41 +00:00
err = index.RefreshFileInfo(FileOptions{
2024-11-21 00:15:30 +00:00
Path: realsrc,
IsDir: isSrcDir,
})
if err != nil {
return errors.ErrEmptyKey
}
refreshConfig := FileOptions{Path: realdst, IsDir: true}
if !isSrcDir {
refreshConfig.Path = filepath.Dir(realdst)
}
2024-11-26 17:21:41 +00:00
err = index.RefreshFileInfo(refreshConfig)
2024-11-21 00:15:30 +00:00
if err != nil {
return errors.ErrEmptyKey
}
return nil
}
func CopyResource(realsrc, realdst string, isSrcDir bool) error {
err := fileutils.CopyFile(realsrc, realdst)
if err != nil {
return err
}
2024-11-26 17:21:41 +00:00
index := GetIndex(rootPath)
2024-11-21 00:15:30 +00:00
refreshConfig := FileOptions{Path: realdst, IsDir: true}
if !isSrcDir {
refreshConfig.Path = filepath.Dir(realdst)
}
2024-11-26 17:21:41 +00:00
err = index.RefreshFileInfo(refreshConfig)
2024-09-16 21:01:16 +00:00
if err != nil {
2024-08-24 22:02:33 +00:00
return errors.ErrEmptyKey
}
return nil
}
func WriteDirectory(opts FileOptions) error {
2024-11-21 00:15:30 +00:00
realPath, _, _ := GetRealPath(rootPath, opts.Path)
2024-08-24 22:02:33 +00:00
// Ensure the parent directories exist
2024-11-21 00:15:30 +00:00
err := os.MkdirAll(realPath, 0775)
2024-08-24 22:02:33 +00:00
if err != nil {
return err
}
2024-11-26 17:21:41 +00:00
index := GetIndex(rootPath)
err = index.RefreshFileInfo(opts)
2024-09-16 21:01:16 +00:00
if err != nil {
2024-08-24 22:02:33 +00:00
return errors.ErrEmptyKey
}
return nil
}
func WriteFile(opts FileOptions, in io.Reader) error {
2024-11-26 17:21:41 +00:00
dst, _, _ := GetRealPath(rootPath, opts.Path)
2024-08-24 22:02:33 +00:00
parentDir := filepath.Dir(dst)
// Create the directory and all necessary parents
2024-11-26 17:21:41 +00:00
err := os.MkdirAll(parentDir, 0775)
2024-08-24 22:02:33 +00:00
if err != nil {
return err
}
// Open the file for writing (create if it doesn't exist, truncate if it does)
file, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
if err != nil {
return err
}
defer file.Close()
// Copy the contents from the reader to the file
_, err = io.Copy(file, in)
if err != nil {
return err
}
opts.Path = parentDir
2024-11-26 17:21:41 +00:00
opts.IsDir = true
index := GetIndex(rootPath)
return index.RefreshFileInfo(opts)
2024-08-24 22:02:33 +00:00
}
// resolveSymlinks resolves symlinks in the given path
2024-09-16 21:01:16 +00:00
func resolveSymlinks(path string) (string, bool, error) {
2024-08-24 22:02:33 +00:00
for {
2024-11-26 17:21:41 +00:00
// Get the file info using os.Lstat to handle symlinks
2024-08-24 22:02:33 +00:00
info, err := os.Lstat(path)
if err != nil {
2024-11-26 17:21:41 +00:00
return path, false, fmt.Errorf("could not stat path: %s, %v", path, err)
2024-08-24 22:02:33 +00:00
}
2024-11-26 17:21:41 +00:00
// Check if the path is a symlink
2024-08-24 22:02:33 +00:00
if info.Mode()&os.ModeSymlink != 0 {
// Read the symlink target
target, err := os.Readlink(path)
if err != nil {
2024-11-26 17:21:41 +00:00
return path, false, fmt.Errorf("could not read symlink: %s, %v", path, err)
2024-08-24 22:02:33 +00:00
}
2024-11-26 17:21:41 +00:00
// Resolve the symlink's target relative to its directory
// This ensures the resolved path is absolute and correctly calculated
2024-08-24 22:02:33 +00:00
path = filepath.Join(filepath.Dir(path), target)
} else {
2024-11-26 17:21:41 +00:00
// Not a symlink, so return the resolved path and whether it's a directory
isDir := info.IsDir()
return path, isDir, nil
2024-08-24 22:02:33 +00:00
}
}
}
2024-02-10 00:13:02 +00:00
// addContent reads and sets content based on the file type.
2024-11-21 00:15:30 +00:00
func getContent(path string) (string, error) {
realPath, _, err := GetRealPath(rootPath, path)
if err != nil {
return "", err
2024-02-10 00:13:02 +00:00
}
2024-11-21 00:15:30 +00:00
content, err := os.ReadFile(realPath)
if err != nil {
return "", err
}
stringContent := string(content)
if !utf8.ValidString(stringContent) {
2024-11-26 17:21:41 +00:00
return "", nil
2024-11-21 00:15:30 +00:00
}
if stringContent == "" {
return "empty-file-x6OlSil", nil
}
return stringContent, nil
2024-02-10 00:13:02 +00:00
}
// detectType detects the file type.
2024-11-26 17:21:41 +00:00
func (i *ItemInfo) detectType(path string, modify, saveContent, readHeader bool) error {
2024-11-21 00:15:30 +00:00
name := i.Name
var contentErr error
2024-02-10 00:13:02 +00:00
2024-11-21 00:15:30 +00:00
ext := filepath.Ext(name)
2021-03-17 18:06:56 +00:00
var buffer []byte
if readHeader {
2024-11-21 00:15:30 +00:00
buffer = i.readFirstBytes(path)
mimetype := mime.TypeByExtension(ext)
2021-03-17 18:06:56 +00:00
if mimetype == "" {
http.DetectContentType(buffer)
2021-03-17 18:06:56 +00:00
}
}
2024-02-10 00:13:02 +00:00
for _, fileType := range AllFiletypeOptions {
if IsMatchingType(ext, fileType) {
i.Type = fileType
}
switch i.Type {
case "text":
if !modify {
i.Type = "textImmutable"
}
if saveContent {
2024-11-21 00:15:30 +00:00
return contentErr
}
case "video":
2024-11-21 00:15:30 +00:00
// TODO add back somewhere else, not during metadata fetch
//parentDir := strings.TrimRight(path, name)
//i.detectSubtitles(parentDir)
case "doc":
if ext == ".pdf" {
i.Type = "pdf"
2024-02-10 00:13:02 +00:00
return nil
}
if saveContent {
2024-11-21 00:15:30 +00:00
return nil
}
}
}
if i.Type == "" {
i.Type = "blob"
2024-02-10 00:13:02 +00:00
if saveContent {
2024-11-21 00:15:30 +00:00
return contentErr
2024-02-10 00:13:02 +00:00
}
}
2024-02-10 00:13:02 +00:00
return nil
}
// readFirstBytes reads the first bytes of the file.
2024-11-26 17:21:41 +00:00
func (i *ItemInfo) readFirstBytes(path string) []byte {
2024-11-21 00:15:30 +00:00
file, err := os.Open(path)
if err != nil {
i.Type = "blob"
return nil
}
2024-08-24 22:02:33 +00:00
defer file.Close()
2021-07-26 10:00:05 +00:00
buffer := make([]byte, 512) //nolint:gomnd
2024-08-24 22:02:33 +00:00
n, err := file.Read(buffer)
if err != nil && err != io.EOF {
i.Type = "blob"
return nil
}
return buffer[:n]
}
2024-11-26 17:21:41 +00:00
// TODO add subtitles back
// detectSubtitles detects subtitles for video files.
2024-11-21 00:15:30 +00:00
//func (i *FileInfo) detectSubtitles(path string) {
// if i.Type != "video" {
// return
// }
// parentDir := filepath.Dir(path)
// fileName := filepath.Base(path)
// i.Subtitles = []string{}
// ext := filepath.Ext(fileName)
// dir, err := os.Open(parentDir)
// if err != nil {
// // Directory must have been deleted, remove it from the index
// return
// }
// defer dir.Close() // Ensure directory handle is closed
//
// files, err := dir.Readdir(-1)
// if err != nil {
// return
// }
//
// base := strings.TrimSuffix(fileName, ext)
// subtitleExts := []string{".vtt", ".txt", ".srt", ".lrc"}
//
// for _, f := range files {
// if f.IsDir() || !strings.HasPrefix(f.Name(), base) {
// continue
// }
//
// for _, subtitleExt := range subtitleExts {
// if strings.HasSuffix(f.Name(), subtitleExt) {
// i.Subtitles = append(i.Subtitles, filepath.Join(parentDir, f.Name()))
// break
// }
// }
// }
//}
2024-08-24 22:02:33 +00:00
func IsNamedPipe(mode os.FileMode) bool {
return mode&os.ModeNamedPipe != 0
}
func IsSymlink(mode os.FileMode) bool {
return mode&os.ModeSymlink != 0
}
func getMutex(path string) *sync.Mutex {
// Lock access to pathMutexes map
pathMutexesMu.Lock()
defer pathMutexesMu.Unlock()
// Create a mutex for the path if it doesn't exist
if pathMutexes[path] == nil {
pathMutexes[path] = &sync.Mutex{}
}
return pathMutexes[path]
}
2024-08-24 22:02:33 +00:00
func Exists(path string) bool {
_, err := os.Stat(path)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return false
}
2024-11-26 17:21:41 +00:00
func (info *FileInfo) SortItems() {
sort.Slice(info.Folders, func(i, j int) bool {
// Convert strings to integers for numeric sorting if both are numeric
numI, errI := strconv.Atoi(info.Folders[i].Name)
numJ, errJ := strconv.Atoi(info.Folders[j].Name)
if errI == nil && errJ == nil {
return numI < numJ
}
// Fallback to case-insensitive lexicographical sorting
return strings.ToLower(info.Folders[i].Name) < strings.ToLower(info.Folders[j].Name)
})
sort.Slice(info.Files, func(i, j int) bool {
// Convert strings to integers for numeric sorting if both are numeric
numI, errI := strconv.Atoi(info.Files[i].Name)
numJ, errJ := strconv.Atoi(info.Files[j].Name)
if errI == nil && errJ == nil {
return numI < numJ
}
// Fallback to case-insensitive lexicographical sorting
return strings.ToLower(info.Files[i].Name) < strings.ToLower(info.Files[j].Name)
})
}