mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-04 16:50:08 -05:00
Compare commits
5
Commits
main
...
v1.1.3-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
690685459e | ||
|
|
6f6244b2fb | ||
|
|
05fcd31909 | ||
|
|
a4107a734a | ||
|
|
991be79521 |
No files matched your search
@@ -2,6 +2,17 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
|
||||
|
||||
## v1.1.3-beta
|
||||
|
||||
**Notes**:
|
||||
- continue create user dir scope even if filesystem path creation fails #1509
|
||||
- access control cache is cleared more aggresively to ensure no delay
|
||||
- removed default .ico favicon in favor of .svg
|
||||
|
||||
**BugFixes**:
|
||||
- indexing used size increases over time #1685
|
||||
- download progress is not shown #1687
|
||||
|
||||
## v1.1.2-beta
|
||||
|
||||
**Notes**:
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
)
|
||||
|
||||
// MakeUserDir makes the user directory according to settings.
|
||||
@@ -26,7 +27,8 @@ func MakeUserDirs(u *users.User, disableScopeChange bool) error {
|
||||
for i, scope := range u.Scopes {
|
||||
source, ok := settings.Config.Server.SourceMap[scope.Name]
|
||||
if !ok {
|
||||
return fmt.Errorf("MakeUserDirs: source not found: %s", scope.Name)
|
||||
logger.Errorf("MakeUserDirs: source not found: %s", scope.Name)
|
||||
continue
|
||||
}
|
||||
// create directory and append user name
|
||||
if filepath.Base(scope.Scope) != cleanedUserName && source.Config.CreateUserDir && !disableScopeChange {
|
||||
@@ -35,31 +37,36 @@ func MakeUserDirs(u *users.User, disableScopeChange bool) error {
|
||||
// If parent directory doesn't exist and createUserDir is enabled, create it
|
||||
if !Exists(parentDir) {
|
||||
if err := MakeUserDir(parentDir); err != nil {
|
||||
return fmt.Errorf("MakeUserDirs: failed to create parent scope directory: %s - %v", scope.Scope, err)
|
||||
logger.Errorf("MakeUserDirs: failed to create parent scope directory: %s - %v", scope.Scope, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Use JoinPathAsUnix to ensure scope remains in Unix format (forward slashes)
|
||||
scope.Scope = utils.JoinPathAsUnix(scope.Scope, cleanedUserName)
|
||||
err := MakeUserDir(fullPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("MakeUserDirs: failed to create user home dir: %s", err)
|
||||
logger.Errorf("MakeUserDirs: failed to create user home dir: %s", err)
|
||||
// Continue to next scope even if this one failed
|
||||
}
|
||||
} else if filepath.Base(scope.Scope) == cleanedUserName && source.Config.CreateUserDir {
|
||||
// create directory exactly as specified
|
||||
fullPath := filepath.Join(source.Path, scope.Scope)
|
||||
parentDir := filepath.Dir(fullPath)
|
||||
if !Exists(parentDir) {
|
||||
return fmt.Errorf("MakeUserDirs: scope folder does not exist: %s", parentDir)
|
||||
logger.Errorf("MakeUserDirs: scope folder does not exist: %s", parentDir)
|
||||
continue
|
||||
}
|
||||
err := MakeUserDir(fullPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create user: failed to create user home dir: %s", err)
|
||||
logger.Errorf("create user: failed to create user home dir: %s", err)
|
||||
// Continue to next scope even if this one failed
|
||||
}
|
||||
} else {
|
||||
// just assigning scope to path provided, so just check that it exists
|
||||
path := filepath.Join(source.Path, scope.Scope)
|
||||
if !Exists(path) {
|
||||
return fmt.Errorf("MakeUserDirs: scope folder does not exist: %s", path)
|
||||
logger.Errorf("MakeUserDirs: scope folder does not exist: %s", path)
|
||||
continue
|
||||
}
|
||||
}
|
||||
u.Scopes[i] = scope
|
||||
|
||||
@@ -17,9 +17,6 @@ func validateAccessRules() {
|
||||
}
|
||||
// Get all sources
|
||||
for sourcePath := range settings.Config.Server.SourceMap {
|
||||
// Clear cache before migration to ensure we get fresh data
|
||||
store.Access.ClearCacheForSource(sourcePath)
|
||||
|
||||
// Get all rules for this source
|
||||
rules, err := store.Access.GetAllRules(sourcePath)
|
||||
if err != nil {
|
||||
@@ -65,7 +62,6 @@ func validateAccessRules() {
|
||||
|
||||
// After migration, clear cache
|
||||
if migratedCount > 0 {
|
||||
store.Access.ClearCacheForSource(sourcePath)
|
||||
logger.Infof("Migrated %d access rules for source %s", migratedCount, sourcePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,7 +643,7 @@ func loadCustomFavicon() {
|
||||
allowedFormats := []string{".ico", ".png", ".svg"}
|
||||
|
||||
// Set default embedded favicon path
|
||||
Env.FaviconEmbeddedPath = "img/icons/favicon.ico"
|
||||
Env.FaviconEmbeddedPath = "img/icons/favicon.svg"
|
||||
|
||||
// Check if a custom favicon path is configured
|
||||
if Config.Frontend.Favicon == "" {
|
||||
|
||||
@@ -153,18 +153,12 @@ func ClearCache() {
|
||||
rulesCache = cache.NewCache[map[string]FrontendAccessRule](1 * time.Minute)
|
||||
}
|
||||
|
||||
// ClearCacheForSource clears the cache for a specific source
|
||||
func (s *Storage) ClearCacheForSource(sourcePath string) {
|
||||
// Clear all caches related to this source
|
||||
accessCache.Delete(accessChangedKey + sourcePath)
|
||||
rulesCache.Delete(accessChangedKey + sourcePath)
|
||||
versionCache.Delete("version:" + sourcePath)
|
||||
|
||||
// Clear all permission caches for this source
|
||||
// We need to iterate through all permission cache keys and remove those for this source
|
||||
// Since we can't iterate through cache keys, we'll just increment the version
|
||||
// which will invalidate all permission caches for this source
|
||||
s.incrementSourceVersion(sourcePath)
|
||||
// clearAllCaches clears ALL caches. This should be called whenever rules are created, updated, or deleted.
|
||||
func (s *Storage) clearAllCaches() {
|
||||
accessCache.ClearAll()
|
||||
versionCache.ClearAll()
|
||||
permissionCache.ClearAll()
|
||||
rulesCache.ClearAll()
|
||||
}
|
||||
|
||||
// RemoveRuleByPath removes a rule by its exact path from the internal storage
|
||||
@@ -180,18 +174,11 @@ func (s *Storage) RemoveRuleByPath(sourcePath, indexPath string) {
|
||||
// Remove the rule by exact path match (don't normalize)
|
||||
if _, exists := rulesBySource[indexPath]; exists {
|
||||
delete(rulesBySource, indexPath)
|
||||
|
||||
// If no rules left for this source, remove the source entry
|
||||
if len(rulesBySource) == 0 {
|
||||
delete(s.AllRules, sourcePath)
|
||||
}
|
||||
|
||||
// Invalidate caches
|
||||
s.incrementSourceVersion(sourcePath)
|
||||
accessCache.Set(accessChangedKey+sourcePath, "false")
|
||||
rulesCache.Delete(accessChangedKey + sourcePath)
|
||||
|
||||
// Save to database
|
||||
s.clearAllCaches()
|
||||
err := s.SaveToDB()
|
||||
if err != nil {
|
||||
logger.Errorf("error saving access rules to database: %v", err)
|
||||
@@ -233,9 +220,7 @@ func (s *Storage) DenyUser(sourcePath, indexPath, username string) error {
|
||||
return errors.ErrExist
|
||||
}
|
||||
rule.Deny.Users[username] = struct{}{}
|
||||
s.incrementSourceVersion(sourcePath)
|
||||
accessCache.Set(accessChangedKey+sourcePath, "false")
|
||||
rulesCache.Delete(accessChangedKey + sourcePath)
|
||||
s.clearAllCaches()
|
||||
return s.SaveToDB()
|
||||
}
|
||||
|
||||
@@ -254,9 +239,7 @@ func (s *Storage) AllowUser(sourcePath, indexPath, username string) error {
|
||||
return errors.ErrExist
|
||||
}
|
||||
rule.Allow.Users[username] = struct{}{}
|
||||
s.incrementSourceVersion(sourcePath)
|
||||
accessCache.Set(accessChangedKey+sourcePath, "false")
|
||||
rulesCache.Delete(accessChangedKey + sourcePath)
|
||||
s.clearAllCaches()
|
||||
return s.SaveToDB()
|
||||
}
|
||||
|
||||
@@ -273,9 +256,7 @@ func (s *Storage) DenyGroup(sourcePath, indexPath, groupname string) error {
|
||||
return errors.ErrExist
|
||||
}
|
||||
rule.Deny.Groups[groupname] = struct{}{}
|
||||
s.incrementSourceVersion(sourcePath)
|
||||
accessCache.Set(accessChangedKey+sourcePath, "false")
|
||||
rulesCache.Delete(accessChangedKey + sourcePath)
|
||||
s.clearAllCaches()
|
||||
return s.SaveToDB()
|
||||
}
|
||||
|
||||
@@ -292,9 +273,7 @@ func (s *Storage) AllowGroup(sourcePath, indexPath, groupname string) error {
|
||||
return errors.ErrExist
|
||||
}
|
||||
rule.Allow.Groups[groupname] = struct{}{}
|
||||
s.incrementSourceVersion(sourcePath)
|
||||
accessCache.Set(accessChangedKey+sourcePath, "false")
|
||||
rulesCache.Delete(accessChangedKey + sourcePath)
|
||||
s.clearAllCaches()
|
||||
return s.SaveToDB()
|
||||
}
|
||||
|
||||
@@ -307,9 +286,7 @@ func (s *Storage) DenyAll(sourcePath, indexPath string) error {
|
||||
return errors.ErrExist
|
||||
}
|
||||
rule.DenyAll = true
|
||||
s.incrementSourceVersion(sourcePath)
|
||||
accessCache.Set(accessChangedKey+sourcePath, "false")
|
||||
rulesCache.Delete(accessChangedKey + sourcePath)
|
||||
s.clearAllCaches()
|
||||
return s.SaveToDB()
|
||||
}
|
||||
|
||||
@@ -659,11 +636,7 @@ func (s *Storage) RemoveAllowUser(sourcePath, indexPath, username string) (bool,
|
||||
if exists {
|
||||
delete(rule.Allow.Users, username)
|
||||
}
|
||||
removed := false
|
||||
if exists {
|
||||
s.incrementSourceVersion(sourcePath)
|
||||
removed = true
|
||||
}
|
||||
removed := exists
|
||||
// If rule is now empty, remove it
|
||||
if len(rule.Allow.Users) == 0 && len(rule.Allow.Groups) == 0 && len(rule.Deny.Users) == 0 && len(rule.Deny.Groups) == 0 {
|
||||
delete(s.AllRules[sourcePath], normalizedPath)
|
||||
@@ -672,8 +645,7 @@ func (s *Storage) RemoveAllowUser(sourcePath, indexPath, username string) (bool,
|
||||
}
|
||||
}
|
||||
if removed {
|
||||
accessCache.Set(accessChangedKey+sourcePath, "false")
|
||||
rulesCache.Delete(accessChangedKey + sourcePath)
|
||||
s.clearAllCaches()
|
||||
return exists, s.SaveToDB()
|
||||
}
|
||||
return false, nil
|
||||
@@ -692,11 +664,7 @@ func (s *Storage) RemoveAllowGroup(sourcePath, indexPath, groupname string) (boo
|
||||
if exists {
|
||||
delete(rule.Allow.Groups, groupname)
|
||||
}
|
||||
removed := false
|
||||
if exists {
|
||||
s.incrementSourceVersion(sourcePath)
|
||||
removed = true
|
||||
}
|
||||
removed := exists
|
||||
// If rule is now empty, remove it
|
||||
if len(rule.Allow.Users) == 0 && len(rule.Allow.Groups) == 0 && len(rule.Deny.Users) == 0 && len(rule.Deny.Groups) == 0 {
|
||||
delete(s.AllRules[sourcePath], normalizedPath)
|
||||
@@ -705,8 +673,7 @@ func (s *Storage) RemoveAllowGroup(sourcePath, indexPath, groupname string) (boo
|
||||
}
|
||||
}
|
||||
if removed {
|
||||
accessCache.Set(accessChangedKey+sourcePath, "false")
|
||||
rulesCache.Delete(accessChangedKey + sourcePath)
|
||||
s.clearAllCaches()
|
||||
return exists, s.SaveToDB()
|
||||
}
|
||||
return exists, nil
|
||||
@@ -725,11 +692,7 @@ func (s *Storage) RemoveDenyUser(sourcePath, indexPath, username string) (bool,
|
||||
if exists {
|
||||
delete(rule.Deny.Users, username)
|
||||
}
|
||||
removed := false
|
||||
if exists {
|
||||
s.incrementSourceVersion(sourcePath)
|
||||
removed = true
|
||||
}
|
||||
removed := exists
|
||||
// If rule is now empty, remove it
|
||||
if len(rule.Allow.Users) == 0 && len(rule.Allow.Groups) == 0 && len(rule.Deny.Users) == 0 && len(rule.Deny.Groups) == 0 {
|
||||
delete(s.AllRules[sourcePath], normalizedPath)
|
||||
@@ -738,8 +701,7 @@ func (s *Storage) RemoveDenyUser(sourcePath, indexPath, username string) (bool,
|
||||
}
|
||||
}
|
||||
if removed {
|
||||
accessCache.Set(accessChangedKey+sourcePath, "false")
|
||||
rulesCache.Delete(accessChangedKey + sourcePath)
|
||||
s.clearAllCaches()
|
||||
return exists, s.SaveToDB()
|
||||
}
|
||||
return false, nil
|
||||
@@ -758,11 +720,7 @@ func (s *Storage) RemoveDenyGroup(sourcePath, indexPath, groupname string) (bool
|
||||
if exists {
|
||||
delete(rule.Deny.Groups, groupname)
|
||||
}
|
||||
removed := false
|
||||
if exists {
|
||||
s.incrementSourceVersion(sourcePath)
|
||||
removed = true
|
||||
}
|
||||
removed := exists
|
||||
// If rule is now empty, remove it
|
||||
if len(rule.Allow.Users) == 0 && len(rule.Allow.Groups) == 0 && len(rule.Deny.Users) == 0 && len(rule.Deny.Groups) == 0 {
|
||||
delete(s.AllRules[sourcePath], normalizedPath)
|
||||
@@ -771,8 +729,7 @@ func (s *Storage) RemoveDenyGroup(sourcePath, indexPath, groupname string) (bool
|
||||
}
|
||||
}
|
||||
if removed {
|
||||
accessCache.Set(accessChangedKey+sourcePath, "false")
|
||||
rulesCache.Delete(accessChangedKey + sourcePath)
|
||||
s.clearAllCaches()
|
||||
return exists, s.SaveToDB()
|
||||
}
|
||||
return exists, nil
|
||||
@@ -790,7 +747,6 @@ func (s *Storage) RemoveDenyAll(sourcePath, indexPath string) (bool, error) {
|
||||
removed := false
|
||||
if rule.DenyAll {
|
||||
rule.DenyAll = false
|
||||
s.incrementSourceVersion(sourcePath)
|
||||
removed = true
|
||||
}
|
||||
// If rule is now empty, remove it
|
||||
@@ -801,8 +757,7 @@ func (s *Storage) RemoveDenyAll(sourcePath, indexPath string) (bool, error) {
|
||||
}
|
||||
}
|
||||
if removed {
|
||||
accessCache.Set(accessChangedKey+sourcePath, "false")
|
||||
rulesCache.Delete(accessChangedKey + sourcePath)
|
||||
s.clearAllCaches()
|
||||
return true, s.SaveToDB()
|
||||
}
|
||||
return false, nil
|
||||
@@ -834,12 +789,8 @@ func (s *Storage) RemoveAllRulesForUser(username string) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
for sp := range changedSourcePaths {
|
||||
s.incrementSourceVersion(sp)
|
||||
accessCache.Set(accessChangedKey+sp, "false")
|
||||
rulesCache.Delete(accessChangedKey + sp)
|
||||
}
|
||||
if changed {
|
||||
s.clearAllCaches()
|
||||
return s.SaveToDB()
|
||||
}
|
||||
return nil
|
||||
@@ -871,12 +822,8 @@ func (s *Storage) RemoveAllRulesForGroup(groupname string) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
for sp := range changedSourcePaths {
|
||||
s.incrementSourceVersion(sp)
|
||||
accessCache.Set(accessChangedKey+sp, "false")
|
||||
rulesCache.Delete(accessChangedKey + sp)
|
||||
}
|
||||
if changed {
|
||||
s.clearAllCaches()
|
||||
return s.SaveToDB()
|
||||
}
|
||||
return nil
|
||||
@@ -1084,16 +1031,6 @@ func (s *Storage) GetAllRulesByGroups(sourcePath string) map[string]map[string]F
|
||||
return allGroupRules
|
||||
}
|
||||
|
||||
// incrementSourceVersion increments the version of a sourcePath to invalidate caches.
|
||||
func (s *Storage) incrementSourceVersion(sourcePath string) {
|
||||
key := "version:" + sourcePath
|
||||
version := 0
|
||||
if v, ok := versionCache.Get(key); ok {
|
||||
version = v
|
||||
}
|
||||
versionCache.Set(key, version+1)
|
||||
}
|
||||
|
||||
// HasAnyVisibleItems checks if a user has access to any items in a given parent path.
|
||||
// This is used to determine if a user should see a folder's contents even when
|
||||
// they don't have direct access to the parent folder.
|
||||
@@ -1164,9 +1101,7 @@ func (s *Storage) RemoveUserCascade(sourcePath, indexPath, username string, allo
|
||||
}
|
||||
|
||||
if changed {
|
||||
s.incrementSourceVersion(sourcePath)
|
||||
accessCache.Set(accessChangedKey+sourcePath, "false")
|
||||
rulesCache.Delete(accessChangedKey + sourcePath)
|
||||
s.clearAllCaches()
|
||||
return removedCount, s.SaveToDB()
|
||||
}
|
||||
|
||||
@@ -1223,9 +1158,7 @@ func (s *Storage) RemoveGroupCascade(sourcePath, indexPath, groupname string, al
|
||||
}
|
||||
|
||||
if changed {
|
||||
s.incrementSourceVersion(sourcePath)
|
||||
accessCache.Set(accessChangedKey+sourcePath, "false")
|
||||
rulesCache.Delete(accessChangedKey + sourcePath)
|
||||
s.clearAllCaches()
|
||||
return removedCount, s.SaveToDB()
|
||||
}
|
||||
|
||||
@@ -1272,11 +1205,7 @@ func (s *Storage) UpdateRules(sourcePath, oldPath, newPath string) (int, error)
|
||||
}
|
||||
|
||||
if updated > 0 {
|
||||
// Invalidate caches
|
||||
s.incrementSourceVersion(sourcePath)
|
||||
accessCache.Set(accessChangedKey+sourcePath, "false")
|
||||
rulesCache.Delete(accessChangedKey + sourcePath)
|
||||
|
||||
s.clearAllCaches()
|
||||
if err := s.SaveToDB(); err != nil {
|
||||
return updated, err
|
||||
}
|
||||
@@ -1307,12 +1236,7 @@ func (s *Storage) UpdateRulePath(sourcePath, oldPath, newPath string) error {
|
||||
// Remove the old rule and add it with the new path
|
||||
delete(rulesBySource, oldPath)
|
||||
rulesBySource[newPath] = rule
|
||||
|
||||
// Invalidate caches
|
||||
s.incrementSourceVersion(sourcePath)
|
||||
accessCache.Set(accessChangedKey+sourcePath, "false")
|
||||
rulesCache.Delete(accessChangedKey + sourcePath)
|
||||
|
||||
s.clearAllCaches()
|
||||
logger.Debugf("access rule path updated: source=%s, fromPath=%s, toPath=%s", sourcePath, oldPath, newPath)
|
||||
return s.SaveToDB()
|
||||
}
|
||||
@@ -117,7 +117,7 @@ func (st usersBackend) Update(user *users.User, actorIsAdmin bool, fields ...str
|
||||
user.Scopes = adjustedScopes
|
||||
err = files.MakeUserDirs(user, true)
|
||||
if err != nil {
|
||||
return err
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
// converting scopes to map of paths intead of names (names can change)
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ require (
|
||||
github.com/goccy/go-yaml v1.18.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/gtsteffaniak/go-cache v1.0.0
|
||||
github.com/gtsteffaniak/go-cache v1.1.0
|
||||
github.com/gtsteffaniak/go-logger v0.2.3
|
||||
github.com/kovidgoyal/imaging v1.7.2
|
||||
github.com/pquerna/otp v1.5.0
|
||||
|
||||
+2
-2
@@ -490,8 +490,8 @@ github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduw
|
||||
github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=
|
||||
github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8=
|
||||
github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs=
|
||||
github.com/gtsteffaniak/go-cache v1.0.0 h1:CuZw6eI/be6Oi3oR94Jb5LhcZShMCs6+1wvS7WGN8oQ=
|
||||
github.com/gtsteffaniak/go-cache v1.0.0/go.mod h1:rHUZvf4huhMX/bnNK4/oQ3sNAE2fYC2jFpAWZiBPWac=
|
||||
github.com/gtsteffaniak/go-cache v1.1.0 h1:ni/2cJpCxpGmaMZd92iTDIGOy+yUcMcxQuW7bSKhkCE=
|
||||
github.com/gtsteffaniak/go-cache v1.1.0/go.mod h1:rHUZvf4huhMX/bnNK4/oQ3sNAE2fYC2jFpAWZiBPWac=
|
||||
github.com/gtsteffaniak/go-logger v0.2.3 h1:8nSC5xSMnMYQI61tsBS1tARpnkv/rOh5lzEXtAG72Nw=
|
||||
github.com/gtsteffaniak/go-logger v0.2.3/go.mod h1:rapapTju4D7bwt6h4JOdZNUXDjnp0WkTBd6TN5yWglI=
|
||||
github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo=
|
||||
|
||||
@@ -189,7 +189,7 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
|
||||
publicRoutes.Handle("GET /static/", http.HandlerFunc(staticAssetHandler))
|
||||
|
||||
// Standard browser favicon and manifest routes
|
||||
router.HandleFunc("GET /favicon.ico", http.HandlerFunc(staticAssetHandler))
|
||||
router.HandleFunc("GET /favicon.svg", http.HandlerFunc(staticAssetHandler))
|
||||
router.HandleFunc("GET /site.webmanifest", http.HandlerFunc(staticAssetHandler))
|
||||
router.HandleFunc("GET /manifest.json", http.HandlerFunc(staticAssetHandler))
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ func staticAssetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Handle special routes that need path mapping
|
||||
var assetPath string
|
||||
switch path {
|
||||
case "favicon.ico", "favicon":
|
||||
case "favicon.svg", "favicon":
|
||||
// Handle custom favicon from filesystem
|
||||
if settings.Env.FaviconIsCustom {
|
||||
http.ServeFile(w, r, settings.Env.FaviconPath)
|
||||
|
||||
@@ -236,9 +236,8 @@ func (idx *Index) GetFsDirInfo(adjustedPath string) (*iteminfo.FileInfo, error)
|
||||
if !dirInfo.IsDir() {
|
||||
// Use handleFile for consistent size calculation across platforms
|
||||
// API calls (GetFsDirInfo) should not update totalSize, so pass false for isRoutineScan
|
||||
realSize, _ := idx.handleFile(dirInfo, adjustedPath, realPath, false)
|
||||
realSize, _ := idx.handleFile(dirInfo, adjustedPath, realPath)
|
||||
size := int64(realSize)
|
||||
|
||||
fileInfo := iteminfo.FileInfo{
|
||||
Path: adjustedPath,
|
||||
ItemInfo: iteminfo.ItemInfo{
|
||||
@@ -365,7 +364,7 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
|
||||
}
|
||||
} else {
|
||||
realFilePath := realPath + "/" + file.Name()
|
||||
size, shouldCountSize := idx.handleFile(file, fullCombined, realFilePath, config.IsRoutineScan)
|
||||
size, shouldCountSize := idx.handleFile(file, fullCombined, realFilePath)
|
||||
itemInfo.DetectType(realFilePath, false)
|
||||
usedCachedPreview := false
|
||||
if !idx.Config.DisableIndexing && config.Recursive {
|
||||
|
||||
@@ -22,9 +22,14 @@ type Scanner struct {
|
||||
lastScanned time.Time
|
||||
quickScanTime int
|
||||
fullScanTime int
|
||||
numDirs uint64 // Local count for this path
|
||||
numFiles uint64 // Local count for this path
|
||||
scannerSize uint64 // Size contributed by this scanner (for delta calculation)
|
||||
|
||||
// size tracking
|
||||
numDirs uint64 // Local count for this path
|
||||
numFiles uint64 // Local count for this path
|
||||
size uint64 // Size contributed by this scanner (for delta calculation)
|
||||
previousNumDirs uint64 // Previous numDirs value (preserved across scans)
|
||||
previousNumFiles uint64 // Previous numFiles value (preserved across scans)
|
||||
previousSize uint64 // Previous size value (preserved across scans)
|
||||
|
||||
// Reference back to parent index
|
||||
idx *Index
|
||||
@@ -121,31 +126,17 @@ func (s *Scanner) runRootScan(quick bool) {
|
||||
IsRoutineScan: true,
|
||||
}
|
||||
|
||||
// Reset counters for full scan
|
||||
// Store previous values before scanning (preserved across scans)
|
||||
prevNumDirs := s.previousNumDirs
|
||||
prevNumFiles := s.previousNumFiles
|
||||
prevSize := s.previousSize
|
||||
|
||||
// Reset counters for full scan (they will be incremented during indexing)
|
||||
if !quick {
|
||||
s.numDirs = 0
|
||||
s.numFiles = 0
|
||||
}
|
||||
|
||||
// Track size before scan for delta calculation
|
||||
previousScannerSize := s.scannerSize
|
||||
|
||||
s.idx.mu.Lock()
|
||||
if previousScannerSize > 0 {
|
||||
if s.idx.totalSize >= previousScannerSize {
|
||||
s.idx.totalSize -= previousScannerSize
|
||||
} else {
|
||||
// Safety check: if totalSize is less than previousScannerSize, something is wrong
|
||||
// Reset to 0 to avoid underflow
|
||||
logger.Warningf("[%s] Scanner [%s] WARNING: totalSize (%d) < previousScannerSize (%d), resetting totalSize to 0", s.idx.Name, s.scanPath, s.idx.totalSize, previousScannerSize)
|
||||
s.idx.totalSize = 0
|
||||
}
|
||||
}
|
||||
indexSizeBefore := s.idx.totalSize
|
||||
s.idx.mu.Unlock()
|
||||
|
||||
logger.Debugf("[%s] Scanner [%s] START: indexSizeBefore=%d, previousScannerSize=%d (quick=%v)", s.idx.Name, s.scanPath, indexSizeBefore, previousScannerSize, quick)
|
||||
|
||||
s.filesChanged = false
|
||||
startTime := time.Now()
|
||||
|
||||
@@ -154,12 +145,30 @@ func (s *Scanner) runRootScan(quick bool) {
|
||||
logger.Errorf("Root scanner error: %v", err)
|
||||
}
|
||||
|
||||
// Calculate delta for this scanner
|
||||
// Root scanner gets values directly from index metadata - simple!
|
||||
s.idx.mu.RLock()
|
||||
indexSizeAfter := s.idx.totalSize
|
||||
rootDirInfo, exists := s.idx.Directories["/"]
|
||||
s.idx.mu.RUnlock()
|
||||
newScannerSize := indexSizeAfter - indexSizeBefore
|
||||
s.scannerSize = newScannerSize
|
||||
|
||||
newNumDirs := prevNumDirs
|
||||
newNumFiles := prevNumFiles
|
||||
newsize := prevSize
|
||||
if exists && rootDirInfo != nil {
|
||||
for _, file := range rootDirInfo.Files {
|
||||
newsize += uint64(file.Size)
|
||||
newNumFiles++
|
||||
}
|
||||
for range rootDirInfo.Folders {
|
||||
newNumDirs++
|
||||
}
|
||||
}
|
||||
// Update scanner with new values
|
||||
s.size = newsize
|
||||
|
||||
// Update previous values for next scan (preserve history - don't reset on new scans)
|
||||
s.previousNumDirs = newNumDirs
|
||||
s.previousNumFiles = newNumFiles
|
||||
s.previousSize = newsize
|
||||
scanDuration := int(time.Since(startTime).Seconds())
|
||||
if quick {
|
||||
s.quickScanTime = scanDuration
|
||||
@@ -178,26 +187,46 @@ func (s *Scanner) runChildScan(quick bool) {
|
||||
IsRoutineScan: true,
|
||||
}
|
||||
|
||||
// Reset counters for full scan
|
||||
// Store previous values before scanning (preserved across scans)
|
||||
prevSize := s.previousSize
|
||||
|
||||
// Reset counters for full scan (they will be incremented during indexing)
|
||||
if !quick {
|
||||
s.numDirs = 0
|
||||
s.numFiles = 0
|
||||
}
|
||||
s.idx.mu.RLock()
|
||||
indexSizeBefore := s.idx.totalSize
|
||||
s.idx.mu.RUnlock()
|
||||
|
||||
s.filesChanged = false
|
||||
startTime := time.Now()
|
||||
|
||||
err := s.idx.indexDirectory(s.scanPath, config)
|
||||
if err != nil {
|
||||
logger.Errorf("Scanner [%s] error: %v", s.scanPath, err)
|
||||
}
|
||||
|
||||
// Calculate new values after scan
|
||||
newNumDirs := s.numDirs
|
||||
newNumFiles := s.numFiles
|
||||
|
||||
// For child scanners, calculate size from the directory info
|
||||
// Since child scanners don't modify totalSize, we get it from the directory metadata
|
||||
s.idx.mu.RLock()
|
||||
indexSizeAfter := s.idx.totalSize
|
||||
dirInfo, exists := s.idx.Directories[s.scanPath]
|
||||
s.idx.mu.RUnlock()
|
||||
newScannerSize := indexSizeAfter - indexSizeBefore
|
||||
s.scannerSize = newScannerSize
|
||||
|
||||
newsize := prevSize
|
||||
if exists && dirInfo != nil {
|
||||
newsize = uint64(dirInfo.Size)
|
||||
}
|
||||
|
||||
// Update scanner with new values
|
||||
s.size = newsize
|
||||
|
||||
// Update previous values for next scan (preserve history - don't reset on new scans)
|
||||
s.previousNumDirs = newNumDirs
|
||||
s.previousNumFiles = newNumFiles
|
||||
s.previousSize = newsize
|
||||
|
||||
scanDuration := int(time.Since(startTime).Seconds())
|
||||
if quick {
|
||||
s.quickScanTime = scanDuration
|
||||
|
||||
@@ -178,7 +178,7 @@ func (idx *Index) SendSourceUpdateEvent() error {
|
||||
logger.Debug("Skipping source update event for mock index.")
|
||||
return nil
|
||||
}
|
||||
reducedIndex, err := GetIndexInfo(idx.Name, false)
|
||||
reducedIndex, err := GetIndexInfo(idx.Name, true)
|
||||
if err != nil {
|
||||
logger.Errorf("[%s] Error getting index info: %v", idx.Name, err)
|
||||
return err
|
||||
@@ -284,6 +284,7 @@ func (idx *Index) aggregateStatsFromScanners() {
|
||||
var totalFiles uint64 = 0
|
||||
var totalQuickScanTime = 0
|
||||
var totalFullScanTime = 0
|
||||
var totalSizeFromScanners uint64 = 0 // Sum of all scanner sizes
|
||||
var mostRecentScan time.Time
|
||||
allScannedAtLeastOnce := true
|
||||
|
||||
@@ -292,6 +293,7 @@ func (idx *Index) aggregateStatsFromScanners() {
|
||||
totalFiles += scanner.numFiles
|
||||
totalQuickScanTime += scanner.quickScanTime
|
||||
totalFullScanTime += scanner.fullScanTime
|
||||
totalSizeFromScanners += scanner.size
|
||||
if scanner.lastScanned.After(mostRecentScan) {
|
||||
mostRecentScan = scanner.lastScanned
|
||||
}
|
||||
@@ -299,6 +301,10 @@ func (idx *Index) aggregateStatsFromScanners() {
|
||||
allScannedAtLeastOnce = false
|
||||
}
|
||||
}
|
||||
|
||||
// Update totalSize by summing all scanner sizes (root scanner handles this)
|
||||
// This ensures totalSize is always the sum of all scanner contributions
|
||||
idx.totalSize = totalSizeFromScanners
|
||||
idx.NumDirs = totalDirs
|
||||
idx.NumFiles = totalFiles
|
||||
idx.QuickScanTime = totalQuickScanTime
|
||||
|
||||
@@ -62,8 +62,7 @@ func getFileDetails(sys any, filePath string) (uint64, uint64, uint64, bool) {
|
||||
|
||||
// handleFile processes a file and returns its size and whether it should be counted
|
||||
// On Unix, always uses syscall to get allocated size (du-like behavior)
|
||||
// isRoutineScan: if true, updates the global totalSize; if false (API calls), only returns size
|
||||
func (idx *Index) handleFile(file os.FileInfo, fullCombined string, realFilePath string, isRoutineScan bool) (size uint64, shouldCountSize bool) {
|
||||
func (idx *Index) handleFile(file os.FileInfo, fullCombined string, realFilePath string) (size uint64, shouldCountSize bool) {
|
||||
var realSize uint64
|
||||
var nlink uint64
|
||||
var ino uint64
|
||||
@@ -88,21 +87,8 @@ func (idx *Index) handleFile(file os.FileInfo, fullCombined string, realFilePath
|
||||
// First time seeing this inode.
|
||||
idx.processedInodes[ino] = struct{}{}
|
||||
idx.FoundHardLinks[fullCombined] = realSize
|
||||
// Only update totalSize during routine scans (not API calls)
|
||||
if isRoutineScan {
|
||||
idx.totalSize += realSize
|
||||
}
|
||||
return realSize, true // Count size for directory total.
|
||||
}
|
||||
|
||||
// It's a regular file.
|
||||
// Only update totalSize during routine scans (not API calls)
|
||||
if isRoutineScan {
|
||||
idx.mu.Lock()
|
||||
idx.totalSize += realSize
|
||||
idx.mu.Unlock()
|
||||
}
|
||||
return realSize, true // Count size.
|
||||
return realSize, true // Count size for directory total.
|
||||
}
|
||||
|
||||
// platform specific rules
|
||||
|
||||
@@ -47,28 +47,11 @@ func getPartitionSize(path string) (uint64, error) {
|
||||
return totalBytes, nil
|
||||
}
|
||||
|
||||
func getFileDetails(sys any, filePath string) (uint64, uint64, uint64, bool) {
|
||||
// On Windows, os.FileInfo.Sys() returns *syscall.Win32FileAttributeData,
|
||||
// which does not contain inode or link count information.
|
||||
// We return false to indicate that we should use the fallback mechanism.
|
||||
// filePath parameter is ignored on Windows
|
||||
return 0, 1, 0, false
|
||||
}
|
||||
|
||||
// handleFile processes a file and returns its size and whether it should be counted
|
||||
// On Windows, uses file.Size() directly (no syscall support for allocated size)
|
||||
// isRoutineScan: if true, updates the global totalSize; if false (API calls), only returns size
|
||||
func (idx *Index) handleFile(file os.FileInfo, fullCombined string, realFilePath string, isRoutineScan bool) (size uint64, shouldCountSize bool) {
|
||||
func (idx *Index) handleFile(file os.FileInfo, fullCombined string, realFilePath string) (size uint64, shouldCountSize bool) {
|
||||
// On Windows, just use the actual file size
|
||||
realSize := uint64(file.Size())
|
||||
|
||||
// Windows doesn't support hard links in the same way, so always count size
|
||||
// Only update totalSize during routine scans (not API calls)
|
||||
if isRoutineScan {
|
||||
idx.mu.Lock()
|
||||
idx.totalSize += realSize
|
||||
idx.mu.Unlock()
|
||||
}
|
||||
return realSize, true
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.3 KiB |
+14
-49
@@ -98,61 +98,26 @@ export async function download(format, files, shareHash = "") {
|
||||
files: fileargs,
|
||||
algo: format,
|
||||
hash: shareHash,
|
||||
...(state.share.token && { token: state.share.token })
|
||||
...(state.share.token && { token: state.share.token }),
|
||||
sessionId: state.sessionId
|
||||
})
|
||||
const url = window.origin + apiPath
|
||||
|
||||
|
||||
// Use fetch to capture errors in the console
|
||||
const response = await fetch(url, {
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
"sessionId": state.sessionId,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
const error = new Error(`Download failed: ${response.status} ${response.statusText}`)
|
||||
console.error('Download API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: errorText,
|
||||
url: url
|
||||
})
|
||||
throw error
|
||||
}
|
||||
|
||||
// Get the blob from the response
|
||||
const blob = await response.blob()
|
||||
// Determine filename from Content-Disposition header or use default
|
||||
let filename = 'download'
|
||||
const contentDisposition = response.headers.get('Content-Disposition')
|
||||
if (contentDisposition) {
|
||||
const utf8 = contentDisposition.split("filename*=utf-8''")[1]
|
||||
if (utf8) {
|
||||
filename = decodeURIComponent(utf8.split(';')[0].trim())
|
||||
} else {
|
||||
const ascii = contentDisposition.split('filename="')[1]
|
||||
if (ascii) {
|
||||
filename = ascii.split('"')[0]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filename = `download.${format}`
|
||||
}
|
||||
|
||||
// Create a blob URL and trigger download
|
||||
const blobUrl = window.URL.createObjectURL(blob)
|
||||
// Create a direct link and trigger the download
|
||||
// This allows the browser to handle the download natively with:
|
||||
// - Native download progress indicator
|
||||
// - Shows up in browser's download menu
|
||||
// - Doesn't load entire file into memory first
|
||||
const link = document.createElement('a')
|
||||
link.href = blobUrl
|
||||
link.setAttribute('download', filename)
|
||||
link.href = url
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
// Clean up the blob URL
|
||||
window.URL.revokeObjectURL(blobUrl)
|
||||
|
||||
|
||||
// Clean up after a short delay
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(link)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
export function post(
|
||||
|
||||
@@ -23,10 +23,10 @@
|
||||
<button
|
||||
class="button button--flat"
|
||||
@click="submit"
|
||||
:aria-label="$t('general.submit')"
|
||||
:title="$t('general.submit')"
|
||||
:aria-label="$t('general.login')"
|
||||
:title="$t('general.login')"
|
||||
>
|
||||
{{ $t("general.submit") }}
|
||||
{{ $t("general.login") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
<p>{{ $t("prompts.renameMessage") }}</p>
|
||||
|
||||
<div v-if="item.type !== 'directory'" class="filename-inputs">
|
||||
<input class="input filename-input" :class="{ 'form-invalid': !validation.valid }" v-focus type="text" @keyup.enter="submit"
|
||||
<input aria-label="New Name" class="input filename-input" :class="{ 'form-invalid': !validation.valid }" v-focus type="text" @keyup.enter="submit"
|
||||
v-model.trim="fileName" @input="updateFullName" />
|
||||
<span class="extension-separator">.</span> <!--eslint-disable-line @intlify/vue-i18n/no-raw-text-->
|
||||
<input class="input extension-input" type="text" @keyup.enter="submit" v-model.trim="fileExtension"
|
||||
@input="updateFullName" />
|
||||
</div>
|
||||
|
||||
<input v-else class="input" :class="{ 'form-invalid': !validation.valid }" v-focus type="text" @keyup.enter="submit"
|
||||
<input v-else class="input" aria-label="New Name" :class="{ 'form-invalid': !validation.valid }" v-focus type="text" @keyup.enter="submit"
|
||||
v-model.trim="name" />
|
||||
<p v-if="!validation.valid && name.length > 0" class="validation-error">
|
||||
<span v-if="validation.reason === 'conflict'">
|
||||
@@ -32,7 +32,7 @@
|
||||
{{ $t("general.cancel") }}
|
||||
</button>
|
||||
<button @click="submit" class="button button--flat" :disabled="!canRename"
|
||||
type="submit" :aria-label="$t('general.rename')" :title="$t('general.rename')">
|
||||
type="submit" :aria-label="$t('general.submit')" :title="$t('general.submit')">
|
||||
{{ $t("general.rename") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
"scope": "Scope{suffix}",
|
||||
"rules": "Rules{suffix}",
|
||||
"downloads": "Downloads{suffix}",
|
||||
"submit": "Login{suffix}",
|
||||
"submit": "Submit{suffix}",
|
||||
"navigation": "Navigation{suffix}",
|
||||
"metadata": "Metadata{suffix}",
|
||||
"loading": "Loading{suffix}",
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
<div v-if="globalVars.recaptcha" id="globalVars.recaptcha"></div>
|
||||
<input class="button button--block" type="submit"
|
||||
:value="createMode ? $t('general.signup') : $t('general.submit')" />
|
||||
:value="createMode ? $t('general.signup') : $t('general.login')" />
|
||||
<p @click="toggleMode" v-if="signup" aria-label="sign up toggle">
|
||||
{{ createMode ? $t("login.loginInstead") : $t("login.createAnAccount") }}
|
||||
</p>
|
||||
|
||||
Whitespace-only changes.
@@ -18,7 +18,7 @@ test("info from search", async({ page, checkForErrors, context }) => {
|
||||
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||
await page.locator('#search').click()
|
||||
await page.locator('#main-input').fill('file.tar.gz');
|
||||
await expect(page.locator('#result-list')).toHaveCount(1);
|
||||
await expect(page.locator('#result-list ul li.search-entry')).toHaveCount(1);
|
||||
await page.locator('li[aria-label="file.tar.gz"]').click({ button: "right" });
|
||||
await page.locator('.selected-count-header').waitFor({ state: 'visible' });
|
||||
await expect(page.locator('.selected-count-header')).toHaveText('1');
|
||||
@@ -32,7 +32,7 @@ test("open from search", async({ page, checkForErrors, context }) => {
|
||||
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||
await page.locator('#search').click()
|
||||
await page.locator('#main-input').fill('file.tar.gz');
|
||||
await expect(page.locator('#result-list')).toHaveCount(1);
|
||||
await expect(page.locator('#result-list ul li.search-entry')).toHaveCount(1);
|
||||
await page.locator('li[aria-label="file.tar.gz"]').click();
|
||||
await expect(page).toHaveTitle("Graham's Filebrowser - Files - file.tar.gz");
|
||||
await expect(page.locator('#previewer')).toContainText('Preview is not available for this file.');
|
||||
@@ -99,8 +99,12 @@ test("delete file", async({ page, checkForErrors, context }) => {
|
||||
await expect(page.locator('div[aria-label="delete-path"]')).toHaveText('/deleteme.txt');
|
||||
await page.locator('button[aria-label="Confirm-Delete"]').click();
|
||||
await checkForNotification(page, "Deleted successfully!");
|
||||
checkForErrors();
|
||||
|
||||
// verify its no longer in index via search
|
||||
await page.locator('#search').click()
|
||||
await page.locator('#main-input').fill('deleteme.txt');
|
||||
await expect(page.locator('#result-list ul li.search-entry')).toHaveCount(0);
|
||||
checkForErrors();
|
||||
})
|
||||
|
||||
test("delete nested file prompt", async({ page, checkForErrors, context }) => {
|
||||
@@ -115,3 +119,23 @@ test("delete nested file prompt", async({ page, checkForErrors, context }) => {
|
||||
await expect(page.locator('div[aria-label="delete-path"]')).toHaveText('/folder#hash/file#.sh');
|
||||
checkForErrors();
|
||||
})
|
||||
|
||||
test("rename file", async({ page, checkForErrors, context }) => {
|
||||
await page.goto("/files/");
|
||||
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||
await page.locator('a[aria-label="renameme.txt"]').waitFor({ state: 'visible' });
|
||||
await page.locator('a[aria-label="renameme.txt"]').click({ button: "right" });
|
||||
await page.locator('.selected-count-header').waitFor({ state: 'visible' });
|
||||
await expect(page.locator('.selected-count-header')).toHaveText('1');
|
||||
await page.locator('button[aria-label="Rename"]').click();
|
||||
await page.locator('input[aria-label="New Name"]').waitFor({ state: 'visible' });
|
||||
await page.locator('input[aria-label="New Name"]').fill('renamed.txt');
|
||||
await page.locator('button[aria-label="Submit"]').click();
|
||||
await checkForNotification(page, "Item renamed successfully!");
|
||||
|
||||
// verify its no longer in index via search
|
||||
await page.locator('#search').click()
|
||||
await page.locator('#main-input').fill('renameme.txt');
|
||||
await expect(page.locator('#result-list ul li.search-entry')).toHaveCount(0);
|
||||
checkForErrors();
|
||||
})
|
||||
Reference in new issue
Block a user