Files
ObsiGate/frontend/js/upload.js
T
bruno 1db3e0ad2f
CI / lint (push) Successful in 1m20s
CI / security (push) Successful in 50s
CI / test (push) Successful in 2m42s
CI / build (push) Successful in 51s
CI / e2e (push) Successful in 10m29s
feat(dragdrop): drag & drop complet de fichiers/dossiers & intégration IA (#89)
2026-09-14 15:31:12 -04:00

206 lines
6.5 KiB
JavaScript

/**
* ObsiGate — File & Folder Upload & Drag-and-Drop Core Module (#89).
*
* Provides utilities for:
* - Traversing recursive directories and files from DataTransfer / FileSystem API
* - Reading files into base64 / text payloads
* - Batch uploading to `/api/vault/{vault}/batch-upload`
* - Managing drop progress modals and confirmation dialogs
*/
import { api } from './auth.js';
import { t } from './i18n.js';
import { showToast } from './ui.js';
import { refreshSidebarTreePreservingState } from './sidebar.js';
/**
* Recursively read all FileSystemEntry items (files and directories) from DataTransferItemList.
* Falls back to plain File objects if FileSystem API is unavailable.
*
* @param {DataTransferItemList|FileList|File[]} items - DataTransfer items or files
* @returns {Promise<Array<{ path: string, file?: File, isDir?: boolean }>>}
*/
export async function readDataTransferEntries(items) {
const results = [];
if (!items) return results;
// Check if items support webkitGetAsEntry
const entries = [];
if (items.length && typeof items[0].webkitGetAsEntry === 'function') {
for (let i = 0; i < items.length; i++) {
const entry = items[i].webkitGetAsEntry();
if (entry) entries.push(entry);
}
}
if (entries.length > 0) {
// Traverse entries recursively
for (const entry of entries) {
await traverseFileSystemEntry(entry, '', results);
}
} else {
// Plain FileList or files
for (let i = 0; i < items.length; i++) {
const file = items[i] instanceof File ? items[i] : items[i].getAsFile ? items[i].getAsFile() : null;
if (file) {
const relPath = file.webkitRelativePath || file.name;
results.push({
path: relPath.replace(/\\/g, '/'),
file,
isDir: false,
});
}
}
}
return results;
}
/**
* Traverse a single FileSystemEntry recursively.
*/
async function traverseFileSystemEntry(entry, parentPath, results) {
const currentPath = parentPath ? `${parentPath}/${entry.name}` : entry.name;
if (entry.isFile) {
const file = await new Promise((resolve, reject) => {
entry.file(resolve, reject);
});
results.push({
path: currentPath.replace(/\\/g, '/'),
file,
isDir: false,
});
} else if (entry.isDirectory) {
results.push({
path: currentPath.replace(/\\/g, '/'),
isDir: true,
});
const dirReader = entry.createReader();
const readAllEntries = async () => {
const childEntries = await new Promise((resolve, reject) => {
dirReader.readEntries(resolve, reject);
});
if (childEntries.length > 0) {
for (const child of childEntries) {
await traverseFileSystemEntry(child, currentPath, results);
}
await readAllEntries(); // Read next batch (readEntries returns max 100 at a time)
}
};
await readAllEntries();
}
}
/**
* Convert a File object to base64 data string.
* @param {File} file
* @returns {Promise<string>}
*/
export function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const res = reader.result;
if (typeof res === 'string') {
resolve(res);
} else {
reject(new Error('Failed to read file as base64 string'));
}
};
reader.onerror = (err) => reject(err);
reader.readAsDataURL(file);
});
}
/**
* Upload a batch of extracted items into a vault and target folder.
*
* @param {string} vault - Target vault name
* @param {string} targetDir - Target relative directory (empty for root)
* @param {Array<{ path: string, file?: File, isDir?: boolean }>} items - Extracted items
* @param {Object} [options]
* @param {boolean} [options.overwrite=true]
* @param {Function} [options.onProgress] - Callback (current, total)
* @returns {Promise<{ success: boolean, uploaded: string[], created_dirs: string[], errors: any[] }>}
*/
export async function uploadBatch(vault, targetDir, items, options = {}) {
if (!vault || !items || !items.length) {
return { success: true, uploaded: [], created_dirs: [], errors: [], total_files: 0 };
}
const { overwrite = true, onProgress = null } = options;
const payloadFiles = [];
const total = items.length;
for (let i = 0; i < total; i++) {
const item = items[i];
if (onProgress) onProgress(i, total, item.path);
if (item.isDir) {
payloadFiles.push({
path: item.path,
is_dir: true,
});
} else if (item.file) {
try {
const b64 = await fileToBase64(item.file);
payloadFiles.push({
path: item.path,
content: b64,
is_dir: false,
});
} catch (err) {
console.warn(`Failed to encode file ${item.path}:`, err);
}
}
}
if (onProgress) onProgress(total, total, 'Sending payload...');
const response = await api(`/api/vault/${encodeURIComponent(vault)}/batch-upload`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
target_dir: targetDir || '',
files: payloadFiles,
overwrite,
}),
});
return response;
}
/**
* Show a simple upload modal / toast and execute the upload.
*
* @param {string} vault
* @param {string} targetDir
* @param {Array<{ path: string, file?: File, isDir?: boolean }>} items
*/
export async function handleVaultDropUpload(vault, targetDir, items) {
if (!items || !items.length) return;
const fileCount = items.filter((x) => !x.isDir).length;
const dirCount = items.filter((x) => x.isDir).length;
const destName = targetDir ? `${vault}/${targetDir}` : vault;
showToast(`${t('upload.uploading', { count: fileCount }) || `Uploading ${fileCount} file(s) to ${destName}...`}`, 'info');
try {
const res = await uploadBatch(vault, targetDir, items);
if (res && res.success) {
showToast(t('upload.success', { count: res.uploaded.length }) || `Imported ${res.uploaded.length} file(s) successfully`, 'success');
} else if (res && res.uploaded && res.uploaded.length > 0) {
showToast(t('upload.partial', { count: res.uploaded.length, errors: res.errors.length }) || `Imported ${res.uploaded.length} file(s) (${res.errors.length} errors)`, 'warning');
} else {
const errDetail = res?.errors?.[0]?.error || 'Unknown error';
showToast(`${t('upload.error') || 'Upload failed'}: ${errDetail}`, 'error');
}
await refreshSidebarTreePreservingState();
} catch (err) {
console.error('Batch upload error:', err);
showToast(`${t('upload.error') || 'Upload failed'}: ${err.message || err}`, 'error');
}
}