import * as plugins from '../plugins.js'; import type * as interfaces from '../interfaces/index.js'; import type { TsView } from '../tsview.classes.tsview.js'; /** * Register S3 API handlers */ export async function registerS3Handlers( typedrouter: plugins.typedrequest.TypedRouter, tsview: TsView ): Promise { console.log('Registering S3 handlers...'); // List all buckets console.log('Registering listBuckets handler'); typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'listBuckets', async () => { console.log('listBuckets handler called'); const smartbucket = await tsview.getSmartBucket(); console.log('smartbucket:', smartbucket ? 'initialized' : 'null'); if (!smartbucket) { console.log('returning empty buckets'); return { buckets: [] }; } try { const command = new plugins.s3.ListBucketsCommand({}); console.log('sending ListBucketsCommand...'); const response = await smartbucket.s3Client.send(command) as plugins.s3.ListBucketsCommandOutput; console.log('response:', response); const buckets = response.Buckets?.map(b => b.Name).filter((name): name is string => !!name) || []; console.log('returning buckets:', buckets); return { buckets }; } catch (err) { console.error('Error listing buckets:', err); return { buckets: [] }; } } ) ); // Create bucket typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'createBucket', async (reqData) => { const smartbucket = await tsview.getSmartBucket(); if (!smartbucket) { return { success: false }; } try { await smartbucket.createBucket(reqData.bucketName); return { success: true }; } catch (err) { console.error('Error creating bucket:', err); return { success: false }; } } ) ); // Delete bucket typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'deleteBucket', async (reqData) => { const smartbucket = await tsview.getSmartBucket(); if (!smartbucket) { return { success: false }; } try { await smartbucket.removeBucket(reqData.bucketName); return { success: true }; } catch (err) { console.error('Error deleting bucket:', err); return { success: false }; } } ) ); // List objects in bucket typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'listObjects', async (reqData) => { const smartbucket = await tsview.getSmartBucket(); if (!smartbucket) { return { objects: [], prefixes: [] }; } try { const bucket = await smartbucket.getBucketByName(reqData.bucketName); if (!bucket) { return { objects: [], prefixes: [] }; } const prefix = reqData.prefix || ''; const delimiter = reqData.delimiter || '/'; // Get the base directory or subdirectory const baseDir = await bucket.getBaseDirectory(); let targetDir = baseDir; if (prefix) { // Navigate to the prefix directory const prefixParts = prefix.replace(/\/$/, '').split('/').filter(Boolean); for (const part of prefixParts) { const subDir = await targetDir.getSubDirectoryByName(part, { getEmptyDirectory: true }); if (subDir) { targetDir = subDir; } else { return { objects: [], prefixes: [] }; } } } const objects: interfaces.IS3Object[] = []; const prefixSet = new Set(); // List files in current directory const files = await targetDir.listFiles(); for (const file of files) { const fullPath = prefix + file.name; objects.push({ key: fullPath, isPrefix: false, }); } // List subdirectories const dirs = await targetDir.listDirectories(); for (const dir of dirs) { const fullPrefix = prefix + dir.name + '/'; prefixSet.add(fullPrefix); } return { objects, prefixes: Array.from(prefixSet), }; } catch (err) { console.error('Error listing objects:', err); return { objects: [], prefixes: [] }; } } ) ); // Get object content typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getObject', async (reqData) => { const smartbucket = await tsview.getSmartBucket(); if (!smartbucket) { throw new Error('S3 not configured'); } try { const bucket = await smartbucket.getBucketByName(reqData.bucketName); if (!bucket) { throw new Error(`Bucket ${reqData.bucketName} not found`); } const content = await bucket.fastGet({ path: reqData.key }); const stats = await bucket.fastStat({ path: reqData.key }); // Determine content type from extension const ext = reqData.key.split('.').pop()?.toLowerCase() || ''; const contentTypeMap: Record = { 'json': 'application/json', 'txt': 'text/plain', 'html': 'text/html', 'css': 'text/css', 'js': 'application/javascript', 'ts': 'text/plain', 'tsx': 'text/plain', 'jsx': 'text/plain', 'md': 'text/markdown', 'csv': 'text/csv', 'yaml': 'text/yaml', 'yml': 'text/yaml', 'log': 'text/plain', 'sh': 'text/plain', 'env': 'text/plain', 'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'gif': 'image/gif', 'webp': 'image/webp', 'svg': 'image/svg+xml', 'pdf': 'application/pdf', 'xml': 'application/xml', }; const contentType = contentTypeMap[ext] || 'application/octet-stream'; return { content: content.toString('base64'), contentType, size: stats?.ContentLength || content.length, lastModified: stats?.LastModified?.toISOString() || new Date().toISOString(), }; } catch (err) { console.error('Error getting object:', err); throw err; } } ) ); // Get object metadata typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getObjectMetadata', async (reqData) => { const smartbucket = await tsview.getSmartBucket(); if (!smartbucket) { throw new Error('S3 not configured'); } try { const bucket = await smartbucket.getBucketByName(reqData.bucketName); if (!bucket) { throw new Error(`Bucket ${reqData.bucketName} not found`); } const stats = await bucket.fastStat({ path: reqData.key }); const ext = reqData.key.split('.').pop()?.toLowerCase() || ''; const contentTypeMap: Record = { 'json': 'application/json', 'txt': 'text/plain', 'html': 'text/html', 'css': 'text/css', 'js': 'application/javascript', 'ts': 'text/plain', 'tsx': 'text/plain', 'jsx': 'text/plain', 'md': 'text/markdown', 'csv': 'text/csv', 'yaml': 'text/yaml', 'yml': 'text/yaml', 'log': 'text/plain', 'sh': 'text/plain', 'env': 'text/plain', 'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'gif': 'image/gif', 'webp': 'image/webp', 'svg': 'image/svg+xml', 'pdf': 'application/pdf', 'xml': 'application/xml', }; const contentType = contentTypeMap[ext] || 'application/octet-stream'; return { contentType, size: stats?.ContentLength || 0, lastModified: stats?.LastModified?.toISOString() || new Date().toISOString(), }; } catch (err) { console.error('Error getting object metadata:', err); throw err; } } ) ); // Put object typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'putObject', async (reqData) => { const smartbucket = await tsview.getSmartBucket(); if (!smartbucket) { return { success: false }; } try { const bucket = await smartbucket.getBucketByName(reqData.bucketName); if (!bucket) { return { success: false }; } const content = Buffer.from(reqData.content, 'base64'); await bucket.fastPut({ path: reqData.key, contents: content, }); return { success: true }; } catch (err) { console.error('Error putting object:', err); return { success: false }; } } ) ); // Delete object typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'deleteObject', async (reqData) => { const smartbucket = await tsview.getSmartBucket(); if (!smartbucket) { return { success: false }; } try { const bucket = await smartbucket.getBucketByName(reqData.bucketName); if (!bucket) { return { success: false }; } await bucket.fastRemove({ path: reqData.key }); return { success: true }; } catch (err) { console.error('Error deleting object:', err); return { success: false }; } } ) ); // Copy object typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'copyObject', async (reqData) => { const smartbucket = await tsview.getSmartBucket(); if (!smartbucket) { return { success: false }; } try { const sourceBucket = await smartbucket.getBucketByName(reqData.sourceBucket); const destBucket = await smartbucket.getBucketByName(reqData.destBucket); if (!sourceBucket || !destBucket) { return { success: false }; } // Read from source const content = await sourceBucket.fastGet({ path: reqData.sourceKey }); // Write to destination await destBucket.fastPut({ path: reqData.destKey, contents: content, }); return { success: true }; } catch (err) { console.error('Error copying object:', err); return { success: false }; } } ) ); }