Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5aa96a4ddb | ||
|
|
2e6848f901 | ||
|
|
9c830c2c14 | ||
|
|
3574b56acb | ||
|
|
18cb8ba413 | ||
|
|
76e61b6ebf | ||
|
|
3dfe215103 | ||
|
|
94c7a4a874 | ||
|
|
b573b2859c | ||
|
|
e2c4d158b1 | ||
|
|
55890611e2 | ||
|
|
5acd1551c9 | ||
|
|
ef2c45a11d | ||
|
|
c1771bb117 | ||
|
|
ddb86745dd | ||
|
|
971719b710 |
@@ -0,0 +1,65 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: File a bug report
|
||||||
|
title: "[Bug]: "
|
||||||
|
labels: ["bug"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened?
|
||||||
|
description: What did you expect to happen?
|
||||||
|
placeholder: Tell us what you see!
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: What OS are you using?
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Windows
|
||||||
|
- Linux
|
||||||
|
- macOS
|
||||||
|
- iOS (iPhone, iPad)
|
||||||
|
- Android
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: service
|
||||||
|
attributes:
|
||||||
|
label: What remote cloud services are you using?
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- S3
|
||||||
|
- OneDrive for personal
|
||||||
|
- Dropbox
|
||||||
|
- webdav
|
||||||
|
- others
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: version-plugin
|
||||||
|
attributes:
|
||||||
|
label: Version of the plugin
|
||||||
|
description: What version of the plugin are you running?
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
id: version-obsidian
|
||||||
|
attributes:
|
||||||
|
label: Version of Obsidian
|
||||||
|
description: What version of Obsidian are you running?
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
id: ensure-no-sensitive-information
|
||||||
|
attributes:
|
||||||
|
label: Ensure no sensitive information
|
||||||
|
description: By submitting this issue, you ensure that no sensitive information is submitted in the issue.
|
||||||
|
options:
|
||||||
|
- label: I ensure that no sensitive information is submitted in the issue.
|
||||||
|
required: true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Some idea about new features
|
||||||
|
title: "[Feature Request]: "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
- type: textarea
|
||||||
|
id: what-are-you-expecting
|
||||||
|
attributes:
|
||||||
|
label: What feature are you suggesting?
|
||||||
|
description: What feature are you suggesting?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: service
|
||||||
|
attributes:
|
||||||
|
label: What remote cloud services are you using, or suggesting adding the feature to?
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- S3
|
||||||
|
- OneDrive for personal
|
||||||
|
- Dropbox
|
||||||
|
- webdav
|
||||||
|
- others
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
id: ensure-no-sensitive-information
|
||||||
|
attributes:
|
||||||
|
label: Ensure no sensitive information
|
||||||
|
description: By submitting this issue, you ensure that no sensitive information is submitted in the issue.
|
||||||
|
options:
|
||||||
|
- label: I ensure that no sensitive information is submitted in the issue.
|
||||||
|
required: true
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"id": "remotely-save",
|
"id": "remotely-save",
|
||||||
"name": "Remotely Save",
|
"name": "Remotely Save",
|
||||||
"version": "0.3.0",
|
"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
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "remotely-save",
|
"name": "remotely-save",
|
||||||
"version": "0.3.0",
|
"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",
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
+24
-1
@@ -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
|
||||||
) {
|
) {
|
||||||
|
this.app.workspace.onLayoutReady(() => {
|
||||||
const intervalID = window.setInterval(() => {
|
const intervalID = window.setInterval(() => {
|
||||||
this.syncRun("auto");
|
this.syncRun("auto");
|
||||||
}, this.settings.autoRunEveryMilliseconds);
|
}, this.settings.autoRunEveryMilliseconds);
|
||||||
this.autoRunIntervalID = intervalID;
|
this.autoRunIntervalID = intervalID;
|
||||||
this.registerInterval(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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-3
@@ -156,13 +156,20 @@ const fixLastModifiedTimeInplace = (allFilesFolders: RemoteItem[]) => {
|
|||||||
// see https://dropbox.tech/developers/pkce--what-and-why-
|
// see https://dropbox.tech/developers/pkce--what-and-why-
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
export const getAuthUrlAndVerifier = async (appKey: string) => {
|
export const getAuthUrlAndVerifier = async (
|
||||||
|
appKey: string,
|
||||||
|
needManualPatse: boolean = false
|
||||||
|
) => {
|
||||||
const auth = new DropboxAuth({
|
const auth = new DropboxAuth({
|
||||||
clientId: appKey,
|
clientId: appKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const callback = needManualPatse
|
||||||
|
? undefined
|
||||||
|
: `obsidian://${COMMAND_CALLBACK_DROPBOX}`;
|
||||||
const authUrl = (
|
const authUrl = (
|
||||||
await auth.getAuthenticationUrl(
|
await auth.getAuthenticationUrl(
|
||||||
`obsidian://${COMMAND_CALLBACK_DROPBOX}`,
|
callback,
|
||||||
undefined,
|
undefined,
|
||||||
"code",
|
"code",
|
||||||
"offline",
|
"offline",
|
||||||
@@ -456,7 +463,7 @@ export const uploadToRemote = async (
|
|||||||
let localContent = undefined;
|
let localContent = undefined;
|
||||||
if (uploadRaw) {
|
if (uploadRaw) {
|
||||||
if (typeof rawContent === "string") {
|
if (typeof rawContent === "string") {
|
||||||
localContent = new TextEncoder().encode(rawContent);
|
localContent = new TextEncoder().encode(rawContent).buffer;
|
||||||
} else {
|
} else {
|
||||||
localContent = rawContent;
|
localContent = rawContent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -690,7 +690,7 @@ export const uploadToRemote = async (
|
|||||||
let localContent = undefined;
|
let localContent = undefined;
|
||||||
if (uploadRaw) {
|
if (uploadRaw) {
|
||||||
if (typeof rawContent === "string") {
|
if (typeof rawContent === "string") {
|
||||||
localContent = new TextEncoder().encode(rawContent);
|
localContent = new TextEncoder().encode(rawContent).buffer;
|
||||||
} else {
|
} else {
|
||||||
localContent = rawContent;
|
localContent = rawContent;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -141,7 +141,7 @@ export const uploadToRemote = async (
|
|||||||
let localContent = undefined;
|
let localContent = undefined;
|
||||||
if (uploadRaw) {
|
if (uploadRaw) {
|
||||||
if (typeof rawContent === "string") {
|
if (typeof rawContent === "string") {
|
||||||
localContent = new TextEncoder().encode(rawContent);
|
localContent = new TextEncoder().encode(rawContent).buffer;
|
||||||
} else {
|
} else {
|
||||||
localContent = rawContent;
|
localContent = rawContent;
|
||||||
}
|
}
|
||||||
@@ -190,7 +190,6 @@ export const listFromRemote = async (
|
|||||||
const contents = [] as _Object[];
|
const contents = [] as _Object[];
|
||||||
|
|
||||||
let isTruncated = true;
|
let isTruncated = true;
|
||||||
let continuationToken = "";
|
|
||||||
do {
|
do {
|
||||||
const rsp = await s3Client.send(new ListObjectsV2Command(confCmd));
|
const rsp = await s3Client.send(new ListObjectsV2Command(confCmd));
|
||||||
|
|
||||||
@@ -206,7 +205,8 @@ export const listFromRemote = async (
|
|||||||
confCmd.ContinuationToken = rsp.NextContinuationToken;
|
confCmd.ContinuationToken = rsp.NextContinuationToken;
|
||||||
if (
|
if (
|
||||||
isTruncated &&
|
isTruncated &&
|
||||||
(continuationToken === undefined || continuationToken === "")
|
(confCmd.ContinuationToken === undefined ||
|
||||||
|
confCmd.ContinuationToken === "")
|
||||||
) {
|
) {
|
||||||
throw Error("isTruncated is true but no continuationToken provided");
|
throw Error("isTruncated is true but no continuationToken provided");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export const uploadToRemote = async (
|
|||||||
let localContent = undefined;
|
let localContent = undefined;
|
||||||
if (uploadRaw) {
|
if (uploadRaw) {
|
||||||
if (typeof rawContent === "string") {
|
if (typeof rawContent === "string") {
|
||||||
localContent = new TextEncoder().encode(rawContent);
|
localContent = new TextEncoder().encode(rawContent).buffer;
|
||||||
} else {
|
} else {
|
||||||
localContent = rawContent;
|
localContent = rawContent;
|
||||||
}
|
}
|
||||||
|
|||||||
+150
-2
@@ -1,4 +1,11 @@
|
|||||||
import { App, Modal, Notice, PluginSettingTab, Setting } from "obsidian";
|
import {
|
||||||
|
App,
|
||||||
|
Modal,
|
||||||
|
Notice,
|
||||||
|
PluginSettingTab,
|
||||||
|
Setting,
|
||||||
|
Platform,
|
||||||
|
} from "obsidian";
|
||||||
import type { SUPPORTED_SERVICES_TYPE, WebdavAuthType } from "./baseTypes";
|
import type { SUPPORTED_SERVICES_TYPE, WebdavAuthType } from "./baseTypes";
|
||||||
import { exportVaultSyncPlansToFiles } from "./debugMode";
|
import { exportVaultSyncPlansToFiles } from "./debugMode";
|
||||||
import { exportQrCodeUri } from "./importExport";
|
import { exportQrCodeUri } from "./importExport";
|
||||||
@@ -110,9 +117,36 @@ class DropboxAuthModal extends Modal {
|
|||||||
async onOpen() {
|
async onOpen() {
|
||||||
let { contentEl } = this;
|
let { contentEl } = this;
|
||||||
|
|
||||||
|
let needManualPatse = false;
|
||||||
|
const userAgent = window.navigator.userAgent.toLocaleLowerCase() || "";
|
||||||
|
// some users report that,
|
||||||
|
// the Linux would open another instance Obsidian if jumping back,
|
||||||
|
// so fallback to manual paste on Linux
|
||||||
|
if (
|
||||||
|
Platform.isDesktopApp &&
|
||||||
|
!Platform.isMacOS &&
|
||||||
|
(/linux/.test(userAgent) ||
|
||||||
|
/ubuntu/.test(userAgent) ||
|
||||||
|
/debian/.test(userAgent) ||
|
||||||
|
/fedora/.test(userAgent) ||
|
||||||
|
/centos/.test(userAgent))
|
||||||
|
) {
|
||||||
|
needManualPatse = true;
|
||||||
|
}
|
||||||
|
|
||||||
const { authUrl, verifier } = await getAuthUrlAndVerifierDropbox(
|
const { authUrl, verifier } = await getAuthUrlAndVerifierDropbox(
|
||||||
this.plugin.settings.dropbox.clientID
|
this.plugin.settings.dropbox.clientID,
|
||||||
|
needManualPatse
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (needManualPatse) {
|
||||||
|
contentEl.createEl("p", {
|
||||||
|
text: "Step 1: Visit the address in a browser, and follow the steps.",
|
||||||
|
});
|
||||||
|
contentEl.createEl("p", {
|
||||||
|
text: 'Step 2: In the end of the web flow, you obtain a long code. Paste it here then click "Submit".',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
this.plugin.oauth2Info.verifier = verifier;
|
this.plugin.oauth2Info.verifier = verifier;
|
||||||
|
|
||||||
contentEl.createEl("p", {
|
contentEl.createEl("p", {
|
||||||
@@ -121,6 +155,7 @@ class DropboxAuthModal extends Modal {
|
|||||||
contentEl.createEl("p", {
|
contentEl.createEl("p", {
|
||||||
text: "Finally you should be redirected to Obsidian.",
|
text: "Finally you should be redirected to Obsidian.",
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const div2 = contentEl.createDiv();
|
const div2 = contentEl.createDiv();
|
||||||
div2.createEl(
|
div2.createEl(
|
||||||
@@ -140,6 +175,70 @@ class DropboxAuthModal extends Modal {
|
|||||||
href: authUrl,
|
href: authUrl,
|
||||||
text: authUrl,
|
text: authUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (needManualPatse) {
|
||||||
|
let authCode = "";
|
||||||
|
new Setting(contentEl)
|
||||||
|
.setName("Auth Code from web page")
|
||||||
|
.setDesc('You need to click "Confirm".')
|
||||||
|
.addText((text) =>
|
||||||
|
text
|
||||||
|
.setPlaceholder("")
|
||||||
|
.setValue("")
|
||||||
|
.onChange((val) => {
|
||||||
|
authCode = val.trim();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.addButton(async (button) => {
|
||||||
|
button.setButtonText("Confirm");
|
||||||
|
button.onClick(async () => {
|
||||||
|
new Notice("Trying to connect to Dropbox");
|
||||||
|
try {
|
||||||
|
const authRes = await sendAuthReqDropbox(
|
||||||
|
this.plugin.settings.dropbox.clientID,
|
||||||
|
verifier,
|
||||||
|
authCode
|
||||||
|
);
|
||||||
|
const self = this;
|
||||||
|
setConfigBySuccessfullAuthInplace(
|
||||||
|
this.plugin.settings.dropbox,
|
||||||
|
authRes,
|
||||||
|
() => self.plugin.saveSettings()
|
||||||
|
);
|
||||||
|
const client = new RemoteClient(
|
||||||
|
"dropbox",
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
this.plugin.settings.dropbox,
|
||||||
|
undefined,
|
||||||
|
this.app.vault.getName(),
|
||||||
|
() => self.plugin.saveSettings()
|
||||||
|
);
|
||||||
|
const username = await client.getUser();
|
||||||
|
this.plugin.settings.dropbox.username = username;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
new Notice(
|
||||||
|
`Good! We've connected to Dropbox as user ${username}!`
|
||||||
|
);
|
||||||
|
this.authDiv.toggleClass(
|
||||||
|
"dropbox-auth-button-hide",
|
||||||
|
this.plugin.settings.dropbox.username !== ""
|
||||||
|
);
|
||||||
|
this.revokeAuthDiv.toggleClass(
|
||||||
|
"dropbox-revoke-auth-button-hide",
|
||||||
|
this.plugin.settings.dropbox.username === ""
|
||||||
|
);
|
||||||
|
this.revokeAuthSetting.setDesc(
|
||||||
|
`You've connected as user ${this.plugin.settings.dropbox.username}. If you want to disconnect, click this button.`
|
||||||
|
);
|
||||||
|
this.close();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
new Notice("Something goes wrong while connecting to Dropbox.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose() {
|
onClose() {
|
||||||
@@ -422,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)
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
|
|||||||
+168
-22
@@ -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"
|
||||||
|
|
||||||
|
const deltimeLocal = r.deltimeLocal !== undefined ? r.deltimeLocal : -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 (
|
if (
|
||||||
r.deltimeLocal !== undefined &&
|
cmtime > 0 &&
|
||||||
r.deltimeLocal >= (r.deltimeRemote !== undefined ? r.deltimeRemote : -1)
|
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,6 +908,10 @@ export const doActualSync = async (
|
|||||||
);
|
);
|
||||||
log.debug(`finish syncing extra data firstly`);
|
log.debug(`finish syncing extra data firstly`);
|
||||||
|
|
||||||
|
log.debug(`concurrency === ${concurrency}`);
|
||||||
|
if (concurrency === 1) {
|
||||||
|
// run everything in sequence
|
||||||
|
// good old way
|
||||||
for (let i = 0; i < sortedKeys.length; ++i) {
|
for (let i = 0; i < sortedKeys.length; ++i) {
|
||||||
const key = sortedKeys[i];
|
const key = sortedKeys[i];
|
||||||
const val = mixedStates[key];
|
const val = mixedStates[key];
|
||||||
@@ -887,20 +933,120 @@ export const doActualSync = async (
|
|||||||
password
|
password
|
||||||
);
|
);
|
||||||
log.debug(`finished ${key}`);
|
log.debug(`finished ${key}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let realCounter = 0;
|
||||||
|
|
||||||
// await Promise.all(
|
log.debug(
|
||||||
// Object.entries(mixedStates).map(async ([k, v]) =>
|
`1. create all folders from shadowest to deepest, also check undefined decision`
|
||||||
// dispatchOperationToActual(
|
);
|
||||||
// k as string,
|
for (let i = sortedKeys.length - 1; i >= 0; --i) {
|
||||||
// vaultRandomID,
|
const key = sortedKeys[i];
|
||||||
// v as FileOrFolderMixedState,
|
const val = mixedStates[key];
|
||||||
// client,
|
|
||||||
// db,
|
if (
|
||||||
// vault,
|
val.decision === undefined ||
|
||||||
// localDeleteFunc,
|
val.decision === "skipFolder" ||
|
||||||
// password
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(`2. delete files and folders from deepest to shadowest`);
|
||||||
|
for (let i = 0; i < sortedKeys.length; ++i) {
|
||||||
|
const key = sortedKeys[i];
|
||||||
|
const val = mixedStates[key];
|
||||||
|
if (
|
||||||
|
val.decision === "uploadLocalDelHistToRemoteFolder" ||
|
||||||
|
val.decision === "keepRemoteDelHistFolder"
|
||||||
|
) {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export class SyncAlgoV2Modal extends Modal {
|
|||||||
const texts = [
|
const texts = [
|
||||||
"Welcome to use Remotely Save!",
|
"Welcome to use Remotely Save!",
|
||||||
|
|
||||||
"From this version 0.3.0, a new algorithm has been developed, but it needs uploading extra meta data files _remotely-save-metadata-on-remote.{json,bin} to YOUR configured cloud destinations, besides your notes.",
|
"From version 0.3.0, a new algorithm has been developed, but it needs uploading extra meta data files _remotely-save-metadata-on-remote.{json,bin} to YOUR configured cloud destinations, besides your notes.",
|
||||||
|
|
||||||
"So that, for example, the second device can know that what files/folders have been deleted on the first device by reading those files.",
|
"So that, for example, the second device can know that what files/folders have been deleted on the first device by reading those files.",
|
||||||
|
|
||||||
@@ -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
@@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"0.3.0": "0.12.15"
|
"0.3.2": "0.12.15",
|
||||||
|
"0.3.4": "0.13.21"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user