feat(s3): add S3 move (object & prefix) support: server handlers, API client methods, UI dialogs/picker, drag-and-drop and validation

This commit is contained in:
2026-01-28 15:35:28 +00:00
parent 4603154408
commit e379c2b6b1
11 changed files with 1064 additions and 10 deletions

View File

@@ -465,4 +465,131 @@ export async function registerS3Handlers(
}
)
);
// Move object (copy + delete)
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_MoveObject>(
'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<interfaces.IReq_MovePrefix>(
'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<void> => {
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) };
}
}
)
);
}