mirror of
https://github.com/standardnotes/server
synced 2026-02-08 08:01:16 -05:00
Compare commits
2 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b510284e01 | ||
|
|
205a1ed637 |
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.25.16](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.15...@standardnotes/analytics@2.25.16) (2023-08-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.25.15](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.14...@standardnotes/analytics@2.25.15) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.25.15",
|
||||
"version": "2.25.16",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.71.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.71.0...@standardnotes/api-gateway@1.71.1) (2023-08-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
# [1.71.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.5...@standardnotes/api-gateway@1.71.0) (2023-08-22)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.71.0",
|
||||
"version": "1.71.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.134.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.133.0...@standardnotes/auth-server@1.134.0) (2023-08-23)
|
||||
|
||||
### Features
|
||||
|
||||
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
|
||||
|
||||
# [1.133.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.132.0...@standardnotes/auth-server@1.133.0) (2023-08-22)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.133.0",
|
||||
"version": "1.134.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -256,6 +256,7 @@ import { PaymentsAccountDeletedEventHandler } from '../Domain/Handler/PaymentsAc
|
||||
import { UpdateStorageQuotaUsedForUser } from '../Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
|
||||
import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler'
|
||||
import { SharedVaultFileRemovedEventHandler } from '../Domain/Handler/SharedVaultFileRemovedEventHandler'
|
||||
import { SharedVaultFileMovedEventHandler } from '../Domain/Handler/SharedVaultFileMovedEventHandler'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(configuration?: {
|
||||
@@ -982,6 +983,14 @@ export class ContainerConfigLoader {
|
||||
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
|
||||
.bind<FileRemovedEventHandler>(TYPES.Auth_FileRemovedEventHandler)
|
||||
.toConstantValue(
|
||||
@@ -1045,6 +1054,7 @@ export class ContainerConfigLoader {
|
||||
['USER_EMAIL_CHANGED', container.get(TYPES.Auth_UserEmailChangedEventHandler)],
|
||||
['FILE_UPLOADED', container.get(TYPES.Auth_FileUploadedEventHandler)],
|
||||
['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)],
|
||||
['SHARED_VAULT_FILE_REMOVED', container.get(TYPES.Auth_SharedVaultFileRemovedEventHandler)],
|
||||
['LISTED_ACCOUNT_CREATED', container.get(TYPES.Auth_ListedAccountCreatedEventHandler)],
|
||||
|
||||
@@ -27,6 +27,7 @@ export class Service implements AuthServiceInterface {
|
||||
async activatePremiumFeatures(dto: {
|
||||
username: string
|
||||
subscriptionPlanName?: string
|
||||
uploadBytesLimit?: number
|
||||
endsAt?: Date
|
||||
}): Promise<Result<string>> {
|
||||
if (!this.container) {
|
||||
|
||||
@@ -168,6 +168,7 @@ const TYPES = {
|
||||
Auth_UserEmailChangedEventHandler: Symbol.for('Auth_UserEmailChangedEventHandler'),
|
||||
Auth_FileUploadedEventHandler: Symbol.for('Auth_FileUploadedEventHandler'),
|
||||
Auth_SharedVaultFileUploadedEventHandler: Symbol.for('Auth_SharedVaultFileUploadedEventHandler'),
|
||||
Auth_SharedVaultFileMovedEventHandler: Symbol.for('Auth_SharedVaultFileMovedEventHandler'),
|
||||
Auth_FileRemovedEventHandler: Symbol.for('Auth_FileRemovedEventHandler'),
|
||||
Auth_SharedVaultFileRemovedEventHandler: Symbol.for('Auth_SharedVaultFileRemovedEventHandler'),
|
||||
Auth_ListedAccountCreatedEventHandler: Symbol.for('Auth_ListedAccountCreatedEventHandler'),
|
||||
|
||||
@@ -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()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export class ActivatePremiumFeatures implements UseCaseInterface<string> {
|
||||
|
||||
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
|
||||
subscription,
|
||||
new Map([[SettingName.NAMES.FileUploadBytesLimit, '-1']]),
|
||||
new Map([[SettingName.NAMES.FileUploadBytesLimit, `${dto.uploadBytesLimit ?? -1}`]]),
|
||||
)
|
||||
|
||||
return Result.ok('Premium features activated.')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export interface ActivatePremiumFeaturesDTO {
|
||||
username: string
|
||||
subscriptionPlanName?: string
|
||||
uploadBytesLimit?: number
|
||||
endsAt?: Date
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.12.13](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.12...@standardnotes/domain-events-infra@1.12.13) (2023-08-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.12.12](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.11...@standardnotes/domain-events-infra@1.12.12) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.12.12",
|
||||
"version": "1.12.13",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.116.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.115.1...@standardnotes/domain-events@2.116.0) (2023-08-23)
|
||||
|
||||
### Features
|
||||
|
||||
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
|
||||
|
||||
## [2.115.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.115.0...@standardnotes/domain-events@2.115.1) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.115.1",
|
||||
"version": "2.116.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { SharedVaultFileMovedEventPayload } from './SharedVaultFileMovedEventPayload'
|
||||
|
||||
export interface SharedVaultFileMovedEvent extends DomainEventInterface {
|
||||
type: 'SHARED_VAULT_FILE_MOVED'
|
||||
payload: SharedVaultFileMovedEventPayload
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,8 @@ export * from './Event/SharedSubscriptionInvitationCanceledEvent'
|
||||
export * from './Event/SharedSubscriptionInvitationCanceledEventPayload'
|
||||
export * from './Event/SharedSubscriptionInvitationCreatedEvent'
|
||||
export * from './Event/SharedSubscriptionInvitationCreatedEventPayload'
|
||||
export * from './Event/SharedVaultFileMovedEvent'
|
||||
export * from './Event/SharedVaultFileMovedEventPayload'
|
||||
export * from './Event/SharedVaultFileRemovedEvent'
|
||||
export * from './Event/SharedVaultFileRemovedEventPayload'
|
||||
export * from './Event/SharedVaultFileUploadedEvent'
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.11.22](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.21...@standardnotes/event-store@1.11.22) (2023-08-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.11.21](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.20...@standardnotes/event-store@1.11.21) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.11.21",
|
||||
"version": "1.11.22",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.22.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.21.0...@standardnotes/files-server@1.22.0) (2023-08-23)
|
||||
|
||||
### Features
|
||||
|
||||
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/files/issues/705)) ([205a1ed](https://github.com/standardnotes/files/commit/205a1ed637b626be13fc656276508f3c7791024f))
|
||||
|
||||
# [1.21.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.4...@standardnotes/files-server@1.21.0) (2023-08-22)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.21.0",
|
||||
"version": "1.22.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -99,7 +99,9 @@ export class ContainerConfigLoader {
|
||||
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)
|
||||
container
|
||||
.bind<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory)
|
||||
.toConstantValue(new DomainEventFactory(container.get<TimerInterface>(TYPES.Files_Timer)))
|
||||
|
||||
if (isConfiguredForInMemoryCache) {
|
||||
container
|
||||
@@ -214,9 +216,26 @@ export class ContainerConfigLoader {
|
||||
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<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)
|
||||
|
||||
// middleware
|
||||
|
||||
@@ -4,16 +4,14 @@ import {
|
||||
DomainEventService,
|
||||
SharedVaultFileUploadedEvent,
|
||||
SharedVaultFileRemovedEvent,
|
||||
SharedVaultFileMovedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(@inject(TYPES.Files_Timer) private timer: TimerInterface) {}
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
createFileRemovedEvent(payload: {
|
||||
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: {
|
||||
sharedVaultUuid: string
|
||||
vaultOwnerUuid: string
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
FileRemovedEvent,
|
||||
SharedVaultFileRemovedEvent,
|
||||
SharedVaultFileUploadedEvent,
|
||||
SharedVaultFileMovedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
|
||||
export interface DomainEventFactoryInterface {
|
||||
@@ -19,6 +20,20 @@ export interface DomainEventFactoryInterface {
|
||||
fileByteSize: number
|
||||
regularSubscriptionUuid: string
|
||||
}): FileRemovedEvent
|
||||
createSharedVaultFileMovedEvent(payload: {
|
||||
fileByteSize: number
|
||||
fileName: string
|
||||
from: {
|
||||
sharedVaultUuid?: string
|
||||
ownerUuid: string
|
||||
filePath: string
|
||||
}
|
||||
to: {
|
||||
sharedVaultUuid?: string
|
||||
ownerUuid: string
|
||||
filePath: string
|
||||
}
|
||||
}): SharedVaultFileMovedEvent
|
||||
createSharedVaultFileUploadedEvent(payload: {
|
||||
sharedVaultUuid: string
|
||||
vaultOwnerUuid: string
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'reflect-metadata'
|
||||
import { Logger } from 'winston'
|
||||
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
|
||||
|
||||
@@ -19,10 +18,8 @@ describe('GetFileMetadata', () => {
|
||||
})
|
||||
|
||||
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 () => {
|
||||
@@ -30,9 +27,8 @@ describe('GetFileMetadata', () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,32 +1,20 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
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 {
|
||||
const size = await this.fileDownloader.getFileSize(`${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
size,
|
||||
}
|
||||
return Result.ok(size)
|
||||
} catch (error) {
|
||||
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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export type GetFileMetadataResponse =
|
||||
| {
|
||||
success: true
|
||||
size: number
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
message: string
|
||||
}
|
||||
@@ -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 { MoveFile } from './MoveFile'
|
||||
import { FileMoverInterface } from '../../Services/FileMoverInterface'
|
||||
import { GetFileMetadata } from '../GetFileMetadata/GetFileMetadata'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
|
||||
describe('MoveFile', () => {
|
||||
let fileMover: FileMoverInterface
|
||||
let getFileMetadataUseCase: GetFileMetadata
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () => new MoveFile(fileMover, logger)
|
||||
const createUseCase = () =>
|
||||
new MoveFile(getFileMetadataUseCase, fileMover, domainEventPublisher, domainEventFactory, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
getFileMetadataUseCase = {} as jest.Mocked<GetFileMetadata>
|
||||
getFileMetadataUseCase.execute = jest.fn().mockReturnValue(Result.ok(1234))
|
||||
|
||||
fileMover = {} as jest.Mocked<FileMoverInterface>
|
||||
fileMover.moveFile = jest.fn().mockReturnValue(413)
|
||||
|
||||
@@ -20,17 +29,34 @@ describe('MoveFile', () => {
|
||||
logger.debug = jest.fn()
|
||||
logger.error = 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 () => {
|
||||
await createUseCase().execute({
|
||||
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 () => {
|
||||
@@ -40,11 +66,174 @@ describe('MoveFile', () => {
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
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',
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,27 +1,97 @@
|
||||
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 { FileMoverInterface } from '../../Services/FileMoverInterface'
|
||||
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> {
|
||||
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>> {
|
||||
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}`)
|
||||
|
||||
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.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()
|
||||
} catch (error) {
|
||||
this.logger.error(`Could not move resource: ${dto.resourceRemoteIdentifier} - ${(error as Error).message}`)
|
||||
|
||||
@@ -2,7 +2,13 @@ import { SharedVaultMoveType } from '@standardnotes/security'
|
||||
|
||||
export interface MoveFileDTO {
|
||||
moveType: SharedVaultMoveType
|
||||
fromUuid: string
|
||||
toUuid: string
|
||||
from: {
|
||||
sharedVaultUuid?: string
|
||||
ownerUuid: string
|
||||
}
|
||||
to: {
|
||||
sharedVaultUuid?: string
|
||||
ownerUuid: string
|
||||
}
|
||||
resourceRemoteIdentifier: string
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('AnnotatedFilesController', () => {
|
||||
finishUploadSession.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
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.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
@@ -183,7 +183,7 @@ describe('AnnotatedFilesController', () => {
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -146,20 +146,21 @@ export class AnnotatedFilesController extends BaseHttpController {
|
||||
chunkSize = this.maxChunkBytes
|
||||
}
|
||||
|
||||
const fileMetadata = await this.getFileMetadata.execute({
|
||||
const fileMetadataOrError = await this.getFileMetadata.execute({
|
||||
ownerUuid: response.locals.userUuid,
|
||||
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 endRange = Math.min(startRange + chunkSize - 1, fileMetadata.size - 1)
|
||||
const endRange = Math.min(startRange + chunkSize - 1, fileSize - 1)
|
||||
|
||||
const headers = {
|
||||
'Content-Range': `bytes ${startRange}-${endRange}/${fileMetadata.size}`,
|
||||
'Content-Range': `bytes ${startRange}-${endRange}/${fileSize}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': endRange - startRange + 1,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
|
||||
@@ -47,8 +47,8 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController {
|
||||
|
||||
const result = await this.moveFile.execute({
|
||||
moveType: moveOperation.type,
|
||||
fromUuid: moveOperation.fromUuid,
|
||||
toUuid: moveOperation.toUuid,
|
||||
from: moveOperation.from,
|
||||
to: moveOperation.to,
|
||||
resourceRemoteIdentifier: locals.remoteIdentifier,
|
||||
})
|
||||
|
||||
@@ -187,20 +187,21 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController {
|
||||
chunkSize = this.maxChunkBytes
|
||||
}
|
||||
|
||||
const fileMetadata = await this.getFileMetadata.execute({
|
||||
const fileMetadataOrError = await this.getFileMetadata.execute({
|
||||
ownerUuid: locals.sharedVaultUuid,
|
||||
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 endRange = Math.min(startRange + chunkSize - 1, fileMetadata.size - 1)
|
||||
const endRange = Math.min(startRange + chunkSize - 1, fileSize - 1)
|
||||
|
||||
const headers = {
|
||||
'Content-Range': `bytes ${startRange}-${endRange}/${fileMetadata.size}`,
|
||||
'Content-Range': `bytes ${startRange}-${endRange}/${fileSize}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': endRange - startRange + 1,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.15.0](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.14.2...@standardnotes/home-server@1.15.0) (2023-08-23)
|
||||
|
||||
### Features
|
||||
|
||||
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
|
||||
|
||||
## [1.14.2](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.14.1...@standardnotes/home-server@1.14.2) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/home-server",
|
||||
"version": "1.14.2",
|
||||
"version": "1.15.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -144,6 +144,7 @@ export class HomeServer implements HomeServerInterface {
|
||||
void this.activatePremiumFeatures({
|
||||
username: request.body.username,
|
||||
subscriptionPlanName: request.body.subscriptionPlanName,
|
||||
uploadBytesLimit: request.body.uploadBytesLimit,
|
||||
endsAt: request.body.endsAt ? new Date(request.body.endsAt) : undefined,
|
||||
}).then((result) => {
|
||||
if (result.isFailed()) {
|
||||
@@ -221,6 +222,7 @@ export class HomeServer implements HomeServerInterface {
|
||||
async activatePremiumFeatures(dto: {
|
||||
username: string
|
||||
subscriptionPlanName?: string
|
||||
uploadBytesLimit?: number
|
||||
endsAt?: Date
|
||||
}): Promise<Result<string>> {
|
||||
if (!this.isRunning() || !this.authService) {
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface HomeServerInterface {
|
||||
activatePremiumFeatures(dto: {
|
||||
username: string
|
||||
subscriptionPlanName?: string
|
||||
uploadBytesLimit?: number
|
||||
endsAt?: Date
|
||||
}): Promise<Result<string>>
|
||||
stop(): Promise<Result<string>>
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.26.10](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.9...@standardnotes/revisions-server@1.26.10) (2023-08-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.26.9](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.8...@standardnotes/revisions-server@1.26.9) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.26.9",
|
||||
"version": "1.26.10",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.20.26](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.25...@standardnotes/scheduler-server@1.20.26) (2023-08-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.25](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.24...@standardnotes/scheduler-server@1.20.25) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.20.25",
|
||||
"version": "1.20.26",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.11.0](https://github.com/standardnotes/server/compare/@standardnotes/security@1.10.0...@standardnotes/security@1.11.0) (2023-08-23)
|
||||
|
||||
### Features
|
||||
|
||||
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
|
||||
|
||||
# [1.10.0](https://github.com/standardnotes/server/compare/@standardnotes/security@1.9.0...@standardnotes/security@1.10.0) (2023-08-22)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/security",
|
||||
"version": "1.10.0",
|
||||
"version": "1.11.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -11,7 +11,13 @@ export interface SharedVaultValetTokenData {
|
||||
uploadBytesLimit?: number
|
||||
moveOperation?: {
|
||||
type: SharedVaultMoveType
|
||||
fromUuid: string
|
||||
toUuid: string
|
||||
from: {
|
||||
sharedVaultUuid?: string
|
||||
ownerUuid: string
|
||||
}
|
||||
to: {
|
||||
sharedVaultUuid?: string
|
||||
ownerUuid: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.83.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.82.0...@standardnotes/syncing-server@1.83.0) (2023-08-23)
|
||||
|
||||
### Features
|
||||
|
||||
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/syncing-server-js/issues/705)) ([205a1ed](https://github.com/standardnotes/syncing-server-js/commit/205a1ed637b626be13fc656276508f3c7791024f))
|
||||
|
||||
# [1.82.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.81.0...@standardnotes/syncing-server@1.82.0) (2023-08-22)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.82.0",
|
||||
"version": "1.83.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -155,6 +155,7 @@ import { Logger } from 'winston'
|
||||
import { ItemRepositoryResolverInterface } from '../Domain/Item/ItemRepositoryResolverInterface'
|
||||
import { TypeORMItemRepositoryResolver } from '../Infra/TypeORM/TypeORMItemRepositoryResolver'
|
||||
import { TransitionItemsFromPrimaryToSecondaryDatabaseForUser } from '../Domain/UseCase/Transition/TransitionItemsFromPrimaryToSecondaryDatabaseForUser/TransitionItemsFromPrimaryToSecondaryDatabaseForUser'
|
||||
import { SharedVaultFileMovedEventHandler } from '../Domain/Handler/SharedVaultFileMovedEventHandler'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
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
|
||||
.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
|
||||
container.bind<ContentDecoder>(TYPES.Sync_ContentDecoder).toDynamicValue(() => new ContentDecoder())
|
||||
@@ -902,6 +912,10 @@ export class ContainerConfigLoader {
|
||||
'SHARED_VAULT_FILE_REMOVED',
|
||||
container.get<SharedVaultFileRemovedEventHandler>(TYPES.Sync_SharedVaultFileRemovedEventHandler),
|
||||
],
|
||||
[
|
||||
'SHARED_VAULT_FILE_MOVED',
|
||||
container.get<SharedVaultFileMovedEventHandler>(TYPES.Sync_SharedVaultFileMovedEventHandler),
|
||||
],
|
||||
])
|
||||
if (!isConfiguredForHomeServer) {
|
||||
container.bind(TYPES.Sync_AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL'))
|
||||
|
||||
@@ -90,6 +90,7 @@ const TYPES = {
|
||||
Sync_ItemRevisionCreationRequestedEventHandler: Symbol.for('Sync_ItemRevisionCreationRequestedEventHandler'),
|
||||
Sync_SharedVaultFileRemovedEventHandler: Symbol.for('Sync_SharedVaultFileRemovedEventHandler'),
|
||||
Sync_SharedVaultFileUploadedEventHandler: Symbol.for('Sync_SharedVaultFileUploadedEventHandler'),
|
||||
Sync_SharedVaultFileMovedEventHandler: Symbol.for('Sync_SharedVaultFileMovedEventHandler'),
|
||||
// Services
|
||||
Sync_ContentDecoder: Symbol.for('Sync_ContentDecoder'),
|
||||
Sync_DomainEventPublisher: Symbol.for('Sync_DomainEventPublisher'),
|
||||
|
||||
@@ -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()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,6 +256,23 @@ describe('CreateSharedVaultFileValetToken', () => {
|
||||
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 () => {
|
||||
sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
|
||||
.fn()
|
||||
|
||||
@@ -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 { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
|
||||
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
|
||||
import { CreateSharedVaultFileValetTokenDTO } from './CreateSharedVaultFileValetTokenDTO'
|
||||
import { SharedVault } from '../../../SharedVault/SharedVault'
|
||||
|
||||
export class CreateSharedVaultFileValetToken implements UseCaseInterface<string> {
|
||||
constructor(
|
||||
@@ -48,6 +54,7 @@ export class CreateSharedVaultFileValetToken implements UseCaseInterface<string>
|
||||
return Result.fail('User does not have permission to perform this operation')
|
||||
}
|
||||
|
||||
let targetSharedVault: SharedVault | null = null
|
||||
if (dto.operation === ValetTokenOperation.Move) {
|
||||
if (!dto.moveOperationType) {
|
||||
return Result.fail('Move operation type is required')
|
||||
@@ -64,6 +71,11 @@ export class CreateSharedVaultFileValetToken implements UseCaseInterface<string>
|
||||
}
|
||||
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({
|
||||
userUuid: userUuid,
|
||||
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 = {
|
||||
sharedVaultUuid: dto.sharedVaultUuid,
|
||||
vaultOwnerUuid: sharedVault.props.userUuid.value,
|
||||
@@ -91,40 +125,21 @@ export class CreateSharedVaultFileValetToken implements UseCaseInterface<string>
|
||||
uploadBytesUsed: sharedVault.props.fileUploadBytesUsed,
|
||||
uploadBytesLimit: dto.sharedVaultOwnerUploadBytesLimit,
|
||||
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)
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,4 +10,5 @@ export interface CreateSharedVaultFileValetTokenDTO {
|
||||
unencryptedFileSize?: number
|
||||
moveOperationType?: SharedVaultMoveType
|
||||
sharedVaultToSharedVaultMoveTargetUuid?: string
|
||||
sharedVaultToSharedVaultMoveTargetOwnerUuid?: string
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.10.20](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.19...@standardnotes/websockets-server@1.10.20) (2023-08-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.10.19](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.10.18...@standardnotes/websockets-server@1.10.19) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/websockets-server",
|
||||
"version": "1.10.19",
|
||||
"version": "1.10.20",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user