mirror of
https://github.com/filebrowser/filebrowser.git
synced 2026-07-18 17:03:58 +00:00
Compare commits
10 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac46cf0671 | ||
|
|
f0785391bf | ||
|
|
9b78324d77 | ||
|
|
fe7efb2e6a | ||
|
|
2651260a1c | ||
|
|
4470288ba1 | ||
|
|
58d22578e8 | ||
|
|
dfc2e887e1 | ||
|
|
aac2516637 | ||
|
|
c05ead7e8e |
15 changed files with 269 additions and 414 deletions
|
|
@ -2,6 +2,15 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.63.18](https://github.com/filebrowser/filebrowser/compare/v2.63.17...v2.63.18) (2026-07-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* avoid recursive conflict checks for copy and move ([#6009](https://github.com/filebrowser/filebrowser/issues/6009)) ([dfc2e88](https://github.com/filebrowser/filebrowser/commit/dfc2e887e1a19d54984a0d7e39a2a63caf73ef19))
|
||||||
|
* deduplicate PT language ([4470288](https://github.com/filebrowser/filebrowser/commit/4470288ba1828a14453a99348fd22c62aa0b9460))
|
||||||
|
* **preview:** keep the EPUB table-of-contents button clear of the header ([#6010](https://github.com/filebrowser/filebrowser/issues/6010)) ([aac2516](https://github.com/filebrowser/filebrowser/commit/aac25166378422135e624e305c410c54a39374fb))
|
||||||
|
|
||||||
## [2.63.17](https://github.com/filebrowser/filebrowser/compare/v2.63.16...v2.63.17) (2026-06-27)
|
## [2.63.17](https://github.com/filebrowser/filebrowser/compare/v2.63.16...v2.63.17) (2026-06-27)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package fileutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -9,6 +10,54 @@ import (
|
||||||
"github.com/spf13/afero"
|
"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
|
// Copying an in-scope directory that contains a symlink whose target escapes
|
||||||
// the user's scope must not dereference that symlink into the destination.
|
// 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
|
// Otherwise a scoped user could exfiltrate out-of-scope file content via the
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,12 @@ func CopyDir(afs afero.Fs, source, dest string, fileMode, dirMode fs.FileMode) e
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
dir, _ := afs.Open(source)
|
dir, err := afs.Open(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dir.Close()
|
||||||
|
|
||||||
obs, err := dir.Readdir(-1)
|
obs, err := dir.Readdir(-1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,9 @@ const event = {
|
||||||
describe("copy and move conflict prompts", () => {
|
describe("copy and move conflict prompts", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
vi.mocked(checkConflict).mockResolvedValue(conflict);
|
vi.mocked(checkConflict).mockResolvedValue(
|
||||||
|
conflict as ConflictingResource[]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("waits for copy conflict detection before calling the copy API", async () => {
|
it("waits for copy conflict detection before calling the copy API", async () => {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,13 @@
|
||||||
|
|
||||||
.epub-reader .tocButton {
|
.epub-reader .tocButton {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
/*
|
||||||
|
* vue-reader positions the TOC toggle at top: 10px, which lands under the
|
||||||
|
* fixed 4em header bar (see .header) — so clicks hit the header's Close
|
||||||
|
* button instead of opening the table of contents. Push it just below the
|
||||||
|
* header, tracking the header's height so it stays clear if that changes.
|
||||||
|
*/
|
||||||
|
top: calc(4em + 10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.epub-reader .tocButton.tocButtonExpanded {
|
.epub-reader .tocButton.tocButtonExpanded {
|
||||||
|
|
|
||||||
|
|
@ -120,12 +120,12 @@
|
||||||
"passwordsDontMatch": "Les mots de passe ne concordent pas",
|
"passwordsDontMatch": "Les mots de passe ne concordent pas",
|
||||||
"signup": "S'inscrire",
|
"signup": "S'inscrire",
|
||||||
"submit": "Se connecter",
|
"submit": "Se connecter",
|
||||||
"username": "Utilisateur",
|
"username": "Identifiant",
|
||||||
"usernameTaken": "Le nom d'utilisateur est déjà pris",
|
"usernameTaken": "L'identifiant est déjà pris",
|
||||||
"wrongCredentials": "Identifiants incorrects !",
|
"wrongCredentials": "Identifiants incorrects !",
|
||||||
"passwordTooShort": "Le mot de passe doit contenir au moins {min} caractères",
|
"passwordTooShort": "Le mot de passe doit contenir au moins {min} caractères",
|
||||||
"logout_reasons": {
|
"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",
|
"permanent": "Permanent",
|
||||||
|
|
@ -194,12 +194,12 @@
|
||||||
"settings": {
|
"settings": {
|
||||||
"aceEditorTheme": "Éditeur de Thème Ace",
|
"aceEditorTheme": "Éditeur de Thème Ace",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"administrator": "Administrateur",
|
"administrator": "Administrateur'ice",
|
||||||
"allowCommands": "Exécuter des commandes",
|
"allowCommands": "Exécuter des commandes",
|
||||||
"allowEdit": "Éditer, renommer et supprimer des fichiers ou des dossiers",
|
"allowEdit": "Éditer, renommer et supprimer des fichiers ou des dossiers",
|
||||||
"allowNew": "Créer de nouveaux fichiers et dossiers",
|
"allowNew": "Créer de nouveaux fichiers et dossiers",
|
||||||
"allowPublish": "Publier de nouveaux posts et pages",
|
"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",
|
"hideLoginButton": "Cacher le bouton d’identification sur les pages publiques",
|
||||||
"avoidChanges": "(Laisser vide pour conserver l'actuel)",
|
"avoidChanges": "(Laisser vide pour conserver l'actuel)",
|
||||||
"branding": "Image de marque",
|
"branding": "Image de marque",
|
||||||
|
|
@ -209,34 +209,34 @@
|
||||||
"commandRunner": "Exécuteur de commandes",
|
"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}.",
|
"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 !",
|
"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",
|
"minimumPasswordLength": "Taille minimale du mot de passe",
|
||||||
"tusUploads": "Uploads segmentés",
|
"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.",
|
"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.",
|
"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.",
|
"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",
|
"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",
|
"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)",
|
"disableExternalLinks": "Désactiver les liens externes (sauf la documentation)",
|
||||||
"disableUsedDiskPercentage": "Désactiver le graphique de pourcentage d'utilisation du disque",
|
"disableUsedDiskPercentage": "Désactiver le graphique de pourcentage d'utilisation du disque",
|
||||||
"documentation": "documentation",
|
"documentation": "documentation",
|
||||||
"examples": "Exemples",
|
"examples": "Exemples",
|
||||||
"executeOnShell": "Exécuter dans le shell",
|
"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.",
|
"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 utilisateurs. Vous pouvez définir des règles spécifiques sur les paramètres de chaque utilisateur pour remplacer celles-ci.",
|
"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",
|
"globalSettings": "Paramètres globaux",
|
||||||
"hideDotfiles": "Cacher les fichiers de configuration commançant par un point",
|
"hideDotfiles": "Cacher les fichiers de configuration commançant par un point",
|
||||||
"insertPath": "Insérer le chemin",
|
"insertPath": "Insérer le chemin",
|
||||||
"insertRegex": "Insérer une expression régulière",
|
"insertRegex": "Insérer une expression régulière",
|
||||||
"instanceName": "Nom de l'instance",
|
"instanceName": "Nom de l'instance",
|
||||||
"language": "Langue",
|
"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",
|
"newPassword": "Votre nouveau mot de passe",
|
||||||
"newPasswordConfirm": "Confirmation du nouveau mot de passe",
|
"newPasswordConfirm": "Confirmation du nouveau mot de passe",
|
||||||
"newUser": "Nouvel utilisateur",
|
"newUser": "Nouvel'le utilisateur'ice",
|
||||||
"password": "Mot de passe",
|
"password": "Mot de passe",
|
||||||
"passwordUpdated": "Mot de passe mis à jour !",
|
"passwordUpdated": "Mot de passe mis à jour !",
|
||||||
"path": "Chemin",
|
"path": "Chemin",
|
||||||
|
|
@ -250,14 +250,14 @@
|
||||||
"share": "Partager des fichiers (autorisation de téléchargement requise)"
|
"share": "Partager des fichiers (autorisation de téléchargement requise)"
|
||||||
},
|
},
|
||||||
"permissions": "Permissions",
|
"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",
|
"profileSettings": "Paramètres du profil",
|
||||||
"redirectAfterCopyMove": "Rediriger vers la destination après une copie/déplacement",
|
"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",
|
"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",
|
"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",
|
"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",
|
"scope": "Portée du dossier utilisateur'ice",
|
||||||
"setDateFormat": "Définir le format de la date",
|
"setDateFormat": "Définir le format de la date",
|
||||||
"settingsUpdated": "Les paramètres ont été mis à jour !",
|
"settingsUpdated": "Les paramètres ont été mis à jour !",
|
||||||
"shareDuration": "Durée du partage",
|
"shareDuration": "Durée du partage",
|
||||||
|
|
@ -270,16 +270,16 @@
|
||||||
"light": "Clair",
|
"light": "Clair",
|
||||||
"title": "Thème"
|
"title": "Thème"
|
||||||
},
|
},
|
||||||
"user": "Utilisateur",
|
"user": "Utilisateur'ice",
|
||||||
"userCommands": "Commandes",
|
"userCommands": "Commandes",
|
||||||
"userCommandsHelp": "Une liste séparée par des espaces des commandes permises pour l'utilisateur. Exemple :\n",
|
"userCommandsHelp": "Une liste séparée par des espaces des commandes permises pour l'utilisateur. Exemple :\n",
|
||||||
"userCreated": "Utilisateur créé !",
|
"userCreated": "Utilisateur'ice créé !",
|
||||||
"userDefaults": "Paramètres par défaut de l'utilisateur",
|
"userDefaults": "Paramètres par défaut de l'utilisateur'ice",
|
||||||
"userDeleted": "Utilisateur supprimé !",
|
"userDeleted": "Utilisateur'ice supprimé !",
|
||||||
"userManagement": "Gestion des utilisateurs",
|
"userManagement": "Gestion des utilisateur'ices",
|
||||||
"userUpdated": "Utilisateur mis à jour !",
|
"userUpdated": "Utilisateur'ice mis à jour !",
|
||||||
"username": "Nom d'utilisateur",
|
"username": "Nom d'utilisateur'ice",
|
||||||
"users": "Utilisateurs",
|
"users": "Utilisateur'ices",
|
||||||
"currentPassword": "Mot de Passe Actuel"
|
"currentPassword": "Mot de Passe Actuel"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,7 @@
|
||||||
"newPasswordConfirm": "Confirmar a sua palavra-passe",
|
"newPasswordConfirm": "Confirmar a sua palavra-passe",
|
||||||
"newUser": "Novo Utilizador",
|
"newUser": "Novo Utilizador",
|
||||||
"password": "Palavra-passe",
|
"password": "Palavra-passe",
|
||||||
"passwordUpdated": "Palavras-passe não coincidem!",
|
"passwordUpdated": "Palavra-passe atualizada!",
|
||||||
"path": "Caminho",
|
"path": "Caminho",
|
||||||
"perm": {
|
"perm": {
|
||||||
"create": "Criar ficheiros e pastas",
|
"create": "Criar ficheiros e pastas",
|
||||||
|
|
|
||||||
|
|
@ -1,309 +0,0 @@
|
||||||
{
|
|
||||||
"buttons": {
|
|
||||||
"cancel": "Cancelar",
|
|
||||||
"clear": "Limpar",
|
|
||||||
"close": "Fechar",
|
|
||||||
"continue": "Continuar",
|
|
||||||
"copy": "Copiar",
|
|
||||||
"copyFile": "Copiar ficheiro",
|
|
||||||
"copyToClipboard": "Copiar",
|
|
||||||
"copyDownloadLinkToClipboard": "Copy download link to clipboard",
|
|
||||||
"create": "Criar",
|
|
||||||
"delete": "Eliminar",
|
|
||||||
"download": "Descarregar",
|
|
||||||
"file": "File",
|
|
||||||
"folder": "Folder",
|
|
||||||
"fullScreen": "Toggle full screen",
|
|
||||||
"hideDotfiles": "Hide dotfiles",
|
|
||||||
"info": "Info",
|
|
||||||
"more": "Mais",
|
|
||||||
"move": "Mover",
|
|
||||||
"moveFile": "Mover ficheiro",
|
|
||||||
"new": "Novo",
|
|
||||||
"next": "Próximo",
|
|
||||||
"ok": "OK",
|
|
||||||
"permalink": "Obter link permanente",
|
|
||||||
"previous": "Anterior",
|
|
||||||
"preview": "Preview",
|
|
||||||
"publish": "Publicar",
|
|
||||||
"rename": "Alterar nome",
|
|
||||||
"replace": "Substituir",
|
|
||||||
"reportIssue": "Reportar erro",
|
|
||||||
"resumeTransfer": "Resume previous transfer",
|
|
||||||
"resumeTransferTooltip": "Skip all conflicting files, except the ones that are smaller on the server as we suppose that their transfert has been interrupted.",
|
|
||||||
"save": "Guardar",
|
|
||||||
"schedule": "Agendar",
|
|
||||||
"search": "Pesquisar",
|
|
||||||
"select": "Selecionar",
|
|
||||||
"selectMultiple": "Selecionar vários",
|
|
||||||
"share": "Partilhar",
|
|
||||||
"shell": "Alternar shell",
|
|
||||||
"submit": "Submit",
|
|
||||||
"switchView": "Alterar vista",
|
|
||||||
"toggleSidebar": "Alternar barra lateral",
|
|
||||||
"update": "Atualizar",
|
|
||||||
"upload": "Enviar",
|
|
||||||
"openFile": "Open file",
|
|
||||||
"openDirect": "View raw",
|
|
||||||
"discardChanges": "Discard",
|
|
||||||
"stopSearch": "Stop searching",
|
|
||||||
"saveChanges": "Save changes",
|
|
||||||
"editAsText": "Edit as Text",
|
|
||||||
"increaseFontSize": "Increase font size",
|
|
||||||
"decreaseFontSize": "Decrease font size",
|
|
||||||
"overrideAll": "Replace all files in destination folder",
|
|
||||||
"skipAll": "Skip all conflicting files",
|
|
||||||
"renameAll": "Rename all files (create a copy)",
|
|
||||||
"singleDecision": "Decide for each conflicting file"
|
|
||||||
},
|
|
||||||
"download": {
|
|
||||||
"downloadFile": "Descarregar ficheiro",
|
|
||||||
"downloadFolder": "Descarregar pasta",
|
|
||||||
"downloadSelected": "Download Selected"
|
|
||||||
},
|
|
||||||
"upload": {
|
|
||||||
"abortUpload": "Are you sure you wish to abort?"
|
|
||||||
},
|
|
||||||
"errors": {
|
|
||||||
"forbidden": "Não tem permissões para aceder a isto",
|
|
||||||
"internal": "Algo correu bastante mal.",
|
|
||||||
"notFound": "Esta localização não é alcançável.",
|
|
||||||
"connection": "The server can't be reached."
|
|
||||||
},
|
|
||||||
"files": {
|
|
||||||
"body": "Corpo",
|
|
||||||
"closePreview": "Fechar pré-visualização",
|
|
||||||
"files": "Ficheiros",
|
|
||||||
"folders": "Pastas",
|
|
||||||
"home": "Início",
|
|
||||||
"lastModified": "Última alteração",
|
|
||||||
"loading": "A carregar...",
|
|
||||||
"lonely": "Sinto-me sozinho...",
|
|
||||||
"metadata": "Metadados",
|
|
||||||
"multipleSelectionEnabled": "Seleção múltipla ativada",
|
|
||||||
"name": "Nome",
|
|
||||||
"size": "Tamanho",
|
|
||||||
"sortByLastModified": "Ordenar pela última alteração",
|
|
||||||
"sortByName": "Ordenar pelo nome",
|
|
||||||
"sortBySize": "Ordenar pelo tamanho",
|
|
||||||
"noPreview": "Preview is not available for this file.",
|
|
||||||
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
|
|
||||||
"csvLoadFailed": "Failed to load CSV file.",
|
|
||||||
"showingRows": "Showing {count} row(s)",
|
|
||||||
"columnSeparator": "Column Separator",
|
|
||||||
"csvSeparators": {
|
|
||||||
"comma": "Comma (,)",
|
|
||||||
"semicolon": "Semicolon (;)",
|
|
||||||
"both": "Both (,) and (;)"
|
|
||||||
},
|
|
||||||
"fileEncoding": "File Encoding"
|
|
||||||
},
|
|
||||||
"help": {
|
|
||||||
"click": "selecionar pasta ou ficheiro",
|
|
||||||
"ctrl": {
|
|
||||||
"click": "selecionar várias pastas e ficheiros",
|
|
||||||
"f": "pesquisar",
|
|
||||||
"s": "guardar um ficheiro ou descarrega a pasta em que está a navegar"
|
|
||||||
},
|
|
||||||
"del": "eliminar os ficheiros selecionados",
|
|
||||||
"doubleClick": "abrir pasta ou ficheiro",
|
|
||||||
"esc": "limpar seleção e/ou fechar menu",
|
|
||||||
"f1": "esta informação",
|
|
||||||
"f2": "alterar nome do ficheiro",
|
|
||||||
"help": "Ajuda"
|
|
||||||
},
|
|
||||||
"login": {
|
|
||||||
"createAnAccount": "Criar uma conta",
|
|
||||||
"loginInstead": "Já tenho uma conta",
|
|
||||||
"password": "Palavra-passe",
|
|
||||||
"passwordConfirm": "Confirmação da palavra-passe",
|
|
||||||
"passwordsDontMatch": "As palavras-passe não coincidem",
|
|
||||||
"signup": "Registar",
|
|
||||||
"submit": "Entrar na conta",
|
|
||||||
"username": "Nome de utilizador",
|
|
||||||
"usernameTaken": "O nome de utilizador já está registado",
|
|
||||||
"wrongCredentials": "Dados errados",
|
|
||||||
"passwordTooShort": "Password must be at least {min} characters",
|
|
||||||
"logout_reasons": {
|
|
||||||
"inactivity": "You have been logged out due to inactivity."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"permanent": "Permanente",
|
|
||||||
"prompts": {
|
|
||||||
"copy": "Copiar",
|
|
||||||
"copyMessage": "Escolha um lugar para onde copiar os ficheiros:",
|
|
||||||
"currentlyNavigating": "A navegar em:",
|
|
||||||
"deleteMessageMultiple": "Quer eliminar {count} ficheiro(s)?",
|
|
||||||
"deleteMessageSingle": "Quer eliminar esta pasta/ficheiro?",
|
|
||||||
"deleteMessageShare": "Are you sure you wish to delete this share({path})?",
|
|
||||||
"deleteUser": "Are you sure you want to delete this user?",
|
|
||||||
"deleteTitle": "Eliminar ficheiros",
|
|
||||||
"displayName": "Nome:",
|
|
||||||
"download": "Descarregar ficheiros",
|
|
||||||
"downloadMessage": "Escolha o formato do ficheiro que quer descarregar.",
|
|
||||||
"error": "Algo correu mal",
|
|
||||||
"fileInfo": "Informação do ficheiro",
|
|
||||||
"filesSelected": "{count} ficheiros selecionados.",
|
|
||||||
"lastModified": "Última alteração",
|
|
||||||
"move": "Mover",
|
|
||||||
"moveMessage": "Escolha uma nova casa para os seus ficheiros/pastas:",
|
|
||||||
"newArchetype": "Criar um novo post baseado num \"archetype\". O seu ficheiro será criado na pasta \"content\".",
|
|
||||||
"newDir": "Nova pasta",
|
|
||||||
"newDirMessage": "Escreva o nome da nova pasta.",
|
|
||||||
"newFile": "Novo ficheiro",
|
|
||||||
"newFileMessage": "Escreva o nome do novo ficheiro.",
|
|
||||||
"numberDirs": "Número de pastas",
|
|
||||||
"numberFiles": "Número de ficheiros",
|
|
||||||
"rename": "Alterar nome",
|
|
||||||
"renameMessage": "Insira um novo nome para",
|
|
||||||
"replace": "Substituir",
|
|
||||||
"replaceMessage": "Já existe um ficheiro com nome igual a um dos que está a tentar enviar. Quer substituí-lo?\n",
|
|
||||||
"schedule": "Agendar",
|
|
||||||
"scheduleMessage": "Escolha uma data para publicar este post.",
|
|
||||||
"show": "Mostrar",
|
|
||||||
"size": "Tamanho",
|
|
||||||
"upload": "Upload",
|
|
||||||
"uploadFiles": "Uploading {files} files...",
|
|
||||||
"uploadMessage": "Select an option to upload.",
|
|
||||||
"optionalPassword": "Optional password",
|
|
||||||
"resolution": "Resolution",
|
|
||||||
"discardEditorChanges": "Are you sure you wish to discard the changes you've made?",
|
|
||||||
"replaceOrSkip": "Replace or skip files",
|
|
||||||
"resolveConflict": "Which files do you want to keep?",
|
|
||||||
"singleConflictResolve": "If you select both versions, a number will be added to the name of the copied file.",
|
|
||||||
"fastConflictResolve": "The destination folder there are {count} files with same name.",
|
|
||||||
"uploadingFiles": "Uploading files",
|
|
||||||
"filesInOrigin": "Files in origin",
|
|
||||||
"filesInDest": "Files in destination",
|
|
||||||
"override": "Overwrite",
|
|
||||||
"skip": "Skip",
|
|
||||||
"forbiddenError": "Forbidden Error",
|
|
||||||
"currentPassword": "Your password",
|
|
||||||
"currentPasswordMessage": "Enter your password to validate this action."
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"images": "Imagens",
|
|
||||||
"music": "Música",
|
|
||||||
"pdf": "PDF",
|
|
||||||
"pressToSearch": "Tecla Enter para pesquisar...",
|
|
||||||
"search": "Pesquisar...",
|
|
||||||
"typeToSearch": "Escrever para pesquisar...",
|
|
||||||
"types": "Tipos",
|
|
||||||
"video": "Vídeos"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"aceEditorTheme": "Ace editor theme",
|
|
||||||
"admin": "Admin",
|
|
||||||
"administrator": "Administrador",
|
|
||||||
"allowCommands": "Executar comandos",
|
|
||||||
"allowEdit": "Editar, renomear e eliminar ficheiros ou pastas",
|
|
||||||
"allowNew": "Criar novos ficheiros e pastas",
|
|
||||||
"allowPublish": "Publicar novas páginas e conteúdos",
|
|
||||||
"allowSignup": "Permitir que os utilizadores criem contas",
|
|
||||||
"hideLoginButton": "Hide the login button from public pages",
|
|
||||||
"avoidChanges": "(deixe em branco para manter)",
|
|
||||||
"branding": "Marca",
|
|
||||||
"brandingDirectoryPath": "Caminho da pasta de marca",
|
|
||||||
"brandingHelp": "Pode personalizar a aparência do seu Navegador de Ficheiros, alterar o nome, substituindo o logótipo, adicionando estilos personalizados e mesmo desativando links externos para o GitHub.\nPara mais informações sobre marca personalizada, por favor veja {0}.",
|
|
||||||
"changePassword": "Alterar palavra-passe",
|
|
||||||
"commandRunner": "Execução de comandos",
|
|
||||||
"commandRunnerHelp": "Aqui pode definir comandos que são executados nos eventos nomeados. Tem de escrever um por linha. As variáveis de ambiente {0} e {1} estarão disponíveis, sendo {0} relativo a {1}. Para mais informações sobre esta funcionalidade e as variáveis de ambiente, veja {2}.",
|
|
||||||
"commandsUpdated": "Comandos atualizados!",
|
|
||||||
"createUserDir": "Criar automaticamente a pasta de início ao adicionar um novo utilizador",
|
|
||||||
"minimumPasswordLength": "Minimum password length",
|
|
||||||
"tusUploads": "Chunked Uploads",
|
|
||||||
"tusUploadsHelp": "File Browser supports chunked file uploads, allowing for the creation of efficient, reliable, resumable and chunked file uploads even on unreliable networks.",
|
|
||||||
"tusUploadsChunkSize": "Indicates to maximum size of a request (direct uploads will be used for smaller uploads). You may input a plain integer denoting byte size input or a string like 10MB, 1GB etc.",
|
|
||||||
"tusUploadsRetryCount": "Number of retries to perform if a chunk fails to upload.",
|
|
||||||
"userHomeBasePath": "Base path for user home directories",
|
|
||||||
"userScopeGenerationPlaceholder": "The scope will be auto generated",
|
|
||||||
"createUserHomeDirectory": "Create user home directory",
|
|
||||||
"customStylesheet": "Folha de estilos personalizada",
|
|
||||||
"defaultUserDescription": "Estas são as configurações padrão para novos utilizadores.",
|
|
||||||
"disableExternalLinks": "Desativar links externos (exceto documentação)",
|
|
||||||
"disableUsedDiskPercentage": "Disable used disk percentage graph",
|
|
||||||
"documentation": "documentação",
|
|
||||||
"examples": "Exemplos",
|
|
||||||
"executeOnShell": "Executar na shell",
|
|
||||||
"executeOnShellDescription": "Por padrão, o Navegador de Ficheiros executa os comandos chamando os seus binários diretamente. Se em vez disso, quiser executá-los numa shell (como Bash ou PowerShell), pode definir isso aqui com os argumentos e bandeiras necessários. Se definido, o comando que executa será anexado como um argumento. Isto aplica-se tanto a comandos do utilizador como a hooks de eventos.",
|
|
||||||
"globalRules": "Isto é um conjunto global de regras de permissão e negação. Elas aplicam-se a todos os utilizadores. Pode especificar regras específicas para cada configuração do utilizador para sobreporem-se a estas.",
|
|
||||||
"globalSettings": "Configurações globais",
|
|
||||||
"hideDotfiles": "Hide dotfiles",
|
|
||||||
"insertPath": "Inserir o caminho",
|
|
||||||
"insertRegex": "Inserir expressão regular",
|
|
||||||
"instanceName": "Nome da instância",
|
|
||||||
"language": "Linguagem",
|
|
||||||
"lockPassword": "Não permitir que o utilizador altere a palavra-passe",
|
|
||||||
"newPassword": "Nova palavra-passe",
|
|
||||||
"newPasswordConfirm": "Confirme a nova palavra-passe",
|
|
||||||
"newUser": "Novo utilizador",
|
|
||||||
"password": "Palavra-passe",
|
|
||||||
"passwordUpdated": "Palavra-passe atualizada!",
|
|
||||||
"path": "Path",
|
|
||||||
"perm": {
|
|
||||||
"create": "Criar ficheiros e pastas",
|
|
||||||
"delete": "Eliminar ficheiros e pastas",
|
|
||||||
"download": "Descarregar",
|
|
||||||
"execute": "Executar comandos",
|
|
||||||
"modify": "Editar ficheiros",
|
|
||||||
"rename": "Alterar o nome ou mover ficheiros e pastas",
|
|
||||||
"share": "Share files (require download permission)"
|
|
||||||
},
|
|
||||||
"permissions": "Permissões",
|
|
||||||
"permissionsHelp": "Pode definir o utilizador como administrador ou escolher as permissões manualmente. Se selecionar a opção \"Administrador\", todas as outras opções serão automaticamente selecionadas. A gestão dos utilizadores é um privilégio restringido aos administradores.\n",
|
|
||||||
"profileSettings": "Configurações do utilizador",
|
|
||||||
"redirectAfterCopyMove": "Redirect to destination after copy/move",
|
|
||||||
"ruleExample1": "previne o acesso a qualquer \"dotfile\" (como .git, .gitignore) em qualquer pasta\n",
|
|
||||||
"ruleExample2": "bloqueia o acesso ao ficheiro chamado Caddyfile na raiz.",
|
|
||||||
"rules": "Regras",
|
|
||||||
"rulesHelp": "Aqui pode definir um conjunto de regras para permitir ou bloquear o acesso do utilizador a determinados ficheiros ou pastas. Os ficheiros bloqueados não irão aparecer durante a navegação. Suportamos expressões regulares e os caminhos dos ficheiros devem ser relativos à base do utilizador.\n",
|
|
||||||
"scope": "Base",
|
|
||||||
"setDateFormat": "Set exact date format",
|
|
||||||
"settingsUpdated": "Configurações atualizadas!",
|
|
||||||
"shareDuration": "Share Duration",
|
|
||||||
"shareManagement": "Share Management",
|
|
||||||
"shareDeleted": "Share deleted!",
|
|
||||||
"singleClick": "Use single clicks to open files and directories",
|
|
||||||
"themes": {
|
|
||||||
"default": "System default",
|
|
||||||
"dark": "Dark",
|
|
||||||
"light": "Light",
|
|
||||||
"title": "Theme"
|
|
||||||
},
|
|
||||||
"user": "Utilizador",
|
|
||||||
"userCommands": "Comandos",
|
|
||||||
"userCommandsHelp": "Uma lista, separada com espaços, de comandos disponíveis para este utilizados. Exemplo:",
|
|
||||||
"userCreated": "Utilizador criado!",
|
|
||||||
"userDefaults": "Configurações padrão do utilizador",
|
|
||||||
"userDeleted": "Utilizador eliminado!",
|
|
||||||
"userManagement": "Gestão de utilizadores",
|
|
||||||
"userUpdated": "Utilizador atualizado!",
|
|
||||||
"username": "Nome de utilizador",
|
|
||||||
"users": "Utilizadores",
|
|
||||||
"currentPassword": "Your Current Password"
|
|
||||||
},
|
|
||||||
"sidebar": {
|
|
||||||
"diskUsed": "{used} of {total} used",
|
|
||||||
"help": "Ajuda",
|
|
||||||
"hugoNew": "Hugo New",
|
|
||||||
"login": "Entrar",
|
|
||||||
"logout": "Sair",
|
|
||||||
"myFiles": "Meus ficheiros",
|
|
||||||
"newFile": "Novo ficheiro",
|
|
||||||
"newFolder": "Nova pasta",
|
|
||||||
"preview": "Pré-visualizar",
|
|
||||||
"settings": "Configurações",
|
|
||||||
"signup": "Registar",
|
|
||||||
"siteSettings": "Configurações do site"
|
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
"linkCopied": "Link copiado!"
|
|
||||||
},
|
|
||||||
"time": {
|
|
||||||
"days": "Dias",
|
|
||||||
"hours": "Horas",
|
|
||||||
"minutes": "Minutos",
|
|
||||||
"seconds": "Segundos",
|
|
||||||
"unit": "Unidades de tempo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { files as api } from "@/api";
|
||||||
|
|
||||||
vi.mock("@/api", () => ({
|
vi.mock("@/api", () => ({
|
||||||
files: {
|
files: {
|
||||||
|
fetch: vi.fn(),
|
||||||
fetchAll: vi.fn(),
|
fetchAll: vi.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
@ -19,8 +20,8 @@ vi.mock("@/stores/layout", () => ({ useLayoutStore: vi.fn() }));
|
||||||
vi.mock("@/stores/upload", () => ({ useUploadStore: vi.fn() }));
|
vi.mock("@/stores/upload", () => ({ useUploadStore: vi.fn() }));
|
||||||
vi.mock("@/utils/url", () => ({ default: {} }));
|
vi.mock("@/utils/url", () => ({ default: {} }));
|
||||||
|
|
||||||
// A move/copy/drag item carries `name` (raw) and `to` (URL-encoded) but no
|
// A move/copy/drag item carries name (raw) and to (URL-encoded) but no
|
||||||
// `fullPath` — mirroring what Move.vue / Copy.vue / ListingItem.vue build.
|
// fullPath - mirroring what Move.vue / Copy.vue / ListingItem.vue build.
|
||||||
function moveItem(name: string, dest: string, size = 12) {
|
function moveItem(name: string, dest: string, size = 12) {
|
||||||
return {
|
return {
|
||||||
from: `/files/source/${encodeURIComponent(name)}`,
|
from: `/files/source/${encodeURIComponent(name)}`,
|
||||||
|
|
@ -167,29 +168,46 @@ describe("checkConflict", () => {
|
||||||
expect(conflicts[0].name).toBe("/target/folder/deep/file.txt");
|
expect(conflicts[0].name).toBe("/target/folder/deep/file.txt");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy/move stats the whole destination, so a same-named directory is a
|
// Copy/move only needs the target directory's direct children. A recursive
|
||||||
// conflict in its own right (regression for the directory case of #5957).
|
// walk can make the UI look frozen on large destinations (regression #6005).
|
||||||
it("reports a directory conflict for copy/move (includeDirectories)", async () => {
|
it("checks only the direct destination listing for copy/move", async () => {
|
||||||
vi.mocked(api.fetchAll).mockResolvedValue([
|
vi.mocked(api.fetch).mockResolvedValue({
|
||||||
{
|
items: [
|
||||||
path: "/target/folder",
|
{
|
||||||
name: "folder",
|
path: "/target/file.txt",
|
||||||
size: 0,
|
name: "file.txt",
|
||||||
modified: "2026-06-04T00:00:00Z",
|
size: 10,
|
||||||
isDir: true,
|
modified: "2026-06-04T00:00:00Z",
|
||||||
},
|
isDir: false,
|
||||||
]);
|
},
|
||||||
|
{
|
||||||
|
path: "/target/folder",
|
||||||
|
name: "folder",
|
||||||
|
size: 0,
|
||||||
|
modified: "2026-06-04T00:00:00Z",
|
||||||
|
isDir: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as Resource);
|
||||||
|
|
||||||
const items = [{ ...moveItem("folder", "/files/target/", 0), isDir: true }];
|
const items = [
|
||||||
|
moveItem("file.txt", "/files/target/"),
|
||||||
|
{ ...moveItem("folder", "/files/target/", 0), isDir: true },
|
||||||
|
];
|
||||||
|
|
||||||
const conflicts = await checkConflict(items, "/files/target/", true);
|
const conflicts = await checkConflict(items, "/files/target/", true);
|
||||||
|
|
||||||
expect(conflicts).toHaveLength(1);
|
expect(api.fetch).toHaveBeenCalledWith("/files/target/");
|
||||||
expect(conflicts[0].name).toBe("/target/folder");
|
expect(api.fetchAll).not.toHaveBeenCalled();
|
||||||
|
expect(conflicts).toHaveLength(2);
|
||||||
|
expect(conflicts.map((conflict) => conflict.name)).toEqual([
|
||||||
|
"/target/file.txt",
|
||||||
|
"/target/folder",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Uploads merge into an existing folder, so the directory itself must not be
|
// Uploads merge into an existing folder, so the directory itself must not be
|
||||||
// reported — only the files inside it can conflict.
|
// reported - only the files inside it can conflict.
|
||||||
it("ignores a directory conflict for uploads (default)", async () => {
|
it("ignores a directory conflict for uploads (default)", async () => {
|
||||||
vi.mocked(api.fetchAll).mockResolvedValue([
|
vi.mocked(api.fetchAll).mockResolvedValue([
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -21,20 +21,61 @@ function conflictKey(item: UploadEntry): string {
|
||||||
return (item.fullPath || item.name).replace(/^\/+/, "");
|
return (item.fullPath || item.name).replace(/^\/+/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServerConflictEntry = {
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
modified: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchConflictEntries(
|
||||||
|
basePath: string,
|
||||||
|
includeDirectories: boolean
|
||||||
|
): Promise<ServerConflictEntry[]> {
|
||||||
|
if (!includeDirectories) {
|
||||||
|
return await api.fetchAll(basePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const destination = await api.fetch(basePath);
|
||||||
|
return destination.items ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function conflictPath(entry: ServerConflictEntry): string {
|
||||||
|
return entry.path.replace(/\\/g, "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConflictMap(
|
||||||
|
serverEntries: ServerConflictEntry[],
|
||||||
|
basePath: string,
|
||||||
|
includeDirectories: boolean
|
||||||
|
): Map<string, ServerConflictEntry> {
|
||||||
|
const serverMap = new Map<string, ServerConflictEntry>();
|
||||||
|
const normBase = removePrefix(basePath).replace(/\/+$/, "");
|
||||||
|
for (const entry of serverEntries) {
|
||||||
|
// A Windows server may return OS-native backslash separators; normalize to
|
||||||
|
// forward slashes so the prefix strip and key lookup line up.
|
||||||
|
const path = conflictPath(entry);
|
||||||
|
const key = includeDirectories
|
||||||
|
? entry.name
|
||||||
|
: path.startsWith(normBase)
|
||||||
|
? path.slice(normBase.length)
|
||||||
|
: path;
|
||||||
|
serverMap.set(key.replace(/^\/+/, ""), entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverMap;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the entries from `files` that already exist under `basePath` on the
|
* Return the entries from `files` that already exist under `basePath` on the
|
||||||
* server, so the caller can prompt the user to overwrite/rename/skip.
|
* server, so the caller can prompt the user to overwrite/rename/skip.
|
||||||
*
|
*
|
||||||
* The whole destination tree is fetched once and indexed by path relative to
|
|
||||||
* the destination, then every entry is looked up directly — no need to mirror
|
|
||||||
* the upload's folder structure.
|
|
||||||
*
|
|
||||||
* Directory handling differs by action, hence `includeDirectories`:
|
* Directory handling differs by action, hence `includeDirectories`:
|
||||||
* - Upload (false): an existing folder is silently merged, so only the
|
* - Upload (false): the destination tree is fetched recursively so nested
|
||||||
* individual files inside it can conflict.
|
* file uploads can be checked; existing folders are silently merged.
|
||||||
* - Copy/move (true): the server stats the destination and rejects it whole if
|
* - Copy/move (true): only the destination directory itself is fetched. These
|
||||||
* a same-named entry exists, so the directory itself is a conflict. The list
|
* operations move flat top-level selections, so a same-named direct child is
|
||||||
* only holds the top-level items being moved, so each is reported once.
|
* the only preflight conflict the backend will reject.
|
||||||
*
|
*
|
||||||
* @param files - flat upload list to check
|
* @param files - flat upload list to check
|
||||||
* @param basePath - server destination path (e.g. "/files/uploads/")
|
* @param basePath - server destination path (e.g. "/files/uploads/")
|
||||||
|
|
@ -47,27 +88,19 @@ export async function checkConflict(
|
||||||
): Promise<ConflictingResource[]> {
|
): Promise<ConflictingResource[]> {
|
||||||
if (files.length === 0) return [];
|
if (files.length === 0) return [];
|
||||||
|
|
||||||
let serverEntries: RecursiveEntry[];
|
let serverEntries: ServerConflictEntry[];
|
||||||
try {
|
try {
|
||||||
// Single API call: fetch the entire server tree under basePath.
|
serverEntries = await fetchConflictEntries(basePath, includeDirectories);
|
||||||
serverEntries = await api.fetchAll(basePath);
|
|
||||||
} catch {
|
} catch {
|
||||||
// The destination doesn't exist yet, so nothing can conflict.
|
// The destination doesn't exist yet, so nothing can conflict.
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// The server returns paths absolute within the user's scope
|
const serverMap = buildConflictMap(
|
||||||
// (e.g. "/uploads/sub/file.txt"). Strip the basePath prefix so the keys line
|
serverEntries,
|
||||||
// up with each entry's conflictKey, which is relative to the destination.
|
basePath,
|
||||||
const normBase = removePrefix(basePath).replace(/\/+$/, "");
|
includeDirectories
|
||||||
const serverMap = new Map<string, RecursiveEntry>();
|
);
|
||||||
for (const entry of serverEntries) {
|
|
||||||
// A Windows server may return OS-native backslash separators; normalize to
|
|
||||||
// forward slashes so the prefix strip and key lookup line up.
|
|
||||||
const path = entry.path.replace(/\\/g, "/");
|
|
||||||
const rel = path.startsWith(normBase) ? path.slice(normBase.length) : path;
|
|
||||||
serverMap.set(rel.replace(/^\/+/, ""), entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
const conflicts: ConflictingResource[] = [];
|
const conflicts: ConflictingResource[] = [];
|
||||||
files.forEach((file, index) => {
|
files.forEach((file, index) => {
|
||||||
|
|
@ -78,7 +111,7 @@ export async function checkConflict(
|
||||||
|
|
||||||
conflicts.push({
|
conflicts.push({
|
||||||
index,
|
index,
|
||||||
name: server.path,
|
name: conflictPath(server),
|
||||||
origin: { lastModified: file.file?.lastModified, size: file.size },
|
origin: { lastModified: file.file?.lastModified, size: file.size },
|
||||||
dest: { lastModified: server.modified, size: server.size },
|
dest: { lastModified: server.modified, size: server.size },
|
||||||
checked: ["origin"],
|
checked: ["origin"],
|
||||||
|
|
|
||||||
22
go.mod
22
go.mod
|
|
@ -4,30 +4,30 @@ go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/asdine/storm/v3 v3.2.1
|
github.com/asdine/storm/v3 v3.2.1
|
||||||
github.com/asticode/go-astisub v0.40.0
|
github.com/asticode/go-astisub v0.41.0
|
||||||
github.com/disintegration/imaging v1.6.2
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/dsoprea/go-exif/v3 v3.0.1
|
github.com/dsoprea/go-exif/v3 v3.0.1
|
||||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/jellydator/ttlcache/v3 v3.4.0
|
github.com/jellydator/ttlcache/v3 v3.4.1
|
||||||
github.com/maruel/natural v1.3.0
|
github.com/maruel/natural v1.3.0
|
||||||
github.com/marusama/semaphore/v2 v2.5.0
|
github.com/marusama/semaphore/v2 v2.5.0
|
||||||
github.com/mholt/archives v0.1.5
|
github.com/mholt/archives v0.1.5
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
github.com/redis/go-redis/v9 v9.20.1
|
github.com/redis/go-redis/v9 v9.21.0
|
||||||
github.com/samber/lo v1.53.0
|
github.com/samber/lo v1.53.0
|
||||||
github.com/shirou/gopsutil/v4 v4.26.5
|
github.com/shirou/gopsutil/v4 v4.26.6
|
||||||
github.com/spf13/afero v1.15.0
|
github.com/spf13/afero v1.15.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
|
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
|
||||||
go.etcd.io/bbolt v1.4.3
|
go.etcd.io/bbolt v1.5.0
|
||||||
golang.org/x/crypto v0.53.0
|
golang.org/x/crypto v0.53.0
|
||||||
golang.org/x/image v0.42.0
|
golang.org/x/image v0.43.0
|
||||||
golang.org/x/text v0.38.0
|
golang.org/x/text v0.38.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
|
@ -35,7 +35,7 @@ require (
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
github.com/andybalholm/brotli v1.2.2 // indirect
|
||||||
github.com/asticode/go-astikit v0.59.0 // indirect
|
github.com/asticode/go-astikit v0.59.0 // indirect
|
||||||
github.com/asticode/go-astits v1.15.0 // indirect
|
github.com/asticode/go-astits v1.15.0 // indirect
|
||||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||||
|
|
@ -52,16 +52,16 @@ require (
|
||||||
github.com/go-errors/errors v1.5.1 // indirect
|
github.com/go-errors/errors v1.5.1 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/golang/geo v0.0.0-20260612074446-f1a45663b0f3 // indirect
|
github.com/golang/geo v0.0.0-20260625163123-7c0e84413537 // indirect
|
||||||
github.com/golang/snappy v1.0.0 // indirect
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.6 // indirect
|
github.com/klauspost/compress v1.19.0 // indirect
|
||||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||||
github.com/minio/minlz v1.1.1 // indirect
|
github.com/minio/minlz v1.1.1 // indirect
|
||||||
github.com/nwaples/rardecode/v2 v2.2.3 // indirect
|
github.com/nwaples/rardecode/v2 v2.2.5 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
|
github.com/pelletier/go-toml/v2 v2.4.2 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.27 // indirect
|
github.com/pierrec/lz4/v4 v4.1.27 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
|
|
|
||||||
44
go.sum
44
go.sum
|
|
@ -4,16 +4,16 @@ github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
|
||||||
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
|
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
|
||||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
|
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
|
||||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
|
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
|
||||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
|
||||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
|
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
|
||||||
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
|
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
|
||||||
github.com/asticode/go-astikit v0.20.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
github.com/asticode/go-astikit v0.20.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||||
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||||
github.com/asticode/go-astikit v0.59.0 h1:tjbwDym+MTSxqkAhJoHRZmHMXK6Jv4vGx+97FptKH6k=
|
github.com/asticode/go-astikit v0.59.0 h1:tjbwDym+MTSxqkAhJoHRZmHMXK6Jv4vGx+97FptKH6k=
|
||||||
github.com/asticode/go-astikit v0.59.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
|
github.com/asticode/go-astikit v0.59.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
|
||||||
github.com/asticode/go-astisub v0.40.0 h1:Z8tAXHngjkh/4L9zqt49ru9UdpDTqBIe+80xexKM3/I=
|
github.com/asticode/go-astisub v0.41.0 h1:XFPIreVnpi9nPNVRmTdRuq4fr9XzGhgCRwpAaccnShM=
|
||||||
github.com/asticode/go-astisub v0.40.0/go.mod h1:WTkuSzFB+Bp7wezuSf2Oxulj5A8zu2zLRVFf6bIFQK8=
|
github.com/asticode/go-astisub v0.41.0/go.mod h1:WTkuSzFB+Bp7wezuSf2Oxulj5A8zu2zLRVFf6bIFQK8=
|
||||||
github.com/asticode/go-astits v1.8.0/go.mod h1:DkOWmBNQpnr9mv24KfZjq4JawCFX1FCqjLVGvO0DygQ=
|
github.com/asticode/go-astits v1.8.0/go.mod h1:DkOWmBNQpnr9mv24KfZjq4JawCFX1FCqjLVGvO0DygQ=
|
||||||
github.com/asticode/go-astits v1.15.0 h1:yRyCiUc8Jj4F7clt2GDxHghMpWuFL5rkaLuGUd2/0J4=
|
github.com/asticode/go-astits v1.15.0 h1:yRyCiUc8Jj4F7clt2GDxHghMpWuFL5rkaLuGUd2/0J4=
|
||||||
github.com/asticode/go-astits v1.15.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
github.com/asticode/go-astits v1.15.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||||
|
|
@ -82,8 +82,8 @@ github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs
|
||||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||||
github.com/golang/geo v0.0.0-20260612074446-f1a45663b0f3 h1:UlucSQUu9SZdDRlMWv5N/T3Rig9gv615vWvHFKrXHWA=
|
github.com/golang/geo v0.0.0-20260625163123-7c0e84413537 h1:KeIaDS/+VEy/bhDYjG3Z78dOyLAU4HXcVxmd0WYHJTE=
|
||||||
github.com/golang/geo v0.0.0-20260612074446-f1a45663b0f3/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
|
github.com/golang/geo v0.0.0-20260625163123-7c0e84413537/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
|
@ -101,13 +101,13 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY=
|
github.com/jellydator/ttlcache/v3 v3.4.1 h1:bOdXmXiycyK6E6Qjyuj5vl+/vU3SCOoDs8a86NbHjAQ=
|
||||||
github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4=
|
github.com/jellydator/ttlcache/v3 v3.4.1/go.mod h1:j7LO12PNghFg5+0v9budMAT4rDK4JY969jb9vOdOBBk=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
|
||||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE=
|
github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE=
|
||||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
|
|
@ -134,10 +134,10 @@ github.com/minio/minlz v1.1.1 h1:OGmft1V6AnI/Wme332U6bhG54nxEan+VFgkD7lat4KM=
|
||||||
github.com/minio/minlz v1.1.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
|
github.com/minio/minlz v1.1.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/nwaples/rardecode/v2 v2.2.3 h1:qaVuy3ChZDbAQZshPLjHeNJKF3Cru8uo9jmgveKIy2A=
|
github.com/nwaples/rardecode/v2 v2.2.5 h1:L5doqgGfQwI7qADJMqnkrSB86rpPsqQDrHeO0HWa5JY=
|
||||||
github.com/nwaples/rardecode/v2 v2.2.3/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
github.com/nwaples/rardecode/v2 v2.2.5/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
||||||
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
|
github.com/pelletier/go-toml/v2 v2.4.2 h1:M2fKKbmyvI+hGId/D0W64qDBMVhJnNR10O5gIbMc//Q=
|
||||||
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.4.2/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
|
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
|
||||||
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||||
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||||
|
|
@ -146,8 +146,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/redis/go-redis/v9 v9.20.1 h1:sfCU6A8P3dXbKyWes02uxA2baehGux9dZHfEKtsTB1w=
|
github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
|
||||||
github.com/redis/go-redis/v9 v9.20.1/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
|
|
@ -156,8 +156,8 @@ github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88ee
|
||||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||||
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
||||||
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||||
github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM=
|
github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs=
|
||||||
github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||||
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
|
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
|
||||||
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
|
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
|
|
@ -201,8 +201,8 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ
|
||||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
|
||||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
|
@ -216,8 +216,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.42.0 h1:1gSs6ehNWXLbkHBIPcWztk3D/6aIA/8hauiAYtlodVY=
|
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
|
||||||
golang.org/x/image v0.42.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
|
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,9 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
|
||||||
|
|
||||||
// Directories creation on POST.
|
// Directories creation on POST.
|
||||||
if strings.HasSuffix(r.URL.Path, "/") {
|
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
|
return errToStatus(err), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,10 @@ The Hook Authentication method in FileBrowser allows developers to delegate user
|
||||||
|
|
||||||
The hook’s output controls user permissions, scope, locale, and other attributes, making it a powerful and extensible authentication mechanism.
|
The hook’s output controls user permissions, scope, locale, and other attributes, making it a powerful and extensible authentication mechanism.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
>
|
||||||
|
> The submitted username and password are attacker-controlled and are handed to your hook command as the `USERNAME` and `PASSWORD` environment variables. File Browser runs the command directly, without a shell, so the values themselves are inert. However, your script must treat them as untrusted: always quote them (`"$USERNAME"`, `"$PASSWORD"`) and never pass them unquoted to a shell, `eval`, `bash -c`, command substitution, or backticks. A hook script that shell-evaluates these values turns any login request into remote code execution.
|
||||||
|
|
||||||
For example, the following code delegates filebrowser authentication to a PowerShell script on Windows. You can configure any command (for example, a script in Python, Node.js, etc.).
|
For example, the following code delegates filebrowser authentication to a PowerShell script on Windows. You can configure any command (for example, a script in Python, Node.js, etc.).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue