2019-01-05 22:44:33 +00:00
|
|
|
package files
|
|
|
|
|
|
|
|
import (
|
2024-09-16 21:01:16 +00:00
|
|
|
"crypto/md5"
|
|
|
|
"crypto/sha1"
|
2019-01-05 22:44:33 +00:00
|
|
|
"crypto/sha256"
|
|
|
|
"crypto/sha512"
|
|
|
|
"encoding/hex"
|
2024-08-24 22:02:33 +00:00
|
|
|
"fmt"
|
2019-01-05 22:44:33 +00:00
|
|
|
"hash"
|
|
|
|
"io"
|
|
|
|
"mime"
|
|
|
|
"net/http"
|
2024-12-17 00:01:55 +00:00
|
|
|
|
2019-01-05 22:44:33 +00:00
|
|
|
"os"
|
2024-08-24 22:02:33 +00:00
|
|
|
"path/filepath"
|
2024-11-26 17:21:41 +00:00
|
|
|
"sort"
|
|
|
|
"strconv"
|
2019-01-05 22:44:33 +00:00
|
|
|
"strings"
|
2023-12-01 23:47:00 +00:00
|
|
|
"sync"
|
2019-01-05 22:44:33 +00:00
|
|
|
"time"
|
2024-02-10 00:13:02 +00:00
|
|
|
"unicode/utf8"
|
2019-01-05 22:44:33 +00:00
|
|
|
|
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"
|
2023-12-01 23:47:00 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2024-10-07 22:44:53 +00:00
|
|
|
pathMutexes = make(map[string]*sync.Mutex)
|
|
|
|
pathMutexesMu sync.Mutex // Mutex to protect the pathMutexes map
|
2019-01-05 22:44:33 +00:00
|
|
|
)
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2019-01-05 22:44:33 +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
|
2019-01-05 22:44:33 +00:00
|
|
|
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:"-"`
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2021-01-07 10:30:17 +00:00
|
|
|
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
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
func (f FileOptions) Components() (string, string) {
|
|
|
|
return filepath.Dir(f.Path), filepath.Base(f.Path)
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
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)
|
2023-12-01 23:47:00 +00:00
|
|
|
// 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
|
2023-12-01 23:47:00 +00:00
|
|
|
}
|
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
|
2023-12-01 23:47:00 +00:00
|
|
|
}
|
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)
|
2023-12-01 23:47:00 +00:00
|
|
|
}
|
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
|
2021-07-26 10:59:09 +00:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-01-05 22:44:33 +00:00
|
|
|
// 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)
|
2019-01-05 22:44:33 +00:00
|
|
|
if err != nil {
|
2024-11-26 17:21:41 +00:00
|
|
|
return subs, err
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
defer reader.Close()
|
|
|
|
|
2023-12-01 23:47:00 +00:00
|
|
|
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
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_, err = io.Copy(h, reader)
|
|
|
|
if err != nil {
|
2024-11-26 17:21:41 +00:00
|
|
|
return subs, err
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
2024-11-26 17:21:41 +00:00
|
|
|
subs[algo] = hex.EncodeToString(h.Sum(nil))
|
|
|
|
return subs, nil
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
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]
|
2023-12-01 23:47:00 +00:00
|
|
|
if i.Type == "" {
|
2024-12-17 00:01:55 +00:00
|
|
|
i.Type = extendedMimeTypeCheck(ext)
|
2021-01-07 10:30:17 +00:00
|
|
|
}
|
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 {
|
2021-01-07 10:30:17 +00:00
|
|
|
|
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]
|
|
|
|
}
|
2021-01-07 10:30:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-26 17:21:41 +00:00
|
|
|
// TODO add subtitles back
|
2023-12-01 23:47:00 +00:00
|
|
|
// 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
|
|
|
|
2023-12-01 23:47:00 +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)
|
|
|
|
})
|
|
|
|
}
|