mirror of
https://github.com/standardnotes/server
synced 2026-07-14 00:01:54 -04:00
refactor: vault files
This commit is contained in:
@@ -139,12 +139,12 @@ import {
|
||||
ValetTokenData,
|
||||
WebSocketConnectionTokenData,
|
||||
} from '@standardnotes/security'
|
||||
import { FileUploadedEventHandler } from '../Domain/Handler/FileUploadedEventHandler'
|
||||
import { UserFileUploadedEventHandler } from '../Domain/Handler/UserFileUploadedEventHandler'
|
||||
import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken'
|
||||
import { CreateListedAccount } from '../Domain/UseCase/CreateListedAccount/CreateListedAccount'
|
||||
import { ListedAccountCreatedEventHandler } from '../Domain/Handler/ListedAccountCreatedEventHandler'
|
||||
import { ListedAccountDeletedEventHandler } from '../Domain/Handler/ListedAccountDeletedEventHandler'
|
||||
import { FileRemovedEventHandler } from '../Domain/Handler/FileRemovedEventHandler'
|
||||
import { UserFileRemovedEventHandler } from '../Domain/Handler/UserFileRemovedEventHandler'
|
||||
import { UserDisabledSessionUserAgentLoggingEventHandler } from '../Domain/Handler/UserDisabledSessionUserAgentLoggingEventHandler'
|
||||
import { SettingInterpreterInterface } from '../Domain/Setting/SettingInterpreterInterface'
|
||||
import { SettingInterpreter } from '../Domain/Setting/SettingInterpreter'
|
||||
@@ -834,8 +834,8 @@ export class ContainerConfigLoader {
|
||||
.bind<SubscriptionReassignedEventHandler>(TYPES.SubscriptionReassignedEventHandler)
|
||||
.to(SubscriptionReassignedEventHandler)
|
||||
container.bind<UserEmailChangedEventHandler>(TYPES.UserEmailChangedEventHandler).to(UserEmailChangedEventHandler)
|
||||
container.bind<FileUploadedEventHandler>(TYPES.FileUploadedEventHandler).to(FileUploadedEventHandler)
|
||||
container.bind<FileRemovedEventHandler>(TYPES.FileRemovedEventHandler).to(FileRemovedEventHandler)
|
||||
container.bind<UserFileUploadedEventHandler>(TYPES.UserFileUploadedEventHandler).to(UserFileUploadedEventHandler)
|
||||
container.bind<UserFileRemovedEventHandler>(TYPES.UserFileRemovedEventHandler).to(UserFileRemovedEventHandler)
|
||||
container
|
||||
.bind<ListedAccountCreatedEventHandler>(TYPES.ListedAccountCreatedEventHandler)
|
||||
.to(ListedAccountCreatedEventHandler)
|
||||
@@ -873,8 +873,8 @@ export class ContainerConfigLoader {
|
||||
['EXTENSION_KEY_GRANTED', container.get(TYPES.ExtensionKeyGrantedEventHandler)],
|
||||
['SUBSCRIPTION_REASSIGNED', container.get(TYPES.SubscriptionReassignedEventHandler)],
|
||||
['USER_EMAIL_CHANGED', container.get(TYPES.UserEmailChangedEventHandler)],
|
||||
['FILE_UPLOADED', container.get(TYPES.FileUploadedEventHandler)],
|
||||
['FILE_REMOVED', container.get(TYPES.FileRemovedEventHandler)],
|
||||
['USER_FILE_UPLOADED', container.get(TYPES.UserFileUploadedEventHandler)],
|
||||
['USER_FILE_REMOVED', container.get(TYPES.UserFileRemovedEventHandler)],
|
||||
['LISTED_ACCOUNT_CREATED', container.get(TYPES.ListedAccountCreatedEventHandler)],
|
||||
['LISTED_ACCOUNT_DELETED', container.get(TYPES.ListedAccountDeletedEventHandler)],
|
||||
[
|
||||
|
||||
@@ -164,8 +164,8 @@ const TYPES = {
|
||||
SubscriptionSyncRequestedEventHandler: Symbol.for('SubscriptionSyncRequestedEventHandler'),
|
||||
ExtensionKeyGrantedEventHandler: Symbol.for('ExtensionKeyGrantedEventHandler'),
|
||||
UserEmailChangedEventHandler: Symbol.for('UserEmailChangedEventHandler'),
|
||||
FileUploadedEventHandler: Symbol.for('FileUploadedEventHandler'),
|
||||
FileRemovedEventHandler: Symbol.for('FileRemovedEventHandler'),
|
||||
UserFileUploadedEventHandler: Symbol.for('UserFileUploadedEventHandler'),
|
||||
UserFileRemovedEventHandler: Symbol.for('UserFileRemovedEventHandler'),
|
||||
ListedAccountCreatedEventHandler: Symbol.for('ListedAccountCreatedEventHandler'),
|
||||
ListedAccountDeletedEventHandler: Symbol.for('ListedAccountDeletedEventHandler'),
|
||||
UserDisabledSessionUserAgentLoggingEventHandler: Symbol.for('UserDisabledSessionUserAgentLoggingEventHandler'),
|
||||
|
||||
@@ -7,12 +7,13 @@ import {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
results,
|
||||
} from 'inversify-express-utils'
|
||||
import { CreateValetTokenPayload, ErrorTag } from '@standardnotes/responses'
|
||||
import { ErrorTag } from '@standardnotes/responses'
|
||||
import { ValetTokenOperation } from '@standardnotes/security'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken'
|
||||
import { CreateValetTokenPayload } from '../Domain/ValetToken/CreateValetTokenPayload'
|
||||
|
||||
@controller('/valet-tokens', TYPES.ApiGatewayAuthMiddleware)
|
||||
export class ValetTokenController extends BaseHttpController {
|
||||
|
||||
+7
-6
@@ -1,25 +1,26 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { FileRemovedEvent } from '@standardnotes/domain-events'
|
||||
import { UserFileRemovedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { User } from '../User/User'
|
||||
import { FileRemovedEventHandler } from './FileRemovedEventHandler'
|
||||
import { UserFileRemovedEventHandler } from './UserFileRemovedEventHandler'
|
||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
|
||||
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
|
||||
|
||||
describe('FileRemovedEventHandler', () => {
|
||||
describe('UserFileRemovedEventHandler', () => {
|
||||
let userSubscriptionService: UserSubscriptionServiceInterface
|
||||
let logger: Logger
|
||||
let user: User
|
||||
let event: FileRemovedEvent
|
||||
let event: UserFileRemovedEvent
|
||||
let subscriptionSettingService: SubscriptionSettingServiceInterface
|
||||
let regularSubscription: UserSubscription
|
||||
let sharedSubscription: UserSubscription
|
||||
|
||||
const createHandler = () => new FileRemovedEventHandler(userSubscriptionService, subscriptionSettingService, logger)
|
||||
const createHandler = () =>
|
||||
new UserFileRemovedEventHandler(userSubscriptionService, subscriptionSettingService, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
@@ -47,7 +48,7 @@ describe('FileRemovedEventHandler', () => {
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
subscriptionSettingService.createOrReplace = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<FileRemovedEvent>
|
||||
event = {} as jest.Mocked<UserFileRemovedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
userUuid: '1-2-3',
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { DomainEventHandlerInterface, FileRemovedEvent } from '@standardnotes/domain-events'
|
||||
import { DomainEventHandlerInterface, UserFileRemovedEvent } from '@standardnotes/domain-events'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
@@ -10,14 +10,14 @@ import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
|
||||
|
||||
@injectable()
|
||||
export class FileRemovedEventHandler implements DomainEventHandlerInterface {
|
||||
export class UserFileRemovedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
|
||||
@inject(TYPES.SubscriptionSettingService) private subscriptionSettingService: SubscriptionSettingServiceInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: FileRemovedEvent): Promise<void> {
|
||||
async handle(event: UserFileRemovedEvent): Promise<void> {
|
||||
const { regularSubscription, sharedSubscription } =
|
||||
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(event.payload.userUuid)
|
||||
if (regularSubscription === null) {
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { FileUploadedEvent } from '@standardnotes/domain-events'
|
||||
import { UserFileUploadedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { FileUploadedEventHandler } from './FileUploadedEventHandler'
|
||||
import { UserFileUploadedEventHandler } from './UserFileUploadedEventHandler'
|
||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
|
||||
@@ -16,13 +16,13 @@ describe('FileUploadedEventHandler', () => {
|
||||
let userSubscriptionService: UserSubscriptionServiceInterface
|
||||
let logger: Logger
|
||||
let user: User
|
||||
let event: FileUploadedEvent
|
||||
let event: UserFileUploadedEvent
|
||||
let subscriptionSettingService: SubscriptionSettingServiceInterface
|
||||
let regularSubscription: UserSubscription
|
||||
let sharedSubscription: UserSubscription
|
||||
|
||||
const createHandler = () =>
|
||||
new FileUploadedEventHandler(userRepository, userSubscriptionService, subscriptionSettingService, logger)
|
||||
new UserFileUploadedEventHandler(userRepository, userSubscriptionService, subscriptionSettingService, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
@@ -53,7 +53,7 @@ describe('FileUploadedEventHandler', () => {
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
subscriptionSettingService.createOrReplace = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<FileUploadedEvent>
|
||||
event = {} as jest.Mocked<UserFileUploadedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
userUuid: '1-2-3',
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { DomainEventHandlerInterface, FileUploadedEvent } from '@standardnotes/domain-events'
|
||||
import { DomainEventHandlerInterface, UserFileUploadedEvent } from '@standardnotes/domain-events'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
@@ -11,7 +11,7 @@ import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscripti
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class FileUploadedEventHandler implements DomainEventHandlerInterface {
|
||||
export class UserFileUploadedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
|
||||
@@ -19,7 +19,7 @@ export class FileUploadedEventHandler implements DomainEventHandlerInterface {
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: FileUploadedEvent): Promise<void> {
|
||||
async handle(event: UserFileUploadedEvent): Promise<void> {
|
||||
const user = await this.userRepository.findOneByUuid(event.payload.userUuid)
|
||||
if (user === null) {
|
||||
this.logger.warn(`Could not find user with uuid: ${event.payload.userUuid}`)
|
||||
@@ -2,7 +2,7 @@ import { inject, injectable } from 'inversify'
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { TokenEncoderInterface, ValetTokenData } from '@standardnotes/security'
|
||||
import { CreateValetTokenPayload, CreateValetTokenResponseData } from '@standardnotes/responses'
|
||||
import { CreateValetTokenResponseData } from '@standardnotes/responses'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
@@ -12,6 +12,7 @@ import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionS
|
||||
import { CreateValetTokenDTO } from './CreateValetTokenDTO'
|
||||
import { SubscriptionSettingsAssociationServiceInterface } from '../../Setting/SubscriptionSettingsAssociationServiceInterface'
|
||||
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
|
||||
import { CreateValetTokenPayload } from '../../ValetToken/CreateValetTokenPayload'
|
||||
|
||||
@injectable()
|
||||
export class CreateValetToken implements UseCaseInterface {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export type CreateValetTokenPayload = {
|
||||
operation: 'read' | 'write' | 'delete' | 'move'
|
||||
resources: Array<{
|
||||
remoteIdentifier: string
|
||||
unencryptedFileSize?: number
|
||||
}>
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { FileRemovedEventPayload } from './FileRemovedEventPayload'
|
||||
|
||||
export interface FileRemovedEvent extends DomainEventInterface {
|
||||
type: 'FILE_REMOVED'
|
||||
payload: FileRemovedEventPayload
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { FileUploadedEventPayload } from './FileUploadedEventPayload'
|
||||
|
||||
export interface FileUploadedEvent extends DomainEventInterface {
|
||||
type: 'FILE_UPLOADED'
|
||||
payload: FileUploadedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { UserFileRemovedEventPayload } from './UserFileRemovedEventPayload'
|
||||
|
||||
export interface UserFileRemovedEvent extends DomainEventInterface {
|
||||
type: 'USER_FILE_REMOVED'
|
||||
payload: UserFileRemovedEventPayload
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export interface FileRemovedEventPayload {
|
||||
export interface UserFileRemovedEventPayload {
|
||||
userUuid: string
|
||||
regularSubscriptionUuid: string
|
||||
fileByteSize: number
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { UserFileUploadedEventPayload } from './UserFileUploadedEventPayload'
|
||||
|
||||
export interface UserFileUploadedEvent extends DomainEventInterface {
|
||||
type: 'USER_FILE_UPLOADED'
|
||||
payload: UserFileUploadedEventPayload
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export interface FileUploadedEventPayload {
|
||||
export interface UserFileUploadedEventPayload {
|
||||
userUuid: string
|
||||
fileByteSize: number
|
||||
filePath: string
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { VaultFileRemovedEventPayload } from './VaultFileRemovedEventPayload'
|
||||
|
||||
export interface VaultFileRemovedEvent extends DomainEventInterface {
|
||||
type: 'VAULT_FILE_REMOVED'
|
||||
payload: VaultFileRemovedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface VaultFileRemovedEventPayload {
|
||||
vaultUuid: string
|
||||
fileByteSize: number
|
||||
filePath: string
|
||||
fileName: string
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { VaultFileUploadedEventPayload } from './VaultFileUploadedEventPayload'
|
||||
|
||||
export interface VaultFileUploadedEvent extends DomainEventInterface {
|
||||
type: 'VAULT_FILE_UPLOADED'
|
||||
payload: VaultFileUploadedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface VaultFileUploadedEventPayload {
|
||||
vaultUuid: string
|
||||
fileByteSize: number
|
||||
filePath: string
|
||||
fileName: string
|
||||
}
|
||||
@@ -26,10 +26,17 @@ export * from './Event/ExitDiscountWithdrawRequestedEvent'
|
||||
export * from './Event/ExitDiscountWithdrawRequestedEventPayload'
|
||||
export * from './Event/ExtensionKeyGrantedEvent'
|
||||
export * from './Event/ExtensionKeyGrantedEventPayload'
|
||||
export * from './Event/FileRemovedEvent'
|
||||
export * from './Event/FileRemovedEventPayload'
|
||||
export * from './Event/FileUploadedEvent'
|
||||
export * from './Event/FileUploadedEventPayload'
|
||||
|
||||
export * from './Event/UserFileRemovedEvent'
|
||||
export * from './Event/UserFileRemovedEventPayload'
|
||||
export * from './Event/UserFileUploadedEvent'
|
||||
export * from './Event/UserFileUploadedEventPayload'
|
||||
|
||||
export * from './Event/VaultFileRemovedEvent'
|
||||
export * from './Event/VaultFileRemovedEventPayload'
|
||||
export * from './Event/VaultFileUploadedEvent'
|
||||
export * from './Event/VaultFileUploadedEventPayload'
|
||||
|
||||
export * from './Event/ItemDumpedEvent'
|
||||
export * from './Event/ItemDumpedEventPayload'
|
||||
export * from './Event/ItemRevisionCreationRequestedEvent'
|
||||
|
||||
@@ -76,8 +76,8 @@ export class ContainerConfigLoader {
|
||||
['SUBSCRIPTION_REASSIGNED', container.get(TYPES.EventHandler)],
|
||||
['USER_CREDENTIALS_CHANGED', container.get(TYPES.EventHandler)],
|
||||
['USER_EMAIL_CHANGED', container.get(TYPES.EventHandler)],
|
||||
['FILE_UPLOADED', container.get(TYPES.EventHandler)],
|
||||
['FILE_REMOVED', container.get(TYPES.EventHandler)],
|
||||
['USER_FILE_UPLOADED', container.get(TYPES.EventHandler)],
|
||||
['USER_FILE_REMOVED', container.get(TYPES.EventHandler)],
|
||||
['LISTED_ACCOUNT_REQUESTED', container.get(TYPES.EventHandler)],
|
||||
['LISTED_ACCOUNT_CREATED', container.get(TYPES.EventHandler)],
|
||||
['LISTED_ACCOUNT_DELETED', container.get(TYPES.EventHandler)],
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,5 @@ export enum ValetTokenOperation {
|
||||
Read = 'read',
|
||||
Write = 'write',
|
||||
Delete = 'delete',
|
||||
Move = 'move',
|
||||
}
|
||||
|
||||
@@ -4,4 +4,11 @@ export type VaultValetTokenData = {
|
||||
vaultUuid: string
|
||||
permittedOperation: ValetTokenOperation
|
||||
remoteIdentifier: string
|
||||
unencryptedFileSize?: number
|
||||
uploadBytesUsed: number
|
||||
uploadBytesLimit: number
|
||||
moveOperation?: {
|
||||
type: 'vault-to-user' | 'user-to-vault'
|
||||
userUuid: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,14 @@ export class CreateVault1684173017359 implements MigrationInterface {
|
||||
name: 'vault_key_timestamp',
|
||||
type: 'bigint',
|
||||
}),
|
||||
new TableColumn({
|
||||
name: 'file_upload_bytes_used',
|
||||
type: 'bigint',
|
||||
}),
|
||||
new TableColumn({
|
||||
name: 'file_upload_bytes_limit',
|
||||
type: 'bigint',
|
||||
}),
|
||||
new TableColumn({
|
||||
name: 'created_at_timestamp',
|
||||
type: 'bigint',
|
||||
|
||||
@@ -47,7 +47,7 @@ import { ContactServiceInterface } from '../Domain/Contact/Service/ContactServic
|
||||
import { ContactService } from '../Domain/Contact/Service/ContactService'
|
||||
import { ContactFactoryInterface } from '../Domain/Contact/Factory/ContactFactoryInterface'
|
||||
import { SnjsVersionFilter } from '../Domain/Item/SaveRule/SnjsVersionFilter'
|
||||
import { CreateVaultFileReadValetToken } from '../Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileReadValetToken'
|
||||
import { CreateVaultFileValetToken } from '../Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileValetToken'
|
||||
|
||||
export class ServerContainerConfigLoader extends CommonContainerConfigLoader {
|
||||
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
|
||||
@@ -115,10 +115,11 @@ export class ServerContainerConfigLoader extends CommonContainerConfigLoader {
|
||||
})
|
||||
|
||||
container
|
||||
.bind<CreateVaultFileReadValetToken>(TYPES.CreateVaultFileReadValetToken)
|
||||
.bind<CreateVaultFileValetToken>(TYPES.CreateVaultFileReadValetToken)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new CreateVaultFileReadValetToken(
|
||||
return new CreateVaultFileValetToken(
|
||||
context.container.get(TYPES.GetGlobalItem),
|
||||
context.container.get(TYPES.VaultService),
|
||||
context.container.get(TYPES.VaultUserService),
|
||||
context.container.get(TYPES.ValetTokenEncoder),
|
||||
context.container.get(TYPES.VALET_TOKEN_TTL),
|
||||
|
||||
@@ -78,6 +78,8 @@ const TYPES = {
|
||||
DuplicateItemSyncedEventHandler: Symbol.for('DuplicateItemSyncedEventHandler'),
|
||||
EmailBackupRequestedEventHandler: Symbol.for('EmailBackupRequestedEventHandler'),
|
||||
ItemRevisionCreationRequestedEventHandler: Symbol.for('ItemRevisionCreationRequestedEventHandler'),
|
||||
VaultFileUploadedEventHandler: Symbol.for('VaultFileUploadedEventHandler'),
|
||||
VaultFileRemovedEventHandler: Symbol.for('VaultFileRemovedEventHandler'),
|
||||
|
||||
ContentDecoder: Symbol.for('ContentDecoder'),
|
||||
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
|
||||
|
||||
@@ -31,6 +31,8 @@ import { ItemRevisionCreationRequestedEventHandler } from '../Domain/Handler/Ite
|
||||
import { FSItemBackupService } from '../Infra/FS/FSItemBackupService'
|
||||
import { CommonContainerConfigLoader } from './CommonContainerConfigLoader'
|
||||
import { UserCredentialsChangedEventHandler } from '../Domain/Handler/UserCredentialsChangedEventHandler'
|
||||
import { VaultFileUploadedEventHandler } from '../Domain/Handler/VaultFileUploadedEventHandler'
|
||||
import { VaultFileRemovedEventHandler } from '../Domain/Handler/VaultFileRemovedEventHandler'
|
||||
|
||||
export class WorkerContainerConfigLoader extends CommonContainerConfigLoader {
|
||||
private readonly DEFAULT_FILE_UPLOAD_PATH = `${__dirname}/../../uploads`
|
||||
@@ -143,6 +145,9 @@ export class WorkerContainerConfigLoader extends CommonContainerConfigLoader {
|
||||
)
|
||||
})
|
||||
|
||||
container.bind<VaultFileUploadedEventHandler>(TYPES.VaultFileUploadedEventHandler).to(VaultFileUploadedEventHandler)
|
||||
container.bind<VaultFileRemovedEventHandler>(TYPES.VaultFileRemovedEventHandler).to(VaultFileRemovedEventHandler)
|
||||
|
||||
// Services
|
||||
container.bind<ContentDecoder>(TYPES.ContentDecoder).toDynamicValue(() => new ContentDecoder())
|
||||
container.bind<AxiosInstance>(TYPES.HTTPClient).toDynamicValue(() => axios.create())
|
||||
@@ -194,6 +199,8 @@ export class WorkerContainerConfigLoader extends CommonContainerConfigLoader {
|
||||
['EMAIL_BACKUP_REQUESTED', context.container.get(TYPES.EmailBackupRequestedEventHandler)],
|
||||
['ITEM_REVISION_CREATION_REQUESTED', context.container.get(TYPES.ItemRevisionCreationRequestedEventHandler)],
|
||||
['USER_CREDENTIALS_CHANGED', container.get(TYPES.UserCredentialsChangedEventHandler)],
|
||||
['VAULT_FILE_REMOVED', container.get(TYPES.VaultFileRemovedEventHandler)],
|
||||
['VAULT_FILE_UPLOADED', container.get(TYPES.VaultFileUploadedEventHandler)],
|
||||
])
|
||||
|
||||
const handler =
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Vault } from '../Domain/Vault/Model/Vault'
|
||||
import { VaultProjection } from '../Projection/VaultProjection'
|
||||
import { VaultUserProjection } from '../Projection/VaultUserProjection'
|
||||
import { VaultUserServiceInterface } from '../Domain/VaultUser/Service/VaultUserServiceInterface'
|
||||
import { CreateVaultFileReadValetToken } from '../Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileReadValetToken'
|
||||
import { CreateVaultFileValetToken } from '../Domain/UseCase/CreateVaultFileValetToken/CreateVaultFileValetToken'
|
||||
|
||||
@controller('/vaults')
|
||||
export class VaultsController extends BaseHttpController {
|
||||
@@ -26,7 +26,7 @@ export class VaultsController extends BaseHttpController {
|
||||
@inject(TYPES.VaultUserService) private vaultUserService: VaultUserServiceInterface,
|
||||
@inject(TYPES.VaultProjector) private vaultProjector: ProjectorInterface<Vault, VaultProjection>,
|
||||
@inject(TYPES.VaultUserProjector) private vaultUserProjector: ProjectorInterface<VaultUser, VaultUserProjection>,
|
||||
@inject(TYPES.CreateVaultFileReadValetToken) private createVaultFileReadValetToken: CreateVaultFileReadValetToken,
|
||||
@inject(TYPES.CreateVaultFileReadValetToken) private createVaultFileReadValetToken: CreateVaultFileValetToken,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -52,6 +52,8 @@ export class VaultsController extends BaseHttpController {
|
||||
vaultUuid: request.params.vaultUuid,
|
||||
fileUuid: request.body.file_uuid,
|
||||
remoteIdentifier: request.body.remote_identifier,
|
||||
moveOperationType: request.body.move_operation_type,
|
||||
operation: request.body.operation,
|
||||
})
|
||||
|
||||
if (valetTokenResult.success === false) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { DomainEventHandlerInterface, VaultFileRemovedEvent } from '@standardnotes/domain-events'
|
||||
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { VaultServiceInterface } from '../Vault/Service/VaultServiceInterface'
|
||||
import { VaultsRepositoryInterface } from '../Vault/Repository/VaultRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class VaultFileRemovedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.VaultService) private vaultService: VaultServiceInterface,
|
||||
@inject(TYPES.VaultRepository) private vaultRepository: VaultsRepositoryInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: VaultFileRemovedEvent): Promise<void> {
|
||||
const vault = await this.vaultService.getVault({
|
||||
vaultUuid: event.payload.vaultUuid,
|
||||
})
|
||||
|
||||
if (vault === null) {
|
||||
this.logger.warn(`Could not find vault with uuid: ${event.payload.vaultUuid}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
vault.fileUploadBytesUsed -= event.payload.fileByteSize
|
||||
|
||||
await this.vaultRepository.save(vault)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { DomainEventHandlerInterface, VaultFileUploadedEvent } from '@standardnotes/domain-events'
|
||||
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { VaultServiceInterface } from '../Vault/Service/VaultServiceInterface'
|
||||
import { VaultsRepositoryInterface } from '../Vault/Repository/VaultRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class VaultFileUploadedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.VaultService) private vaultService: VaultServiceInterface,
|
||||
@inject(TYPES.VaultRepository) private vaultRepository: VaultsRepositoryInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: VaultFileUploadedEvent): Promise<void> {
|
||||
const vault = await this.vaultService.getVault({
|
||||
vaultUuid: event.payload.vaultUuid,
|
||||
})
|
||||
|
||||
if (vault === null) {
|
||||
this.logger.warn(`Could not find vault with uuid: ${event.payload.vaultUuid}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
vault.fileUploadBytesUsed += event.payload.fileByteSize
|
||||
|
||||
await this.vaultRepository.save(vault)
|
||||
}
|
||||
}
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { VaultValetTokenData, TokenEncoderInterface, ValetTokenOperation } from '@standardnotes/security'
|
||||
import { CreateValetTokenResponseData } from '@standardnotes/responses'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { CreateVaultFileReadValetTokenDTO } from './CreateVaultFileReadValetTokenDTO'
|
||||
import { GetGlobalItem } from '../GetGlobalItem/GetGlobalItem'
|
||||
import { VaultUserServiceInterface } from '../../VaultUser/Service/VaultUserServiceInterface'
|
||||
|
||||
@injectable()
|
||||
export class CreateVaultFileReadValetToken implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.GetGlobalItem) private getGlobalItem: GetGlobalItem,
|
||||
@inject(TYPES.VaultUserService) private vaultUserService: VaultUserServiceInterface,
|
||||
@inject(TYPES.ValetTokenEncoder) private tokenEncoder: TokenEncoderInterface<VaultValetTokenData>,
|
||||
@inject(TYPES.VALET_TOKEN_TTL) private valetTokenTTL: number,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateVaultFileReadValetTokenDTO): Promise<CreateValetTokenResponseData> {
|
||||
const itemResponse = await this.getGlobalItem.execute({
|
||||
itemUuid: dto.fileUuid,
|
||||
})
|
||||
|
||||
if (
|
||||
itemResponse.success === false ||
|
||||
!itemResponse.item.vaultUuid ||
|
||||
itemResponse.item.vaultUuid !== dto.vaultUuid
|
||||
) {
|
||||
return {
|
||||
success: false,
|
||||
reason: 'invalid-parameters',
|
||||
}
|
||||
}
|
||||
|
||||
const vaultUser = await this.vaultUserService.getUserForVault({
|
||||
userUuid: dto.userUuid,
|
||||
vaultUuid: dto.vaultUuid,
|
||||
})
|
||||
|
||||
if (!vaultUser) {
|
||||
return {
|
||||
success: false,
|
||||
reason: 'invalid-parameters',
|
||||
}
|
||||
}
|
||||
|
||||
const tokenData: VaultValetTokenData = {
|
||||
vaultUuid: dto.vaultUuid,
|
||||
permittedOperation: ValetTokenOperation.Read,
|
||||
remoteIdentifier: dto.remoteIdentifier,
|
||||
}
|
||||
|
||||
const valetToken = this.tokenEncoder.encodeExpirableToken(tokenData, this.valetTokenTTL)
|
||||
|
||||
return { success: true, valetToken }
|
||||
}
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
export type CreateVaultFileReadValetTokenDTO = {
|
||||
userUuid: string
|
||||
vaultUuid: string
|
||||
fileUuid: string
|
||||
remoteIdentifier: string
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { VaultValetTokenData, TokenEncoderInterface, ValetTokenOperation } from '@standardnotes/security'
|
||||
import { CreateValetTokenResponseData } from '@standardnotes/responses'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { CreateVaultFileValetTokenDTO } from './CreateVaultFileValetTokenDTO'
|
||||
import { GetGlobalItem } from '../GetGlobalItem/GetGlobalItem'
|
||||
import { VaultUserServiceInterface } from '../../VaultUser/Service/VaultUserServiceInterface'
|
||||
import { VaultServiceInterface } from '../../Vault/Service/VaultServiceInterface'
|
||||
|
||||
@injectable()
|
||||
export class CreateVaultFileValetToken implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.GetGlobalItem) private getGlobalItem: GetGlobalItem,
|
||||
@inject(TYPES.VaultService) private vaultService: VaultServiceInterface,
|
||||
@inject(TYPES.VaultUserService) private vaultUserService: VaultUserServiceInterface,
|
||||
@inject(TYPES.ValetTokenEncoder) private tokenEncoder: TokenEncoderInterface<VaultValetTokenData>,
|
||||
@inject(TYPES.VALET_TOKEN_TTL) private valetTokenTTL: number,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateVaultFileValetTokenDTO): Promise<CreateValetTokenResponseData> {
|
||||
const itemResponse = await this.getGlobalItem.execute({
|
||||
itemUuid: dto.fileUuid,
|
||||
})
|
||||
|
||||
const failValue: CreateValetTokenResponseData = {
|
||||
success: false,
|
||||
reason: 'invalid-parameters',
|
||||
}
|
||||
|
||||
if (itemResponse.success === false) {
|
||||
return failValue
|
||||
}
|
||||
|
||||
if (!itemResponse.item.vaultUuid || itemResponse.item.vaultUuid !== dto.vaultUuid) {
|
||||
return failValue
|
||||
}
|
||||
|
||||
const vaultUser = await this.vaultUserService.getUserForVault({
|
||||
userUuid: dto.userUuid,
|
||||
vaultUuid: dto.vaultUuid,
|
||||
})
|
||||
|
||||
if (!vaultUser) {
|
||||
return failValue
|
||||
}
|
||||
|
||||
const vault = await this.vaultService.getVault({
|
||||
vaultUuid: dto.vaultUuid,
|
||||
})
|
||||
|
||||
if (!vault) {
|
||||
return failValue
|
||||
}
|
||||
|
||||
const tokenData: VaultValetTokenData = {
|
||||
vaultUuid: dto.vaultUuid,
|
||||
permittedOperation: dto.operation,
|
||||
remoteIdentifier: dto.remoteIdentifier,
|
||||
uploadBytesUsed: vault.fileUploadBytesUsed,
|
||||
uploadBytesLimit: vault.fileUploadBytesLimit,
|
||||
}
|
||||
|
||||
if (dto.operation === ValetTokenOperation.Move) {
|
||||
if (!dto.moveOperationType) {
|
||||
return failValue
|
||||
}
|
||||
|
||||
if (dto.moveOperationType === 'vault-to-user') {
|
||||
if (vaultUser.permissions === 'read') {
|
||||
return failValue
|
||||
} else {
|
||||
tokenData.moveOperation = {
|
||||
type: 'vault-to-user',
|
||||
userUuid: dto.userUuid,
|
||||
}
|
||||
}
|
||||
} else if (dto.moveOperationType === 'user-to-vault') {
|
||||
if (vaultUser.permissions === 'read') {
|
||||
return failValue
|
||||
} else {
|
||||
tokenData.moveOperation = {
|
||||
type: 'user-to-vault',
|
||||
userUuid: dto.userUuid,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return failValue
|
||||
}
|
||||
}
|
||||
|
||||
const valetToken = this.tokenEncoder.encodeExpirableToken(tokenData, this.valetTokenTTL)
|
||||
|
||||
return { success: true, valetToken }
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { ValetTokenOperation } from '@standardnotes/security'
|
||||
|
||||
export type CreateVaultFileValetTokenDTO = {
|
||||
userUuid: string
|
||||
vaultUuid: string
|
||||
fileUuid: string
|
||||
remoteIdentifier: string
|
||||
operation: ValetTokenOperation
|
||||
moveOperationType?: 'vault-to-user' | 'user-to-vault'
|
||||
}
|
||||
@@ -3,6 +3,8 @@ export type VaultHash = {
|
||||
user_uuid: string
|
||||
specified_items_key_uuid: string
|
||||
vault_key_timestamp: number
|
||||
file_upload_bytes_used: number
|
||||
file_upload_bytes_limit: number
|
||||
created_at_timestamp?: number
|
||||
updated_at_timestamp?: number
|
||||
}
|
||||
|
||||
@@ -24,6 +24,18 @@ export class Vault {
|
||||
})
|
||||
declare vaultKeyTimestamp: number
|
||||
|
||||
@Column({
|
||||
name: 'file_upload_bytes_used',
|
||||
type: 'bigint',
|
||||
})
|
||||
declare fileUploadBytesUsed: number
|
||||
|
||||
@Column({
|
||||
name: 'file_upload_bytes_limit',
|
||||
type: 'bigint',
|
||||
})
|
||||
declare fileUploadBytesLimit: number
|
||||
|
||||
@Column({
|
||||
name: 'created_at_timestamp',
|
||||
type: 'bigint',
|
||||
|
||||
@@ -29,6 +29,8 @@ export class VaultService implements VaultServiceInterface {
|
||||
user_uuid: dto.userUuid,
|
||||
specified_items_key_uuid: dto.specifiedItemsKeyUuid,
|
||||
vault_key_timestamp: dto.vaultKeyTimestamp,
|
||||
file_upload_bytes_limit: 1_000_000,
|
||||
file_upload_bytes_used: 0,
|
||||
created_at_timestamp: timestamp,
|
||||
updated_at_timestamp: timestamp,
|
||||
},
|
||||
|
||||
@@ -3,6 +3,8 @@ export type VaultProjection = {
|
||||
user_uuid: string
|
||||
specified_items_key_uuid: string
|
||||
vault_key_timestamp: number
|
||||
file_upload_bytes_used: number
|
||||
file_upload_bytes_limit: number
|
||||
created_at_timestamp: number
|
||||
updated_at_timestamp: number
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ export class VaultProjector implements ProjectorInterface<Vault, VaultProjection
|
||||
user_uuid: vault.userUuid,
|
||||
specified_items_key_uuid: vault.specifiedItemsKeyUuid,
|
||||
vault_key_timestamp: vault.vaultKeyTimestamp,
|
||||
file_upload_bytes_used: vault.fileUploadBytesUsed,
|
||||
file_upload_bytes_limit: vault.fileUploadBytesLimit,
|
||||
created_at_timestamp: vault.createdAtTimestamp,
|
||||
updated_at_timestamp: vault.updatedAtTimestamp,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user