diff --git a/src/baseTypes.ts b/src/baseTypes.ts index 38024f4..0ffe7b9 100644 --- a/src/baseTypes.ts +++ b/src/baseTypes.ts @@ -107,6 +107,8 @@ export interface RemotelySavePluginSettings { conflictAction?: ConflictActionType; howToCleanEmptyFolder?: EmptyFolderCleanType; + protectModifyPercentage?: number; + /** * @deprecated */ diff --git a/src/langs/en.json b/src/langs/en.json index 1e59e42..8a18a8b 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -23,6 +23,7 @@ "syncrun_shortstep2skip": "2/2 Remotely Save real sync is skipped in dry run mode.", "syncrun_shortstep2": "2/2 Remotely Save finish!", "syncrun_abort": "{{manifestID}}-{{theDate}}: abort sync, triggerSource={{triggerSource}}, error while {{syncStatus}}", + "syncrun_abort_protectmodifypercentage": "Abort! you set changing files >= {{protectModifyPercentage}}% is not allowed but {{realModifyDeleteCount}}/{{allFilesCount}}={{percent}}% is going to be modified or deleted! If you are sure you want this sync, please adjust the allowed ratio in the settings.", "protocol_saveqr": "New not-oauth2 settings for {{manifestName}} saved. Reopen the plugin Settings to the effect.", "protocol_callbacknotsupported": "Your uri call a callback that's not supported yet: {{params}}", "protocol_dropbox_connecting": "Connecting to Dropbox...\nPlease DO NOT close this modal.", @@ -253,6 +254,11 @@ "settings_cleanemptyfolder_desc": "The sync algorithm majorly deals with files, so you need to specify how to deal with empty folders.", "settings_cleanemptyfolder_skip": "leave them as is (default)", "settings_cleanemptyfolder_clean_both": "delete local and remote", + "settings_protectmodifypercentage": "Abort Sync If Modification Above Percentage", + "settings_protectmodifypercentage_desc": "Abort the sync if more than n% of the files are going to be deleted / modified. Useful to protect users' files from unexpected modifications. You can set to 100 to disable the protection, or set to 0 to always block the sync.", + "settings_protectmodifypercentage_000_desc": "0 (always block)", + "settings_protectmodifypercentage_050_desc": "50 (default)", + "settings_protectmodifypercentage_100_desc": "100 (disable the protection)", "settings_importexport": "Import and Export Partial Settings", "settings_export": "Export", "settings_export_desc": "Export not-oauth2 settings by generating a qrcode.", diff --git a/src/langs/zh_cn.json b/src/langs/zh_cn.json index c57bebb..62b1f8d 100644 --- a/src/langs/zh_cn.json +++ b/src/langs/zh_cn.json @@ -23,6 +23,7 @@ "syncrun_shortstep2skip": "2/2 Remotely Save 在空跑模式,跳过实际数据交换步骤。", "syncrun_shortstep2": "2/2 Remotely Save 已完成同步!", "syncrun_abort": "{{manifestID}}-{{theDate}}:中断同步,同步来源={{triggerSource}},出错阶段={{syncStatus}}", + "syncrun_abort_protectmodifypercentage": "中断同步!您设置了不允许 >= {{protectModifyPercentage}}% 的变更,但是现在 {{realModifyDeleteCount}}/{{allFilesCount}}={{percent}}% 的文件会被修改或删除!如果您确认这次同步是您想要的,那么请在设置里修改允许比例。", "protocol_saveqr": " {{manifestName}} 新的非 oauth2 设置保存完成。请重启插件设置页使之生效。", "protocol_callbacknotsupported": "您的 uri callback 暂不支持: {{params}}", "protocol_dropbox_connecting": "正在连接 Dropbox……\n请不要关闭此弹窗。", @@ -253,6 +254,11 @@ "settings_cleanemptyfolder_desc": "同步算法主要是针对文件处理的,您要要手动指定空文件夹如何处理。", "settings_cleanemptyfolder_skip": "跳过处理空文件夹(默认)", "settings_cleanemptyfolder_clean_both": "删除本地和服务器的空文件夹", + "settings_protectmodifypercentage": "如果修改超过百分比则中止同步", + "settings_protectmodifypercentage_desc": "如果算法检测到超过 n% 的文件会被修改或删除,则中止同步。从而可以保护用户的文件免受预料之外的修改。您可以设置为 100 而去除此保护,也可以设置为 0 总是强制中止所有同步。", + "settings_protectmodifypercentage_000_desc": "0(总是强制中止)", + "settings_protectmodifypercentage_050_desc": "50(默认值)", + "settings_protectmodifypercentage_100_desc": "100(去除此保护)", "settings_importexport": "导入导出部分设置", "settings_export": "导出", "settings_export_desc": "用 QR 码导出非 oauth2 的设置信息。", diff --git a/src/langs/zh_tw.json b/src/langs/zh_tw.json index f94b42e..57e6a72 100644 --- a/src/langs/zh_tw.json +++ b/src/langs/zh_tw.json @@ -23,6 +23,7 @@ "syncrun_shortstep2skip": "2/2 Remotely Save 在空跑模式,跳過實際資料交換步驟。", "syncrun_shortstep2": "2/2 Remotely Save 已完成同步!", "syncrun_abort": "{{manifestID}}-{{theDate}}:中斷同步,同步來源={{triggerSource}},出錯階段={{syncStatus}}", + "syncrun_abort_protectmodifypercentage": "中斷同步!您設定了不允許 >= {{protectModifyPercentage}}% 的變更,但是現在 {{realModifyDeleteCount}}/{{allFilesCount}}={{percent}}% 的檔案會被修改或刪除!如果您確認這次同步是您想要的,那麼請在設定裡修改允許比例。", "protocol_saveqr": " {{manifestName}} 新的非 oauth2 設定儲存完成。請重啟外掛設定頁使之生效。", "protocol_callbacknotsupported": "您的 uri callback 暫不支援: {{params}}", "protocol_dropbox_connecting": "正在連線 Dropbox……\n請不要關閉此彈窗。", @@ -253,6 +254,11 @@ "settings_cleanemptyfolder_desc": "同步演算法主要是針對檔案處理的,您需要手動指定空資料夾如何處理。", "settings_cleanemptyfolder_skip": "跳過處理空資料夾(預設)", "settings_cleanemptyfolder_clean_both": "刪除本地和伺服器的空資料夾", + "settings_protectmodifypercentage": "如果修改超過百分比則中止同步", + "settings_protectmodifypercentage_desc": "如果演算法檢測到超過 n% 的檔案會被修改或刪除,則中止同步。從而可以保護使用者的檔案免受預料之外的修改。您可以設定為 100 而去除此保護,也可以設定為 0 總是強制中止所有同步。", + "settings_protectmodifypercentage_000_desc": "0(總是強制中止)", + "settings_protectmodifypercentage_050_desc": "50(預設值)", + "settings_protectmodifypercentage_100_desc": "100(去除此保護)", "settings_importexport": "匯入匯出部分設定", "settings_export": "匯出", "settings_export_desc": "用 QR 碼匯出非 oauth2 的設定資訊。", diff --git a/src/main.ts b/src/main.ts index 46938ae..2d0e5f2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -93,6 +93,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = { agreeToUseSyncV3: false, conflictAction: "keep_newer", howToCleanEmptyFolder: "skip", + protectModifyPercentage: 50, }; interface OAuth2Info { @@ -338,6 +339,24 @@ export default class RemotelySavePlugin extends Plugin { this.settings.password, this.settings.concurrency ?? 5, (key: string) => self.trash(key), + this.settings.protectModifyPercentage ?? 50, + ( + protectModifyPercentage: number, + realModifyDeleteCount: number, + allFilesCount: number + ) => { + const percent = ( + (100 * realModifyDeleteCount) / + allFilesCount + ).toFixed(1); + const res = t("syncrun_abort_protectmodifypercentage", { + protectModifyPercentage, + realModifyDeleteCount, + allFilesCount, + percent, + }); + return res; + }, ( realCounter: number, realTotalCount: number, @@ -866,6 +885,9 @@ export default class RemotelySavePlugin extends Plugin { if (this.settings.howToCleanEmptyFolder === undefined) { this.settings.howToCleanEmptyFolder = "skip"; } + if (this.settings.protectModifyPercentage === undefined) { + this.settings.protectModifyPercentage = 50; + } await this.saveSettings(); } diff --git a/src/settings.ts b/src/settings.ts index b61cc67..4a0062f 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1968,6 +1968,29 @@ export class RemotelySaveSettingTab extends PluginSettingTab { }); }); + new Setting(advDiv) + .setName(t("settings_protectmodifypercentage")) + .setDesc(t("settings_protectmodifypercentage_desc")) + .addDropdown((dropdown) => { + for (const i of Array.from({ length: 11 }, (x, i) => i * 10)) { + let desc = `${i}`; + if (i === 0) { + desc = t("settings_protectmodifypercentage_000_desc"); + } else if (i === 50) { + desc = t("settings_protectmodifypercentage_050_desc"); + } else if (i === 100) { + desc = t("settings_protectmodifypercentage_100_desc"); + } + dropdown.addOption(`${i}`, desc); + } + dropdown + .setValue(`${this.plugin.settings.protectModifyPercentage ?? 50}`) + .onChange(async (val) => { + this.plugin.settings.protectModifyPercentage = parseInt(val); + await this.plugin.saveSettings(); + }); + }); + ////////////////////////////////////////////////// // below for import and export functions ////////////////////////////////////////////////// diff --git a/src/sync.ts b/src/sync.ts index 832daa9..501eebc 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -788,12 +788,18 @@ const splitThreeStepsOnEntityMappings = ( (k1, k2) => k2.length - k1.length ); - let realTotalCount = 0; + let allFilesCount = 0; // how many files in entities + let realModifyDeleteCount = 0; // how many files to be modified / deleted + let realTotalCount = 0; // how many files to be delt with for (let i = 0; i < sortedKeys.length; ++i) { const key = sortedKeys[i]; const val = mixedEntityMappings[key]; + if (!key.endsWith("/")) { + allFilesCount += 1; + } + if ( val.decision === "equal" || val.decision === "folder_existed_both" || @@ -829,6 +835,10 @@ const splitThreeStepsOnEntityMappings = ( k.push(val); } realTotalCount += 1; + + if (val.decision.startsWith("deleted")) { + realModifyDeleteCount += 1; + } } else if ( val.decision === "modified_local" || val.decision === "modified_remote" || @@ -851,6 +861,13 @@ const splitThreeStepsOnEntityMappings = ( uploadDownloads[0].push(val); // only one level is needed here } realTotalCount += 1; + + if ( + val.decision.startsWith("modified") || + val.decision.startsWith("conflict") + ) { + realModifyDeleteCount += 1; + } } else { throw Error(`unknown decision ${val.decision} for ${key}`); } @@ -865,6 +882,8 @@ const splitThreeStepsOnEntityMappings = ( folderCreationOps: folderCreationOps, deletionOps: deletionOps, uploadDownloads: uploadDownloads, + allFilesCount: allFilesCount, + realModifyDeleteCount: realModifyDeleteCount, realTotalCount: realTotalCount, }; }; @@ -1014,16 +1033,47 @@ export const doActualSync = async ( password: string, concurrency: number, localDeleteFunc: any, + protectModifyPercentage: number, + getProtectModifyPercentageErrorStrFunc: any, callbackSyncProcess: any, db: InternalDBs ) => { console.debug(`concurrency === ${concurrency}`); - const { folderCreationOps, deletionOps, uploadDownloads, realTotalCount } = - splitThreeStepsOnEntityMappings(mixedEntityMappings); + const { + folderCreationOps, + deletionOps, + uploadDownloads, + allFilesCount, + realModifyDeleteCount, + realTotalCount, + } = splitThreeStepsOnEntityMappings(mixedEntityMappings); // console.debug(`folderCreationOps: ${JSON.stringify(folderCreationOps)}`); // console.debug(`deletionOps: ${JSON.stringify(deletionOps)}`); // console.debug(`uploadDownloads: ${JSON.stringify(uploadDownloads)}`); - // console.debug(`realTotalCount: ${JSON.stringify(realTotalCount)}`); + console.debug(`allFilesCount: ${allFilesCount}`); + console.debug(`realModifyDeleteCount: ${realModifyDeleteCount}`); + console.debug(`realTotalCount: ${realTotalCount}`); + + console.debug(`protectModifyPercentage: ${protectModifyPercentage}`); + + if ( + protectModifyPercentage >= 0 && + realModifyDeleteCount >= 0 && + allFilesCount > 0 + ) { + if ( + realModifyDeleteCount * 100 >= + allFilesCount * protectModifyPercentage + ) { + const errorStr: string = getProtectModifyPercentageErrorStrFunc( + protectModifyPercentage, + realModifyDeleteCount, + allFilesCount + ); + + throw Error(errorStr); + } + } const nested = [folderCreationOps, deletionOps, uploadDownloads]; const logTexts = [