From 9b78324d773c790951cc6a97840c4b55f66b5f3d Mon Sep 17 00:00:00 2001 From: tianrking <10758833+tianrking@users.noreply.github.com> Date: Sat, 18 Jul 2026 02:14:41 +0800 Subject: [PATCH 01/11] fix(http): run upload hooks for directories (#6034) --- http/resource.go | 4 +++- http/resource_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/http/resource.go b/http/resource.go index eda4e40389d..d8f5cad9248 100644 --- a/http/resource.go +++ b/http/resource.go @@ -130,7 +130,9 @@ func resourcePostHandler(fileCache FileCache) handleFunc { // Directories creation on POST. if strings.HasSuffix(r.URL.Path, "/") { - err := d.user.Fs.MkdirAll(r.URL.Path, d.settings.DirMode) + err := d.RunHook(func() error { + return d.user.Fs.MkdirAll(r.URL.Path, d.settings.DirMode) + }, "upload", r.URL.Path, "", d.user) return errToStatus(err), err } diff --git a/http/resource_test.go b/http/resource_test.go index e22d152b973..5a29518dd69 100644 --- a/http/resource_test.go +++ b/http/resource_test.go @@ -222,3 +222,38 @@ func TestResourcePostCleanupDoesNotDeleteThroughSymlink(t *testing.T) { t.Fatalf("VULNERABLE: out-of-scope victim.txt deleted by cleanup RemoveAll (status=%d): %v", rec.Code, statErr) } } + +func TestResourcePostRunsUploadHooksForDirectories(t *testing.T) { + root := t.TempDir() + userScope := filepath.Join(root, "user") + if err := os.MkdirAll(userScope, 0o755); err != nil { + t.Fatal(err) + } + + key := []byte("test-signing-key") + perm := users.Permissions{Create: true} + st := scopedUserStorage(t, userScope, perm, key) + if err := st.Settings.Save(&settings.Settings{ + Key: key, + Commands: map[string][]string{ + "after_upload": {"filebrowser-hook-command-that-does-not-exist"}, + }, + }); err != nil { + t.Fatal(err) + } + + req, _ := http.NewRequest(http.MethodPost, "/created/", http.NoBody) + req.Header.Set("X-Auth", signToken(t, perm, key)) + rec := httptest.NewRecorder() + handle(resourcePostHandler(diskcache.NewNoOp()), "", st, &settings.Server{EnableExec: true}).ServeHTTP(rec, req) + + // A missing after_upload command makes the request fail only if the hook ran. + // It avoids a platform-specific helper executable while still exercising the + // same path the web UI uses for directory uploads. + if rec.Code != http.StatusInternalServerError { + t.Fatalf("expected directory upload hook failure to return 500, got %d body=%q", rec.Code, rec.Body.String()) + } + if _, err := os.Stat(filepath.Join(userScope, "created")); err != nil { + t.Fatalf("expected directory to be created before its after hook, got %v", err) + } +} From f0785391bf11cc8ec53bff7b2f36a29a02f536dc Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 07:20:43 +0200 Subject: [PATCH 02/11] chore: update translations (#6019) Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com> --- frontend/src/i18n/fr.json | 50 +++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/frontend/src/i18n/fr.json b/frontend/src/i18n/fr.json index 985763f1ddf..4f22d4dfe5e 100644 --- a/frontend/src/i18n/fr.json +++ b/frontend/src/i18n/fr.json @@ -120,12 +120,12 @@ "passwordsDontMatch": "Les mots de passe ne concordent pas", "signup": "S'inscrire", "submit": "Se connecter", - "username": "Utilisateur", - "usernameTaken": "Le nom d'utilisateur est déjà pris", + "username": "Identifiant", + "usernameTaken": "L'identifiant est déjà pris", "wrongCredentials": "Identifiants incorrects !", "passwordTooShort": "Le mot de passe doit contenir au moins {min} caractères", "logout_reasons": { - "inactivity": "Vous avez été déconnecté(e) en raison d'une inactivité prolongée." + "inactivity": "Vous avez été déconnecté'e en raison d'une inactivité prolongée." } }, "permanent": "Permanent", @@ -194,12 +194,12 @@ "settings": { "aceEditorTheme": "Éditeur de Thème Ace", "admin": "Admin", - "administrator": "Administrateur", + "administrator": "Administrateur'ice", "allowCommands": "Exécuter des commandes", "allowEdit": "Éditer, renommer et supprimer des fichiers ou des dossiers", "allowNew": "Créer de nouveaux fichiers et dossiers", "allowPublish": "Publier de nouveaux posts et pages", - "allowSignup": "Autoriser les utilisateurs à s'inscrire", + "allowSignup": "Autoriser les utilisateur'ices à s'inscrire", "hideLoginButton": "Cacher le bouton d’identification sur les pages publiques", "avoidChanges": "(Laisser vide pour conserver l'actuel)", "branding": "Image de marque", @@ -209,34 +209,34 @@ "commandRunner": "Exécuteur de commandes", "commandRunnerHelp": "Ici, vous pouvez définir les commandes qui seront exécutées lors des événements nommés précédemments. Vous devez en écrire une par ligne. Les variables d'environnement {0} et {1} seront disponibles, {0} étant relatif à {1}. Pour plus d'informations sur cette fonctionnalité et les variables d'environnement disponibles, veuillez lire la {2}.", "commandsUpdated": "Commandes mises à jour !", - "createUserDir": "Créer automatiquement un dossier pour l'utilisateur", + "createUserDir": "Créer automatiquement un dossier pour l'utilisateur'ice", "minimumPasswordLength": "Taille minimale du mot de passe", "tusUploads": "Uploads segmentés", "tusUploadsHelp": "File Browser prend en charge les uploads segmentés afin de permettre une gestion efficace, fiable et reprenable sur des réseaux instables.", "tusUploadsChunkSize": "Taille maximale autorisée par segment (les uploads directs seront utilisés pour les fichiers plus petits). Vous pouvez entrer un entier en octets ou une chaîne telle que 10MB, 1GB, etc.", "tusUploadsRetryCount": "Nombre de tentatives en cas d'échec d'un segment.", - "userHomeBasePath": "Chemin de base pour les dossiers personnels des utilisateurs", + "userHomeBasePath": "Chemin de base pour les dossiers personnels des utilisateur'ices", "userScopeGenerationPlaceholder": "Le périmètre sera généré automatiquement", - "createUserHomeDirectory": "Créer le dossier personnel de l'utilisateur", + "createUserHomeDirectory": "Créer le dossier personnel de l'utilisateur'ice", "customStylesheet": "Feuille de style personnalisée", - "defaultUserDescription": "Paramètres par défaut pour les nouveaux utilisateurs.", + "defaultUserDescription": "Paramètres par défaut pour les nouveaux utilisateur'ices.", "disableExternalLinks": "Désactiver les liens externes (sauf la documentation)", "disableUsedDiskPercentage": "Désactiver le graphique de pourcentage d'utilisation du disque", "documentation": "documentation", "examples": "Exemples", "executeOnShell": "Exécuter dans le shell", - "executeOnShellDescription": "Par défaut, File Browser exécute les commandes en appelant directement leurs binaires. Si vous voulez les exécuter sur un shell à la place (comme Bash ou PowerShell), vous pouvez le définir ici avec les arguments et les drapeaux requis. S'il est défini, la commande que vous exécutez sera ajoutée en tant qu'argument. Cela s'applique à la fois aux commandes utilisateur et aux crochets d'événements.", - "globalRules": "Il s'agit d'un ensemble global de règles d'autorisation et d'interdiction. Elles s'appliquent à tous les utilisateurs. Vous pouvez définir des règles spécifiques sur les paramètres de chaque utilisateur pour remplacer celles-ci.", + "executeOnShellDescription": "Par défaut, File Browser exécute les commandes en appelant directement leurs binaires. Si vous voulez les exécuter sur un shell à la place (comme Bash ou PowerShell), vous pouvez le définir ici avec les arguments et les drapeaux requis. S'il est défini, la commande que vous exécutez sera ajoutée en tant qu'argument. Cela s'applique à la fois aux commandes utilisateur'ice et aux crochets d'événements.", + "globalRules": "Il s'agit d'un ensemble global de règles d'autorisation et d'interdiction. Elles s'appliquent à tous les utilisateur'ices. Vous pouvez définir des règles spécifiques sur les paramètres de chaque utilisateur'ice pour remplacer celles-ci.", "globalSettings": "Paramètres globaux", "hideDotfiles": "Cacher les fichiers de configuration commançant par un point", "insertPath": "Insérer le chemin", "insertRegex": "Insérer une expression régulière", "instanceName": "Nom de l'instance", "language": "Langue", - "lockPassword": "Empêcher l'utilisateur de changer son mot de passe", + "lockPassword": "Empêcher l'utilisateur'ice de changer son mot de passe", "newPassword": "Votre nouveau mot de passe", "newPasswordConfirm": "Confirmation du nouveau mot de passe", - "newUser": "Nouvel utilisateur", + "newUser": "Nouvel'le utilisateur'ice", "password": "Mot de passe", "passwordUpdated": "Mot de passe mis à jour !", "path": "Chemin", @@ -250,14 +250,14 @@ "share": "Partager des fichiers (autorisation de téléchargement requise)" }, "permissions": "Permissions", - "permissionsHelp": "Vous pouvez définir l'utilisateur comme étant un administrateur ou encore choisir les permissions individuellement. Si vous sélectionnez \"Administrateur\", toutes les autres options seront automatiquement activées. La gestion des utilisateurs est un privilège que seul l'administrateur possède.\n", + "permissionsHelp": "Vous pouvez définir l'utilisateur'ice comme étant un'e administrateur'ice ou encore choisir les permissions individuellement. Si vous sélectionnez \"Administrateur'ice\", toutes les autres options seront automatiquement activées. La gestion des utilisateur'ices est un privilège que seul l'administrateur'ice possède.\n", "profileSettings": "Paramètres du profil", "redirectAfterCopyMove": "Rediriger vers la destination après une copie/déplacement", "ruleExample1": "Bloque l'accès à tous les fichiers commençant par un point (comme par exemple .git, .gitignore) dans tous les dossiers.\n", - "ruleExample2": "Bloque l'accès au fichier nommé \"Caddyfile\" à la racine du dossier utilisateur", + "ruleExample2": "Bloque l'accès au fichier nommé \"Caddyfile\" à la racine du dossier utilisateur'ice", "rules": "Règles", - "rulesHelp": "Vous pouvez définir ici un ensemble de règles pour cet utilisateur. Les fichiers bloqués ne seront pas affichés et ne seront pas accessibles par l'utilisateur. Les expressions régulières sont supportées et les chemins d'accès sont relatifs par rapport au dossier de l'utilisateur.\n", - "scope": "Portée du dossier utilisateur", + "rulesHelp": "Vous pouvez définir ici un ensemble de règles pour cet'te utilisateur'ice. Les fichiers bloqués ne seront pas affichés et ne seront pas accessibles par l'utilisateur'ice. Les expressions régulières sont supportées et les chemins d'accès sont relatifs par rapport au dossier de l'utilisateur'ice.\n", + "scope": "Portée du dossier utilisateur'ice", "setDateFormat": "Définir le format de la date", "settingsUpdated": "Les paramètres ont été mis à jour !", "shareDuration": "Durée du partage", @@ -270,16 +270,16 @@ "light": "Clair", "title": "Thème" }, - "user": "Utilisateur", + "user": "Utilisateur'ice", "userCommands": "Commandes", "userCommandsHelp": "Une liste séparée par des espaces des commandes permises pour l'utilisateur. Exemple :\n", - "userCreated": "Utilisateur créé !", - "userDefaults": "Paramètres par défaut de l'utilisateur", - "userDeleted": "Utilisateur supprimé !", - "userManagement": "Gestion des utilisateurs", - "userUpdated": "Utilisateur mis à jour !", - "username": "Nom d'utilisateur", - "users": "Utilisateurs", + "userCreated": "Utilisateur'ice créé !", + "userDefaults": "Paramètres par défaut de l'utilisateur'ice", + "userDeleted": "Utilisateur'ice supprimé !", + "userManagement": "Gestion des utilisateur'ices", + "userUpdated": "Utilisateur'ice mis à jour !", + "username": "Nom d'utilisateur'ice", + "users": "Utilisateur'ices", "currentPassword": "Mot de Passe Actuel" }, "sidebar": { From ac46cf06719575477d5125e7472037c204b3702d Mon Sep 17 00:00:00 2001 From: TowyTowy <85077986+TowyTowy@users.noreply.github.com> Date: Sat, 18 Jul 2026 07:21:44 +0200 Subject: [PATCH 03/11] fix: return error instead of panicking on an unreadable directory during copy (#6020) Co-authored-by: Claude Fable 5 --- fileutils/copy_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++ fileutils/dir.go | 7 +++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/fileutils/copy_test.go b/fileutils/copy_test.go index ccdcc9018b4..6899e0327a7 100644 --- a/fileutils/copy_test.go +++ b/fileutils/copy_test.go @@ -2,6 +2,7 @@ package fileutils import ( "os" + "path" "path/filepath" "testing" @@ -9,6 +10,54 @@ import ( "github.com/spf13/afero" ) +// failingOpenFs wraps an afero.Fs and makes Open fail for one specific path, +// while every other operation (including Stat) is delegated unchanged. It +// simulates a directory that can be stat-ed but not opened/read — for example +// an unreadable sub-directory, or one whose permissions changed or that was +// removed after its parent was listed (a TOCTOU race) — encountered during a +// recursive copy. +type failingOpenFs struct { + afero.Fs + failOpen string +} + +func (f *failingOpenFs) Open(name string) (afero.File, error) { + if path.Clean(name) == path.Clean(f.failOpen) { + return nil, os.ErrPermission + } + return f.Fs.Open(name) +} + +// CopyDir is documented to keep going when it hits an error and to report the +// error afterwards. A sub-directory that cannot be opened must therefore yield +// an error (and leave the other, readable entries copied) rather than +// panicking on a nil directory handle. +func TestCopyDirUnreadableSubdirReturnsError(t *testing.T) { + mem := afero.NewMemMapFs() + if err := mem.MkdirAll("/srcdir/sub", 0o755); err != nil { + t.Fatal(err) + } + if err := afero.WriteFile(mem, "/srcdir/ok.txt", []byte("readable"), 0o644); err != nil { + t.Fatal(err) + } + + afs := &failingOpenFs{Fs: mem, failOpen: "/srcdir/sub"} + + err := Copy(afs, "/srcdir", "/dstdir", 0o644, 0o755) + if err == nil { + t.Fatal("expected an error when a sub-directory cannot be opened") + } + + // The readable sibling must still have been copied (continue-on-error). + data, readErr := afero.ReadFile(afs, "/dstdir/ok.txt") + if readErr != nil { + t.Fatalf("readable sibling was not copied: %v", readErr) + } + if string(data) != "readable" { + t.Fatalf("unexpected copied content: %q", string(data)) + } +} + // Copying an in-scope directory that contains a symlink whose target escapes // the user's scope must not dereference that symlink into the destination. // Otherwise a scoped user could exfiltrate out-of-scope file content via the diff --git a/fileutils/dir.go b/fileutils/dir.go index e0b049db40d..4bd7c9256d3 100644 --- a/fileutils/dir.go +++ b/fileutils/dir.go @@ -23,7 +23,12 @@ func CopyDir(afs afero.Fs, source, dest string, fileMode, dirMode fs.FileMode) e return err } - dir, _ := afs.Open(source) + dir, err := afs.Open(source) + if err != nil { + return err + } + defer dir.Close() + obs, err := dir.Readdir(-1) if err != nil { return err From bcacb720a0492b47442b0b4eaacbed4316fc65e5 Mon Sep 17 00:00:00 2001 From: Michal Pryc Date: Sat, 18 Oct 2025 21:32:21 +0200 Subject: [PATCH 04/11] UPSTREAM: Add disableUserProfile branding option New config option: --branding.disableUserProfile This option allows to disable User Profile together with User settings. Signed-off-by: Michal Pryc --- cmd/config.go | 9 +- cmd/config_init.go | 129 ++++++++++++++++++++++++- cmd/config_set.go | 56 ++++++++++- frontend/index.html | 1 + frontend/src/components/Sidebar.vue | 6 +- frontend/src/i18n/en.json | 1 + frontend/src/router/index.ts | 18 +++- frontend/src/types/settings.d.ts | 1 + frontend/src/utils/constants.ts | 2 + frontend/src/views/Settings.vue | 3 +- frontend/src/views/settings/Global.vue | 9 ++ http/static.go | 1 + settings/branding.go | 1 + 13 files changed, 228 insertions(+), 9 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index cf923eac0e8..5eeaacc1cde 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -57,9 +57,11 @@ func addConfigFlags(flags *pflag.FlagSet) { flags.String("branding.files", "", "path to directory with images and custom styles") flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links") flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph") - - flags.Uint64("tus.chunkSize", settings.DefaultTusChunkSize, "the tus chunk size") - flags.Uint16("tus.retryCount", settings.DefaultTusRetryCount, "the tus retry count") + flags.Bool("branding.disableUserProfile", false, "disable user profile in sidebar and settings/profile page") + // NB: these are string so they can be presented as octal in the help text + // as that's the conventional representation for modes in Unix. + flags.String("file-mode", fmt.Sprintf("%O", settings.DefaultFileMode), "Mode bits that new files are created with") + flags.String("dir-mode", fmt.Sprintf("%O", settings.DefaultDirMode), "Mode bits that new directories are created with") } func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) { @@ -212,6 +214,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut fmt.Fprintf(w, "\tFiles override:\t%s\n", set.Branding.Files) fmt.Fprintf(w, "\tDisable external links:\t%t\n", set.Branding.DisableExternal) fmt.Fprintf(w, "\tDisable used disk percentage graph:\t%t\n", set.Branding.DisableUsedPercentage) + fmt.Fprintf(w, "\tDisable user profile:\t%t\n", set.Branding.DisableUserProfile) fmt.Fprintf(w, "\tColor:\t%s\n", set.Branding.Color) fmt.Fprintf(w, "\tTheme:\t%s\n", set.Branding.Theme) diff --git a/cmd/config_init.go b/cmd/config_init.go index 359d02a3435..eb1549bd517 100644 --- a/cmd/config_init.go +++ b/cmd/config_init.go @@ -46,7 +46,134 @@ override the options.`, return err } - err = st.Auth.Save(auther) + minLength, err := getUint(flags, "minimum-password-length") + if err != nil { + return err + } + + shell, err := getString(flags, "shell") + if err != nil { + return err + } + + brandingName, err := getString(flags, "branding.name") + if err != nil { + return err + } + + brandingDisableExternal, err := getBool(flags, "branding.disableExternal") + if err != nil { + return err + } + + brandingDisableUsedPercentage, err := getBool(flags, "branding.disableUsedPercentage") + if err != nil { + return err + } + + brandingDisableUserProfile, err := getBool(flags, "branding.disableUserProfile") + if err != nil { + return err + } + + brandingTheme, err := getString(flags, "branding.theme") + if err != nil { + return err + } + + brandingFiles, err := getString(flags, "branding.files") + if err != nil { + return err + } + + s := &settings.Settings{ + Key: key, + Signup: signup, + CreateUserDir: createUserDir, + MinimumPasswordLength: minLength, + Shell: convertCmdStrToCmdArray(shell), + AuthMethod: authMethod, + Defaults: defaults, + Branding: settings.Branding{ + Name: brandingName, + DisableExternal: brandingDisableExternal, + DisableUsedPercentage: brandingDisableUsedPercentage, + DisableUserProfile: brandingDisableUserProfile, + Theme: brandingTheme, + Files: brandingFiles, + }, + } + + s.FileMode, err = getMode(flags, "file-mode") + if err != nil { + return err + } + + s.DirMode, err = getMode(flags, "dir-mode") + if err != nil { + return err + } + + address, err := getString(flags, "address") + if err != nil { + return err + } + + socket, err := getString(flags, "socket") + if err != nil { + return err + } + + root, err := getString(flags, "root") + if err != nil { + return err + } + + baseURL, err := getString(flags, "baseurl") + if err != nil { + return err + } + + tlsKey, err := getString(flags, "key") + if err != nil { + return err + } + + cert, err := getString(flags, "cert") + if err != nil { + return err + } + + port, err := getString(flags, "port") + if err != nil { + return err + } + + log, err := getString(flags, "log") + if err != nil { + return err + } + + ser := &settings.Server{ + Address: address, + Socket: socket, + Root: root, + BaseURL: baseURL, + TLSKey: tlsKey, + TLSCert: cert, + Port: port, + Log: log, + } + + err = d.store.Settings.Save(s) + if err != nil { + return err + } + err = d.store.Settings.SaveServer(ser) + if err != nil { + return err + } + err = d.store.Auth.Save(auther) if err != nil { return err } diff --git a/cmd/config_set.go b/cmd/config_set.go index df357a02658..626e6e82f0b 100644 --- a/cmd/config_set.go +++ b/cmd/config_set.go @@ -29,7 +29,61 @@ you want to change. Other options will remain unchanged.`, return err } - auther, err := st.Auth.Get(set.AuthMethod) + hasAuth := false + flags.Visit(func(flag *pflag.Flag) { + if err != nil { + return + } + switch flag.Name { + case "baseurl": + ser.BaseURL, err = getString(flags, flag.Name) + case "root": + ser.Root, err = getString(flags, flag.Name) + case "socket": + ser.Socket, err = getString(flags, flag.Name) + case "cert": + ser.TLSCert, err = getString(flags, flag.Name) + case "key": + ser.TLSKey, err = getString(flags, flag.Name) + case "address": + ser.Address, err = getString(flags, flag.Name) + case "port": + ser.Port, err = getString(flags, flag.Name) + case "log": + ser.Log, err = getString(flags, flag.Name) + case "signup": + set.Signup, err = getBool(flags, flag.Name) + case "auth.method": + hasAuth = true + case "shell": + var shell string + shell, err = getString(flags, flag.Name) + set.Shell = convertCmdStrToCmdArray(shell) + case "create-user-dir": + set.CreateUserDir, err = getBool(flags, flag.Name) + case "minimum-password-length": + set.MinimumPasswordLength, err = getUint(flags, flag.Name) + case "branding.name": + set.Branding.Name, err = getString(flags, flag.Name) + case "branding.color": + set.Branding.Color, err = getString(flags, flag.Name) + case "branding.theme": + set.Branding.Theme, err = getString(flags, flag.Name) + case "branding.disableExternal": + set.Branding.DisableExternal, err = getBool(flags, flag.Name) + case "branding.disableUsedPercentage": + set.Branding.DisableUsedPercentage, err = getBool(flags, flag.Name) + case "branding.disableUserProfile": + set.Branding.DisableUserProfile, err = getBool(flags, flag.Name) + case "branding.files": + set.Branding.Files, err = getString(flags, flag.Name) + case "file-mode": + set.FileMode, err = getMode(flags, flag.Name) + case "dir-mode": + set.DirMode, err = getMode(flags, flag.Name) + } + }) + if err != nil { return err } diff --git a/frontend/index.html b/frontend/index.html index 19308a95434..ffa4df85457 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -37,6 +37,7 @@ Color: "", DisableExternal: false, DisableUsedPercentage: false, + DisableUserProfile: false, EnableExec: true, EnableThumbs: true, LogoutPage: "", diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index ff74aa40eb6..6578a928b66 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -2,7 +2,7 @@