123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- import { BaseHttpController, controller, httpDelete, httpGet, httpPost, results } from 'inversify-express-utils'
- import { Request, Response } from 'express'
- import { inject } from 'inversify'
- import { Writable } from 'stream'
- import { SharedVaultValetTokenData, ValetTokenOperation } from '@standardnotes/security'
- import { Logger } from 'winston'
- import TYPES from '../../Bootstrap/Types'
- import { CreateUploadSession } from '../../Domain/UseCase/CreateUploadSession/CreateUploadSession'
- import { FinishUploadSession } from '../../Domain/UseCase/FinishUploadSession/FinishUploadSession'
- import { GetFileMetadata } from '../../Domain/UseCase/GetFileMetadata/GetFileMetadata'
- import { MoveFile } from '../../Domain/UseCase/MoveFile/MoveFile'
- import { RemoveFile } from '../../Domain/UseCase/RemoveFile/RemoveFile'
- import { StreamDownloadFile } from '../../Domain/UseCase/StreamDownloadFile/StreamDownloadFile'
- import { UploadFileChunk } from '../../Domain/UseCase/UploadFileChunk/UploadFileChunk'
- @controller('/v1/shared-vault/files', TYPES.Files_SharedVaultValetTokenAuthMiddleware)
- export class AnnotatedSharedVaultFilesController extends BaseHttpController {
- constructor(
- @inject(TYPES.Files_UploadFileChunk) private uploadFileChunk: UploadFileChunk,
- @inject(TYPES.Files_CreateUploadSession) private createUploadSession: CreateUploadSession,
- @inject(TYPES.Files_FinishUploadSession) private finishUploadSession: FinishUploadSession,
- @inject(TYPES.Files_StreamDownloadFile) private streamDownloadFile: StreamDownloadFile,
- @inject(TYPES.Files_GetFileMetadata) private getFileMetadata: GetFileMetadata,
- @inject(TYPES.Files_RemoveFile) private removeFile: RemoveFile,
- @inject(TYPES.Files_MoveFile) private moveFile: MoveFile,
- @inject(TYPES.Files_MAX_CHUNK_BYTES) private maxChunkBytes: number,
- @inject(TYPES.Files_Logger) private logger: Logger,
- ) {
- super()
- }
- @httpPost('/move')
- async moveFileRequest(
- _request: Request,
- response: Response,
- ): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
- const locals = response.locals as SharedVaultValetTokenData
- if (locals.permittedOperation !== ValetTokenOperation.Move) {
- return this.badRequest('Not permitted for this operation')
- }
- const moveOperation = locals.moveOperation
- if (!moveOperation) {
- return this.badRequest('Missing move operation data')
- }
- const result = await this.moveFile.execute({
- moveType: moveOperation.type,
- fromUuid: moveOperation.fromUuid,
- toUuid: moveOperation.toUuid,
- resourceRemoteIdentifier: locals.remoteIdentifier,
- })
- if (result.isFailed()) {
- return this.badRequest(result.getError())
- }
- return this.json({ success: true })
- }
- @httpPost('/upload/create-session')
- async startUpload(
- _request: Request,
- response: Response,
- ): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
- const locals = response.locals as SharedVaultValetTokenData
- if (locals.permittedOperation !== ValetTokenOperation.Write) {
- return this.badRequest('Not permitted for this operation')
- }
- const result = await this.createUploadSession.execute({
- ownerUuid: locals.sharedVaultUuid,
- resourceRemoteIdentifier: locals.remoteIdentifier,
- })
- if (!result.success) {
- return this.badRequest(result.message)
- }
- return this.json({ success: true, uploadId: result.uploadId })
- }
- @httpPost('/upload/chunk')
- async uploadChunk(
- request: Request,
- response: Response,
- ): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
- const locals = response.locals as SharedVaultValetTokenData
- if (locals.permittedOperation !== ValetTokenOperation.Write) {
- return this.badRequest('Not permitted for this operation')
- }
- const chunkId = +(request.headers['x-chunk-id'] as string)
- if (!chunkId) {
- return this.badRequest('Missing x-chunk-id header in request.')
- }
- const result = await this.uploadFileChunk.execute({
- ownerUuid: locals.sharedVaultUuid,
- resourceRemoteIdentifier: locals.remoteIdentifier,
- resourceUnencryptedFileSize: locals.unencryptedFileSize as number,
- chunkId,
- data: request.body,
- })
- if (!result.success) {
- return this.badRequest(result.message)
- }
- return this.json({ success: true, message: 'Chunk uploaded successfully' })
- }
- @httpPost('/upload/close-session')
- public async finishUpload(
- _request: Request,
- response: Response,
- ): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
- const locals = response.locals as SharedVaultValetTokenData
- if (locals.permittedOperation !== ValetTokenOperation.Write) {
- return this.badRequest('Not permitted for this operation')
- }
- if (locals.uploadBytesLimit === undefined) {
- return this.badRequest('Missing upload bytes limit')
- }
- const result = await this.finishUploadSession.execute({
- userUuid: locals.vaultOwnerUuid,
- sharedVaultUuid: locals.sharedVaultUuid,
- resourceRemoteIdentifier: locals.remoteIdentifier,
- uploadBytesLimit: locals.uploadBytesLimit,
- uploadBytesUsed: locals.uploadBytesUsed,
- })
- if (result.isFailed()) {
- this.logger.error(result.getError())
- return this.badRequest(result.getError())
- }
- return this.json({ success: true, message: 'File uploaded successfully' })
- }
- @httpDelete('/')
- async remove(
- _request: Request,
- response: Response,
- ): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
- const locals = response.locals as SharedVaultValetTokenData
- if (locals.permittedOperation !== ValetTokenOperation.Delete) {
- return this.badRequest('Not permitted for this operation')
- }
- const result = await this.removeFile.execute({
- vaultInput: {
- sharedVaultUuid: locals.sharedVaultUuid,
- vaultOwnerUuid: locals.vaultOwnerUuid,
- resourceRemoteIdentifier: locals.remoteIdentifier,
- },
- })
- if (result.isFailed()) {
- return this.badRequest(result.getError())
- }
- return this.json({ success: true, message: 'File removed successfully' })
- }
- @httpGet('/')
- async download(
- request: Request,
- response: Response,
- ): Promise<results.BadRequestErrorMessageResult | (() => Writable)> {
- const locals = response.locals as SharedVaultValetTokenData
- if (locals.permittedOperation !== ValetTokenOperation.Read) {
- return this.badRequest('Not permitted for this operation')
- }
- const range = request.headers['range']
- if (!range) {
- return this.badRequest('File download requires range header to be set.')
- }
- let chunkSize = +(request.headers['x-chunk-size'] as string)
- if (!chunkSize || chunkSize > this.maxChunkBytes) {
- chunkSize = this.maxChunkBytes
- }
- const fileMetadata = await this.getFileMetadata.execute({
- ownerUuid: locals.sharedVaultUuid,
- resourceRemoteIdentifier: locals.remoteIdentifier,
- })
- if (!fileMetadata.success) {
- return this.badRequest(fileMetadata.message)
- }
- const startRange = Number(range.replace(/\D/g, ''))
- const endRange = Math.min(startRange + chunkSize - 1, fileMetadata.size - 1)
- const headers = {
- 'Content-Range': `bytes ${startRange}-${endRange}/${fileMetadata.size}`,
- 'Accept-Ranges': 'bytes',
- 'Content-Length': endRange - startRange + 1,
- 'Content-Type': 'application/octet-stream',
- }
- response.writeHead(206, headers)
- const result = await this.streamDownloadFile.execute({
- ownerUuid: locals.sharedVaultUuid,
- resourceRemoteIdentifier: locals.remoteIdentifier,
- startRange,
- endRange,
- })
- if (!result.success) {
- return this.badRequest(result.message)
- }
- return () => result.readStream.pipe(response)
- }
- }
|