Compare commits

...
4 Commits
Author SHA1 Message Date
Graham Steffaniak 5597b28f10 update anonymous user access 2026-05-13 20:03:23 -05:00
Graham Steffaniak 6bfc397419 Fix inline svg attack (#2376) 2026-05-01 13:49:55 -05:00
Graham Steffaniak 112740bdd4 security bugfix (#2374) 2026-05-01 13:38:44 -05:00
Graham Steffaniak 9117fb2b5f fix context menu (#2373) 2026-05-01 10:05:04 -05:00
37 changed files with 216 additions and 102 deletions

No files matched your search

+9
View File
@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
## v1.3.9
**Security**:
- [Critical] Unauthenticated Path Traversal in Public Share Delete Allows Arbitrary File Deletion GHSA-fwj3-42wh-8673
- [Moderate] Stored XSS via SVG File in Public Share (Missing CSP Header) GHSA-mmpx-jh39-wrv6
**BugFixes**:
- Fix context menu items and adjusted when items show to more accurately reflect permissions.
## v1.3.8
**BugFixes**:
+1
View File
@@ -15,3 +15,4 @@ userDefaults:
permissions:
download: true
create: true
share: true
+14 -10
View File
@@ -101,15 +101,6 @@ func ApplyUserDefaults(u *users.User) {
u.PreferEditorForMarkdown = d.PreferEditorForMarkdown
u.FileLoading = d.FileLoading
u.Permissions.Api = d.Permissions.Api
u.Permissions.Admin = d.Permissions.Admin
u.Permissions.Modify = d.Permissions.Modify
u.Permissions.Share = d.Permissions.Share
u.Permissions.Realtime = d.Permissions.Realtime
u.Permissions.Delete = d.Permissions.Delete
u.Permissions.Create = d.Permissions.Create
u.Permissions.Download = boolValueOrDefault(d.Permissions.Download, true)
u.Preview.DisableHideSidebar = d.Preview.DisableHideSidebar
u.Preview.Image = boolValueOrDefault(d.Preview.Image, true)
u.Preview.Video = boolValueOrDefault(d.Preview.Video, true)
@@ -122,11 +113,24 @@ func ApplyUserDefaults(u *users.User) {
u.Preview.Folder = boolValueOrDefault(d.Preview.Folder, true)
u.Preview.Models = boolValueOrDefault(d.Preview.Models, true)
if u.Username == "anonymous" {
return
}
u.Permissions.Api = d.Permissions.Api
u.Permissions.Admin = d.Permissions.Admin
u.Permissions.Modify = d.Permissions.Modify
u.Permissions.Share = d.Permissions.Share
u.Permissions.Realtime = d.Permissions.Realtime
u.Permissions.Delete = d.Permissions.Delete
u.Permissions.Create = d.Permissions.Create
u.Permissions.Download = boolValueOrDefault(d.Permissions.Download, true)
if u.LoginMethod == "" && d.LoginMethod != "" {
u.LoginMethod = users.LoginMethod(d.LoginMethod)
}
if len(u.Scopes) == 0 && u.Username != "anonymous" {
if len(u.Scopes) == 0 {
for _, source := range Config.Server.Sources {
if source.Config.DefaultEnabled {
u.Scopes = append(u.Scopes, users.SourceScope{
+2
View File
@@ -99,6 +99,8 @@ func setContentDisposition(w http.ResponseWriter, r *http.Request, fileName stri
dispositionType := "attachment"
if r.URL.Query().Get("inline") == "true" {
dispositionType = "inline"
// Inline SVG (and similar) can execute embedded scripts when opened as a top-level document; match upstream filebrowser mitigation.
w.Header().Set("Content-Security-Policy", "script-src 'none'")
}
// standard: ASCII-only safe fallback
+1 -1
View File
@@ -150,7 +150,7 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
// ========================================
api.HandleFunc("GET /share/list", withPermShare(shareListHandler))
api.HandleFunc("GET /share/direct", withPermShare(shareDirectDownloadHandler))
api.HandleFunc("GET /share", withPermShare(shareGetHandler))
api.HandleFunc("GET /share", withUser(shareGetHandler))
api.HandleFunc("POST /share", withPermShare(sharePostHandler))
api.HandleFunc("PATCH /share", withPermShare(sharePatchHandler))
api.HandleFunc("DELETE /share", withPermShare(shareDeleteHandler))
+5 -1
View File
@@ -53,7 +53,11 @@ type handleFunc func(w http.ResponseWriter, r *http.Request, data *requestContex
func withHashFileHelper(fn handleFunc) handleFunc {
return withOrWithoutUserHelper(func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
hash := r.URL.Query().Get("hash")
path := r.URL.Query().Get("path")
inputPath := r.URL.Query().Get("path")
path, err := utils.SanitizeUserPath(inputPath)
if err != nil && inputPath != "" {
return http.StatusBadRequest, err
}
// Get the file link by hash
link, err := store.Share.GetByHash(hash)
+23 -13
View File
@@ -251,25 +251,35 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
// Process each item one at a time
for _, item := range items {
rawPath := item.Path
// Validate item
if item.Path == "" {
if rawPath == "" {
response.Failed = append(response.Failed, BulkDeleteItem{
Source: item.Source,
Path: item.Path,
Path: rawPath,
Message: "path was empty",
})
continue
}
// Prevent deletion of root
if item.Path == "/" {
if rawPath == "/" {
response.Failed = append(response.Failed, BulkDeleteItem{
Source: item.Source,
Path: item.Path,
Path: rawPath,
Message: "cannot delete root directory",
})
continue
}
sanitizedPath, err := utils.SanitizeUserPath(rawPath)
if err != nil {
response.Failed = append(response.Failed, BulkDeleteItem{
Source: item.Source,
Path: sanitizedPath,
Message: err.Error(),
})
continue
}
if d.share != nil {
sourceName := d.share.GetSourceName()
@@ -281,13 +291,13 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
if err != nil {
response.Failed = append(response.Failed, BulkDeleteItem{
Source: item.Source,
Path: item.Path,
Path: sanitizedPath,
Message: "user does not have access",
})
continue
}
withoutUserScope := strings.TrimPrefix(d.share.Path, userScope)
indexPath := utils.JoinPathAsUnix(withoutUserScope, item.Path)
indexPath := utils.JoinPathAsUnix(withoutUserScope, sanitizedPath)
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
FollowSymlinks: true,
@@ -305,7 +315,7 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
logger.Errorf("resource bulk delete handler: error deleting file/directory: %v", err)
response.Failed = append(response.Failed, BulkDeleteItem{
Source: item.Source,
Path: item.Path,
Path: sanitizedPath,
Message: "error deleting file/directory, admin must check the logs",
})
continue
@@ -317,7 +327,7 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
if item.Source == "" {
response.Failed = append(response.Failed, BulkDeleteItem{
Source: item.Source,
Path: item.Path,
Path: sanitizedPath,
Message: "source was empty, source is required",
})
continue
@@ -328,7 +338,7 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
if err != nil {
response.Failed = append(response.Failed, BulkDeleteItem{
Source: item.Source,
Path: item.Path,
Path: sanitizedPath,
Message: fmt.Sprintf("user does not have access: %v", err),
})
continue
@@ -338,7 +348,7 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
if idx == nil {
response.Failed = append(response.Failed, BulkDeleteItem{
Source: item.Source,
Path: item.Path,
Path: sanitizedPath,
Message: "source not found",
})
continue
@@ -347,14 +357,14 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
// Get file info
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
FollowSymlinks: true,
Path: item.Path,
Path: sanitizedPath,
Source: item.Source,
ShowHidden: true,
}, store.Access, filePermUser, store.Share)
if err != nil {
response.Failed = append(response.Failed, BulkDeleteItem{
Source: item.Source,
Path: item.Path,
Path: sanitizedPath,
Message: err.Error(),
})
continue
@@ -363,7 +373,7 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
if err != nil {
response.Failed = append(response.Failed, BulkDeleteItem{
Source: item.Source,
Path: item.Path,
Path: sanitizedPath,
Message: err.Error(),
})
continue
+94 -49
View File
@@ -14,11 +14,23 @@
:class="{ 'dark-mode': isDarkMode, 'centered': centered }"
:key="showCreate ? 'create-mode' : 'normal-mode'"
>
<div v-if="!showLimitedOptions" class="context-menu-header">
<div
v-if="showCreateToggle"
class="context-menu-header"
>
<div
class="action button clickable"
v-if="showCreateButton"
@click="toggleShowCreate"
v-if="showCreateToggle"
class="action button clickable context-menu-create-toggle"
aria-label="Create Actions Toggle"
:class="{ 'context-menu-create-toggle--disabled': createToggleDisabled }"
role="button"
:aria-disabled="createToggleDisabled ? 'true' : 'false'"
:tabindex="createToggleDisabled ? -1 : 0"
@click="onCreateToggleClick"
@keydown.enter.prevent="onCreateToggleClick"
@keydown.space.prevent="onCreateToggleClick"
@mouseenter="onCreateToggleMouseEnter"
@mouseleave="hideTooltip"
>
<i v-if="!showCreate" class="material-symbols">add</i>
<i v-else class="material-symbols">arrow_back</i>
@@ -35,23 +47,29 @@
</div>
<hr v-if="showDivider" class="divider">
<action
v-if="showCreateFiles"
v-if="showCreateFileActions"
icon="create_new_folder"
:label="$t('files.newFolder')"
@action="showNewDirPrompt"
/>
<action
v-if="showCreateFiles"
v-if="showCreateFileActions"
icon="note_add"
:label="$t('files.newFile')"
@action="showPrompt('newFile')"
/>
<action
v-if="showCreateFiles"
v-if="showCreateFileActions"
icon="file_upload"
:label="$t('general.upload')"
@action="showUploadPrompt"
/>
<action
v-if="showArchive"
icon="archive"
:label="$t('prompts.archive')"
@action="showArchivePrompt"
/>
<action
v-if="showInfo"
icon="info"
@@ -65,12 +83,6 @@
:label="$t('general.download')"
@action="startDownload"
/>
<action
v-if="showArchive"
icon="archive"
:label="$t('prompts.archive')"
@action="showArchivePrompt"
/>
<action
v-if="showUnarchive"
icon="unarchive"
@@ -229,7 +241,10 @@ export default {
computed: {
// Either from prop or from state
providedItems() {
if (this.items) return this.items;
// Only treat non-empty explicit `items` as overriding selection ([] means "use global state")
if (Array.isArray(this.items) && this.items.length > 0) {
return this.items;
}
// Fallback to global selection (indices or objects)
if (state.selected.length === 0) return [];
// Map to actual items from state.req
@@ -253,43 +268,60 @@ export default {
permissions() {
return getters.permissions();
},
/**
* Whether the (+) panel can be opened (create file ops and/or admin-only rows like access rules).
*/
canOpenCreatePanel() {
if (getters.isShare()) {
return !!getters.permissions().create;
}
return getters.isAdmin() || !!state.user?.permissions?.create;
},
/** New folder / new file / upload — requires permissions.create only (not admin alone). */
showCreateFileActions() {
if (this.showLimitedOptions) return false;
if (!this.permissions.create) return false;
return this.showCreate && !this.isSearchActive;
},
req() {
return state.req;
},
isShare() {
return getters.isShare();
},
showCreateFiles() {
if (this.showLimitedOptions) return false;
return getters.permissions().create;
},
showCreateActions() {
if (this.showLimitedOptions) return false;
return this.showCreate && !this.isSearchActive;
},
showInfo() {
if (this.showLimitedOptions) return this.selectedCount == 1;
return !this.showCreate && this.selectedCount == 1;
},
showDownload() {
if (this.showLimitedOptions) return false;
return !this.showCreate && this.permissions.download && this.selectedCount > 0;
return (
!this.showCreate &&
!!this.permissions.download &&
this.selectedCount > 0
);
},
showArchive() {
if (this.showLimitedOptions || getters.isShare()) return false;
if (!this.permissions.create) return false;
return !this.showCreate && this.selectedCount > 0 && !this.showUnarchive;
return (
this.showCreate &&
this.selectedCount > 0 &&
!this.showUnarchive &&
!this.isSearchActive
);
},
showUnarchive() {
if (this.showLimitedOptions || getters.isShare()) return false;
if (!this.permissions.create) return false;
if (this.showCreate) return false;
if (this.selectedCount !== 1) return false;
const item = this.firstSelected;
return item && isArchivePath(item.path || item.from || item.name);
},
showShareAction() {
if (this.showLimitedOptions) return false;
return (this.showCreate || this.selectedCount == 1) && this.showShare;
return this.selectedCount <= 1 && this.showShare;
},
showRename() {
if (this.showLimitedOptions) return false;
@@ -314,16 +346,17 @@ export default {
if (this.showLimitedOptions) return false;
return !this.showCreate && !this.isSearchActive && this.req?.items?.length > 0;
},
showCreateButton() {
/** + control in listing / share directory context; disabled when the panel cannot be opened. */
showCreateToggle() {
if (this.showLimitedOptions || this.createOnly) return false;
return !this.isSearchActive && this.permissions.create && !this.isShare;
return !this.isSearchActive;
},
createToggleDisabled() {
return !this.canOpenCreatePanel;
},
showDivider() {
if (this.showLimitedOptions || this.createOnly) return false;
if (getters.isShare()) {
return state.shareInfo?.allowCreate
}
return state.user?.permissions?.create || state.user?.permissions?.share || state.user?.permissions?.admin;
return this.showCreateToggle || this.selectedCount > 0;
},
showSelectMultiple() {
if (this.showLimitedOptions) return false;
@@ -363,11 +396,14 @@ export default {
return false;
}
const cv = getters.currentView();
const showDelete = cv != "settings" && !this.isSearchActive && this.permissions.delete;
return showDelete;
return (
cv != "settings" &&
!this.isSearchActive &&
!!this.permissions.delete
);
},
hasDownload() {
return this.selectedCount > 0 && this.permissions.download;
return this.selectedCount > 0 && !!this.permissions.download;
},
isPreview() {
return getters.isPreviewView();
@@ -384,7 +420,10 @@ export default {
if (getters.isShare()) {
return false;
}
return this.permissions.admin && this.showCreate;
if (this.selectedCount > 1) {
return false;
}
return getters.isAdmin() && this.showCreate;
},
showShare() {
if (getters.isShare()) {
@@ -480,8 +519,16 @@ export default {
y: event.clientY,
});
},
onCreateToggleClick() {
if (this.createToggleDisabled) return;
this.toggleShowCreate();
},
onCreateToggleMouseEnter(event) {
if (!this.createToggleDisabled) return;
this.showTooltip(event, this.$t("messages.noCreateOptions"));
},
toggleShowCreate() {
if (!this.permissions.create) {
if (!this.canOpenCreatePanel) {
this.showCreate = false;
return;
}
@@ -492,11 +539,9 @@ export default {
},
showAccessPrompt() {
mutations.closeHovers();
let sourceName = this.firstSelected?.source || state.req.source;
let path = this.firstSelected?.path || state.req.path;
if (this.firstSelected && !this.firstSelected.isDir) {
path = url.removeLastDir(path) + '/';
}
const item = this.firstSelected;
let sourceName = item?.source || state.req.source;
let path = item?.path || state.req.path;
mutations.showPrompt({
name: "access",
props: {
@@ -568,7 +613,7 @@ export default {
}, 300);
},
startShowCreate() {
if (!this.permissions.create) {
if (!this.canOpenCreatePanel) {
return;
}
this.showCreate = true;
@@ -613,17 +658,12 @@ export default {
this.posY = contextProps.posY;
},
initializeCreateState() {
// If createOnly is set, always show create actions
if (this.createOnly) {
this.showCreate = true;
return;
}
// Only set initial showCreate state, don't override user choices
if (this.selectedCount > 0 || !this.permissions.create) {
this.showCreate = false;
} else {
this.showCreate = true;
}
// Right-click / prompt: start in normal mode; user opens create via + or sidebar (createOnly).
this.showCreate = false;
},
toggleMultipleSelection() {
mutations.setMultiple(true);
@@ -853,6 +893,11 @@ export default {
padding: 0.25em;
}
.context-menu-create-toggle--disabled {
opacity: 0.45;
cursor: not-allowed;
}
#context-menu .action {
display: flex;
align-items: center;
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "إعادة تعيين المسار",
"pathReassigned": "تم تحديث المسار بنجاح!",
"pathReassignFailed": "فشل تحديث المسار. يرجى المحاولة مرة أخرى.",
"incompleteResultsDetails": "عرض أقصى النتائج ({max}). حاول تضييق نطاق البحث للحصول على المزيد من النتائج تحت {max}."
"incompleteResultsDetails": "عرض أقصى النتائج ({max}). حاول تضييق نطاق البحث للحصول على المزيد من النتائج تحت {max}.",
"noCreateOptions": "لا توجد خيارات إنشاء متاحة"
},
"duplicateFinder": {
"title": "أداة البحث عن الملفات المكررة",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "Změna přiřazení cesty",
"pathReassigned": "Cesta byla úspěšně aktualizována!",
"pathReassignFailed": "Nepodařilo se aktualizovat cestu. Zkuste to prosím znovu.",
"incompleteResultsDetails": "Zobrazeno max. výsledků ({max}). Zkuste zúžit hledání a získat více výsledků pod {max}."
"incompleteResultsDetails": "Zobrazeno max. výsledků ({max}). Zkuste zúžit hledání a získat více výsledků pod {max}.",
"noCreateOptions": "Nejsou k dispozici žádné možnosti vytváření"
},
"duplicateFinder": {
"title": "Vyhledávač duplicitních souborů",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "Pfad neu zuweisen",
"pathReassigned": "Pfad erfolgreich aktualisiert!",
"pathReassignFailed": "Der Pfad konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut.",
"incompleteResultsDetails": "Zeigt max Ergebnisse ({max}). Versuchen Sie, Ihre Suche einzugrenzen, um mehr Ergebnisse unter {max} zu erhalten."
"incompleteResultsDetails": "Zeigt max Ergebnisse ({max}). Versuchen Sie, Ihre Suche einzugrenzen, um mehr Ergebnisse unter {max} zu erhalten.",
"noCreateOptions": "Es sind keine Erstellungsoptionen verfügbar"
},
"duplicateFinder": {
"title": "Duplicate File Finder",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "Επαναπροσδιορισμός διαδρομής",
"pathReassigned": "Η διαδρομή ενημερώθηκε επιτυχώς!",
"pathReassignFailed": "Απέτυχε η ενημέρωση της διαδρομής. Προσπαθήστε ξανά.",
"incompleteResultsDetails": "Εμφάνιση μέγιστων αποτελεσμάτων ({max}). Προσπαθήστε να περιορίσετε την αναζήτησή σας για να πάρετε περισσότερα αποτελέσματα κάτω από {max}."
"incompleteResultsDetails": "Εμφάνιση μέγιστων αποτελεσμάτων ({max}). Προσπαθήστε να περιορίσετε την αναζήτησή σας για να πάρετε περισσότερα αποτελέσματα κάτω από {max}.",
"noCreateOptions": "Δεν υπάρχουν διαθέσιμες επιλογές δημιουργίας"
},
"duplicateFinder": {
"title": "Αναζήτηση διπλών αρχείων",
+2 -1
View File
@@ -5,7 +5,8 @@
"reassignPath": "Reassign Path",
"pathReassigned": "Path successfully updated!",
"pathReassignFailed": "Failed to update path. Please try again.",
"incompleteResultsDetails": "Showing max results ({max}). Try narrowing your search to get more results under {max}."
"incompleteResultsDetails": "Showing max results ({max}). Try narrowing your search to get more results under {max}.",
"noCreateOptions": "No create options are available"
},
"general": {
"divider": "Divider{suffix}",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "Reasignar ruta",
"pathReassigned": "¡La ruta se actualizó correctamente!",
"pathReassignFailed": "No se pudo actualizar la ruta. Inténtalo de nuevo",
"incompleteResultsDetails": "Mostrando resultados máximos ({max}). Intenta restringir la búsqueda para obtener más resultados en {max}."
"incompleteResultsDetails": "Mostrando resultados máximos ({max}). Intenta restringir la búsqueda para obtener más resultados en {max}.",
"noCreateOptions": "No hay opciones de creación disponibles"
},
"duplicateFinder": {
"title": "Buscador de archivos duplicados",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "Réaffecter le chemin",
"pathReassigned": "Mise à jour du chemin d'accès réussie !",
"pathReassignFailed": "Échec de la mise à jour du chemin d'accès. Veuillez réessayer.",
"incompleteResultsDetails": "Résultats maximums ({max}). Essayez d'affiner votre recherche pour obtenir plus de résultats sous {max}."
"incompleteResultsDetails": "Résultats maximums ({max}). Essayez d'affiner votre recherche pour obtenir plus de résultats sous {max}.",
"noCreateOptions": "Aucune option de création n'est disponible"
},
"duplicateFinder": {
"title": "Recherche de fichiers en double",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "הקצאת נתיב מחדש",
"pathReassigned": "הנתיב עודכן בהצלחה!",
"pathReassignFailed": "לא ניתן לעדכן את הנתיב. אנא נסה שוב.",
"incompleteResultsDetails": "הצגת תוצאות מקסימליות ({max}). נסה לצמצם את החיפוש כדי לקבל תוצאות נוספות תחת {max}."
"incompleteResultsDetails": "הצגת תוצאות מקסימליות ({max}). נסה לצמצם את החיפוש כדי לקבל תוצאות נוספות תחת {max}.",
"noCreateOptions": "אין אפשרויות יצירה זמינות"
},
"duplicateFinder": {
"title": "מאתר קבצים כפולים",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "Útvonal újbóli kijelölése",
"pathReassigned": "Az útvonal sikeresen frissült!",
"pathReassignFailed": "Nem sikerült frissíteni az elérési utat. Kérjük, próbálja újra.",
"incompleteResultsDetails": "Maximális eredmények megjelenítése ({max}). Próbálja szűkíteni a keresést, hogy több eredményt kapjon a {max} alatt ."
"incompleteResultsDetails": "Maximális eredmények megjelenítése ({max}). Próbálja szűkíteni a keresést, hogy több eredményt kapjon a {max} alatt .",
"noCreateOptions": "Nincs lehetőség létrehozására"
},
"duplicateFinder": {
"title": "Duplikátum fájl kereső",
+2 -1
View File
@@ -5,7 +5,8 @@
"reassignPath": "Riassegna percorso",
"pathReassigned": "Percorso aggiornato con successo!",
"pathReassignFailed": "Aggiornamento del percorso fallito. Riprova.",
"incompleteResultsDetails": "Visualizzazione dei risultati massimi ({max}). Prova a restringere la ricerca per ottenere più risultati sotto {max}."
"incompleteResultsDetails": "Visualizzazione dei risultati massimi ({max}). Prova a restringere la ricerca per ottenere più risultati sotto {max}.",
"noCreateOptions": "Non sono disponibili opzioni di creazione"
},
"general": {
"divider": "Divisore{suffix}",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "パスの再割り当て",
"pathReassigned": "パスは正常に更新されました!",
"pathReassignFailed": "パスの更新に失敗しました。再試行してください。",
"incompleteResultsDetails": "最大結果を表示 ({max}).{max} でより多くの結果を得るために絞り込んで検索してみてください。"
"incompleteResultsDetails": "最大結果を表示 ({max}).{max} でより多くの結果を得るために絞り込んで検索してみてください。",
"noCreateOptions": "作成オプションはありません"
},
"duplicateFinder": {
"title": "重複ファイルファインダー",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "경로 재할당",
"pathReassigned": "경로가 성공적으로 업데이트되었습니다!",
"pathReassignFailed": "경로를 업데이트하지 못했습니다. 다시 시도하세요.",
"incompleteResultsDetails": "최대 결과 표시 ({max}). 검색 범위를 좁히면 {max} 에서 더 많은 결과를 확인할 수 있습니다."
"incompleteResultsDetails": "최대 결과 표시 ({max}). 검색 범위를 좁히면 {max} 에서 더 많은 결과를 확인할 수 있습니다.",
"noCreateOptions": "사용 가능한 생성 옵션이 없습니다."
},
"duplicateFinder": {
"title": "중복 파일 찾기",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "Pad opnieuw toewijzen",
"pathReassigned": "Pad succesvol bijgewerkt!",
"pathReassignFailed": "Het bijwerken van het pad is mislukt. Probeer het opnieuw.",
"incompleteResultsDetails": "Max resultaten ({max}). Probeer uw zoekopdracht te verfijnen voor meer resultaten onder {max}."
"incompleteResultsDetails": "Max resultaten ({max}). Probeer uw zoekopdracht te verfijnen voor meer resultaten onder {max}.",
"noCreateOptions": "Er zijn geen opties beschikbaar"
},
"duplicateFinder": {
"title": "Duplicaat bestand zoeken",
+2 -1
View File
@@ -5,7 +5,8 @@
"reassignPath": "Pad opnieuw toewijzen",
"pathReassigned": "Pad succesvol bijgewerkt!",
"pathReassignFailed": "Het bijwerken van het pad is mislukt. Probeer het opnieuw.",
"incompleteResultsDetails": "Max. resultaten weergeven({max}). Probeer de zoekopdracht aan te passen voor meer resultaten onder {max}."
"incompleteResultsDetails": "Max. resultaten weergeven({max}). Probeer de zoekopdracht aan te passen voor meer resultaten onder {max}.",
"noCreateOptions": "Er zijn geen opties beschikbaar"
},
"general": {
"divider": "Scheiding{suffix}",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "Ponowne przypisanie ścieżki",
"pathReassigned": "Ścieżka została pomyślnie zaktualizowana!",
"pathReassignFailed": "Nie udało się zaktualizować ścieżki. Spróbuj ponownie.",
"incompleteResultsDetails": "Wyświetlanie maksymalnych wyników ({max}). Spróbuj zawęzić wyszukiwanie, aby uzyskać więcej wyników pod {max}."
"incompleteResultsDetails": "Wyświetlanie maksymalnych wyników ({max}). Spróbuj zawęzić wyszukiwanie, aby uzyskać więcej wyników pod {max}.",
"noCreateOptions": "Opcje tworzenia nie są dostępne"
},
"duplicateFinder": {
"title": "Wyszukiwarka duplikatów plików",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "Reatribuir caminho",
"pathReassigned": "Caminho atualizado com sucesso!",
"pathReassignFailed": "Falha ao atualizar o caminho. Tente novamente.",
"incompleteResultsDetails": "Mostrando o máximo de resultados ({max}). Tente restringir sua pesquisa para obter mais resultados em {max}."
"incompleteResultsDetails": "Mostrando o máximo de resultados ({max}). Tente restringir sua pesquisa para obter mais resultados em {max}.",
"noCreateOptions": "Nenhuma opção de criação está disponível"
},
"duplicateFinder": {
"title": "Localizador de arquivos duplicados",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "Reatribuir trajetória",
"pathReassigned": "Caminho atualizado com sucesso!",
"pathReassignFailed": "Falha ao atualizar o caminho. Por favor, tente novamente.",
"incompleteResultsDetails": "Mostrando resultados máximos ({max}). Tente restringir a sua pesquisa para obter mais resultados em {max}."
"incompleteResultsDetails": "Mostrando resultados máximos ({max}). Tente restringir a sua pesquisa para obter mais resultados em {max}.",
"noCreateOptions": "Não estão disponíveis opções de criação"
},
"duplicateFinder": {
"title": "Localizador de ficheiros duplicados",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "Reatribuiți calea",
"pathReassigned": "Calea a fost actualizată cu succes!",
"pathReassignFailed": "A eșuat actualizarea căii. Vă rugăm să încercați din nou.",
"incompleteResultsDetails": "Afișarea rezultatelor maxime ({max}). Încercați să restrângeți căutarea pentru a obține mai multe rezultate la {max}."
"incompleteResultsDetails": "Afișarea rezultatelor maxime ({max}). Încercați să restrângeți căutarea pentru a obține mai multe rezultate la {max}.",
"noCreateOptions": "Nu sunt disponibile opțiuni de creare"
},
"duplicateFinder": {
"title": "Duplicate fișier Finder",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "Переназначить путь",
"pathReassigned": "Путь успешно обновлен!",
"pathReassignFailed": "Не удалось обновить путь. Пожалуйста, попробуйте еще раз.",
"incompleteResultsDetails": "Показано максимум результатов ({max}). Попробуйте сузить поиск, чтобы получить больше результатов по запросу {max}."
"incompleteResultsDetails": "Показано максимум результатов ({max}). Попробуйте сузить поиск, чтобы получить больше результатов по запросу {max}.",
"noCreateOptions": "Опции создания недоступны"
},
"duplicateFinder": {
"title": "Поиск дубликатов файлов",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "Opätovné priradenie cesty",
"pathReassigned": "Cesta bola úspešne aktualizovaná!",
"pathReassignFailed": "Nepodarilo sa aktualizovať cestu. Skúste to prosím znova.",
"incompleteResultsDetails": "Zobrazené maximálne výsledky ({max}). Skúste zúžiť vyhľadávanie, aby ste získali viac výsledkov pod {max}."
"incompleteResultsDetails": "Zobrazené maximálne výsledky ({max}). Skúste zúžiť vyhľadávanie, aby ste získali viac výsledkov pod {max}.",
"noCreateOptions": "Nie sú k dispozícii žiadne možnosti vytvárania"
},
"duplicateFinder": {
"title": "Vyhľadávač duplicitných súborov",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "Omfördela sökväg",
"pathReassigned": "Sökvägen har uppdaterats!",
"pathReassignFailed": "Misslyckades med att uppdatera sökvägen. Vänligen försök igen.",
"incompleteResultsDetails": "Visar max resultat ({max}). Försök att begränsa din sökning för att få fler resultat under {max}."
"incompleteResultsDetails": "Visar max resultat ({max}). Försök att begränsa din sökning för att få fler resultat under {max}.",
"noCreateOptions": "Inga alternativ för skapande är tillgängliga"
},
"duplicateFinder": {
"title": "Hitta duplicerade filer",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "Yolu Yeniden Atayın",
"pathReassigned": "Yol başarıyla güncellendi!",
"pathReassignFailed": "Yol güncellenemedi. Lütfen tekrar deneyin.",
"incompleteResultsDetails": "Maksimum sonuç gösteriliyor ({max}). {max} altında daha fazla sonuç almak için aramanızı daraltmayı deneyin."
"incompleteResultsDetails": "Maksimum sonuç gösteriliyor ({max}). {max} altında daha fazla sonuç almak için aramanızı daraltmayı deneyin.",
"noCreateOptions": "Oluşturma seçeneği mevcut değil"
},
"duplicateFinder": {
"title": "Yinelenen Dosya Bulucu",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "Перепризначити шлях",
"pathReassigned": "Шлях успішно оновлено!",
"pathReassignFailed": "Не вдалося оновити шлях. Спробуйте ще раз.",
"incompleteResultsDetails": "Показано максимум результатів ({max}). Спробуйте звузити пошук, щоб отримати більше результатів за темою {max}."
"incompleteResultsDetails": "Показано максимум результатів ({max}). Спробуйте звузити пошук, щоб отримати більше результатів за темою {max}.",
"noCreateOptions": "Опції створення недоступні"
},
"duplicateFinder": {
"title": "Пошук дублікатів файлів",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "重新分配路径",
"pathReassigned": "路径更新成功!",
"pathReassignFailed": "更新路径失败。请重试。",
"incompleteResultsDetails": "显示最大结果 ({max})。请尝试缩小搜索范围,以获得{max} 下的更多结果。"
"incompleteResultsDetails": "显示最大结果 ({max})。请尝试缩小搜索范围,以获得{max} 下的更多结果。",
"noCreateOptions": "没有创建选项"
},
"duplicateFinder": {
"title": "重复文件查找器",
+2 -1
View File
@@ -866,7 +866,8 @@
"reassignPath": "重新指定路徑",
"pathReassigned": "路徑已成功更新!",
"pathReassignFailed": "更新路徑失敗。請再試一次。",
"incompleteResultsDetails": "目前只顯示前 {max} 筆結果。請縮小搜尋範圍,以取得少於 {max} 筆的結果。"
"incompleteResultsDetails": "目前只顯示前 {max} 筆結果。請縮小搜尋範圍,以取得少於 {max} 筆的結果。",
"noCreateOptions": "沒有建立選項"
},
"duplicateFinder": {
"title": "重複檔案搜尋器",
+2 -1
View File
@@ -580,7 +580,8 @@ export const getters = {
};
}
return {
share: state.user?.permissions?.share,
share:
!!(state.user?.permissions?.share || state.user?.permissions?.admin),
modify: state.user?.permissions?.modify,
create: state.user?.permissions?.create,
delete: state.user?.permissions?.delete,
+1
View File
@@ -1143,6 +1143,7 @@ export default {
showCentered: getters.isMobile(),
posX: event.clientX,
posY: event.clientY,
createOnly: this.selectedCount == 0,
},
});
},
@@ -35,7 +35,7 @@ test("info from listing - regular item", async({ page, checkForErrors, context }
await expect(page.locator('button[aria-label="Move file"]')).toBeVisible();
await expect(page.locator('button[aria-label="Copy file"]')).toBeVisible();
await expect(page.locator('button[aria-label="Select all"]')).toBeVisible();
await expect(page.locator('button[aria-label="Create archive"]')).toBeVisible();
await expect(page.locator('button[aria-label="Create archive"]')).toBeHidden();
checkForErrors();
});
@@ -44,6 +44,19 @@ test("context menu", async ({ page, checkForErrors, context, theme }) => {
return;
}
await page.screenshot({ path: `./generated/listing/right-click-${theme}.jpg`, quality: jpgQuality });
// assert options exist and don't exist
await expect(page.locator('button[aria-label="New folder"]')).toBeHidden();
await expect(page.locator('button[aria-label="New file"]')).toBeHidden();
await expect(page.locator('button[aria-label="Upload"]')).toBeHidden();
await expect(page.locator('button[aria-label="Info"]')).toBeVisible();
await expect(page.locator('button[aria-label="Download"]')).toBeVisible();
await expect(page.locator('button[aria-label="Share"]')).toBeVisible();
await expect(page.locator('button[aria-label="Delete"]')).toBeVisible();
await expect(page.locator('button[aria-label="Rename"]')).toBeVisible();
await expect(page.locator('button[aria-label="Move file"]')).toBeVisible();
await expect(page.locator('button[aria-label="Copy file"]')).toBeVisible();
await expect(page.locator('button[aria-label="Select all"]')).toBeVisible();
await expect(page.locator('button[aria-label="Create archive"]')).toBeHidden();
});
test("info from search", async ({ page, checkForErrors, context, theme }) => {
@@ -76,7 +89,6 @@ test("no viewer available", async ({ page, checkForErrors, context, theme }) =>
await page.screenshot({ path: `./generated/viewer/no-viewer-available-${theme}.jpg`, quality: jpgQuality });
})
test("copy from listing to new folder", async ({ page, checkForErrors, context, theme }) => {
if (theme === 'light') {
return;