Add file rename functionality and improve preview handling

- Implement file/folder rename feature with dialog and API endpoint
- Add rename button to file browser with keyboard shortcuts
- Switch PDF preview from object to embed tag for better compatibility
- Fix CORS issues by fetching preview files as blobs with object URLs
- Add proper cleanup for object URLs to prevent memory leaks
- Add renameObject utility function for MinIO operations
This commit is contained in:
2025-06-04 18:14:00 +02:00
parent 673b6c6748
commit bac1bb2b5e
4 changed files with 317 additions and 16 deletions

View File

@@ -0,0 +1,93 @@
import { renameFile, renameFolder } from '~/server/utils/minio';
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event);
const { oldName, newName, isFolder } = body;
if (!oldName || !newName) {
throw createError({
statusCode: 400,
statusMessage: 'Old name and new name are required',
});
}
// Validate new name doesn't contain invalid characters
const invalidChars = /[<>:"|?*\\]/g;
if (invalidChars.test(newName)) {
throw createError({
statusCode: 400,
statusMessage: 'File name contains invalid characters',
});
}
// For files, we need to preserve the timestamp prefix and path
let oldPath = oldName;
let newPath = '';
if (!isFolder) {
// Extract the directory path and filename
const lastSlash = oldName.lastIndexOf('/');
const directory = lastSlash > -1 ? oldName.substring(0, lastSlash + 1) : '';
const oldFilename = lastSlash > -1 ? oldName.substring(lastSlash + 1) : oldName;
// Extract timestamp prefix if present
const timestampMatch = oldFilename.match(/^(\d{10,})-(.+)$/);
let timestamp = '';
if (timestampMatch) {
timestamp = timestampMatch[1] + '-';
}
// Preserve file extension
const oldExtIndex = oldFilename.lastIndexOf('.');
const extension = oldExtIndex > -1 ? oldFilename.substring(oldExtIndex) : '';
// Remove extension from new name if user included it
let cleanNewName = newName;
if (newName.endsWith(extension)) {
cleanNewName = newName.substring(0, newName.length - extension.length);
}
// Construct new path with preserved timestamp and extension
newPath = directory + timestamp + cleanNewName + extension;
} else {
// For folders, handle the path construction
const lastSlash = oldName.lastIndexOf('/', oldName.length - 2);
const directory = lastSlash > -1 ? oldName.substring(0, lastSlash + 1) : '';
// Ensure folder name ends with /
const folderName = newName.endsWith('/') ? newName : newName + '/';
newPath = directory + folderName;
}
// Check if new name already exists (you might want to implement this check)
// const exists = await checkFileExists(newPath);
// if (exists) {
// throw createError({
// statusCode: 409,
// statusMessage: 'A file or folder with this name already exists',
// });
// }
// Perform the rename
if (isFolder) {
await renameFolder(oldPath, newPath);
} else {
await renameFile(oldPath, newPath);
}
return {
success: true,
oldPath,
newPath,
message: `Successfully renamed ${isFolder ? 'folder' : 'file'}`,
};
} catch (error: any) {
console.error('Failed to rename:', error);
throw createError({
statusCode: 500,
statusMessage: error.message || 'Failed to rename',
});
}
});

View File

@@ -136,7 +136,13 @@ export const getDownloadUrl = async (fileName: string, expiry: number = 60 * 60)
const bucketName = useRuntimeConfig().minio.bucketName;
// Extract just the filename from the full path
const filename = fileName.split('/').pop() || fileName;
let filename = fileName.split('/').pop() || fileName;
// Remove timestamp prefix if present (e.g., "1234567890-filename.pdf" -> "filename.pdf")
const timestampMatch = filename.match(/^\d{10,}-(.+)$/);
if (timestampMatch) {
filename = timestampMatch[1];
}
// Force download with Content-Disposition header
const responseHeaders = {
@@ -220,3 +226,74 @@ export const getPreviewUrl = async (fileName: string, contentType: string) => {
return await client.presignedGetObject(bucketName, fileName, 60 * 60, responseHeaders);
};
// Rename file (copy and delete)
export const renameFile = async (oldPath: string, newPath: string) => {
const client = getMinioClient();
const bucketName = useRuntimeConfig().minio.bucketName;
try {
// Copy the object to the new name
await client.copyObject(
bucketName,
newPath,
`/${bucketName}/${oldPath}`
);
// Delete the old object
await client.removeObject(bucketName, oldPath);
return true;
} catch (error) {
console.error('Error renaming file:', error);
throw error;
}
};
// Rename folder (copy all contents and delete)
export const renameFolder = async (oldPath: string, newPath: string) => {
const client = getMinioClient();
const bucketName = useRuntimeConfig().minio.bucketName;
// Ensure paths end with /
const oldPrefix = oldPath.endsWith('/') ? oldPath : oldPath + '/';
const newPrefix = newPath.endsWith('/') ? newPath : newPath + '/';
// List all objects in the folder
const objectsList: string[] = [];
return new Promise((resolve, reject) => {
const stream = client.listObjectsV2(bucketName, oldPrefix, true);
stream.on('data', (obj) => {
if (obj && obj.name) {
objectsList.push(obj.name);
}
});
stream.on('error', reject);
stream.on('end', async () => {
try {
// Copy all objects to new location
for (const objectName of objectsList) {
const newObjectName = objectName.replace(oldPrefix, newPrefix);
await client.copyObject(
bucketName,
newObjectName,
`/${bucketName}/${objectName}`
);
}
// Delete all old objects
if (objectsList.length > 0) {
await client.removeObjects(bucketName, objectsList);
}
resolve(true);
} catch (error) {
reject(error);
}
});
});
};