filebrowser/backend/files/indexingFiles.go

351 lines
9.3 KiB
Go
Raw Normal View History

2024-11-26 17:21:41 +00:00
package files
import (
"fmt"
"os"
"path/filepath"
2025-01-27 00:21:12 +00:00
"runtime"
2025-01-05 19:05:33 +00:00
"slices"
2024-11-26 17:21:41 +00:00
"strings"
"sync"
"time"
2025-01-21 14:02:43 +00:00
"github.com/gtsteffaniak/filebrowser/backend/cache"
"github.com/gtsteffaniak/filebrowser/backend/logger"
2024-12-17 00:01:55 +00:00
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/utils"
2024-11-26 17:21:41 +00:00
)
type Index struct {
2025-01-05 19:05:33 +00:00
settings.Source
2024-11-26 17:21:41 +00:00
Directories map[string]*FileInfo
NumDirs uint64
NumFiles uint64
NumDeleted uint64
FilesChangedDuringIndexing bool
currentSchedule int
assessment string
indexingTime int
LastIndexed time.Time
SmartModifier time.Duration
mu sync.RWMutex
scannerMu sync.Mutex
}
var (
2025-01-05 19:05:33 +00:00
indexes map[string]*Index
2024-11-26 17:21:41 +00:00
indexesMutex sync.RWMutex
2025-01-05 19:05:33 +00:00
RootPaths map[string]string
2024-11-26 17:21:41 +00:00
)
2025-01-05 19:05:33 +00:00
func Initialize(source settings.Source) {
indexesMutex.RLock()
newIndex := Index{
Source: source,
Directories: make(map[string]*FileInfo),
}
if RootPaths == nil {
RootPaths = make(map[string]string)
}
RootPaths[source.Name] = source.Path
indexes = make(map[string]*Index)
indexes[newIndex.Source.Name] = &newIndex
indexesMutex.RUnlock()
if !newIndex.Source.Config.Disabled {
2024-11-26 17:21:41 +00:00
time.Sleep(time.Second)
2025-02-16 14:07:38 +00:00
logger.Info(fmt.Sprintf("initializing index: [%v]", newIndex.Source.Name))
2025-01-05 19:05:33 +00:00
newIndex.RunIndexing("/", false)
go newIndex.setupIndexingScanners()
} else {
2025-02-16 14:07:38 +00:00
logger.Debug("indexing disabled for source: " + newIndex.Source.Name)
2024-11-26 17:21:41 +00:00
}
}
// Define a function to recursively index files and directories
2025-01-05 19:05:33 +00:00
func (idx *Index) indexDirectory(adjustedPath string, quick, recursive bool) error {
realPath := strings.TrimRight(idx.Source.Path, "/") + adjustedPath
2024-11-26 17:21:41 +00:00
// Open the directory
dir, err := os.Open(realPath)
if err != nil {
2025-01-05 19:05:33 +00:00
idx.RemoveDirectory(adjustedPath) // Remove, must have been deleted
2024-11-26 17:21:41 +00:00
return err
}
defer dir.Close()
dirInfo, err := dir.Stat()
if err != nil {
return err
}
combinedPath := adjustedPath + "/"
if adjustedPath == "/" {
combinedPath = "/"
}
// get whats currently in cache
2025-01-05 19:05:33 +00:00
idx.mu.RLock()
2024-11-26 17:21:41 +00:00
cacheDirItems := []ItemInfo{}
modChange := true // default to true
2025-01-05 19:05:33 +00:00
cachedDir, exists := idx.Directories[adjustedPath]
2024-11-26 17:21:41 +00:00
if exists && quick {
modChange = dirInfo.ModTime() != cachedDir.ModTime
cacheDirItems = cachedDir.Folders
}
2025-01-05 19:05:33 +00:00
idx.mu.RUnlock()
2024-11-26 17:21:41 +00:00
// If the directory has not been modified since the last index, skip expensive readdir
// recursively check cached dirs for mod time changes as well
if !modChange && recursive {
for _, item := range cacheDirItems {
2025-01-05 19:05:33 +00:00
err = idx.indexDirectory(combinedPath+item.Name, quick, true)
2024-11-26 17:21:41 +00:00
if err != nil {
2025-01-21 14:02:43 +00:00
logger.Error(fmt.Sprintf("error indexing directory %v : %v", combinedPath+item.Name, err))
2024-11-26 17:21:41 +00:00
}
}
return nil
}
if quick {
2025-01-05 19:05:33 +00:00
idx.mu.Lock()
idx.FilesChangedDuringIndexing = true
idx.mu.Unlock()
2024-11-26 17:21:41 +00:00
}
// Read directory contents
files, err := dir.Readdir(-1)
if err != nil {
return err
}
var totalSize int64
fileInfos := []ItemInfo{}
dirInfos := []ItemInfo{}
// Process each file and directory in the current directory
for _, file := range files {
2025-02-16 14:07:38 +00:00
isHidden := isHidden(file, idx.Source.Path+combinedPath)
2025-01-05 19:05:33 +00:00
isDir := file.IsDir()
fullCombined := combinedPath + file.Name()
2025-01-27 00:21:12 +00:00
if idx.shouldSkip(isDir, isHidden, fullCombined) {
2025-01-05 19:05:33 +00:00
continue
}
2024-11-26 17:21:41 +00:00
itemInfo := &ItemInfo{
Name: file.Name(),
ModTime: file.ModTime(),
2025-01-27 00:21:12 +00:00
Hidden: isHidden,
2024-11-26 17:21:41 +00:00
}
2025-01-05 19:05:33 +00:00
// fix for .app files on macos which are technically directories, but we don't want to treat them as such
if isDir && strings.HasSuffix(file.Name(), ".app") {
isDir = false
}
if isDir {
// skip non-indexable dirs.
if file.Name() == "$RECYCLE.BIN" || file.Name() == "System Volume Information" {
continue
}
2024-11-26 17:21:41 +00:00
dirPath := combinedPath + file.Name()
if recursive {
// Recursively index the subdirectory
2025-01-05 19:05:33 +00:00
err = idx.indexDirectory(dirPath, quick, recursive)
2024-11-26 17:21:41 +00:00
if err != nil {
2025-01-21 14:02:43 +00:00
logger.Error(fmt.Sprintf("Failed to index directory %s: %v", dirPath, err))
2024-11-26 17:21:41 +00:00
continue
}
}
2025-01-05 19:05:33 +00:00
realDirInfo, exists := idx.GetMetadataInfo(dirPath, true)
2024-11-26 17:21:41 +00:00
if exists {
itemInfo.Size = realDirInfo.Size
}
totalSize += itemInfo.Size
itemInfo.Type = "directory"
dirInfos = append(dirInfos, *itemInfo)
2025-01-05 19:05:33 +00:00
idx.NumDirs++
2024-11-26 17:21:41 +00:00
} else {
2025-01-05 19:05:33 +00:00
itemInfo.DetectType(fullCombined, false)
2024-11-26 17:21:41 +00:00
itemInfo.Size = file.Size()
fileInfos = append(fileInfos, *itemInfo)
totalSize += itemInfo.Size
2025-01-05 19:05:33 +00:00
idx.NumFiles++
2024-11-26 17:21:41 +00:00
}
}
2025-01-05 19:05:33 +00:00
if totalSize == 0 && idx.Source.Config.IgnoreZeroSizeFolders {
return nil
}
2024-11-26 17:21:41 +00:00
// Create FileInfo for the current directory
dirFileInfo := &FileInfo{
Path: adjustedPath,
Files: fileInfos,
Folders: dirInfos,
}
dirFileInfo.ItemInfo = ItemInfo{
Name: dirInfo.Name(),
Type: "directory",
Size: totalSize,
ModTime: dirInfo.ModTime(),
}
dirFileInfo.SortItems()
// Update the current directory metadata in the index
2025-01-05 19:05:33 +00:00
idx.UpdateMetadata(dirFileInfo)
2024-11-26 17:21:41 +00:00
return nil
}
2025-01-05 19:05:33 +00:00
func (idx *Index) makeIndexPath(subPath string) string {
2024-11-26 17:21:41 +00:00
if strings.HasPrefix(subPath, "./") {
subPath = strings.TrimPrefix(subPath, ".")
}
2025-01-05 19:05:33 +00:00
if idx.Source.Path == subPath || subPath == "." {
2024-11-26 17:21:41 +00:00
return "/"
}
// clean path
subPath = strings.TrimSuffix(subPath, "/")
// remove index prefix
2025-01-05 19:05:33 +00:00
adjustedPath := strings.TrimPrefix(subPath, idx.Source.Path)
2025-01-13 00:50:22 +00:00
adjustedPath = strings.ReplaceAll(adjustedPath, "\\", "/")
2024-11-26 17:21:41 +00:00
// remove trailing slash
adjustedPath = strings.TrimSuffix(adjustedPath, "/")
if !strings.HasPrefix(adjustedPath, "/") {
adjustedPath = "/" + adjustedPath
}
return adjustedPath
}
2025-01-05 19:05:33 +00:00
func (idx *Index) recursiveUpdateDirSizes(childInfo *FileInfo, previousSize int64) {
2024-11-26 17:21:41 +00:00
parentDir := utils.GetParentDirectoryPath(childInfo.Path)
2025-01-05 19:05:33 +00:00
parentInfo, exists := idx.GetMetadataInfo(parentDir, true)
2024-11-26 17:21:41 +00:00
if !exists || parentDir == "" {
return
}
newSize := parentInfo.Size - previousSize + childInfo.Size
parentInfo.Size += newSize
2025-01-05 19:05:33 +00:00
idx.UpdateMetadata(parentInfo)
idx.recursiveUpdateDirSizes(parentInfo, newSize)
2024-11-26 17:21:41 +00:00
}
2025-01-05 19:05:33 +00:00
func (idx *Index) GetRealPath(relativePath ...string) (string, bool, error) {
combined := append([]string{idx.Source.Path}, relativePath...)
joinedPath := filepath.Join(combined...)
2025-01-21 14:02:43 +00:00
isDir, _ := cache.RealPath.Get(joinedPath + ":isdir").(bool)
cached, ok := cache.RealPath.Get(joinedPath).(string)
2025-01-05 19:05:33 +00:00
if ok && cached != "" {
return cached, isDir, nil
}
// Convert relative path to absolute path
absolutePath, err := filepath.Abs(joinedPath)
if err != nil {
return absolutePath, false, fmt.Errorf("could not get real path: %v, %s", joinedPath, err)
}
// Resolve symlinks and get the real path
realPath, isDir, err := resolveSymlinks(absolutePath)
if err == nil {
2025-01-21 14:02:43 +00:00
cache.RealPath.Set(joinedPath, realPath)
cache.RealPath.Set(joinedPath+":isdir", isDir)
2025-01-05 19:05:33 +00:00
}
return realPath, isDir, err
}
func (idx *Index) RefreshFileInfo(opts FileOptions) error {
2024-11-26 17:21:41 +00:00
refreshOptions := FileOptions{
Path: opts.Path,
IsDir: opts.IsDir,
}
if !refreshOptions.IsDir {
2025-01-05 19:05:33 +00:00
refreshOptions.Path = idx.makeIndexPath(filepath.Dir(refreshOptions.Path))
2024-11-26 17:21:41 +00:00
refreshOptions.IsDir = true
} else {
2025-01-05 19:05:33 +00:00
refreshOptions.Path = idx.makeIndexPath(refreshOptions.Path)
2024-11-26 17:21:41 +00:00
}
2025-01-05 19:05:33 +00:00
err := idx.indexDirectory(refreshOptions.Path, false, false)
2024-11-26 17:21:41 +00:00
if err != nil {
return fmt.Errorf("file/folder does not exist to refresh data: %s", refreshOptions.Path)
}
2025-01-05 19:05:33 +00:00
file, exists := idx.GetMetadataInfo(refreshOptions.Path, true)
2024-11-26 17:21:41 +00:00
if !exists {
return fmt.Errorf("file/folder does not exist in metadata: %s", refreshOptions.Path)
}
2025-01-05 19:05:33 +00:00
current, firstExisted := idx.GetMetadataInfo(refreshOptions.Path, true)
2024-11-26 17:21:41 +00:00
refreshParentInfo := firstExisted && current.Size != file.Size
//utils.PrintStructFields(*file)
2025-01-05 19:05:33 +00:00
result := idx.UpdateMetadata(file)
2024-11-26 17:21:41 +00:00
if !result {
return fmt.Errorf("file/folder does not exist in metadata: %s", refreshOptions.Path)
}
if !exists {
return nil
}
if refreshParentInfo {
2025-01-05 19:05:33 +00:00
idx.recursiveUpdateDirSizes(file, current.Size)
2024-11-26 17:21:41 +00:00
}
return nil
}
2025-01-05 19:05:33 +00:00
2025-01-27 00:21:12 +00:00
func isHidden(file os.FileInfo, srcPath string) bool {
// Check if the file starts with a dot (common on Unix systems)
if file.Name()[0] == '.' {
return true
}
if runtime.GOOS == "windows" {
return checkWindowsHidden(filepath.Join(srcPath, file.Name()))
}
// Default behavior for non-Windows systems
return false
2025-01-05 19:05:33 +00:00
}
func (idx *Index) shouldSkip(isDir bool, isHidden bool, fullCombined string) bool {
// check inclusions first
if isDir && len(idx.Source.Config.Include.Folders) > 0 {
if !slices.Contains(idx.Source.Config.Include.Folders, fullCombined) {
return true
}
}
if !isDir && len(idx.Source.Config.Include.Files) > 0 {
if !slices.Contains(idx.Source.Config.Include.Files, fullCombined) {
return true
}
}
2025-02-08 00:12:11 +00:00
if !isDir && len(idx.Source.Config.Include.FileEndsWith) > 0 {
shouldSkip := true
for _, end := range idx.Source.Config.Include.FileEndsWith {
if strings.HasSuffix(fullCombined, end) {
shouldSkip = false
break
}
}
if shouldSkip {
return true
}
}
2025-01-05 19:05:33 +00:00
// check exclusions
if isDir && slices.Contains(idx.Source.Config.Exclude.Folders, fullCombined) {
return true
}
if !isDir && slices.Contains(idx.Source.Config.Exclude.Files, fullCombined) {
return true
}
if idx.Source.Config.IgnoreHidden && isHidden {
return true
}
2025-02-08 00:12:11 +00:00
if !isDir && len(idx.Source.Config.Exclude.FileEndsWith) > 0 {
shouldSkip := false
for _, end := range idx.Source.Config.Exclude.FileEndsWith {
if strings.HasSuffix(fullCombined, end) {
shouldSkip = true
break
}
}
return shouldSkip
}
2025-01-05 19:05:33 +00:00
return false
}