Compare commits

..
7 Commits
Author SHA1 Message Date
fyears 5aa96a4ddb 0.3.4 2022-03-06 23:16:34 +08:00
fyears 2e6848f901 combine mtime and ctime 2022-03-06 23:15:59 +08:00
fyears 9c830c2c14 check for undefined decision 2022-03-06 19:03:24 +08:00
fyears 3574b56acb add more options to concurrency 2022-03-06 18:52:56 +08:00
fyears 18cb8ba413 add sync once after start up 2022-03-06 18:49:15 +08:00
fyears 76e61b6ebf upload and download in parallel 2022-03-06 18:12:49 +08:00
fyears 3dfe215103 better sync operations for folders 2022-03-06 13:22:24 +08:00
8 changed files with 277 additions and 52 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"id": "remotely-save", "id": "remotely-save",
"name": "Remotely Save", "name": "Remotely Save",
"version": "0.3.2", "version": "0.3.4",
"minAppVersion": "0.12.15", "minAppVersion": "0.13.21",
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.", "description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
"author": "fyears", "author": "fyears",
"authorUrl": "https://github.com/fyears", "authorUrl": "https://github.com/fyears",
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "remotely-save", "name": "remotely-save",
"version": "0.3.2", "version": "0.3.4",
"description": "This is yet another sync plugin for Obsidian app.", "description": "This is yet another sync plugin for Obsidian app.",
"scripts": { "scripts": {
"dev2": "node esbuild.config.mjs", "dev2": "node esbuild.config.mjs",
@@ -70,7 +70,8 @@
"loglevel": "^1.8.0", "loglevel": "^1.8.0",
"mime-types": "^2.1.33", "mime-types": "^2.1.33",
"nanoid": "^3.1.30", "nanoid": "^3.1.30",
"obsidian": "^0.12.0", "obsidian": "^0.13.26",
"p-queue": "^7.2.0",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"process": "^0.11.10", "process": "^0.11.10",
"qrcode": "^1.5.0", "qrcode": "^1.5.0",
+4
View File
@@ -56,7 +56,9 @@ export interface RemotelySavePluginSettings {
currLogLevel?: string; currLogLevel?: string;
vaultRandomID?: string; vaultRandomID?: string;
autoRunEveryMilliseconds?: number; autoRunEveryMilliseconds?: number;
initRunAfterMilliseconds?: number;
agreeToUploadExtraMetadata?: boolean; agreeToUploadExtraMetadata?: boolean;
concurrency?: number;
} }
export interface RemoteItem { export interface RemoteItem {
@@ -113,3 +115,5 @@ export interface FileOrFolderMixedState {
syncDone?: "done"; syncDone?: "done";
remoteEncryptedKey?: string; remoteEncryptedKey?: string;
} }
export const API_VER_STAT_FOLDER = "0.13.27";
+29 -6
View File
@@ -53,7 +53,9 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
currLogLevel: "info", currLogLevel: "info",
vaultRandomID: "", vaultRandomID: "",
autoRunEveryMilliseconds: -1, autoRunEveryMilliseconds: -1,
initRunAfterMilliseconds: -1,
agreeToUploadExtraMetadata: false, agreeToUploadExtraMetadata: false,
concurrency: 5,
}; };
interface OAuth2Info { interface OAuth2Info {
@@ -64,7 +66,7 @@ interface OAuth2Info {
revokeAuthSetting?: Setting; revokeAuthSetting?: Setting;
} }
type SyncTriggerSourceType = "manual" | "auto" | "dry"; type SyncTriggerSourceType = "manual" | "auto" | "dry" | "autoOnceInit";
const iconNameSyncWait = `remotely-save-sync-wait`; const iconNameSyncWait = `remotely-save-sync-wait`;
const iconNameSyncRunning = `remotely-save-sync-running`; const iconNameSyncRunning = `remotely-save-sync-running`;
@@ -199,6 +201,7 @@ export default class RemotelySavePlugin extends Plugin {
origMetadataOnRemote.deletions, origMetadataOnRemote.deletions,
localHistory, localHistory,
client.serviceType, client.serviceType,
this.app.vault,
this.settings.password this.settings.password
); );
log.info(plan.mixedStates); // for debugging log.info(plan.mixedStates); // for debugging
@@ -229,6 +232,7 @@ export default class RemotelySavePlugin extends Plugin {
deletions, deletions,
(key: string) => self.trash(key), (key: string) => self.trash(key),
this.settings.password, this.settings.password,
this.settings.concurrency,
(i: number, totalCount: number, pathName: string, decision: string) => (i: number, totalCount: number, pathName: string, decision: string) =>
self.setCurrSyncMsg(i, totalCount, pathName, decision) self.setCurrSyncMsg(i, totalCount, pathName, decision)
); );
@@ -531,6 +535,9 @@ export default class RemotelySavePlugin extends Plugin {
if (!this.settings.agreeToUploadExtraMetadata) { if (!this.settings.agreeToUploadExtraMetadata) {
const syncAlgoV2Modal = new SyncAlgoV2Modal(this.app, this); const syncAlgoV2Modal = new SyncAlgoV2Modal(this.app, this);
syncAlgoV2Modal.open(); syncAlgoV2Modal.open();
} else {
this.enableAutoSyncIfSet();
this.enableInitSyncIfSet();
} }
} }
@@ -659,11 +666,27 @@ export default class RemotelySavePlugin extends Plugin {
this.settings.autoRunEveryMilliseconds !== null && this.settings.autoRunEveryMilliseconds !== null &&
this.settings.autoRunEveryMilliseconds > 0 this.settings.autoRunEveryMilliseconds > 0
) { ) {
const intervalID = window.setInterval(() => { this.app.workspace.onLayoutReady(() => {
this.syncRun("auto"); const intervalID = window.setInterval(() => {
}, this.settings.autoRunEveryMilliseconds); this.syncRun("auto");
this.autoRunIntervalID = intervalID; }, this.settings.autoRunEveryMilliseconds);
this.registerInterval(intervalID); this.autoRunIntervalID = intervalID;
this.registerInterval(intervalID);
});
}
}
enableInitSyncIfSet() {
if (
this.settings.initRunAfterMilliseconds !== undefined &&
this.settings.initRunAfterMilliseconds !== null &&
this.settings.initRunAfterMilliseconds > 0
) {
this.app.workspace.onLayoutReady(() => {
window.setTimeout(() => {
this.syncRun("autoOnceInit");
}, this.settings.initRunAfterMilliseconds);
});
} }
} }
+49
View File
@@ -521,6 +521,55 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
}); });
}); });
const runOnceStartUpDiv = generalDiv.createEl("div");
new Setting(runOnceStartUpDiv)
.setName("run once on start up automatically")
.setDesc(
`This settings allows setting running ONCE on start up automatically. This will take effect on NEXT start up after changing. This setting, is different from "schedule for auto run" which starts syncing after EVERY interval.`
)
.addDropdown((dropdown) => {
dropdown.addOption("-1", "(not set)");
dropdown.addOption(
`${1000 * 10 * 1}`,
"sync once after 10 seconds of start up"
);
dropdown.addOption(
`${1000 * 30 * 1}`,
"sync once after 30 seconds of start up"
);
dropdown
.setValue(`${this.plugin.settings.initRunAfterMilliseconds}`)
.onChange(async (val: string) => {
const realVal = parseInt(val);
this.plugin.settings.initRunAfterMilliseconds = realVal;
await this.plugin.saveSettings();
});
});
const concurrencyDiv = generalDiv.createEl("div");
new Setting(concurrencyDiv)
.setName("Concurrency")
.setDesc(
"How many files do you want to download or upload in parallel at most? By default it's set to 5. If you meet any problems such as rate limit, you can reduce the concurrency to a lower value."
)
.addDropdown((dropdown) => {
dropdown.addOption("1", "1");
dropdown.addOption("2", "2");
dropdown.addOption("3", "3");
dropdown.addOption("5", "5 (default)");
dropdown.addOption("10", "10");
dropdown.addOption("15", "15");
dropdown.addOption("20", "20");
dropdown
.setValue(`${this.plugin.settings.concurrency}`)
.onChange(async (val) => {
const realVal = parseInt(val);
this.plugin.settings.concurrency = realVal;
await this.plugin.saveSettings();
});
});
////////////////////////////////////////////////// //////////////////////////////////////////////////
// below for general chooser (part 1/2) // below for general chooser (part 1/2)
////////////////////////////////////////////////// //////////////////////////////////////////////////
+187 -41
View File
@@ -1,9 +1,17 @@
import { TAbstractFile, TFile, TFolder, Vault } from "obsidian"; import {
import type { TAbstractFile,
TFile,
TFolder,
Vault,
requireApiVersion,
} from "obsidian";
import PQueue from "p-queue";
import {
RemoteItem, RemoteItem,
SUPPORTED_SERVICES_TYPE, SUPPORTED_SERVICES_TYPE,
DecisionType, DecisionType,
FileOrFolderMixedState, FileOrFolderMixedState,
API_VER_STAT_FOLDER,
} from "./baseTypes"; } from "./baseTypes";
import { import {
decryptBase32ToString, decryptBase32ToString,
@@ -293,7 +301,7 @@ const ensembleMixedStates = async (
r = { r = {
key: entry.path, key: entry.path,
existLocal: true, existLocal: true,
mtimeLocal: entry.stat.mtime, mtimeLocal: Math.max(entry.stat.mtime, entry.stat.ctime),
sizeLocal: entry.stat.size, sizeLocal: entry.stat.size,
}; };
} else if (entry instanceof TFolder) { } else if (entry instanceof TFolder) {
@@ -505,9 +513,10 @@ const assignOperationToFileInplace = (
throw Error(`no decision for ${JSON.stringify(r)}`); throw Error(`no decision for ${JSON.stringify(r)}`);
}; };
const assignOperationToFolderInplace = ( const assignOperationToFolderInplace = async (
origRecord: FileOrFolderMixedState, origRecord: FileOrFolderMixedState,
keptFolder: Set<string>, keptFolder: Set<string>,
vault: Vault,
password: string = "" password: string = ""
) => { ) => {
let r = origRecord; let r = origRecord;
@@ -523,10 +532,42 @@ const assignOperationToFolderInplace = (
if (r.deltimeLocal !== undefined || r.deltimeRemote !== undefined) { if (r.deltimeLocal !== undefined || r.deltimeRemote !== undefined) {
// it has some deletion "commands" // it has some deletion "commands"
if (
r.deltimeLocal !== undefined && const deltimeLocal = r.deltimeLocal !== undefined ? r.deltimeLocal : -1;
r.deltimeLocal >= (r.deltimeRemote !== undefined ? r.deltimeRemote : -1) const deltimeRemote =
) { r.deltimeRemote !== undefined ? r.deltimeRemote : -1;
// if it was created after deletion, we should keep it as is
if (requireApiVersion(API_VER_STAT_FOLDER)) {
if (r.existLocal) {
try {
const { ctime, mtime } = await vault.adapter.stat(r.key);
const cmtime = Math.max(ctime, mtime);
if (
cmtime > 0 &&
cmtime >= deltimeLocal &&
cmtime >= deltimeRemote
) {
keptFolder.add(getParentFolder(r.key));
if (r.existLocal && r.existRemote) {
r.decision = "skipFolder";
r.decisionBranch = 14;
} else if (r.existLocal || r.existRemote) {
r.decision = "createFolder";
r.decisionBranch = 15;
} else {
throw Error(
`Error: Folder ${r.key} doesn't exist locally and remotely but is marked must be kept. Abort.`
);
}
}
} catch (error) {
// pass
}
}
}
if (deltimeLocal > 0 && deltimeLocal > deltimeRemote) {
r.decision = "uploadLocalDelHistToRemoteFolder"; r.decision = "uploadLocalDelHistToRemoteFolder";
r.decisionBranch = 8; r.decisionBranch = 8;
} else { } else {
@@ -585,6 +626,7 @@ export const getSyncPlan = async (
remoteDeleteHistory: DeletionOnRemote[], remoteDeleteHistory: DeletionOnRemote[],
localDeleteHistory: FileFolderHistoryRecord[], localDeleteHistory: FileFolderHistoryRecord[],
remoteType: SUPPORTED_SERVICES_TYPE, remoteType: SUPPORTED_SERVICES_TYPE,
vault: Vault,
password: string = "" password: string = ""
) => { ) => {
const mixedStates = await ensembleMixedStates( const mixedStates = await ensembleMixedStates(
@@ -609,7 +651,7 @@ export const getSyncPlan = async (
// decide some folders // decide some folders
// because the keys are sorted by length // because the keys are sorted by length
// so all the children must have been shown up before in the iteration // so all the children must have been shown up before in the iteration
assignOperationToFolderInplace(val, keptFolder, password); await assignOperationToFolderInplace(val, keptFolder, vault, password);
} else { } else {
// get all operations of files // get all operations of files
// and at the same time get some helper info for folders // and at the same time get some helper info for folders
@@ -850,10 +892,10 @@ export const doActualSync = async (
deletions: DeletionOnRemote[], deletions: DeletionOnRemote[],
localDeleteFunc: any, localDeleteFunc: any,
password: string = "", password: string = "",
concurrency: number = 1,
callbackSyncProcess?: any callbackSyncProcess?: any
) => { ) => {
const mixedStates = syncPlan.mixedStates; const mixedStates = syncPlan.mixedStates;
let i = 0;
const totalCount = sortedKeys.length || 0; const totalCount = sortedKeys.length || 0;
log.debug(`start syncing extra data firstly`); log.debug(`start syncing extra data firstly`);
@@ -866,41 +908,145 @@ export const doActualSync = async (
); );
log.debug(`finish syncing extra data firstly`); log.debug(`finish syncing extra data firstly`);
for (let i = 0; i < sortedKeys.length; ++i) { log.debug(`concurrency === ${concurrency}`);
const key = sortedKeys[i]; if (concurrency === 1) {
const val = mixedStates[key]; // run everything in sequence
// good old way
for (let i = 0; i < sortedKeys.length; ++i) {
const key = sortedKeys[i];
const val = mixedStates[key];
log.debug(`start syncing "${key}" with plan ${JSON.stringify(val)}`); log.debug(`start syncing "${key}" with plan ${JSON.stringify(val)}`);
if (callbackSyncProcess !== undefined) { if (callbackSyncProcess !== undefined) {
await callbackSyncProcess(i, totalCount, key, val.decision); await callbackSyncProcess(i, totalCount, key, val.decision);
}
await dispatchOperationToActual(
key,
vaultRandomID,
val,
client,
db,
vault,
localDeleteFunc,
password
);
log.debug(`finished ${key}`);
}
} else {
let realCounter = 0;
log.debug(
`1. create all folders from shadowest to deepest, also check undefined decision`
);
for (let i = sortedKeys.length - 1; i >= 0; --i) {
const key = sortedKeys[i];
const val = mixedStates[key];
if (
val.decision === undefined ||
val.decision === "skipFolder" ||
val.decision === "createFolder"
) {
log.debug(`start syncing "${key}" with plan ${JSON.stringify(val)}`);
if (callbackSyncProcess !== undefined) {
await callbackSyncProcess(realCounter, totalCount, key, val.decision);
}
realCounter += 1;
await dispatchOperationToActual(
key,
vaultRandomID,
val,
client,
db,
vault,
localDeleteFunc,
password
);
log.debug(`finished ${key}`);
}
} }
await dispatchOperationToActual( log.debug(`2. delete files and folders from deepest to shadowest`);
key, for (let i = 0; i < sortedKeys.length; ++i) {
vaultRandomID, const key = sortedKeys[i];
val, const val = mixedStates[key];
client, if (
db, val.decision === "uploadLocalDelHistToRemoteFolder" ||
vault, val.decision === "keepRemoteDelHistFolder"
localDeleteFunc, ) {
password log.debug(`start syncing "${key}" with plan ${JSON.stringify(val)}`);
);
log.debug(`finished ${key}`);
// await Promise.all( if (callbackSyncProcess !== undefined) {
// Object.entries(mixedStates).map(async ([k, v]) => await callbackSyncProcess(realCounter, totalCount, key, val.decision);
// dispatchOperationToActual( }
// k as string, realCounter += 1;
// vaultRandomID,
// v as FileOrFolderMixedState, await dispatchOperationToActual(
// client, key,
// db, vaultRandomID,
// vault, val,
// localDeleteFunc, client,
// password db,
// ) vault,
// ) localDeleteFunc,
// ); password
);
log.debug(`finished ${key}`);
}
}
log.debug(
`3. upload or download files in parallel, with the desired concurrency=${concurrency}`
);
const queue = new PQueue({ concurrency: concurrency, autoStart: true });
// const commands: any[] = [];
for (let i = 0; i < sortedKeys.length; ++i) {
const key = sortedKeys[i];
const val = mixedStates[key];
if (
val.decision === "skipUploading" ||
val.decision === "uploadLocalDelHistToRemote" ||
val.decision === "keepRemoteDelHist" ||
val.decision === "uploadLocalToRemote" ||
val.decision === "downloadRemoteToLocal"
) {
const fn = async () => {
log.debug(`start syncing "${key}" with plan ${JSON.stringify(val)}`);
if (callbackSyncProcess !== undefined) {
await callbackSyncProcess(
realCounter,
totalCount,
key,
val.decision
);
realCounter += 1;
}
await dispatchOperationToActual(
key,
vaultRandomID,
val,
client,
db,
vault,
localDeleteFunc,
password
);
log.debug(`finished ${key}`);
};
queue.add(fn);
}
}
await queue.onIdle();
} }
}; };
+1
View File
@@ -60,6 +60,7 @@ export class SyncAlgoV2Modal extends Modal {
log.info("agree to use the new algorithm"); log.info("agree to use the new algorithm");
this.plugin.saveAgreeToUseNewSyncAlgorithm(); this.plugin.saveAgreeToUseNewSyncAlgorithm();
this.plugin.enableAutoSyncIfSet(); this.plugin.enableAutoSyncIfSet();
this.plugin.enableInitSyncIfSet();
} else { } else {
log.info("do not agree to use the new algorithm"); log.info("do not agree to use the new algorithm");
this.plugin.unload(); this.plugin.unload();
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"0.3.2": "0.12.15" "0.3.2": "0.12.15",
"0.3.4": "0.13.21"
} }