diff --git a/packages/lib/Synchronizer.ts b/packages/lib/Synchronizer.ts index 0f929fd0e..c4e314e3e 100644 --- a/packages/lib/Synchronizer.ts +++ b/packages/lib/Synchronizer.ts @@ -479,6 +479,31 @@ export default class Synchronizer { void this.cancel(); }); + // ======================================================================== + // 2. DELETE_REMOTE + // ------------------------------------------------------------------------ + // Delete the remote items that have been deleted locally. + // ======================================================================== + + if (syncSteps.indexOf('delete_remote') >= 0) { + const deletedItems = await BaseItem.deletedItems(syncTargetId); + for (let i = 0; i < deletedItems.length; i++) { + if (this.cancelling()) break; + + const item = deletedItems[i]; + const path = BaseItem.systemPath(item.item_id); + this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted'); + await this.apiCall('delete', path); + + if (item.item_type === BaseModel.TYPE_RESOURCE) { + const remoteContentPath = resourceRemotePath(item.item_id); + await this.apiCall('delete', remoteContentPath); + } + + await BaseItem.remoteDeletedItem(syncTargetId, item.item_id); + } + } // DELETE_REMOTE STEP + // ======================================================================== // 1. UPLOAD // ------------------------------------------------------------------------ @@ -763,31 +788,6 @@ export default class Synchronizer { } } // UPLOAD STEP - // ======================================================================== - // 2. DELETE_REMOTE - // ------------------------------------------------------------------------ - // Delete the remote items that have been deleted locally. - // ======================================================================== - - if (syncSteps.indexOf('delete_remote') >= 0) { - const deletedItems = await BaseItem.deletedItems(syncTargetId); - for (let i = 0; i < deletedItems.length; i++) { - if (this.cancelling()) break; - - const item = deletedItems[i]; - const path = BaseItem.systemPath(item.item_id); - this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted'); - await this.apiCall('delete', path); - - if (item.item_type === BaseModel.TYPE_RESOURCE) { - const remoteContentPath = resourceRemotePath(item.item_id); - await this.apiCall('delete', remoteContentPath); - } - - await BaseItem.remoteDeletedItem(syncTargetId, item.item_id); - } - } // DELETE_REMOTE STEP - // ------------------------------------------------------------------------ // 3. DELTA // ------------------------------------------------------------------------ diff --git a/packages/server/src/models/UserModel.ts b/packages/server/src/models/UserModel.ts index 921ed429d..c8e1b575f 100644 --- a/packages/server/src/models/UserModel.ts +++ b/packages/server/src/models/UserModel.ts @@ -180,7 +180,7 @@ export default class UserModel extends BaseModel { } public async checkMaxItemSizeLimit(user: User, buffer: Buffer, item: Item, joplinItem: any) { - // If the item is encrypted, we apply a multipler because encrypted + // If the item is encrypted, we apply a multiplier because encrypted // items can be much larger (seems to be up to twice the size but for // safety let's go with 2.2). @@ -198,14 +198,20 @@ export default class UserModel extends BaseModel { )); } - // Also apply a multiplier to take into account E2EE overhead - const maxTotalItemSize = getMaxTotalItemSize(user) * 1.5; - if (maxTotalItemSize && user.total_item_size + itemSize >= maxTotalItemSize) { - throw new ErrorPayloadTooLarge(_('Cannot save %s "%s" because it would go over the total allowed size (%s) for this account', - isNote ? _('note') : _('attachment'), - itemTitle ? itemTitle : item.name, - formatBytes(maxTotalItemSize) - )); + // We allow lock files to go through so that sync can happen, which in + // turns allow user to fix oversized account by deleting items. + const isWhiteListed = itemSize < 200 && item.name.startsWith('locks/'); + + if (!isWhiteListed) { + // Also apply a multiplier to take into account E2EE overhead + const maxTotalItemSize = getMaxTotalItemSize(user) * 1.5; + if (maxTotalItemSize && user.total_item_size + itemSize >= maxTotalItemSize) { + throw new ErrorPayloadTooLarge(_('Cannot save %s "%s" because it would go over the total allowed size (%s) for this account', + isNote ? _('note') : _('attachment'), + itemTitle ? itemTitle : item.name, + formatBytes(maxTotalItemSize) + )); + } } } diff --git a/packages/server/src/routes/api/items.ts b/packages/server/src/routes/api/items.ts index 5ac8e945f..04a1d231c 100644 --- a/packages/server/src/routes/api/items.ts +++ b/packages/server/src/routes/api/items.ts @@ -5,7 +5,7 @@ import Router from '../../utils/Router'; import { RouteType } from '../../utils/types'; import { AppContext } from '../../utils/types'; import * as fs from 'fs-extra'; -import { ErrorForbidden, ErrorMethodNotAllowed, ErrorNotFound, ErrorPayloadTooLarge } from '../../utils/errors'; +import { ErrorForbidden, ErrorMethodNotAllowed, ErrorNotFound, ErrorPayloadTooLarge, errorToPlainObject } from '../../utils/errors'; import ItemModel, { ItemSaveOption, SaveFromRawContentItem } from '../../models/ItemModel'; import { requestDeltaPagination, requestPagination } from '../../models/utils/pagination'; import { AclAction } from '../../models/BaseModel'; @@ -66,7 +66,9 @@ export async function putItemContents(path: SubPath, ctx: AppContext, isBatch: b const output = await ctx.joplin.models.item().saveFromRawContent(ctx.joplin.owner, items, saveOptions); for (const [name] of Object.entries(output)) { if (output[name].item) output[name].item = ctx.joplin.models.item().toApiOutput(output[name].item) as Item; + if (output[name].error) output[name].error = errorToPlainObject(output[name].error); } + return output; } diff --git a/packages/server/src/utils/errors.ts b/packages/server/src/utils/errors.ts index 0124ee693..5b7cd7e53 100644 --- a/packages/server/src/utils/errors.ts +++ b/packages/server/src/utils/errors.ts @@ -114,3 +114,17 @@ export function errorToString(error: Error): string { if (error.stack) msg.push(error.stack); return msg.join(': '); } + +interface PlainObjectError { + httpCode?: number; + message?: string; + code?: string; +} + +export function errorToPlainObject(error: any): PlainObjectError { + const output: PlainObjectError = {}; + if ('httpCode' in error) output.httpCode = error.httpCode; + if ('code' in error) output.code = error.code; + if ('message' in error) output.message = error.message; + return output; +}