- Add @aws-sdk/client-s3 dependency for direct S3 operations - Fix listBuckets handler which was hardcoded to return empty array - Now properly queries S3 and returns actual bucket names
333 lines
9.9 KiB
TypeScript
333 lines
9.9 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> {
|
|
// List all buckets
|
|
typedrouter.addTypedHandler(
|
|
new plugins.typedrequest.TypedHandler<interfaces.IReq_ListBuckets>(
|
|
'listBuckets',
|
|
async () => {
|
|
const smartbucket = await tsview.getSmartBucket();
|
|
if (!smartbucket) {
|
|
return { buckets: [] };
|
|
}
|
|
|
|
try {
|
|
const command = new plugins.s3.ListBucketsCommand({});
|
|
const response = await smartbucket.s3Client.send(command) as plugins.s3.ListBucketsCommandOutput;
|
|
const buckets = response.Buckets?.map(b => b.Name).filter((name): name is string => !!name) || [];
|
|
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',
|
|
'png': 'image/png',
|
|
'jpg': 'image/jpeg',
|
|
'jpeg': 'image/jpeg',
|
|
'gif': 'image/gif',
|
|
'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',
|
|
'png': 'image/png',
|
|
'jpg': 'image/jpeg',
|
|
'jpeg': 'image/jpeg',
|
|
'gif': 'image/gif',
|
|
'pdf': 'application/pdf',
|
|
};
|
|
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 };
|
|
}
|
|
}
|
|
)
|
|
);
|
|
}
|