([]);
const filteredFiles = ref([]);
+const selectedItems = ref([]);
const searchQuery = ref('');
const loading = ref(false);
const uploading = ref(false);
@@ -460,19 +499,17 @@ const createNewFolder = async () => {
}
};
-// Download file
+// Download file (using proxy for Safari compatibility)
const downloadFile = async (file: FileItem) => {
downloadingFiles.value[file.name] = true;
try {
- const response = await $fetch('/api/files/download', {
- params: { fileName: file.name },
- });
+ // Use proxy download endpoint for better mobile compatibility
+ const proxyUrl = `/api/files/proxy-download?fileName=${encodeURIComponent(file.name)}`;
- // For mobile Safari compatibility, use a link element
+ // Create a link element
const link = document.createElement('a');
- link.href = response.url;
- link.target = '_blank';
+ link.href = proxyUrl;
// Extract clean filename for download
let filename = file.displayName;
@@ -481,16 +518,94 @@ const downloadFile = async (file: FileItem) => {
}
link.download = filename;
+ link.style.display = 'none';
document.body.appendChild(link);
link.click();
- document.body.removeChild(link);
+
+ // Clean up
+ setTimeout(() => {
+ document.body.removeChild(link);
+ }, 100);
} catch (error) {
- toast.error('Failed to generate download link');
+ toast.error('Failed to download file');
} finally {
downloadingFiles.value[file.name] = false;
}
};
+// Download selected files
+const downloadSelected = async () => {
+ const selectedFiles = files.value.filter(file => selectedItems.value.includes(file.name) && !file.isFolder);
+
+ if (selectedFiles.length === 0) {
+ toast.warning('Please select files to download (folders cannot be downloaded)');
+ return;
+ }
+
+ // Download files sequentially with a small delay
+ for (const file of selectedFiles) {
+ await downloadFile(file);
+ await new Promise(resolve => setTimeout(resolve, 500)); // Small delay between downloads
+ }
+
+ selectedItems.value = [];
+};
+
+// Confirm delete selected
+const confirmDeleteSelected = () => {
+ const itemsToDelete = files.value.filter(file => selectedItems.value.includes(file.name));
+
+ if (itemsToDelete.length === 0) return;
+
+ const hasFolder = itemsToDelete.some(item => item.isFolder);
+ const message = hasFolder
+ ? `Are you sure you want to delete ${itemsToDelete.length} item(s)? This includes folders with all their contents.`
+ : `Are you sure you want to delete ${itemsToDelete.length} file(s)?`;
+
+ if (confirm(message)) {
+ deleteSelected();
+ }
+};
+
+// Delete selected items
+const deleteSelected = async () => {
+ const itemsToDelete = files.value.filter(file => selectedItems.value.includes(file.name));
+
+ deleting.value = true;
+ let successCount = 0;
+ let errorCount = 0;
+
+ try {
+ for (const item of itemsToDelete) {
+ try {
+ await $fetch('/api/files/delete', {
+ method: 'POST',
+ body: {
+ fileName: item.name,
+ isFolder: item.isFolder,
+ },
+ });
+ successCount++;
+ } catch (error) {
+ errorCount++;
+ }
+ }
+
+ if (successCount > 0 && errorCount === 0) {
+ toast.success(`${successCount} item(s) deleted successfully`);
+ } else if (successCount > 0 && errorCount > 0) {
+ toast.warning(`${successCount} item(s) deleted, ${errorCount} failed`);
+ } else {
+ toast.error('Failed to delete items');
+ }
+
+ selectedItems.value = [];
+ await loadFiles();
+ } finally {
+ deleting.value = false;
+ }
+};
+
// Confirm delete
const confirmDelete = (file: FileItem) => {
fileToDelete.value = file;
diff --git a/server/api/files/proxy-download.ts b/server/api/files/proxy-download.ts
new file mode 100644
index 0000000..f6be459
--- /dev/null
+++ b/server/api/files/proxy-download.ts
@@ -0,0 +1,56 @@
+import { getDownloadUrl } from '~/server/utils/minio';
+
+export default defineEventHandler(async (event) => {
+ try {
+ const query = getQuery(event);
+ const fileName = query.fileName as string;
+
+ if (!fileName) {
+ throw createError({
+ statusCode: 400,
+ statusMessage: 'File name is required',
+ });
+ }
+
+ // Get the download URL from MinIO
+ const url = await getDownloadUrl(fileName);
+
+ // Fetch the file from MinIO
+ const response = await fetch(url);
+
+ if (!response.ok) {
+ throw createError({
+ statusCode: response.status,
+ statusMessage: 'Failed to fetch file from storage',
+ });
+ }
+
+ // Get the file data
+ const arrayBuffer = await response.arrayBuffer();
+ const buffer = Buffer.from(arrayBuffer);
+
+ // Extract clean filename (remove timestamp prefix)
+ let cleanFileName = fileName.split('/').pop() || fileName;
+ const timestampMatch = cleanFileName.match(/^\d{10,}-(.+)$/);
+ if (timestampMatch) {
+ cleanFileName = timestampMatch[1];
+ }
+
+ // Get content type
+ const contentType = response.headers.get('content-type') || 'application/octet-stream';
+
+ // Set headers for download
+ setHeader(event, 'Content-Type', contentType);
+ setHeader(event, 'Content-Disposition', `attachment; filename="${cleanFileName}"`);
+ // Content-Length header is set automatically by Nitro when returning a buffer
+
+ // Return the file buffer
+ return buffer;
+ } catch (error: any) {
+ console.error('Failed to proxy download:', error);
+ throw createError({
+ statusCode: 500,
+ statusMessage: error.message || 'Failed to download file',
+ });
+ }
+});