add protection!

This commit is contained in:
fyears 2024-03-17 18:01:59 +08:00
parent c828a341ba
commit 7ec0102c7a
7 changed files with 119 additions and 4 deletions

View File

@ -107,6 +107,8 @@ export interface RemotelySavePluginSettings {
conflictAction?: ConflictActionType;
howToCleanEmptyFolder?: EmptyFolderCleanType;
protectModifyPercentage?: number;
/**
* @deprecated
*/

View File

@ -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.",

View File

@ -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 的设置信息。",

View File

@ -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 的設定資訊。",

View File

@ -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();
}

View File

@ -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
//////////////////////////////////////////////////

View File

@ -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 = [