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, overwrite: true, }); 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, overwrite: true, }); return { success: true }; } catch (err) { console.error('Error copying object:', err); return { success: false }; } } ) ); // Delete prefix (folder and all contents) typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'deletePrefix', 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 baseDir = await bucket.getBaseDirectory(); let targetDir = baseDir; // Navigate to the prefix directory const prefix = reqData.prefix.replace(/\/$/, ''); const prefixParts = prefix.split('/').filter(Boolean); for (const part of prefixParts) { const subDir = await targetDir.getSubDirectoryByName(part, { getEmptyDirectory: true }); if (subDir) { targetDir = subDir; } else { return { success: false }; } } // Delete the directory and all its contents await targetDir.delete({ mode: 'permanent' }); return { success: true }; } catch (err) { console.error('Error deleting prefix:', err); return { success: false }; } } ) ); // Get object URL (for downloads) typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getObjectUrl', 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`); } // Get the content and create a data URL const content = await bucket.fastGet({ 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'; const base64 = content.toString('base64'); const url = `data:${contentType};base64,${base64}`; return { url }; } catch (err) { console.error('Error getting object URL:', err); throw err; } } ) ); // Move object (copy + delete) typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'moveObject', async (reqData) => { const smartbucket = await tsview.getSmartBucket(); if (!smartbucket) { return { success: false, error: 'S3 not configured' }; } try { const bucket = await smartbucket.getBucketByName(reqData.bucketName); if (!bucket) { return { success: false, error: `Bucket ${reqData.bucketName} not found` }; } // Read source content const content = await bucket.fastGet({ path: reqData.sourceKey }); // Write to destination await bucket.fastPut({ path: reqData.destKey, contents: content, overwrite: true, }); // Delete source await bucket.fastRemove({ path: reqData.sourceKey }); return { success: true }; } catch (err) { console.error('Error moving object:', err); return { success: false, error: String(err) }; } } ) ); // Move prefix (folder) - copy all objects then delete all typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'movePrefix', async (reqData) => { const smartbucket = await tsview.getSmartBucket(); if (!smartbucket) { return { success: false, movedCount: 0, error: 'S3 not configured' }; } try { const bucket = await smartbucket.getBucketByName(reqData.bucketName); if (!bucket) { return { success: false, movedCount: 0, error: `Bucket ${reqData.bucketName} not found` }; } // List all objects under the source prefix recursively const allObjects: string[] = []; const listRecursive = async (prefix: string): Promise => { const baseDir = await bucket.getBaseDirectory(); let targetDir = baseDir; if (prefix) { 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; } } } // Get files in this directory const files = await targetDir.listFiles(); for (const file of files) { allObjects.push(prefix + file.name); } // Recurse into subdirectories const dirs = await targetDir.listDirectories(); for (const dir of dirs) { await listRecursive(prefix + dir.name + '/'); } }; await listRecursive(reqData.sourcePrefix); // Copy all objects to new location for (const objKey of allObjects) { const relativePath = objKey.substring(reqData.sourcePrefix.length); const newKey = reqData.destPrefix + relativePath; const content = await bucket.fastGet({ path: objKey }); await bucket.fastPut({ path: newKey, contents: content, overwrite: true, }); } // Delete the source directory const baseDir = await bucket.getBaseDirectory(); let targetDir = baseDir; const prefix = reqData.sourcePrefix.replace(/\/$/, ''); const prefixParts = prefix.split('/').filter(Boolean); for (const part of prefixParts) { const subDir = await targetDir.getSubDirectoryByName(part, { getEmptyDirectory: true }); if (subDir) { targetDir = subDir; } else { return { success: false, movedCount: 0, error: 'Source folder not found' }; } } await targetDir.delete({ mode: 'permanent' }); return { success: true, movedCount: allObjects.length }; } catch (err) { console.error('Error moving prefix:', err); return { success: false, movedCount: 0, error: String(err) }; } } ) ); }