文件
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

184 行
6.2 KiB
TypeScript

import { Dispatch } from 'redux';
import BaseModel, { ModelType } from '../../../BaseModel';
import BaseItem from '../../../models/BaseItem';
import ItemChange from '../../../models/ItemChange';
import Resource from '../../../models/Resource';
import time from '../../../time';
import resourceRemotePath from './resourceRemotePath';
import { ApiCallFunction, LogSyncOperationFunction, SyncAction } from './types';
import { FileApi } from '../../../file-api';
import Logger from '@joplin/utils/Logger';
import { DeletedItemEntity } from '../../database/types';
const logger = Logger.create('syncDeleteStep');
export default async (
syncTargetId: number,
cancelling: ()=> boolean,
logSyncOperation: LogSyncOperationFunction,
apiCall: ApiCallFunction,
api: FileApi,
dispatch: Dispatch,
) => {
const supportsBatchDelete = api.supportsMultiDelete;
let toDelete = await BaseItem.deletedItems(syncTargetId);
if (supportsBatchDelete) {
toDelete = await batchDeleteStep(toDelete, syncTargetId, cancelling, apiCall, logSyncOperation);
}
for (let i = 0; i < toDelete.length; i++) {
if (cancelling()) break;
const item = toDelete[i];
const path = systemPath(item);
const isResource = item.item_type === BaseModel.TYPE_RESOURCE;
try {
await apiCall('delete', path);
if (isResource) {
const remoteContentPath = resourceRemotePath(item.item_id);
await apiCall('delete', remoteContentPath);
}
logSyncOperation(SyncAction.DeleteRemote, null, { id: item.item_id }, 'local has been deleted');
} catch (error) {
if (error.code === 'isReadOnly') {
let remoteContent = await apiCall('get', path);
if (remoteContent) {
try {
remoteContent = await BaseItem.unserialize(remoteContent);
} catch (unserializeError) {
if (unserializeError.code === 'malformedItem') {
logger.warn(`Skipping item from sync target: ${path}: ${unserializeError.message}`);
await BaseItem.remoteDeletedItems(syncTargetId, [item.item_id]);
continue;
}
throw unserializeError;
}
const ItemClass = BaseItem.itemClass(item.item_type);
// For remote deletion, remoteItemUpdatedTime can be reset to 0
let nextQueries = BaseItem.updateSyncTimeQueries(syncTargetId, remoteContent, time.unixMs());
if (isResource) {
nextQueries = nextQueries.concat(Resource.setLocalStateQueries(remoteContent.id, {
fetch_status: Resource.FETCH_STATUS_IDLE,
}));
}
await ItemClass.save(remoteContent, { isNew: true, autoTimestamp: false, changeSource: ItemChange.SOURCE_SYNC, nextQueries });
if (isResource) dispatch({ type: 'SYNC_CREATED_OR_UPDATED_RESOURCE', id: remoteContent.id });
}
} else {
throw error;
}
}
await BaseItem.remoteDeletedItems(syncTargetId, [item.item_id]);
}
};
// Batch deletion returns objects and not true Error instances
type BatchError = { code?: string|number };
type ErrorLike = BatchError|Error;
const systemPath = (deletedItem: DeletedItemEntity) => BaseItem.systemPath(deletedItem.item_id);
const isReadOnlyError = (error: ErrorLike) => 'code' in error && error.code === 'isReadOnly';
const isNotFoundError = (error: ErrorLike) => 'httpCode' in error && error.httpCode === 404;
const isNotSupportedError = (error: ErrorLike) => 'code' in error && error.code === 'methodNotSupported';
const batchDeleteStep = async (
toDelete: DeletedItemEntity[],
syncTargetId: number,
cancellingSync: ()=> boolean,
apiCall: ApiCallFunction,
logSyncOperation: LogSyncOperationFunction,
) => {
let supported = true;
const cancelling = () => {
return !supported || cancellingSync();
};
const handleError = (error: ErrorLike, items: DeletedItemEntity[]) => {
// Read-only and unsupported errors are handled elsewhere
if (!isReadOnlyError(error) && !isNotSupportedError(error)) {
logger.warn('Failed to batch delete item(s)', items.map(item => item.item_id), error, 'Retrying with individual item deletion...');
}
if (isNotSupportedError(error)) {
logger.info('Batch deletion not supported');
supported = false;
}
};
// Items that could not be successfully processed by batch delete:
const toRetryIndividually: DeletedItemEntity[] = [];
const batchSize = 20;
for (let i = 0; i < toDelete.length; i += batchSize) {
const batch = toDelete.slice(i, i + batchSize);
if (cancelling()) {
toRetryIndividually.push(...batch);
continue;
}
const pathToItem = new Map<string, DeletedItemEntity>();
for (const item of batch) {
const itemPath = systemPath(item);
pathToItem.set(itemPath, item);
if (item.item_type === ModelType.Resource) {
const resourcePath = resourceRemotePath(item.item_id);
pathToItem.set(resourcePath, item);
}
}
try {
const { itemIdToErrors } = await execMultiDelete(apiCall, pathToItem);
const successfulItems = [];
for (const item of batch) {
const errors = itemIdToErrors.get(item.item_id) ?? [];
// "Not found" errors often indicate that the item is already deleted. They can be ignored:
const successful = errors.every(error => isNotFoundError(error));
if (successful) {
successfulItems.push(item);
logSyncOperation(SyncAction.DeleteRemote, null, { id: item.item_id }, 'local has been deleted');
} else {
for (const error of errors) {
handleError(error, [item]);
}
toRetryIndividually.push(item);
}
}
await BaseItem.remoteDeletedItems(syncTargetId, successfulItems.map(item => item.item_id));
} catch (error) {
handleError(error, batch);
toRetryIndividually.push(...batch);
}
}
return toRetryIndividually;
};
const execMultiDelete = async (apiCall: ApiCallFunction, pathToItem: Map<string, DeletedItemEntity>) => {
const paths = [...pathToItem.keys()];
const response = await apiCall('multiDelete', paths);
const itemsResponse = response.items as Record<string, { error?: BatchError }>;
const itemIdToErrors = new Map<string, ErrorLike[]>();
for (const [itemName, { error }] of Object.entries(itemsResponse)) {
const item = pathToItem.get(itemName);
if (!item) throw new Error(`Invalid response: Item ${itemName} was not requested for deletion.`);
if (error) {
const errors = itemIdToErrors.get(item.item_id) ?? [];
errors.push(error);
itemIdToErrors.set(item.item_id, errors);
}
}
return { itemIdToErrors };
};