镜像自地址
https://github.com/lhly/cloud-drive-renamer.git
已同步 2026-10-20 13:47:47 +08:00
feat: integrate conflict detection and crash recovery
这个提交包含在:
父节点
a13175441c
当前提交
85eb3b3ca1
@ -5,6 +5,14 @@ import { FileType, PreviewItem } from '../../types/file-selector';
|
|||||||
import { RuleConfig } from '../../types/rule';
|
import { RuleConfig } from '../../types/rule';
|
||||||
import { RuleFactory } from '../../rules/rule-factory';
|
import { RuleFactory } from '../../rules/rule-factory';
|
||||||
import { BatchExecutor, ExecutorState } from '../../core/executor';
|
import { BatchExecutor, ExecutorState } from '../../core/executor';
|
||||||
|
import { buildExecutionPlan } from '../../core/execution-plan';
|
||||||
|
import {
|
||||||
|
ConflictDetector,
|
||||||
|
ConflictResolution,
|
||||||
|
ConflictResult,
|
||||||
|
showConflictResolutionDialog,
|
||||||
|
} from '../../core/conflict-detector';
|
||||||
|
import { crashRecovery, initCrashRecovery } from '../../core/crash-recovery';
|
||||||
import { BatchResults, ProgressEvent } from '../../types/core';
|
import { BatchResults, ProgressEvent } from '../../types/core';
|
||||||
import { I18nService } from '../../utils/i18n';
|
import { I18nService } from '../../utils/i18n';
|
||||||
import { parseFileName } from '../../utils/helpers';
|
import { parseFileName } from '../../utils/helpers';
|
||||||
@ -133,6 +141,8 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
private syncMessage: string | null = null;
|
private syncMessage: string | null = null;
|
||||||
|
|
||||||
private executor: BatchExecutor | null = null;
|
private executor: BatchExecutor | null = null;
|
||||||
|
private recoveryChecked = false;
|
||||||
|
private operationIndexByFileId: Map<string, number> | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error message
|
* Error message
|
||||||
@ -197,15 +207,21 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
if (this.open) {
|
if (this.open) {
|
||||||
this.resetExecutionState();
|
this.resetExecutionState();
|
||||||
await this.loadAllFiles();
|
await this.loadAllFiles();
|
||||||
|
await this.ensureCrashRecovery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProperties: PropertyValues<this>): void {
|
protected updated(changedProperties: PropertyValues<this>): void {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
if (changedProperties.has('open') && this.open) {
|
if (changedProperties.has('open')) {
|
||||||
|
if (this.open) {
|
||||||
this.resetExecutionState();
|
this.resetExecutionState();
|
||||||
void this.loadAllFiles();
|
void this.loadAllFiles();
|
||||||
|
void this.ensureCrashRecovery();
|
||||||
|
} else {
|
||||||
|
this.recoveryChecked = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,6 +257,20 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ensureCrashRecovery(): Promise<void> {
|
||||||
|
if (this.recoveryChecked || !this.adapter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.recoveryChecked = true;
|
||||||
|
try {
|
||||||
|
await initCrashRecovery(this.adapter);
|
||||||
|
} catch (error) {
|
||||||
|
const errorObj = error instanceof Error ? error : new Error(String(error));
|
||||||
|
logger.warn('[FileSelectorPanel] Crash recovery check failed:', errorObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get file type from extension
|
* Get file type from extension
|
||||||
* @private
|
* @private
|
||||||
@ -440,29 +470,68 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const executionPlan = this.previewList;
|
const selectedFiles = this.selectedFiles;
|
||||||
if (executionPlan.length === 0) {
|
const previewPlan = this.previewList;
|
||||||
|
|
||||||
|
if (previewPlan.length === 0) {
|
||||||
alert(I18nService.t('no_rename_needed'));
|
alert(I18nService.t('no_rename_needed'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm if there are conflicts
|
const newNames = selectedFiles.map((file) => this.newNameMap.get(file.id) || file.name);
|
||||||
if (this.conflictIds.size > 0) {
|
|
||||||
const conflictMessage = I18nService.t('conflicts_detected_confirm', [String(this.conflictIds.size)]);
|
|
||||||
const confirmed = confirm(conflictMessage);
|
|
||||||
|
|
||||||
|
let conflicts: Map<string, ConflictResult> | null = null;
|
||||||
|
let resolution: ConflictResolution | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const detector = new ConflictDetector(this.adapter);
|
||||||
|
conflicts = await detector.detectConflicts(selectedFiles, newNames);
|
||||||
|
|
||||||
|
const conflictCount = Array.from(conflicts.values()).filter((result) => result.hasConflict).length;
|
||||||
|
if (conflictCount > 0) {
|
||||||
|
resolution = showConflictResolutionDialog(conflictCount);
|
||||||
|
if (!resolution) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorObj = error instanceof Error ? error : new Error(String(error));
|
||||||
|
logger.error('[FileSelectorPanel] Conflict detection failed:', errorObj);
|
||||||
|
const confirmed = confirm(I18nService.t('conflict_check_failed_confirm'));
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const executionPlan = buildExecutionPlan({
|
||||||
|
files: selectedFiles,
|
||||||
|
newNames,
|
||||||
|
conflicts: conflicts ?? undefined,
|
||||||
|
resolution,
|
||||||
|
skipUnchanged: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (executionPlan.tasks.length === 0) {
|
||||||
|
alert(I18nService.t('no_rename_needed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedNameMap = new Map<string, string>();
|
||||||
|
selectedFiles.forEach((file, index) => {
|
||||||
|
resolvedNameMap.set(file.id, executionPlan.resolvedNames[index] || file.name);
|
||||||
|
});
|
||||||
|
this.newNameMap = resolvedNameMap;
|
||||||
|
this.conflictIds = new Set();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.resetExecutionState();
|
this.resetExecutionState();
|
||||||
this.executing = true;
|
this.executing = true;
|
||||||
this.executorState = ExecutorState.RUNNING;
|
this.executorState = ExecutorState.RUNNING;
|
||||||
|
|
||||||
this.executionItems = executionPlan.map((item) => ({
|
this.executionItems = executionPlan.tasks.map((task) => ({
|
||||||
...item,
|
file: task.file,
|
||||||
|
newName: task.newName,
|
||||||
|
conflict: false,
|
||||||
done: undefined,
|
done: undefined,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
}));
|
}));
|
||||||
@ -474,15 +543,29 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
failed: 0,
|
failed: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.operationIndexByFileId = new Map(
|
||||||
|
executionPlan.tasks.map((task) => [task.file.id, task.index])
|
||||||
|
);
|
||||||
|
|
||||||
|
await crashRecovery.saveOperationState({
|
||||||
|
platform: this.adapter.platform,
|
||||||
|
files: executionPlan.tasks.map((task) => task.file),
|
||||||
|
rule: this.ruleConfig,
|
||||||
|
completed: [],
|
||||||
|
failed: [],
|
||||||
|
tasks: executionPlan.tasks,
|
||||||
|
});
|
||||||
|
|
||||||
// Execute batch rename
|
// Execute batch rename
|
||||||
const executor = new BatchExecutor(
|
const executor = new BatchExecutor(
|
||||||
this.selectedFiles,
|
executionPlan.tasks.map((task) => task.file),
|
||||||
this.ruleConfig,
|
this.ruleConfig,
|
||||||
this.adapter,
|
this.adapter,
|
||||||
{
|
{
|
||||||
requestInterval: this.adapter.getConfig().requestInterval,
|
requestInterval: this.adapter.getConfig().requestInterval,
|
||||||
maxConcurrent: this.adapter.getConfig().maxConcurrent,
|
maxConcurrent: this.adapter.getConfig().maxConcurrent,
|
||||||
skipUnchanged: true,
|
skipUnchanged: true,
|
||||||
|
tasks: executionPlan.tasks,
|
||||||
onProgress: (progress) => {
|
onProgress: (progress) => {
|
||||||
this.handleProgress(progress);
|
this.handleProgress(progress);
|
||||||
},
|
},
|
||||||
@ -501,6 +584,10 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
|
|
||||||
this.applyExecutionResults(results);
|
this.applyExecutionResults(results);
|
||||||
void this.syncAfterRename();
|
void this.syncAfterRename();
|
||||||
|
|
||||||
|
if (this.executorState !== ExecutorState.CANCELLED) {
|
||||||
|
await crashRecovery.clearOperationState();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorObj = error instanceof Error ? error : new Error(String(error));
|
const errorObj = error instanceof Error ? error : new Error(String(error));
|
||||||
this.error = errorObj.message;
|
this.error = errorObj.message;
|
||||||
@ -517,6 +604,17 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.operationIndexByFileId) {
|
||||||
|
const index = this.operationIndexByFileId.get(progress.fileId);
|
||||||
|
if (index !== undefined) {
|
||||||
|
if (progress.status === 'failed') {
|
||||||
|
void crashRecovery.markAsFailed(index);
|
||||||
|
} else if (progress.status === 'success') {
|
||||||
|
void crashRecovery.markAsCompleted(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.executionItems = this.executionItems.map((item) => {
|
this.executionItems = this.executionItems.map((item) => {
|
||||||
if (item.file.id !== progress.fileId) {
|
if (item.file.id !== progress.fileId) {
|
||||||
return item;
|
return item;
|
||||||
@ -643,6 +741,7 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
|
|
||||||
this.executor.cancel();
|
this.executor.cancel();
|
||||||
this.executorState = this.executor.getState();
|
this.executorState = this.executor.getState();
|
||||||
|
void crashRecovery.clearOperationState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleSync(): void {
|
private handleSync(): void {
|
||||||
@ -752,6 +851,7 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
this.executionResults = null;
|
this.executionResults = null;
|
||||||
this.syncStatus = 'idle';
|
this.syncStatus = 'idle';
|
||||||
this.syncMessage = null;
|
this.syncMessage = null;
|
||||||
|
this.operationIndexByFileId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -760,6 +860,7 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
*/
|
*/
|
||||||
private handleClose(): void {
|
private handleClose(): void {
|
||||||
this.resetExecutionState();
|
this.resetExecutionState();
|
||||||
|
this.recoveryChecked = false;
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('panel-close', {
|
new CustomEvent('panel-close', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
|
|||||||
@ -106,6 +106,7 @@
|
|||||||
"sync_not_supported": "Auto sync is not supported on this platform. Please refresh the page.",
|
"sync_not_supported": "Auto sync is not supported on this platform. Please refresh the page.",
|
||||||
"conflicts_detected": "Conflicts detected: $1",
|
"conflicts_detected": "Conflicts detected: $1",
|
||||||
"conflicts_detected_confirm": "Conflicts detected: $1. Continue?",
|
"conflicts_detected_confirm": "Conflicts detected: $1. Continue?",
|
||||||
|
"conflict_check_failed_confirm": "Conflict check failed. Continue?",
|
||||||
"preview_empty": "No files to preview",
|
"preview_empty": "No files to preview",
|
||||||
"preview_empty_hint": "Select files and configure rules to see the preview",
|
"preview_empty_hint": "Select files and configure rules to see the preview",
|
||||||
"preview_loading": "Generating preview...",
|
"preview_loading": "Generating preview...",
|
||||||
|
|||||||
@ -106,6 +106,7 @@
|
|||||||
"sync_not_supported": "当前平台暂不支持自动同步,请手动刷新页面",
|
"sync_not_supported": "当前平台暂不支持自动同步,请手动刷新页面",
|
||||||
"conflicts_detected": "检测到 $1 个冲突",
|
"conflicts_detected": "检测到 $1 个冲突",
|
||||||
"conflicts_detected_confirm": "检测到 $1 个冲突,是否继续?",
|
"conflicts_detected_confirm": "检测到 $1 个冲突,是否继续?",
|
||||||
|
"conflict_check_failed_confirm": "冲突检测失败,是否继续执行?",
|
||||||
"preview_empty": "暂无文件可预览",
|
"preview_empty": "暂无文件可预览",
|
||||||
"preview_empty_hint": "选择文件并配置规则以查看预览",
|
"preview_empty_hint": "选择文件并配置规则以查看预览",
|
||||||
"preview_loading": "生成预览中...",
|
"preview_loading": "生成预览中...",
|
||||||
|
|||||||
@ -106,6 +106,7 @@
|
|||||||
"sync_not_supported": "目前平台暫不支援自動同步,請手動重新整理頁面",
|
"sync_not_supported": "目前平台暫不支援自動同步,請手動重新整理頁面",
|
||||||
"conflicts_detected": "偵測到 $1 個衝突",
|
"conflicts_detected": "偵測到 $1 個衝突",
|
||||||
"conflicts_detected_confirm": "偵測到 $1 個衝突,是否繼續?",
|
"conflicts_detected_confirm": "偵測到 $1 個衝突,是否繼續?",
|
||||||
|
"conflict_check_failed_confirm": "衝突偵測失敗,是否繼續執行?",
|
||||||
"preview_empty": "暫無檔案可預覽",
|
"preview_empty": "暫無檔案可預覽",
|
||||||
"preview_empty_hint": "選擇檔案並設定規則以查看預覽",
|
"preview_empty_hint": "選擇檔案並設定規則以查看預覽",
|
||||||
"preview_loading": "產生預覽中...",
|
"preview_loading": "產生預覽中...",
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户