From 656bd3b3d9b671dd0faba21e3a3d69c1624b2721 Mon Sep 17 00:00:00 2001 From: Sathvik Hebbar Date: Sat, 7 Oct 2023 01:20:35 +0530 Subject: [PATCH 1/4] Add copy file endpoint code --- src/app.ts | 2 + src/endpoints/file/copyFile.ts | 79 ++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 src/endpoints/file/copyFile.ts 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/endpoints/file/copyFile.ts b/src/endpoints/file/copyFile.ts new file mode 100644 index 0000000..a24dd87 --- /dev/null +++ b/src/endpoints/file/copyFile.ts @@ -0,0 +1,79 @@ +import Elysia from 'elysia'; +import { isPathValid } from '../../utils/pathUtils'; +import { getConfig } from '../../utils/config'; +import { join } from 'path'; +import { getFile, writeFile } from '../../utils/fileUtils'; +import { fileAlreadyExistsMsg } from '../../constants/commonResponses' + + +export function addCopyFileEndpoint(app: Elysia) { + return app.get('file/copy', async ({ query, set }) => { + + // Verify that the SOURCE file path is valid + let relativeSourcePath = query.source ? (query.source as string) : null; + + // Get the DESTINATION file path + let relativeDestinationPath = query.destination ? (query.destination as string) : null; + + let overwrite = false; + + if (query.overwrite) { + if (query.overwrite === "true") + overwrite = true; + else if (query.overwrite === "false") + overwrite = false; + else { + set.status = 400; + return 'Overwrite must be one of "true" and "false"'; + } + } + + + if (!isPathValid(relativeSourcePath)) { + set.status = 400; + return 'Source file path is invalid'; + } + + if (!isPathValid(relativeDestinationPath)) { + set.status = 400; + return 'Relative destination path is invalid'; + } + + relativeSourcePath = relativeSourcePath as string; + + relativeDestinationPath = relativeDestinationPath as string; + + // Get the full path to the SOURCE file + const sourceFilePath = join(getConfig().dataDir, relativeSourcePath); + + // Get the full path to the DESTINATION file + const destinationFilePath = join(getConfig().dataDir, relativeDestinationPath); + + var file = null; + try { + // Get source file + file = await getFile(sourceFilePath); + if (file === null) { + set.status = 404; + return 'Source file does not exist or is a directory'; + } + } catch (error) { + set.status = 500; + return "There was an error when reading the source file"; + } + const fileWriteResult = await writeFile(destinationFilePath, file, overwrite); + if (fileWriteResult.isOk()) { + return Response(null, { status: 204 }); + } + else { + if (fileWriteResult.error == fileAlreadyExistsMsg) { + set.status = 409; + return "File exists and overwrite is not set"; + } + else { + set.status = 500; + return `An error occurred while copying the file ${fileWriteResult.error}`; + } + } + }); +} From 9f856c85e7459d344497b04eaf5bdd747cac6240 Mon Sep 17 00:00:00 2001 From: Sathvik Hebbar Date: Sat, 7 Oct 2023 01:29:30 +0530 Subject: [PATCH 2/4] Minor change, GET to POST method for copy --- src/endpoints/file/copyFile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/file/copyFile.ts b/src/endpoints/file/copyFile.ts index a24dd87..2c2a6dd 100644 --- a/src/endpoints/file/copyFile.ts +++ b/src/endpoints/file/copyFile.ts @@ -7,7 +7,7 @@ import { fileAlreadyExistsMsg } from '../../constants/commonResponses' export function addCopyFileEndpoint(app: Elysia) { - return app.get('file/copy', async ({ query, set }) => { + return app.post('file/copy', async ({ query, set }) => { // Verify that the SOURCE file path is valid let relativeSourcePath = query.source ? (query.source as string) : null; From ddabf8f033ea96678cd1e22710586838340f3032 Mon Sep 17 00:00:00 2001 From: Sathvik Hebbar Date: Sun, 8 Oct 2023 20:02:44 +0530 Subject: [PATCH 3/4] Move copy functionality to fileUtils.ts, restructure code --- .gitignore | 7 ++- src/constants/commonResponses.ts | 8 +++ src/endpoints/file/copyFile.ts | 85 ++++++++++++++------------------ src/utils/fileUtils.ts | 46 +++++++++++++++++ 4 files changed, 98 insertions(+), 48 deletions(-) diff --git a/.gitignore b/.gitignore index d5fd851..982d97d 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,9 @@ yarn-error.log* **/*.log package-lock.json **/*.bun -idea/ \ No newline at end of file +idea/ + +# utility script to rebuild docker +# image and restart the API after +# changes to code +/runner-script.sh 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 index 2c2a6dd..ab10869 100644 --- a/src/endpoints/file/copyFile.ts +++ b/src/endpoints/file/copyFile.ts @@ -1,22 +1,24 @@ import Elysia from 'elysia'; -import { isPathValid } from '../../utils/pathUtils'; -import { getConfig } from '../../utils/config'; -import { join } from 'path'; -import { getFile, writeFile } from '../../utils/fileUtils'; -import { fileAlreadyExistsMsg } from '../../constants/commonResponses' - +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 }) => { // Verify that the SOURCE file path is valid - let relativeSourcePath = query.source ? (query.source as string) : null; + let relativeSourcePath = query.source ? (query.source as string) : ""; // Get the DESTINATION file path - let relativeDestinationPath = query.destination ? (query.destination as string) : null; + let relativeDestinationPath = query.destination ? (query.destination as string) : ""; let overwrite = false; - if (query.overwrite) { if (query.overwrite === "true") overwrite = true; @@ -24,56 +26,45 @@ export function addCopyFileEndpoint(app: Elysia) { overwrite = false; else { set.status = 400; - return 'Overwrite must be one of "true" and "false"'; + return invalidOverwriteFlagMsg; } } - - if (!isPathValid(relativeSourcePath)) { + const sourcePathValidationResult = validateRelativePath(relativeSourcePath); + if (sourcePathValidationResult.isErr()) { set.status = 400; - return 'Source file path is invalid'; + return invalidSourcePathMsg; } + const absoluteSourcePath = sourcePathValidationResult.value.absolutePath; - if (!isPathValid(relativeDestinationPath)) { + const destinationPathValidationResult = validateRelativePath(relativeDestinationPath); + if (destinationPathValidationResult.isErr()) { set.status = 400; - return 'Relative destination path is invalid'; + return invalidDestinationPathMsg; } + const absoluteDestinationPath = destinationPathValidationResult.value.absolutePath; + - relativeSourcePath = relativeSourcePath as string; - - relativeDestinationPath = relativeDestinationPath as string; - - // Get the full path to the SOURCE file - const sourceFilePath = join(getConfig().dataDir, relativeSourcePath); - - // Get the full path to the DESTINATION file - const destinationFilePath = join(getConfig().dataDir, relativeDestinationPath); - - var file = null; - try { - // Get source file - file = await getFile(sourceFilePath); - if (file === null) { - set.status = 404; - return 'Source file does not exist or is a directory'; - } - } catch (error) { - set.status = 500; - return "There was an error when reading the source file"; - } - const fileWriteResult = await writeFile(destinationFilePath, file, overwrite); - if (fileWriteResult.isOk()) { - return Response(null, { status: 204 }); + let copyResult = await copyFile(absoluteSourcePath, absoluteDestinationPath, overwrite); + if (copyResult.isOk()) { + return Response("", { status: 204 }); } else { - if (fileWriteResult.error == fileAlreadyExistsMsg) { - set.status = 409; - return "File exists and overwrite is not set"; - } - else { - set.status = 500; - return `An error occurred while copying the file ${fileWriteResult.error}`; + 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 From fc184a035005bda1c08f366519042c50ec540a22 Mon Sep 17 00:00:00 2001 From: Sathvik Hebbar Date: Mon, 9 Oct 2023 18:02:14 +0530 Subject: [PATCH 4/4] Refactor to use validateRelativePath appropriately, modify .gitignore --- .gitignore | 4 ---- src/endpoints/file/copyFile.ts | 35 ++++++++++++++++------------------ 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 982d97d..3c36a35 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,3 @@ package-lock.json **/*.bun idea/ -# utility script to rebuild docker -# image and restart the API after -# changes to code -/runner-script.sh diff --git a/src/endpoints/file/copyFile.ts b/src/endpoints/file/copyFile.ts index ab10869..160d156 100644 --- a/src/endpoints/file/copyFile.ts +++ b/src/endpoints/file/copyFile.ts @@ -12,11 +12,19 @@ import { export function addCopyFileEndpoint(app: Elysia) { return app.post('file/copy', async ({ query, set }) => { - // Verify that the SOURCE file path is valid - let relativeSourcePath = query.source ? (query.source as string) : ""; + const sourcePathValidationResult = validateRelativePath(query.source); + if (sourcePathValidationResult.isErr()) { + set.status = 400; + return invalidSourcePathMsg; + } + const sourcePath = sourcePathValidationResult.value; - // Get the DESTINATION file path - let relativeDestinationPath = query.destination ? (query.destination as string) : ""; + const destinationPathValidationResult = validateRelativePath(query.destination); + if (destinationPathValidationResult.isErr()) { + set.status = 400; + return invalidDestinationPathMsg; + } + const destinationPath = destinationPathValidationResult.value; let overwrite = false; if (query.overwrite) { @@ -30,22 +38,11 @@ export function addCopyFileEndpoint(app: Elysia) { } } - const sourcePathValidationResult = validateRelativePath(relativeSourcePath); - if (sourcePathValidationResult.isErr()) { - set.status = 400; - return invalidSourcePathMsg; - } - const absoluteSourcePath = sourcePathValidationResult.value.absolutePath; - - const destinationPathValidationResult = validateRelativePath(relativeDestinationPath); - if (destinationPathValidationResult.isErr()) { - set.status = 400; - return invalidDestinationPathMsg; - } - const absoluteDestinationPath = destinationPathValidationResult.value.absolutePath; - + let copyResult = await copyFile( + sourcePath.absolutePath, + destinationPath.absolutePath, + overwrite); - let copyResult = await copyFile(absoluteSourcePath, absoluteDestinationPath, overwrite); if (copyResult.isOk()) { return Response("", { status: 204 }); }