mirror of
https://github.com/standardnotes/server
synced 2026-01-16 20:04:32 -05:00
feat: update storage quota used for user based on shared vault files (#689)
* feat: update storage quota used for user based on shared vault files * fix: use case binding * fix: increase file upload bytes limit for shared vaults
This commit is contained in:
@@ -253,6 +253,9 @@ import { BaseSessionsController } from '../Infra/InversifyExpressUtils/Base/Base
|
||||
import { Transform } from 'stream'
|
||||
import { ActivatePremiumFeatures } from '../Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures'
|
||||
import { PaymentsAccountDeletedEventHandler } from '../Domain/Handler/PaymentsAccountDeletedEventHandler'
|
||||
import { UpdateStorageQuotaUsedForUser } from '../Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
|
||||
import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler'
|
||||
import { SharedVaultFileRemovedEventHandler } from '../Domain/Handler/SharedVaultFileRemovedEventHandler'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(configuration?: {
|
||||
@@ -882,6 +885,15 @@ export class ContainerConfigLoader {
|
||||
container.bind<VerifyPredicate>(TYPES.Auth_VerifyPredicate).to(VerifyPredicate)
|
||||
container.bind<CreateCrossServiceToken>(TYPES.Auth_CreateCrossServiceToken).to(CreateCrossServiceToken)
|
||||
container.bind<ProcessUserRequest>(TYPES.Auth_ProcessUserRequest).to(ProcessUserRequest)
|
||||
container
|
||||
.bind<UpdateStorageQuotaUsedForUser>(TYPES.Auth_UpdateStorageQuotaUsedForUser)
|
||||
.toConstantValue(
|
||||
new UpdateStorageQuotaUsedForUser(
|
||||
container.get(TYPES.Auth_UserRepository),
|
||||
container.get(TYPES.Auth_UserSubscriptionService),
|
||||
container.get(TYPES.Auth_SubscriptionSettingService),
|
||||
),
|
||||
)
|
||||
|
||||
// Controller
|
||||
container
|
||||
@@ -951,8 +963,38 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<UserEmailChangedEventHandler>(TYPES.Auth_UserEmailChangedEventHandler)
|
||||
.to(UserEmailChangedEventHandler)
|
||||
container.bind<FileUploadedEventHandler>(TYPES.Auth_FileUploadedEventHandler).to(FileUploadedEventHandler)
|
||||
container.bind<FileRemovedEventHandler>(TYPES.Auth_FileRemovedEventHandler).to(FileRemovedEventHandler)
|
||||
container
|
||||
.bind<FileUploadedEventHandler>(TYPES.Auth_FileUploadedEventHandler)
|
||||
.toConstantValue(
|
||||
new FileUploadedEventHandler(
|
||||
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
|
||||
container.get(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SharedVaultFileUploadedEventHandler>(TYPES.Auth_SharedVaultFileUploadedEventHandler)
|
||||
.toConstantValue(
|
||||
new SharedVaultFileUploadedEventHandler(
|
||||
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
|
||||
container.get(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<FileRemovedEventHandler>(TYPES.Auth_FileRemovedEventHandler)
|
||||
.toConstantValue(
|
||||
new FileRemovedEventHandler(
|
||||
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
|
||||
container.get(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SharedVaultFileRemovedEventHandler>(TYPES.Auth_SharedVaultFileRemovedEventHandler)
|
||||
.toConstantValue(
|
||||
new SharedVaultFileRemovedEventHandler(
|
||||
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
|
||||
container.get(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<ListedAccountCreatedEventHandler>(TYPES.Auth_ListedAccountCreatedEventHandler)
|
||||
.to(ListedAccountCreatedEventHandler)
|
||||
@@ -999,7 +1041,9 @@ export class ContainerConfigLoader {
|
||||
['SUBSCRIPTION_REASSIGNED', container.get(TYPES.Auth_SubscriptionReassignedEventHandler)],
|
||||
['USER_EMAIL_CHANGED', container.get(TYPES.Auth_UserEmailChangedEventHandler)],
|
||||
['FILE_UPLOADED', container.get(TYPES.Auth_FileUploadedEventHandler)],
|
||||
['SHARED_VAULT_FILE_UPLOADED', container.get(TYPES.Auth_SharedVaultFileUploadedEventHandler)],
|
||||
['FILE_REMOVED', container.get(TYPES.Auth_FileRemovedEventHandler)],
|
||||
['SHARED_VAULT_FILE_REMOVED', container.get(TYPES.Auth_SharedVaultFileRemovedEventHandler)],
|
||||
['LISTED_ACCOUNT_CREATED', container.get(TYPES.Auth_ListedAccountCreatedEventHandler)],
|
||||
['LISTED_ACCOUNT_DELETED', container.get(TYPES.Auth_ListedAccountDeletedEventHandler)],
|
||||
[
|
||||
|
||||
@@ -152,6 +152,7 @@ const TYPES = {
|
||||
Auth_ActivatePremiumFeatures: Symbol.for('Auth_ActivatePremiumFeatures'),
|
||||
Auth_SignInWithRecoveryCodes: Symbol.for('Auth_SignInWithRecoveryCodes'),
|
||||
Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'),
|
||||
Auth_UpdateStorageQuotaUsedForUser: Symbol.for('Auth_UpdateStorageQuotaUsedForUser'),
|
||||
// Handlers
|
||||
Auth_UserRegisteredEventHandler: Symbol.for('Auth_UserRegisteredEventHandler'),
|
||||
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
|
||||
@@ -165,7 +166,9 @@ const TYPES = {
|
||||
Auth_ExtensionKeyGrantedEventHandler: Symbol.for('Auth_ExtensionKeyGrantedEventHandler'),
|
||||
Auth_UserEmailChangedEventHandler: Symbol.for('Auth_UserEmailChangedEventHandler'),
|
||||
Auth_FileUploadedEventHandler: Symbol.for('Auth_FileUploadedEventHandler'),
|
||||
Auth_SharedVaultFileUploadedEventHandler: Symbol.for('Auth_SharedVaultFileUploadedEventHandler'),
|
||||
Auth_FileRemovedEventHandler: Symbol.for('Auth_FileRemovedEventHandler'),
|
||||
Auth_SharedVaultFileRemovedEventHandler: Symbol.for('Auth_SharedVaultFileRemovedEventHandler'),
|
||||
Auth_ListedAccountCreatedEventHandler: Symbol.for('Auth_ListedAccountCreatedEventHandler'),
|
||||
Auth_ListedAccountDeletedEventHandler: Symbol.for('Auth_ListedAccountDeletedEventHandler'),
|
||||
Auth_UserDisabledSessionUserAgentLoggingEventHandler: Symbol.for(
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { FileRemovedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { User } from '../User/User'
|
||||
import { FileRemovedEventHandler } from './FileRemovedEventHandler'
|
||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
|
||||
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
|
||||
|
||||
describe('FileRemovedEventHandler', () => {
|
||||
let userSubscriptionService: UserSubscriptionServiceInterface
|
||||
let logger: Logger
|
||||
let regularUser: User
|
||||
let sharedUser: User
|
||||
let event: FileRemovedEvent
|
||||
let subscriptionSettingService: SubscriptionSettingServiceInterface
|
||||
let regularSubscription: UserSubscription
|
||||
let sharedSubscription: UserSubscription
|
||||
|
||||
const createHandler = () => new FileRemovedEventHandler(userSubscriptionService, subscriptionSettingService, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
regularUser = {
|
||||
uuid: '123',
|
||||
} as jest.Mocked<User>
|
||||
|
||||
sharedUser = {
|
||||
uuid: '234',
|
||||
} as jest.Mocked<User>
|
||||
|
||||
regularSubscription = {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: UserSubscriptionType.Regular,
|
||||
user: Promise.resolve(regularUser),
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
sharedSubscription = {
|
||||
uuid: '2-3-4',
|
||||
subscriptionType: UserSubscriptionType.Shared,
|
||||
user: Promise.resolve(sharedUser),
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue({ regularSubscription, sharedSubscription: null })
|
||||
|
||||
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
subscriptionSettingService.createOrReplace = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<FileRemovedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
userUuid: '1-2-3',
|
||||
fileByteSize: 123,
|
||||
filePath: '1-2-3/2-3-4',
|
||||
fileName: '2-3-4',
|
||||
regularSubscriptionUuid: '4-5-6',
|
||||
}
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should do nothing a bytes used setting does not exist', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not do anything if a user subscription is not found', async () => {
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
|
||||
value: 345,
|
||||
})
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should update a bytes used setting', async () => {
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
|
||||
value: 345,
|
||||
})
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
|
||||
props: {
|
||||
name: 'FILE_UPLOAD_BYTES_USED',
|
||||
sensitive: false,
|
||||
unencryptedValue: '222',
|
||||
serverEncryptionVersion: 0,
|
||||
},
|
||||
user: regularUser,
|
||||
userSubscription: {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: 'regular',
|
||||
user: Promise.resolve(regularUser),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should update a bytes used setting on both shared and regular subscription', async () => {
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
.fn()
|
||||
.mockReturnValue({ regularSubscription, sharedSubscription })
|
||||
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
|
||||
value: 345,
|
||||
})
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(subscriptionSettingService.createOrReplace).toHaveBeenNthCalledWith(1, {
|
||||
props: {
|
||||
name: 'FILE_UPLOAD_BYTES_USED',
|
||||
sensitive: false,
|
||||
unencryptedValue: '222',
|
||||
serverEncryptionVersion: 0,
|
||||
},
|
||||
user: regularUser,
|
||||
userSubscription: {
|
||||
uuid: '1-2-3',
|
||||
subscriptionType: 'regular',
|
||||
user: Promise.resolve(regularUser),
|
||||
},
|
||||
})
|
||||
|
||||
expect(subscriptionSettingService.createOrReplace).toHaveBeenNthCalledWith(2, {
|
||||
props: {
|
||||
name: 'FILE_UPLOAD_BYTES_USED',
|
||||
sensitive: false,
|
||||
unencryptedValue: '222',
|
||||
serverEncryptionVersion: 0,
|
||||
},
|
||||
user: sharedUser,
|
||||
userSubscription: {
|
||||
uuid: '2-3-4',
|
||||
subscriptionType: 'shared',
|
||||
user: Promise.resolve(sharedUser),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,63 +1,19 @@
|
||||
import { DomainEventHandlerInterface, FileRemovedEvent } from '@standardnotes/domain-events'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
|
||||
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
|
||||
|
||||
@injectable()
|
||||
export class FileRemovedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
|
||||
@inject(TYPES.Auth_SubscriptionSettingService)
|
||||
private subscriptionSettingService: SubscriptionSettingServiceInterface,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
) {}
|
||||
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
|
||||
|
||||
async handle(event: FileRemovedEvent): Promise<void> {
|
||||
const { regularSubscription, sharedSubscription } =
|
||||
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(event.payload.userUuid)
|
||||
if (regularSubscription === null) {
|
||||
this.logger.warn(`Could not find regular user subscription for user with uuid: ${event.payload.userUuid}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await this.updateUploadBytesUsedSetting(regularSubscription, event.payload.fileByteSize)
|
||||
|
||||
if (sharedSubscription !== null) {
|
||||
await this.updateUploadBytesUsedSetting(sharedSubscription, event.payload.fileByteSize)
|
||||
}
|
||||
}
|
||||
|
||||
private async updateUploadBytesUsedSetting(subscription: UserSubscription, byteSize: number): Promise<void> {
|
||||
const user = await subscription.user
|
||||
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
|
||||
userUuid: user.uuid,
|
||||
userSubscriptionUuid: subscription.uuid,
|
||||
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
|
||||
const result = await this.updateStorageQuotaUsedForUserUseCase.execute({
|
||||
userUuid: event.payload.userUuid,
|
||||
bytesUsed: -event.payload.fileByteSize,
|
||||
})
|
||||
if (bytesUsedSetting === null) {
|
||||
this.logger.warn(`Could not find bytes used setting for user with uuid: ${user.uuid}`)
|
||||
|
||||
return
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Failed to update storage quota used for user: ${result.getError()}`)
|
||||
}
|
||||
|
||||
const bytesUsed = bytesUsedSetting.value as string
|
||||
|
||||
await this.subscriptionSettingService.createOrReplace({
|
||||
userSubscription: subscription,
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.NAMES.FileUploadBytesUsed,
|
||||
unencryptedValue: (+bytesUsed - byteSize).toString(),
|
||||
sensitive: false,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +1,19 @@
|
||||
import { DomainEventHandlerInterface, FileUploadedEvent } from '@standardnotes/domain-events'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
|
||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { User } from '../User/User'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
|
||||
|
||||
@injectable()
|
||||
export class FileUploadedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
|
||||
@inject(TYPES.Auth_SubscriptionSettingService)
|
||||
private subscriptionSettingService: SubscriptionSettingServiceInterface,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
) {}
|
||||
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
|
||||
|
||||
async handle(event: FileUploadedEvent): Promise<void> {
|
||||
const userUuidOrError = Uuid.create(event.payload.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
this.logger.warn(userUuidOrError.getError())
|
||||
|
||||
return
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const user = await this.userRepository.findOneByUuid(userUuid)
|
||||
if (user === null) {
|
||||
this.logger.warn(`Could not find user with uuid: ${userUuid.value}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const { regularSubscription, sharedSubscription } =
|
||||
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(userUuid.value)
|
||||
if (regularSubscription === null) {
|
||||
this.logger.warn(`Could not find regular user subscription for user with uuid: ${userUuid.value}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await this.updateUploadBytesUsedSetting(regularSubscription, user, event.payload.fileByteSize)
|
||||
|
||||
if (sharedSubscription !== null) {
|
||||
await this.updateUploadBytesUsedSetting(sharedSubscription, user, event.payload.fileByteSize)
|
||||
}
|
||||
}
|
||||
|
||||
private async updateUploadBytesUsedSetting(
|
||||
subscription: UserSubscription,
|
||||
user: User,
|
||||
byteSize: number,
|
||||
): Promise<void> {
|
||||
let bytesUsed = '0'
|
||||
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
|
||||
userUuid: (await subscription.user).uuid,
|
||||
userSubscriptionUuid: subscription.uuid,
|
||||
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
|
||||
const result = await this.updateStorageQuotaUsedForUserUseCase.execute({
|
||||
userUuid: event.payload.userUuid,
|
||||
bytesUsed: event.payload.fileByteSize,
|
||||
})
|
||||
if (bytesUsedSetting !== null) {
|
||||
bytesUsed = bytesUsedSetting.value as string
|
||||
}
|
||||
|
||||
await this.subscriptionSettingService.createOrReplace({
|
||||
userSubscription: subscription,
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.NAMES.FileUploadBytesUsed,
|
||||
unencryptedValue: (+bytesUsed + byteSize).toString(),
|
||||
sensitive: false,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
},
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Failed to update storage quota used for user: ${result.getError()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { DomainEventHandlerInterface, SharedVaultFileRemovedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
|
||||
|
||||
export class SharedVaultFileRemovedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
|
||||
|
||||
async handle(event: SharedVaultFileRemovedEvent): Promise<void> {
|
||||
const result = await this.updateStorageQuotaUsedForUserUseCase.execute({
|
||||
userUuid: event.payload.vaultOwnerUuid,
|
||||
bytesUsed: -event.payload.fileByteSize,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Failed to update storage quota used for user: ${result.getError()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { DomainEventHandlerInterface, SharedVaultFileUploadedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
|
||||
|
||||
export class SharedVaultFileUploadedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
|
||||
|
||||
async handle(event: SharedVaultFileUploadedEvent): Promise<void> {
|
||||
const result = await this.updateStorageQuotaUsedForUserUseCase.execute({
|
||||
userUuid: event.payload.vaultOwnerUuid,
|
||||
bytesUsed: event.payload.fileByteSize,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Failed to update storage quota used for user: ${result.getError()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,22 @@
|
||||
import 'reflect-metadata'
|
||||
import { UpdateStorageQuotaUsedForUser } from './UpdateStorageQuotaUsedForUser'
|
||||
|
||||
import { FileUploadedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
|
||||
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
|
||||
import { User } from '../../User/User'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { FileUploadedEventHandler } from './FileUploadedEventHandler'
|
||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
|
||||
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
|
||||
|
||||
describe('FileUploadedEventHandler', () => {
|
||||
describe('UpdateStorageQuotaUsedForUser', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let userSubscriptionService: UserSubscriptionServiceInterface
|
||||
let logger: Logger
|
||||
let user: User
|
||||
let event: FileUploadedEvent
|
||||
let subscriptionSettingService: SubscriptionSettingServiceInterface
|
||||
let regularSubscription: UserSubscription
|
||||
let sharedSubscription: UserSubscription
|
||||
|
||||
const createHandler = () =>
|
||||
new FileUploadedEventHandler(userRepository, userSubscriptionService, subscriptionSettingService, logger)
|
||||
const createUseCase = () =>
|
||||
new UpdateStorageQuotaUsedForUser(userRepository, userSubscriptionService, subscriptionSettingService)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
@@ -52,23 +46,15 @@ describe('FileUploadedEventHandler', () => {
|
||||
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
subscriptionSettingService.createOrReplace = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<FileUploadedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
fileByteSize: 123,
|
||||
filePath: '00000000-0000-0000-0000-000000000000/2-3-4',
|
||||
fileName: '2-3-4',
|
||||
}
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should create a bytes used setting if one does not exist', async () => {
|
||||
await createHandler().handle(event)
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
bytesUsed: 123,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
|
||||
props: {
|
||||
name: 'FILE_UPLOAD_BYTES_USED',
|
||||
@@ -86,9 +72,11 @@ describe('FileUploadedEventHandler', () => {
|
||||
})
|
||||
|
||||
it('should not do anything if a user uuid is invalid', async () => {
|
||||
event.payload.userUuid = 'invalid'
|
||||
|
||||
await createHandler().handle(event)
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: 'invalid',
|
||||
bytesUsed: 123,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -96,7 +84,11 @@ describe('FileUploadedEventHandler', () => {
|
||||
it('should not do anything if a user is not found', async () => {
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
bytesUsed: 123,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -109,16 +101,24 @@ describe('FileUploadedEventHandler', () => {
|
||||
.fn()
|
||||
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
|
||||
|
||||
await createHandler().handle(event)
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
bytesUsed: 123,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
|
||||
expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should update a bytes used setting if one does exist', async () => {
|
||||
it('should add bytes used setting if one does exist', async () => {
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
|
||||
value: 345,
|
||||
})
|
||||
await createHandler().handle(event)
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
bytesUsed: 123,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
|
||||
props: {
|
||||
@@ -136,6 +136,32 @@ describe('FileUploadedEventHandler', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should subtract bytes used setting if one does exist', async () => {
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
|
||||
value: 345,
|
||||
})
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
bytesUsed: -123,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
|
||||
props: {
|
||||
name: 'FILE_UPLOAD_BYTES_USED',
|
||||
sensitive: false,
|
||||
unencryptedValue: '222',
|
||||
serverEncryptionVersion: 0,
|
||||
},
|
||||
user,
|
||||
userSubscription: {
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
subscriptionType: 'regular',
|
||||
user: Promise.resolve(user),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should update a bytes used setting on both regular and shared subscription', async () => {
|
||||
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
|
||||
.fn()
|
||||
@@ -144,7 +170,11 @@ describe('FileUploadedEventHandler', () => {
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
|
||||
value: 345,
|
||||
})
|
||||
await createHandler().handle(event)
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
bytesUsed: 123,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
|
||||
props: {
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
|
||||
import { EncryptionVersion } from '../../Encryption/EncryptionVersion'
|
||||
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { UpdateStorageQuotaUsedForUserDTO } from './UpdateStorageQuotaUsedForUserDTO'
|
||||
import { User } from '../../User/User'
|
||||
|
||||
export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private userSubscriptionService: UserSubscriptionServiceInterface,
|
||||
private subscriptionSettingService: SubscriptionSettingServiceInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: UpdateStorageQuotaUsedForUserDTO): Promise<Result<void>> {
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const user = await this.userRepository.findOneByUuid(userUuid)
|
||||
if (user === null) {
|
||||
return Result.fail(`Could not find user with uuid: ${userUuid.value}`)
|
||||
}
|
||||
|
||||
const { regularSubscription, sharedSubscription } =
|
||||
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(userUuid.value)
|
||||
if (regularSubscription === null) {
|
||||
return Result.fail(`Could not find regular user subscription for user with uuid: ${userUuid.value}`)
|
||||
}
|
||||
|
||||
await this.updateUploadBytesUsedSetting(regularSubscription, user, dto.bytesUsed)
|
||||
|
||||
if (sharedSubscription !== null) {
|
||||
await this.updateUploadBytesUsedSetting(sharedSubscription, user, dto.bytesUsed)
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async updateUploadBytesUsedSetting(
|
||||
subscription: UserSubscription,
|
||||
user: User,
|
||||
bytesUsed: number,
|
||||
): Promise<void> {
|
||||
let bytesAlreadyUsed = '0'
|
||||
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
|
||||
userUuid: (await subscription.user).uuid,
|
||||
userSubscriptionUuid: subscription.uuid,
|
||||
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
|
||||
})
|
||||
if (bytesUsedSetting !== null) {
|
||||
bytesAlreadyUsed = bytesUsedSetting.value as string
|
||||
}
|
||||
|
||||
await this.subscriptionSettingService.createOrReplace({
|
||||
userSubscription: subscription,
|
||||
user,
|
||||
props: {
|
||||
name: SettingName.NAMES.FileUploadBytesUsed,
|
||||
unencryptedValue: (+bytesAlreadyUsed + bytesUsed).toString(),
|
||||
sensitive: false,
|
||||
serverEncryptionVersion: EncryptionVersion.Unencrypted,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface UpdateStorageQuotaUsedForUserDTO {
|
||||
userUuid: string
|
||||
bytesUsed: number
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export interface SharedVaultFileRemovedEventPayload {
|
||||
sharedVaultUuid: string
|
||||
vaultOwnerUuid: string
|
||||
fileByteSize: number
|
||||
filePath: string
|
||||
fileName: string
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export interface SharedVaultFileUploadedEventPayload {
|
||||
sharedVaultUuid: string
|
||||
vaultOwnerUuid: string
|
||||
fileByteSize: number
|
||||
filePath: string
|
||||
fileName: string
|
||||
|
||||
@@ -7,6 +7,6 @@ module.exports = {
|
||||
transform: {
|
||||
...tsjPreset.transform,
|
||||
},
|
||||
coveragePathIgnorePatterns: ['/Bootstrap/', 'HealthCheckController', '/Infra/FS'],
|
||||
coveragePathIgnorePatterns: ['/Bootstrap/', 'HealthCheckController', '/Infra/FS', '/Domain/Event/'],
|
||||
setupFilesAfterEnv: ['./test-setup.ts'],
|
||||
}
|
||||
|
||||
@@ -72,6 +72,16 @@ export class ContainerConfigLoader {
|
||||
await import('newrelic')
|
||||
}
|
||||
|
||||
// env vars
|
||||
container.bind(TYPES.Files_VALET_TOKEN_SECRET).toConstantValue(env.get('VALET_TOKEN_SECRET'))
|
||||
container
|
||||
.bind(TYPES.Files_MAX_CHUNK_BYTES)
|
||||
.toConstantValue(env.get('MAX_CHUNK_BYTES', true) ? +env.get('MAX_CHUNK_BYTES', true) : 100000000)
|
||||
container.bind(TYPES.Files_VERSION).toConstantValue(env.get('VERSION', true) ?? 'development')
|
||||
container
|
||||
.bind(TYPES.Files_FILE_UPLOAD_PATH)
|
||||
.toConstantValue(env.get('FILE_UPLOAD_PATH', true) ?? `${__dirname}/../../uploads`)
|
||||
|
||||
const isConfiguredForHomeServer = env.get('MODE', true) === 'home-server'
|
||||
const isConfiguredForInMemoryCache = env.get('CACHE_TYPE', true) === 'memory'
|
||||
|
||||
@@ -85,6 +95,12 @@ export class ContainerConfigLoader {
|
||||
|
||||
container.bind<TimerInterface>(TYPES.Files_Timer).toConstantValue(new Timer())
|
||||
|
||||
// services
|
||||
container
|
||||
.bind<TokenDecoderInterface<ValetTokenData>>(TYPES.Files_ValetTokenDecoder)
|
||||
.toConstantValue(new TokenDecoder<ValetTokenData>(container.get(TYPES.Files_VALET_TOKEN_SECRET)))
|
||||
container.bind<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory).to(DomainEventFactory)
|
||||
|
||||
if (isConfiguredForInMemoryCache) {
|
||||
container
|
||||
.bind<UploadRepositoryInterface>(TYPES.Files_UploadRepository)
|
||||
@@ -157,16 +173,6 @@ export class ContainerConfigLoader {
|
||||
)
|
||||
}
|
||||
|
||||
// env vars
|
||||
container.bind(TYPES.Files_VALET_TOKEN_SECRET).toConstantValue(env.get('VALET_TOKEN_SECRET'))
|
||||
container
|
||||
.bind(TYPES.Files_MAX_CHUNK_BYTES)
|
||||
.toConstantValue(env.get('MAX_CHUNK_BYTES', true) ? +env.get('MAX_CHUNK_BYTES', true) : 100000000)
|
||||
container.bind(TYPES.Files_VERSION).toConstantValue(env.get('VERSION', true) ?? 'development')
|
||||
container
|
||||
.bind(TYPES.Files_FILE_UPLOAD_PATH)
|
||||
.toConstantValue(env.get('FILE_UPLOAD_PATH', true) ?? `${__dirname}/../../uploads`)
|
||||
|
||||
if (!isConfiguredForHomeServer && (env.get('S3_AWS_REGION', true) || env.get('S3_ENDPOINT', true))) {
|
||||
const s3Opts: S3ClientConfig = {
|
||||
apiVersion: 'latest',
|
||||
@@ -198,7 +204,16 @@ export class ContainerConfigLoader {
|
||||
container.bind<UploadFileChunk>(TYPES.Files_UploadFileChunk).to(UploadFileChunk)
|
||||
container.bind<StreamDownloadFile>(TYPES.Files_StreamDownloadFile).to(StreamDownloadFile)
|
||||
container.bind<CreateUploadSession>(TYPES.Files_CreateUploadSession).to(CreateUploadSession)
|
||||
container.bind<FinishUploadSession>(TYPES.Files_FinishUploadSession).to(FinishUploadSession)
|
||||
container
|
||||
.bind<FinishUploadSession>(TYPES.Files_FinishUploadSession)
|
||||
.toConstantValue(
|
||||
new FinishUploadSession(
|
||||
container.get(TYPES.Files_FileUploader),
|
||||
container.get(TYPES.Files_UploadRepository),
|
||||
container.get(TYPES.Files_DomainEventPublisher),
|
||||
container.get(TYPES.Files_DomainEventFactory),
|
||||
),
|
||||
)
|
||||
container.bind<GetFileMetadata>(TYPES.Files_GetFileMetadata).to(GetFileMetadata)
|
||||
container.bind<RemoveFile>(TYPES.Files_RemoveFile).to(RemoveFile)
|
||||
container.bind<MoveFile>(TYPES.Files_MoveFile).to(MoveFile)
|
||||
@@ -210,12 +225,6 @@ export class ContainerConfigLoader {
|
||||
.bind<SharedVaultValetTokenAuthMiddleware>(TYPES.Files_SharedVaultValetTokenAuthMiddleware)
|
||||
.to(SharedVaultValetTokenAuthMiddleware)
|
||||
|
||||
// services
|
||||
container
|
||||
.bind<TokenDecoderInterface<ValetTokenData>>(TYPES.Files_ValetTokenDecoder)
|
||||
.toConstantValue(new TokenDecoder<ValetTokenData>(container.get(TYPES.Files_VALET_TOKEN_SECRET)))
|
||||
container.bind<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory).to(DomainEventFactory)
|
||||
|
||||
// Handlers
|
||||
container
|
||||
.bind<AccountDeletionRequestedEventHandler>(TYPES.Files_AccountDeletionRequestedEventHandler)
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { DomainEventFactory } from './DomainEventFactory'
|
||||
|
||||
describe('DomainEventFactory', () => {
|
||||
let timer: TimerInterface
|
||||
|
||||
const createFactory = () => new DomainEventFactory(timer)
|
||||
|
||||
beforeEach(() => {
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
|
||||
})
|
||||
|
||||
it('should create a SHARED_VAULT_FILE_UPLOADED event', () => {
|
||||
expect(
|
||||
createFactory().createSharedVaultFileUploadedEvent({
|
||||
sharedVaultUuid: '1-2-3',
|
||||
filePath: 'foo/bar',
|
||||
fileName: 'baz',
|
||||
fileByteSize: 123,
|
||||
}),
|
||||
).toEqual({
|
||||
createdAt: new Date(1),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '1-2-3',
|
||||
userIdentifierType: 'shared-vault-uuid',
|
||||
},
|
||||
origin: 'files',
|
||||
},
|
||||
payload: {
|
||||
sharedVaultUuid: '1-2-3',
|
||||
filePath: 'foo/bar',
|
||||
fileName: 'baz',
|
||||
fileByteSize: 123,
|
||||
},
|
||||
type: 'SHARED_VAULT_FILE_UPLOADED',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a SHARED_VAULT_FILE_REMOVED event', () => {
|
||||
expect(
|
||||
createFactory().createSharedVaultFileRemovedEvent({
|
||||
sharedVaultUuid: '1-2-3',
|
||||
filePath: 'foo/bar',
|
||||
fileName: 'baz',
|
||||
fileByteSize: 123,
|
||||
}),
|
||||
).toEqual({
|
||||
createdAt: new Date(1),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '1-2-3',
|
||||
userIdentifierType: 'shared-vault-uuid',
|
||||
},
|
||||
origin: 'files',
|
||||
},
|
||||
payload: {
|
||||
sharedVaultUuid: '1-2-3',
|
||||
filePath: 'foo/bar',
|
||||
fileName: 'baz',
|
||||
fileByteSize: 123,
|
||||
},
|
||||
type: 'SHARED_VAULT_FILE_REMOVED',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a FILE_UPLOADED event', () => {
|
||||
expect(
|
||||
createFactory().createFileUploadedEvent({
|
||||
fileByteSize: 123,
|
||||
fileName: '2-3-4',
|
||||
filePath: '1-2-3/2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
}),
|
||||
).toEqual({
|
||||
createdAt: new Date(1),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '1-2-3',
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: 'files',
|
||||
},
|
||||
payload: {
|
||||
fileByteSize: 123,
|
||||
fileName: '2-3-4',
|
||||
filePath: '1-2-3/2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
},
|
||||
type: 'FILE_UPLOADED',
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a FILE_REMOVED event', () => {
|
||||
expect(
|
||||
createFactory().createFileRemovedEvent({
|
||||
fileByteSize: 123,
|
||||
fileName: '2-3-4',
|
||||
filePath: '1-2-3/2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
regularSubscriptionUuid: '1-2-3',
|
||||
}),
|
||||
).toEqual({
|
||||
createdAt: new Date(1),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '1-2-3',
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: 'files',
|
||||
},
|
||||
payload: {
|
||||
fileByteSize: 123,
|
||||
fileName: '2-3-4',
|
||||
filePath: '1-2-3/2-3-4',
|
||||
userUuid: '1-2-3',
|
||||
regularSubscriptionUuid: '1-2-3',
|
||||
},
|
||||
type: 'FILE_REMOVED',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -58,6 +58,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
|
||||
createSharedVaultFileUploadedEvent(payload: {
|
||||
sharedVaultUuid: string
|
||||
vaultOwnerUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
@@ -78,6 +79,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
|
||||
createSharedVaultFileRemovedEvent(payload: {
|
||||
sharedVaultUuid: string
|
||||
vaultOwnerUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
|
||||
@@ -21,12 +21,14 @@ export interface DomainEventFactoryInterface {
|
||||
}): FileRemovedEvent
|
||||
createSharedVaultFileUploadedEvent(payload: {
|
||||
sharedVaultUuid: string
|
||||
vaultOwnerUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
}): SharedVaultFileUploadedEvent
|
||||
createSharedVaultFileRemovedEvent(payload: {
|
||||
sharedVaultUuid: string
|
||||
vaultOwnerUuid: string
|
||||
filePath: string
|
||||
fileName: string
|
||||
fileByteSize: number
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import {
|
||||
DomainEventPublisherInterface,
|
||||
FileUploadedEvent,
|
||||
SharedVaultFileUploadedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
import { FileUploaderInterface } from '../../Services/FileUploaderInterface'
|
||||
import { UploadRepositoryInterface } from '../../Upload/UploadRepositoryInterface'
|
||||
@@ -17,10 +15,9 @@ describe('FinishUploadSession', () => {
|
||||
let uploadRepository: UploadRepositoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () =>
|
||||
new FinishUploadSession(fileUploader, uploadRepository, domainEventPublisher, domainEventFactory, logger)
|
||||
new FinishUploadSession(fileUploader, uploadRepository, domainEventPublisher, domainEventFactory)
|
||||
|
||||
beforeEach(() => {
|
||||
fileUploader = {} as jest.Mocked<FileUploaderInterface>
|
||||
@@ -38,11 +35,6 @@ describe('FinishUploadSession', () => {
|
||||
domainEventFactory.createSharedVaultFileUploadedEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<SharedVaultFileUploadedEvent>)
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
logger.error = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should not finish an upload session if non existing', async () => {
|
||||
@@ -50,8 +42,7 @@ describe('FinishUploadSession', () => {
|
||||
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
ownerUuid: '1-2-3',
|
||||
ownerType: 'user',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
uploadBytesLimit: 100,
|
||||
uploadBytesUsed: 0,
|
||||
})
|
||||
@@ -60,24 +51,33 @@ describe('FinishUploadSession', () => {
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not finish an upload session user uuid is invalid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: 'invalid',
|
||||
uploadBytesLimit: 100,
|
||||
uploadBytesUsed: 0,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(fileUploader.finishUploadSession).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should indicate of an error in finishing session fails', async () => {
|
||||
uploadRepository.retrieveUploadSessionId = jest.fn().mockImplementation(() => {
|
||||
throw new Error('oops')
|
||||
})
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
ownerUuid: '1-2-3',
|
||||
ownerType: 'user',
|
||||
uploadBytesLimit: 100,
|
||||
uploadBytesUsed: 0,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
message: 'Could not finish upload session',
|
||||
const result = await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
uploadBytesLimit: 100,
|
||||
uploadBytesUsed: 0,
|
||||
})
|
||||
|
||||
expect(result.getError()).toEqual('Could not finish upload session')
|
||||
|
||||
expect(fileUploader.finishUploadSession).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -85,13 +85,12 @@ describe('FinishUploadSession', () => {
|
||||
it('should finish an upload session', async () => {
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
ownerUuid: '1-2-3',
|
||||
ownerType: 'user',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
uploadBytesLimit: 100,
|
||||
uploadBytesUsed: 0,
|
||||
})
|
||||
|
||||
expect(fileUploader.finishUploadSession).toHaveBeenCalledWith('123', '1-2-3/2-3-4', [
|
||||
expect(fileUploader.finishUploadSession).toHaveBeenCalledWith('123', '00000000-0000-0000-0000-000000000000/2-3-4', [
|
||||
{ tag: '123', chunkId: 1, chunkSize: 1 },
|
||||
])
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
@@ -100,18 +99,32 @@ describe('FinishUploadSession', () => {
|
||||
it('should finish an upload session for a vault shared file', async () => {
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
ownerUuid: '1-2-3',
|
||||
ownerType: 'shared-vault',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
|
||||
uploadBytesLimit: 100,
|
||||
uploadBytesUsed: 0,
|
||||
})
|
||||
|
||||
expect(fileUploader.finishUploadSession).toHaveBeenCalledWith('123', '1-2-3/2-3-4', [
|
||||
expect(fileUploader.finishUploadSession).toHaveBeenCalledWith('123', '00000000-0000-0000-0000-000000000000/2-3-4', [
|
||||
{ tag: '123', chunkId: 1, chunkSize: 1 },
|
||||
])
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not finish an upload session for a vault shared file if shared vault uuid is invalid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
sharedVaultUuid: 'invalid',
|
||||
uploadBytesLimit: 100,
|
||||
uploadBytesUsed: 0,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
expect(fileUploader.finishUploadSession).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not finish an upload session if the file size exceeds storage quota', async () => {
|
||||
uploadRepository.retrieveUploadChunkResults = jest.fn().mockReturnValue([
|
||||
{ tag: '123', chunkId: 1, chunkSize: 60 },
|
||||
@@ -119,18 +132,13 @@ describe('FinishUploadSession', () => {
|
||||
{ tag: '345', chunkId: 3, chunkSize: 20 },
|
||||
])
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
ownerUuid: '1-2-3',
|
||||
ownerType: 'user',
|
||||
uploadBytesLimit: 100,
|
||||
uploadBytesUsed: 20,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
message: 'Could not finish upload session. You are out of space.',
|
||||
const result = await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
uploadBytesLimit: 100,
|
||||
uploadBytesUsed: 20,
|
||||
})
|
||||
expect(result.getError()).toEqual('Could not finish upload session. You are out of space.')
|
||||
|
||||
expect(fileUploader.finishUploadSession).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
@@ -143,17 +151,13 @@ describe('FinishUploadSession', () => {
|
||||
{ tag: '345', chunkId: 3, chunkSize: 20 },
|
||||
])
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
ownerUuid: '1-2-3',
|
||||
ownerType: 'user',
|
||||
uploadBytesLimit: -1,
|
||||
uploadBytesUsed: 20,
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
uploadBytesLimit: -1,
|
||||
uploadBytesUsed: 20,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(fileUploader.finishUploadSession).toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
|
||||
@@ -1,39 +1,41 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { FinishUploadSessionDTO } from './FinishUploadSessionDTO'
|
||||
import { FinishUploadSessionResponse } from './FinishUploadSessionResponse'
|
||||
import { FileUploaderInterface } from '../../Services/FileUploaderInterface'
|
||||
import { UploadRepositoryInterface } from '../../Upload/UploadRepositoryInterface'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class FinishUploadSession implements UseCaseInterface {
|
||||
export class FinishUploadSession implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
@inject(TYPES.Files_FileUploader) private fileUploader: FileUploaderInterface,
|
||||
@inject(TYPES.Files_UploadRepository) private uploadRepository: UploadRepositoryInterface,
|
||||
@inject(TYPES.Files_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@inject(TYPES.Files_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
@inject(TYPES.Files_Logger) private logger: Logger,
|
||||
private fileUploader: FileUploaderInterface,
|
||||
private uploadRepository: UploadRepositoryInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: FinishUploadSessionDTO): Promise<FinishUploadSessionResponse> {
|
||||
async execute(dto: FinishUploadSessionDTO): Promise<Result<void>> {
|
||||
try {
|
||||
this.logger.debug(`Finishing upload session for resource: ${dto.resourceRemoteIdentifier}`)
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const filePath = `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`
|
||||
let sharedVaultUuid: Uuid | undefined
|
||||
if (dto.sharedVaultUuid !== undefined) {
|
||||
const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
|
||||
if (sharedVaultUuidOrError.isFailed()) {
|
||||
return Result.fail(sharedVaultUuidOrError.getError())
|
||||
}
|
||||
sharedVaultUuid = sharedVaultUuidOrError.getValue()
|
||||
}
|
||||
|
||||
const filePath = `${sharedVaultUuid ? sharedVaultUuid.value : userUuid.value}/${dto.resourceRemoteIdentifier}`
|
||||
|
||||
const uploadId = await this.uploadRepository.retrieveUploadSessionId(filePath)
|
||||
if (uploadId === undefined) {
|
||||
this.logger.warn(`Could not find upload session for file path: ${filePath}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not finish upload session',
|
||||
}
|
||||
return Result.fail('Could not finish upload session')
|
||||
}
|
||||
|
||||
const uploadChunkResults = await this.uploadRepository.retrieveUploadChunkResults(uploadId)
|
||||
@@ -46,46 +48,35 @@ export class FinishUploadSession implements UseCaseInterface {
|
||||
const userHasUnlimitedStorage = dto.uploadBytesLimit === -1
|
||||
const remainingSpaceLeft = dto.uploadBytesLimit - dto.uploadBytesUsed
|
||||
if (!userHasUnlimitedStorage && remainingSpaceLeft < totalFileSize) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not finish upload session. You are out of space.',
|
||||
}
|
||||
return Result.fail('Could not finish upload session. You are out of space.')
|
||||
}
|
||||
|
||||
await this.fileUploader.finishUploadSession(uploadId, filePath, uploadChunkResults)
|
||||
|
||||
if (dto.ownerType === 'user') {
|
||||
if (sharedVaultUuid !== undefined) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createFileUploadedEvent({
|
||||
userUuid: dto.ownerUuid,
|
||||
filePath: `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`,
|
||||
this.domainEventFactory.createSharedVaultFileUploadedEvent({
|
||||
sharedVaultUuid: sharedVaultUuid.value,
|
||||
vaultOwnerUuid: userUuid.value,
|
||||
filePath,
|
||||
fileName: dto.resourceRemoteIdentifier,
|
||||
fileByteSize: totalFileSize,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createSharedVaultFileUploadedEvent({
|
||||
sharedVaultUuid: dto.ownerUuid,
|
||||
filePath: `${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`,
|
||||
this.domainEventFactory.createFileUploadedEvent({
|
||||
userUuid: userUuid.value,
|
||||
filePath,
|
||||
fileName: dto.resourceRemoteIdentifier,
|
||||
fileByteSize: totalFileSize,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
return Result.ok()
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Could not finish upload session for resource: ${dto.resourceRemoteIdentifier} - ${(error as Error).message}`,
|
||||
)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not finish upload session',
|
||||
}
|
||||
return Result.fail('Could not finish upload session')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type FinishUploadSessionDTO = {
|
||||
ownerUuid: string
|
||||
ownerType: 'user' | 'shared-vault'
|
||||
userUuid: string
|
||||
sharedVaultUuid?: string
|
||||
resourceRemoteIdentifier: string
|
||||
uploadBytesUsed: number
|
||||
uploadBytesLimit: number
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export type FinishUploadSessionResponse =
|
||||
| {
|
||||
success: true
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
message: string
|
||||
}
|
||||
@@ -80,6 +80,7 @@ describe('RemoveFile', () => {
|
||||
vaultInput: {
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
sharedVaultUuid: '1-2-3',
|
||||
vaultOwnerUuid: '3-4-5',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export class RemoveFile implements UseCaseInterface<boolean> {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createSharedVaultFileRemovedEvent({
|
||||
sharedVaultUuid: dto.vaultInput.sharedVaultUuid,
|
||||
vaultOwnerUuid: dto.vaultInput.vaultOwnerUuid,
|
||||
filePath: `${dto.vaultInput.sharedVaultUuid}/${dto.vaultInput.resourceRemoteIdentifier}`,
|
||||
fileName: dto.vaultInput.resourceRemoteIdentifier,
|
||||
fileByteSize: removedFileSize,
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface RemoveFileDTO {
|
||||
}
|
||||
vaultInput?: {
|
||||
sharedVaultUuid: string
|
||||
vaultOwnerUuid: string
|
||||
resourceRemoteIdentifier: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { ValetTokenOperation } from '@standardnotes/security'
|
||||
import { BadRequestErrorMessageResult } from 'inversify-express-utils/lib/results'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
import { Logger } from 'winston'
|
||||
import { Request, Response } from 'express'
|
||||
import { Writable, Readable } from 'stream'
|
||||
import { results } from 'inversify-express-utils'
|
||||
|
||||
import { CreateUploadSession } from '../../Domain/UseCase/CreateUploadSession/CreateUploadSession'
|
||||
import { FinishUploadSession } from '../../Domain/UseCase/FinishUploadSession/FinishUploadSession'
|
||||
import { StreamDownloadFile } from '../../Domain/UseCase/StreamDownloadFile/StreamDownloadFile'
|
||||
import { UploadFileChunk } from '../../Domain/UseCase/UploadFileChunk/UploadFileChunk'
|
||||
|
||||
import { Request, Response } from 'express'
|
||||
import { Writable, Readable } from 'stream'
|
||||
import { AnnotatedFilesController } from './AnnotatedFilesController'
|
||||
import { GetFileMetadata } from '../../Domain/UseCase/GetFileMetadata/GetFileMetadata'
|
||||
import { results } from 'inversify-express-utils'
|
||||
import { RemoveFile } from '../../Domain/UseCase/RemoveFile/RemoveFile'
|
||||
import { ValetTokenOperation } from '@standardnotes/security'
|
||||
import { BadRequestErrorMessageResult } from 'inversify-express-utils/lib/results'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('AnnotatedFilesController', () => {
|
||||
let uploadFileChunk: UploadFileChunk
|
||||
@@ -26,6 +27,7 @@ describe('AnnotatedFilesController', () => {
|
||||
let response: Response
|
||||
let readStream: Readable
|
||||
const maxChunkBytes = 100_000
|
||||
let logger: Logger
|
||||
|
||||
const createController = () =>
|
||||
new AnnotatedFilesController(
|
||||
@@ -36,9 +38,13 @@ describe('AnnotatedFilesController', () => {
|
||||
getFileMetadata,
|
||||
removeFile,
|
||||
maxChunkBytes,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
|
||||
readStream = {} as jest.Mocked<Readable>
|
||||
readStream.pipe = jest.fn().mockReturnValue(new Writable())
|
||||
|
||||
@@ -52,7 +58,7 @@ describe('AnnotatedFilesController', () => {
|
||||
createUploadSession.execute = jest.fn().mockReturnValue({ success: true, uploadId: '123' })
|
||||
|
||||
finishUploadSession = {} as jest.Mocked<FinishUploadSession>
|
||||
finishUploadSession.execute = jest.fn().mockReturnValue({ success: true })
|
||||
finishUploadSession.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
getFileMetadata = {} as jest.Mocked<GetFileMetadata>
|
||||
getFileMetadata.execute = jest.fn().mockReturnValue({ success: true, size: 555_555 })
|
||||
@@ -233,8 +239,7 @@ describe('AnnotatedFilesController', () => {
|
||||
|
||||
expect(finishUploadSession.execute).toHaveBeenCalledWith({
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
ownerType: 'user',
|
||||
ownerUuid: '1-2-3',
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -249,7 +254,7 @@ describe('AnnotatedFilesController', () => {
|
||||
it('should return bad request if upload session could not be finished', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Write
|
||||
|
||||
finishUploadSession.execute = jest.fn().mockReturnValue({ success: false })
|
||||
finishUploadSession.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const httpResponse = await createController().finishUpload(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
@@ -2,6 +2,9 @@ import { BaseHttpController, controller, httpDelete, httpGet, httpPost, results
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { Writable } from 'stream'
|
||||
import { ValetTokenOperation } from '@standardnotes/security'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { UploadFileChunk } from '../../Domain/UseCase/UploadFileChunk/UploadFileChunk'
|
||||
import { StreamDownloadFile } from '../../Domain/UseCase/StreamDownloadFile/StreamDownloadFile'
|
||||
@@ -9,7 +12,6 @@ import { CreateUploadSession } from '../../Domain/UseCase/CreateUploadSession/Cr
|
||||
import { FinishUploadSession } from '../../Domain/UseCase/FinishUploadSession/FinishUploadSession'
|
||||
import { GetFileMetadata } from '../../Domain/UseCase/GetFileMetadata/GetFileMetadata'
|
||||
import { RemoveFile } from '../../Domain/UseCase/RemoveFile/RemoveFile'
|
||||
import { ValetTokenOperation } from '@standardnotes/security'
|
||||
|
||||
@controller('/v1/files', TYPES.Files_ValetTokenAuthMiddleware)
|
||||
export class AnnotatedFilesController extends BaseHttpController {
|
||||
@@ -21,6 +23,7 @@ export class AnnotatedFilesController extends BaseHttpController {
|
||||
@inject(TYPES.Files_GetFileMetadata) private getFileMetadata: GetFileMetadata,
|
||||
@inject(TYPES.Files_RemoveFile) private removeFile: RemoveFile,
|
||||
@inject(TYPES.Files_MAX_CHUNK_BYTES) private maxChunkBytes: number,
|
||||
@inject(TYPES.Files_Logger) private logger: Logger,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -85,15 +88,16 @@ export class AnnotatedFilesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
const result = await this.finishUploadSession.execute({
|
||||
ownerUuid: response.locals.userUuid,
|
||||
ownerType: 'user',
|
||||
userUuid: response.locals.userUuid,
|
||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||
uploadBytesLimit: response.locals.uploadBytesLimit,
|
||||
uploadBytesUsed: response.locals.uploadBytesUsed,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return this.badRequest(result.message)
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(result.getError())
|
||||
|
||||
return this.badRequest(result.getError())
|
||||
}
|
||||
|
||||
return this.json({ success: true, message: 'File uploaded successfully' })
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { Writable } from 'stream'
|
||||
import { SharedVaultValetTokenData, ValetTokenOperation } from '@standardnotes/security'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { CreateUploadSession } from '../../Domain/UseCase/CreateUploadSession/CreateUploadSession'
|
||||
@@ -24,6 +25,7 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController {
|
||||
@inject(TYPES.Files_RemoveFile) private removeFile: RemoveFile,
|
||||
@inject(TYPES.Files_MoveFile) private moveFile: MoveFile,
|
||||
@inject(TYPES.Files_MAX_CHUNK_BYTES) private maxChunkBytes: number,
|
||||
@inject(TYPES.Files_Logger) private logger: Logger,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -120,15 +122,17 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
const result = await this.finishUploadSession.execute({
|
||||
ownerUuid: locals.sharedVaultUuid,
|
||||
ownerType: 'shared-vault',
|
||||
userUuid: locals.vaultOwnerUuid,
|
||||
sharedVaultUuid: locals.sharedVaultUuid,
|
||||
resourceRemoteIdentifier: locals.remoteIdentifier,
|
||||
uploadBytesLimit: locals.uploadBytesLimit,
|
||||
uploadBytesUsed: locals.uploadBytesUsed,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return this.badRequest(result.message)
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(result.getError())
|
||||
|
||||
return this.badRequest(result.getError())
|
||||
}
|
||||
|
||||
return this.json({ success: true, message: 'File uploaded successfully' })
|
||||
@@ -147,6 +151,7 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController {
|
||||
const result = await this.removeFile.execute({
|
||||
vaultInput: {
|
||||
sharedVaultUuid: locals.sharedVaultUuid,
|
||||
vaultOwnerUuid: locals.vaultOwnerUuid,
|
||||
resourceRemoteIdentifier: locals.remoteIdentifier,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -63,6 +63,7 @@ export class SharedVaultValetTokenAuthMiddleware extends BaseMiddleware {
|
||||
|
||||
const whitelistedData: SharedVaultValetTokenData = {
|
||||
sharedVaultUuid: valetTokenData.sharedVaultUuid,
|
||||
vaultOwnerUuid: valetTokenData.vaultOwnerUuid,
|
||||
remoteIdentifier: valetTokenData.remoteIdentifier,
|
||||
permittedOperation: valetTokenData.permittedOperation,
|
||||
uploadBytesUsed: valetTokenData.uploadBytesUsed,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SharedVaultMoveType } from './SharedVaultMoveType'
|
||||
|
||||
export interface SharedVaultValetTokenData {
|
||||
sharedVaultUuid: string
|
||||
vaultOwnerUuid: string
|
||||
permittedOperation: ValetTokenOperation
|
||||
remoteIdentifier: string
|
||||
unencryptedFileSize?: number
|
||||
|
||||
@@ -15,7 +15,7 @@ import { AddUserToSharedVault } from '../AddUserToSharedVault/AddUserToSharedVau
|
||||
import { SharedVault } from '../../../SharedVault/SharedVault'
|
||||
|
||||
export class CreateSharedVault implements UseCaseInterface<CreateSharedVaultResult> {
|
||||
private readonly FILE_UPLOAD_BYTES_LIMIT = 1_000_000
|
||||
private readonly FILE_UPLOAD_BYTES_LIMIT = 1_000_000_000
|
||||
|
||||
constructor(
|
||||
private addUserToSharedVault: AddUserToSharedVault,
|
||||
|
||||
@@ -85,6 +85,7 @@ export class CreateSharedVaultFileValetToken implements UseCaseInterface<string>
|
||||
|
||||
const tokenData: SharedVaultValetTokenData = {
|
||||
sharedVaultUuid: dto.sharedVaultUuid,
|
||||
vaultOwnerUuid: sharedVault.props.userUuid.value,
|
||||
permittedOperation: dto.operation,
|
||||
remoteIdentifier: dto.remoteIdentifier,
|
||||
uploadBytesUsed: sharedVault.props.fileUploadBytesUsed,
|
||||
|
||||
Reference in New Issue
Block a user