mirror of
https://github.com/standardnotes/server
synced 2026-07-30 20:16:37 -04:00
refactor: vault files
This commit is contained in:
@@ -45,6 +45,10 @@ import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountD
|
||||
import { SharedSubscriptionInvitationCanceledEventHandler } from '../Domain/Handler/SharedSubscriptionInvitationCanceledEventHandler'
|
||||
import { InMemoryUploadRepository } from '../Infra/InMemory/InMemoryUploadRepository'
|
||||
import { VaultValetTokenAuthMiddleware } from '../Controller/VaultValetTokenAuthMiddleware'
|
||||
import { FileMoverInterface } from '../Domain/Services/FileMoverInterface'
|
||||
import { S3FileMover } from '../Infra/S3/S3FileMover'
|
||||
import { FSFileMover } from '../Infra/FS/FSFileMover'
|
||||
import { MoveFile } from '../Domain/UseCase/MoveFile/MoveFile'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(): Promise<Container> {
|
||||
@@ -98,12 +102,14 @@ export class ContainerConfigLoader {
|
||||
container.bind<FileDownloaderInterface>(TYPES.FileDownloader).to(S3FileDownloader)
|
||||
container.bind<FileUploaderInterface>(TYPES.FileUploader).to(S3FileUploader)
|
||||
container.bind<FileRemoverInterface>(TYPES.FileRemover).to(S3FileRemover)
|
||||
container.bind<FileMoverInterface>(TYPES.FileMover).to(S3FileMover)
|
||||
} else {
|
||||
container.bind<FileDownloaderInterface>(TYPES.FileDownloader).to(FSFileDownloader)
|
||||
container
|
||||
.bind<FileUploaderInterface>(TYPES.FileUploader)
|
||||
.toConstantValue(new FSFileUploader(container.get(TYPES.FILE_UPLOAD_PATH), container.get(TYPES.Logger)))
|
||||
container.bind<FileRemoverInterface>(TYPES.FileRemover).to(FSFileRemover)
|
||||
container.bind<FileMoverInterface>(TYPES.FileMover).to(FSFileMover)
|
||||
}
|
||||
|
||||
if (env.get('SNS_TOPIC_ARN', true)) {
|
||||
@@ -146,6 +152,7 @@ export class ContainerConfigLoader {
|
||||
container.bind<FinishUploadSession>(TYPES.FinishUploadSession).to(FinishUploadSession)
|
||||
container.bind<GetFileMetadata>(TYPES.GetFileMetadata).to(GetFileMetadata)
|
||||
container.bind<RemoveFile>(TYPES.RemoveFile).to(RemoveFile)
|
||||
container.bind<MoveFile>(TYPES.MoveFile).to(MoveFile)
|
||||
container.bind<MarkFilesToBeRemoved>(TYPES.MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
|
||||
|
||||
// middleware
|
||||
|
||||
@@ -13,6 +13,7 @@ const TYPES = {
|
||||
FinishUploadSession: Symbol.for('FinishUploadSession'),
|
||||
GetFileMetadata: Symbol.for('GetFileMetadata'),
|
||||
RemoveFile: Symbol.for('RemoveFile'),
|
||||
MoveFile: Symbol.for('MoveFile'),
|
||||
MarkFilesToBeRemoved: Symbol.for('MarkFilesToBeRemoved'),
|
||||
|
||||
// services
|
||||
@@ -23,6 +24,7 @@ const TYPES = {
|
||||
FileUploader: Symbol.for('FileUploader'),
|
||||
FileDownloader: Symbol.for('FileDownloader'),
|
||||
FileRemover: Symbol.for('FileRemover'),
|
||||
FileMover: Symbol.for('FileMover'),
|
||||
|
||||
// repositories
|
||||
UploadRepository: Symbol.for('UploadRepository'),
|
||||
|
||||
@@ -35,7 +35,7 @@ export class FilesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
const result = await this.createUploadSession.execute({
|
||||
userUuid: response.locals.userUuid,
|
||||
ownerUuid: response.locals.userUuid,
|
||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||
})
|
||||
|
||||
@@ -61,7 +61,7 @@ export class FilesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
const result = await this.uploadFileChunk.execute({
|
||||
userUuid: response.locals.userUuid,
|
||||
ownerUuid: response.locals.userUuid,
|
||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||
resourceUnencryptedFileSize: response.locals.permittedResources[0].unencryptedFileSize,
|
||||
chunkId,
|
||||
@@ -85,7 +85,8 @@ export class FilesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
const result = await this.finishUploadSession.execute({
|
||||
userUuid: response.locals.userUuid,
|
||||
ownerUuid: response.locals.userUuid,
|
||||
ownerType: 'user',
|
||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||
uploadBytesLimit: response.locals.uploadBytesLimit,
|
||||
uploadBytesUsed: response.locals.uploadBytesUsed,
|
||||
@@ -140,7 +141,7 @@ export class FilesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
const fileMetadata = await this.getFileMetadata.execute({
|
||||
userUuid: response.locals.userUuid,
|
||||
ownerUuid: response.locals.userUuid,
|
||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||
})
|
||||
|
||||
@@ -161,7 +162,7 @@ export class FilesController extends BaseHttpController {
|
||||
response.writeHead(206, headers)
|
||||
|
||||
const result = await this.streamDownloadFile.execute({
|
||||
userUuid: response.locals.userUuid,
|
||||
ownerUuid: response.locals.userUuid,
|
||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||
startRange,
|
||||
endRange,
|
||||
|
||||
@@ -1,28 +1,136 @@
|
||||
import { BaseHttpController, controller, httpGet, results } from 'inversify-express-utils'
|
||||
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 TYPES from '../Bootstrap/Types'
|
||||
import { UploadFileChunk } from '../Domain/UseCase/UploadFileChunk/UploadFileChunk'
|
||||
import { StreamDownloadFile } from '../Domain/UseCase/StreamDownloadFile/StreamDownloadFile'
|
||||
import { CreateUploadSession } from '../Domain/UseCase/CreateUploadSession/CreateUploadSession'
|
||||
import { FinishUploadSession } from '../Domain/UseCase/FinishUploadSession/FinishUploadSession'
|
||||
import { GetFileMetadata } from '../Domain/UseCase/GetFileMetadata/GetFileMetadata'
|
||||
import { VaultValetTokenData, ValetTokenOperation } from '@standardnotes/security'
|
||||
import { RemoveFile } from '../Domain/UseCase/RemoveFile/RemoveFile'
|
||||
import { ValetTokenOperation, VaultValetTokenData } from '@standardnotes/security'
|
||||
|
||||
@controller('/v1/vault/files', TYPES.VaultValetTokenAuthMiddleware)
|
||||
export class VaultFilesController extends BaseHttpController {
|
||||
export class FilesController extends BaseHttpController {
|
||||
constructor(
|
||||
@inject(TYPES.UploadFileChunk) private uploadFileChunk: UploadFileChunk,
|
||||
@inject(TYPES.CreateUploadSession) private createUploadSession: CreateUploadSession,
|
||||
@inject(TYPES.FinishUploadSession) private finishUploadSession: FinishUploadSession,
|
||||
@inject(TYPES.StreamDownloadFile) private streamDownloadFile: StreamDownloadFile,
|
||||
@inject(TYPES.GetFileMetadata) private getFileMetadata: GetFileMetadata,
|
||||
@inject(TYPES.RemoveFile) private removeFile: RemoveFile,
|
||||
@inject(TYPES.MAX_CHUNK_BYTES) private maxChunkBytes: number,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/upload/create-session')
|
||||
async startUpload(
|
||||
_request: Request,
|
||||
response: Response,
|
||||
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
|
||||
const locals = response.locals as VaultValetTokenData
|
||||
if (locals.permittedOperation !== ValetTokenOperation.Write) {
|
||||
return this.badRequest('Not permitted for this operation')
|
||||
}
|
||||
|
||||
const result = await this.createUploadSession.execute({
|
||||
ownerUuid: locals.vaultUuid,
|
||||
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 VaultValetTokenData
|
||||
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.vaultUuid,
|
||||
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 VaultValetTokenData
|
||||
if (locals.permittedOperation !== ValetTokenOperation.Write) {
|
||||
return this.badRequest('Not permitted for this operation')
|
||||
}
|
||||
|
||||
const result = await this.finishUploadSession.execute({
|
||||
ownerUuid: locals.vaultUuid,
|
||||
ownerType: 'vault',
|
||||
resourceRemoteIdentifier: locals.remoteIdentifier,
|
||||
uploadBytesLimit: locals.uploadBytesLimit,
|
||||
uploadBytesUsed: locals.uploadBytesUsed,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return this.badRequest(result.message)
|
||||
}
|
||||
|
||||
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 VaultValetTokenData
|
||||
if (locals.permittedOperation !== ValetTokenOperation.Delete) {
|
||||
return this.badRequest('Not permitted for this operation')
|
||||
}
|
||||
|
||||
const result = await this.removeFile.execute({
|
||||
vaultUuid: locals.vaultUuid,
|
||||
resourceRemoteIdentifier: locals.remoteIdentifier,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return this.badRequest(result.message)
|
||||
}
|
||||
|
||||
return this.json({ success: true, message: 'File removed successfully' })
|
||||
}
|
||||
|
||||
@httpGet('/')
|
||||
async download(
|
||||
request: Request,
|
||||
response: Response,
|
||||
): Promise<results.BadRequestErrorMessageResult | (() => Writable)> {
|
||||
if (response.locals.permittedOperation !== ValetTokenOperation.Read) {
|
||||
const locals = response.locals as VaultValetTokenData
|
||||
if (locals.permittedOperation !== ValetTokenOperation.Read) {
|
||||
return this.badRequest('Not permitted for this operation')
|
||||
}
|
||||
|
||||
@@ -36,10 +144,8 @@ export class VaultFilesController extends BaseHttpController {
|
||||
chunkSize = this.maxChunkBytes
|
||||
}
|
||||
|
||||
const locals = response.locals as VaultValetTokenData
|
||||
|
||||
const fileMetadata = await this.getFileMetadata.execute({
|
||||
vaultUuid: locals.vaultUuid,
|
||||
ownerUuid: locals.vaultUuid,
|
||||
resourceRemoteIdentifier: locals.remoteIdentifier,
|
||||
})
|
||||
|
||||
@@ -60,7 +166,7 @@ export class VaultFilesController extends BaseHttpController {
|
||||
response.writeHead(206, headers)
|
||||
|
||||
const result = await this.streamDownloadFile.execute({
|
||||
vaultUuid: locals.vaultUuid,
|
||||
ownerUuid: locals.vaultUuid,
|
||||
resourceRemoteIdentifier: locals.remoteIdentifier,
|
||||
startRange,
|
||||
endRange,
|
||||
|
||||
@@ -64,6 +64,8 @@ export class VaultValetTokenAuthMiddleware extends BaseMiddleware {
|
||||
vaultUuid: valetTokenData.vaultUuid,
|
||||
remoteIdentifier: valetTokenData.remoteIdentifier,
|
||||
permittedOperation: valetTokenData.permittedOperation,
|
||||
uploadBytesUsed: valetTokenData.uploadBytesUsed,
|
||||
uploadBytesLimit: valetTokenData.uploadBytesLimit,
|
||||
}
|
||||
|
||||
Object.assign(response.locals, whitelistedData)
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { FileUploadedEvent, FileRemovedEvent, DomainEventService } from '@standardnotes/domain-events'
|
||||
import {
|
||||
UserFileRemovedEvent,
|
||||
UserFileUploadedEvent,
|
||||
DomainEventService,
|
||||
VaultFileUploadedEvent,
|
||||
VaultFileRemovedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
@@ -9,15 +15,15 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
||||
|
||||
createFileRemovedEvent(payload: {
|
||||
createUserFileRemovedEvent(payload: {
|
||||
userUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
regularSubscriptionUuid: string
|
||||
}): FileRemovedEvent {
|
||||
}): UserFileRemovedEvent {
|
||||
return {
|
||||
type: 'FILE_REMOVED',
|
||||
type: 'USER_FILE_REMOVED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
@@ -30,14 +36,14 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
}
|
||||
}
|
||||
|
||||
createFileUploadedEvent(payload: {
|
||||
createUserFileUploadedEvent(payload: {
|
||||
userUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
}): FileUploadedEvent {
|
||||
}): UserFileUploadedEvent {
|
||||
return {
|
||||
type: 'FILE_UPLOADED',
|
||||
type: 'USER_FILE_UPLOADED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
@@ -49,4 +55,44 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
createVaultFileUploadedEvent(payload: {
|
||||
vaultUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
}): VaultFileUploadedEvent {
|
||||
return {
|
||||
type: 'VAULT_FILE_UPLOADED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: payload.vaultUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.Files,
|
||||
},
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
createVaultFileRemovedEvent(payload: {
|
||||
vaultUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
}): VaultFileRemovedEvent {
|
||||
return {
|
||||
type: 'VAULT_FILE_REMOVED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: payload.vaultUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.Files,
|
||||
},
|
||||
payload,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
import { FileUploadedEvent, FileRemovedEvent } from '@standardnotes/domain-events'
|
||||
import {
|
||||
UserFileUploadedEvent,
|
||||
UserFileRemovedEvent,
|
||||
VaultFileUploadedEvent,
|
||||
VaultFileRemovedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
|
||||
export interface DomainEventFactoryInterface {
|
||||
createFileUploadedEvent(payload: {
|
||||
createUserFileUploadedEvent(payload: {
|
||||
userUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
}): FileUploadedEvent
|
||||
createFileRemovedEvent(payload: {
|
||||
}): UserFileUploadedEvent
|
||||
createUserFileRemovedEvent(payload: {
|
||||
userUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
regularSubscriptionUuid: string
|
||||
}): FileRemovedEvent
|
||||
}): UserFileRemovedEvent
|
||||
|
||||
createVaultFileUploadedEvent(payload: {
|
||||
vaultUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
}): VaultFileUploadedEvent
|
||||
createVaultFileRemovedEvent(payload: {
|
||||
vaultUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
}): VaultFileRemovedEvent
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
||||
}
|
||||
|
||||
const response = await this.markFilesToBeRemoved.execute({
|
||||
userUuid: event.payload.userUuid,
|
||||
ownerUuid: event.payload.userUuid,
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
@@ -32,7 +32,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
||||
|
||||
for (const fileRemoved of response.filesRemoved) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createFileRemovedEvent({
|
||||
this.domainEventFactory.createUserFileRemovedEvent({
|
||||
regularSubscriptionUuid: event.payload.regularSubscriptionUuid,
|
||||
...fileRemoved,
|
||||
}),
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ export class SharedSubscriptionInvitationCanceledEventHandler implements DomainE
|
||||
}
|
||||
|
||||
const response = await this.markFilesToBeRemoved.execute({
|
||||
userUuid: event.payload.inviteeIdentifier,
|
||||
ownerUuid: event.payload.inviteeIdentifier,
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
@@ -32,7 +32,7 @@ export class SharedSubscriptionInvitationCanceledEventHandler implements DomainE
|
||||
|
||||
for (const fileRemoved of response.filesRemoved) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createFileRemovedEvent({
|
||||
this.domainEventFactory.createUserFileRemovedEvent({
|
||||
regularSubscriptionUuid: event.payload.inviterSubscriptionUuid,
|
||||
...fileRemoved,
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface FileMoverInterface {
|
||||
moveFile(sourcePath: string, destinationPath: string): Promise<void>
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export class CreateUploadSession implements UseCaseInterface {
|
||||
try {
|
||||
this.logger.debug(`Creating upload session for resource: ${dto.resourceRemoteIdentifier}`)
|
||||
|
||||
const filePath = `${dto.userUuid}/${dto.resourceRemoteIdentifier}`
|
||||
const filePath = `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`
|
||||
|
||||
const uploadId = await this.fileUploader.createUploadSession(filePath)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type CreateUploadSessionDTO = {
|
||||
userUuid: string
|
||||
ownerUuid: string
|
||||
resourceRemoteIdentifier: string
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export class FinishUploadSession implements UseCaseInterface {
|
||||
try {
|
||||
this.logger.debug(`Finishing upload session for resource: ${dto.resourceRemoteIdentifier}`)
|
||||
|
||||
const filePath = `${dto.userUuid}/${dto.resourceRemoteIdentifier}`
|
||||
const filePath = `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`
|
||||
|
||||
const uploadId = await this.uploadRepository.retrieveUploadSessionId(filePath)
|
||||
if (uploadId === undefined) {
|
||||
@@ -53,14 +53,25 @@ export class FinishUploadSession implements UseCaseInterface {
|
||||
|
||||
await this.fileUploader.finishUploadSession(uploadId, filePath, uploadChunkResults)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createFileUploadedEvent({
|
||||
userUuid: dto.userUuid,
|
||||
filePath: `${dto.userUuid}/${dto.resourceRemoteIdentifier}`,
|
||||
fileName: dto.resourceRemoteIdentifier,
|
||||
fileByteSize: totalFileSize,
|
||||
}),
|
||||
)
|
||||
if (dto.ownerType === 'user') {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createUserFileUploadedEvent({
|
||||
userUuid: dto.ownerUuid,
|
||||
filePath: `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`,
|
||||
fileName: dto.resourceRemoteIdentifier,
|
||||
fileByteSize: totalFileSize,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createVaultFileUploadedEvent({
|
||||
vaultUuid: dto.ownerUuid,
|
||||
filePath: `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`,
|
||||
fileName: dto.resourceRemoteIdentifier,
|
||||
fileByteSize: totalFileSize,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export type FinishUploadSessionDTO = {
|
||||
userUuid: string
|
||||
ownerUuid: string
|
||||
ownerType: 'user' | 'vault'
|
||||
resourceRemoteIdentifier: string
|
||||
uploadBytesUsed: number
|
||||
uploadBytesLimit: number
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Logger } from 'winston'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { GetFileMetadataDTO, isGetFileMetadataVaultOwned } from './GetFileMetadataDTO'
|
||||
import { GetFileMetadataDTO } from './GetFileMetadataDTO'
|
||||
import { GetFileMetadataResponse } from './GetFileMetadataResponse'
|
||||
|
||||
@injectable()
|
||||
@@ -14,16 +14,15 @@ export class GetFileMetadata implements UseCaseInterface {
|
||||
) {}
|
||||
|
||||
async execute(dto: GetFileMetadataDTO): Promise<GetFileMetadataResponse> {
|
||||
const ownerUuid = isGetFileMetadataVaultOwned(dto) ? dto.vaultUuid : dto.userUuid
|
||||
try {
|
||||
const size = await this.fileDownloader.getFileSize(`${ownerUuid}/${dto.resourceRemoteIdentifier}`)
|
||||
const size = await this.fileDownloader.getFileSize(`${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
size,
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Could not get file metadata for resource: ${ownerUuid}/${dto.resourceRemoteIdentifier}`)
|
||||
this.logger.error(`Could not get file metadata for resource: ${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not get file metadata.',
|
||||
|
||||
@@ -1,15 +1,4 @@
|
||||
export type GetFileMetadataDTO = GetVaultFileMetadataDTO | GetUserFileMetadataDTO
|
||||
|
||||
export type GetVaultFileMetadataDTO = {
|
||||
vaultUuid: string
|
||||
export type GetFileMetadataDTO = {
|
||||
ownerUuid: string
|
||||
resourceRemoteIdentifier: string
|
||||
}
|
||||
|
||||
export type GetUserFileMetadataDTO = {
|
||||
userUuid: string
|
||||
resourceRemoteIdentifier: string
|
||||
}
|
||||
|
||||
export function isGetFileMetadataVaultOwned(dto: GetFileMetadataDTO): dto is GetVaultFileMetadataDTO {
|
||||
return (dto as GetVaultFileMetadataDTO).vaultUuid !== undefined
|
||||
}
|
||||
|
||||
@@ -16,16 +16,16 @@ export class MarkFilesToBeRemoved implements UseCaseInterface {
|
||||
|
||||
async execute(dto: MarkFilesToBeRemovedDTO): Promise<MarkFilesToBeRemovedResponse> {
|
||||
try {
|
||||
this.logger.debug(`Marking files for later removal for user: ${dto.userUuid}`)
|
||||
this.logger.debug(`Marking files for later removal for user: ${dto.ownerUuid}`)
|
||||
|
||||
const filesRemoved = await this.fileRemover.markFilesToBeRemoved(dto.userUuid)
|
||||
const filesRemoved = await this.fileRemover.markFilesToBeRemoved(dto.ownerUuid)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
filesRemoved,
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Could not mark resources for removal: ${dto.userUuid} - ${(error as Error).message}`)
|
||||
this.logger.error(`Could not mark resources for removal: ${dto.ownerUuid} - ${(error as Error).message}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export type MarkFilesToBeRemovedDTO = {
|
||||
userUuid: string
|
||||
ownerUuid: string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { MoveFile } from './MoveFile'
|
||||
import { FileMoverInterface } from '../../Services/FileMoverInterface'
|
||||
|
||||
describe('MoveFile', () => {
|
||||
let fileRemover: FileMoverInterface
|
||||
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () => new MoveFile(fileRemover, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
fileRemover = {} as jest.Mocked<FileMoverInterface>
|
||||
fileRemover.moveFile = jest.fn().mockReturnValue(413)
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
logger.error = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should remove a file', async () => {
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
fromUserUuid: '1-2-3',
|
||||
toVaultUuid: '4-5-6',
|
||||
regularSubscriptionUuid: '3-4-5',
|
||||
})
|
||||
|
||||
expect(fileRemover.moveFile).toHaveBeenCalledWith('1-2-3/2-3-4', '4-5-6/2-3-4')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { FileMoverInterface } from '../../Services/FileMoverInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { MoveFileDTO, isMoveFileFromUserToVaultDTO } from './MoveFileDTO'
|
||||
import { MoveFileResponse } from './MoveFileResponse'
|
||||
|
||||
@injectable()
|
||||
export class MoveFile implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.FileMover) private fileMover: FileMoverInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: MoveFileDTO): Promise<MoveFileResponse> {
|
||||
try {
|
||||
this.logger.debug(`Removing file: ${dto.resourceRemoteIdentifier}`)
|
||||
|
||||
const srcPath = isMoveFileFromUserToVaultDTO(dto)
|
||||
? `${dto.fromUserUuid}/${dto.resourceRemoteIdentifier}`
|
||||
: `${dto.fromVaultUuid}/${dto.resourceRemoteIdentifier}`
|
||||
|
||||
const destPath = isMoveFileFromUserToVaultDTO(dto)
|
||||
? `${dto.toVaultUuid}/${dto.resourceRemoteIdentifier}`
|
||||
: `${dto.toUserUuid}/${dto.resourceRemoteIdentifier}`
|
||||
|
||||
await this.fileMover.moveFile(srcPath, destPath)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Could not remove resource: ${dto.resourceRemoteIdentifier} - ${(error as Error).message}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not remove resource',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export type MoveFileDTO = MoveFileFromUserToVaultDTO | MoveFileFromVaultToUserDTO
|
||||
|
||||
export type MoveFileFromUserToVaultDTO = {
|
||||
toVaultUuid: string
|
||||
fromUserUuid: string
|
||||
resourceRemoteIdentifier: string
|
||||
regularSubscriptionUuid: string
|
||||
}
|
||||
|
||||
export type MoveFileFromVaultToUserDTO = {
|
||||
fromVaultUuid: string
|
||||
toUserUuid: string
|
||||
resourceRemoteIdentifier: string
|
||||
regularSubscriptionUuid: string
|
||||
}
|
||||
|
||||
export function isMoveFileFromUserToVaultDTO(dto: MoveFileDTO): dto is MoveFileFromUserToVaultDTO {
|
||||
return (dto as MoveFileFromUserToVaultDTO).fromUserUuid !== undefined
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export type MoveFileResponse =
|
||||
| {
|
||||
success: true
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
message: string
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import TYPES from '../../../Bootstrap/Types'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
import { FileRemoverInterface } from '../../Services/FileRemoverInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { RemoveFileDTO } from './RemoveFileDTO'
|
||||
import { RemoveFileDTO, isRemoveUserFileDTO } from './RemoveFileDTO'
|
||||
import { RemoveFileResponse } from './RemoveFileResponse'
|
||||
|
||||
@injectable()
|
||||
@@ -22,19 +22,32 @@ export class RemoveFile implements UseCaseInterface {
|
||||
try {
|
||||
this.logger.debug(`Removing file: ${dto.resourceRemoteIdentifier}`)
|
||||
|
||||
const filePath = `${dto.userUuid}/${dto.resourceRemoteIdentifier}`
|
||||
const ownerUuid = isRemoveUserFileDTO(dto) ? dto.userUuid : dto.vaultUuid
|
||||
|
||||
const filePath = `${ownerUuid}/${dto.resourceRemoteIdentifier}`
|
||||
|
||||
const removedFileSize = await this.fileRemover.remove(filePath)
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createFileRemovedEvent({
|
||||
userUuid: dto.userUuid,
|
||||
filePath: `${dto.userUuid}/${dto.resourceRemoteIdentifier}`,
|
||||
fileName: dto.resourceRemoteIdentifier,
|
||||
fileByteSize: removedFileSize,
|
||||
regularSubscriptionUuid: dto.regularSubscriptionUuid,
|
||||
}),
|
||||
)
|
||||
if (isRemoveUserFileDTO(dto)) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createUserFileRemovedEvent({
|
||||
userUuid: dto.userUuid,
|
||||
filePath: `${dto.userUuid}/${dto.resourceRemoteIdentifier}`,
|
||||
fileName: dto.resourceRemoteIdentifier,
|
||||
fileByteSize: removedFileSize,
|
||||
regularSubscriptionUuid: dto.regularSubscriptionUuid,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createVaultFileRemovedEvent({
|
||||
vaultUuid: dto.vaultUuid,
|
||||
filePath: `${dto.vaultUuid}/${dto.resourceRemoteIdentifier}`,
|
||||
fileName: dto.resourceRemoteIdentifier,
|
||||
fileByteSize: removedFileSize,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
export type RemoveFileDTO = {
|
||||
export type RemoveFileDTO = RemoveUserFileDTO | RemoveVaultFileDTO
|
||||
|
||||
export type RemoveUserFileDTO = {
|
||||
userUuid: string
|
||||
resourceRemoteIdentifier: string
|
||||
regularSubscriptionUuid: string
|
||||
}
|
||||
|
||||
export type RemoveVaultFileDTO = {
|
||||
vaultUuid: string
|
||||
resourceRemoteIdentifier: string
|
||||
}
|
||||
|
||||
export function isRemoveUserFileDTO(dto: RemoveFileDTO): dto is RemoveUserFileDTO {
|
||||
return (dto as RemoveUserFileDTO).userUuid !== undefined
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Logger } from 'winston'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { StreamDownloadFileDTO, isStreamDownloadFileVaultOwned } from './StreamDownloadFileDTO'
|
||||
import { StreamDownloadFileDTO } from './StreamDownloadFileDTO'
|
||||
import { StreamDownloadFileResponse } from './StreamDownloadFileResponse'
|
||||
|
||||
@injectable()
|
||||
@@ -14,10 +14,9 @@ export class StreamDownloadFile implements UseCaseInterface {
|
||||
) {}
|
||||
|
||||
async execute(dto: StreamDownloadFileDTO): Promise<StreamDownloadFileResponse> {
|
||||
const ownerUuid = isStreamDownloadFileVaultOwned(dto) ? dto.vaultUuid : dto.userUuid
|
||||
try {
|
||||
const readStream = await this.fileDownloader.createDownloadStream(
|
||||
`${ownerUuid}/${dto.resourceRemoteIdentifier}`,
|
||||
`${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`,
|
||||
dto.startRange,
|
||||
dto.endRange,
|
||||
)
|
||||
@@ -27,7 +26,9 @@ export class StreamDownloadFile implements UseCaseInterface {
|
||||
readStream,
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Could not create a download stream for resource: ${ownerUuid}/${dto.resourceRemoteIdentifier}`)
|
||||
this.logger.error(
|
||||
`Could not create a download stream for resource: ${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`,
|
||||
)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@@ -1,19 +1,6 @@
|
||||
export type StreamDownloadFileDTO = StreamDownloadVaultFileDTO | StreamDownloadUserFileDTO
|
||||
|
||||
export type StreamDownloadVaultFileDTO = {
|
||||
vaultUuid: string
|
||||
export type StreamDownloadFileDTO = {
|
||||
ownerUuid: string
|
||||
resourceRemoteIdentifier: string
|
||||
startRange: number
|
||||
endRange: number
|
||||
}
|
||||
|
||||
export type StreamDownloadUserFileDTO = {
|
||||
userUuid: string
|
||||
resourceRemoteIdentifier: string
|
||||
startRange: number
|
||||
endRange: number
|
||||
}
|
||||
|
||||
export function isStreamDownloadFileVaultOwned(dto: StreamDownloadFileDTO): dto is StreamDownloadVaultFileDTO {
|
||||
return (dto as StreamDownloadVaultFileDTO).vaultUuid !== undefined
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export class UploadFileChunk implements UseCaseInterface {
|
||||
`Starting upload file chunk ${dto.chunkId} with ${dto.data.byteLength} bytes for resource: ${dto.resourceRemoteIdentifier}`,
|
||||
)
|
||||
|
||||
const filePath = `${dto.userUuid}/${dto.resourceRemoteIdentifier}`
|
||||
const filePath = `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`
|
||||
|
||||
const uploadId = await this.uploadRepository.retrieveUploadSessionId(filePath)
|
||||
if (uploadId === undefined) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ChunkId } from '../../Upload/ChunkId'
|
||||
export type UploadFileChunkDTO = {
|
||||
data: Uint8Array
|
||||
chunkId: ChunkId
|
||||
userUuid: string
|
||||
ownerUuid: string
|
||||
resourceRemoteIdentifier: string
|
||||
resourceUnencryptedFileSize: number
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { promises } from 'fs'
|
||||
|
||||
import { FileMoverInterface } from '../../Domain/Services/FileMoverInterface'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
|
||||
@injectable()
|
||||
export class FSFileMover implements FileMoverInterface {
|
||||
constructor(@inject(TYPES.FILE_UPLOAD_PATH) private fileUploadPath: string) {}
|
||||
|
||||
async moveFile(sourcePath: string, destinationPath: string): Promise<void> {
|
||||
const sourceFullPath = `${this.fileUploadPath}/${sourcePath}`
|
||||
const destinationFullPath = `${this.fileUploadPath}/${destinationPath}`
|
||||
|
||||
await promises.rename(sourceFullPath, destinationFullPath)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { CopyObjectCommand, DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { FileMoverInterface } from '../../Domain/Services/FileMoverInterface'
|
||||
|
||||
@injectable()
|
||||
export class S3FileMover implements FileMoverInterface {
|
||||
constructor(
|
||||
@inject(TYPES.S3) private s3Client: S3Client,
|
||||
@inject(TYPES.S3_BUCKET_NAME) private s3BucketName: string,
|
||||
) {}
|
||||
|
||||
async moveFile(sourcePath: string, destinationPath: string): Promise<void> {
|
||||
await this.s3Client.send(
|
||||
new CopyObjectCommand({
|
||||
Bucket: this.s3BucketName,
|
||||
CopySource: `${this.s3BucketName}/${sourcePath}`,
|
||||
Key: destinationPath,
|
||||
}),
|
||||
)
|
||||
|
||||
await this.s3Client.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: this.s3BucketName,
|
||||
Key: sourcePath,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user