diff --git a/.gitignore b/.gitignore index d5fd851..3c36a35 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,5 @@ yarn-error.log* **/*.log package-lock.json **/*.bun -idea/ \ No newline at end of file +idea/ + diff --git a/src/app.ts b/src/app.ts index 546e630..1d447c8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,6 +6,7 @@ import {addGetFileEndpoint} from './endpoints/file/getFile'; import {addUploadFileEndpoint} from './endpoints/file/uploadFile'; import {addCheckFileExistsEndpoint} from './endpoints/file/checkFileExists'; import {addGetFileSizeEndpoint} from './endpoints/file/getFileSize'; +import { addCopyFileEndpoint } from './endpoints/file/copyFile'; export function buildApp() { let app = new Elysia(); @@ -20,5 +21,6 @@ export function buildApp() { addGetFileSizeEndpoint(app); addUploadFileEndpoint(app); addCheckFileExistsEndpoint(app); + addCopyFileEndpoint(app); return app; } diff --git a/src/constants/commonResponses.ts b/src/constants/commonResponses.ts index b61a821..15a686b 100644 --- a/src/constants/commonResponses.ts +++ b/src/constants/commonResponses.ts @@ -13,6 +13,14 @@ export const unsupportedBinaryDataTypeMsg = 'Unsupported binary data type'; export const invalidFileFormatInFormDataMsg = 'Invalid file format in form data'; export const noFileFieldInFormDataMsg = 'No file field in form data'; export const invalidFormDataStructure = 'Invalid structure for multipart/form-data'; +export const invalidOverwriteFlagMsg = 'Parameter "overwrite" must be either "true" or "false"'; + +// For operations involving a source and a destination +// such as copy and move +export const invalidSourcePathMsg = 'Invalid source file path'; +export const invalidDestinationPathMsg = 'Invalid destination file path'; +export const sourceFileDoesNotExistOrIsADirectoryMsg = 'Source file does not exist or is a directory'; +export const destinationFileExistsAndOverwriteIsNotSetMsg = 'Destination file exists and overwrite is not set'; // Internal server errors export const dirCreateFailMsg = 'Could not create directory'; diff --git a/src/endpoints/file/copyFile.ts b/src/endpoints/file/copyFile.ts new file mode 100644 index 0000000..160d156 --- /dev/null +++ b/src/endpoints/file/copyFile.ts @@ -0,0 +1,67 @@ +import Elysia from 'elysia'; +import { validateRelativePath } from '../../utils/pathUtils'; +import { copyFile } from '../../utils/fileUtils'; +import { + invalidOverwriteFlagMsg, + invalidSourcePathMsg, + invalidDestinationPathMsg, + sourceFileDoesNotExistOrIsADirectoryMsg, + destinationFileExistsAndOverwriteIsNotSetMsg +} from '../../constants/commonResponses' + +export function addCopyFileEndpoint(app: Elysia) { + return app.post('file/copy', async ({ query, set }) => { + + const sourcePathValidationResult = validateRelativePath(query.source); + if (sourcePathValidationResult.isErr()) { + set.status = 400; + return invalidSourcePathMsg; + } + const sourcePath = sourcePathValidationResult.value; + + const destinationPathValidationResult = validateRelativePath(query.destination); + if (destinationPathValidationResult.isErr()) { + set.status = 400; + return invalidDestinationPathMsg; + } + const destinationPath = destinationPathValidationResult.value; + + let overwrite = false; + if (query.overwrite) { + if (query.overwrite === "true") + overwrite = true; + else if (query.overwrite === "false") + overwrite = false; + else { + set.status = 400; + return invalidOverwriteFlagMsg; + } + } + + let copyResult = await copyFile( + sourcePath.absolutePath, + destinationPath.absolutePath, + overwrite); + + if (copyResult.isOk()) { + return Response("", { status: 204 }); + } + else { + switch (copyResult.error) { + case sourceFileDoesNotExistOrIsADirectoryMsg: { + set.status = 404; + break; + } + case destinationFileExistsAndOverwriteIsNotSetMsg: { + set.status = 409; + break; + } + default: { + set.status = 500; + break; + } + } + return copyResult.error; + } + }); +} diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts index 28137e1..1830235 100644 --- a/src/utils/fileUtils.ts +++ b/src/utils/fileUtils.ts @@ -2,9 +2,11 @@ import {BunFile} from 'bun'; import Bun from 'bun'; import {Result, err, ok} from 'neverthrow'; import { + destinationFileExistsAndOverwriteIsNotSetMsg, dirCreateFailMsg, fileAlreadyExistsMsg, fileMustNotEmptyMsg, + sourceFileDoesNotExistOrIsADirectoryMsg, unknownErrorMsg, } from '../constants/commonResponses'; import {dirname} from 'path'; @@ -62,3 +64,47 @@ export async function writeFile( return err(unknownErrorMsg); } } + +/** + * Copies the file specified by {@link absoluteSourcePath} + * to the path specified by {@link absoluteDestinationPath}. + * Creates the directories specified by {@link absoluteDestinationPath} + * if they do not exist. + * @param absoluteSourcePath The absolute path to the source file. + * @param absoluteDestinationPath The absolute path to the destination file. + * @param overwrite Whether to overwrite the file at the destination + * path if there is one already. + * @returns Returns true if the file was written successfully, + * or an error message string if there was an error. + */ +export async function copyFile( + absoluteSourcePath: string, + absoluteDestinationPath: string, + overwrite: boolean +) : Promise> { + let sourceFile = null; + try { + + // Get source file + sourceFile = await getFile(absoluteSourcePath); + + if (sourceFile === null) + return err(sourceFileDoesNotExistOrIsADirectoryMsg); + + } catch (error) { + return err(`An error occurred while reading the source file (detail: ${error})`); + } + + const fileWriteResult = await writeFile(absoluteDestinationPath, sourceFile, overwrite); + if (fileWriteResult.isOk()) { + return ok(true); + } + else { + if (fileWriteResult.error == fileAlreadyExistsMsg) { + return err(destinationFileExistsAndOverwriteIsNotSetMsg); + } + else { + return err(`An error occurred while copying the file (detail: ${fileWriteResult.error})`); + } + } +} \ No newline at end of file