refactor: vault files

This commit is contained in:
Mo
2023-05-28 14:11:31 -05:00
parent 690f18708a
commit 94deab2ee0
69 changed files with 761 additions and 212 deletions
@@ -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,
}),
@@ -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
}