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:
93
server/api/files/rename.ts
Normal file
93
server/api/files/rename.ts
Normal 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',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user