Files
tsview/ts/api/handlers.s3.ts

467 lines
14 KiB
TypeScript

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<void> {
console.log('Registering S3 handlers...');
// List all buckets
console.log('Registering listBuckets handler');
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_ListBuckets>(
'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<interfaces.IReq_CreateBucket>(
'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<interfaces.IReq_DeleteBucket>(
'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<interfaces.IReq_ListObjects>(
'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<string>();
// 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<interfaces.IReq_GetObject>(
'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<string, string> = {
'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<interfaces.IReq_GetObjectMetadata>(
'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<string, string> = {
'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<interfaces.IReq_PutObject>(
'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<interfaces.IReq_DeleteObject>(
'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<interfaces.IReq_CopyObject>(
'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 };
}
}
)
);
// Delete prefix (folder and all contents)
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_DeletePrefix>(
'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<interfaces.IReq_GetObjectUrl>(
'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<string, string> = {
'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;
}
}
)
);
}