mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-04 16:50:08 -05:00
Compare commits
3
Commits
main
...
v1.3.1-stable
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bfc397419 | ||
|
|
112740bdd4 | ||
|
|
9117fb2b5f |
No files matched your search
@@ -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**:
|
||||
|
||||
@@ -15,3 +15,4 @@ userDefaults:
|
||||
permissions:
|
||||
download: true
|
||||
create: true
|
||||
share: true
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -866,7 +866,8 @@
|
||||
"reassignPath": "إعادة تعيين المسار",
|
||||
"pathReassigned": "تم تحديث المسار بنجاح!",
|
||||
"pathReassignFailed": "فشل تحديث المسار. يرجى المحاولة مرة أخرى.",
|
||||
"incompleteResultsDetails": "عرض أقصى النتائج ({max}). حاول تضييق نطاق البحث للحصول على المزيد من النتائج تحت {max}."
|
||||
"incompleteResultsDetails": "عرض أقصى النتائج ({max}). حاول تضييق نطاق البحث للحصول على المزيد من النتائج تحت {max}.",
|
||||
"noCreateOptions": "لا توجد خيارات إنشاء متاحة"
|
||||
},
|
||||
"duplicateFinder": {
|
||||
"title": "أداة البحث عن الملفات المكررة",
|
||||
|
||||
@@ -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ů",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -866,7 +866,8 @@
|
||||
"reassignPath": "Επαναπροσδιορισμός διαδρομής",
|
||||
"pathReassigned": "Η διαδρομή ενημερώθηκε επιτυχώς!",
|
||||
"pathReassignFailed": "Απέτυχε η ενημέρωση της διαδρομής. Προσπαθήστε ξανά.",
|
||||
"incompleteResultsDetails": "Εμφάνιση μέγιστων αποτελεσμάτων ({max}). Προσπαθήστε να περιορίσετε την αναζήτησή σας για να πάρετε περισσότερα αποτελέσματα κάτω από {max}."
|
||||
"incompleteResultsDetails": "Εμφάνιση μέγιστων αποτελεσμάτων ({max}). Προσπαθήστε να περιορίσετε την αναζήτησή σας για να πάρετε περισσότερα αποτελέσματα κάτω από {max}.",
|
||||
"noCreateOptions": "Δεν υπάρχουν διαθέσιμες επιλογές δημιουργίας"
|
||||
},
|
||||
"duplicateFinder": {
|
||||
"title": "Αναζήτηση διπλών αρχείων",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -866,7 +866,8 @@
|
||||
"reassignPath": "הקצאת נתיב מחדש",
|
||||
"pathReassigned": "הנתיב עודכן בהצלחה!",
|
||||
"pathReassignFailed": "לא ניתן לעדכן את הנתיב. אנא נסה שוב.",
|
||||
"incompleteResultsDetails": "הצגת תוצאות מקסימליות ({max}). נסה לצמצם את החיפוש כדי לקבל תוצאות נוספות תחת {max}."
|
||||
"incompleteResultsDetails": "הצגת תוצאות מקסימליות ({max}). נסה לצמצם את החיפוש כדי לקבל תוצאות נוספות תחת {max}.",
|
||||
"noCreateOptions": "אין אפשרויות יצירה זמינות"
|
||||
},
|
||||
"duplicateFinder": {
|
||||
"title": "מאתר קבצים כפולים",
|
||||
|
||||
@@ -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ő",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -866,7 +866,8 @@
|
||||
"reassignPath": "パスの再割り当て",
|
||||
"pathReassigned": "パスは正常に更新されました!",
|
||||
"pathReassignFailed": "パスの更新に失敗しました。再試行してください。",
|
||||
"incompleteResultsDetails": "最大結果を表示 ({max}).{max} でより多くの結果を得るために絞り込んで検索してみてください。"
|
||||
"incompleteResultsDetails": "最大結果を表示 ({max}).{max} でより多くの結果を得るために絞り込んで検索してみてください。",
|
||||
"noCreateOptions": "作成オプションはありません"
|
||||
},
|
||||
"duplicateFinder": {
|
||||
"title": "重複ファイルファインダー",
|
||||
|
||||
@@ -866,7 +866,8 @@
|
||||
"reassignPath": "경로 재할당",
|
||||
"pathReassigned": "경로가 성공적으로 업데이트되었습니다!",
|
||||
"pathReassignFailed": "경로를 업데이트하지 못했습니다. 다시 시도하세요.",
|
||||
"incompleteResultsDetails": "최대 결과 표시 ({max}). 검색 범위를 좁히면 {max} 에서 더 많은 결과를 확인할 수 있습니다."
|
||||
"incompleteResultsDetails": "최대 결과 표시 ({max}). 검색 범위를 좁히면 {max} 에서 더 많은 결과를 확인할 수 있습니다.",
|
||||
"noCreateOptions": "사용 가능한 생성 옵션이 없습니다."
|
||||
},
|
||||
"duplicateFinder": {
|
||||
"title": "중복 파일 찾기",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -866,7 +866,8 @@
|
||||
"reassignPath": "Переназначить путь",
|
||||
"pathReassigned": "Путь успешно обновлен!",
|
||||
"pathReassignFailed": "Не удалось обновить путь. Пожалуйста, попробуйте еще раз.",
|
||||
"incompleteResultsDetails": "Показано максимум результатов ({max}). Попробуйте сузить поиск, чтобы получить больше результатов по запросу {max}."
|
||||
"incompleteResultsDetails": "Показано максимум результатов ({max}). Попробуйте сузить поиск, чтобы получить больше результатов по запросу {max}.",
|
||||
"noCreateOptions": "Опции создания недоступны"
|
||||
},
|
||||
"duplicateFinder": {
|
||||
"title": "Поиск дубликатов файлов",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -866,7 +866,8 @@
|
||||
"reassignPath": "Перепризначити шлях",
|
||||
"pathReassigned": "Шлях успішно оновлено!",
|
||||
"pathReassignFailed": "Не вдалося оновити шлях. Спробуйте ще раз.",
|
||||
"incompleteResultsDetails": "Показано максимум результатів ({max}). Спробуйте звузити пошук, щоб отримати більше результатів за темою {max}."
|
||||
"incompleteResultsDetails": "Показано максимум результатів ({max}). Спробуйте звузити пошук, щоб отримати більше результатів за темою {max}.",
|
||||
"noCreateOptions": "Опції створення недоступні"
|
||||
},
|
||||
"duplicateFinder": {
|
||||
"title": "Пошук дублікатів файлів",
|
||||
|
||||
@@ -866,7 +866,8 @@
|
||||
"reassignPath": "重新分配路径",
|
||||
"pathReassigned": "路径更新成功!",
|
||||
"pathReassignFailed": "更新路径失败。请重试。",
|
||||
"incompleteResultsDetails": "显示最大结果 ({max})。请尝试缩小搜索范围,以获得{max} 下的更多结果。"
|
||||
"incompleteResultsDetails": "显示最大结果 ({max})。请尝试缩小搜索范围,以获得{max} 下的更多结果。",
|
||||
"noCreateOptions": "没有创建选项"
|
||||
},
|
||||
"duplicateFinder": {
|
||||
"title": "重复文件查找器",
|
||||
|
||||
@@ -866,7 +866,8 @@
|
||||
"reassignPath": "重新指定路徑",
|
||||
"pathReassigned": "路徑已成功更新!",
|
||||
"pathReassignFailed": "更新路徑失敗。請再試一次。",
|
||||
"incompleteResultsDetails": "目前只顯示前 {max} 筆結果。請縮小搜尋範圍,以取得少於 {max} 筆的結果。"
|
||||
"incompleteResultsDetails": "目前只顯示前 {max} 筆結果。請縮小搜尋範圍,以取得少於 {max} 筆的結果。",
|
||||
"noCreateOptions": "沒有建立選項"
|
||||
},
|
||||
"duplicateFinder": {
|
||||
"title": "重複檔案搜尋器",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user