From 1a164e0a08e623cdb68634cc997096e67b184a00 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 31 Jan 2026 20:59:43 +0000 Subject: [PATCH] Smarter ZIP download naming. --- frontend/src/components/DownloadButton.vue | 11 +++--- frontend/src/utils/fileutil.ts | 41 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/DownloadButton.vue b/frontend/src/components/DownloadButton.vue index 623ee10..3b0e4d5 100644 --- a/frontend/src/components/DownloadButton.vue +++ b/frontend/src/components/DownloadButton.vue @@ -6,6 +6,7 @@ import { useMainStore } from '@/stores/main' import { apiFetch } from '@/repositories/Client' import type { SelectedItems } from '@/repositories/Document' +import { zipName } from '@/utils/fileutil' import { reactive } from 'vue'; const store = useMainStore() @@ -106,7 +107,6 @@ const filesystemdl = async (sel: SelectedItems, handle: FileSystemDirectoryHandl ++store.dprogress.fileidx const reader = res.body.getReader() await writable.truncate(0) - store.error = "Direct download." store.dprogress.tlast = Date.now() while (true) { const { value, done } = await reader.read() @@ -136,7 +136,7 @@ const download = async () => { console.log('Download', sel) if (sel.keys.length === 0) { console.warn('Attempted download but no files found. Missing selected keys:', sel.missing) - store.error = 'No existing files selected' + store.showToast('No existing files selected') store.selected.clear() return } @@ -144,7 +144,7 @@ const download = async () => { const files = sel.recursive.filter(([rel, full, doc]) => !doc.dir) if (files.length === 1) { store.selected.clear() - store.error = "Single file via browser downloads" + store.showToast(`Downloading ${files[0]![0].split('/').pop()}`) return linkdl(`/files/${files[0]![1]}`) } // Use FileSystem API if multiple files and the browser supports it @@ -164,9 +164,10 @@ const download = async () => { } // Otherwise, zip and download console.log("Falling back to zip download") - const name = sel.keys.length === 1 ? sel.docs[sel.keys[0]!]!.name : 'download' + const items = sel.keys.map(k => sel.docs[k]!) + const name = zipName(items) linkdl(`/zip/${Array.from(sel.keys).join('+')}/${name}.zip`) - store.error = "Downloading as ZIP via browser downloads" + store.showToast(`Downloading ${name}.zip`) store.selected.clear() } diff --git a/frontend/src/utils/fileutil.ts b/frontend/src/utils/fileutil.ts index 3e0ddb5..a5e91e0 100644 --- a/frontend/src/utils/fileutil.ts +++ b/frontend/src/utils/fileutil.ts @@ -6,3 +6,44 @@ export const exists = (path: string[]) => { const p = path.join('/') return store.document.some(doc => (doc.loc ? `${doc.loc}/${doc.name}` : doc.name) === p) } + +/** Strip file extension intelligently (handles .tar.gz, name.with.dots.pdf, etc.) */ +export const stripExt = (name: string): string => { + // Common compound extensions + const compoundExts = ['.tar.gz', '.tar.bz2', '.tar.xz', '.tar.zst'] + const lower = name.toLowerCase() + for (const ext of compoundExts) { + if (lower.endsWith(ext)) return name.slice(0, -ext.length) + } + // Regular extension: only strip if the extension looks like one (2-5 chars, alphanumeric) + const lastDot = name.lastIndexOf('.') + if (lastDot > 0) { + const ext = name.slice(lastDot + 1) + if (ext.length >= 2 && ext.length <= 5 && /^[a-zA-Z0-9]+$/.test(ext)) { + return name.slice(0, lastDot) + } + } + return name +} + +/** Generate a sensible zip filename for a selection of items */ +export const zipName = (items: { name: string; loc: string }[]): string => { + const names = items.map(d => d.name) + if (names.length === 1) { + // Single item - use its name + return stripExt(names[0]!) + } + // Check if all items share the same direct parent folder + const locs = items.map(d => d.loc) + const sameLoc = locs.every(loc => loc === locs[0]) + if (sameLoc && locs[0]) { + // All items in same folder - use folder name + return locs[0].split('/').pop()! + } + if (names.length <= 3) { + // Few items from different folders - join basenames with dot + return names.map(stripExt).join('.') + } + // Many items from different folders - first basename + indicator + return `${stripExt(names[0]!)}.etc` +}