fix folder sync

This commit is contained in:
fyears 2024-06-12 23:02:26 +08:00
parent adacda7ee5
commit 9f67d41786
6 changed files with 110 additions and 141 deletions

View File

@ -2,13 +2,6 @@
import AggregateError from "aggregate-error"; import AggregateError from "aggregate-error";
import PQueue from "p-queue"; import PQueue from "p-queue";
import XRegExp from "xregexp"; import XRegExp from "xregexp";
import { checkProRunnableAndFixInplace } from "../pro/src/account";
import { duplicateFile, isMergable, mergeFile } from "../pro/src/conflictLogic";
import {
clearFileContentHistoryByVaultAndProfile,
getFileContentHistoryByVaultAndProfile,
upsertFileContentHistoryByVaultAndProfile,
} from "../pro/src/localdb";
import type { import type {
ConflictActionType, ConflictActionType,
EmptyFolderCleanType, EmptyFolderCleanType,
@ -18,21 +11,21 @@ import type {
SUPPORTED_SERVICES_TYPE, SUPPORTED_SERVICES_TYPE,
SyncDirectionType, SyncDirectionType,
SyncTriggerSourceType, SyncTriggerSourceType,
} from "./baseTypes"; } from "../../src/baseTypes";
import { copyFile, copyFileOrFolder, copyFolder } from "./copyLogic"; import { copyFile, copyFileOrFolder, copyFolder } from "../../src/copyLogic";
import type { FakeFs } from "./fsAll"; import type { FakeFs } from "../../src/fsAll";
import type { FakeFsEncrypt } from "./fsEncrypt"; import type { FakeFsEncrypt } from "../../src/fsEncrypt";
import { import {
type InternalDBs, type InternalDBs,
clearPrevSyncRecordByVaultAndProfile, clearPrevSyncRecordByVaultAndProfile,
getAllPrevSyncRecordsByVaultAndProfile, getAllPrevSyncRecordsByVaultAndProfile,
insertSyncPlanRecordByVault, insertSyncPlanRecordByVault,
upsertPrevSyncRecordByVaultAndProfile, upsertPrevSyncRecordByVaultAndProfile,
} from "./localdb"; } from "../../src/localdb";
import { import {
DEFAULT_FILE_NAME_FOR_METADATAONREMOTE, DEFAULT_FILE_NAME_FOR_METADATAONREMOTE,
DEFAULT_FILE_NAME_FOR_METADATAONREMOTE2, DEFAULT_FILE_NAME_FOR_METADATAONREMOTE2,
} from "./metadataOnRemote"; } from "../../src/metadataOnRemote";
import { import {
atWhichLevel, atWhichLevel,
getParentFolder, getParentFolder,
@ -40,8 +33,15 @@ import {
isSpecialFolderNameToSkip, isSpecialFolderNameToSkip,
roughSizeOfObject, roughSizeOfObject,
unixTimeToStr, unixTimeToStr,
} from "./misc"; } from "../../src/misc";
import type { Profiler } from "./profiler"; import type { Profiler } from "../../src/profiler";
import { checkProRunnableAndFixInplace } from "./account";
import { duplicateFile, isMergable, mergeFile } from "./conflictLogic";
import {
clearFileContentHistoryByVaultAndProfile,
getFileContentHistoryByVaultAndProfile,
upsertFileContentHistoryByVaultAndProfile,
} from "./localdb";
const copyEntityAndFixTimeFormat = ( const copyEntityAndFixTimeFormat = (
src: Entity, src: Entity,
@ -291,7 +291,6 @@ const ensembleMixedEnties = async (
*/ */
const getSyncPlanInplace = async ( const getSyncPlanInplace = async (
mixedEntityMappings: Record<string, MixedEntity>, mixedEntityMappings: Record<string, MixedEntity>,
howToCleanEmptyFolder: EmptyFolderCleanType,
skipSizeLargerThan: number, skipSizeLargerThan: number,
conflictAction: ConflictActionType, conflictAction: ConflictActionType,
syncDirection: SyncDirectionType, syncDirection: SyncDirectionType,
@ -309,7 +308,6 @@ const getSyncPlanInplace = async (
profiler?.insertSize("sizeof sortedKeys", sortedKeys); profiler?.insertSize("sizeof sortedKeys", sortedKeys);
const keptFolder = new Set<string>(); const keptFolder = new Set<string>();
const mayDeleteFolder = new Set<string>();
for (let i = 0; i < sortedKeys.length; ++i) { for (let i = 0; i < sortedKeys.length; ++i) {
if (i % 100 === 0) { if (i % 100 === 0) {
@ -331,7 +329,6 @@ const getSyncPlanInplace = async (
// parent should also be kept // parent should also be kept
// console.debug(`${key} in keptFolder`) // console.debug(`${key} in keptFolder`)
keptFolder.add(getParentFolder(key)); keptFolder.add(getParentFolder(key));
mayDeleteFolder.delete(getParentFolder(key));
// should fill the missing part // should fill the missing part
if (local !== undefined && remote !== undefined) { if (local !== undefined && remote !== undefined) {
mixedEntry.decisionBranch = 101; mixedEntry.decisionBranch = 101;
@ -366,111 +363,103 @@ const getSyncPlanInplace = async (
mixedEntry.change = true; mixedEntry.change = true;
} }
keptFolder.delete(key); // no need to save it in the Set later keptFolder.delete(key); // no need to save it in the Set later
mayDeleteFolder.delete(key); // must ignore this
} else { } else {
if (howToCleanEmptyFolder === "skip") {
mixedEntry.decisionBranch = 105;
mixedEntry.decision = "folder_to_skip";
mixedEntry.change = false;
keptFolder.add(getParentFolder(key)); // we want to keep parent!
mayDeleteFolder.delete(getParentFolder(key)); // we don't want to delete parent!
} else if (howToCleanEmptyFolder === "clean_both") {
if (local !== undefined && remote !== undefined) { if (local !== undefined && remote !== undefined) {
if (syncDirection === "bidirectional") { // both exist, do nothing
if (mayDeleteFolder.has(key)) { mixedEntry.decisionBranch = 121;
// from 0.5.6 and on,
// we only delete the folders caused by file deletion
mixedEntry.decisionBranch = 106;
mixedEntry.decision = "folder_to_be_deleted_on_both";
mixedEntry.change = true;
mayDeleteFolder.add(getParentFolder(key));
mayDeleteFolder.delete(key); // good to remove now
} else {
mixedEntry.decisionBranch = 115;
mixedEntry.decision = "folder_existed_both_then_do_nothing"; mixedEntry.decision = "folder_existed_both_then_do_nothing";
mixedEntry.change = false; mixedEntry.change = false;
keptFolder.add(getParentFolder(key)); // we want to keep parent! keptFolder.add(getParentFolder(key));
mayDeleteFolder.delete(getParentFolder(key)); // we don't want to delete parent! } else if (local !== undefined && remote === undefined) {
} if (prevSync !== undefined) {
} else { // then the folder is deleted on remote
// right now it does nothing because of "incremental" if (syncDirection === "incremental_push_only") {
// TODO: should we delete?? mixedEntry.decisionBranch = 122;
mixedEntry.decisionBranch = 109; mixedEntry.decision = "folder_to_skip";
keptFolder.add(getParentFolder(key));
mixedEntry.change = false;
} else if (syncDirection === "incremental_pull_only") {
mixedEntry.decisionBranch = 123;
mixedEntry.decision = "folder_to_skip"; mixedEntry.decision = "folder_to_skip";
mixedEntry.change = false; mixedEntry.change = false;
keptFolder.add(getParentFolder(key)); // we want to keep parent! keptFolder.add(getParentFolder(key));
mayDeleteFolder.delete(getParentFolder(key)); // we don't want to delete parent! } else {
} // bidirectional
} else if (local !== undefined && remote === undefined) { mixedEntry.decisionBranch = 124;
if (syncDirection === "bidirectional") {
if (mayDeleteFolder.has(key)) {
// from 0.5.6 and on,
// we only delete the folders caused by file deletion
mixedEntry.decisionBranch = 110;
mixedEntry.decision = "folder_to_be_deleted_on_local"; mixedEntry.decision = "folder_to_be_deleted_on_local";
mixedEntry.change = true; mixedEntry.change = true;
mayDeleteFolder.add(getParentFolder(key)); }
mayDeleteFolder.delete(key); // good to remove now
} else { } else {
// the folder might be created locally // then the folder is created on local
// so we want to create it remotely as well.
mixedEntry.decisionBranch = 116; if (syncDirection === "incremental_push_only") {
mixedEntry.decisionBranch = 125;
mixedEntry.decision = mixedEntry.decision =
"folder_existed_local_then_also_create_remote"; "folder_existed_local_then_also_create_remote";
mixedEntry.change = false; mixedEntry.change = true;
keptFolder.add(getParentFolder(key)); // we want to keep parent! keptFolder.add(getParentFolder(key));
mayDeleteFolder.delete(getParentFolder(key)); // we don't want to delete parent! } else if (syncDirection === "incremental_pull_only") {
} mixedEntry.decisionBranch = 126;
} else {
// right now it does nothing because of "incremental"
// TODO: should we delete??
mixedEntry.decisionBranch = 111;
mixedEntry.decision = "folder_to_skip"; mixedEntry.decision = "folder_to_skip";
mixedEntry.change = false; mixedEntry.change = false;
keptFolder.add(getParentFolder(key)); // we want to keep parent! keptFolder.add(getParentFolder(key));
mayDeleteFolder.delete(getParentFolder(key)); // we don't want to delete parent! } else {
// bidirectional
mixedEntry.decisionBranch = 127;
mixedEntry.decision =
"folder_existed_local_then_also_create_remote";
mixedEntry.change = true;
keptFolder.add(getParentFolder(key));
}
} }
} else if (local === undefined && remote !== undefined) { } else if (local === undefined && remote !== undefined) {
if (syncDirection === "bidirectional") { if (prevSync !== undefined) {
if (mayDeleteFolder.has(key)) { // then the folder is deleted on local
// from 0.5.6 and on, if (syncDirection === "incremental_push_only") {
// we only delete the folders caused by file deletion mixedEntry.decisionBranch = 128;
mixedEntry.decisionBranch = 112;
mixedEntry.decision = "folder_to_be_deleted_on_remote";
mixedEntry.change = true;
mayDeleteFolder.add(getParentFolder(key));
mayDeleteFolder.delete(key); // good to remove now
} else {
// the folder might be created remotely
// so we want to create it locally as well.
mixedEntry.decisionBranch = 117;
mixedEntry.decision =
"folder_existed_remote_then_also_create_local";
mixedEntry.change = false;
keptFolder.add(getParentFolder(key)); // we want to keep parent!
mayDeleteFolder.delete(getParentFolder(key)); // we don't want to delete parent!
}
} else {
// right now it does nothing because of "incremental"
// TODO: should we delete??
mixedEntry.decisionBranch = 113;
mixedEntry.decision = "folder_to_skip"; mixedEntry.decision = "folder_to_skip";
mixedEntry.change = false; mixedEntry.change = false;
keptFolder.add(getParentFolder(key)); // we want to keep parent! keptFolder.add(getParentFolder(key));
mayDeleteFolder.delete(getParentFolder(key)); // we don't want to delete parent! } else if (syncDirection === "incremental_pull_only") {
mixedEntry.decisionBranch = 129;
mixedEntry.decision = "folder_to_skip";
mixedEntry.change = false;
keptFolder.add(getParentFolder(key));
} else {
// bidirectional
mixedEntry.decisionBranch = 130;
mixedEntry.decision = "folder_to_be_deleted_on_remote";
mixedEntry.change = true;
}
} else {
// then the folder is created on remote
if (syncDirection === "incremental_push_only") {
mixedEntry.decisionBranch = 131;
mixedEntry.decision = "folder_to_skip";
mixedEntry.change = false;
keptFolder.add(getParentFolder(key));
} else if (syncDirection === "incremental_pull_only") {
mixedEntry.decisionBranch = 132;
mixedEntry.decision =
"folder_existed_remote_then_also_create_local";
mixedEntry.change = true;
keptFolder.add(getParentFolder(key));
} else {
// bidirectional
mixedEntry.decisionBranch = 133;
mixedEntry.decision =
"folder_existed_remote_then_also_create_local";
mixedEntry.change = true;
keptFolder.add(getParentFolder(key));
}
} }
} else { } else {
// local === undefined && remote === undefined // local === undefined && remote === undefined
// no folder to delete, do nothing // no folder to delete or create, do nothing
mixedEntry.decisionBranch = 114; mixedEntry.decisionBranch = 134;
mixedEntry.decision = "folder_to_skip"; mixedEntry.decision = "folder_to_skip";
mixedEntry.change = false; mixedEntry.change = false;
} }
} else {
throw Error(
`do not know how to deal with empty folder ${mixedEntry.key}`
);
}
} }
} else { } else {
// file // file
@ -712,7 +701,6 @@ const getSyncPlanInplace = async (
mixedEntry.decisionBranch = 4; mixedEntry.decisionBranch = 4;
mixedEntry.decision = "local_is_deleted_thus_also_delete_remote"; mixedEntry.decision = "local_is_deleted_thus_also_delete_remote";
mixedEntry.change = true; mixedEntry.change = true;
mayDeleteFolder.add(getParentFolder(key));
} }
} else { } else {
// if B is in the previous list and MODIFIED, B has been deleted by A but modified by B // if B is in the previous list and MODIFIED, B has been deleted by A but modified by B
@ -780,7 +768,6 @@ const getSyncPlanInplace = async (
mixedEntry.decisionBranch = 7; mixedEntry.decisionBranch = 7;
mixedEntry.decision = "remote_is_deleted_thus_also_delete_local"; mixedEntry.decision = "remote_is_deleted_thus_also_delete_local";
mixedEntry.change = true; mixedEntry.change = true;
mayDeleteFolder.add(getParentFolder(key));
} }
} else { } else {
// if A is in the previous list and MODIFIED, A has been deleted by B but modified by A // if A is in the previous list and MODIFIED, A has been deleted by B but modified by A
@ -826,9 +813,6 @@ const getSyncPlanInplace = async (
keptFolder.delete("/"); keptFolder.delete("/");
keptFolder.delete(""); keptFolder.delete("");
mayDeleteFolder.delete("/");
mayDeleteFolder.delete("");
if (keptFolder.size > 0) { if (keptFolder.size > 0) {
throw Error(`unexpectedly keptFolder no decisions: ${[...keptFolder]}`); throw Error(`unexpectedly keptFolder no decisions: ${[...keptFolder]}`);
} }
@ -1643,7 +1627,6 @@ export async function syncer(
mixedEntityMappings = await getSyncPlanInplace( mixedEntityMappings = await getSyncPlanInplace(
mixedEntityMappings, mixedEntityMappings,
settings.howToCleanEmptyFolder ?? "clean_both",
settings.skipSizeLargerThan ?? -1, settings.skipSizeLargerThan ?? -1,
settings.conflictAction ?? "keep_newer", settings.conflictAction ?? "keep_newer",
settings.syncDirection ?? "bidirectional", settings.syncDirection ?? "bidirectional",

View File

@ -165,7 +165,6 @@ export interface RemotelySavePluginSettings {
enableStatusBarInfo?: boolean; enableStatusBarInfo?: boolean;
deleteToWhere?: "system" | "obsidian"; deleteToWhere?: "system" | "obsidian";
conflictAction?: ConflictActionType; conflictAction?: ConflictActionType;
howToCleanEmptyFolder?: EmptyFolderCleanType;
protectModifyPercentage?: number; protectModifyPercentage?: number;
syncDirection?: SyncDirectionType; syncDirection?: SyncDirectionType;
@ -194,6 +193,11 @@ export interface RemotelySavePluginSettings {
* @deprecated * @deprecated
*/ */
logToDB?: boolean; logToDB?: boolean;
/**
* @deprecated
*/
howToCleanEmptyFolder?: EmptyFolderCleanType;
} }
export const COMMAND_URI = "remotely-save"; export const COMMAND_URI = "remotely-save";

View File

@ -1,5 +1,6 @@
import type { Vault } from "obsidian"; import type { Vault } from "obsidian";
import type { SyncPlanType } from "../pro/src/sync";
import { import {
DEFAULT_DEBUG_FOLDER, DEFAULT_DEBUG_FOLDER,
DEFAULT_PROFILER_RESULT_FILE_PREFIX, DEFAULT_PROFILER_RESULT_FILE_PREFIX,
@ -11,7 +12,6 @@ import {
} from "./localdb"; } from "./localdb";
import type { InternalDBs } from "./localdb"; import type { InternalDBs } from "./localdb";
import { mkdirpInVault } from "./misc"; import { mkdirpInVault } from "./misc";
import type { SyncPlanType } from "./sync";
const getSubsetOfSyncPlan = (x: string, onlyChange: boolean) => { const getSubsetOfSyncPlan = (x: string, onlyChange: boolean) => {
if (!onlyChange) { if (!onlyChange) {

View File

@ -6,9 +6,9 @@ ep2(localforage);
export type LocalForage = typeof localforage; export type LocalForage = typeof localforage;
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import type { SyncPlanType } from "../pro/src/sync";
import type { Entity, SUPPORTED_SERVICES_TYPE } from "./baseTypes"; import type { Entity, SUPPORTED_SERVICES_TYPE } from "./baseTypes";
import { unixTimeToStr } from "./misc"; import { unixTimeToStr } from "./misc";
import type { SyncPlanType } from "./sync";
const DB_VERSION_NUMBER_IN_HISTORY = [20211114, 20220108, 20220326, 20240220]; const DB_VERSION_NUMBER_IN_HISTORY = [20211114, 20220108, 20220326, 20240220];
export const DEFAULT_DB_VERSION_NUMBER: number = 20240220; export const DEFAULT_DB_VERSION_NUMBER: number = 20240220;

View File

@ -48,6 +48,7 @@ import {
sendAuthReq as sendAuthReqYandexDisk, sendAuthReq as sendAuthReqYandexDisk,
setConfigBySuccessfullAuthInplace as setConfigBySuccessfullAuthInplaceYandexDisk, setConfigBySuccessfullAuthInplace as setConfigBySuccessfullAuthInplaceYandexDisk,
} from "../pro/src/fsYandexDisk"; } from "../pro/src/fsYandexDisk";
import { syncer } from "../pro/src/sync";
import type { import type {
RemotelySavePluginSettings, RemotelySavePluginSettings,
SyncTriggerSourceType, SyncTriggerSourceType,
@ -93,7 +94,6 @@ import {
import { changeMobileStatusBar } from "./misc"; import { changeMobileStatusBar } from "./misc";
import { DEFAULT_PROFILER_CONFIG, type Profiler } from "./profiler"; import { DEFAULT_PROFILER_CONFIG, type Profiler } from "./profiler";
import { RemotelySaveSettingTab } from "./settings"; import { RemotelySaveSettingTab } from "./settings";
import { syncer } from "./sync";
import { SyncAlgoV3Modal } from "./syncAlgoV3Notice"; import { SyncAlgoV3Modal } from "./syncAlgoV3Notice";
const DEFAULT_SETTINGS: RemotelySavePluginSettings = { const DEFAULT_SETTINGS: RemotelySavePluginSettings = {

View File

@ -2250,24 +2250,6 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
}); });
}); });
new Setting(advDiv)
.setName(t("settings_cleanemptyfolder"))
.setDesc(t("settings_cleanemptyfolder_desc"))
.addDropdown((dropdown) => {
dropdown.addOption("skip", t("settings_cleanemptyfolder_skip"));
dropdown.addOption(
"clean_both",
t("settings_cleanemptyfolder_clean_both")
);
dropdown
.setValue(this.plugin.settings.howToCleanEmptyFolder ?? "clean_both")
.onChange(async (val) => {
this.plugin.settings.howToCleanEmptyFolder =
val as EmptyFolderCleanType;
await this.plugin.saveSettings();
});
});
const percentage1 = new Setting(advDiv) const percentage1 = new Setting(advDiv)
.setName(t("settings_protectmodifypercentage")) .setName(t("settings_protectmodifypercentage"))
.setDesc(t("settings_protectmodifypercentage_desc")); .setDesc(t("settings_protectmodifypercentage_desc"));