Browse Source

feat: add handling file moving and updating storage quota (#705)

* feat: add handling file moving and updating storage quota

* fix: getting file metada when moving files

* fix: missing event handler binding
Karol Sójko 1 year ago
parent
commit
205a1ed637
30 changed files with 624 additions and 111 deletions
  1. 10 0
      packages/auth/src/Bootstrap/Container.ts
  2. 1 0
      packages/auth/src/Bootstrap/Service.ts
  3. 1 0
      packages/auth/src/Bootstrap/Types.ts
  4. 28 0
      packages/auth/src/Domain/Handler/SharedVaultFileMovedEventHandler.ts
  5. 1 1
      packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.ts
  6. 1 0
      packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeaturesDTO.ts
  7. 7 0
      packages/domain-events/src/Domain/Event/SharedVaultFileMovedEvent.ts
  8. 14 0
      packages/domain-events/src/Domain/Event/SharedVaultFileMovedEventPayload.ts
  9. 2 0
      packages/domain-events/src/Domain/index.ts
  10. 22 3
      packages/files/src/Bootstrap/Container.ts
  11. 30 4
      packages/files/src/Domain/Event/DomainEventFactory.ts
  12. 15 0
      packages/files/src/Domain/Event/DomainEventFactoryInterface.ts
  13. 5 9
      packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.spec.ts
  14. 7 19
      packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.ts
  15. 0 9
      packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadataResponse.ts
  16. 198 9
      packages/files/src/Domain/UseCase/MoveFile/MoveFile.spec.ts
  17. 78 8
      packages/files/src/Domain/UseCase/MoveFile/MoveFile.ts
  18. 8 2
      packages/files/src/Domain/UseCase/MoveFile/MoveFileDTO.ts
  19. 2 2
      packages/files/src/Infra/InversifyExpress/AnnotatedFilesController.spec.ts
  20. 6 5
      packages/files/src/Infra/InversifyExpress/AnnotatedFilesController.ts
  21. 8 7
      packages/files/src/Infra/InversifyExpress/AnnotatedSharedVaultFilesController.ts
  22. 2 0
      packages/home-server/src/Server/HomeServer.ts
  23. 1 0
      packages/home-server/src/Server/HomeServerInterface.ts
  24. 8 2
      packages/security/src/Domain/Token/SharedVaultValetTokenData.ts
  25. 14 0
      packages/syncing-server/src/Bootstrap/Container.ts
  26. 1 0
      packages/syncing-server/src/Bootstrap/Types.ts
  27. 90 0
      packages/syncing-server/src/Domain/Handler/SharedVaultFileMovedEventHandler.ts
  28. 17 0
      packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.spec.ts
  29. 46 31
      packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.ts
  30. 1 0
      packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetTokenDTO.ts

+ 10 - 0
packages/auth/src/Bootstrap/Container.ts

@@ -256,6 +256,7 @@ import { PaymentsAccountDeletedEventHandler } from '../Domain/Handler/PaymentsAc
 import { UpdateStorageQuotaUsedForUser } from '../Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
 import { UpdateStorageQuotaUsedForUser } from '../Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
 import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler'
 import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler'
 import { SharedVaultFileRemovedEventHandler } from '../Domain/Handler/SharedVaultFileRemovedEventHandler'
 import { SharedVaultFileRemovedEventHandler } from '../Domain/Handler/SharedVaultFileRemovedEventHandler'
+import { SharedVaultFileMovedEventHandler } from '../Domain/Handler/SharedVaultFileMovedEventHandler'
 
 
 export class ContainerConfigLoader {
 export class ContainerConfigLoader {
   async load(configuration?: {
   async load(configuration?: {
@@ -982,6 +983,14 @@ export class ContainerConfigLoader {
           container.get(TYPES.Auth_Logger),
           container.get(TYPES.Auth_Logger),
         ),
         ),
       )
       )
+    container
+      .bind<SharedVaultFileMovedEventHandler>(TYPES.Auth_SharedVaultFileMovedEventHandler)
+      .toConstantValue(
+        new SharedVaultFileMovedEventHandler(
+          container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
+          container.get(TYPES.Auth_Logger),
+        ),
+      )
     container
     container
       .bind<FileRemovedEventHandler>(TYPES.Auth_FileRemovedEventHandler)
       .bind<FileRemovedEventHandler>(TYPES.Auth_FileRemovedEventHandler)
       .toConstantValue(
       .toConstantValue(
@@ -1045,6 +1054,7 @@ export class ContainerConfigLoader {
       ['USER_EMAIL_CHANGED', container.get(TYPES.Auth_UserEmailChangedEventHandler)],
       ['USER_EMAIL_CHANGED', container.get(TYPES.Auth_UserEmailChangedEventHandler)],
       ['FILE_UPLOADED', container.get(TYPES.Auth_FileUploadedEventHandler)],
       ['FILE_UPLOADED', container.get(TYPES.Auth_FileUploadedEventHandler)],
       ['SHARED_VAULT_FILE_UPLOADED', container.get(TYPES.Auth_SharedVaultFileUploadedEventHandler)],
       ['SHARED_VAULT_FILE_UPLOADED', container.get(TYPES.Auth_SharedVaultFileUploadedEventHandler)],
+      ['SHARED_VAULT_FILE_MOVED', container.get(TYPES.Auth_SharedVaultFileMovedEventHandler)],
       ['FILE_REMOVED', container.get(TYPES.Auth_FileRemovedEventHandler)],
       ['FILE_REMOVED', container.get(TYPES.Auth_FileRemovedEventHandler)],
       ['SHARED_VAULT_FILE_REMOVED', container.get(TYPES.Auth_SharedVaultFileRemovedEventHandler)],
       ['SHARED_VAULT_FILE_REMOVED', container.get(TYPES.Auth_SharedVaultFileRemovedEventHandler)],
       ['LISTED_ACCOUNT_CREATED', container.get(TYPES.Auth_ListedAccountCreatedEventHandler)],
       ['LISTED_ACCOUNT_CREATED', container.get(TYPES.Auth_ListedAccountCreatedEventHandler)],

+ 1 - 0
packages/auth/src/Bootstrap/Service.ts

@@ -27,6 +27,7 @@ export class Service implements AuthServiceInterface {
   async activatePremiumFeatures(dto: {
   async activatePremiumFeatures(dto: {
     username: string
     username: string
     subscriptionPlanName?: string
     subscriptionPlanName?: string
+    uploadBytesLimit?: number
     endsAt?: Date
     endsAt?: Date
   }): Promise<Result<string>> {
   }): Promise<Result<string>> {
     if (!this.container) {
     if (!this.container) {

+ 1 - 0
packages/auth/src/Bootstrap/Types.ts

@@ -168,6 +168,7 @@ const TYPES = {
   Auth_UserEmailChangedEventHandler: Symbol.for('Auth_UserEmailChangedEventHandler'),
   Auth_UserEmailChangedEventHandler: Symbol.for('Auth_UserEmailChangedEventHandler'),
   Auth_FileUploadedEventHandler: Symbol.for('Auth_FileUploadedEventHandler'),
   Auth_FileUploadedEventHandler: Symbol.for('Auth_FileUploadedEventHandler'),
   Auth_SharedVaultFileUploadedEventHandler: Symbol.for('Auth_SharedVaultFileUploadedEventHandler'),
   Auth_SharedVaultFileUploadedEventHandler: Symbol.for('Auth_SharedVaultFileUploadedEventHandler'),
+  Auth_SharedVaultFileMovedEventHandler: Symbol.for('Auth_SharedVaultFileMovedEventHandler'),
   Auth_FileRemovedEventHandler: Symbol.for('Auth_FileRemovedEventHandler'),
   Auth_FileRemovedEventHandler: Symbol.for('Auth_FileRemovedEventHandler'),
   Auth_SharedVaultFileRemovedEventHandler: Symbol.for('Auth_SharedVaultFileRemovedEventHandler'),
   Auth_SharedVaultFileRemovedEventHandler: Symbol.for('Auth_SharedVaultFileRemovedEventHandler'),
   Auth_ListedAccountCreatedEventHandler: Symbol.for('Auth_ListedAccountCreatedEventHandler'),
   Auth_ListedAccountCreatedEventHandler: Symbol.for('Auth_ListedAccountCreatedEventHandler'),

+ 28 - 0
packages/auth/src/Domain/Handler/SharedVaultFileMovedEventHandler.ts

@@ -0,0 +1,28 @@
+import { DomainEventHandlerInterface, SharedVaultFileMovedEvent } from '@standardnotes/domain-events'
+import { Logger } from 'winston'
+
+import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
+
+export class SharedVaultFileMovedEventHandler implements DomainEventHandlerInterface {
+  constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
+
+  async handle(event: SharedVaultFileMovedEvent): Promise<void> {
+    const subtractResult = await this.updateStorageQuotaUsedForUserUseCase.execute({
+      userUuid: event.payload.from.ownerUuid,
+      bytesUsed: -event.payload.fileByteSize,
+    })
+
+    if (subtractResult.isFailed()) {
+      this.logger.error(`Failed to update storage quota used for user: ${subtractResult.getError()}`)
+    }
+
+    const addResult = await this.updateStorageQuotaUsedForUserUseCase.execute({
+      userUuid: event.payload.to.ownerUuid,
+      bytesUsed: event.payload.fileByteSize,
+    })
+
+    if (addResult.isFailed()) {
+      this.logger.error(`Failed to update storage quota used for user: ${addResult.getError()}`)
+    }
+  }
+}

+ 1 - 1
packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.ts

@@ -57,7 +57,7 @@ export class ActivatePremiumFeatures implements UseCaseInterface<string> {
 
 
     await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
     await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
       subscription,
       subscription,
-      new Map([[SettingName.NAMES.FileUploadBytesLimit, '-1']]),
+      new Map([[SettingName.NAMES.FileUploadBytesLimit, `${dto.uploadBytesLimit ?? -1}`]]),
     )
     )
 
 
     return Result.ok('Premium features activated.')
     return Result.ok('Premium features activated.')

+ 1 - 0
packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeaturesDTO.ts

@@ -1,5 +1,6 @@
 export interface ActivatePremiumFeaturesDTO {
 export interface ActivatePremiumFeaturesDTO {
   username: string
   username: string
   subscriptionPlanName?: string
   subscriptionPlanName?: string
+  uploadBytesLimit?: number
   endsAt?: Date
   endsAt?: Date
 }
 }

+ 7 - 0
packages/domain-events/src/Domain/Event/SharedVaultFileMovedEvent.ts

@@ -0,0 +1,7 @@
+import { DomainEventInterface } from './DomainEventInterface'
+import { SharedVaultFileMovedEventPayload } from './SharedVaultFileMovedEventPayload'
+
+export interface SharedVaultFileMovedEvent extends DomainEventInterface {
+  type: 'SHARED_VAULT_FILE_MOVED'
+  payload: SharedVaultFileMovedEventPayload
+}

+ 14 - 0
packages/domain-events/src/Domain/Event/SharedVaultFileMovedEventPayload.ts

@@ -0,0 +1,14 @@
+export interface SharedVaultFileMovedEventPayload {
+  fileByteSize: number
+  fileName: string
+  from: {
+    sharedVaultUuid?: string
+    ownerUuid: string
+    filePath: string
+  }
+  to: {
+    sharedVaultUuid?: string
+    ownerUuid: string
+    filePath: string
+  }
+}

+ 2 - 0
packages/domain-events/src/Domain/index.ts

@@ -64,6 +64,8 @@ export * from './Event/SharedSubscriptionInvitationCanceledEvent'
 export * from './Event/SharedSubscriptionInvitationCanceledEventPayload'
 export * from './Event/SharedSubscriptionInvitationCanceledEventPayload'
 export * from './Event/SharedSubscriptionInvitationCreatedEvent'
 export * from './Event/SharedSubscriptionInvitationCreatedEvent'
 export * from './Event/SharedSubscriptionInvitationCreatedEventPayload'
 export * from './Event/SharedSubscriptionInvitationCreatedEventPayload'
+export * from './Event/SharedVaultFileMovedEvent'
+export * from './Event/SharedVaultFileMovedEventPayload'
 export * from './Event/SharedVaultFileRemovedEvent'
 export * from './Event/SharedVaultFileRemovedEvent'
 export * from './Event/SharedVaultFileRemovedEventPayload'
 export * from './Event/SharedVaultFileRemovedEventPayload'
 export * from './Event/SharedVaultFileUploadedEvent'
 export * from './Event/SharedVaultFileUploadedEvent'

+ 22 - 3
packages/files/src/Bootstrap/Container.ts

@@ -99,7 +99,9 @@ export class ContainerConfigLoader {
     container
     container
       .bind<TokenDecoderInterface<ValetTokenData>>(TYPES.Files_ValetTokenDecoder)
       .bind<TokenDecoderInterface<ValetTokenData>>(TYPES.Files_ValetTokenDecoder)
       .toConstantValue(new TokenDecoder<ValetTokenData>(container.get(TYPES.Files_VALET_TOKEN_SECRET)))
       .toConstantValue(new TokenDecoder<ValetTokenData>(container.get(TYPES.Files_VALET_TOKEN_SECRET)))
-    container.bind<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory).to(DomainEventFactory)
+    container
+      .bind<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory)
+      .toConstantValue(new DomainEventFactory(container.get<TimerInterface>(TYPES.Files_Timer)))
 
 
     if (isConfiguredForInMemoryCache) {
     if (isConfiguredForInMemoryCache) {
       container
       container
@@ -214,9 +216,26 @@ export class ContainerConfigLoader {
           container.get(TYPES.Files_DomainEventFactory),
           container.get(TYPES.Files_DomainEventFactory),
         ),
         ),
       )
       )
-    container.bind<GetFileMetadata>(TYPES.Files_GetFileMetadata).to(GetFileMetadata)
+    container
+      .bind<GetFileMetadata>(TYPES.Files_GetFileMetadata)
+      .toConstantValue(
+        new GetFileMetadata(
+          container.get<FileDownloaderInterface>(TYPES.Files_FileDownloader),
+          container.get<winston.Logger>(TYPES.Files_Logger),
+        ),
+      )
     container.bind<RemoveFile>(TYPES.Files_RemoveFile).to(RemoveFile)
     container.bind<RemoveFile>(TYPES.Files_RemoveFile).to(RemoveFile)
-    container.bind<MoveFile>(TYPES.Files_MoveFile).to(MoveFile)
+    container
+      .bind<MoveFile>(TYPES.Files_MoveFile)
+      .toConstantValue(
+        new MoveFile(
+          container.get<GetFileMetadata>(TYPES.Files_GetFileMetadata),
+          container.get<FileMoverInterface>(TYPES.Files_FileMover),
+          container.get<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher),
+          container.get<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory),
+          container.get<winston.Logger>(TYPES.Files_Logger),
+        ),
+      )
     container.bind<MarkFilesToBeRemoved>(TYPES.Files_MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
     container.bind<MarkFilesToBeRemoved>(TYPES.Files_MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
 
 
     // middleware
     // middleware

+ 30 - 4
packages/files/src/Domain/Event/DomainEventFactory.ts

@@ -4,16 +4,14 @@ import {
   DomainEventService,
   DomainEventService,
   SharedVaultFileUploadedEvent,
   SharedVaultFileUploadedEvent,
   SharedVaultFileRemovedEvent,
   SharedVaultFileRemovedEvent,
+  SharedVaultFileMovedEvent,
 } from '@standardnotes/domain-events'
 } from '@standardnotes/domain-events'
 import { TimerInterface } from '@standardnotes/time'
 import { TimerInterface } from '@standardnotes/time'
-import { inject, injectable } from 'inversify'
 
 
-import TYPES from '../../Bootstrap/Types'
 import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
 import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
 
 
-@injectable()
 export class DomainEventFactory implements DomainEventFactoryInterface {
 export class DomainEventFactory implements DomainEventFactoryInterface {
-  constructor(@inject(TYPES.Files_Timer) private timer: TimerInterface) {}
+  constructor(private timer: TimerInterface) {}
 
 
   createFileRemovedEvent(payload: {
   createFileRemovedEvent(payload: {
     userUuid: string
     userUuid: string
@@ -56,6 +54,34 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
     }
     }
   }
   }
 
 
+  createSharedVaultFileMovedEvent(payload: {
+    fileByteSize: number
+    fileName: string
+    from: {
+      sharedVaultUuid?: string
+      ownerUuid: string
+      filePath: string
+    }
+    to: {
+      sharedVaultUuid?: string
+      ownerUuid: string
+      filePath: string
+    }
+  }): SharedVaultFileMovedEvent {
+    return {
+      type: 'SHARED_VAULT_FILE_MOVED',
+      createdAt: this.timer.getUTCDate(),
+      meta: {
+        correlation: {
+          userIdentifier: payload.from.sharedVaultUuid ?? payload.from.ownerUuid,
+          userIdentifierType: payload.from.sharedVaultUuid ? 'shared-vault-uuid' : 'uuid',
+        },
+        origin: DomainEventService.Files,
+      },
+      payload,
+    }
+  }
+
   createSharedVaultFileUploadedEvent(payload: {
   createSharedVaultFileUploadedEvent(payload: {
     sharedVaultUuid: string
     sharedVaultUuid: string
     vaultOwnerUuid: string
     vaultOwnerUuid: string

+ 15 - 0
packages/files/src/Domain/Event/DomainEventFactoryInterface.ts

@@ -3,6 +3,7 @@ import {
   FileRemovedEvent,
   FileRemovedEvent,
   SharedVaultFileRemovedEvent,
   SharedVaultFileRemovedEvent,
   SharedVaultFileUploadedEvent,
   SharedVaultFileUploadedEvent,
+  SharedVaultFileMovedEvent,
 } from '@standardnotes/domain-events'
 } from '@standardnotes/domain-events'
 
 
 export interface DomainEventFactoryInterface {
 export interface DomainEventFactoryInterface {
@@ -19,6 +20,20 @@ export interface DomainEventFactoryInterface {
     fileByteSize: number
     fileByteSize: number
     regularSubscriptionUuid: string
     regularSubscriptionUuid: string
   }): FileRemovedEvent
   }): FileRemovedEvent
+  createSharedVaultFileMovedEvent(payload: {
+    fileByteSize: number
+    fileName: string
+    from: {
+      sharedVaultUuid?: string
+      ownerUuid: string
+      filePath: string
+    }
+    to: {
+      sharedVaultUuid?: string
+      ownerUuid: string
+      filePath: string
+    }
+  }): SharedVaultFileMovedEvent
   createSharedVaultFileUploadedEvent(payload: {
   createSharedVaultFileUploadedEvent(payload: {
     sharedVaultUuid: string
     sharedVaultUuid: string
     vaultOwnerUuid: string
     vaultOwnerUuid: string

+ 5 - 9
packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.spec.ts

@@ -1,4 +1,3 @@
-import 'reflect-metadata'
 import { Logger } from 'winston'
 import { Logger } from 'winston'
 import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
 import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
 
 
@@ -19,10 +18,8 @@ describe('GetFileMetadata', () => {
   })
   })
 
 
   it('should return the file metadata', async () => {
   it('should return the file metadata', async () => {
-    expect(await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })).toEqual({
-      success: true,
-      size: 123,
-    })
+    const result = await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })
+    expect(result.getValue()).toEqual(123)
   })
   })
 
 
   it('should not return the file metadata if it fails', async () => {
   it('should not return the file metadata if it fails', async () => {
@@ -30,9 +27,8 @@ describe('GetFileMetadata', () => {
       throw new Error('ooops')
       throw new Error('ooops')
     })
     })
 
 
-    expect(await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })).toEqual({
-      success: false,
-      message: 'Could not get file metadata.',
-    })
+    const result = await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })
+
+    expect(result.isFailed()).toBe(true)
   })
   })
 })
 })

+ 7 - 19
packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.ts

@@ -1,32 +1,20 @@
-import { inject, injectable } from 'inversify'
 import { Logger } from 'winston'
 import { Logger } from 'winston'
-import TYPES from '../../../Bootstrap/Types'
 import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
 import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
-import { UseCaseInterface } from '../UseCaseInterface'
 import { GetFileMetadataDTO } from './GetFileMetadataDTO'
 import { GetFileMetadataDTO } from './GetFileMetadataDTO'
-import { GetFileMetadataResponse } from './GetFileMetadataResponse'
+import { Result, UseCaseInterface } from '@standardnotes/domain-core'
 
 
-@injectable()
-export class GetFileMetadata implements UseCaseInterface {
-  constructor(
-    @inject(TYPES.Files_FileDownloader) private fileDownloader: FileDownloaderInterface,
-    @inject(TYPES.Files_Logger) private logger: Logger,
-  ) {}
+export class GetFileMetadata implements UseCaseInterface<number> {
+  constructor(private fileDownloader: FileDownloaderInterface, private logger: Logger) {}
 
 
-  async execute(dto: GetFileMetadataDTO): Promise<GetFileMetadataResponse> {
+  async execute(dto: GetFileMetadataDTO): Promise<Result<number>> {
     try {
     try {
       const size = await this.fileDownloader.getFileSize(`${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`)
       const size = await this.fileDownloader.getFileSize(`${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`)
 
 
-      return {
-        success: true,
-        size,
-      }
+      return Result.ok(size)
     } catch (error) {
     } catch (error) {
       this.logger.error(`Could not get file metadata for resource: ${dto.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.',
-      }
+
+      return Result.fail('Could not get file metadata')
     }
     }
   }
   }
 }
 }

+ 0 - 9
packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadataResponse.ts

@@ -1,9 +0,0 @@
-export type GetFileMetadataResponse =
-  | {
-      success: true
-      size: number
-    }
-  | {
-      success: false
-      message: string
-    }

+ 198 - 9
packages/files/src/Domain/UseCase/MoveFile/MoveFile.spec.ts

@@ -1,18 +1,27 @@
-import 'reflect-metadata'
-
+import { DomainEventPublisherInterface, SharedVaultFileMovedEvent } from '@standardnotes/domain-events'
+import { Result } from '@standardnotes/domain-core'
 import { Logger } from 'winston'
 import { Logger } from 'winston'
 
 
 import { MoveFile } from './MoveFile'
 import { MoveFile } from './MoveFile'
 import { FileMoverInterface } from '../../Services/FileMoverInterface'
 import { FileMoverInterface } from '../../Services/FileMoverInterface'
+import { GetFileMetadata } from '../GetFileMetadata/GetFileMetadata'
+import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
 
 
 describe('MoveFile', () => {
 describe('MoveFile', () => {
   let fileMover: FileMoverInterface
   let fileMover: FileMoverInterface
+  let getFileMetadataUseCase: GetFileMetadata
+  let domainEventPublisher: DomainEventPublisherInterface
+  let domainEventFactory: DomainEventFactoryInterface
 
 
   let logger: Logger
   let logger: Logger
 
 
-  const createUseCase = () => new MoveFile(fileMover, logger)
+  const createUseCase = () =>
+    new MoveFile(getFileMetadataUseCase, fileMover, domainEventPublisher, domainEventFactory, logger)
 
 
   beforeEach(() => {
   beforeEach(() => {
+    getFileMetadataUseCase = {} as jest.Mocked<GetFileMetadata>
+    getFileMetadataUseCase.execute = jest.fn().mockReturnValue(Result.ok(1234))
+
     fileMover = {} as jest.Mocked<FileMoverInterface>
     fileMover = {} as jest.Mocked<FileMoverInterface>
     fileMover.moveFile = jest.fn().mockReturnValue(413)
     fileMover.moveFile = jest.fn().mockReturnValue(413)
 
 
@@ -20,17 +29,34 @@ describe('MoveFile', () => {
     logger.debug = jest.fn()
     logger.debug = jest.fn()
     logger.error = jest.fn()
     logger.error = jest.fn()
     logger.warn = jest.fn()
     logger.warn = jest.fn()
+
+    domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
+    domainEventPublisher.publish = jest.fn()
+
+    domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
+    domainEventFactory.createSharedVaultFileMovedEvent = jest
+      .fn()
+      .mockReturnValue({} as jest.Mocked<SharedVaultFileMovedEvent>)
   })
   })
 
 
   it('should move a file', async () => {
   it('should move a file', async () => {
     await createUseCase().execute({
     await createUseCase().execute({
       resourceRemoteIdentifier: '2-3-4',
       resourceRemoteIdentifier: '2-3-4',
-      fromUuid: '1-2-3',
-      toUuid: '4-5-6',
-      moveType: 'shared-vault-to-user',
+      from: {
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+        ownerUuid: '00000000-0000-0000-0000-000000000000',
+      },
+      to: {
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
+        ownerUuid: '00000000-0000-0000-0000-000000000001',
+      },
+      moveType: 'shared-vault-to-shared-vault',
     })
     })
 
 
-    expect(fileMover.moveFile).toHaveBeenCalledWith('1-2-3/2-3-4', '4-5-6/2-3-4')
+    expect(fileMover.moveFile).toHaveBeenCalledWith(
+      '00000000-0000-0000-0000-000000000000/2-3-4',
+      '00000000-0000-0000-0000-000000000001/2-3-4',
+    )
   })
   })
 
 
   it('should indicate an error if moving fails', async () => {
   it('should indicate an error if moving fails', async () => {
@@ -40,11 +66,174 @@ describe('MoveFile', () => {
 
 
     const result = await createUseCase().execute({
     const result = await createUseCase().execute({
       resourceRemoteIdentifier: '2-3-4',
       resourceRemoteIdentifier: '2-3-4',
-      fromUuid: '1-2-3',
-      toUuid: '4-5-6',
+      from: {
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+        ownerUuid: '00000000-0000-0000-0000-000000000000',
+      },
+      to: {
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
+        ownerUuid: '00000000-0000-0000-0000-000000000001',
+      },
+      moveType: 'shared-vault-to-shared-vault',
+    })
+
+    expect(result.isFailed()).toEqual(true)
+  })
+
+  it('should return an error if the from shared vault uuid is invalid', async () => {
+    const result = await createUseCase().execute({
+      resourceRemoteIdentifier: '2-3-4',
+      from: {
+        sharedVaultUuid: 'invalid',
+        ownerUuid: '00000000-0000-0000-0000-000000000000',
+      },
+      to: {
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
+        ownerUuid: '00000000-0000-0000-0000-000000000001',
+      },
+      moveType: 'shared-vault-to-shared-vault',
+    })
+
+    expect(result.isFailed()).toEqual(true)
+  })
+
+  it('should return an error if the to shared vault uuid is invalid', async () => {
+    const result = await createUseCase().execute({
+      resourceRemoteIdentifier: '2-3-4',
+      from: {
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+        ownerUuid: '00000000-0000-0000-0000-000000000000',
+      },
+      to: {
+        sharedVaultUuid: 'invalid',
+        ownerUuid: '00000000-0000-0000-0000-000000000001',
+      },
+      moveType: 'shared-vault-to-shared-vault',
+    })
+
+    expect(result.isFailed()).toEqual(true)
+  })
+
+  it('should return an error if the from owner uuid is invalid', async () => {
+    const result = await createUseCase().execute({
+      resourceRemoteIdentifier: '2-3-4',
+      from: {
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+        ownerUuid: 'invalid',
+      },
+      to: {
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
+        ownerUuid: '00000000-0000-0000-0000-000000000001',
+      },
+      moveType: 'shared-vault-to-shared-vault',
+    })
+
+    expect(result.isFailed()).toEqual(true)
+  })
+
+  it('should return an error if the to owner uuid is invalid', async () => {
+    const result = await createUseCase().execute({
+      resourceRemoteIdentifier: '2-3-4',
+      from: {
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+        ownerUuid: '00000000-0000-0000-0000-000000000000',
+      },
+      to: {
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
+        ownerUuid: 'invalid',
+      },
+      moveType: 'shared-vault-to-shared-vault',
+    })
+
+    expect(result.isFailed()).toEqual(true)
+  })
+
+  it('should return an error if the file metadata cannot be retrieved', async () => {
+    getFileMetadataUseCase.execute = jest.fn().mockReturnValue(Result.fail('oops'))
+
+    const result = await createUseCase().execute({
+      resourceRemoteIdentifier: '2-3-4',
+      from: {
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+        ownerUuid: '00000000-0000-0000-0000-000000000000',
+      },
+      to: {
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
+        ownerUuid: '00000000-0000-0000-0000-000000000001',
+      },
+      moveType: 'shared-vault-to-shared-vault',
+    })
+
+    expect(result.isFailed()).toEqual(true)
+  })
+
+  it('should move file from user to shared vault', async () => {
+    const result = await createUseCase().execute({
+      resourceRemoteIdentifier: '2-3-4',
+      from: {
+        ownerUuid: '00000000-0000-0000-0000-000000000000',
+      },
+      to: {
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
+        ownerUuid: '00000000-0000-0000-0000-000000000002',
+      },
+      moveType: 'user-to-shared-vault',
+    })
+
+    expect(fileMover.moveFile).toHaveBeenCalledWith(
+      '00000000-0000-0000-0000-000000000000/2-3-4',
+      '00000000-0000-0000-0000-000000000001/2-3-4',
+    )
+    expect(result.isFailed()).toEqual(false)
+  })
+
+  it('should move file from shared vault to user', async () => {
+    const result = await createUseCase().execute({
+      resourceRemoteIdentifier: '2-3-4',
+      from: {
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
+        ownerUuid: '00000000-0000-0000-0000-000000000002',
+      },
+      to: {
+        ownerUuid: '00000000-0000-0000-0000-000000000000',
+      },
       moveType: 'shared-vault-to-user',
       moveType: 'shared-vault-to-user',
     })
     })
 
 
+    expect(fileMover.moveFile).toHaveBeenCalledWith(
+      '00000000-0000-0000-0000-000000000001/2-3-4',
+      '00000000-0000-0000-0000-000000000000/2-3-4',
+    )
+    expect(result.isFailed()).toEqual(false)
+  })
+
+  it('should fail if moving from shared vault to user without shared vault uuid', async () => {
+    const result = await createUseCase().execute({
+      resourceRemoteIdentifier: '2-3-4',
+      from: {
+        ownerUuid: '00000000-0000-0000-0000-000000000000',
+      },
+      to: {
+        ownerUuid: '00000000-0000-0000-0000-000000000000',
+      },
+      moveType: 'shared-vault-to-user',
+    })
+
+    expect(result.isFailed()).toEqual(true)
+  })
+
+  it('should fail if moving from user to shared vault without shared vault uuid', async () => {
+    const result = await createUseCase().execute({
+      resourceRemoteIdentifier: '2-3-4',
+      from: {
+        ownerUuid: '00000000-0000-0000-0000-000000000000',
+      },
+      to: {
+        ownerUuid: '00000000-0000-0000-0000-000000000000',
+      },
+      moveType: 'user-to-shared-vault',
+    })
+
     expect(result.isFailed()).toEqual(true)
     expect(result.isFailed()).toEqual(true)
   })
   })
 })
 })

+ 78 - 8
packages/files/src/Domain/UseCase/MoveFile/MoveFile.ts

@@ -1,27 +1,97 @@
-import { inject, injectable } from 'inversify'
 import { Logger } from 'winston'
 import { Logger } from 'winston'
+import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
 
 
-import TYPES from '../../../Bootstrap/Types'
 import { FileMoverInterface } from '../../Services/FileMoverInterface'
 import { FileMoverInterface } from '../../Services/FileMoverInterface'
 import { MoveFileDTO } from './MoveFileDTO'
 import { MoveFileDTO } from './MoveFileDTO'
-import { Result, UseCaseInterface } from '@standardnotes/domain-core'
+import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
+import { GetFileMetadata } from '../GetFileMetadata/GetFileMetadata'
 
 
-@injectable()
 export class MoveFile implements UseCaseInterface<boolean> {
 export class MoveFile implements UseCaseInterface<boolean> {
   constructor(
   constructor(
-    @inject(TYPES.Files_FileMover) private fileMover: FileMoverInterface,
-    @inject(TYPES.Files_Logger) private logger: Logger,
+    private getFileMetadataUseCase: GetFileMetadata,
+    private fileMover: FileMoverInterface,
+    private domainEventPublisher: DomainEventPublisherInterface,
+    private domainEventFactory: DomainEventFactoryInterface,
+    private logger: Logger,
   ) {}
   ) {}
 
 
   async execute(dto: MoveFileDTO): Promise<Result<boolean>> {
   async execute(dto: MoveFileDTO): Promise<Result<boolean>> {
     try {
     try {
-      const srcPath = `${dto.fromUuid}/${dto.resourceRemoteIdentifier}`
-      const destPath = `${dto.toUuid}/${dto.resourceRemoteIdentifier}`
+      let fromSharedVaultUuid: Uuid | undefined = undefined
+      if (dto.from.sharedVaultUuid !== undefined) {
+        const fromSharedVaultUuidOrError = Uuid.create(dto.from.sharedVaultUuid)
+        if (fromSharedVaultUuidOrError.isFailed()) {
+          return Result.fail(fromSharedVaultUuidOrError.getError())
+        }
+        fromSharedVaultUuid = fromSharedVaultUuidOrError.getValue()
+      }
+
+      let toSharedVaultUuid: Uuid | undefined = undefined
+      if (dto.to.sharedVaultUuid !== undefined) {
+        const toSharedVaultUuidOrError = Uuid.create(dto.to.sharedVaultUuid)
+        if (toSharedVaultUuidOrError.isFailed()) {
+          return Result.fail(toSharedVaultUuidOrError.getError())
+        }
+        toSharedVaultUuid = toSharedVaultUuidOrError.getValue()
+      }
+
+      const fromOwnerUuidOrError = Uuid.create(dto.from.ownerUuid)
+      if (fromOwnerUuidOrError.isFailed()) {
+        return Result.fail(fromOwnerUuidOrError.getError())
+      }
+      const fromOwnerUuid = fromOwnerUuidOrError.getValue()
+
+      const toOwnerUuidOrError = Uuid.create(dto.to.ownerUuid)
+      if (toOwnerUuidOrError.isFailed()) {
+        return Result.fail(toOwnerUuidOrError.getError())
+      }
+      const toOwnerUuid = toOwnerUuidOrError.getValue()
+
+      if (['shared-vault-to-shared-vault', 'shared-vault-to-user'].includes(dto.moveType) && !fromSharedVaultUuid) {
+        return Result.fail('Source shared vault UUID is required')
+      }
+
+      if (['user-to-shared-vault', 'shared-vault-to-shared-vault'].includes(dto.moveType) && !toSharedVaultUuid) {
+        return Result.fail('Target shared vault UUID is required')
+      }
+
+      const fromUuid = dto.moveType === 'user-to-shared-vault' ? fromOwnerUuid.value : fromSharedVaultUuid?.value
+      const toUuid = dto.moveType === 'shared-vault-to-user' ? toOwnerUuid.value : toSharedVaultUuid?.value
+
+      const srcPath = `${fromUuid}/${dto.resourceRemoteIdentifier}`
+      const destPath = `${toUuid}/${dto.resourceRemoteIdentifier}`
 
 
       this.logger.debug(`Moving file from ${srcPath} to ${destPath}`)
       this.logger.debug(`Moving file from ${srcPath} to ${destPath}`)
 
 
+      const metadataResultOrError = await this.getFileMetadataUseCase.execute({
+        resourceRemoteIdentifier: dto.resourceRemoteIdentifier,
+        ownerUuid: fromUuid as string,
+      })
+      if (metadataResultOrError.isFailed()) {
+        return Result.fail(metadataResultOrError.getError())
+      }
+      const fileSize = metadataResultOrError.getValue()
+
       await this.fileMover.moveFile(srcPath, destPath)
       await this.fileMover.moveFile(srcPath, destPath)
 
 
+      await this.domainEventPublisher.publish(
+        this.domainEventFactory.createSharedVaultFileMovedEvent({
+          fileByteSize: fileSize,
+          fileName: dto.resourceRemoteIdentifier,
+          from: {
+            sharedVaultUuid: fromSharedVaultUuid?.value,
+            ownerUuid: fromOwnerUuid.value,
+            filePath: srcPath,
+          },
+          to: {
+            sharedVaultUuid: toSharedVaultUuid?.value,
+            ownerUuid: toOwnerUuid.value,
+            filePath: destPath,
+          },
+        }),
+      )
+
       return Result.ok()
       return Result.ok()
     } catch (error) {
     } catch (error) {
       this.logger.error(`Could not move resource: ${dto.resourceRemoteIdentifier} - ${(error as Error).message}`)
       this.logger.error(`Could not move resource: ${dto.resourceRemoteIdentifier} - ${(error as Error).message}`)

+ 8 - 2
packages/files/src/Domain/UseCase/MoveFile/MoveFileDTO.ts

@@ -2,7 +2,13 @@ import { SharedVaultMoveType } from '@standardnotes/security'
 
 
 export interface MoveFileDTO {
 export interface MoveFileDTO {
   moveType: SharedVaultMoveType
   moveType: SharedVaultMoveType
-  fromUuid: string
-  toUuid: string
+  from: {
+    sharedVaultUuid?: string
+    ownerUuid: string
+  }
+  to: {
+    sharedVaultUuid?: string
+    ownerUuid: string
+  }
   resourceRemoteIdentifier: string
   resourceRemoteIdentifier: string
 }
 }

+ 2 - 2
packages/files/src/Infra/InversifyExpress/AnnotatedFilesController.spec.ts

@@ -61,7 +61,7 @@ describe('AnnotatedFilesController', () => {
     finishUploadSession.execute = jest.fn().mockReturnValue(Result.ok())
     finishUploadSession.execute = jest.fn().mockReturnValue(Result.ok())
 
 
     getFileMetadata = {} as jest.Mocked<GetFileMetadata>
     getFileMetadata = {} as jest.Mocked<GetFileMetadata>
-    getFileMetadata.execute = jest.fn().mockReturnValue({ success: true, size: 555_555 })
+    getFileMetadata.execute = jest.fn().mockReturnValue(Result.ok(555_555))
 
 
     removeFile = {} as jest.Mocked<RemoveFile>
     removeFile = {} as jest.Mocked<RemoveFile>
     removeFile.execute = jest.fn().mockReturnValue(Result.ok())
     removeFile.execute = jest.fn().mockReturnValue(Result.ok())
@@ -183,7 +183,7 @@ describe('AnnotatedFilesController', () => {
 
 
     request.headers['range'] = 'bytes=0-'
     request.headers['range'] = 'bytes=0-'
 
 
-    getFileMetadata.execute = jest.fn().mockReturnValue({ success: false, message: 'error' })
+    getFileMetadata.execute = jest.fn().mockReturnValue(Result.fail('error'))
 
 
     const httpResponse = await createController().download(request, response)
     const httpResponse = await createController().download(request, response)
 
 

+ 6 - 5
packages/files/src/Infra/InversifyExpress/AnnotatedFilesController.ts

@@ -146,20 +146,21 @@ export class AnnotatedFilesController extends BaseHttpController {
       chunkSize = this.maxChunkBytes
       chunkSize = this.maxChunkBytes
     }
     }
 
 
-    const fileMetadata = await this.getFileMetadata.execute({
+    const fileMetadataOrError = await this.getFileMetadata.execute({
       ownerUuid: response.locals.userUuid,
       ownerUuid: response.locals.userUuid,
       resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
       resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
     })
     })
 
 
-    if (!fileMetadata.success) {
-      return this.badRequest(fileMetadata.message)
+    if (fileMetadataOrError.isFailed()) {
+      return this.badRequest(fileMetadataOrError.getError())
     }
     }
+    const fileSize = fileMetadataOrError.getValue()
 
 
     const startRange = Number(range.replace(/\D/g, ''))
     const startRange = Number(range.replace(/\D/g, ''))
-    const endRange = Math.min(startRange + chunkSize - 1, fileMetadata.size - 1)
+    const endRange = Math.min(startRange + chunkSize - 1, fileSize - 1)
 
 
     const headers = {
     const headers = {
-      'Content-Range': `bytes ${startRange}-${endRange}/${fileMetadata.size}`,
+      'Content-Range': `bytes ${startRange}-${endRange}/${fileSize}`,
       'Accept-Ranges': 'bytes',
       'Accept-Ranges': 'bytes',
       'Content-Length': endRange - startRange + 1,
       'Content-Length': endRange - startRange + 1,
       'Content-Type': 'application/octet-stream',
       'Content-Type': 'application/octet-stream',

+ 8 - 7
packages/files/src/Infra/InversifyExpress/AnnotatedSharedVaultFilesController.ts

@@ -47,8 +47,8 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController {
 
 
     const result = await this.moveFile.execute({
     const result = await this.moveFile.execute({
       moveType: moveOperation.type,
       moveType: moveOperation.type,
-      fromUuid: moveOperation.fromUuid,
-      toUuid: moveOperation.toUuid,
+      from: moveOperation.from,
+      to: moveOperation.to,
       resourceRemoteIdentifier: locals.remoteIdentifier,
       resourceRemoteIdentifier: locals.remoteIdentifier,
     })
     })
 
 
@@ -187,20 +187,21 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController {
       chunkSize = this.maxChunkBytes
       chunkSize = this.maxChunkBytes
     }
     }
 
 
-    const fileMetadata = await this.getFileMetadata.execute({
+    const fileMetadataOrError = await this.getFileMetadata.execute({
       ownerUuid: locals.sharedVaultUuid,
       ownerUuid: locals.sharedVaultUuid,
       resourceRemoteIdentifier: locals.remoteIdentifier,
       resourceRemoteIdentifier: locals.remoteIdentifier,
     })
     })
 
 
-    if (!fileMetadata.success) {
-      return this.badRequest(fileMetadata.message)
+    if (fileMetadataOrError.isFailed()) {
+      return this.badRequest(fileMetadataOrError.getError())
     }
     }
+    const fileSize = fileMetadataOrError.getValue()
 
 
     const startRange = Number(range.replace(/\D/g, ''))
     const startRange = Number(range.replace(/\D/g, ''))
-    const endRange = Math.min(startRange + chunkSize - 1, fileMetadata.size - 1)
+    const endRange = Math.min(startRange + chunkSize - 1, fileSize - 1)
 
 
     const headers = {
     const headers = {
-      'Content-Range': `bytes ${startRange}-${endRange}/${fileMetadata.size}`,
+      'Content-Range': `bytes ${startRange}-${endRange}/${fileSize}`,
       'Accept-Ranges': 'bytes',
       'Accept-Ranges': 'bytes',
       'Content-Length': endRange - startRange + 1,
       'Content-Length': endRange - startRange + 1,
       'Content-Type': 'application/octet-stream',
       'Content-Type': 'application/octet-stream',

+ 2 - 0
packages/home-server/src/Server/HomeServer.ts

@@ -144,6 +144,7 @@ export class HomeServer implements HomeServerInterface {
             void this.activatePremiumFeatures({
             void this.activatePremiumFeatures({
               username: request.body.username,
               username: request.body.username,
               subscriptionPlanName: request.body.subscriptionPlanName,
               subscriptionPlanName: request.body.subscriptionPlanName,
+              uploadBytesLimit: request.body.uploadBytesLimit,
               endsAt: request.body.endsAt ? new Date(request.body.endsAt) : undefined,
               endsAt: request.body.endsAt ? new Date(request.body.endsAt) : undefined,
             }).then((result) => {
             }).then((result) => {
               if (result.isFailed()) {
               if (result.isFailed()) {
@@ -221,6 +222,7 @@ export class HomeServer implements HomeServerInterface {
   async activatePremiumFeatures(dto: {
   async activatePremiumFeatures(dto: {
     username: string
     username: string
     subscriptionPlanName?: string
     subscriptionPlanName?: string
+    uploadBytesLimit?: number
     endsAt?: Date
     endsAt?: Date
   }): Promise<Result<string>> {
   }): Promise<Result<string>> {
     if (!this.isRunning() || !this.authService) {
     if (!this.isRunning() || !this.authService) {

+ 1 - 0
packages/home-server/src/Server/HomeServerInterface.ts

@@ -6,6 +6,7 @@ export interface HomeServerInterface {
   activatePremiumFeatures(dto: {
   activatePremiumFeatures(dto: {
     username: string
     username: string
     subscriptionPlanName?: string
     subscriptionPlanName?: string
+    uploadBytesLimit?: number
     endsAt?: Date
     endsAt?: Date
   }): Promise<Result<string>>
   }): Promise<Result<string>>
   stop(): Promise<Result<string>>
   stop(): Promise<Result<string>>

+ 8 - 2
packages/security/src/Domain/Token/SharedVaultValetTokenData.ts

@@ -11,7 +11,13 @@ export interface SharedVaultValetTokenData {
   uploadBytesLimit?: number
   uploadBytesLimit?: number
   moveOperation?: {
   moveOperation?: {
     type: SharedVaultMoveType
     type: SharedVaultMoveType
-    fromUuid: string
-    toUuid: string
+    from: {
+      sharedVaultUuid?: string
+      ownerUuid: string
+    }
+    to: {
+      sharedVaultUuid?: string
+      ownerUuid: string
+    }
   }
   }
 }
 }

+ 14 - 0
packages/syncing-server/src/Bootstrap/Container.ts

@@ -155,6 +155,7 @@ import { Logger } from 'winston'
 import { ItemRepositoryResolverInterface } from '../Domain/Item/ItemRepositoryResolverInterface'
 import { ItemRepositoryResolverInterface } from '../Domain/Item/ItemRepositoryResolverInterface'
 import { TypeORMItemRepositoryResolver } from '../Infra/TypeORM/TypeORMItemRepositoryResolver'
 import { TypeORMItemRepositoryResolver } from '../Infra/TypeORM/TypeORMItemRepositoryResolver'
 import { TransitionItemsFromPrimaryToSecondaryDatabaseForUser } from '../Domain/UseCase/Transition/TransitionItemsFromPrimaryToSecondaryDatabaseForUser/TransitionItemsFromPrimaryToSecondaryDatabaseForUser'
 import { TransitionItemsFromPrimaryToSecondaryDatabaseForUser } from '../Domain/UseCase/Transition/TransitionItemsFromPrimaryToSecondaryDatabaseForUser/TransitionItemsFromPrimaryToSecondaryDatabaseForUser'
+import { SharedVaultFileMovedEventHandler } from '../Domain/Handler/SharedVaultFileMovedEventHandler'
 
 
 export class ContainerConfigLoader {
 export class ContainerConfigLoader {
   private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
   private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -872,6 +873,15 @@ export class ContainerConfigLoader {
           container.get<winston.Logger>(TYPES.Sync_Logger),
           container.get<winston.Logger>(TYPES.Sync_Logger),
         ),
         ),
       )
       )
+    container
+      .bind<SharedVaultFileMovedEventHandler>(TYPES.Sync_SharedVaultFileMovedEventHandler)
+      .toConstantValue(
+        new SharedVaultFileMovedEventHandler(
+          container.get<UpdateStorageQuotaUsedInSharedVault>(TYPES.Sync_UpdateStorageQuotaUsedInSharedVault),
+          container.get<AddNotificationsForUsers>(TYPES.Sync_AddNotificationsForUsers),
+          container.get<winston.Logger>(TYPES.Sync_Logger),
+        ),
+      )
 
 
     // Services
     // Services
     container.bind<ContentDecoder>(TYPES.Sync_ContentDecoder).toDynamicValue(() => new ContentDecoder())
     container.bind<ContentDecoder>(TYPES.Sync_ContentDecoder).toDynamicValue(() => new ContentDecoder())
@@ -902,6 +912,10 @@ export class ContainerConfigLoader {
         'SHARED_VAULT_FILE_REMOVED',
         'SHARED_VAULT_FILE_REMOVED',
         container.get<SharedVaultFileRemovedEventHandler>(TYPES.Sync_SharedVaultFileRemovedEventHandler),
         container.get<SharedVaultFileRemovedEventHandler>(TYPES.Sync_SharedVaultFileRemovedEventHandler),
       ],
       ],
+      [
+        'SHARED_VAULT_FILE_MOVED',
+        container.get<SharedVaultFileMovedEventHandler>(TYPES.Sync_SharedVaultFileMovedEventHandler),
+      ],
     ])
     ])
     if (!isConfiguredForHomeServer) {
     if (!isConfiguredForHomeServer) {
       container.bind(TYPES.Sync_AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL'))
       container.bind(TYPES.Sync_AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL'))

+ 1 - 0
packages/syncing-server/src/Bootstrap/Types.ts

@@ -90,6 +90,7 @@ const TYPES = {
   Sync_ItemRevisionCreationRequestedEventHandler: Symbol.for('Sync_ItemRevisionCreationRequestedEventHandler'),
   Sync_ItemRevisionCreationRequestedEventHandler: Symbol.for('Sync_ItemRevisionCreationRequestedEventHandler'),
   Sync_SharedVaultFileRemovedEventHandler: Symbol.for('Sync_SharedVaultFileRemovedEventHandler'),
   Sync_SharedVaultFileRemovedEventHandler: Symbol.for('Sync_SharedVaultFileRemovedEventHandler'),
   Sync_SharedVaultFileUploadedEventHandler: Symbol.for('Sync_SharedVaultFileUploadedEventHandler'),
   Sync_SharedVaultFileUploadedEventHandler: Symbol.for('Sync_SharedVaultFileUploadedEventHandler'),
+  Sync_SharedVaultFileMovedEventHandler: Symbol.for('Sync_SharedVaultFileMovedEventHandler'),
   // Services
   // Services
   Sync_ContentDecoder: Symbol.for('Sync_ContentDecoder'),
   Sync_ContentDecoder: Symbol.for('Sync_ContentDecoder'),
   Sync_DomainEventPublisher: Symbol.for('Sync_DomainEventPublisher'),
   Sync_DomainEventPublisher: Symbol.for('Sync_DomainEventPublisher'),

+ 90 - 0
packages/syncing-server/src/Domain/Handler/SharedVaultFileMovedEventHandler.ts

@@ -0,0 +1,90 @@
+import { DomainEventHandlerInterface, SharedVaultFileMovedEvent } from '@standardnotes/domain-events'
+import { NotificationPayload, NotificationType, Uuid } from '@standardnotes/domain-core'
+import { Logger } from 'winston'
+
+import { UpdateStorageQuotaUsedInSharedVault } from '../UseCase/SharedVaults/UpdateStorageQuotaUsedInSharedVault/UpdateStorageQuotaUsedInSharedVault'
+import { AddNotificationsForUsers } from '../UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers'
+
+export class SharedVaultFileMovedEventHandler implements DomainEventHandlerInterface {
+  constructor(
+    private updateStorageQuotaUsedInSharedVaultUseCase: UpdateStorageQuotaUsedInSharedVault,
+    private addNotificationsForUsers: AddNotificationsForUsers,
+    private logger: Logger,
+  ) {}
+
+  async handle(event: SharedVaultFileMovedEvent): Promise<void> {
+    if (event.payload.from.sharedVaultUuid !== undefined) {
+      const sharedVaultUuidOrError = Uuid.create(event.payload.from.sharedVaultUuid)
+      if (sharedVaultUuidOrError.isFailed()) {
+        this.logger.error(sharedVaultUuidOrError.getError())
+
+        return
+      }
+      const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+
+      const subtractResult = await this.updateStorageQuotaUsedInSharedVaultUseCase.execute({
+        sharedVaultUuid: sharedVaultUuid.value,
+        bytesUsed: -event.payload.fileByteSize,
+      })
+
+      if (subtractResult.isFailed()) {
+        this.logger.error(`Failed to update storage quota used in shared vault: ${subtractResult.getError()}`)
+
+        return
+      }
+
+      const notificationPayload = NotificationPayload.create({
+        sharedVaultUuid: sharedVaultUuid,
+        type: NotificationType.create(NotificationType.TYPES.SharedVaultFileRemoved).getValue(),
+        version: '1.0',
+      }).getValue()
+
+      const notificationResult = await this.addNotificationsForUsers.execute({
+        sharedVaultUuid: sharedVaultUuid.value,
+        type: NotificationType.TYPES.SharedVaultFileRemoved,
+        payload: notificationPayload,
+        version: '1.0',
+      })
+      if (notificationResult.isFailed()) {
+        this.logger.error(`Failed to add notification for users: ${notificationResult.getError()}`)
+      }
+    }
+
+    if (event.payload.to.sharedVaultUuid !== undefined) {
+      const sharedVaultUuidOrError = Uuid.create(event.payload.to.sharedVaultUuid)
+      if (sharedVaultUuidOrError.isFailed()) {
+        this.logger.error(sharedVaultUuidOrError.getError())
+
+        return
+      }
+      const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+
+      const addResult = await this.updateStorageQuotaUsedInSharedVaultUseCase.execute({
+        sharedVaultUuid: sharedVaultUuid.value,
+        bytesUsed: event.payload.fileByteSize,
+      })
+
+      if (addResult.isFailed()) {
+        this.logger.error(`Failed to update storage quota used in shared vault: ${addResult.getError()}`)
+
+        return
+      }
+
+      const notificationPayload = NotificationPayload.create({
+        sharedVaultUuid: sharedVaultUuid,
+        type: NotificationType.create(NotificationType.TYPES.SharedVaultFileUploaded).getValue(),
+        version: '1.0',
+      }).getValue()
+
+      const notificationResult = await this.addNotificationsForUsers.execute({
+        sharedVaultUuid: sharedVaultUuid.value,
+        type: NotificationType.TYPES.SharedVaultFileUploaded,
+        payload: notificationPayload,
+        version: '1.0',
+      })
+      if (notificationResult.isFailed()) {
+        this.logger.error(`Failed to add notification for users: ${notificationResult.getError()}`)
+      }
+    }
+  }
+}

+ 17 - 0
packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.spec.ts

@@ -256,6 +256,23 @@ describe('CreateSharedVaultFileValetToken', () => {
       expect(result.getError()).toBe('User does not have permission to perform this operation')
       expect(result.getError()).toBe('User does not have permission to perform this operation')
     })
     })
 
 
+    it('should return error when target shared vault does not exist for shared-vault-to-shared-vault move operation', async () => {
+      sharedVaultRepository.findByUuid = jest.fn().mockResolvedValueOnce(sharedVault).mockResolvedValueOnce(null)
+
+      const useCase = createUseCase()
+      const result = await useCase.execute({
+        userUuid: '00000000-0000-0000-0000-000000000000',
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+        remoteIdentifier: 'remote-identifier',
+        operation: ValetTokenOperation.Move,
+        moveOperationType: 'shared-vault-to-shared-vault',
+        sharedVaultToSharedVaultMoveTargetUuid: '00000000-0000-0000-0000-000000000000',
+      })
+
+      expect(result.isFailed()).toBe(true)
+      expect(result.getError()).toBe('Target shared vault not found')
+    })
+
     it('should create move valet token for shared-vault-to-shared-vault operation', async () => {
     it('should create move valet token for shared-vault-to-shared-vault operation', async () => {
       sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
       sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
         .fn()
         .fn()

+ 46 - 31
packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.ts

@@ -1,9 +1,15 @@
-import { SharedVaultValetTokenData, TokenEncoderInterface, ValetTokenOperation } from '@standardnotes/security'
+import {
+  SharedVaultMoveType,
+  SharedVaultValetTokenData,
+  TokenEncoderInterface,
+  ValetTokenOperation,
+} from '@standardnotes/security'
 import { Result, SharedVaultUserPermission, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
 import { Result, SharedVaultUserPermission, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
 
 
 import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
 import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
 import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
 import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
 import { CreateSharedVaultFileValetTokenDTO } from './CreateSharedVaultFileValetTokenDTO'
 import { CreateSharedVaultFileValetTokenDTO } from './CreateSharedVaultFileValetTokenDTO'
+import { SharedVault } from '../../../SharedVault/SharedVault'
 
 
 export class CreateSharedVaultFileValetToken implements UseCaseInterface<string> {
 export class CreateSharedVaultFileValetToken implements UseCaseInterface<string> {
   constructor(
   constructor(
@@ -48,6 +54,7 @@ export class CreateSharedVaultFileValetToken implements UseCaseInterface<string>
       return Result.fail('User does not have permission to perform this operation')
       return Result.fail('User does not have permission to perform this operation')
     }
     }
 
 
+    let targetSharedVault: SharedVault | null = null
     if (dto.operation === ValetTokenOperation.Move) {
     if (dto.operation === ValetTokenOperation.Move) {
       if (!dto.moveOperationType) {
       if (!dto.moveOperationType) {
         return Result.fail('Move operation type is required')
         return Result.fail('Move operation type is required')
@@ -64,6 +71,11 @@ export class CreateSharedVaultFileValetToken implements UseCaseInterface<string>
         }
         }
         const sharedVaultTargetUuid = sharedVaultTargetUuidOrError.getValue()
         const sharedVaultTargetUuid = sharedVaultTargetUuidOrError.getValue()
 
 
+        targetSharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultTargetUuid)
+        if (!targetSharedVault) {
+          return Result.fail('Target shared vault not found')
+        }
+
         const toSharedVaultUser = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({
         const toSharedVaultUser = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({
           userUuid: userUuid,
           userUuid: userUuid,
           sharedVaultUuid: sharedVaultTargetUuid,
           sharedVaultUuid: sharedVaultTargetUuid,
@@ -83,6 +95,28 @@ export class CreateSharedVaultFileValetToken implements UseCaseInterface<string>
       }
       }
     }
     }
 
 
+    const fromSharedVaultUuid = ['shared-vault-to-user', 'shared-vault-to-shared-vault'].includes(
+      dto.moveOperationType as string,
+    )
+      ? sharedVaultUuid.value
+      : undefined
+
+    const fromOwnerUuid =
+      dto.moveOperationType === 'user-to-shared-vault' ? userUuid.value : sharedVault.props.userUuid.value
+
+    const toSharedVaultUuid = targetSharedVault
+      ? targetSharedVault.id.toString()
+      : dto.moveOperationType === 'shared-vault-to-user'
+      ? undefined
+      : sharedVaultUuid.value
+
+    const toOwnerUuid =
+      dto.moveOperationType === 'user-to-shared-vault'
+        ? sharedVault.props.userUuid.value
+        : targetSharedVault
+        ? targetSharedVault.props.userUuid.value
+        : userUuid.value
+
     const tokenData: SharedVaultValetTokenData = {
     const tokenData: SharedVaultValetTokenData = {
       sharedVaultUuid: dto.sharedVaultUuid,
       sharedVaultUuid: dto.sharedVaultUuid,
       vaultOwnerUuid: sharedVault.props.userUuid.value,
       vaultOwnerUuid: sharedVault.props.userUuid.value,
@@ -91,40 +125,21 @@ export class CreateSharedVaultFileValetToken implements UseCaseInterface<string>
       uploadBytesUsed: sharedVault.props.fileUploadBytesUsed,
       uploadBytesUsed: sharedVault.props.fileUploadBytesUsed,
       uploadBytesLimit: dto.sharedVaultOwnerUploadBytesLimit,
       uploadBytesLimit: dto.sharedVaultOwnerUploadBytesLimit,
       unencryptedFileSize: dto.unencryptedFileSize,
       unencryptedFileSize: dto.unencryptedFileSize,
-      moveOperation: this.createMoveOperationData(dto),
+      moveOperation: {
+        type: dto.moveOperationType as SharedVaultMoveType,
+        from: {
+          sharedVaultUuid: fromSharedVaultUuid,
+          ownerUuid: fromOwnerUuid,
+        },
+        to: {
+          sharedVaultUuid: toSharedVaultUuid,
+          ownerUuid: toOwnerUuid,
+        },
+      },
     }
     }
 
 
     const valetToken = this.tokenEncoder.encodeExpirableToken(tokenData, this.valetTokenTTL)
     const valetToken = this.tokenEncoder.encodeExpirableToken(tokenData, this.valetTokenTTL)
 
 
     return Result.ok(valetToken)
     return Result.ok(valetToken)
   }
   }
-
-  private createMoveOperationData(dto: CreateSharedVaultFileValetTokenDTO): SharedVaultValetTokenData['moveOperation'] {
-    if (!dto.moveOperationType) {
-      return undefined
-    }
-
-    let fromUuid: string
-    let toUuid: string
-    switch (dto.moveOperationType) {
-      case 'shared-vault-to-user':
-        fromUuid = dto.sharedVaultUuid
-        toUuid = dto.userUuid
-        break
-      case 'user-to-shared-vault':
-        fromUuid = dto.userUuid
-        toUuid = dto.sharedVaultUuid
-        break
-      case 'shared-vault-to-shared-vault':
-        fromUuid = dto.sharedVaultUuid
-        toUuid = dto.sharedVaultToSharedVaultMoveTargetUuid as string
-        break
-    }
-
-    return {
-      type: dto.moveOperationType,
-      fromUuid,
-      toUuid,
-    }
-  }
 }
 }

+ 1 - 0
packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetTokenDTO.ts

@@ -10,4 +10,5 @@ export interface CreateSharedVaultFileValetTokenDTO {
   unencryptedFileSize?: number
   unencryptedFileSize?: number
   moveOperationType?: SharedVaultMoveType
   moveOperationType?: SharedVaultMoveType
   sharedVaultToSharedVaultMoveTargetUuid?: string
   sharedVaultToSharedVaultMoveTargetUuid?: string
+  sharedVaultToSharedVaultMoveTargetOwnerUuid?: string
 }
 }