@makolabs/ripple
Version:
Simple Svelte 5 powered component library ✨
195 lines (194 loc) • 8.23 kB
JavaScript
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';
}
}