Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ yarn-error.log*
**/*.log
package-lock.json
**/*.bun
idea/
idea/

2 changes: 2 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -20,5 +21,6 @@ export function buildApp() {
addGetFileSizeEndpoint(app);
addUploadFileEndpoint(app);
addCheckFileExistsEndpoint(app);
addCopyFileEndpoint(app);
return app;
}
8 changes: 8 additions & 0 deletions src/constants/commonResponses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
67 changes: 67 additions & 0 deletions src/endpoints/file/copyFile.ts
Original file line number Diff line number Diff line change
@@ -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;
}
});
}
46 changes: 46 additions & 0 deletions src/utils/fileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<Result<boolean, string>> {
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})`);
}
}
}