UNPKG

@makolabs/ripple

Version:

Simple Svelte 5 powered component library ✨

195 lines (194 loc) 8.23 kB
import { BaseAdapter } from './BaseAdapter.js'; /** * S3 storage adapter for the FileBrowser component */ export class S3Adapter extends BaseAdapter { basePath; constructor(basePath, publicS3BasePath) { super(); this.basePath = basePath || publicS3BasePath || 'export/clarkfin/'; } getName() { return 'S3'; } async isConfigured() { // S3 should always be configured if the environment variables are set return true; } // S3 does not require authentication, but we implement the method to satisfy the interface async authenticate() { return true; // S3 is considered authenticated if isConfigured() returns true } // Override setReopenFlag to store the S3 browser reopening flag setReopenFlag() { if (typeof localStorage !== 'undefined') { localStorage.setItem('reopen_s3_browser', 'true'); } } // Check if the S3 browser should be reopened shouldReopenBrowser() { if (typeof localStorage !== 'undefined') { return localStorage.getItem('reopen_s3_browser') === 'true'; } return false; } // Clear the S3 browser reopening flag clearReopenFlag() { if (typeof localStorage !== 'undefined') { localStorage.removeItem('reopen_s3_browser'); } } async list(path, searchQuery) { try { // Ensure path ends with a forward slash const normalizedPath = path.endsWith('/') ? path : path + '/'; // Build API URL including search term if provided let apiUrl = `/api/s3/list?prefix=${encodeURIComponent(normalizedPath)}`; if (searchQuery) { apiUrl += `&search=${encodeURIComponent(searchQuery)}`; } // Use the API endpoint for listing S3 files const response = await fetch(apiUrl); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to fetch files'); } const result = await response.json(); // Transform folders - use the folderName property from the API const folders = (result.folders || []) .map((folder) => { return { key: folder.prefix, // Use the folderName from the API if available, otherwise extract it from the prefix name: folder.folderName || this.extractFolderName(folder.prefix, normalizedPath), lastModified: new Date(), size: 0, isFolder: true }; }) .filter((folder) => folder.name); // Filter out empty folder names // Transform files - only include files that are directly in this directory const fileItems = (result.files || []) .filter((file) => { // Skip the directory marker itself if it exists as a file if (file.key === normalizedPath) return false; // Get the relative path within the current directory const relativePath = file.key.slice(normalizedPath.length); // Only include files that don't have additional path separators // (indicating they're directly in this directory) return !relativePath.includes('/'); }) .map((file) => ({ key: file.key, name: this.removeFileExtensions(file.key.slice(normalizedPath.length)), lastModified: new Date(file.lastModified), size: file.size, isFolder: false })); // Combine folders and files const files = [...folders, ...fileItems]; // Sort by lastModified in reverse order (newest first), but keep folders first files.sort((a, b) => { // Keep folders first if (a.isFolder !== b.isFolder) { return a.isFolder ? -1 : 1; } // Sort by lastModified in reverse order (newest first) return b.lastModified.getTime() - a.lastModified.getTime(); }); // Create breadcrumbs const breadcrumbs = this.createBreadcrumbs(normalizedPath); // Determine parent path const pathParts = normalizedPath.split('/').filter(Boolean); let parentPath = ''; if (pathParts.length > 0) { // Remove the last part and join the rest pathParts.pop(); parentPath = pathParts.length > 0 ? pathParts.join('/') + '/' : this.basePath; // Ensure we never navigate outside the basePath if (!parentPath.startsWith(this.basePath)) { parentPath = this.basePath; } } // Debug log console.log(`S3 listing path: ${normalizedPath}, files: ${fileItems.length}, folders: ${folders.length}`); if (folders.length > 0) { console.log('Sample folder names:', folders.slice(0, 3).map((f) => f.name)); } return { files, currentPath: normalizedPath, parentPath: normalizedPath !== this.basePath ? parentPath : undefined, breadcrumbs }; } catch (err) { console.error('Error fetching S3 files:', err); throw err; } } async download(file) { try { const response = await fetch(`/api/s3/download?key=${encodeURIComponent(file.key)}`); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to download file'); } const url = await response.text(); return url; } catch (err) { console.error('Error downloading S3 file:', err); throw err; } } // Helper method to remove file extensions removeFileExtensions(filename) { // First remove .csv.gz if it exists const name = filename.replace(/\.csv\.gz$/, ''); // Then remove any remaining extension return name.replace(/\.[^/.]+$/, ''); } // Helper method to create breadcrumbs createBreadcrumbs(path) { // If we're at base path, reset breadcrumbs if (path === this.basePath) { return [{ name: 'S3', path: this.basePath, current: true, clickable: true }]; } // Extract parts for display const pathWithoutBase = path.startsWith(this.basePath) ? path.slice(this.basePath.length) : ''; // Add base path as the first item const breadcrumbs = [{ name: 'S3', path: this.basePath, current: false, clickable: true }]; // Add current directory parts if (pathWithoutBase) { const parts = pathWithoutBase.split('/').filter(Boolean); let currentPath = this.basePath; parts.forEach((part, index) => { currentPath += part + '/'; breadcrumbs.push({ name: part, path: currentPath, current: index === parts.length - 1, clickable: true }); }); } return breadcrumbs; } // Helper method to extract folder name from a prefix extractFolderName(prefix, currentPath) { // If the prefix starts with the current path, extract the next segment if (prefix.startsWith(currentPath)) { const relativePath = prefix.slice(currentPath.length); return relativePath.split('/')[0]; } // Fallback: use the last non-empty segment before the trailing slash const segments = prefix.split('/').filter(Boolean); return segments.length > 0 ? segments[segments.length - 1] : ''; } // Implement the abstract method from BaseAdapter getApiPath() { return 's3'; } }