filebrowser/backend/files/file.go

458 lines
12 KiB
Go
Raw Permalink 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"
2024-12-17 00:01:55 +00:00
"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"
2025-01-21 14:02:43 +00:00
"github.com/gtsteffaniak/filebrowser/backend/cache"
2024-12-17 00:01:55 +00:00
"github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/backend/fileutils"
2025-02-16 14:07:38 +00:00
"github.com/gtsteffaniak/filebrowser/backend/logger"
2025-01-21 14:02:43 +00:00
"github.com/gtsteffaniak/filebrowser/backend/settings"
2024-12-17 00:01:55 +00:00
"github.com/gtsteffaniak/filebrowser/backend/users"
2025-01-21 14:02:43 +00:00
"github.com/gtsteffaniak/filebrowser/backend/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 {
2025-01-21 14:02:43 +00:00
Name string `json:"name"` // name of the file
Size int64 `json:"size"` // length in bytes for regular files
ModTime time.Time `json:"modified"` // modification time
Type string `json:"type"` // type of the file, either "directory" or a file mimetype
2025-01-27 00:21:12 +00:00
Hidden bool `json:"hidden"` // whether the file is hidden
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
2025-01-21 14:02:43 +00:00
Files []ItemInfo `json:"files"` // files in the directory
Folders []ItemInfo `json:"folders"` // folders in the directory
Path string `json:"path"` // path scoped to the associated index
2024-11-26 17:21:41 +00:00
}
// for efficiency, a response will be a pointer to the data
// extra calculated fields can be added here
type ExtendedFileInfo struct {
2025-02-16 14:07:38 +00:00
FileInfo
2025-01-21 14:02:43 +00:00
Content string `json:"content,omitempty"` // text content of a file, if requested
Subtitles []string `json:"subtitles,omitempty"` // subtitles for video files
Checksums map[string]string `json:"checksums,omitempty"` // checksums for the file
Token string `json:"token,omitempty"` // token for the file -- used for sharing
OnlyOfficeId string `json:"onlyOfficeId,omitempty"` // id for onlyoffice files
Source string `json:"source"` // associated index source for the file
RealPath string `json:"-"`
}
// FileOptions are the options when getting a file info.
type FileOptions struct {
2024-08-24 22:02:33 +00:00
Path string // realpath
2025-01-05 19:05:33 +00:00
Source string
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) {
response := ExtendedFileInfo{}
2025-01-05 19:05:33 +00:00
if opts.Source == "" {
opts.Source = "default"
}
index := GetIndex(opts.Source)
if index == nil {
return response, fmt.Errorf("could not get index: %v ", opts.Source)
}
opts.Path = index.makeIndexPath(opts.Path)
// 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
2025-01-05 19:05:33 +00:00
realPath, isDir, err := index.GetRealPath(opts.Path)
2024-11-21 00:15:30 +00:00
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 {
2025-02-16 14:07:38 +00:00
return response, fmt.Errorf("could not get metadata for path: %v", opts.Path)
}
2024-11-21 00:15:30 +00:00
if opts.Content {
2025-01-05 19:05:33 +00:00
content, err := getContent("default", opts.Path)
2024-11-21 00:15:30 +00:00
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
}
2025-02-16 14:07:38 +00:00
response.FileInfo = *info
2025-01-05 19:05:33 +00:00
response.RealPath = realPath
2025-02-16 14:07:38 +00:00
response.Source = opts.Source
2025-01-21 14:02:43 +00:00
if settings.Config.Integrations.OnlyOffice.Secret != "" && info.Type != "directory" && isOnlyOffice(info.Name) {
response.OnlyOfficeId = generateOfficeId(realPath)
}
2025-02-16 14:07:38 +00:00
if strings.HasPrefix(info.Type, "video") {
response.detectSubtitles(realPath)
}
2024-11-26 17:21:41 +00:00
return response, nil
}
2025-01-21 14:02:43 +00:00
func generateOfficeId(realPath string) string {
key, ok := cache.OnlyOffice.Get(realPath).(string)
if !ok {
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
documentKey := utils.HashSHA256(realPath + timestamp)
cache.OnlyOffice.Set(realPath, documentKey)
return documentKey
}
return key
}
// 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
}
2025-01-05 19:05:33 +00:00
func DeleteFiles(source, absPath string, dirPath string) error {
2024-08-24 22:02:33 +00:00
err := os.RemoveAll(absPath)
if err != nil {
return err
}
2025-01-05 19:05:33 +00:00
index := GetIndex(source)
refreshConfig := FileOptions{Path: dirPath, IsDir: true}
2024-12-17 00:01:55 +00:00
err = index.RefreshFileInfo(refreshConfig)
2024-11-21 00:15:30 +00:00
if err != nil {
return err
}
return nil
}
2025-01-05 19:05:33 +00:00
func MoveResource(source, realsrc, realdst string, isSrcDir bool) error {
2024-11-21 00:15:30 +00:00
err := fileutils.MoveFile(realsrc, realdst)
if err != nil {
return err
}
2025-01-05 19:05:33 +00:00
index := GetIndex(source)
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{
2025-01-26 00:31:40 +00:00
Path: realsrc,
2024-11-21 00:15:30 +00:00
IsDir: isSrcDir,
})
if err != nil {
2025-01-26 00:31:40 +00:00
return fmt.Errorf("could not refresh index for source: %v", err)
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-11-21 00:15:30 +00:00
if err != nil {
2025-01-26 00:31:40 +00:00
return fmt.Errorf("could not refresh index for dest: %v", err)
2024-11-21 00:15:30 +00:00
}
return nil
}
2025-01-05 19:05:33 +00:00
func CopyResource(source, realsrc, realdst string, isSrcDir bool) error {
2024-11-21 00:15:30 +00:00
err := fileutils.CopyFile(realsrc, realdst)
if err != nil {
return err
}
2025-01-05 19:05:33 +00:00
index := GetIndex(source)
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 {
2025-01-05 19:05:33 +00:00
idx := GetIndex(opts.Source)
realPath, _, _ := idx.GetRealPath(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
}
2025-01-05 19:05:33 +00:00
err = idx.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 {
2025-01-05 19:05:33 +00:00
idx := GetIndex(opts.Source)
dst, _, _ := idx.GetRealPath(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
2025-01-05 19:05:33 +00:00
return idx.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.
2025-01-05 19:05:33 +00:00
func getContent(source, path string) (string, error) {
idx := GetIndex(source)
realPath, _, err := idx.GetRealPath(path)
2024-11-21 00:15:30 +00:00
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
}
2024-12-17 00:01:55 +00:00
// DetectType detects the MIME type of a file and updates the ItemInfo struct.
2025-01-05 19:05:33 +00:00
func (i *ItemInfo) DetectType(realPath string, saveContent bool) {
2024-11-21 00:15:30 +00:00
name := i.Name
ext := filepath.Ext(name)
2024-02-10 00:13:02 +00:00
2024-12-17 00:01:55 +00:00
// Attempt MIME detection by file extension
2025-02-08 00:12:11 +00:00
if ext == ".md" {
i.Type = "text/markdown"
return
}
2024-12-17 00:01:55 +00:00
i.Type = strings.Split(mime.TypeByExtension(ext), ";")[0]
if i.Type == "" {
2024-12-17 00:01:55 +00:00
i.Type = extendedMimeTypeCheck(ext)
}
2024-12-17 00:01:55 +00:00
if i.Type == "blob" {
// Read only the first 512 bytes for efficient MIME detection
2025-01-05 19:05:33 +00:00
file, err := os.Open(realPath)
2024-12-17 00:01:55 +00:00
if err != nil {
2024-12-17 00:01:55 +00:00
} else {
defer file.Close()
buffer := make([]byte, 512)
n, _ := file.Read(buffer) // Ignore errors from Read
i.Type = strings.Split(http.DetectContentType(buffer[:n]), ";")[0]
}
}
}
2024-11-26 17:21:41 +00:00
// TODO add subtitles back
// detectSubtitles detects subtitles for video files.
2025-02-16 14:07:38 +00:00
func (i *ExtendedFileInfo) detectSubtitles(path string) {
if !strings.HasPrefix(i.Type, "video") {
logger.Debug("subtitles are not supported for this file : " + path)
return
}
idx := GetIndex(i.Source)
parentInfo, exists := idx.GetReducedMetadata(filepath.Dir(i.Path), true)
if !exists {
return
}
base := strings.Split(i.Name, ".")[0]
for _, f := range parentInfo.Files {
baseName := strings.Split(f.Name, ".")[0]
if baseName != base {
continue
}
for _, subtitleExt := range []string{".vtt", ".srt", ".lrc", ".sbv", ".ass", ".ssa", ".sub", ".smi"} {
if strings.HasSuffix(f.Name, subtitleExt) {
fullPathBase := strings.Split(i.Path, ".")[0]
i.Subtitles = append(i.Subtitles, fullPathBase+subtitleExt)
}
}
}
}
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 {
2025-01-05 19:05:33 +00:00
nameWithoutExt := strings.Split(info.Folders[i].Name, ".")[0]
nameWithoutExt2 := strings.Split(info.Folders[j].Name, ".")[0]
2024-11-26 17:21:41 +00:00
// Convert strings to integers for numeric sorting if both are numeric
2025-01-05 19:05:33 +00:00
numI, errI := strconv.Atoi(nameWithoutExt)
numJ, errJ := strconv.Atoi(nameWithoutExt2)
2024-11-26 17:21:41 +00:00
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 {
2025-01-05 19:05:33 +00:00
nameWithoutExt := strings.Split(info.Files[i].Name, ".")[0]
nameWithoutExt2 := strings.Split(info.Files[j].Name, ".")[0]
2024-11-26 17:21:41 +00:00
// Convert strings to integers for numeric sorting if both are numeric
2025-01-05 19:05:33 +00:00
numI, errI := strconv.Atoi(nameWithoutExt)
numJ, errJ := strconv.Atoi(nameWithoutExt2)
2024-11-26 17:21:41 +00:00
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)
})
}