incremental push / pull only modes

This commit is contained in:
fyears 2024-03-17 23:58:54 +08:00
parent d51659d3d1
commit 06bf59bf2a
9 changed files with 313 additions and 137 deletions

View File

@ -37,15 +37,35 @@ We have _five_ input sources:
Init run, consuming remote deletions :
TBD
Change history data into _local previous succeeded sync history_.
Later runs, use the first, second, third sources **only**.
Table modified based on synclone and rsinc. The number inside the table cell is the decision branch in the code.
Bidirectional table is modified based on synclone and rsinc. Incremental push / pull only tables is further modified based on the bidirectional table. The number inside the table cell is the decision branch in the code.
Bidirectional:
| local\remote | remote unchanged | remote modified | remote deleted | remote created |
| --------------- | ------------------ | ------------------------- | ------------------ | ------------------------- |
| local unchanged | (02/21) do nothing | (09) pull remote | (07) delete local | (??) conflict |
| local modified | (10) push local | (16/17/18/19/20) conflict | (08) push local | (??) conflict |
| local deleted | (04) delete remote | (05) pull | (01) clean history | (03) pull remote |
| local created | (??) conflict | (??) conflict | (06) push local | (11/12/13/14/15) conflict |
| local unchanged | (02/21) do nothing | (09) pull | (07) delete local | (??) conflict |
| local modified | (10) push | (16/17/18/19/20) conflict | (08) push | (??) conflict |
| local deleted | (04) delete remote | (05) pull | (01) clean history | (03) pull |
| local created | (??) conflict | (??) conflict | (06) push | (11/12/13/14/15) conflict |
Incremental push only:
| local\remote | remote unchanged | remote modified | remote deleted | remote created |
| --------------- | ---------------------------- | ---------------------------- | ---------------------- | ---------------------------- |
| local unchanged | (02/21) do nothing | **(26) conflict push** | **(32) conflict push** | (??) conflict |
| local modified | (10) push | **(25) conflict push** | (08) push | (??) conflict |
| local deleted | **(29) conflict do nothing** | **(30) conflict do nothing** | (01) clean history | **(28) conflict do nothing** |
| local created | (??) conflict | (??) conflict | (06) push | **(23) conflict push** |
Incremental pull only:
| local\remote | remote unchanged | remote modified | remote deleted | remote created |
| --------------- | ---------------------- | ---------------------- | ---------------------------- | ---------------------- |
| local unchanged | (02/21) do nothing | (09) pull | **(33) conflict do nothing** | (??) conflict |
| local modified | **(27) conflict pull** | **(24) conflict pull** | **(34) conflict do nothing** | (??) conflict |
| local deleted | **(35) conflict pull** | (05) pull | (01) clean history | (03) pull |
| local created | (??) conflict | (??) conflict | **(31) conflict do nothing** | **(22) conflict pull** |

View File

@ -7,6 +7,7 @@
- [x] deletion: true deletion status computation
- [x] meta data: no remote meta data any more
- [x] migration: old data auto transfer to new db (hopefully)
- [ ] partial sync: force push
- [ ] partial sync: force pull
- [x] sync direction: incremental push only
- [x] sync direction: incremental pull only
- [x] sync protection: warning based on the threshold
- [ ] partial sync: better sync on save

View File

@ -83,6 +83,11 @@ export interface OnedriveConfig {
remoteBaseDir?: string;
}
export type SyncDirectionType =
| "bidirectional"
| "incremental_pull_only"
| "incremental_push_only";
export interface RemotelySavePluginSettings {
s3: S3Config;
webdav: WebdavConfig;
@ -108,6 +113,7 @@ export interface RemotelySavePluginSettings {
howToCleanEmptyFolder?: EmptyFolderCleanType;
protectModifyPercentage?: number;
syncDirection?: SyncDirectionType;
/**
* @deprecated
@ -147,21 +153,22 @@ export type ConflictActionType = "keep_newer" | "keep_larger" | "rename_both";
export type DecisionTypeForMixedEntity =
| "only_history"
| "equal"
| "modified_local"
| "modified_remote"
| "created_local"
| "created_remote"
| "deleted_local"
| "deleted_remote"
| "conflict_created_keep_local"
| "conflict_created_keep_remote"
| "conflict_created_keep_both"
| "conflict_modified_keep_local"
| "conflict_modified_keep_remote"
| "conflict_modified_keep_both"
| "folder_existed_both"
| "folder_existed_local"
| "folder_existed_remote"
| "local_is_modified_then_push"
| "remote_is_modified_then_pull"
| "local_is_created_then_push"
| "remote_is_created_then_pull"
| "local_is_deleted_thus_also_delete_remote"
| "remote_is_deleted_thus_also_delete_local"
| "conflict_created_then_keep_local"
| "conflict_created_then_keep_remote"
| "conflict_created_then_keep_both"
| "conflict_created_then_do_nothing"
| "conflict_modified_then_keep_local"
| "conflict_modified_then_keep_remote"
| "conflict_modified_then_keep_both"
| "folder_existed_both_then_do_nothing"
| "folder_existed_local_then_also_create_remote"
| "folder_existed_remote_then_also_create_local"
| "folder_to_be_created"
| "folder_to_skip"
| "folder_to_be_deleted";

View File

@ -247,7 +247,7 @@
"settings_deletetowhere_system_trash": "system trash (default)",
"settings_deletetowhere_obsidian_trash": "Obsidian .trash folder",
"settings_conflictaction": "Action For Conflict",
"settings_conflictaction_desc": "If a file is created or modified on both side since last update, it's a conflict event. How to deal with it?",
"settings_conflictaction_desc": "If a file is created or modified on both side since last update, it's a conflict event. How to deal with it? This only works for bidirectional sync.",
"settings_conflictaction_keep_newer": "newer version survives (default)",
"settings_conflictaction_keep_larger": "larger size version survives",
"settings_cleanemptyfolder": "Action For Empty Folders",
@ -259,6 +259,11 @@
"settings_protectmodifypercentage_000_desc": "0 (always block)",
"settings_protectmodifypercentage_050_desc": "50 (default)",
"settings_protectmodifypercentage_100_desc": "100 (disable the protection)",
"setting_syncdirection": "Sync Direction",
"setting_syncdirection_desc": "Which direction should the plugin sync to? Please be aware that only CHANGED files (based on time and size) are synced regardless any option.",
"setting_syncdirection_bidirectional_desc": "Bidirectional (default)",
"setting_syncdirection_incremental_push_only_desc": "Incremental Push Only (aka backup mode)",
"setting_syncdirection_incremental_pull_only_desc": "Incremental Pull Only",
"settings_importexport": "Import and Export Partial Settings",
"settings_export": "Export",
"settings_export_desc": "Export not-oauth2 settings by generating a qrcode.",
@ -293,8 +298,8 @@
"settings_resetcache_desc": "Reset local internal caches/databases (for debugging purposes). You would want to reload the plugin after resetting this. This option will not empty the {s3, password...} settings.",
"settings_resetcache_button": "Reset",
"settings_resetcache_notice": "Local internal cache/databases deleted. Please manually reload the plugin.",
"syncalgov3_title": "Remotely Save has a HUGE update on the sync algorithm",
"syncalgov3_texts": "Welcome to use Remotely Save!\nFrom this version, a new algorithm has been developed: More robust deletion sync, minimal conflict handling, no meta data uploaded any more, deletion / modification protection... Stay tune for more! A full introduction is in the <a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>doc website</a>.\nIf you agree to use this, please read and check two checkboxes then click the \"Agree\" button, and enjoy the plugin!\nIf you do not agree, please click the \"Do Not Agree\" button, the plugin will unload itself.\nAlso, please consider <a href='https://github.com/remotely-save/remotely-save'>visit the GitHub repo and star ⭐ it</a>! Or even <a href='https://github.com/remotely-save/donation'>buy me a coffee</a>. Your support is very important to me! Thanks!",
"syncalgov3_title": "Remotely Save has HUGE updates on the sync algorithm",
"syncalgov3_texts": "Welcome to use Remotely Save!\nFrom this version, a new algorithm has been developed:\n<ul><li>More robust deletion sync,</li><li>minimal conflict handling,</li><li>no meta data uploaded any more,</li><li>deletion / modification protection,</li><li>backup mode</li><li>...</li></ul>\nStay tune for more! A full introduction is in the <a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>doc website</a>.\nIf you agree to use this, please read and check two checkboxes then click the \"Agree\" button, and enjoy the plugin!\nIf you do not agree, please click the \"Do Not Agree\" button, the plugin will unload itself.\nAlso, please consider <a href='https://github.com/remotely-save/remotely-save'>visit the GitHub repo and star ⭐ it</a>! Or even <a href='https://github.com/remotely-save/donation'>buy me a coffee</a>. Your support is very important to me! Thanks!",
"syncalgov3_checkbox_manual_backup": "I will backup my vault manually firstly.",
"syncalgov3_checkbox_requiremultidevupdate": "I understand I need to update the plugin ACROSS ALL DEVICES to make them work properly.",
"syncalgov3_button_agree": "Agree",

View File

@ -247,7 +247,7 @@
"settings_deletetowhere_system_trash": "系统回收站(默认)",
"settings_deletetowhere_obsidian_trash": "Obsidian .trash 文件夹",
"settings_conflictaction": "处理冲突",
"settings_conflictaction_desc": "如果一个文件,在本地和服务器都被创建或者修改了,那么这就是一个“冲突”情况。如何处理?",
"settings_conflictaction_desc": "如果一个文件,在本地和服务器都被创建或者修改了,那么这就是一个“冲突”情况。如何处理?这个设置只在双向同步时候生效。",
"settings_conflictaction_keep_newer": "保留最后修改的版本(默认)",
"settings_conflictaction_keep_larger": "保留文件体积较大的版本",
"settings_cleanemptyfolder": "处理空文件夹",
@ -259,6 +259,11 @@
"settings_protectmodifypercentage_000_desc": "0总是强制中止",
"settings_protectmodifypercentage_050_desc": "50默认值",
"settings_protectmodifypercentage_100_desc": "100去除此保护",
"setting_syncdirection": "同步方向",
"setting_syncdirection_desc": "插件应该向哪里同步?注意每个选项都是只有修改了的文件(基于修改时间和大小判断)才会触发同步动作。",
"setting_syncdirection_bidirectional_desc": "双向同步(默认)",
"setting_syncdirection_incremental_push_only_desc": "只增量推送(也即:备份模式)",
"setting_syncdirection_incremental_pull_only_desc": "只增量拉取",
"settings_importexport": "导入导出部分设置",
"settings_export": "导出",
"settings_export_desc": "用 QR 码导出非 oauth2 的设置信息。",
@ -294,7 +299,7 @@
"settings_resetcache_button": "重设",
"settings_resetcache_notice": "本地同步缓存和数据库已被删除。请手动重新载入此插件。",
"syncalgov3_title": "Remotely Save 的同步算法有重大更新",
"syncalgov3_texts": "欢迎使用 Remotely Save\n从这个版本开始插件更新了同步算法更稳健的删除同步、引入冲突处理、避免上传元数据、修改删除保护…… 敬请期待更多更新!详细介绍请参阅<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>文档网站</a>。\n如果您同意使用新版本请阅读和勾选两个勾选框然后点击“同意”按钮开始使用插件吧\n如果您不同意请点击“不同意”按钮插件将自动停止运行unload。\n此外请考虑<a href='https://github.com/remotely-save/remotely-save'>访问 GitHub 页面然后点赞 ⭐</a>!您的支持对我十分重要!谢谢!",
"syncalgov3_texts": "欢迎使用 Remotely Save\n从这个版本开始插件更新了同步算法\n<ul><li>更稳健的删除同步</li><li>引入冲突处理</li><li>避免上传元数据</li><li>修改删除保护</li><li>备份模式</li><li>……</li></ul>\n敬请期待更多更新!详细介绍请参阅<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>文档网站</a>。\n如果您同意使用新版本请阅读和勾选两个勾选框然后点击“同意”按钮开始使用插件吧\n如果您不同意请点击“不同意”按钮插件将自动停止运行unload。\n此外请考虑<a href='https://github.com/remotely-save/remotely-save'>访问 GitHub 页面然后点赞 ⭐</a>!您的支持对我十分重要!谢谢!",
"syncalgov3_checkbox_manual_backup": "我将会首先手动备份我的库Vault。",
"syncalgov3_checkbox_requiremultidevupdate": "我理解,我需要在所有设备上都更新此插件使之正常运行。",
"syncalgov3_button_agree": "同意",

View File

@ -247,7 +247,7 @@
"settings_deletetowhere_system_trash": "系統回收站(預設)",
"settings_deletetowhere_obsidian_trash": "Obsidian .trash 資料夾",
"settings_conflictaction": "處理衝突",
"settings_conflictaction_desc": "如果一個檔案,在本地和伺服器都被建立或者修改了,那麼這就是一個“衝突”情況。如何處理?",
"settings_conflictaction_desc": "如果一個檔案,在本地和伺服器都被建立或者修改了,那麼這就是一個“衝突”情況。如何處理?這個設定只在雙向同步時候生效。",
"settings_conflictaction_keep_newer": "保留最後修改的版本(預設)",
"settings_conflictaction_keep_larger": "保留檔案體積較大的版本",
"settings_cleanemptyfolder": "處理空資料夾",
@ -259,6 +259,11 @@
"settings_protectmodifypercentage_000_desc": "0總是強制中止",
"settings_protectmodifypercentage_050_desc": "50預設值",
"settings_protectmodifypercentage_100_desc": "100去除此保護",
"setting_syncdirection": "同步方向",
"setting_syncdirection_desc": "外掛應該向哪裡同步?注意每個選項都是隻有修改了的檔案(基於修改時間和大小判斷)才會觸發同步動作。",
"setting_syncdirection_bidirectional_desc": "雙向同步(預設)",
"setting_syncdirection_incremental_push_only_desc": "只增量推送(也即:備份模式)",
"setting_syncdirection_incremental_pull_only_desc": "只增量拉取",
"settings_importexport": "匯入匯出部分設定",
"settings_export": "匯出",
"settings_export_desc": "用 QR 碼匯出非 oauth2 的設定資訊。",
@ -294,7 +299,7 @@
"settings_resetcache_button": "重設",
"settings_resetcache_notice": "本地同步快取和資料庫已被刪除。請手動重新載入此外掛。",
"syncalgov3_title": "Remotely Save 的同步演算法有重大更新",
"syncalgov3_texts": "歡迎使用 Remotely Save\n從這個版本開始外掛更新了同步演算法更穩健的刪除同步、引入衝突處理、避免上傳元資料、修改刪除保護…… 敬請期待更多更新!詳細介紹請參閱<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>文件網站</a>。\n如果您同意使用新版本請閱讀和勾選兩個勾選框然後點選“同意”按鈕開始使用外掛吧\n如果您不同意請點選“不同意”按鈕外掛將自動停止執行unload。\n此外請考慮<a href='https://github.com/remotely-save/remotely-save'>訪問 GitHub 頁面然後點贊 ⭐</a>!您的支援對我十分重要!謝謝!",
"syncalgov3_texts": "歡迎使用 Remotely Save\n從這個版本開始外掛更新了同步演算法\n<ul><li>更穩健的刪除同步</li><li>引入衝突處理</li><li>避免上傳元資料</li><li>修改刪除保護</li><li>備份模式</li><li>……</li></ul>\n敬請期待更多更新!詳細介紹請參閱<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>文件網站</a>。\n如果您同意使用新版本請閱讀和勾選兩個勾選框然後點選“同意”按鈕開始使用外掛吧\n如果您不同意請點選“不同意”按鈕外掛將自動停止執行unload。\n此外請考慮<a href='https://github.com/remotely-save/remotely-save'>訪問 GitHub 頁面然後點贊 ⭐</a>!您的支援對我十分重要!謝謝!",
"syncalgov3_checkbox_manual_backup": "我將會首先手動備份我的庫Vault。",
"syncalgov3_checkbox_requiremultidevupdate": "我理解,我需要在所有裝置上都更新此外掛使之正常執行。",
"syncalgov3_button_agree": "同意",

View File

@ -94,6 +94,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
conflictAction: "keep_newer",
howToCleanEmptyFolder: "skip",
protectModifyPercentage: 50,
syncDirection: "bidirectional",
};
interface OAuth2Info {
@ -309,7 +310,8 @@ export default class RemotelySavePlugin extends Plugin {
mixedEntityMappings,
this.settings.howToCleanEmptyFolder ?? "skip",
this.settings.skipSizeLargerThan ?? -1,
this.settings.conflictAction ?? "keep_newer"
this.settings.conflictAction ?? "keep_newer",
this.settings.syncDirection ?? "bidirectional"
);
console.info(`mixedEntityMappings:`);
console.info(mixedEntityMappings); // for debugging
@ -888,6 +890,9 @@ export default class RemotelySavePlugin extends Plugin {
if (this.settings.protectModifyPercentage === undefined) {
this.settings.protectModifyPercentage = 50;
}
if (this.settings.syncDirection === undefined) {
this.settings.syncDirection = "bidirectional";
}
await this.saveSettings();
}

View File

@ -18,6 +18,7 @@ import {
EmptyFolderCleanType,
SUPPORTED_SERVICES_TYPE,
SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR,
SyncDirectionType,
VALID_REQURL,
WebdavAuthType,
WebdavDepthType,
@ -1991,6 +1992,31 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
});
});
new Setting(advDiv)
.setName(t("setting_syncdirection"))
.setDesc(t("setting_syncdirection_desc"))
.addDropdown((dropdown) => {
dropdown.addOption(
"bidirectional",
t("setting_syncdirection_bidirectional_desc")
);
dropdown.addOption(
"incremental_push_only",
t("setting_syncdirection_incremental_push_only_desc")
);
dropdown.addOption(
"incremental_pull_only",
t("setting_syncdirection_incremental_pull_only_desc")
);
dropdown
.setValue(this.plugin.settings.syncDirection ?? "bidirectional")
.onChange(async (val) => {
this.plugin.settings.syncDirection = val as SyncDirectionType;
await this.plugin.saveSettings();
});
});
//////////////////////////////////////////////////
// below for import and export functions
//////////////////////////////////////////////////

View File

@ -5,6 +5,7 @@ import type {
EmptyFolderCleanType,
Entity,
MixedEntity,
SyncDirectionType,
} from "./baseTypes";
import { isInsideObsFolder } from "./obsFolderLister";
import {
@ -479,13 +480,14 @@ export const ensembleMixedEnties = async (
/**
* Heavy lifting.
* Basically follow the sync algorithm of https://github.com/Jwink3101/syncrclone
* @param mixedEntityMappings
* Also deal with syncDirection which makes it more complicated
*/
export const getSyncPlanInplace = async (
mixedEntityMappings: Record<string, MixedEntity>,
howToCleanEmptyFolder: EmptyFolderCleanType,
skipSizeLargerThan: number,
conflictAction: ConflictActionType
conflictAction: ConflictActionType,
syncDirection: SyncDirectionType
) => {
// from long(deep) to short(shadow)
const sortedKeys = Object.keys(mixedEntityMappings).sort(
@ -511,14 +513,27 @@ export const getSyncPlanInplace = async (
// should fill the missing part
if (local !== undefined && remote !== undefined) {
mixedEntry.decisionBranch = 101;
mixedEntry.decision = "folder_existed_both";
mixedEntry.decision = "folder_existed_both_then_do_nothing";
} else if (local !== undefined && remote === undefined) {
mixedEntry.decisionBranch = 102;
mixedEntry.decision = "folder_existed_local";
if (syncDirection === "incremental_pull_only") {
mixedEntry.decisionBranch = 107;
mixedEntry.decision = "folder_to_skip";
} else {
mixedEntry.decisionBranch = 102;
mixedEntry.decision =
"folder_existed_local_then_also_create_remote";
}
} else if (local === undefined && remote !== undefined) {
mixedEntry.decisionBranch = 103;
mixedEntry.decision = "folder_existed_remote";
if (syncDirection === "incremental_push_only") {
mixedEntry.decisionBranch = 108;
mixedEntry.decision = "folder_to_skip";
} else {
mixedEntry.decisionBranch = 103;
mixedEntry.decision =
"folder_existed_remote_then_also_create_local";
}
} else {
// why?? how??
mixedEntry.decisionBranch = 104;
mixedEntry.decision = "folder_to_be_created";
}
@ -530,6 +545,7 @@ export const getSyncPlanInplace = async (
} else if (howToCleanEmptyFolder === "clean_both") {
mixedEntry.decisionBranch = 106;
mixedEntry.decision = "folder_to_be_deleted";
// TODO: what to do in different sync direction?
} else {
throw Error(
`do not know how to deal with empty folder ${mixedEntry.key}`
@ -571,9 +587,15 @@ export const getSyncPlanInplace = async (
skipSizeLargerThan <= 0 ||
remote.sizeEnc! <= skipSizeLargerThan
) {
mixedEntry.decisionBranch = 9;
mixedEntry.decision = "modified_remote";
keptFolder.add(getParentFolder(key));
if (syncDirection === "incremental_push_only") {
mixedEntry.decisionBranch = 26;
mixedEntry.decision = "conflict_modified_then_keep_local";
keptFolder.add(getParentFolder(key));
} else {
mixedEntry.decisionBranch = 9;
mixedEntry.decision = "remote_is_modified_then_pull";
keptFolder.add(getParentFolder(key));
}
} else {
throw Error(
`remote is modified (branch 9) but size larger than ${skipSizeLargerThan}, don't know what to do: ${JSON.stringify(
@ -587,9 +609,15 @@ export const getSyncPlanInplace = async (
skipSizeLargerThan <= 0 ||
local.sizeEnc! <= skipSizeLargerThan
) {
mixedEntry.decisionBranch = 10;
mixedEntry.decision = "modified_local";
keptFolder.add(getParentFolder(key));
if (syncDirection === "incremental_pull_only") {
mixedEntry.decisionBranch = 27;
mixedEntry.decision = "conflict_modified_then_keep_remote";
keptFolder.add(getParentFolder(key));
} else {
mixedEntry.decisionBranch = 10;
mixedEntry.decision = "local_is_modified_then_push";
keptFolder.add(getParentFolder(key));
}
} else {
throw Error(
`local is modified (branch 10) but size larger than ${skipSizeLargerThan}, don't know what to do: ${JSON.stringify(
@ -598,64 +626,94 @@ export const getSyncPlanInplace = async (
);
}
} else if (!localEqualPrevSync && !remoteEqualPrevSync) {
// If both compare False, (didn't exist means both are new. Both exist but don't compare means both are modified)
// If both compare False (Didn't exist means both are new. Both exist but don't compare means both are modified)
if (prevSync === undefined) {
if (conflictAction === "keep_newer") {
if (
(local.mtimeCli ?? local.mtimeSvr ?? 0) >=
(remote.mtimeCli ?? remote.mtimeSvr ?? 0)
) {
mixedEntry.decisionBranch = 11;
mixedEntry.decision = "conflict_created_keep_local";
keptFolder.add(getParentFolder(key));
// Didn't exist means both are new
if (syncDirection === "bidirectional") {
if (conflictAction === "keep_newer") {
if (
(local.mtimeCli ?? local.mtimeSvr ?? 0) >=
(remote.mtimeCli ?? remote.mtimeSvr ?? 0)
) {
mixedEntry.decisionBranch = 11;
mixedEntry.decision = "conflict_created_then_keep_local";
keptFolder.add(getParentFolder(key));
} else {
mixedEntry.decisionBranch = 12;
mixedEntry.decision = "conflict_created_then_keep_remote";
keptFolder.add(getParentFolder(key));
}
} else if (conflictAction === "keep_larger") {
if (local.sizeEnc! >= remote.sizeEnc!) {
mixedEntry.decisionBranch = 13;
mixedEntry.decision = "conflict_created_then_keep_local";
keptFolder.add(getParentFolder(key));
} else {
mixedEntry.decisionBranch = 14;
mixedEntry.decision = "conflict_created_then_keep_remote";
keptFolder.add(getParentFolder(key));
}
} else {
mixedEntry.decisionBranch = 12;
mixedEntry.decision = "conflict_created_keep_remote";
mixedEntry.decisionBranch = 15;
mixedEntry.decision = "conflict_created_then_keep_both";
keptFolder.add(getParentFolder(key));
}
} else if (conflictAction === "keep_larger") {
if (local.sizeEnc! >= remote.sizeEnc!) {
mixedEntry.decisionBranch = 13;
mixedEntry.decision = "conflict_created_keep_local";
keptFolder.add(getParentFolder(key));
} else {
mixedEntry.decisionBranch = 14;
mixedEntry.decision = "conflict_created_keep_remote";
keptFolder.add(getParentFolder(key));
}
} else {
mixedEntry.decisionBranch = 15;
mixedEntry.decision = "conflict_created_keep_both";
} else if (syncDirection === "incremental_pull_only") {
mixedEntry.decisionBranch = 22;
mixedEntry.decision = "conflict_created_then_keep_remote";
keptFolder.add(getParentFolder(key));
} else if (syncDirection === "incremental_push_only") {
mixedEntry.decisionBranch = 23;
mixedEntry.decision = "conflict_created_then_keep_local";
keptFolder.add(getParentFolder(key));
} else {
throw Error(
`no idea how to deal with syncDirection=${syncDirection} while conflict created`
);
}
} else {
if (conflictAction === "keep_newer") {
if (
(local.mtimeCli ?? local.mtimeSvr ?? 0) >=
(remote.mtimeCli ?? remote.mtimeSvr ?? 0)
) {
mixedEntry.decisionBranch = 16;
mixedEntry.decision = "conflict_modified_keep_local";
keptFolder.add(getParentFolder(key));
// Both exist but don't compare means both are modified
if (syncDirection === "bidirectional") {
if (conflictAction === "keep_newer") {
if (
(local.mtimeCli ?? local.mtimeSvr ?? 0) >=
(remote.mtimeCli ?? remote.mtimeSvr ?? 0)
) {
mixedEntry.decisionBranch = 16;
mixedEntry.decision = "conflict_modified_then_keep_local";
keptFolder.add(getParentFolder(key));
} else {
mixedEntry.decisionBranch = 17;
mixedEntry.decision = "conflict_modified_then_keep_remote";
keptFolder.add(getParentFolder(key));
}
} else if (conflictAction === "keep_larger") {
if (local.sizeEnc! >= remote.sizeEnc!) {
mixedEntry.decisionBranch = 18;
mixedEntry.decision = "conflict_modified_then_keep_local";
keptFolder.add(getParentFolder(key));
} else {
mixedEntry.decisionBranch = 19;
mixedEntry.decision = "conflict_modified_then_keep_remote";
keptFolder.add(getParentFolder(key));
}
} else {
mixedEntry.decisionBranch = 17;
mixedEntry.decision = "conflict_modified_keep_remote";
mixedEntry.decisionBranch = 20;
mixedEntry.decision = "conflict_modified_then_keep_both";
keptFolder.add(getParentFolder(key));
}
} else if (conflictAction === "keep_larger") {
if (local.sizeEnc! >= remote.sizeEnc!) {
mixedEntry.decisionBranch = 18;
mixedEntry.decision = "conflict_modified_keep_local";
keptFolder.add(getParentFolder(key));
} else {
mixedEntry.decisionBranch = 19;
mixedEntry.decision = "conflict_modified_keep_remote";
keptFolder.add(getParentFolder(key));
}
} else {
mixedEntry.decisionBranch = 20;
mixedEntry.decision = "conflict_modified_keep_both";
} else if (syncDirection === "incremental_pull_only") {
mixedEntry.decisionBranch = 24;
mixedEntry.decision = "conflict_modified_then_keep_remote";
keptFolder.add(getParentFolder(key));
} else if (syncDirection === "incremental_push_only") {
mixedEntry.decisionBranch = 25;
mixedEntry.decision = "conflict_modified_then_keep_local";
keptFolder.add(getParentFolder(key));
} else {
throw Error(
`no idea how to deal with syncDirection=${syncDirection} while conflict modified`
);
}
}
} else {
@ -675,9 +733,15 @@ export const getSyncPlanInplace = async (
skipSizeLargerThan <= 0 ||
remote.sizeEnc! <= skipSizeLargerThan
) {
mixedEntry.decisionBranch = 3;
mixedEntry.decision = "created_remote";
keptFolder.add(getParentFolder(key));
if (syncDirection === "incremental_push_only") {
mixedEntry.decisionBranch = 28;
mixedEntry.decision = "conflict_created_then_do_nothing";
keptFolder.add(getParentFolder(key));
} else {
mixedEntry.decisionBranch = 3;
mixedEntry.decision = "remote_is_created_then_pull";
keptFolder.add(getParentFolder(key));
}
} else {
throw Error(
`remote is created (branch 3) but size larger than ${skipSizeLargerThan}, don't know what to do: ${JSON.stringify(
@ -691,17 +755,33 @@ export const getSyncPlanInplace = async (
prevSync.sizeEnc === remote.sizeEnc
) {
// if B is in the previous list and UNMODIFIED, B has been deleted by A
mixedEntry.decisionBranch = 4;
mixedEntry.decision = "deleted_local";
if (syncDirection === "incremental_push_only") {
mixedEntry.decisionBranch = 29;
mixedEntry.decision = "conflict_created_then_do_nothing";
keptFolder.add(getParentFolder(key));
} else if (syncDirection === "incremental_pull_only") {
mixedEntry.decisionBranch = 35;
mixedEntry.decision = "conflict_created_then_keep_remote";
keptFolder.add(getParentFolder(key));
} else {
mixedEntry.decisionBranch = 4;
mixedEntry.decision = "local_is_deleted_thus_also_delete_remote";
}
} else {
// if B is in the previous list and MODIFIED, B has been deleted by A but modified by B
if (
skipSizeLargerThan <= 0 ||
remote.sizeEnc! <= skipSizeLargerThan
) {
mixedEntry.decisionBranch = 5;
mixedEntry.decision = "modified_remote";
keptFolder.add(getParentFolder(key));
if (syncDirection === "incremental_push_only") {
mixedEntry.decisionBranch = 30;
mixedEntry.decision = "conflict_created_then_do_nothing";
keptFolder.add(getParentFolder(key));
} else {
mixedEntry.decisionBranch = 5;
mixedEntry.decision = "remote_is_modified_then_pull";
keptFolder.add(getParentFolder(key));
}
} else {
throw Error(
`remote is modified (branch 5) but size larger than ${skipSizeLargerThan}, don't know what to do: ${JSON.stringify(
@ -716,9 +796,15 @@ export const getSyncPlanInplace = async (
if (prevSync === undefined) {
// if A is not in the previous list, A is new
if (skipSizeLargerThan <= 0 || local.sizeEnc! <= skipSizeLargerThan) {
mixedEntry.decisionBranch = 6;
mixedEntry.decision = "created_local";
keptFolder.add(getParentFolder(key));
if (syncDirection === "incremental_pull_only") {
mixedEntry.decisionBranch = 31;
mixedEntry.decision = "conflict_created_then_do_nothing";
keptFolder.add(getParentFolder(key));
} else {
mixedEntry.decisionBranch = 6;
mixedEntry.decision = "local_is_created_then_push";
keptFolder.add(getParentFolder(key));
}
} else {
throw Error(
`local is created (branch 6) but size larger than ${skipSizeLargerThan}, don't know what to do: ${JSON.stringify(
@ -732,14 +818,28 @@ export const getSyncPlanInplace = async (
prevSync.sizeEnc === local.sizeEnc
) {
// if A is in the previous list and UNMODIFIED, A has been deleted by B
mixedEntry.decisionBranch = 7;
mixedEntry.decision = "deleted_remote";
if (syncDirection === "incremental_push_only") {
mixedEntry.decisionBranch = 32;
mixedEntry.decision = "conflict_created_then_keep_local";
} else if (syncDirection === "incremental_pull_only") {
mixedEntry.decisionBranch = 33;
mixedEntry.decision = "conflict_created_then_do_nothing";
} else {
mixedEntry.decisionBranch = 7;
mixedEntry.decision = "remote_is_deleted_thus_also_delete_local";
}
} else {
// if A is in the previous list and MODIFIED, A has been deleted by B but modified by A
if (skipSizeLargerThan <= 0 || local.sizeEnc! <= skipSizeLargerThan) {
mixedEntry.decisionBranch = 8;
mixedEntry.decision = "modified_local";
keptFolder.add(getParentFolder(key));
if (syncDirection === "incremental_pull_only") {
mixedEntry.decisionBranch = 34;
mixedEntry.decision = "conflict_created_then_do_nothing";
keptFolder.add(getParentFolder(key));
} else {
mixedEntry.decisionBranch = 8;
mixedEntry.decision = "local_is_modified_then_push";
keptFolder.add(getParentFolder(key));
}
} else {
throw Error(
`local is modified (branch 8) but size larger than ${skipSizeLargerThan}, don't know what to do: ${JSON.stringify(
@ -802,13 +902,14 @@ const splitThreeStepsOnEntityMappings = (
if (
val.decision === "equal" ||
val.decision === "folder_existed_both" ||
val.decision === "conflict_created_then_do_nothing" ||
val.decision === "folder_existed_both_then_do_nothing" ||
val.decision === "folder_to_skip"
) {
// pass
} else if (
val.decision === "folder_existed_local" ||
val.decision === "folder_existed_remote" ||
val.decision === "folder_existed_local_then_also_create_remote" ||
val.decision === "folder_existed_remote_then_also_create_local" ||
val.decision === "folder_to_be_created"
) {
// console.debug(`splitting folder: key=${key},val=${JSON.stringify(val)}`);
@ -823,8 +924,8 @@ const splitThreeStepsOnEntityMappings = (
realTotalCount += 1;
} else if (
val.decision === "only_history" ||
val.decision === "deleted_local" ||
val.decision === "deleted_remote" ||
val.decision === "local_is_deleted_thus_also_delete_remote" ||
val.decision === "remote_is_deleted_thus_also_delete_local" ||
val.decision === "folder_to_be_deleted"
) {
const level = atWhichLevel(key);
@ -840,16 +941,16 @@ const splitThreeStepsOnEntityMappings = (
realModifyDeleteCount += 1;
}
} else if (
val.decision === "modified_local" ||
val.decision === "modified_remote" ||
val.decision === "created_local" ||
val.decision === "created_remote" ||
val.decision === "conflict_created_keep_local" ||
val.decision === "conflict_created_keep_remote" ||
val.decision === "conflict_created_keep_both" ||
val.decision === "conflict_modified_keep_local" ||
val.decision === "conflict_modified_keep_remote" ||
val.decision === "conflict_modified_keep_both"
val.decision === "local_is_modified_then_push" ||
val.decision === "remote_is_modified_then_pull" ||
val.decision === "local_is_created_then_push" ||
val.decision === "remote_is_created_then_pull" ||
val.decision === "conflict_created_then_keep_local" ||
val.decision === "conflict_created_then_keep_remote" ||
val.decision === "conflict_created_then_keep_both" ||
val.decision === "conflict_modified_then_keep_local" ||
val.decision === "conflict_modified_then_keep_remote" ||
val.decision === "conflict_modified_then_keep_both"
) {
if (
uploadDownloads.length === 0 ||
@ -910,16 +1011,17 @@ const dispatchOperationToActualV3 = async (
clearPrevSyncRecordByVaultAndProfile(db, vaultRandomID, profileID, key);
} else if (
r.decision === "equal" ||
r.decision === "conflict_created_then_do_nothing" ||
r.decision === "folder_to_skip" ||
r.decision === "folder_existed_both"
r.decision === "folder_existed_both_then_do_nothing"
) {
// pass
} else if (
r.decision === "modified_local" ||
r.decision === "created_local" ||
r.decision === "folder_existed_local" ||
r.decision === "conflict_created_keep_local" ||
r.decision === "conflict_modified_keep_local"
r.decision === "local_is_modified_then_push" ||
r.decision === "local_is_created_then_push" ||
r.decision === "folder_existed_local_then_also_create_remote" ||
r.decision === "conflict_created_then_keep_local" ||
r.decision === "conflict_modified_then_keep_local"
) {
if (
client.serviceType === "onedrive" &&
@ -949,11 +1051,11 @@ const dispatchOperationToActualV3 = async (
);
}
} else if (
r.decision === "modified_remote" ||
r.decision === "created_remote" ||
r.decision === "conflict_created_keep_remote" ||
r.decision === "conflict_modified_keep_remote" ||
r.decision === "folder_existed_remote"
r.decision === "remote_is_modified_then_pull" ||
r.decision === "remote_is_created_then_pull" ||
r.decision === "conflict_created_then_keep_remote" ||
r.decision === "conflict_modified_then_keep_remote" ||
r.decision === "folder_existed_remote_then_also_create_local"
) {
await mkdirpInVault(r.key, vault);
await client.downloadFromRemote(
@ -969,7 +1071,7 @@ const dispatchOperationToActualV3 = async (
profileID,
r.remote!
);
} else if (r.decision === "deleted_local") {
} else if (r.decision === "local_is_deleted_thus_also_delete_remote") {
// local is deleted, we need to delete remote now
await client.deleteFromRemote(r.key, password, r.remote!.keyEnc);
await clearPrevSyncRecordByVaultAndProfile(
@ -978,7 +1080,7 @@ const dispatchOperationToActualV3 = async (
profileID,
r.key
);
} else if (r.decision === "deleted_remote") {
} else if (r.decision === "remote_is_deleted_thus_also_delete_local") {
// remote is deleted, we need to delete local now
await localDeleteFunc(r.key);
await clearPrevSyncRecordByVaultAndProfile(
@ -988,8 +1090,8 @@ const dispatchOperationToActualV3 = async (
r.key
);
} else if (
r.decision === "conflict_created_keep_both" ||
r.decision === "conflict_modified_keep_both"
r.decision === "conflict_created_then_keep_both" ||
r.decision === "conflict_modified_then_keep_both"
) {
throw Error(`${r.decision} not implemented yet: ${JSON.stringify(r)}`);
} else if (r.decision === "folder_to_be_created") {