feat(syncing-server): add procedure to recalculate content sizes (#1027)
This commit is contained in:
parent
c00c7becae
commit
70bbf11db5
10 changed files with 261 additions and 0 deletions
50
packages/syncing-server/bin/content_size.ts
Normal file
50
packages/syncing-server/bin/content_size.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import 'reflect-metadata'
|
||||||
|
|
||||||
|
import { Logger } from 'winston'
|
||||||
|
|
||||||
|
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||||
|
import TYPES from '../src/Bootstrap/Types'
|
||||||
|
import { Env } from '../src/Bootstrap/Env'
|
||||||
|
import { FixContentSizes } from '../src/Domain/UseCase/Syncing/FixContentSizes/FixContentSizes'
|
||||||
|
import { Result } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
const inputArgs = process.argv.slice(2)
|
||||||
|
const userUuid = inputArgs[0]
|
||||||
|
|
||||||
|
const container = new ContainerConfigLoader('worker')
|
||||||
|
void container.load().then((container) => {
|
||||||
|
const env: Env = new Env()
|
||||||
|
env.load()
|
||||||
|
|
||||||
|
const logger: Logger = container.get(TYPES.Sync_Logger)
|
||||||
|
|
||||||
|
logger.info('Starting fixing of content sizes', {
|
||||||
|
userId: userUuid,
|
||||||
|
})
|
||||||
|
|
||||||
|
const fixContentSizes = container.get<FixContentSizes>(TYPES.Sync_FixContentSizes)
|
||||||
|
|
||||||
|
Promise.resolve(fixContentSizes.execute({ userUuid }))
|
||||||
|
.then((result: Result<void>) => {
|
||||||
|
if (result.isFailed()) {
|
||||||
|
logger.error(`Error while fixing content sizes: ${result.getError()}`, {
|
||||||
|
userId: userUuid,
|
||||||
|
})
|
||||||
|
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Finished fixing of content sizes', {
|
||||||
|
userId: userUuid,
|
||||||
|
})
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(`Error while fixing content sizes: ${error.message}`, {
|
||||||
|
userId: userUuid,
|
||||||
|
})
|
||||||
|
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
})
|
11
packages/syncing-server/docker/entrypoint-content-size.js
Normal file
11
packages/syncing-server/docker/entrypoint-content-size.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const pnp = require(path.normalize(path.resolve(__dirname, '../../..', '.pnp.cjs'))).setup()
|
||||||
|
|
||||||
|
const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/content_size.js')))
|
||||||
|
|
||||||
|
Object.defineProperty(exports, '__esModule', { value: true })
|
||||||
|
|
||||||
|
exports.default = index
|
|
@ -16,6 +16,11 @@ case "$COMMAND" in
|
||||||
exec node docker/entrypoint-statistics.js
|
exec node docker/entrypoint-statistics.js
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
'content-size' )
|
||||||
|
EMAIL=$1 && shift 1
|
||||||
|
exec node docker/entrypoint-content-size.js $EMAIL
|
||||||
|
;;
|
||||||
|
|
||||||
* )
|
* )
|
||||||
echo "[Docker] Unknown command"
|
echo "[Docker] Unknown command"
|
||||||
;;
|
;;
|
||||||
|
|
|
@ -167,6 +167,7 @@ import { MetricsStoreInterface } from '../Domain/Metrics/MetricsStoreInterface'
|
||||||
import { RedisMetricStore } from '../Infra/Redis/RedisMetricStore'
|
import { RedisMetricStore } from '../Infra/Redis/RedisMetricStore'
|
||||||
import { DummyMetricStore } from '../Infra/Dummy/DummyMetricStore'
|
import { DummyMetricStore } from '../Infra/Dummy/DummyMetricStore'
|
||||||
import { CheckForTrafficAbuse } from '../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
|
import { CheckForTrafficAbuse } from '../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
|
||||||
|
import { FixContentSizes } from '../Domain/UseCase/Syncing/FixContentSizes/FixContentSizes'
|
||||||
|
|
||||||
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
|
||||||
|
@ -955,6 +956,14 @@ export class ContainerConfigLoader {
|
||||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
container
|
||||||
|
.bind<FixContentSizes>(TYPES.Sync_FixContentSizes)
|
||||||
|
.toConstantValue(
|
||||||
|
new FixContentSizes(
|
||||||
|
container.get<ItemRepositoryInterface>(TYPES.Sync_SQLItemRepository),
|
||||||
|
container.get<Logger>(TYPES.Sync_Logger),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
container
|
container
|
||||||
|
|
|
@ -97,6 +97,7 @@ const TYPES = {
|
||||||
Sync_TransferSharedVaultItems: Symbol.for('Sync_TransferSharedVaultItems'),
|
Sync_TransferSharedVaultItems: Symbol.for('Sync_TransferSharedVaultItems'),
|
||||||
Sync_DumpItem: Symbol.for('Sync_DumpItem'),
|
Sync_DumpItem: Symbol.for('Sync_DumpItem'),
|
||||||
Sync_CheckForTrafficAbuse: Symbol.for('Sync_CheckForTrafficAbuse'),
|
Sync_CheckForTrafficAbuse: Symbol.for('Sync_CheckForTrafficAbuse'),
|
||||||
|
Sync_FixContentSizes: Symbol.for('Sync_FixContentSizes'),
|
||||||
// Handlers
|
// Handlers
|
||||||
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
|
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
|
||||||
Sync_AccountDeletionVerificationRequestedEventHandler: Symbol.for(
|
Sync_AccountDeletionVerificationRequestedEventHandler: Symbol.for(
|
||||||
|
|
|
@ -249,4 +249,25 @@ describe('Item', () => {
|
||||||
|
|
||||||
expect(entity.isIdenticalTo(otherEntity)).toBeFalsy()
|
expect(entity.isIdenticalTo(otherEntity)).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should calculate content size of the item', () => {
|
||||||
|
const entity = Item.create(
|
||||||
|
{
|
||||||
|
duplicateOf: null,
|
||||||
|
itemsKeyId: 'items-key-id',
|
||||||
|
content: 'content',
|
||||||
|
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||||
|
encItemKey: 'enc-item-key',
|
||||||
|
authHash: 'auth-hash',
|
||||||
|
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||||
|
deleted: false,
|
||||||
|
updatedWithSession: null,
|
||||||
|
dates: Dates.create(new Date(123), new Date(123)).getValue(),
|
||||||
|
timestamps: Timestamps.create(123, 123).getValue(),
|
||||||
|
},
|
||||||
|
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||||
|
).getValue()
|
||||||
|
|
||||||
|
expect(entity.calculateContentSize()).toEqual(943)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,6 +16,10 @@ export class Item extends Aggregate<ItemProps> {
|
||||||
return Result.ok<Item>(new Item(props, id))
|
return Result.ok<Item>(new Item(props, id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
calculateContentSize(): number {
|
||||||
|
return Buffer.byteLength(JSON.stringify(this))
|
||||||
|
}
|
||||||
|
|
||||||
get uuid(): Uuid {
|
get uuid(): Uuid {
|
||||||
const uuidOrError = Uuid.create(this._id.toString())
|
const uuidOrError = Uuid.create(this._id.toString())
|
||||||
if (uuidOrError.isFailed()) {
|
if (uuidOrError.isFailed()) {
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { Logger } from 'winston'
|
||||||
|
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||||
|
import { FixContentSizes } from './FixContentSizes'
|
||||||
|
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
|
||||||
|
import { Item } from '../../../Item/Item'
|
||||||
|
|
||||||
|
describe('FixContentSizes', () => {
|
||||||
|
let itemRepository: ItemRepositoryInterface
|
||||||
|
let logger: Logger
|
||||||
|
|
||||||
|
const createUseCase = () => new FixContentSizes(itemRepository, logger)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const existingItem = Item.create(
|
||||||
|
{
|
||||||
|
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||||
|
updatedWithSession: null,
|
||||||
|
content: 'foobar',
|
||||||
|
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||||
|
encItemKey: null,
|
||||||
|
authHash: null,
|
||||||
|
itemsKeyId: null,
|
||||||
|
duplicateOf: null,
|
||||||
|
deleted: false,
|
||||||
|
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||||
|
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||||
|
},
|
||||||
|
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||||
|
).getValue()
|
||||||
|
|
||||||
|
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||||
|
itemRepository.findAll = jest.fn().mockReturnValue([existingItem])
|
||||||
|
itemRepository.countAll = jest.fn().mockReturnValue(1)
|
||||||
|
itemRepository.updateContentSize = jest.fn()
|
||||||
|
|
||||||
|
logger = {} as jest.Mocked<Logger>
|
||||||
|
logger.info = jest.fn()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fix content sizes', async () => {
|
||||||
|
const useCase = createUseCase()
|
||||||
|
|
||||||
|
const result = await useCase.execute({
|
||||||
|
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBeFalsy()
|
||||||
|
expect(itemRepository.updateContentSize).toHaveBeenCalledTimes(1)
|
||||||
|
expect(itemRepository.updateContentSize).toHaveBeenCalledWith('00000000-0000-0000-0000-000000000000', 947)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error if user uuid is invalid', async () => {
|
||||||
|
const useCase = createUseCase()
|
||||||
|
|
||||||
|
const result = await useCase.execute({
|
||||||
|
userUuid: 'invalid',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBeTruthy()
|
||||||
|
expect(result.getError()).toEqual('Given value is not a valid uuid: invalid')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should do nothing if the content size is correct', async () => {
|
||||||
|
const existingItem = Item.create(
|
||||||
|
{
|
||||||
|
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||||
|
updatedWithSession: null,
|
||||||
|
content: 'foobar',
|
||||||
|
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
|
||||||
|
contentSize: 947,
|
||||||
|
encItemKey: null,
|
||||||
|
authHash: null,
|
||||||
|
itemsKeyId: null,
|
||||||
|
duplicateOf: null,
|
||||||
|
deleted: false,
|
||||||
|
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
|
||||||
|
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
|
||||||
|
},
|
||||||
|
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
|
||||||
|
).getValue()
|
||||||
|
itemRepository.findAll = jest.fn().mockReturnValue([existingItem])
|
||||||
|
|
||||||
|
const useCase = createUseCase()
|
||||||
|
|
||||||
|
const result = await useCase.execute({
|
||||||
|
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBeFalsy()
|
||||||
|
expect(itemRepository.updateContentSize).toHaveBeenCalledTimes(0)
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||||
|
import { Logger } from 'winston'
|
||||||
|
|
||||||
|
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
|
||||||
|
|
||||||
|
import { FixContentSizesDTO } from './FixContentSizesDTO'
|
||||||
|
|
||||||
|
export class FixContentSizes implements UseCaseInterface<void> {
|
||||||
|
constructor(
|
||||||
|
private itemRepository: ItemRepositoryInterface,
|
||||||
|
private logger: Logger,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(dto: FixContentSizesDTO): Promise<Result<void>> {
|
||||||
|
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||||
|
if (userUuidOrError.isFailed()) {
|
||||||
|
return Result.fail(userUuidOrError.getError())
|
||||||
|
}
|
||||||
|
const userUuid = userUuidOrError.getValue()
|
||||||
|
|
||||||
|
const count = await this.itemRepository.countAll({
|
||||||
|
userUuid: userUuid.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.logger.info(`Fixing content sizes for ${count} items`, {
|
||||||
|
userId: userUuid.value,
|
||||||
|
codeTag: 'FixContentSizes',
|
||||||
|
})
|
||||||
|
|
||||||
|
const pageSize = 100
|
||||||
|
let page = 1
|
||||||
|
const totalPages = Math.ceil(count / pageSize)
|
||||||
|
|
||||||
|
for (page; page <= totalPages; page++) {
|
||||||
|
const items = await this.itemRepository.findAll({
|
||||||
|
userUuid: userUuid.value,
|
||||||
|
sortOrder: 'ASC',
|
||||||
|
sortBy: 'created_at_timestamp',
|
||||||
|
offset: (page - 1) * pageSize,
|
||||||
|
limit: pageSize,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.props.contentSize != item.calculateContentSize()) {
|
||||||
|
this.logger.info(`Fixing content size for item ${item.id}`, {
|
||||||
|
userId: userUuid.value,
|
||||||
|
codeTag: 'FixContentSizes',
|
||||||
|
itemUuid: item.uuid.value,
|
||||||
|
oldContentSize: item.props.contentSize,
|
||||||
|
newContentSize: item.calculateContentSize(),
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.itemRepository.updateContentSize(item.id.toString(), item.calculateContentSize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Finished fixing content sizes for ${count} items`, {
|
||||||
|
userId: userUuid.value,
|
||||||
|
codeTag: 'FixContentSizes',
|
||||||
|
})
|
||||||
|
|
||||||
|
return Result.ok()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface FixContentSizesDTO {
|
||||||
|
userUuid: string
|
||||||
|
}
|
Loading…
Reference in a new issue