From 3d583ff21d344a71aa861c7e5b1426ebc2d54c22 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Mon, 14 Mar 2022 05:30:10 +0500 Subject: [PATCH] feat(drive): new route delete file api --- api/public/swagger.yaml | 51 ++++++++++++++++++++++++- api/src/controllers/drive.ts | 73 +++++++++++++++++++++++++++--------- api/src/routes/api/drive.ts | 37 +++++++++++------- api/src/utils/validation.ts | 15 +------- 4 files changed, 130 insertions(+), 46 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 47d2cf1..89678b5 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -585,6 +585,7 @@ paths: responses: '204': description: 'No content' + description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request." summary: 'Get file from SASjs Drive' tags: - Drive @@ -594,11 +595,57 @@ paths: parameters: - in: query - name: filePath - required: true + name: _filePath + required: false schema: type: string example: /Public/somefolder/some.file + requestBody: + required: false + content: + multipart/form-data: + schema: + type: object + properties: + filePath: + type: string + delete: + operationId: DeleteFile + responses: + '200': + description: Ok + content: + application/json: + schema: + properties: + status: {type: string} + required: + - status + type: object + description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request." + summary: 'Delete file from SASjs Drive' + tags: + - Drive + security: + - + bearerAuth: [] + parameters: + - + in: query + name: _filePath + required: false + schema: + type: string + example: /Public/somefolder/some.file + requestBody: + required: false + content: + multipart/form-data: + schema: + type: object + properties: + filePath: + type: string post: operationId: SaveFile responses: diff --git a/api/src/controllers/drive.ts b/api/src/controllers/drive.ts index d8174a1..8b752a8 100644 --- a/api/src/controllers/drive.ts +++ b/api/src/controllers/drive.ts @@ -13,9 +13,15 @@ import { Get, Patch, UploadedFile, - FormField + FormField, + Delete } from 'tsoa' -import { fileExists, createFile, moveFile, createFolder } from '@sasjs/utils' +import { + fileExists, + moveFile, + createFolder, + deleteFile as deleteFileOnSystem +} from '@sasjs/utils' import { createFileTree, ExecutionController, getTreeExample } from './internal' import { FileTree, isFileTree, TreeNode } from '../types' @@ -25,18 +31,6 @@ interface DeployPayload { appLoc?: string fileTree: FileTree } -interface FilePayload { - /** - * Path of the file - * @example "/Public/somefolder/some.file" - */ - filePath: string - /** - * Contents of the file - * @example "Contents of the File" - */ - fileContent: string -} interface DeployResponse { status: string @@ -93,16 +87,39 @@ export class DriveController { } /** + * It's optional to either provide `_filePath` in url as query parameter + * Or provide `filePath` in body as form field. + * But it's required to provide else API will respond with Bad Request. + * * @summary Get file from SASjs Drive - * @query filePath Location of SAS program - * @example filePath "/Public/somefolder/some.file" + * @query _filePath Location of SAS program + * @example _filePath "/Public/somefolder/some.file" */ @Get('/file') public async getFile( @Request() request: express.Request, - @Query() filePath: string + + @Query() _filePath?: string, + @FormField() filePath?: string ) { - return getFile(request, filePath) + return getFile(request, (_filePath ?? filePath)!) + } + + /** + * It's optional to either provide `_filePath` in url as query parameter + * Or provide `filePath` in body as form field. + * But it's required to provide else API will respond with Bad Request. + * + * @summary Delete file from SASjs Drive + * @query _filePath Location of SAS program + * @example _filePath "/Public/somefolder/some.file" + */ + @Delete('/file') + public async deleteFile( + @Query() _filePath?: string, + @FormField() filePath?: string + ) { + return deleteFile((_filePath ?? filePath)!) } /** @@ -210,6 +227,26 @@ const getFile = async (req: express.Request, filePath: string) => { req.res?.sendFile(path.resolve(filePathFull)) } +const deleteFile = async (filePath: string) => { + const driveFilesPath = getTmpFilesFolderPath() + + const filePathFull = path + .join(getTmpFilesFolderPath(), filePath) + .replace(new RegExp('/', 'g'), path.sep) + + if (!filePathFull.includes(driveFilesPath)) { + throw new Error('Cannot delete file outside drive.') + } + + if (!(await fileExists(filePathFull))) { + throw new Error('File does not exist.') + } + + await deleteFileOnSystem(filePathFull) + + return { status: 'success' } +} + const saveFile = async ( filePath: string, multerFile: Express.Multer.File diff --git a/api/src/routes/api/drive.ts b/api/src/routes/api/drive.ts index 102ada3..881d884 100644 --- a/api/src/routes/api/drive.ts +++ b/api/src/routes/api/drive.ts @@ -3,12 +3,7 @@ import { deleteFile } from '@sasjs/utils' import { multerSingle } from '../../middlewares/multer' import { DriveController } from '../../controllers/' -import { - getFileDriveValidation, - updateFileDriveValidation, - uploadFileBodyValidation, - uploadFileParamValidation -} from '../../utils' +import { fileBodyValidation, fileParamValidation } from '../../utils' const controller = new DriveController() @@ -28,11 +23,27 @@ driveRouter.post('/deploy', async (req, res) => { }) driveRouter.get('/file', async (req, res) => { - const { error, value: query } = getFileDriveValidation(req.query) - if (error) return res.status(400).send(error.details[0].message) + const { error: errQ, value: query } = fileParamValidation(req.query) + const { error: errB, value: body } = fileBodyValidation(req.body) + + if (errQ && errB) return res.status(400).send(errB.details[0].message) try { - await controller.getFile(req, query.filePath) + await controller.getFile(req, query._filePath, body.filePath) + } catch (err: any) { + res.status(403).send(err.toString()) + } +}) + +driveRouter.delete('/file', async (req, res) => { + const { error: errQ, value: query } = fileParamValidation(req.query) + const { error: errB, value: body } = fileBodyValidation(req.body) + + if (errQ && errB) return res.status(400).send(errB.details[0].message) + + try { + const response = await controller.deleteFile(query._filePath, body.filePath) + res.send(response) } catch (err: any) { res.status(403).send(err.toString()) } @@ -42,8 +53,8 @@ driveRouter.post( '/file', (...arg) => multerSingle('file', arg), async (req, res) => { - const { error: errQ, value: query } = uploadFileParamValidation(req.query) - const { error: errB, value: body } = uploadFileBodyValidation(req.body) + const { error: errQ, value: query } = fileParamValidation(req.query) + const { error: errB, value: body } = fileBodyValidation(req.body) if (errQ && errB) { if (req.file) await deleteFile(req.file.path) @@ -70,8 +81,8 @@ driveRouter.patch( '/file', (...arg) => multerSingle('file', arg), async (req, res) => { - const { error: errQ, value: query } = uploadFileParamValidation(req.query) - const { error: errB, value: body } = uploadFileBodyValidation(req.body) + const { error: errQ, value: query } = fileParamValidation(req.query) + const { error: errB, value: body } = fileBodyValidation(req.body) if (errQ && errB) { if (req.file) await deleteFile(req.file.path) diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index 1bd1c31..aff7251 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -66,25 +66,14 @@ export const registerClientValidation = (data: any): Joi.ValidationResult => clientSecret: Joi.string().required() }).validate(data) -export const getFileDriveValidation = (data: any): Joi.ValidationResult => - Joi.object({ - filePath: Joi.string().required() - }).validate(data) - -export const updateFileDriveValidation = (data: any): Joi.ValidationResult => - Joi.object({ - filePath: Joi.string().required(), - fileContent: Joi.string().required() - }).validate(data) - -export const uploadFileBodyValidation = (data: any): Joi.ValidationResult => +export const fileBodyValidation = (data: any): Joi.ValidationResult => Joi.object({ filePath: Joi.string().pattern(/.sas$/).required().messages({ 'string.pattern.base': `Valid extensions for filePath: .sas` }) }).validate(data) -export const uploadFileParamValidation = (data: any): Joi.ValidationResult => +export const fileParamValidation = (data: any): Joi.ValidationResult => Joi.object({ _filePath: Joi.string().pattern(/.sas$/).required().messages({ 'string.pattern.base': `Valid extensions for filePath: .sas`