2025-03-19 23:28:29 +01:00
|
|
|
/**
|
|
|
|
|
* OxiCloud - File Operations Module
|
|
|
|
|
* This file handles file and folder operations (create, move, delete, rename, upload)
|
|
|
|
|
*/
|
|
|
|
|
|
2026-04-13 15:09:10 +02:00
|
|
|
import { refreshUserData } from '../../app/authSession.js';
|
|
|
|
|
import { loadFiles } from '../../app/filesView.js';
|
|
|
|
|
import { app } from '../../app/state.js';
|
|
|
|
|
import { showConfirmDialog, ui } from '../../app/ui.js';
|
|
|
|
|
import { getCsrfHeaders, getCsrfToken } from '../../core/csrf.js';
|
|
|
|
|
import { i18n } from '../../core/i18n.js';
|
|
|
|
|
import { notifications } from '../../core/notifications.js';
|
|
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
/**
|
2026-03-03 01:10:50 +01:00
|
|
|
* Get authorization headers for API requests.
|
|
|
|
|
* Tokens are now in HttpOnly cookies — no explicit Authorization header needed.
|
|
|
|
|
* @returns {Object} Headers object
|
2026-02-08 22:44:42 +01:00
|
|
|
*/
|
|
|
|
|
function getAuthHeaders() {
|
2026-03-03 01:10:50 +01:00
|
|
|
return { ...getCsrfHeaders() };
|
2026-02-08 22:44:42 +01:00
|
|
|
}
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
// File Operations Module
|
|
|
|
|
const fileOps = {
|
2026-02-13 22:08:36 +01:00
|
|
|
// ========================================================================
|
2026-02-14 10:46:23 +01:00
|
|
|
// Upload progress — notification bell integration
|
2026-02-13 22:08:36 +01:00
|
|
|
// ========================================================================
|
2026-02-14 10:46:23 +01:00
|
|
|
_currentBatchId: null,
|
2026-04-07 22:48:59 +02:00
|
|
|
_isUploading: false, // Guard against concurrent upload calls
|
2026-02-13 22:08:36 +01:00
|
|
|
|
2026-02-14 10:46:23 +01:00
|
|
|
/** Start a new upload batch in the notification bell */
|
2026-02-18 10:53:43 +01:00
|
|
|
_initUploadToast(totalFiles, folderName) {
|
2026-04-13 15:09:10 +02:00
|
|
|
this._currentBatchId = notifications ? notifications.addUploadBatch(totalFiles, folderName) : null;
|
2026-02-13 22:08:36 +01:00
|
|
|
},
|
|
|
|
|
|
2026-02-14 10:46:23 +01:00
|
|
|
/** Finalise the batch in the notification bell */
|
2026-02-13 22:08:36 +01:00
|
|
|
_finishUploadToast(successCount, totalFiles) {
|
2026-04-13 15:09:10 +02:00
|
|
|
if (notifications && this._currentBatchId) {
|
|
|
|
|
notifications.finishBatch(this._currentBatchId, successCount, totalFiles);
|
2026-02-14 10:46:23 +01:00
|
|
|
}
|
2026-02-13 22:08:36 +01:00
|
|
|
},
|
|
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
/**
|
|
|
|
|
* Some drag-and-drop sources can inject directory placeholders into
|
|
|
|
|
* DataTransfer.files. Browsers fail those with net::ERR_ACCESS_DENIED
|
|
|
|
|
* when trying to send them as normal files.
|
|
|
|
|
*/
|
|
|
|
|
_canReadFileBlob(file) {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
try {
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
reader.onload = () => resolve(true);
|
|
|
|
|
reader.onerror = () => resolve(false);
|
|
|
|
|
reader.readAsArrayBuffer(file.slice(0, 1));
|
|
|
|
|
} catch (_) {
|
|
|
|
|
resolve(false);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
/**
|
2026-02-13 22:08:36 +01:00
|
|
|
* Upload a single file via XMLHttpRequest with progress events.
|
2026-02-14 10:46:23 +01:00
|
|
|
* Progress is reported to the notification bell via batchId + fileName.
|
|
|
|
|
* Returns a promise that resolves with { ok, data?, errorMsg?, isQuotaError? }.
|
2026-02-13 22:08:36 +01:00
|
|
|
*/
|
2026-02-18 10:53:43 +01:00
|
|
|
_uploadFileXHR(formData, batchId, fileName, timeoutMs = 120000) {
|
2026-02-13 22:08:36 +01:00
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const xhr = new XMLHttpRequest();
|
2026-04-13 15:09:10 +02:00
|
|
|
const notif = notifications;
|
2026-03-06 13:18:36 +01:00
|
|
|
// Do NOT set xhr.timeout — it is a TOTAL deadline from send() to
|
|
|
|
|
// response and would kill large uploads even while data is flowing.
|
|
|
|
|
// Instead we rely on the stall timer (no progress for N seconds)
|
|
|
|
|
// and a generous hard deadline that scales with file size.
|
|
|
|
|
xhr.timeout = 0;
|
|
|
|
|
const hardDeadlineMs = Math.max(timeoutMs * 4, 600000); // min 10 min
|
2026-02-18 10:53:43 +01:00
|
|
|
let lastProgressPctSent = -1;
|
|
|
|
|
|
|
|
|
|
let isSettled = false;
|
|
|
|
|
let stallTimer = null;
|
|
|
|
|
let hardTimer = null;
|
|
|
|
|
|
|
|
|
|
const safeUpdateFile = (pct, status) => {
|
|
|
|
|
if (!notif || !batchId) return;
|
|
|
|
|
try {
|
|
|
|
|
notif.updateFile(batchId, fileName, pct, status);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('Notification update failed for upload row:', fileName, e);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const finalize = (result) => {
|
|
|
|
|
if (isSettled) return;
|
|
|
|
|
isSettled = true;
|
|
|
|
|
if (stallTimer) {
|
|
|
|
|
clearTimeout(stallTimer);
|
|
|
|
|
stallTimer = null;
|
|
|
|
|
}
|
|
|
|
|
if (hardTimer) {
|
|
|
|
|
clearTimeout(hardTimer);
|
|
|
|
|
hardTimer = null;
|
|
|
|
|
}
|
|
|
|
|
resolve(result);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resetStallTimer = () => {
|
|
|
|
|
if (stallTimer) clearTimeout(stallTimer);
|
|
|
|
|
stallTimer = setTimeout(() => {
|
2026-04-07 22:48:59 +02:00
|
|
|
try {
|
|
|
|
|
xhr.abort();
|
|
|
|
|
} catch (_) {}
|
2026-02-18 10:53:43 +01:00
|
|
|
safeUpdateFile(0, 'error');
|
|
|
|
|
finalize({
|
|
|
|
|
ok: false,
|
|
|
|
|
isTimeout: true,
|
|
|
|
|
errorMsg: `Upload stalled for ${Math.round(timeoutMs / 1000)}s`
|
|
|
|
|
});
|
|
|
|
|
}, timeoutMs);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
resetStallTimer();
|
|
|
|
|
hardTimer = setTimeout(() => {
|
2026-04-07 22:48:59 +02:00
|
|
|
try {
|
|
|
|
|
xhr.abort();
|
|
|
|
|
} catch (_) {}
|
2026-02-18 10:53:43 +01:00
|
|
|
safeUpdateFile(0, 'error');
|
|
|
|
|
finalize({
|
|
|
|
|
ok: false,
|
|
|
|
|
isTimeout: true,
|
|
|
|
|
errorMsg: `Upload hard timeout after ${Math.round(hardDeadlineMs / 1000)}s`
|
|
|
|
|
});
|
|
|
|
|
}, hardDeadlineMs);
|
2026-02-13 22:08:36 +01:00
|
|
|
|
|
|
|
|
xhr.upload.addEventListener('progress', (e) => {
|
2026-02-18 10:53:43 +01:00
|
|
|
resetStallTimer();
|
|
|
|
|
if (e.lengthComputable) {
|
2026-02-13 22:08:36 +01:00
|
|
|
const pct = Math.round((e.loaded / e.total) * 100);
|
2026-03-06 13:18:36 +01:00
|
|
|
// Throttle UI updates: every 2% for smooth progress on large files
|
|
|
|
|
if (pct === 100 || pct - lastProgressPctSent >= 2) {
|
2026-02-18 10:53:43 +01:00
|
|
|
lastProgressPctSent = pct;
|
|
|
|
|
safeUpdateFile(pct, 'uploading');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
xhr.addEventListener('readystatechange', () => {
|
|
|
|
|
// Keep watchdog alive while request is actively moving through states
|
|
|
|
|
if (xhr.readyState > 1 && xhr.readyState < 4) {
|
|
|
|
|
resetStallTimer();
|
2026-02-13 22:08:36 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
xhr.addEventListener('load', () => {
|
|
|
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
2026-02-18 10:53:43 +01:00
|
|
|
safeUpdateFile(100, 'done');
|
2026-02-13 22:08:36 +01:00
|
|
|
let data = null;
|
2026-04-07 22:48:59 +02:00
|
|
|
try {
|
|
|
|
|
data = JSON.parse(xhr.responseText);
|
|
|
|
|
} catch (_) {}
|
2026-02-18 10:53:43 +01:00
|
|
|
finalize({ ok: true, data });
|
2026-02-13 22:08:36 +01:00
|
|
|
} else {
|
2026-02-18 10:53:43 +01:00
|
|
|
safeUpdateFile(0, 'error');
|
2026-02-14 10:34:07 +01:00
|
|
|
// Parse error body for quota-exceeded or other messages
|
|
|
|
|
let errorMsg = null;
|
|
|
|
|
let isQuotaError = false;
|
|
|
|
|
try {
|
|
|
|
|
const errBody = JSON.parse(xhr.responseText);
|
|
|
|
|
errorMsg = errBody.error || null;
|
|
|
|
|
isQuotaError = errBody.error_type === 'QuotaExceeded' || xhr.status === 507;
|
|
|
|
|
} catch (_) {}
|
2026-02-18 10:53:43 +01:00
|
|
|
finalize({ ok: false, errorMsg, isQuotaError });
|
2026-02-13 22:08:36 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
xhr.addEventListener('error', () => {
|
2026-02-18 10:53:43 +01:00
|
|
|
safeUpdateFile(0, 'error');
|
|
|
|
|
finalize({ ok: false });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
xhr.addEventListener('abort', () => {
|
|
|
|
|
safeUpdateFile(0, 'error');
|
2026-04-07 22:48:59 +02:00
|
|
|
finalize({
|
|
|
|
|
ok: false,
|
|
|
|
|
isTimeout: true,
|
|
|
|
|
errorMsg: `Upload aborted/stalled: ${fileName}`
|
|
|
|
|
});
|
2026-02-18 10:53:43 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
xhr.addEventListener('timeout', () => {
|
|
|
|
|
safeUpdateFile(0, 'error');
|
2026-04-07 22:48:59 +02:00
|
|
|
finalize({
|
|
|
|
|
ok: false,
|
|
|
|
|
isTimeout: true,
|
|
|
|
|
errorMsg: `Timeout after ${Math.round(timeoutMs / 1000)}s`
|
|
|
|
|
});
|
2026-02-13 22:08:36 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
xhr.open('POST', '/api/files/upload');
|
|
|
|
|
|
2026-03-03 01:10:50 +01:00
|
|
|
// Auth is handled by HttpOnly cookies — no explicit header needed
|
2026-02-13 22:08:36 +01:00
|
|
|
xhr.setRequestHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
2026-03-03 01:10:50 +01:00
|
|
|
// CSRF double-submit: echo the CSRF cookie as a request header
|
|
|
|
|
const _csrfTok = getCsrfToken();
|
|
|
|
|
if (_csrfTok) xhr.setRequestHeader('X-CSRF-Token', _csrfTok);
|
2026-02-13 22:08:36 +01:00
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
try {
|
|
|
|
|
xhr.send(formData);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
safeUpdateFile(0, 'error');
|
|
|
|
|
finalize({
|
|
|
|
|
ok: false,
|
|
|
|
|
errorMsg: `Client send() failed: ${e?.message || 'unknown error'}`
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-02-13 22:08:36 +01:00
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
/**
|
|
|
|
|
* Upload a single file via fetch + AbortController.
|
|
|
|
|
* Used by folder uploads to avoid browser XHR edge-cases with dragged entries.
|
|
|
|
|
* Returns { ok, data?, errorMsg?, isQuotaError?, isTimeout? }.
|
|
|
|
|
*/
|
|
|
|
|
async _uploadFileFetch(formData, timeoutMs = 60000) {
|
|
|
|
|
const controller = new AbortController();
|
|
|
|
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/files/upload', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
...getAuthHeaders(),
|
|
|
|
|
'Cache-Control': 'no-cache, no-store, must-revalidate'
|
|
|
|
|
},
|
|
|
|
|
body: formData,
|
|
|
|
|
signal: controller.signal,
|
|
|
|
|
cache: 'no-store'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Read body as text first (always consume the response fully)
|
|
|
|
|
let rawText = '';
|
2026-04-07 22:48:59 +02:00
|
|
|
try {
|
|
|
|
|
rawText = await response.text();
|
|
|
|
|
} catch (_) {}
|
2026-02-18 10:53:43 +01:00
|
|
|
|
|
|
|
|
let body = null;
|
2026-04-07 22:48:59 +02:00
|
|
|
try {
|
|
|
|
|
body = JSON.parse(rawText);
|
|
|
|
|
} catch (_) {}
|
2026-02-18 10:53:43 +01:00
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
return { ok: true, data: body };
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
const errorMsg = body && typeof body === 'object' ? body.error || null : rawText || null;
|
2026-02-18 10:53:43 +01:00
|
|
|
const isQuotaError = (body && typeof body === 'object' && body.error_type === 'QuotaExceeded') || response.status === 507;
|
|
|
|
|
return { ok: false, errorMsg, isQuotaError };
|
|
|
|
|
} catch (e) {
|
|
|
|
|
const isTimeout = e?.name === 'AbortError';
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
isTimeout,
|
2026-04-07 22:48:59 +02:00
|
|
|
errorMsg: isTimeout ? `Timeout after ${Math.round(timeoutMs / 1000)}s` : `Fetch upload failed: ${e?.message || 'network error'}`
|
2026-02-18 10:53:43 +01:00
|
|
|
};
|
|
|
|
|
} finally {
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2026-02-13 22:08:36 +01:00
|
|
|
// ========================================================================
|
|
|
|
|
// Upload files (via button or drag-and-drop)
|
|
|
|
|
// ========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Upload files to the server with real-time progress indication
|
2025-03-19 23:28:29 +01:00
|
|
|
* @param {FileList} files - Files to upload
|
|
|
|
|
*/
|
|
|
|
|
async uploadFiles(files) {
|
2026-02-18 10:53:43 +01:00
|
|
|
const originalFiles = Array.from(files || []);
|
|
|
|
|
if (originalFiles.length === 0) return;
|
2026-02-13 22:08:36 +01:00
|
|
|
|
2026-02-16 16:18:39 +01:00
|
|
|
// Guard: prevent concurrent upload calls (e.g. double drop events)
|
|
|
|
|
if (this._isUploading) {
|
|
|
|
|
console.warn('Upload already in progress, ignoring duplicate call');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._isUploading = true;
|
2026-02-13 22:08:36 +01:00
|
|
|
|
2026-02-16 16:18:39 +01:00
|
|
|
try {
|
|
|
|
|
// Legacy progress bar (inside dropzone) — keep working for drag-drop
|
|
|
|
|
const progressBar = document.querySelector('.progress-fill');
|
|
|
|
|
const uploadProgressDiv = document.querySelector('.upload-progress');
|
2026-04-07 22:48:59 +02:00
|
|
|
if (uploadProgressDiv) {
|
|
|
|
|
uploadProgressDiv.style.display = 'block';
|
|
|
|
|
}
|
|
|
|
|
if (progressBar) {
|
|
|
|
|
progressBar.style.width = '0%';
|
|
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
// Filter out unreadable entries (typically dropped folders/placeholders)
|
|
|
|
|
const readableFiles = [];
|
|
|
|
|
const skippedEntries = [];
|
|
|
|
|
for (const f of originalFiles) {
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
|
const readable = await this._canReadFileBlob(f);
|
|
|
|
|
if (readable) readableFiles.push(f);
|
|
|
|
|
else skippedEntries.push(f.name || 'Unnamed entry');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const totalFiles = readableFiles.length;
|
|
|
|
|
|
2026-04-13 15:09:10 +02:00
|
|
|
if (skippedEntries.length > 0 && notifications) {
|
|
|
|
|
const locale = i18n?.getCurrentLocale?.() || 'en';
|
2026-02-18 10:53:43 +01:00
|
|
|
const title = locale.startsWith('es') ? 'Entradas omitidas' : 'Entries skipped';
|
|
|
|
|
const text = locale.startsWith('es')
|
|
|
|
|
? `Se omitieron ${skippedEntries.length} carpeta(s)/entrada(s) no legibles. Usa "Subir carpeta".`
|
|
|
|
|
: `${skippedEntries.length} unreadable folder/entry items were skipped. Use "Upload folder".`;
|
2026-04-13 15:09:10 +02:00
|
|
|
notifications.addNotification({
|
2026-02-18 10:53:43 +01:00
|
|
|
icon: 'fa-folder-open',
|
|
|
|
|
iconClass: 'upload',
|
|
|
|
|
title,
|
|
|
|
|
text
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (totalFiles === 0) {
|
|
|
|
|
if (uploadProgressDiv) uploadProgressDiv.style.display = 'none';
|
|
|
|
|
this._isUploading = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show upload notification (only for actual readable files)
|
2026-02-16 16:18:39 +01:00
|
|
|
this._initUploadToast(totalFiles);
|
|
|
|
|
const batchId = this._currentBatchId;
|
2025-03-19 23:28:29 +01:00
|
|
|
|
2026-02-16 16:18:39 +01:00
|
|
|
let uploadedCount = 0;
|
|
|
|
|
let successCount = 0;
|
2026-02-13 22:08:36 +01:00
|
|
|
|
2026-02-16 16:18:39 +01:00
|
|
|
for (let i = 0; i < totalFiles; i++) {
|
2026-02-18 10:53:43 +01:00
|
|
|
const file = readableFiles[i];
|
|
|
|
|
|
2026-02-16 16:18:39 +01:00
|
|
|
const formData = new FormData();
|
2025-03-19 23:28:29 +01:00
|
|
|
|
2026-04-13 15:09:10 +02:00
|
|
|
const targetFolderId = app.currentPath || app.userHomeFolderId;
|
2026-02-16 16:18:39 +01:00
|
|
|
if (targetFolderId) formData.append('folder_id', targetFolderId);
|
|
|
|
|
formData.append('file', file);
|
2025-03-19 23:28:29 +01:00
|
|
|
|
2026-02-16 16:18:39 +01:00
|
|
|
console.log(`Uploading file to folder: ${targetFolderId || 'root'}`, {
|
2026-04-07 22:48:59 +02:00
|
|
|
file: file.name,
|
|
|
|
|
size: file.size
|
2026-02-16 16:18:39 +01:00
|
|
|
});
|
2026-02-13 22:08:36 +01:00
|
|
|
|
2026-03-06 13:18:36 +01:00
|
|
|
// Scale stall timeout with file size:
|
|
|
|
|
// base 120s + 60s per GB, so a 7 GB file gets ~540s stall limit
|
|
|
|
|
const sizeGB = file.size / (1024 * 1024 * 1024);
|
|
|
|
|
const dynamicTimeout = Math.max(120000, 120000 + Math.ceil(sizeGB) * 60000);
|
|
|
|
|
const result = await this._uploadFileXHR(formData, batchId, file.name, dynamicTimeout);
|
2026-02-13 22:08:36 +01:00
|
|
|
|
2026-02-16 16:18:39 +01:00
|
|
|
uploadedCount++;
|
|
|
|
|
|
|
|
|
|
// Legacy dropzone bar
|
|
|
|
|
if (progressBar) {
|
2026-04-07 22:50:42 +02:00
|
|
|
progressBar.style.width = `${(uploadedCount / totalFiles) * 100}%`;
|
2026-02-16 16:18:39 +01:00
|
|
|
}
|
|
|
|
|
// Notify bell of per-file completion
|
2026-04-13 15:09:10 +02:00
|
|
|
if (notifications && batchId) {
|
2026-02-18 10:53:43 +01:00
|
|
|
try {
|
2026-04-13 15:09:10 +02:00
|
|
|
notifications.fileCompleted(batchId, result.ok);
|
2026-02-18 10:53:43 +01:00
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('Batch progress update failed:', e);
|
|
|
|
|
}
|
2026-02-16 16:18:39 +01:00
|
|
|
}
|
2026-02-13 22:08:36 +01:00
|
|
|
|
2026-02-16 16:18:39 +01:00
|
|
|
if (result.ok) {
|
|
|
|
|
successCount++;
|
|
|
|
|
console.log(`Successfully uploaded ${file.name}`, result.data);
|
|
|
|
|
} else {
|
|
|
|
|
console.error(`Upload error for ${file.name}`);
|
2026-04-13 15:09:10 +02:00
|
|
|
if (result.isTimeout && notifications) {
|
|
|
|
|
notifications.addNotification({
|
2026-02-18 10:53:43 +01:00
|
|
|
icon: 'fa-clock',
|
|
|
|
|
iconClass: 'error',
|
|
|
|
|
title: file.name,
|
|
|
|
|
text: result.errorMsg || 'Upload timeout'
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-02-16 16:18:39 +01:00
|
|
|
if (result.isQuotaError) {
|
2026-04-13 15:09:10 +02:00
|
|
|
const msg = result.errorMsg || i18n?.t('storage_quota_exceeded') || 'Storage quota exceeded';
|
|
|
|
|
if (notifications) {
|
|
|
|
|
notifications.addNotification({
|
2026-02-16 16:18:39 +01:00
|
|
|
icon: 'fa-exclamation-triangle',
|
|
|
|
|
iconClass: 'error',
|
|
|
|
|
title: file.name,
|
|
|
|
|
text: msg
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
break;
|
2026-02-14 10:46:23 +01:00
|
|
|
}
|
2026-02-14 10:34:07 +01:00
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
}
|
2026-02-13 22:08:36 +01:00
|
|
|
|
2026-02-16 16:18:39 +01:00
|
|
|
// All done
|
|
|
|
|
this._finishUploadToast(successCount, totalFiles);
|
2026-02-13 22:08:36 +01:00
|
|
|
|
2026-02-16 16:18:39 +01:00
|
|
|
// Refresh storage usage display
|
2026-04-13 15:09:10 +02:00
|
|
|
try {
|
|
|
|
|
await refreshUserData();
|
|
|
|
|
} catch (_) {}
|
2026-02-14 10:34:07 +01:00
|
|
|
|
2026-02-16 16:18:39 +01:00
|
|
|
try {
|
2026-04-13 15:09:10 +02:00
|
|
|
await loadFiles({ forceRefresh: true });
|
2026-02-16 16:18:39 +01:00
|
|
|
} catch (reloadError) {
|
|
|
|
|
console.error('Error reloading files:', reloadError);
|
|
|
|
|
}
|
2026-02-13 22:08:36 +01:00
|
|
|
|
2026-02-16 21:51:53 +01:00
|
|
|
const dropzone = document.getElementById('dropzone');
|
|
|
|
|
if (dropzone) dropzone.style.display = 'none';
|
|
|
|
|
if (uploadProgressDiv) uploadProgressDiv.style.display = 'none';
|
2026-02-16 16:18:39 +01:00
|
|
|
} finally {
|
|
|
|
|
this._isUploading = false;
|
|
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
},
|
|
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
/**
|
|
|
|
|
* Upload folder files maintaining directory structure
|
|
|
|
|
* Creates subfolders as needed, then uploads files into them
|
|
|
|
|
* @param {FileList} files - Files from folder input (with webkitRelativePath)
|
|
|
|
|
*/
|
|
|
|
|
async uploadFolderFiles(files) {
|
2026-02-18 10:53:43 +01:00
|
|
|
const entries = Array.from(files || []).map((file) => ({
|
|
|
|
|
file,
|
|
|
|
|
relativePath: file.webkitRelativePath || file.name
|
|
|
|
|
}));
|
|
|
|
|
await this.uploadFolderEntries(entries);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Upload folder-like entries preserving relative paths.
|
|
|
|
|
* @param {Array<{file: File, relativePath: string}>} entries
|
|
|
|
|
*/
|
|
|
|
|
async uploadFolderEntries(entries) {
|
|
|
|
|
const rawEntries = Array.isArray(entries) ? entries : [];
|
|
|
|
|
if (rawEntries.length === 0) return;
|
|
|
|
|
|
|
|
|
|
// Guard: prevent concurrent upload calls
|
|
|
|
|
if (this._isUploading) {
|
|
|
|
|
console.warn('Upload already in progress, ignoring duplicate call');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._isUploading = true;
|
|
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
const progressBar = document.querySelector('.progress-fill');
|
|
|
|
|
const uploadProgressDiv = document.querySelector('.upload-progress');
|
2026-04-07 22:48:59 +02:00
|
|
|
if (uploadProgressDiv) {
|
|
|
|
|
uploadProgressDiv.style.display = 'block';
|
|
|
|
|
}
|
|
|
|
|
if (progressBar) {
|
|
|
|
|
progressBar.style.width = '0%';
|
|
|
|
|
}
|
2026-02-08 22:44:42 +01:00
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
try {
|
|
|
|
|
// Filter unreadable entries
|
|
|
|
|
const validEntries = [];
|
|
|
|
|
for (const e of rawEntries) {
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
|
const readable = await this._canReadFileBlob(e.file);
|
|
|
|
|
if (readable) validEntries.push(e);
|
|
|
|
|
else console.warn(`Skipping unreadable folder entry: ${e.relativePath || e.file?.name}`);
|
2026-02-08 22:44:42 +01:00
|
|
|
}
|
2026-02-18 10:53:43 +01:00
|
|
|
|
|
|
|
|
const totalFiles = validEntries.length;
|
|
|
|
|
if (totalFiles === 0) {
|
|
|
|
|
if (uploadProgressDiv) uploadProgressDiv.style.display = 'none';
|
|
|
|
|
return;
|
2026-02-08 22:44:42 +01:00
|
|
|
}
|
2026-02-13 22:08:36 +01:00
|
|
|
|
2026-04-13 15:09:10 +02:00
|
|
|
const currentFolderId = app.currentPath || app.userHomeFolderId;
|
2026-02-13 22:08:36 +01:00
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
// Build folder structure from relative paths
|
|
|
|
|
const folderMap = new Map();
|
|
|
|
|
folderMap.set('', currentFolderId);
|
|
|
|
|
|
|
|
|
|
const folderPaths = new Set();
|
|
|
|
|
for (const entry of validEntries) {
|
|
|
|
|
const rel = entry.relativePath || entry.file.name;
|
|
|
|
|
const parts = rel.split('/');
|
|
|
|
|
for (let i = 1; i < parts.length; i++) {
|
|
|
|
|
const path = parts.slice(0, i).join('/');
|
|
|
|
|
folderPaths.add(path);
|
|
|
|
|
}
|
2026-02-13 22:08:36 +01:00
|
|
|
}
|
2026-02-18 10:53:43 +01:00
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
const sortedPaths = [...folderPaths].sort((a, b) => a.split('/').length - b.split('/').length);
|
2026-02-18 10:53:43 +01:00
|
|
|
|
|
|
|
|
// Create folders first
|
|
|
|
|
for (const folderPath of sortedPaths) {
|
|
|
|
|
const parts = folderPath.split('/');
|
|
|
|
|
const folderName = parts[parts.length - 1];
|
|
|
|
|
const parentPath = parts.slice(0, -1).join('/');
|
|
|
|
|
const parentId = folderMap.get(parentPath) || currentFolderId;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/folders', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
...getAuthHeaders(),
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'Cache-Control': 'no-cache, no-store, must-revalidate'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
name: folderName,
|
|
|
|
|
parent_id: parentId
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
const folder = await response.json();
|
|
|
|
|
folderMap.set(folderPath, folder.id);
|
|
|
|
|
console.log(`Created folder: ${folderPath} -> ${folder.id}`);
|
|
|
|
|
} else {
|
|
|
|
|
console.error(`Error creating folder ${folderPath}:`, await response.text());
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`Network error creating folder ${folderPath}:`, error);
|
|
|
|
|
}
|
2026-02-14 10:46:23 +01:00
|
|
|
}
|
2026-02-18 10:53:43 +01:00
|
|
|
|
|
|
|
|
// Detect root folder(s) from entry paths
|
2026-04-07 22:48:59 +02:00
|
|
|
const rootFolderNames = [
|
|
|
|
|
...new Set(
|
|
|
|
|
validEntries
|
|
|
|
|
.map((entry) => {
|
|
|
|
|
const rel = entry.relativePath || entry.file.name;
|
|
|
|
|
return rel.split('/')[0] || '';
|
|
|
|
|
})
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
)
|
|
|
|
|
];
|
2026-04-13 15:09:10 +02:00
|
|
|
const locale = i18n?.getCurrentLocale?.() || 'en';
|
2026-04-07 22:48:59 +02:00
|
|
|
const rootFolderLabel =
|
|
|
|
|
rootFolderNames.length <= 1
|
|
|
|
|
? rootFolderNames[0] || ''
|
|
|
|
|
: locale.startsWith('es')
|
|
|
|
|
? `${rootFolderNames.length} carpetas`
|
|
|
|
|
: `${rootFolderNames.length} folders`;
|
2026-02-18 10:53:43 +01:00
|
|
|
|
|
|
|
|
// Upload files — pass folder name for folder-level progress display
|
|
|
|
|
this._initUploadToast(totalFiles, rootFolderLabel);
|
|
|
|
|
const batchId = this._currentBatchId;
|
|
|
|
|
|
|
|
|
|
let uploadedCount = 0;
|
|
|
|
|
let successCount = 0;
|
|
|
|
|
let quotaStop = false;
|
|
|
|
|
|
|
|
|
|
// ── Concurrent upload with limited parallelism ──────────
|
|
|
|
|
// FIFOs are pre-caught by the 0-byte arrayBuffer guard,
|
|
|
|
|
// so all files reaching fetch() are regular. Keep-alive
|
|
|
|
|
// reuses TCP connections across workers for speed.
|
|
|
|
|
const CONCURRENCY = 10;
|
2026-04-12 00:29:34 +02:00
|
|
|
const TIMEOUT_BASE_MS = 30000; // 30s base for normal files
|
|
|
|
|
const TIMEOUT_PER_MB_MS = 2000; // +2s per MB (supports ≥4 Mbps)
|
|
|
|
|
const TIMEOUT_MIN_MS = 10000; // floor for tiny files
|
2026-04-07 22:48:59 +02:00
|
|
|
const TIMEOUT_MS_ZERO = 3000; // 3s for 0-byte files
|
2026-02-18 10:53:43 +01:00
|
|
|
|
|
|
|
|
const uploadOneFile = async (idx) => {
|
|
|
|
|
if (quotaStop) return;
|
|
|
|
|
const entry = validEntries[idx];
|
|
|
|
|
const file = entry.file;
|
|
|
|
|
const rel = entry.relativePath || file.name;
|
|
|
|
|
|
|
|
|
|
let result = { ok: false, errorMsg: 'Unknown client error' };
|
|
|
|
|
try {
|
|
|
|
|
const parts = rel.split('/');
|
|
|
|
|
const parentPath = parts.slice(0, -1).join('/');
|
|
|
|
|
const targetFolderId = folderMap.get(parentPath) || currentFolderId;
|
|
|
|
|
|
|
|
|
|
// ── FIFO/pipe guard (0-byte files only) ──
|
|
|
|
|
// Named pipes (runit supervise/control) report size=0
|
|
|
|
|
// but block on open(). Pre-read only 0-byte files into
|
|
|
|
|
// memory; files with size>0 are always regular files and
|
|
|
|
|
// go straight to FormData (zero extra memory copy).
|
2026-04-07 22:48:59 +02:00
|
|
|
let uploadFile = file; // default: use original File
|
2026-02-18 10:53:43 +01:00
|
|
|
if (file.size === 0) {
|
|
|
|
|
try {
|
|
|
|
|
const buf = await Promise.race([
|
|
|
|
|
file.arrayBuffer(),
|
2026-04-07 22:48:59 +02:00
|
|
|
new Promise((_, rej) => setTimeout(() => rej(new Error('read-timeout')), 2000))
|
2026-02-18 10:53:43 +01:00
|
|
|
]);
|
|
|
|
|
uploadFile = new Blob([buf], {
|
|
|
|
|
type: file.type || 'application/octet-stream'
|
|
|
|
|
});
|
|
|
|
|
} catch {
|
|
|
|
|
console.warn(`[SKIP] #${idx} ${rel} — cannot read 0-byte file (FIFO/pipe?), skipping`);
|
|
|
|
|
uploadedCount++;
|
|
|
|
|
successCount++;
|
2026-04-13 15:09:10 +02:00
|
|
|
if (notifications && batchId) {
|
2026-04-07 22:48:59 +02:00
|
|
|
try {
|
2026-04-13 15:09:10 +02:00
|
|
|
notifications.fileCompleted(batchId, true);
|
2026-04-07 22:48:59 +02:00
|
|
|
} catch (_) {}
|
2026-02-18 10:53:43 +01:00
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
formData.append('folder_id', targetFolderId);
|
|
|
|
|
formData.append('file', uploadFile, file.name);
|
|
|
|
|
|
2026-04-12 02:13:08 +02:00
|
|
|
const thisTimeout =
|
|
|
|
|
file.size === 0
|
|
|
|
|
? TIMEOUT_MS_ZERO
|
|
|
|
|
: Math.max(TIMEOUT_MIN_MS, TIMEOUT_BASE_MS + Math.ceil(file.size / (1024 * 1024)) * TIMEOUT_PER_MB_MS);
|
2026-02-18 10:53:43 +01:00
|
|
|
console.log(`[UPLOAD START] #${idx} ${rel} (${file.size} bytes, timeout=${thisTimeout}ms)`);
|
|
|
|
|
|
|
|
|
|
result = await this._uploadFileFetch(formData, thisTimeout);
|
|
|
|
|
|
2026-04-07 22:50:42 +02:00
|
|
|
console.log(`[UPLOAD END] #${idx} ${rel} ok=${result.ok}${result.errorMsg ? ` err=${result.errorMsg}` : ''}`);
|
2026-02-18 10:53:43 +01:00
|
|
|
} catch (e) {
|
|
|
|
|
result = {
|
|
|
|
|
ok: false,
|
|
|
|
|
errorMsg: `Client exception: ${e?.message || 'unknown'}`
|
|
|
|
|
};
|
|
|
|
|
console.error(`[UPLOAD EXCEPTION] #${idx} ${rel}:`, e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uploadedCount++;
|
|
|
|
|
|
2026-04-13 15:09:10 +02:00
|
|
|
if (notifications && batchId) {
|
2026-04-07 22:48:59 +02:00
|
|
|
try {
|
2026-04-13 15:09:10 +02:00
|
|
|
notifications.fileCompleted(batchId, result.ok);
|
2026-04-07 22:48:59 +02:00
|
|
|
} catch (_) {}
|
2026-02-18 10:53:43 +01:00
|
|
|
}
|
|
|
|
|
if (progressBar && uploadedCount % 10 === 0) {
|
2026-04-07 22:50:42 +02:00
|
|
|
progressBar.style.width = `${(uploadedCount / totalFiles) * 100}%`;
|
2026-02-18 10:53:43 +01:00
|
|
|
}
|
|
|
|
|
if (uploadedCount % 50 === 0 || uploadedCount === totalFiles) {
|
|
|
|
|
console.log(`Progress: ${uploadedCount}/${totalFiles} (${successCount} ok)`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result.ok) {
|
|
|
|
|
successCount++;
|
|
|
|
|
} else if (result.isQuotaError) {
|
|
|
|
|
quotaStop = true;
|
2026-04-13 15:09:10 +02:00
|
|
|
if (notifications) {
|
|
|
|
|
notifications.addNotification({
|
2026-02-14 10:46:23 +01:00
|
|
|
icon: 'fa-exclamation-triangle',
|
|
|
|
|
iconClass: 'error',
|
|
|
|
|
title: file.name,
|
2026-02-18 10:53:43 +01:00
|
|
|
text: result.errorMsg || 'Storage quota exceeded'
|
2026-02-14 10:46:23 +01:00
|
|
|
});
|
|
|
|
|
}
|
2026-02-14 10:34:07 +01:00
|
|
|
}
|
2026-02-18 10:53:43 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Pool-based concurrency: always keep CONCURRENCY tasks in flight
|
|
|
|
|
let nextIdx = 0;
|
|
|
|
|
const runNext = async () => {
|
|
|
|
|
while (nextIdx < totalFiles && !quotaStop) {
|
|
|
|
|
const idx = nextIdx++;
|
|
|
|
|
await uploadOneFile(idx);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const workers = [];
|
|
|
|
|
for (let w = 0; w < Math.min(CONCURRENCY, totalFiles); w++) {
|
|
|
|
|
workers.push(runNext());
|
2026-02-08 22:44:42 +01:00
|
|
|
}
|
2026-02-18 10:53:43 +01:00
|
|
|
await Promise.all(workers);
|
2026-02-14 10:34:07 +01:00
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
this._finishUploadToast(successCount, totalFiles);
|
2026-02-16 21:51:53 +01:00
|
|
|
|
2026-04-13 15:09:10 +02:00
|
|
|
try {
|
|
|
|
|
await refreshUserData();
|
|
|
|
|
} catch (_) {}
|
2026-02-16 21:51:53 +01:00
|
|
|
|
2026-02-18 10:53:43 +01:00
|
|
|
try {
|
2026-04-13 15:09:10 +02:00
|
|
|
await loadFiles({ forceRefresh: true });
|
2026-02-18 10:53:43 +01:00
|
|
|
} catch (reloadError) {
|
|
|
|
|
console.error('Error reloading files:', reloadError);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dropzone = document.getElementById('dropzone');
|
|
|
|
|
if (dropzone) dropzone.style.display = 'none';
|
|
|
|
|
if (uploadProgressDiv) uploadProgressDiv.style.display = 'none';
|
|
|
|
|
} finally {
|
|
|
|
|
this._isUploading = false;
|
|
|
|
|
}
|
2026-02-08 22:44:42 +01:00
|
|
|
},
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
/**
|
|
|
|
|
* Create a new folder
|
|
|
|
|
* @param {string} name - Folder name
|
|
|
|
|
*/
|
|
|
|
|
async createFolder(name) {
|
|
|
|
|
try {
|
2025-04-01 21:14:09 +02:00
|
|
|
console.log('Creating folder with name:', name);
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
// Send the actual request to the backend to create the folder
|
2025-03-19 23:28:29 +01:00
|
|
|
const response = await fetch('/api/folders', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
2026-02-08 22:44:42 +01:00
|
|
|
...getAuthHeaders(),
|
2025-04-12 12:37:12 +02:00
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'Cache-Control': 'no-cache, no-store, must-revalidate'
|
2025-03-19 23:28:29 +01:00
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
name: name,
|
2026-04-13 15:09:10 +02:00
|
|
|
parent_id: app.currentPath || app.userHomeFolderId || null
|
2025-03-19 23:28:29 +01:00
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
2026-02-12 09:41:25 +01:00
|
|
|
// Get the created folder from the backend
|
2025-04-12 12:37:12 +02:00
|
|
|
const folder = await response.json();
|
|
|
|
|
console.log('Folder created successfully:', folder);
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2026-02-16 21:51:53 +01:00
|
|
|
// Optimistic UI: add folder card directly from server response
|
|
|
|
|
// — no reload needed since the backend already confirmed creation.
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.addFolderToView(folder);
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Folder created', `"${name}" created successfully`);
|
2025-03-19 23:28:29 +01:00
|
|
|
} else {
|
|
|
|
|
const errorData = await response.text();
|
|
|
|
|
console.error('Create folder error:', errorData);
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error creating the folder');
|
2025-03-19 23:28:29 +01:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error creating folder:', error);
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error creating the folder');
|
2025-03-19 23:28:29 +01:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Move a file to another folder
|
|
|
|
|
* @param {string} fileId - File ID
|
|
|
|
|
* @param {string} targetFolderId - Target folder ID
|
|
|
|
|
* @returns {Promise<boolean>} - Success status
|
|
|
|
|
*/
|
|
|
|
|
async moveFile(fileId, targetFolderId) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`/api/files/${fileId}/move`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: {
|
2026-02-08 22:44:42 +01:00
|
|
|
...getAuthHeaders(),
|
2025-03-19 23:28:29 +01:00
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
2026-04-07 22:48:59 +02:00
|
|
|
folder_id: targetFolderId === '' ? null : targetFolderId
|
2025-03-19 23:28:29 +01:00
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
// Reload files after moving
|
2026-04-13 15:09:10 +02:00
|
|
|
await loadFiles();
|
|
|
|
|
ui.showNotification('File moved', 'File moved successfully');
|
2025-03-19 23:28:29 +01:00
|
|
|
return true;
|
|
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
let errorMessage = 'Unknown error';
|
2025-03-19 23:28:29 +01:00
|
|
|
try {
|
|
|
|
|
const errorData = await response.json();
|
2026-02-12 09:41:25 +01:00
|
|
|
errorMessage = errorData.error || 'Unknown error';
|
2026-04-07 22:50:42 +02:00
|
|
|
} catch (_e) {
|
2026-02-12 09:41:25 +01:00
|
|
|
errorMessage = 'Error processing server response';
|
2025-03-19 23:28:29 +01:00
|
|
|
}
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', `Error moving the file: ${errorMessage}`);
|
2025-03-19 23:28:29 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error moving file:', error);
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error moving the file');
|
2025-03-19 23:28:29 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Move a folder to another folder
|
|
|
|
|
* @param {string} folderId - Folder ID
|
|
|
|
|
* @param {string} targetFolderId - Target folder ID
|
|
|
|
|
* @returns {Promise<boolean>} - Success status
|
|
|
|
|
*/
|
|
|
|
|
async moveFolder(folderId, targetFolderId) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`/api/folders/${folderId}/move`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: {
|
2026-02-08 22:44:42 +01:00
|
|
|
...getAuthHeaders(),
|
2025-03-19 23:28:29 +01:00
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
2026-04-07 22:48:59 +02:00
|
|
|
parent_id: targetFolderId === '' ? null : targetFolderId
|
2025-03-19 23:28:29 +01:00
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
// Reload files after moving
|
2026-04-13 15:09:10 +02:00
|
|
|
await loadFiles();
|
|
|
|
|
ui.showNotification('Folder moved', 'Folder moved successfully');
|
2025-03-19 23:28:29 +01:00
|
|
|
return true;
|
|
|
|
|
} else {
|
2026-02-12 09:41:25 +01:00
|
|
|
let errorMessage = 'Unknown error';
|
2025-03-19 23:28:29 +01:00
|
|
|
try {
|
|
|
|
|
const errorData = await response.json();
|
2026-02-12 09:41:25 +01:00
|
|
|
errorMessage = errorData.error || 'Unknown error';
|
2026-04-07 22:50:42 +02:00
|
|
|
} catch (_e) {
|
2026-02-12 09:41:25 +01:00
|
|
|
errorMessage = 'Error processing server response';
|
2025-03-19 23:28:29 +01:00
|
|
|
}
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', `Error moving the folder: ${errorMessage}`);
|
2025-03-19 23:28:29 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error moving folder:', error);
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error moving the folder');
|
2025-03-19 23:28:29 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2026-03-25 18:49:38 +01:00
|
|
|
/**
|
|
|
|
|
* @typedef {Object} BatchResult
|
|
|
|
|
* @property {number} success number of files|folders sucessfully updated
|
|
|
|
|
* @property {number} errors number of files|folders in error
|
|
|
|
|
* /
|
2026-04-07 22:50:42 +02:00
|
|
|
|
2026-03-25 18:49:38 +01:00
|
|
|
/**
|
|
|
|
|
* Move files & folders
|
|
|
|
|
* @param {string[]} fileIds - File IDs
|
|
|
|
|
* @param {string[]} folderIds - Folder IDs
|
|
|
|
|
* @param {string} targetFolderId - Target folder ID
|
|
|
|
|
* @returns {Promise<BatchResult>} - Success status
|
|
|
|
|
*/
|
|
|
|
|
async batchMove(fileIds, folderIds, targetFolderId) {
|
|
|
|
|
// TODO ensure not moving a folder into itself
|
2026-04-07 22:48:59 +02:00
|
|
|
let success = 0,
|
|
|
|
|
errors = 0;
|
2026-03-25 18:49:38 +01:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Batch move files in a single request
|
|
|
|
|
if (fileIds.length > 0) {
|
|
|
|
|
const res = await fetch('/api/batch/files/move', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
2026-04-07 22:48:59 +02:00
|
|
|
body: JSON.stringify({
|
|
|
|
|
file_ids: fileIds,
|
|
|
|
|
target_folder_id: targetFolderId
|
|
|
|
|
})
|
2026-03-25 18:49:38 +01:00
|
|
|
});
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
success += data.stats?.successful || 0;
|
|
|
|
|
errors += data.stats?.failed || 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Batch move folders in a single request
|
|
|
|
|
if (folderIds.length > 0) {
|
|
|
|
|
const res = await fetch('/api/batch/folders/move', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
2026-04-07 22:48:59 +02:00
|
|
|
body: JSON.stringify({
|
|
|
|
|
folder_ids: folderIds,
|
|
|
|
|
target_folder_id: targetFolderId
|
|
|
|
|
})
|
2026-03-25 18:49:38 +01:00
|
|
|
});
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
success += data.stats?.successful || 0;
|
|
|
|
|
errors += data.stats?.failed || 0;
|
|
|
|
|
}
|
2026-04-07 22:48:59 +02:00
|
|
|
} catch (err) {
|
2026-03-25 18:49:38 +01:00
|
|
|
console.error('Batch move error:', err);
|
|
|
|
|
errors++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success,
|
|
|
|
|
errors
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
2026-02-24 18:08:33 -08:00
|
|
|
/**
|
|
|
|
|
* Copy a file to another folder
|
|
|
|
|
* @param {string} fileId - File ID
|
|
|
|
|
* @param {string} targetFolderId - Target folder ID
|
|
|
|
|
* @returns {Promise<boolean>} - Success status
|
|
|
|
|
*/
|
|
|
|
|
async copyFile(fileId, targetFolderId) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/batch/files/copy', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
...getAuthHeaders(),
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
file_ids: [fileId],
|
2026-04-07 22:48:59 +02:00
|
|
|
target_folder_id: targetFolderId === '' ? null : targetFolderId
|
2026-02-24 18:08:33 -08:00
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
2026-04-07 22:50:42 +02:00
|
|
|
await response.json();
|
2026-02-24 18:08:33 -08:00
|
|
|
// Reload files after copying
|
2026-04-13 15:09:10 +02:00
|
|
|
await loadFiles();
|
|
|
|
|
ui.showNotification('File copied', 'File copied successfully');
|
2026-02-24 18:08:33 -08:00
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
let errorMessage = 'Unknown error';
|
|
|
|
|
try {
|
|
|
|
|
const errorData = await response.json();
|
|
|
|
|
errorMessage = errorData.error || 'Unknown error';
|
2026-04-07 22:50:42 +02:00
|
|
|
} catch (_e) {
|
2026-02-24 18:08:33 -08:00
|
|
|
errorMessage = 'Error processing server response';
|
|
|
|
|
}
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', `Error copying the file: ${errorMessage}`);
|
2026-02-24 18:08:33 -08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error copying file:', error);
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error copying the file');
|
2026-02-24 18:08:33 -08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Copy a folder to another folder
|
|
|
|
|
* Note: Backend folder copy is not yet implemented, this shows a notification
|
|
|
|
|
* @param {string} folderId - Folder ID
|
|
|
|
|
* @param {string} targetFolderId - Target folder ID
|
|
|
|
|
* @returns {Promise<boolean>} - Success status
|
|
|
|
|
*/
|
2026-04-12 02:13:08 +02:00
|
|
|
async copyFolder(_folderId, _targetFolderId) {
|
2026-02-24 18:08:33 -08:00
|
|
|
// Folder copy is not yet implemented in the backend
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Not implemented', 'Folder copy is not yet supported');
|
2026-02-24 18:08:33 -08:00
|
|
|
return false;
|
|
|
|
|
},
|
|
|
|
|
|
2026-03-25 18:49:38 +01:00
|
|
|
/**
|
|
|
|
|
* Copy files & folders
|
|
|
|
|
* @param {string[]} fileIds - File IDs
|
|
|
|
|
* @param {string[]} folderIds - Folder IDs
|
|
|
|
|
* @param {string} targetFolderId - Target folder ID
|
|
|
|
|
* @returns {Promise<boolean>} - Success status
|
|
|
|
|
*/
|
|
|
|
|
async batchCopy(fileIds, folderIds, targetFolderId) {
|
|
|
|
|
// FIXME ensure not moving a folder into itself
|
|
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
let success = 0,
|
|
|
|
|
errors = 0;
|
2026-03-25 18:49:38 +01:00
|
|
|
try {
|
|
|
|
|
// Batch copy files
|
|
|
|
|
if (fileIds.length > 0) {
|
|
|
|
|
const res = await fetch('/api/batch/files/copy', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
2026-04-07 22:48:59 +02:00
|
|
|
body: JSON.stringify({
|
|
|
|
|
file_ids: fileIds,
|
|
|
|
|
target_folder_id: targetFolderId
|
|
|
|
|
})
|
2026-03-25 18:49:38 +01:00
|
|
|
});
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
success += data.stats?.successful || 0;
|
|
|
|
|
errors += data.stats?.failed || 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Note: Folder copy is not yet implemented in batch API
|
|
|
|
|
if (folderIds.length > 0) {
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Info', 'Folder copy is not yet supported in batch mode');
|
2026-03-25 18:49:38 +01:00
|
|
|
errors += folderIds.lenngth;
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Batch copy error:', err);
|
|
|
|
|
errors++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success,
|
|
|
|
|
errors
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
/**
|
|
|
|
|
* Rename a file
|
|
|
|
|
* @param {string} fileId - File ID
|
|
|
|
|
* @param {string} newName - New file name
|
|
|
|
|
* @returns {Promise<boolean>} - Success status
|
|
|
|
|
*/
|
|
|
|
|
async renameFile(fileId, newName) {
|
|
|
|
|
try {
|
|
|
|
|
console.log(`Renaming file ${fileId} to "${newName}"`);
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`/api/files/${fileId}/rename`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: {
|
|
|
|
|
...getAuthHeaders(),
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ name: newName })
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('Response status:', response.status);
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification(
|
|
|
|
|
i18n ? i18n.t('notifications.file_renamed') : 'File renamed',
|
|
|
|
|
i18n ? i18n.t('notifications.file_renamed_to', { name: newName }) : `File renamed to "${newName}"`
|
2026-02-08 22:44:42 +01:00
|
|
|
);
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
const errorText = await response.text();
|
|
|
|
|
console.error('Error response:', errorText);
|
2026-02-12 09:41:25 +01:00
|
|
|
let errorMessage = 'Unknown error';
|
2026-02-08 22:44:42 +01:00
|
|
|
try {
|
|
|
|
|
const errorData = JSON.parse(errorText);
|
|
|
|
|
errorMessage = errorData.error || response.statusText;
|
2026-04-07 22:50:42 +02:00
|
|
|
} catch (_e) {
|
2026-02-08 22:44:42 +01:00
|
|
|
errorMessage = errorText || response.statusText;
|
|
|
|
|
}
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', `Error renaming the file: ${errorMessage}`);
|
2026-02-08 22:44:42 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error renaming file:', error);
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error renaming the file');
|
2026-02-08 22:44:42 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
/**
|
|
|
|
|
* Rename a folder
|
|
|
|
|
* @param {string} folderId - Folder ID
|
|
|
|
|
* @param {string} newName - New folder name
|
|
|
|
|
* @returns {Promise<boolean>} - Success status
|
|
|
|
|
*/
|
|
|
|
|
async renameFolder(folderId, newName) {
|
|
|
|
|
try {
|
|
|
|
|
console.log(`Renaming folder ${folderId} to "${newName}"`);
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`/api/folders/${folderId}/rename`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: {
|
2026-02-08 22:44:42 +01:00
|
|
|
...getAuthHeaders(),
|
2025-03-19 23:28:29 +01:00
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ name: newName })
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('Response status:', response.status);
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Folder renamed', `Folder renamed to "${newName}"`);
|
2025-03-19 23:28:29 +01:00
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
const errorText = await response.text();
|
|
|
|
|
console.error('Error response:', errorText);
|
|
|
|
|
|
2026-02-12 09:41:25 +01:00
|
|
|
let errorMessage = 'Unknown error';
|
2025-03-19 23:28:29 +01:00
|
|
|
try {
|
|
|
|
|
// Try to parse as JSON
|
|
|
|
|
const errorData = JSON.parse(errorText);
|
|
|
|
|
errorMessage = errorData.error || response.statusText;
|
2026-04-07 22:50:42 +02:00
|
|
|
} catch (_e) {
|
2025-03-19 23:28:29 +01:00
|
|
|
// If not JSON, use text as is
|
|
|
|
|
errorMessage = errorText || response.statusText;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', `Error renaming the folder: ${errorMessage}`);
|
2025-03-19 23:28:29 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error renaming folder:', error);
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error renaming the folder');
|
2025-03-19 23:28:29 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
2025-03-24 17:49:53 +01:00
|
|
|
* Move a file to trash
|
2025-03-19 23:28:29 +01:00
|
|
|
* @param {string} fileId - File ID
|
2025-03-24 17:49:53 +01:00
|
|
|
* @param {string} fileName - File name
|
2025-03-19 23:28:29 +01:00
|
|
|
* @returns {Promise<boolean>} - Success status
|
|
|
|
|
*/
|
|
|
|
|
async deleteFile(fileId, fileName) {
|
2026-02-08 22:44:42 +01:00
|
|
|
const confirmed = await showConfirmDialog({
|
2026-04-13 15:09:10 +02:00
|
|
|
title: i18n ? i18n.t('dialogs.confirm_delete') : 'Move to trash',
|
|
|
|
|
message: i18n ? i18n.t('dialogs.confirm_delete_file', { name: fileName }) : `Are you sure you want to move the file "${fileName}" to trash?`,
|
|
|
|
|
confirmText: i18n ? i18n.t('actions.delete') : 'Delete'
|
2026-02-08 22:44:42 +01:00
|
|
|
});
|
|
|
|
|
if (!confirmed) return false;
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
try {
|
2025-03-24 17:49:53 +01:00
|
|
|
// Use the trash API endpoint
|
|
|
|
|
const response = await fetch(`/api/trash/files/${fileId}`, {
|
2026-02-08 22:44:42 +01:00
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: getAuthHeaders()
|
2025-03-19 23:28:29 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
2026-04-13 15:09:10 +02:00
|
|
|
loadFiles();
|
|
|
|
|
ui.showNotification('File moved to trash', `"${fileName}" moved to trash`);
|
2025-03-19 23:28:29 +01:00
|
|
|
return true;
|
|
|
|
|
} else {
|
2025-03-24 17:49:53 +01:00
|
|
|
// Fallback to direct deletion if trash fails
|
|
|
|
|
const fallbackResponse = await fetch(`/api/files/${fileId}`, {
|
2026-02-08 22:44:42 +01:00
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: getAuthHeaders()
|
2025-03-24 17:49:53 +01:00
|
|
|
});
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
if (fallbackResponse.ok) {
|
2026-04-13 15:09:10 +02:00
|
|
|
loadFiles();
|
|
|
|
|
ui.showNotification('File deleted', `"${fileName}" deleted successfully`);
|
2025-03-24 17:49:53 +01:00
|
|
|
return true;
|
|
|
|
|
} else {
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error deleting the file');
|
2025-03-24 17:49:53 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error deleting file:', error);
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error deleting the file');
|
2025-03-19 23:28:29 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
2025-03-24 17:49:53 +01:00
|
|
|
* Move a folder to trash
|
2025-03-19 23:28:29 +01:00
|
|
|
* @param {string} folderId - Folder ID
|
|
|
|
|
* @param {string} folderName - Folder name
|
|
|
|
|
* @returns {Promise<boolean>} - Success status
|
|
|
|
|
*/
|
|
|
|
|
async deleteFolder(folderId, folderName) {
|
2026-02-08 22:44:42 +01:00
|
|
|
const confirmed = await showConfirmDialog({
|
2026-04-13 15:09:10 +02:00
|
|
|
title: i18n ? i18n.t('dialogs.confirm_delete') : 'Move to trash',
|
|
|
|
|
message: i18n
|
|
|
|
|
? i18n.t('dialogs.confirm_delete_folder', { name: folderName })
|
2026-04-07 22:48:59 +02:00
|
|
|
: `Are you sure you want to move the folder "${folderName}" and all its contents to trash?`,
|
2026-04-13 15:09:10 +02:00
|
|
|
confirmText: i18n ? i18n.t('actions.delete') : 'Delete'
|
2026-02-08 22:44:42 +01:00
|
|
|
});
|
|
|
|
|
if (!confirmed) return false;
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2025-03-19 23:28:29 +01:00
|
|
|
try {
|
2025-03-24 17:49:53 +01:00
|
|
|
// Use the trash API endpoint
|
|
|
|
|
const response = await fetch(`/api/trash/folders/${folderId}`, {
|
2026-02-08 22:44:42 +01:00
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: getAuthHeaders()
|
2025-03-19 23:28:29 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
// If we're inside the folder we just deleted, go back up
|
2026-04-13 15:09:10 +02:00
|
|
|
if (app.currentPath === folderId) {
|
|
|
|
|
app.currentPath = '';
|
|
|
|
|
ui.updateBreadcrumb('');
|
2025-03-19 23:28:29 +01:00
|
|
|
}
|
2026-04-13 15:09:10 +02:00
|
|
|
loadFiles();
|
|
|
|
|
ui.showNotification('Folder moved to trash', `"${folderName}" moved to trash`);
|
2025-03-19 23:28:29 +01:00
|
|
|
return true;
|
|
|
|
|
} else {
|
2025-03-24 17:49:53 +01:00
|
|
|
// Fallback to direct deletion if trash fails
|
|
|
|
|
const fallbackResponse = await fetch(`/api/folders/${folderId}`, {
|
2026-02-08 22:44:42 +01:00
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: getAuthHeaders()
|
2025-03-24 17:49:53 +01:00
|
|
|
});
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
if (fallbackResponse.ok) {
|
|
|
|
|
// If we're inside the folder we just deleted, go back up
|
2026-04-13 15:09:10 +02:00
|
|
|
if (app.currentPath === folderId) {
|
|
|
|
|
app.currentPath = '';
|
|
|
|
|
ui.updateBreadcrumb('');
|
2025-03-24 17:49:53 +01:00
|
|
|
}
|
2026-04-13 15:09:10 +02:00
|
|
|
loadFiles();
|
|
|
|
|
ui.showNotification('Folder deleted', `"${folderName}" deleted successfully`);
|
2025-03-24 17:49:53 +01:00
|
|
|
return true;
|
|
|
|
|
} else {
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error deleting the folder');
|
2025-03-24 17:49:53 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error deleting folder:', error);
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error deleting the folder');
|
2025-03-19 23:28:29 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
2025-03-24 17:49:53 +01:00
|
|
|
},
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
/**
|
2026-02-12 09:41:25 +01:00
|
|
|
* Get trash items
|
|
|
|
|
* @returns {Promise<Array>} - List of trash items
|
2025-03-24 17:49:53 +01:00
|
|
|
*/
|
|
|
|
|
async getTrashItems() {
|
|
|
|
|
try {
|
2026-02-08 22:44:42 +01:00
|
|
|
const response = await fetch('/api/trash', {
|
|
|
|
|
headers: getAuthHeaders()
|
|
|
|
|
});
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
if (response.ok) {
|
|
|
|
|
return await response.json();
|
|
|
|
|
} else {
|
|
|
|
|
console.error('Error fetching trash items:', response.statusText);
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error fetching trash items:', error);
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
/**
|
2026-02-12 09:41:25 +01:00
|
|
|
* Restore an item from trash
|
|
|
|
|
* @param {string} trashId - Trash item ID
|
|
|
|
|
* @returns {Promise<boolean>} - Operation success
|
2025-03-24 17:49:53 +01:00
|
|
|
*/
|
|
|
|
|
async restoreFromTrash(trashId) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`/api/trash/${trashId}/restore`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
2026-02-08 22:44:42 +01:00
|
|
|
...getAuthHeaders(),
|
2025-03-24 17:49:53 +01:00
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({})
|
|
|
|
|
});
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
if (response.ok) {
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Item restored', 'Item restored successfully');
|
2025-03-24 17:49:53 +01:00
|
|
|
return true;
|
|
|
|
|
} else {
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error restoring the item');
|
2025-03-24 17:49:53 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error restoring item from trash:', error);
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error restoring the item');
|
2025-03-24 17:49:53 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
/**
|
2026-02-12 09:41:25 +01:00
|
|
|
* Permanently delete a trash item
|
|
|
|
|
* @param {string} trashId - Trash item ID
|
|
|
|
|
* @returns {Promise<boolean>} - Operation success
|
2025-03-24 17:49:53 +01:00
|
|
|
*/
|
|
|
|
|
async deletePermanently(trashId) {
|
2026-02-08 22:44:42 +01:00
|
|
|
const confirmed = await showConfirmDialog({
|
2026-04-13 15:09:10 +02:00
|
|
|
title: i18n ? i18n.t('dialogs.confirm_permanent_delete') : 'Delete permanently',
|
|
|
|
|
message: i18n
|
|
|
|
|
? i18n.t('dialogs.confirm_permanent_delete_msg')
|
2026-04-07 22:48:59 +02:00
|
|
|
: 'Are you sure you want to permanently delete this item? This action cannot be undone.',
|
2026-04-13 15:09:10 +02:00
|
|
|
confirmText: i18n ? i18n.t('actions.delete_permanently') : 'Delete permanently'
|
2026-02-08 22:44:42 +01:00
|
|
|
});
|
|
|
|
|
if (!confirmed) return false;
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
try {
|
|
|
|
|
const response = await fetch(`/api/trash/${trashId}`, {
|
2026-02-08 22:44:42 +01:00
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: getAuthHeaders()
|
2025-03-24 17:49:53 +01:00
|
|
|
});
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
if (response.ok) {
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Item deleted', 'Item permanently deleted');
|
2025-03-24 17:49:53 +01:00
|
|
|
return true;
|
|
|
|
|
} else {
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error deleting the item');
|
2025-03-24 17:49:53 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error deleting item permanently:', error);
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error deleting the item');
|
2025-03-24 17:49:53 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
/**
|
2026-02-12 09:41:25 +01:00
|
|
|
* Empty the trash
|
|
|
|
|
* @returns {Promise<boolean>} - Operation success
|
2025-03-24 17:49:53 +01:00
|
|
|
*/
|
|
|
|
|
async emptyTrash() {
|
2026-02-08 22:44:42 +01:00
|
|
|
const confirmed = await showConfirmDialog({
|
2026-04-13 15:09:10 +02:00
|
|
|
title: i18n ? i18n.t('dialogs.confirm_empty_trash') : 'Empty trash',
|
|
|
|
|
message: i18n ? i18n.t('trash.empty_confirm') : 'Are you sure you want to empty the trash? This action will permanently delete all items.',
|
|
|
|
|
confirmText: i18n ? i18n.t('actions.empty_trash') : 'Empty trash'
|
2026-02-08 22:44:42 +01:00
|
|
|
});
|
|
|
|
|
if (!confirmed) return false;
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/trash/empty', {
|
2026-02-08 22:44:42 +01:00
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: getAuthHeaders()
|
2025-03-24 17:49:53 +01:00
|
|
|
});
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2025-03-24 17:49:53 +01:00
|
|
|
if (response.ok) {
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Trash emptied', 'The trash has been emptied successfully');
|
2025-03-24 17:49:53 +01:00
|
|
|
return true;
|
|
|
|
|
} else {
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error emptying the trash');
|
2025-03-24 17:49:53 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error emptying trash:', error);
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error emptying the trash');
|
2025-03-24 17:49:53 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
2025-04-02 01:22:05 +02:00
|
|
|
},
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2025-04-02 01:22:05 +02:00
|
|
|
/**
|
2026-02-12 09:41:25 +01:00
|
|
|
* Download a file
|
|
|
|
|
* @param {string} fileId - File ID
|
|
|
|
|
* @param {string} fileName - File name
|
2025-04-02 01:22:05 +02:00
|
|
|
*/
|
2026-02-08 22:44:42 +01:00
|
|
|
async downloadFile(fileId, fileName) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`/api/files/${fileId}`, {
|
|
|
|
|
headers: getAuthHeaders()
|
|
|
|
|
});
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
const blob = await response.blob();
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
link.href = url;
|
|
|
|
|
link.download = fileName;
|
|
|
|
|
document.body.appendChild(link);
|
|
|
|
|
link.click();
|
|
|
|
|
document.body.removeChild(link);
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
} else {
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error downloading the file');
|
2026-02-08 22:44:42 +01:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error downloading file:', error);
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error downloading the file');
|
2026-02-08 22:44:42 +01:00
|
|
|
}
|
2025-04-02 01:22:05 +02:00
|
|
|
},
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2025-04-02 01:22:05 +02:00
|
|
|
/**
|
2026-02-12 09:41:25 +01:00
|
|
|
* Download a folder as ZIP
|
|
|
|
|
* @param {string} folderId - Folder ID
|
|
|
|
|
* @param {string} folderName - Folder name
|
2025-04-02 01:22:05 +02:00
|
|
|
*/
|
|
|
|
|
async downloadFolder(folderId, folderName) {
|
|
|
|
|
try {
|
|
|
|
|
// Show notification to user
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Preparing download', 'Preparing the folder for download...');
|
2026-04-07 22:48:59 +02:00
|
|
|
|
2026-02-08 22:44:42 +01:00
|
|
|
const response = await fetch(`/api/folders/${folderId}/download?format=zip`, {
|
|
|
|
|
headers: getAuthHeaders()
|
|
|
|
|
});
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
const blob = await response.blob();
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
link.href = url;
|
|
|
|
|
link.download = `${folderName}.zip`;
|
|
|
|
|
document.body.appendChild(link);
|
|
|
|
|
link.click();
|
|
|
|
|
document.body.removeChild(link);
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
} else {
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error downloading the folder');
|
2026-02-08 22:44:42 +01:00
|
|
|
}
|
2025-04-02 01:22:05 +02:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error downloading folder:', error);
|
2026-04-13 15:09:10 +02:00
|
|
|
ui.showNotification('Error', 'Error downloading the folder');
|
2025-04-02 01:22:05 +02:00
|
|
|
}
|
2025-03-19 23:28:29 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-13 15:09:10 +02:00
|
|
|
export { fileOps, getAuthHeaders };
|