mirror of
https://github.com/sasjs/server.git
synced 2026-01-17 02:40:05 +00:00
fix(drive): update file API is same as create file
This commit is contained in:
@@ -233,21 +233,6 @@ components:
|
|||||||
- status
|
- status
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
FilePayload:
|
|
||||||
properties:
|
|
||||||
filePath:
|
|
||||||
type: string
|
|
||||||
description: 'Path of the file'
|
|
||||||
example: /Public/somefolder/some.file
|
|
||||||
fileContent:
|
|
||||||
type: string
|
|
||||||
description: 'Contents of the file'
|
|
||||||
example: 'Contents of the File'
|
|
||||||
required:
|
|
||||||
- filePath
|
|
||||||
- fileContent
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
TreeNode:
|
TreeNode:
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
@@ -626,7 +611,7 @@ paths:
|
|||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {status: success}
|
value: {status: success}
|
||||||
'400':
|
'403':
|
||||||
description: 'File already exists'
|
description: 'File already exists'
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
@@ -635,7 +620,7 @@ paths:
|
|||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {status: failure, message: 'File request failed.'}
|
value: {status: failure, message: 'File request failed.'}
|
||||||
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 provided else API will respond with Bad Request."
|
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: 'Create a file in SASjs Drive'
|
summary: 'Create a file in SASjs Drive'
|
||||||
tags:
|
tags:
|
||||||
- Drive
|
- Drive
|
||||||
@@ -677,8 +662,8 @@ paths:
|
|||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {status: success}
|
value: {status: success}
|
||||||
'400':
|
'403':
|
||||||
description: 'Unable to get File'
|
description: ""
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
@@ -686,19 +671,36 @@ paths:
|
|||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {status: failure, message: 'File request failed.'}
|
value: {status: failure, message: 'File request failed.'}
|
||||||
|
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: 'Modify a file in SASjs Drive'
|
summary: 'Modify a file in SASjs Drive'
|
||||||
tags:
|
tags:
|
||||||
- Drive
|
- Drive
|
||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters: []
|
parameters:
|
||||||
|
-
|
||||||
|
description: 'Location of SAS program'
|
||||||
|
in: query
|
||||||
|
name: _filePath
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: /Public/somefolder/some.file.sas
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
multipart/form-data:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/FilePayload'
|
type: object
|
||||||
|
properties:
|
||||||
|
file:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
filePath:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- file
|
||||||
/SASjsApi/drive/filetree:
|
/SASjsApi/drive/filetree:
|
||||||
get:
|
get:
|
||||||
operationId: GetFileTree
|
operationId: GetFileTree
|
||||||
@@ -1054,7 +1056,7 @@ paths:
|
|||||||
anyOf:
|
anyOf:
|
||||||
- {type: string}
|
- {type: string}
|
||||||
- {type: string, format: byte}
|
- {type: string, format: byte}
|
||||||
description: "Trigger a SAS program using it's location in the _program URL parameter.\nEnable debugging using the _debug URL parameter. Setting _debug=131 will\ncause the log to be streamed in the output.\n\nAdditional URL parameters are turned into SAS macro variables.\n\nAny files provided in the request body are placed into the SAS session with\ncorresponding _WEBIN_XXX variables created.\n\nThe response headers can be adjusted using the mfs_httpheader() macro. Any\nfile type can be returned, including binary files such as zip or xls.\n\nThis behaviour differs for POST requests, in which case the reponse is\nalways JSON."
|
description: "Trigger a SAS program using it's location in the _program URL parameter.\nEnable debugging using the _debug URL parameter. Setting _debug=131 will\ncause the log to be streamed in the output.\n\nAdditional URL parameters are turned into SAS macro variables.\n\nAny files provided in the request body are placed into the SAS session with\ncorresponding _WEBIN_XXX variables created.\n\nThe response headers can be adjusted using the mfs_httpheader() macro. Any\nfile type can be returned, including binary files such as zip or xls.\n\nIf _debug is >= 131, response headers will contain Content-Type: 'text/plain'\n\nThis behaviour differs for POST requests, in which case the response is\nalways JSON."
|
||||||
summary: 'Execute Stored Program, return raw _webout content.'
|
summary: 'Execute Stored Program, return raw _webout content.'
|
||||||
tags:
|
tags:
|
||||||
- STP
|
- STP
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export class DriveController {
|
|||||||
/**
|
/**
|
||||||
* It's optional to either provide `_filePath` in url as query parameter
|
* It's optional to either provide `_filePath` in url as query parameter
|
||||||
* Or provide `filePath` in body as form field.
|
* Or provide `filePath` in body as form field.
|
||||||
* But it's required to provided else API will respond with Bad Request.
|
* But it's required to provide else API will respond with Bad Request.
|
||||||
*
|
*
|
||||||
* @summary Create a file in SASjs Drive
|
* @summary Create a file in SASjs Drive
|
||||||
* @param _filePath Location of SAS program
|
* @param _filePath Location of SAS program
|
||||||
@@ -118,7 +118,7 @@ export class DriveController {
|
|||||||
@Example<UpdateFileResponse>({
|
@Example<UpdateFileResponse>({
|
||||||
status: 'success'
|
status: 'success'
|
||||||
})
|
})
|
||||||
@Response<UpdateFileResponse>(400, 'File already exists', {
|
@Response<UpdateFileResponse>(403, 'File already exists', {
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'File request failed.'
|
message: 'File request failed.'
|
||||||
})
|
})
|
||||||
@@ -132,21 +132,29 @@ 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 Modify a file in SASjs Drive
|
* @summary Modify a file in SASjs Drive
|
||||||
|
* @param _filePath Location of SAS program
|
||||||
|
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<UpdateFileResponse>({
|
@Example<UpdateFileResponse>({
|
||||||
status: 'success'
|
status: 'success'
|
||||||
})
|
})
|
||||||
@Response<UpdateFileResponse>(400, 'Unable to get File', {
|
@Response<UpdateFileResponse>(403, `File doesn't exist`, {
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'File request failed.'
|
message: 'File request failed.'
|
||||||
})
|
})
|
||||||
@Patch('/file')
|
@Patch('/file')
|
||||||
public async updateFile(
|
public async updateFile(
|
||||||
@Body() body: FilePayload
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
@Query() _filePath?: string,
|
||||||
|
@FormField() filePath?: string
|
||||||
): Promise<UpdateFileResponse> {
|
): Promise<UpdateFileResponse> {
|
||||||
return updateFile(body)
|
return updateFile((_filePath ?? filePath)!, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -227,25 +235,27 @@ const saveFile = async (
|
|||||||
return { status: 'success' }
|
return { status: 'success' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFile = async (body: FilePayload): Promise<GetFileResponse> => {
|
const updateFile = async (
|
||||||
const { filePath, fileContent } = body
|
filePath: string,
|
||||||
try {
|
multerFile: Express.Multer.File
|
||||||
const filePathFull = path
|
): Promise<GetFileResponse> => {
|
||||||
.join(getTmpFilesFolderPath(), filePath)
|
const driveFilesPath = getTmpFilesFolderPath()
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
|
||||||
|
|
||||||
await validateFilePath(filePathFull)
|
const filePathFull = path
|
||||||
await createFile(filePathFull, fileContent)
|
.join(driveFilesPath, filePath)
|
||||||
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
return { status: 'success' }
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
} catch (err: any) {
|
throw new Error('Cannot modify file outside drive.')
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'failure',
|
|
||||||
message: 'File request failed.',
|
|
||||||
error: typeof err === 'object' ? err.toString() : err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(await fileExists(filePathFull))) {
|
||||||
|
throw new Error(`File doesn't exist.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await moveFile(multerFile.path, filePathFull)
|
||||||
|
|
||||||
|
return { status: 'success' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateFilePath = async (filePath: string) => {
|
const validateFilePath = async (filePath: string) => {
|
||||||
|
|||||||
@@ -66,21 +66,33 @@ driveRouter.post(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
driveRouter.patch('/file', async (req, res) => {
|
driveRouter.patch(
|
||||||
const { error, value: body } = updateFileDriveValidation(req.body)
|
'/file',
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
(...arg) => multerSingle('file', arg),
|
||||||
|
async (req, res) => {
|
||||||
|
const { error: errQ, value: query } = uploadFileParamValidation(req.query)
|
||||||
|
const { error: errB, value: body } = uploadFileBodyValidation(req.body)
|
||||||
|
|
||||||
try {
|
if (errQ && errB) {
|
||||||
const response = await controller.updateFile(body)
|
if (req.file) await deleteFile(req.file.path)
|
||||||
res.send(response)
|
return res.status(400).send(errB.details[0].message)
|
||||||
} catch (err: any) {
|
}
|
||||||
const statusCode = err.code
|
|
||||||
|
|
||||||
delete err.code
|
if (!req.file) return res.status(400).send('"file" is not present.')
|
||||||
|
|
||||||
res.status(statusCode).send(err)
|
try {
|
||||||
|
const response = await controller.updateFile(
|
||||||
|
req.file,
|
||||||
|
query._filePath,
|
||||||
|
body.filePath
|
||||||
|
)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
await deleteFile(req.file.path)
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
driveRouter.get('/fileTree', async (req, res) => {
|
driveRouter.get('/fileTree', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -321,6 +321,166 @@ describe('files', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
it('should update a SAS file on drive having filePath as form field', async () => {
|
||||||
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
|
const pathToCopy = path.join(
|
||||||
|
fileUtilModules.getTmpFilesFolderPath(),
|
||||||
|
pathToUpload
|
||||||
|
)
|
||||||
|
await copy(fileToAttachPath, pathToCopy)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.attach('file', fileToAttachPath)
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(200)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update a SAS file on drive having _filePath as query param', async () => {
|
||||||
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
|
const pathToCopy = path.join(
|
||||||
|
fileUtilModules.getTmpFilesFolderPath(),
|
||||||
|
pathToUpload
|
||||||
|
)
|
||||||
|
await copy(fileToAttachPath, pathToCopy)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.attach('file', fileToAttachPath)
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(200)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.field('filePath', '/my/path/code.sas')
|
||||||
|
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||||
|
.expect(401)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Unauthorized')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Forbidden if file is not present', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', `/my/path/code-${generateTimestamp()}.sas`)
|
||||||
|
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||||
|
.expect(403)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`Error: File doesn't exist.`)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Forbidden if filePath outside Drive', async () => {
|
||||||
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
|
const pathToUpload = '/../path/code.sas'
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.attach('file', fileToAttachPath)
|
||||||
|
.expect(403)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Error: Cannot modify file outside drive.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if filePath is missing', async () => {
|
||||||
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.attach('file', fileToAttachPath)
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`"filePath" is required`)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
|
||||||
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
|
const pathToUpload = '/my/path/code.oth'
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.attach('file', fileToAttachPath)
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Valid extensions for filePath: .sas')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if file is missing', async () => {
|
||||||
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('"file" is not present.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should respond with Bad Request if attached file doesn't has correct extension", async () => {
|
||||||
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.oth')
|
||||||
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.attach('file', fileToAttachPath)
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(
|
||||||
|
`File extension '.oth' not acceptable. Valid extension(s): .sas`
|
||||||
|
)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if attached file exceeds file limit', async () => {
|
||||||
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
|
const attachedFile = Buffer.from('.'.repeat(20 * 1024 * 1024))
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.attach('file', attachedFile, 'another.sas')
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(
|
||||||
|
'File size is over limit. File limit is: 10 MB'
|
||||||
|
)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -42,11 +42,15 @@ const Main = (props: any) => {
|
|||||||
setEditMode(true)
|
setEditMode(true)
|
||||||
} else {
|
} else {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
|
||||||
|
formData.append('file', stringBlob, 'filename.sas')
|
||||||
|
formData.append('filePath', props.selectedFilePath)
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.patch(`/SASjsApi/drive/file`, {
|
.patch(`/SASjsApi/drive/file`, formData)
|
||||||
filePath: props.selectedFilePath,
|
|
||||||
fileContent: fileContent
|
|
||||||
})
|
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setEditMode(false)
|
setEditMode(false)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user