UNPKG

r2drive

Version:

A modern desktop file manager for Cloudflare R2 storage with intuitive drag & drop interface

369 lines (318 loc) 11.4 kB
// Interfaz para el objeto de retorno de la API de listado export interface BucketListResponse { success: boolean; prefix: string; files: Array<{ key: string; name: string; size: number; lastModified: Date; isFolder: boolean; }>; folders: Array<{ prefix: string; name: string; }>; parentFolder: string; } // Interfaz para el seguimiento del progreso export interface UploadProgress { totalFiles: number; uploadedFiles: number; currentFile: string; percentComplete: number; } // Función para verificar la conexión al bucket usando la API export async function verifyBucketConnection(): Promise<boolean> { try { const response = await fetch('/api/r2/verify'); const data = await response.json(); return data.success && data.connected; } catch (error) { console.error('Error al verificar la conexión al bucket:', error); return false; } } // Función para listar objetos del bucket export async function listBucketObjects(prefix: string = ''): Promise<BucketListResponse> { try { const response = await fetch(`/api/r2/list?prefix=${encodeURIComponent(prefix)}`); if (!response.ok) { throw new Error('Error al listar objetos del bucket'); } return await response.json(); } catch (error) { console.error('Error al listar objetos del bucket:', error); throw error; } } // Función para subir un archivo usando la API export async function uploadFile( file: File, path: string, onProgress?: (progress: number) => void ): Promise<string> { return new Promise((resolve, reject) => { try { // Crear FormData para enviar el archivo const formData = new FormData(); formData.append('file', file); formData.append('path', path); // Usar XMLHttpRequest para poder monitorear el progreso const xhr = new XMLHttpRequest(); // Manejar el evento de progreso xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable && onProgress) { const percentComplete = Math.round((event.loaded / event.total) * 100); onProgress(percentComplete); } }); // Manejar la finalización de la solicitud xhr.addEventListener('load', () => { if (xhr.status >= 200 && xhr.status < 300) { try { const response = JSON.parse(xhr.responseText); if (onProgress) onProgress(100); resolve(response.key); } catch { reject(new Error('Error al analizar la respuesta del servidor')); } } else { let errorMessage = 'Error al subir el archivo'; try { const errorData = JSON.parse(xhr.responseText); errorMessage = errorData.error || errorMessage; } catch { // Si no se puede analizar el error, usar el mensaje predeterminado } reject(new Error(errorMessage)); } }); // Manejar errores de red xhr.addEventListener('error', () => { reject(new Error('Error de red al subir el archivo')); }); // Manejar tiempo de espera agotado xhr.addEventListener('timeout', () => { reject(new Error('Tiempo de espera agotado al subir el archivo')); }); // Abrir y enviar la solicitud xhr.open('POST', '/api/r2/upload'); xhr.send(formData); } catch (error) { console.error('Error al subir archivo:', error); reject(error); } }); } export async function uploadDirectory( entry: FileSystemDirectoryEntry, currentPath: string = '', onProgress?: (progress: UploadProgress) => void ): Promise<string[]> { const uploadedFiles: string[] = []; let totalFilesCount = 0; let processedFilesCount = 0; // Primera pasada para contar archivos async function countFiles(entry: FileSystemDirectoryEntry): Promise<number> { const reader = entry.createReader(); const readEntries = (): Promise<FileSystemEntry[]> => { return new Promise((resolve, reject) => { reader.readEntries(resolve, reject); }); }; let count = 0; let entries: FileSystemEntry[] = []; do { const batch = await readEntries(); if (batch.length === 0) break; entries = entries.concat(batch); } while (true); for (const entry of entries) { if (entry.isFile) { count++; } else if (entry.isDirectory) { count += await countFiles(entry as FileSystemDirectoryEntry); } } return count; } // Contar archivos primero totalFilesCount = await countFiles(entry); if (onProgress) { onProgress({ totalFiles: totalFilesCount, uploadedFiles: 0, currentFile: '', percentComplete: 0 }); } // Normalizar el path para que siempre termine con / si no está vacío let uploadPath = currentPath || ''; if (uploadPath && !uploadPath.endsWith('/') && uploadPath !== '') { uploadPath = `${uploadPath}/`; } // Verificar si la ruta ya termina con el nombre del directorio // Esto evita la duplicación de carpetas como "Cueva/Cueva" const dirName = entry.name; const pathSegments = uploadPath.split('/').filter(Boolean); const lastSegment = pathSegments[pathSegments.length - 1]; // Construir el path base para los archivos let basePath; if (lastSegment === dirName) { // Si la última parte de la ruta ya coincide con el nombre del directorio, // no añadir el nombre de nuevo basePath = uploadPath; console.log(`La carpeta ${dirName} ya está incluida en la ruta ${uploadPath}. Evitando duplicación.`); } else { // Caso normal: añadir el nombre del directorio a la ruta basePath = uploadPath ? `${uploadPath}${dirName}` : dirName; } console.log(`Subiendo directorio ${entry.name} a la ruta: ${basePath}`); const processEntry = async (entry: FileSystemEntry, path: string) => { if (entry.isFile) { const fileEntry = entry as FileSystemFileEntry; const file: File = await new Promise((resolve, reject) => { fileEntry.file(resolve, reject); }); if (onProgress) { onProgress({ totalFiles: totalFilesCount, uploadedFiles: processedFilesCount, currentFile: file.name, percentComplete: Math.floor((processedFilesCount / totalFilesCount) * 100) }); } const key = await uploadFile(file, path); uploadedFiles.push(key); processedFilesCount++; if (onProgress) { onProgress({ totalFiles: totalFilesCount, uploadedFiles: processedFilesCount, currentFile: file.name, percentComplete: Math.floor((processedFilesCount / totalFilesCount) * 100) }); } } else if (entry.isDirectory) { const dirEntry = entry as FileSystemDirectoryEntry; // Aplicar la misma lógica de no duplicación para subdirectorios const dirName = entry.name; const pathSegments = path.split('/').filter(Boolean); const lastSegment = pathSegments[pathSegments.length - 1]; let subDirPath; if (lastSegment === dirName) { // Si la ruta ya termina con el nombre del directorio, no duplicar subDirPath = path; } else { subDirPath = `${path}/${dirName}`; } const subDirectoryFiles = await uploadDirectoryContents(dirEntry, subDirPath); uploadedFiles.push(...subDirectoryFiles); } }; const uploadDirectoryContents = async (dirEntry: FileSystemDirectoryEntry, path: string): Promise<string[]> => { const subDirFiles: string[] = []; let entries: FileSystemEntry[] = []; // Crear un nuevo reader específico para este directorio const reader = dirEntry.createReader(); const readEntries = (): Promise<FileSystemEntry[]> => { return new Promise((resolve, reject) => { reader.readEntries(resolve, reject); }); }; do { const batch = await readEntries(); if (batch.length === 0) break; entries = entries.concat(batch); } while (true); for (const entry of entries) { if (entry.isFile) { const fileEntry = entry as FileSystemFileEntry; const file: File = await new Promise((resolve, reject) => { fileEntry.file(resolve, reject); }); if (onProgress) { onProgress({ totalFiles: totalFilesCount, uploadedFiles: processedFilesCount, currentFile: file.name, percentComplete: Math.floor((processedFilesCount / totalFilesCount) * 100) }); } const key = await uploadFile(file, path); subDirFiles.push(key); console.log(`Subido archivo ${file.name} a ${path}`); processedFilesCount++; if (onProgress) { onProgress({ totalFiles: totalFilesCount, uploadedFiles: processedFilesCount, currentFile: file.name, percentComplete: Math.floor((processedFilesCount / totalFilesCount) * 100) }); } } else if (entry.isDirectory) { const dirEntry = entry as FileSystemDirectoryEntry; // Aplicar la misma lógica de no duplicación para subdirectorios const dirName = entry.name; const pathSegments = path.split('/').filter(Boolean); const lastSegment = pathSegments[pathSegments.length - 1]; let subDirPath; if (lastSegment === dirName) { // Si la ruta ya termina con el nombre del directorio, no duplicar subDirPath = path; } else { subDirPath = `${path}/${dirName}`; } const subDirResult = await uploadDirectoryContents(dirEntry, subDirPath); subDirFiles.push(...subDirResult); } } return subDirFiles; }; await processEntry(entry, basePath); return uploadedFiles; } // Función para crear una carpeta en el bucket export async function createFolder(folderName: string, prefix: string = ''): Promise<string> { try { const response = await fetch('/api/r2/create-folder', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ folderName, prefix }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Error al crear carpeta'); } const data = await response.json(); return data.folderPath; } catch (error) { console.error('Error al crear carpeta:', error); throw error; } } // Función para eliminar un objeto del bucket (archivo o carpeta) export async function deleteObject(key: string, isFolder: boolean): Promise<boolean> { try { const response = await fetch('/api/r2/delete', { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ key, isFolder }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Error al eliminar objeto'); } const data = await response.json(); return data.success; } catch (error) { console.error('Error al eliminar objeto:', error); throw error; } }