Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7082616aa | ||
|
|
d9f926e376 | ||
|
|
13a2d6f53a | ||
|
|
5092c59ed3 | ||
|
|
fc9ab2e63f | ||
|
|
91af2c849f | ||
|
|
fafe74526d | ||
|
|
320f91f1a5 | ||
|
|
ea7c9ae203 | ||
|
|
2bebc7226d | ||
|
|
c9b86fc42d | ||
|
|
5d11801c28 | ||
|
|
1f8754633f | ||
|
|
6cdbc5b32e | ||
|
|
a78a2a88cf | ||
|
|
4035218706 | ||
|
|
b0cb1248a5 | ||
|
|
f936704a6a | ||
|
|
04f3a0c97c | ||
|
|
c637038d95 | ||
|
|
e7c24b55a3 | ||
|
|
9a66e3181d | ||
|
|
510755d7f0 | ||
|
|
27fe428fe4 | ||
|
|
ffabaacaf1 |
@@ -0,0 +1,3 @@
|
|||||||
|
DROPBOX_APP_KEY=
|
||||||
|
ONEDRIVE_CLIENT_ID=
|
||||||
|
ONEDRIVE_AUTHORITY=https://
|
||||||
@@ -15,6 +15,11 @@ jobs:
|
|||||||
|
|
||||||
environment: env-for-buildci
|
environment: env-for-buildci
|
||||||
|
|
||||||
|
env:
|
||||||
|
DROPBOX_APP_KEY: ${{secrets.DROPBOX_APP_KEY}}
|
||||||
|
ONEDRIVE_CLIENT_ID: ${{secrets.ONEDRIVE_CLIENT_ID}}
|
||||||
|
ONEDRIVE_AUTHORITY: ${{secrets.ONEDRIVE_AUTHORITY}}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [16.x]
|
node-version: [16.x]
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
main.js
|
main.js
|
||||||
|
data.json
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ As of Jan 2022, the plugin is considered in BETA stage. **DO NOT USE IT for any
|
|||||||
- Webdav
|
- Webdav
|
||||||
- **Obsidiain Mobile supported.** Vaults can be synced across mobile and desktop devices with the cloud service as the "broker".
|
- **Obsidiain Mobile supported.** Vaults can be synced across mobile and desktop devices with the cloud service as the "broker".
|
||||||
- **[End-to-end encryption](./docs/encryption.md) supported.** Files would be encrypted using openssl format before being sent to the cloud **if** user specify a password.
|
- **[End-to-end encryption](./docs/encryption.md) supported.** Files would be encrypted using openssl format before being sent to the cloud **if** user specify a password.
|
||||||
|
- **Scheduled auto sync supported.** You can also manually trigger the sync using sidebar ribbon, or using the command from the command palette (or even bind the hot key combination to the command then press the hot key combination).
|
||||||
- **[Minimal Intrusive](./docs/minimal_intrusive_design.md).**
|
- **[Minimal Intrusive](./docs/minimal_intrusive_design.md).**
|
||||||
- **Fully open source under [Apache-2.0 License](./LICENSE).**
|
- **Fully open source under [Apache-2.0 License](./LICENSE).**
|
||||||
- **[Sync Algorithm open](./docs/sync_algorithm.md) for discussion.**
|
- **[Sync Algorithm open](./docs/sync_algorithm.md) for discussion.**
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
- **Users have to trigger the sync manually.** This design is intentional because the plugin is in beta, and it's better for users to be exactly aware of the running of this plugin.
|
|
||||||
- **"deletion" operation can only be triggered from local device.** It's because of the "[minimal intrusive design](./docs/minimal_intrusive_design.md)". May be changed in the future.
|
- **"deletion" operation can only be triggered from local device.** It's because of the "[minimal intrusive design](./docs/minimal_intrusive_design.md)". May be changed in the future.
|
||||||
- **No Conflict resolution. No content-diff-and-patch algorithm.** All files and folders are compared using their local and remote "last modified time" and those with later "last modified time" wins.
|
- **No Conflict resolution. No content-diff-and-patch algorithm.** All files and folders are compared using their local and remote "last modified time" and those with later "last modified time" wins.
|
||||||
- **Cloud services cost you money.** Always be aware of the costs and pricing.
|
- **Cloud services cost you money.** Always be aware of the costs and pricing.
|
||||||
@@ -47,7 +47,7 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
|||||||
|
|
||||||
## Download and Install
|
## Download and Install
|
||||||
|
|
||||||
- Option #1: You can search, download, and install the plugin in official "community plugin list".
|
- Option #1: Search in the official "community plugin list", or click [obsidian://show-plugin?id=remotely-save](obsidian://show-plugin?id=remotely-save), then install the plugin.
|
||||||
- Option #2: You can also use [Obsidian42 - BRAT](https://github.com/TfTHacker/obsidian42-brat) to install this plugin. Input `fyears/remotely-save` in the configuration of BRAT.
|
- Option #2: You can also use [Obsidian42 - BRAT](https://github.com/TfTHacker/obsidian42-brat) to install this plugin. Input `fyears/remotely-save` in the configuration of BRAT.
|
||||||
- Option #3: [](https://github.com/fyears/remotely-save/releases) Manually download assets (`main.js`, `manifest.json`, `styles.css`) from the latest release.
|
- Option #3: [](https://github.com/fyears/remotely-save/releases) Manually download assets (`main.js`, `manifest.json`, `styles.css`) from the latest release.
|
||||||
- Option #4: [](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml) Every artifacts are placed in the "Summary" under every successful builds. It's automatically generated by every commit, may break something.
|
- Option #4: [](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml) Every artifacts are placed in the "Summary" under every successful builds. It's automatically generated by every commit, may break something.
|
||||||
@@ -61,7 +61,7 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
|||||||
- Download and enable this plugin.
|
- Download and enable this plugin.
|
||||||
- Enter your information to the settings of this plugin.
|
- Enter your information to the settings of this plugin.
|
||||||
- If you want to enable end-to-end encryption, also set a password in settings. If you do not specify a password, the files and folders are synced in plain, original content to the cloud.
|
- If you want to enable end-to-end encryption, also set a password in settings. If you do not specify a password, the files and folders are synced in plain, original content to the cloud.
|
||||||
- Click the new "switch" icon on the ribbon (the left sidebar), **every time** you want to sync your vault between local and remote. (No "auto sync" yet.)
|
- Click the new "circle arrow" icon on the ribbon (the left sidebar), **every time** you want to sync your vault between local and remote. (Or, you could configure auto sync in the settings panel (See next chapter).) While syncing, the icon becomes "two half-circle arrows". Besides clicking the icon on the sidebar ribbon, you can also activate the corresponding command in the command palette.
|
||||||
- **Be patient while syncing.** Especially in the first-time sync.
|
- **Be patient while syncing.** Especially in the first-time sync.
|
||||||
|
|
||||||
### Dropbox
|
### Dropbox
|
||||||
@@ -91,22 +91,12 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
|||||||
- Your data would be synced to a `${vaultName}` sub folder on your webdav server.
|
- Your data would be synced to a `${vaultName}` sub folder on your webdav server.
|
||||||
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
||||||
|
|
||||||
## Import And Export Not-Oauth2 Plugin Settings By QR Code
|
## Scheduled Auto Sync
|
||||||
|
|
||||||
It's often verbose / inconvenient to input not-oauth2 credentials on mobile. (On different desktop computers, it's trivial to copy and paste `data.json` across different devices.)
|
- You can configure auto syncing every N minutes in settings.
|
||||||
|
- In auto sync mode, if any error occurs, the plugin would **fail silently**.
|
||||||
|
- Auto sync only works when Obsidian is being opened. It's **technically impossible** to auto sync while Obsidian is in background, because the plugin just works in the browser environment provided by Obsidian.
|
||||||
|
|
||||||
So this plugin has a helper function to export those settings as a QR code, then you could use mobile device's camera to import the settings.
|
## Bonus: Import And Export Not-Oauth2 Plugin Settings By QR Code
|
||||||
|
|
||||||
Attention:
|
See [here](./docs/import_export_some_settings.md) for more details.
|
||||||
|
|
||||||
1. Oauth2 - related information is omitted. It means that Dropbox, OneDrive login credentials are NOT included in the QR Code.
|
|
||||||
2. Please, NEVER share the QR Code to others. It's as equivalent to the login credentials.
|
|
||||||
|
|
||||||
Steps:
|
|
||||||
|
|
||||||
1. Configure the plugin settings on one device, and make sure the sync functions work. (E.g, you could sync notes using S3 credentials.)
|
|
||||||
2. Open plugin settings page. Then scroll down the page, until the section "Import and Export Settings". Click the button "Get QR Code". A new modal should show up and you should see a QR code.
|
|
||||||
3. On a second device, make sure the vault name is the same as the first device's.
|
|
||||||
4. On that second device, use its camera app, or any apps that support scanning QR codes, to scan the QR code from the first device. (On latest iOS, the system's built in camera app should work. On Android, at least one open source app [Binary Eye](https://github.com/markusfisch/BinaryEye) is tested to be working.)
|
|
||||||
5. A link / url / address should be identified in the scan-QR-code app, and you could follow the instruction in the app, and then you should be redirected to open the Obsidian app.
|
|
||||||
6. And finally, there should be a new notice in the Obsidian app saying the settings are successfully imported. Otherwise please check the error message in the notice.
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
## Bonus: Import And Export Not-Oauth2 Plugin Settings By QR Code
|
||||||
|
|
||||||
|
It's often verbose / inconvenient to input not-oauth2 credentials on mobile. (On different desktop computers, it's trivial to copy and paste `data.json` across different devices.)
|
||||||
|
|
||||||
|
So this plugin has a helper function to export those settings as a QR code, then you could use mobile device's camera to import the settings.
|
||||||
|
|
||||||
|
Attention:
|
||||||
|
|
||||||
|
1. Oauth2 - related information is omitted. It means that Dropbox, OneDrive login credentials are NOT included in the QR Code.
|
||||||
|
2. Please, NEVER share the QR Code to others. It's as equivalent to the login credentials.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
1. Configure the plugin settings on one device, and make sure the sync functions work. (E.g, you could sync notes using S3 credentials.)
|
||||||
|
2. Open plugin settings page. Then scroll down the page, until the section "Import and Export Settings". Click the button "Get QR Code". A new modal should show up and you should see a QR code.
|
||||||
|
3. On a second device, make sure the vault name is the same as the first device's.
|
||||||
|
4. On that second device, use its camera app, or any apps that support scanning QR codes, to scan the QR code from the first device. (On latest iOS, the system's built in camera app should work. On Android, at least one open source app [Binary Eye](https://github.com/markusfisch/BinaryEye) is tested to be working.)
|
||||||
|
5. A link / url / address should be identified in the scan-QR-code app, and you could follow the instruction in the app, and then you should be redirected to open the Obsidian app.
|
||||||
|
6. And finally, there should be a new notice in the Obsidian app saying the settings are successfully imported. Otherwise please check the error message in the notice.
|
||||||
@@ -9,6 +9,8 @@ if you want to view the source, please visit the github repository of this plugi
|
|||||||
*/
|
*/
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
console.log(`esbuild version = ${esbuild.version}`);
|
||||||
|
|
||||||
const prod = process.argv[2] === "production";
|
const prod = process.argv[2] === "production";
|
||||||
|
|
||||||
const DEFAULT_DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY || "";
|
const DEFAULT_DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY || "";
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "remotely-save",
|
"id": "remotely-save",
|
||||||
"name": "Remotely Save",
|
"name": "Remotely Save",
|
||||||
"version": "0.2.10",
|
"version": "0.2.13",
|
||||||
"minAppVersion": "0.12.15",
|
"minAppVersion": "0.12.15",
|
||||||
"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",
|
||||||
|
|||||||
+8
-3
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "remotely-save",
|
"name": "remotely-save",
|
||||||
"version": "0.2.10",
|
"version": "0.2.13",
|
||||||
"description": "This is yet another sync plugin for Obsidian app.",
|
"description": "This is yet another sync plugin for Obsidian app.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node esbuild.config.mjs",
|
"dev2": "node esbuild.config.mjs",
|
||||||
"build2": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
"build2": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||||
"build": "webpack --mode production",
|
"build": "webpack --mode production",
|
||||||
|
"dev": "webpack --mode development --watch",
|
||||||
"format": "npx prettier --write .",
|
"format": "npx prettier --write .",
|
||||||
"clean": "npx rimraf main.js",
|
"clean": "npx rimraf main.js",
|
||||||
"test": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"module\\\":\\\"commonjs\\\"} mocha -r ts-node/register 'tests/**/*.ts'"
|
"test": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"module\\\":\\\"commonjs\\\"} mocha -r ts-node/register 'tests/**/*.ts'"
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
"@microsoft/microsoft-graph-types": "^2.11.0",
|
"@microsoft/microsoft-graph-types": "^2.11.0",
|
||||||
"@types/chai": "^4.2.22",
|
"@types/chai": "^4.2.22",
|
||||||
"@types/chai-as-promised": "^7.1.4",
|
"@types/chai-as-promised": "^7.1.4",
|
||||||
|
"@types/feather-icons": "^4.7.0",
|
||||||
"@types/jsdom": "^16.2.13",
|
"@types/jsdom": "^16.2.13",
|
||||||
"@types/lodash": "^4.14.178",
|
"@types/lodash": "^4.14.178",
|
||||||
"@types/mime-types": "^2.1.1",
|
"@types/mime-types": "^2.1.1",
|
||||||
@@ -35,7 +37,7 @@
|
|||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"esbuild": "^0.14.0",
|
"esbuild": "^0.14.3",
|
||||||
"jsdom": "^19.0.0",
|
"jsdom": "^19.0.0",
|
||||||
"mocha": "^9.1.3",
|
"mocha": "^9.1.3",
|
||||||
"prettier": "^2.4.1",
|
"prettier": "^2.4.1",
|
||||||
@@ -52,6 +54,7 @@
|
|||||||
"@aws-sdk/lib-storage": "^3.40.1",
|
"@aws-sdk/lib-storage": "^3.40.1",
|
||||||
"@aws-sdk/signature-v4-crt": "^3.37.0",
|
"@aws-sdk/signature-v4-crt": "^3.37.0",
|
||||||
"@azure/msal-node": "^1.4.0",
|
"@azure/msal-node": "^1.4.0",
|
||||||
|
"@fyears/tsqueue": "^1.0.1",
|
||||||
"@microsoft/microsoft-graph-client": "^3.0.1",
|
"@microsoft/microsoft-graph-client": "^3.0.1",
|
||||||
"acorn": "^8.5.0",
|
"acorn": "^8.5.0",
|
||||||
"assert": "^2.0.0",
|
"assert": "^2.0.0",
|
||||||
@@ -60,10 +63,12 @@
|
|||||||
"codemirror": "^5.63.1",
|
"codemirror": "^5.63.1",
|
||||||
"crypto-browserify": "^3.12.0",
|
"crypto-browserify": "^3.12.0",
|
||||||
"dropbox": "^10.22.0",
|
"dropbox": "^10.22.0",
|
||||||
|
"feather-icons": "^4.28.0",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"loglevel": "^1.8.0",
|
"loglevel": "^1.8.0",
|
||||||
"mime-types": "^2.1.33",
|
"mime-types": "^2.1.33",
|
||||||
|
"nanoid": "^3.1.30",
|
||||||
"obsidian": "^0.12.0",
|
"obsidian": "^0.12.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export interface WebdavConfig {
|
|||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
authType: WebdavAuthType;
|
authType: WebdavAuthType;
|
||||||
|
manualRecursive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OnedriveConfig {
|
export interface OnedriveConfig {
|
||||||
@@ -53,6 +54,8 @@ export interface RemotelySavePluginSettings {
|
|||||||
password: string;
|
password: string;
|
||||||
serviceType: SUPPORTED_SERVICES_TYPE;
|
serviceType: SUPPORTED_SERVICES_TYPE;
|
||||||
currLogLevel?: string;
|
currLogLevel?: string;
|
||||||
|
vaultRandomID?: string;
|
||||||
|
autoRunEveryMilliseconds?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RemoteItem {
|
export interface RemoteItem {
|
||||||
|
|||||||
+7
-3
@@ -1,7 +1,7 @@
|
|||||||
import { TAbstractFile, TFolder, TFile, Vault } from "obsidian";
|
import { TAbstractFile, TFolder, TFile, Vault } from "obsidian";
|
||||||
|
|
||||||
import type { SyncPlanType } from "./sync";
|
import type { SyncPlanType } from "./sync";
|
||||||
import { readAllSyncPlanRecordTexts } from "./localdb";
|
import { readAllSyncPlanRecordTextsByVault } from "./localdb";
|
||||||
import type { InternalDBs } from "./localdb";
|
import type { InternalDBs } from "./localdb";
|
||||||
import { mkdirpInVault } from "./misc";
|
import { mkdirpInVault } from "./misc";
|
||||||
|
|
||||||
@@ -11,10 +11,14 @@ const log = origLog.getLogger("rs-default");
|
|||||||
const DEFAULT_DEBUG_FOLDER = "_debug_remotely_save/";
|
const DEFAULT_DEBUG_FOLDER = "_debug_remotely_save/";
|
||||||
const DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX = "sync_plans_hist_exported_on_";
|
const DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX = "sync_plans_hist_exported_on_";
|
||||||
|
|
||||||
export const exportSyncPlansToFiles = async (db: InternalDBs, vault: Vault) => {
|
export const exportVaultSyncPlansToFiles = async (
|
||||||
|
db: InternalDBs,
|
||||||
|
vault: Vault,
|
||||||
|
vaultRandomID: string
|
||||||
|
) => {
|
||||||
log.info("exporting");
|
log.info("exporting");
|
||||||
await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault);
|
await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault);
|
||||||
const records = await readAllSyncPlanRecordTexts(db);
|
const records = await readAllSyncPlanRecordTextsByVault(db, vaultRandomID);
|
||||||
let md = "";
|
let md = "";
|
||||||
if (records.length === 0) {
|
if (records.length === 0) {
|
||||||
md = "No sync plans history found";
|
md = "No sync plans history found";
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const exportQrCodeUri = async (
|
|||||||
const settings2 = cloneDeep(settings);
|
const settings2 = cloneDeep(settings);
|
||||||
delete settings2.dropbox;
|
delete settings2.dropbox;
|
||||||
delete settings2.onedrive;
|
delete settings2.onedrive;
|
||||||
|
delete settings2.vaultRandomID;
|
||||||
const data = encodeURIComponent(JSON.stringify(settings2));
|
const data = encodeURIComponent(JSON.stringify(settings2));
|
||||||
const vault = encodeURIComponent(currentVaultName);
|
const vault = encodeURIComponent(currentVaultName);
|
||||||
const version = encodeURIComponent(pluginVersion);
|
const version = encodeURIComponent(pluginVersion);
|
||||||
|
|||||||
+159
-31
@@ -9,7 +9,8 @@ export type LocalForage = typeof localforage;
|
|||||||
import * as origLog from "loglevel";
|
import * as origLog from "loglevel";
|
||||||
const log = origLog.getLogger("rs-default");
|
const log = origLog.getLogger("rs-default");
|
||||||
|
|
||||||
export const DEFAULT_DB_VERSION_NUMBER: number = 20211114;
|
const DB_VERSION_NUMBER_IN_HISTORY = [20211114, 20220108];
|
||||||
|
export const DEFAULT_DB_VERSION_NUMBER: number = 20220108;
|
||||||
export const DEFAULT_DB_NAME = "remotelysavedb";
|
export const DEFAULT_DB_NAME = "remotelysavedb";
|
||||||
export const DEFAULT_TBL_VERSION = "schemaversion";
|
export const DEFAULT_TBL_VERSION = "schemaversion";
|
||||||
export const DEFAULT_TBL_DELETE_HISTORY = "filefolderoperationhistory";
|
export const DEFAULT_TBL_DELETE_HISTORY = "filefolderoperationhistory";
|
||||||
@@ -25,6 +26,7 @@ export interface FileFolderHistoryRecord {
|
|||||||
actionType: "delete" | "rename";
|
actionType: "delete" | "rename";
|
||||||
keyType: "folder" | "file";
|
keyType: "folder" | "file";
|
||||||
renameTo: string;
|
renameTo: string;
|
||||||
|
vaultRandomID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SyncMetaMappingRecord {
|
interface SyncMetaMappingRecord {
|
||||||
@@ -37,12 +39,14 @@ interface SyncMetaMappingRecord {
|
|||||||
remoteExtraKey: string;
|
remoteExtraKey: string;
|
||||||
remoteType: SUPPORTED_SERVICES_TYPE;
|
remoteType: SUPPORTED_SERVICES_TYPE;
|
||||||
keyType: "folder" | "file";
|
keyType: "folder" | "file";
|
||||||
|
vaultRandomID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SyncPlanRecord {
|
interface SyncPlanRecord {
|
||||||
ts: number;
|
ts: number;
|
||||||
remoteType: string;
|
remoteType: string;
|
||||||
syncPlan: string;
|
syncPlan: string;
|
||||||
|
vaultRandomID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InternalDBs {
|
export interface InternalDBs {
|
||||||
@@ -52,7 +56,103 @@ export interface InternalDBs {
|
|||||||
syncPlansTbl: LocalForage;
|
syncPlansTbl: LocalForage;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const prepareDBs = async () => {
|
/**
|
||||||
|
* This migration mainly aims to assign vault name or vault id into all tables.
|
||||||
|
* @param db
|
||||||
|
* @param vaultRandomID
|
||||||
|
*/
|
||||||
|
const migrateDBsFrom20211114To20220108 = async (
|
||||||
|
db: InternalDBs,
|
||||||
|
vaultRandomID: string
|
||||||
|
) => {
|
||||||
|
const oldVer = 20211114;
|
||||||
|
const newVer = 20220108;
|
||||||
|
log.debug(`start upgrading internal db from ${oldVer} to ${newVer}`);
|
||||||
|
|
||||||
|
const allPromisesToWait: Promise<any>[] = [];
|
||||||
|
|
||||||
|
log.debug("assign vault id to any delete history");
|
||||||
|
const keysInDeleteHistoryTbl = await db.deleteHistoryTbl.keys();
|
||||||
|
for (const key of keysInDeleteHistoryTbl) {
|
||||||
|
if (key.startsWith(vaultRandomID)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const value = (await db.deleteHistoryTbl.getItem(
|
||||||
|
key
|
||||||
|
)) as FileFolderHistoryRecord;
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (value.vaultRandomID === undefined || value.vaultRandomID === "") {
|
||||||
|
value.vaultRandomID = vaultRandomID;
|
||||||
|
}
|
||||||
|
const newKey = `${vaultRandomID}\t${key}`;
|
||||||
|
allPromisesToWait.push(db.deleteHistoryTbl.setItem(newKey, value));
|
||||||
|
allPromisesToWait.push(db.deleteHistoryTbl.removeItem(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("assign vault id to any sync mapping");
|
||||||
|
const keysInSyncMappingTbl = await db.syncMappingTbl.keys();
|
||||||
|
for (const key of keysInSyncMappingTbl) {
|
||||||
|
if (key.startsWith(vaultRandomID)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const value = (await db.syncMappingTbl.getItem(
|
||||||
|
key
|
||||||
|
)) as SyncMetaMappingRecord;
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (value.vaultRandomID === undefined || value.vaultRandomID === "") {
|
||||||
|
value.vaultRandomID = vaultRandomID;
|
||||||
|
}
|
||||||
|
const newKey = `${vaultRandomID}\t${key}`;
|
||||||
|
allPromisesToWait.push(db.syncMappingTbl.setItem(newKey, value));
|
||||||
|
allPromisesToWait.push(db.syncMappingTbl.removeItem(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("assign vault id to any sync plan records");
|
||||||
|
const keysInSyncPlansTbl = await db.syncPlansTbl.keys();
|
||||||
|
for (const key of keysInSyncPlansTbl) {
|
||||||
|
if (key.startsWith(vaultRandomID)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const value = (await db.syncPlansTbl.getItem(key)) as SyncPlanRecord;
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (value.vaultRandomID === undefined || value.vaultRandomID === "") {
|
||||||
|
value.vaultRandomID = vaultRandomID;
|
||||||
|
}
|
||||||
|
const newKey = `${vaultRandomID}\t${key}`;
|
||||||
|
allPromisesToWait.push(db.syncPlansTbl.setItem(newKey, value));
|
||||||
|
allPromisesToWait.push(db.syncPlansTbl.removeItem(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("finally update version if everything is ok");
|
||||||
|
await Promise.all(allPromisesToWait);
|
||||||
|
await db.versionTbl.setItem("version", newVer);
|
||||||
|
|
||||||
|
log.debug(`finish upgrading internal db from ${oldVer} to ${newVer}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrateDBs = async (
|
||||||
|
db: InternalDBs,
|
||||||
|
oldVer: number,
|
||||||
|
newVer: number,
|
||||||
|
vaultRandomID: string
|
||||||
|
) => {
|
||||||
|
if (oldVer === newVer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (oldVer === 20211114 && newVer === 20220108) {
|
||||||
|
return await migrateDBsFrom20211114To20220108(db, vaultRandomID);
|
||||||
|
}
|
||||||
|
// not implemented
|
||||||
|
throw Error(`not supported internal db changes from ${oldVer} to ${newVer}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prepareDBs = async (vaultRandomID: string) => {
|
||||||
const db = {
|
const db = {
|
||||||
versionTbl: localforage.createInstance({
|
versionTbl: localforage.createInstance({
|
||||||
name: DEFAULT_DB_NAME,
|
name: DEFAULT_DB_NAME,
|
||||||
@@ -74,11 +174,22 @@ export const prepareDBs = async () => {
|
|||||||
|
|
||||||
const originalVersion = (await db.versionTbl.getItem("version")) as number;
|
const originalVersion = (await db.versionTbl.getItem("version")) as number;
|
||||||
if (originalVersion === null) {
|
if (originalVersion === null) {
|
||||||
|
log.debug(
|
||||||
|
`no internal db version, setting it to ${DEFAULT_DB_VERSION_NUMBER}`
|
||||||
|
);
|
||||||
await db.versionTbl.setItem("version", DEFAULT_DB_VERSION_NUMBER);
|
await db.versionTbl.setItem("version", DEFAULT_DB_VERSION_NUMBER);
|
||||||
} else if (originalVersion === DEFAULT_DB_VERSION_NUMBER) {
|
} else if (originalVersion === DEFAULT_DB_VERSION_NUMBER) {
|
||||||
// do nothing
|
// do nothing
|
||||||
} else {
|
} else {
|
||||||
await migrateDBs(db, originalVersion, DEFAULT_DB_VERSION_NUMBER);
|
log.debug(
|
||||||
|
`trying to upgrade db version from ${originalVersion} to ${DEFAULT_DB_VERSION_NUMBER}`
|
||||||
|
);
|
||||||
|
await migrateDBs(
|
||||||
|
db,
|
||||||
|
originalVersion,
|
||||||
|
DEFAULT_DB_VERSION_NUMBER,
|
||||||
|
vaultRandomID
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("db connected");
|
log.info("db connected");
|
||||||
@@ -103,33 +214,32 @@ export const destroyDBs = async () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const migrateDBs = async (db: InternalDBs, oldVer: number, newVer: number) => {
|
export const loadDeleteRenameHistoryTableByVault = async (
|
||||||
if (oldVer === newVer) {
|
db: InternalDBs,
|
||||||
return;
|
vaultRandomID: string
|
||||||
}
|
) => {
|
||||||
// not implemented
|
|
||||||
throw Error(`not supported internal db changes from ${oldVer} to ${newVer}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loadDeleteRenameHistoryTable = async (db: InternalDBs) => {
|
|
||||||
const records = [] as FileFolderHistoryRecord[];
|
const records = [] as FileFolderHistoryRecord[];
|
||||||
await db.deleteHistoryTbl.iterate((value, key, iterationNumber) => {
|
await db.deleteHistoryTbl.iterate((value, key, iterationNumber) => {
|
||||||
|
if (key.startsWith(`${vaultRandomID}\t`)) {
|
||||||
records.push(value as FileFolderHistoryRecord);
|
records.push(value as FileFolderHistoryRecord);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
records.sort((a, b) => a.actionWhen - b.actionWhen); // ascending
|
records.sort((a, b) => a.actionWhen - b.actionWhen); // ascending
|
||||||
return records;
|
return records;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clearDeleteRenameHistoryOfKey = async (
|
export const clearDeleteRenameHistoryOfKeyAndVault = async (
|
||||||
db: InternalDBs,
|
db: InternalDBs,
|
||||||
key: string
|
key: string,
|
||||||
|
vaultRandomID: string
|
||||||
) => {
|
) => {
|
||||||
await db.deleteHistoryTbl.removeItem(key);
|
await db.deleteHistoryTbl.removeItem(`${vaultRandomID}\t${key}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const insertDeleteRecord = async (
|
export const insertDeleteRecordByVault = async (
|
||||||
db: InternalDBs,
|
db: InternalDBs,
|
||||||
fileOrFolder: TAbstractFile
|
fileOrFolder: TAbstractFile,
|
||||||
|
vaultRandomID: string
|
||||||
) => {
|
) => {
|
||||||
// log.info(fileOrFolder);
|
// log.info(fileOrFolder);
|
||||||
let k: FileFolderHistoryRecord;
|
let k: FileFolderHistoryRecord;
|
||||||
@@ -143,6 +253,7 @@ export const insertDeleteRecord = async (
|
|||||||
actionType: "delete",
|
actionType: "delete",
|
||||||
keyType: "file",
|
keyType: "file",
|
||||||
renameTo: "",
|
renameTo: "",
|
||||||
|
vaultRandomID: vaultRandomID,
|
||||||
};
|
};
|
||||||
} else if (fileOrFolder instanceof TFolder) {
|
} else if (fileOrFolder instanceof TFolder) {
|
||||||
// key should endswith "/"
|
// key should endswith "/"
|
||||||
@@ -158,15 +269,17 @@ export const insertDeleteRecord = async (
|
|||||||
actionType: "delete",
|
actionType: "delete",
|
||||||
keyType: "folder",
|
keyType: "folder",
|
||||||
renameTo: "",
|
renameTo: "",
|
||||||
|
vaultRandomID: vaultRandomID,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
await db.deleteHistoryTbl.setItem(k.key, k);
|
await db.deleteHistoryTbl.setItem(`${vaultRandomID}\t${k.key}`, k);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const insertRenameRecord = async (
|
export const insertRenameRecordByVault = async (
|
||||||
db: InternalDBs,
|
db: InternalDBs,
|
||||||
fileOrFolder: TAbstractFile,
|
fileOrFolder: TAbstractFile,
|
||||||
oldPath: string
|
oldPath: string,
|
||||||
|
vaultRandomID: string
|
||||||
) => {
|
) => {
|
||||||
// log.info(fileOrFolder);
|
// log.info(fileOrFolder);
|
||||||
let k: FileFolderHistoryRecord;
|
let k: FileFolderHistoryRecord;
|
||||||
@@ -180,6 +293,7 @@ export const insertRenameRecord = async (
|
|||||||
actionType: "rename",
|
actionType: "rename",
|
||||||
keyType: "file",
|
keyType: "file",
|
||||||
renameTo: fileOrFolder.path,
|
renameTo: fileOrFolder.path,
|
||||||
|
vaultRandomID: vaultRandomID,
|
||||||
};
|
};
|
||||||
} else if (fileOrFolder instanceof TFolder) {
|
} else if (fileOrFolder instanceof TFolder) {
|
||||||
const key = oldPath.endsWith("/") ? oldPath : `${oldPath}/`;
|
const key = oldPath.endsWith("/") ? oldPath : `${oldPath}/`;
|
||||||
@@ -195,12 +309,13 @@ export const insertRenameRecord = async (
|
|||||||
actionType: "rename",
|
actionType: "rename",
|
||||||
keyType: "folder",
|
keyType: "folder",
|
||||||
renameTo: renameTo,
|
renameTo: renameTo,
|
||||||
|
vaultRandomID: vaultRandomID,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
await db.deleteHistoryTbl.setItem(k.key, k);
|
await db.deleteHistoryTbl.setItem(`${vaultRandomID}\t${k.key}`, k);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const upsertSyncMetaMappingData = async (
|
export const upsertSyncMetaMappingDataByVault = async (
|
||||||
serviceType: SUPPORTED_SERVICES_TYPE,
|
serviceType: SUPPORTED_SERVICES_TYPE,
|
||||||
db: InternalDBs,
|
db: InternalDBs,
|
||||||
localKey: string,
|
localKey: string,
|
||||||
@@ -209,7 +324,8 @@ export const upsertSyncMetaMappingData = async (
|
|||||||
remoteKey: string,
|
remoteKey: string,
|
||||||
remoteMTime: number,
|
remoteMTime: number,
|
||||||
remoteSize: number,
|
remoteSize: number,
|
||||||
remoteExtraKey: string /* ETag from s3 */
|
remoteExtraKey: string,
|
||||||
|
vaultRandomID: string
|
||||||
) => {
|
) => {
|
||||||
const aggregratedInfo: SyncMetaMappingRecord = {
|
const aggregratedInfo: SyncMetaMappingRecord = {
|
||||||
localKey: localKey,
|
localKey: localKey,
|
||||||
@@ -221,19 +337,24 @@ export const upsertSyncMetaMappingData = async (
|
|||||||
remoteExtraKey: remoteExtraKey,
|
remoteExtraKey: remoteExtraKey,
|
||||||
remoteType: serviceType,
|
remoteType: serviceType,
|
||||||
keyType: localKey.endsWith("/") ? "folder" : "file",
|
keyType: localKey.endsWith("/") ? "folder" : "file",
|
||||||
|
vaultRandomID: vaultRandomID,
|
||||||
};
|
};
|
||||||
await db.syncMappingTbl.setItem(remoteKey, aggregratedInfo);
|
await db.syncMappingTbl.setItem(
|
||||||
|
`${vaultRandomID}\t${remoteKey}`,
|
||||||
|
aggregratedInfo
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSyncMetaMappingByRemoteKey = async (
|
export const getSyncMetaMappingByRemoteKeyAndVault = async (
|
||||||
serviceType: SUPPORTED_SERVICES_TYPE,
|
serviceType: SUPPORTED_SERVICES_TYPE,
|
||||||
db: InternalDBs,
|
db: InternalDBs,
|
||||||
remoteKey: string,
|
remoteKey: string,
|
||||||
remoteMTime: number,
|
remoteMTime: number,
|
||||||
remoteExtraKey: string
|
remoteExtraKey: string,
|
||||||
|
vaultRandomID: string
|
||||||
) => {
|
) => {
|
||||||
const potentialItem = (await db.syncMappingTbl.getItem(
|
const potentialItem = (await db.syncMappingTbl.getItem(
|
||||||
remoteKey
|
`${vaultRandomID}\t${remoteKey}`
|
||||||
)) as SyncMetaMappingRecord;
|
)) as SyncMetaMappingRecord;
|
||||||
|
|
||||||
if (potentialItem === null) {
|
if (potentialItem === null) {
|
||||||
@@ -258,26 +379,33 @@ export const clearAllSyncMetaMapping = async (db: InternalDBs) => {
|
|||||||
await db.syncMappingTbl.clear();
|
await db.syncMappingTbl.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const insertSyncPlanRecord = async (
|
export const insertSyncPlanRecordByVault = async (
|
||||||
db: InternalDBs,
|
db: InternalDBs,
|
||||||
syncPlan: SyncPlanType
|
syncPlan: SyncPlanType,
|
||||||
|
vaultRandomID: string
|
||||||
) => {
|
) => {
|
||||||
const record = {
|
const record = {
|
||||||
ts: syncPlan.ts,
|
ts: syncPlan.ts,
|
||||||
|
vaultRandomID: vaultRandomID,
|
||||||
remoteType: syncPlan.remoteType,
|
remoteType: syncPlan.remoteType,
|
||||||
syncPlan: JSON.stringify(syncPlan /* directly stringify */, null, 2),
|
syncPlan: JSON.stringify(syncPlan /* directly stringify */, null, 2),
|
||||||
} as SyncPlanRecord;
|
} as SyncPlanRecord;
|
||||||
await db.syncPlansTbl.setItem(`${syncPlan.ts}`, record);
|
await db.syncPlansTbl.setItem(`${vaultRandomID}\t${syncPlan.ts}`, record);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clearAllSyncPlanRecords = async (db: InternalDBs) => {
|
export const clearAllSyncPlanRecords = async (db: InternalDBs) => {
|
||||||
await db.syncPlansTbl.clear();
|
await db.syncPlansTbl.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const readAllSyncPlanRecordTexts = async (db: InternalDBs) => {
|
export const readAllSyncPlanRecordTextsByVault = async (
|
||||||
|
db: InternalDBs,
|
||||||
|
vaultRandomID: string
|
||||||
|
) => {
|
||||||
const records = [] as SyncPlanRecord[];
|
const records = [] as SyncPlanRecord[];
|
||||||
await db.syncPlansTbl.iterate((value, key, iterationNumber) => {
|
await db.syncPlansTbl.iterate((value, key, iterationNumber) => {
|
||||||
|
if (key.startsWith(`${vaultRandomID}\t`)) {
|
||||||
records.push(value as SyncPlanRecord);
|
records.push(value as SyncPlanRecord);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
records.sort((a, b) => -(a.ts - b.ts)); // descending
|
records.sort((a, b) => -(a.ts - b.ts)); // descending
|
||||||
|
|
||||||
|
|||||||
+235
-95
@@ -1,5 +1,7 @@
|
|||||||
import { Modal, Notice, Plugin, Setting } from "obsidian";
|
import { Modal, Notice, Plugin, Setting, addIcon, setIcon } from "obsidian";
|
||||||
import cloneDeep from "lodash/cloneDeep";
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import feather from "feather-icons";
|
||||||
import type { RemotelySavePluginSettings } from "./baseTypes";
|
import type { RemotelySavePluginSettings } from "./baseTypes";
|
||||||
import {
|
import {
|
||||||
COMMAND_CALLBACK,
|
COMMAND_CALLBACK,
|
||||||
@@ -10,10 +12,10 @@ import {
|
|||||||
import { importQrCodeUri } from "./importExport";
|
import { importQrCodeUri } from "./importExport";
|
||||||
import type { InternalDBs } from "./localdb";
|
import type { InternalDBs } from "./localdb";
|
||||||
import {
|
import {
|
||||||
insertDeleteRecord,
|
insertDeleteRecordByVault,
|
||||||
insertRenameRecord,
|
insertRenameRecordByVault,
|
||||||
insertSyncPlanRecord,
|
insertSyncPlanRecordByVault,
|
||||||
loadDeleteRenameHistoryTable,
|
loadDeleteRenameHistoryTableByVault,
|
||||||
prepareDBs,
|
prepareDBs,
|
||||||
} from "./localdb";
|
} from "./localdb";
|
||||||
import { RemoteClient } from "./remote";
|
import { RemoteClient } from "./remote";
|
||||||
@@ -47,6 +49,8 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
|||||||
password: "",
|
password: "",
|
||||||
serviceType: "s3",
|
serviceType: "s3",
|
||||||
currLogLevel: "info",
|
currLogLevel: "info",
|
||||||
|
vaultRandomID: "",
|
||||||
|
autoRunEveryMilliseconds: -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface OAuth2Info {
|
interface OAuth2Info {
|
||||||
@@ -57,6 +61,19 @@ interface OAuth2Info {
|
|||||||
revokeAuthSetting?: Setting;
|
revokeAuthSetting?: Setting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SyncTriggerSourceType = "manual" | "auto";
|
||||||
|
|
||||||
|
const iconNameSyncWait = `remotely-save-sync-wait`;
|
||||||
|
const iconNameSyncRunning = `remotely-save-sync-running`;
|
||||||
|
const iconSvgSyncWait = feather.icons["rotate-ccw"].toSvg({
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
const iconSvgSyncRunning = feather.icons["refresh-ccw"].toSvg({
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
export default class RemotelySavePlugin extends Plugin {
|
export default class RemotelySavePlugin extends Plugin {
|
||||||
settings: RemotelySavePluginSettings;
|
settings: RemotelySavePluginSettings;
|
||||||
// cm: CodeMirror.Editor;
|
// cm: CodeMirror.Editor;
|
||||||
@@ -64,10 +81,163 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
syncStatus: SyncStatusType;
|
syncStatus: SyncStatusType;
|
||||||
oauth2Info: OAuth2Info;
|
oauth2Info: OAuth2Info;
|
||||||
currLogLevel: string;
|
currLogLevel: string;
|
||||||
|
currSyncMsg?: string;
|
||||||
|
syncRibbon?: HTMLElement;
|
||||||
|
autoRunIntervalID?: number;
|
||||||
|
|
||||||
|
async syncRun(triggerSource: SyncTriggerSourceType = "manual") {
|
||||||
|
const getNotice = (x: string) => {
|
||||||
|
// only show notices in manual mode
|
||||||
|
// no notice in auto mode
|
||||||
|
if (triggerSource === "manual") {
|
||||||
|
new Notice(x);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (this.syncStatus !== "idle") {
|
||||||
|
// here the notice is shown regardless of triggerSource
|
||||||
|
new Notice(`Remotely Save already running in stage ${this.syncStatus}!`);
|
||||||
|
if (this.currSyncMsg !== undefined && this.currSyncMsg !== "") {
|
||||||
|
new Notice(this.currSyncMsg);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let originLabel = `${this.manifest.name}`;
|
||||||
|
if (this.syncRibbon !== undefined) {
|
||||||
|
originLabel = this.syncRibbon.getAttribute("aria-label");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info(
|
||||||
|
`${
|
||||||
|
this.manifest.id
|
||||||
|
}-${Date.now()}: start sync, triggerSource=${triggerSource}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.syncRibbon !== undefined) {
|
||||||
|
setIcon(this.syncRibbon, iconNameSyncRunning);
|
||||||
|
this.syncRibbon.setAttribute(
|
||||||
|
"aria-label",
|
||||||
|
`${this.manifest.name}: ${triggerSource} syncing`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.info(`huh ${this.settings.password}`)
|
||||||
|
getNotice(
|
||||||
|
`1/7 Remotely Save Sync Preparing (${this.settings.serviceType})`
|
||||||
|
);
|
||||||
|
this.syncStatus = "preparing";
|
||||||
|
|
||||||
|
getNotice("2/7 Starting to fetch remote meta data.");
|
||||||
|
this.syncStatus = "getting_remote_meta";
|
||||||
|
const self = this;
|
||||||
|
const client = new RemoteClient(
|
||||||
|
this.settings.serviceType,
|
||||||
|
this.settings.s3,
|
||||||
|
this.settings.webdav,
|
||||||
|
this.settings.dropbox,
|
||||||
|
this.settings.onedrive,
|
||||||
|
this.app.vault.getName(),
|
||||||
|
() => self.saveSettings()
|
||||||
|
);
|
||||||
|
const remoteRsp = await client.listFromRemote();
|
||||||
|
log.info(remoteRsp);
|
||||||
|
|
||||||
|
getNotice("3/7 Starting to fetch local meta data.");
|
||||||
|
this.syncStatus = "getting_local_meta";
|
||||||
|
const local = this.app.vault.getAllLoadedFiles();
|
||||||
|
const localHistory = await loadDeleteRenameHistoryTableByVault(
|
||||||
|
this.db,
|
||||||
|
this.settings.vaultRandomID
|
||||||
|
);
|
||||||
|
// log.info(local);
|
||||||
|
// log.info(localHistory);
|
||||||
|
|
||||||
|
getNotice("4/7 Checking password correct or not.");
|
||||||
|
this.syncStatus = "checking_password";
|
||||||
|
const passwordCheckResult = await isPasswordOk(
|
||||||
|
remoteRsp.Contents,
|
||||||
|
this.settings.password
|
||||||
|
);
|
||||||
|
if (!passwordCheckResult.ok) {
|
||||||
|
getNotice("something goes wrong while checking password");
|
||||||
|
throw Error(passwordCheckResult.reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotice("5/7 Starting to generate sync plan.");
|
||||||
|
this.syncStatus = "generating_plan";
|
||||||
|
const syncPlan = await getSyncPlan(
|
||||||
|
remoteRsp.Contents,
|
||||||
|
local,
|
||||||
|
localHistory,
|
||||||
|
this.db,
|
||||||
|
this.settings.vaultRandomID,
|
||||||
|
client.serviceType,
|
||||||
|
this.settings.password
|
||||||
|
);
|
||||||
|
log.info(syncPlan.mixedStates); // for debugging
|
||||||
|
await insertSyncPlanRecordByVault(
|
||||||
|
this.db,
|
||||||
|
syncPlan,
|
||||||
|
this.settings.vaultRandomID
|
||||||
|
);
|
||||||
|
|
||||||
|
// The operations above are read only and kind of safe.
|
||||||
|
// The operations below begins to write or delete (!!!) something.
|
||||||
|
|
||||||
|
getNotice("6/7 Remotely Save Sync data exchanging!");
|
||||||
|
|
||||||
|
this.syncStatus = "syncing";
|
||||||
|
await doActualSync(
|
||||||
|
client,
|
||||||
|
this.db,
|
||||||
|
this.settings.vaultRandomID,
|
||||||
|
this.app.vault,
|
||||||
|
syncPlan,
|
||||||
|
this.settings.password,
|
||||||
|
(i: number, totalCount: number, pathName: string, decision: string) =>
|
||||||
|
self.setCurrSyncMsg(i, totalCount, pathName, decision)
|
||||||
|
);
|
||||||
|
|
||||||
|
getNotice("7/7 Remotely Save finish!");
|
||||||
|
this.currSyncMsg = "";
|
||||||
|
this.syncStatus = "finish";
|
||||||
|
this.syncStatus = "idle";
|
||||||
|
|
||||||
|
if (this.syncRibbon !== undefined) {
|
||||||
|
setIcon(this.syncRibbon, iconNameSyncWait);
|
||||||
|
this.syncRibbon.setAttribute("aria-label", originLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
`${
|
||||||
|
this.manifest.id
|
||||||
|
}-${Date.now()}: finish sync, triggerSource=${triggerSource}`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const msg = `${
|
||||||
|
this.manifest.id
|
||||||
|
}-${Date.now()}: abort sync, triggerSource=${triggerSource}, error while ${
|
||||||
|
this.syncStatus
|
||||||
|
}`;
|
||||||
|
log.info(msg);
|
||||||
|
log.info(error);
|
||||||
|
getNotice(msg);
|
||||||
|
getNotice(error.message);
|
||||||
|
this.syncStatus = "idle";
|
||||||
|
if (this.syncRibbon !== undefined) {
|
||||||
|
setIcon(this.syncRibbon, iconNameSyncWait);
|
||||||
|
this.syncRibbon.setAttribute("aria-label", originLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
log.info(`loading plugin ${this.manifest.id}`);
|
log.info(`loading plugin ${this.manifest.id}`);
|
||||||
|
|
||||||
|
addIcon(iconNameSyncWait, iconSvgSyncWait);
|
||||||
|
addIcon(iconNameSyncRunning, iconSvgSyncRunning);
|
||||||
|
|
||||||
this.oauth2Info = {
|
this.oauth2Info = {
|
||||||
verifier: "",
|
verifier: "",
|
||||||
helperModal: undefined,
|
helperModal: undefined,
|
||||||
@@ -76,6 +246,8 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
revokeAuthSetting: undefined,
|
revokeAuthSetting: undefined,
|
||||||
}; // init
|
}; // init
|
||||||
|
|
||||||
|
this.currSyncMsg = "";
|
||||||
|
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
|
|
||||||
if (this.settings.currLogLevel !== undefined) {
|
if (this.settings.currLogLevel !== undefined) {
|
||||||
@@ -83,6 +255,7 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.checkIfOauthExpires();
|
await this.checkIfOauthExpires();
|
||||||
|
await this.checkIfVaultIDAssigned(); // MUST before prepareDB()
|
||||||
|
|
||||||
await this.prepareDB();
|
await this.prepareDB();
|
||||||
|
|
||||||
@@ -90,13 +263,22 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
|
|
||||||
this.registerEvent(
|
this.registerEvent(
|
||||||
this.app.vault.on("delete", async (fileOrFolder) => {
|
this.app.vault.on("delete", async (fileOrFolder) => {
|
||||||
await insertDeleteRecord(this.db, fileOrFolder);
|
await insertDeleteRecordByVault(
|
||||||
|
this.db,
|
||||||
|
fileOrFolder,
|
||||||
|
this.settings.vaultRandomID
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.registerEvent(
|
this.registerEvent(
|
||||||
this.app.vault.on("rename", async (fileOrFolder, oldPath) => {
|
this.app.vault.on("rename", async (fileOrFolder, oldPath) => {
|
||||||
await insertRenameRecord(this.db, fileOrFolder, oldPath);
|
await insertRenameRecordByVault(
|
||||||
|
this.db,
|
||||||
|
fileOrFolder,
|
||||||
|
oldPath,
|
||||||
|
this.settings.vaultRandomID
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -277,92 +459,19 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.addRibbonIcon("switch", "Remotely Save", async () => {
|
this.syncRibbon = this.addRibbonIcon(
|
||||||
if (this.syncStatus !== "idle") {
|
iconNameSyncWait,
|
||||||
new Notice(
|
`${this.manifest.name}`,
|
||||||
`Remotely Save already running in stage ${this.syncStatus}!`
|
async () => this.syncRun("manual")
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
//log.info(`huh ${this.settings.password}`)
|
|
||||||
new Notice(
|
|
||||||
`1/7 Remotely Save Sync Preparing (${this.settings.serviceType})`
|
|
||||||
);
|
|
||||||
this.syncStatus = "preparing";
|
|
||||||
|
|
||||||
new Notice("2/7 Starting to fetch remote meta data.");
|
|
||||||
this.syncStatus = "getting_remote_meta";
|
|
||||||
const self = this;
|
|
||||||
const client = new RemoteClient(
|
|
||||||
this.settings.serviceType,
|
|
||||||
this.settings.s3,
|
|
||||||
this.settings.webdav,
|
|
||||||
this.settings.dropbox,
|
|
||||||
this.settings.onedrive,
|
|
||||||
this.app.vault.getName(),
|
|
||||||
() => self.saveSettings()
|
|
||||||
);
|
|
||||||
const remoteRsp = await client.listFromRemote();
|
|
||||||
// log.info(remoteRsp);
|
|
||||||
|
|
||||||
new Notice("3/7 Starting to fetch local meta data.");
|
|
||||||
this.syncStatus = "getting_local_meta";
|
|
||||||
const local = this.app.vault.getAllLoadedFiles();
|
|
||||||
const localHistory = await loadDeleteRenameHistoryTable(this.db);
|
|
||||||
// log.info(local);
|
|
||||||
// log.info(localHistory);
|
|
||||||
|
|
||||||
new Notice("4/7 Checking password correct or not.");
|
|
||||||
this.syncStatus = "checking_password";
|
|
||||||
const passwordCheckResult = await isPasswordOk(
|
|
||||||
remoteRsp.Contents,
|
|
||||||
this.settings.password
|
|
||||||
);
|
|
||||||
if (!passwordCheckResult.ok) {
|
|
||||||
new Notice("something goes wrong while checking password");
|
|
||||||
throw Error(passwordCheckResult.reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
new Notice("5/7 Starting to generate sync plan.");
|
|
||||||
this.syncStatus = "generating_plan";
|
|
||||||
const syncPlan = await getSyncPlan(
|
|
||||||
remoteRsp.Contents,
|
|
||||||
local,
|
|
||||||
localHistory,
|
|
||||||
this.db,
|
|
||||||
client.serviceType,
|
|
||||||
this.settings.password
|
|
||||||
);
|
|
||||||
log.info(syncPlan.mixedStates); // for debugging
|
|
||||||
await insertSyncPlanRecord(this.db, syncPlan);
|
|
||||||
|
|
||||||
// The operations above are read only and kind of safe.
|
|
||||||
// The operations below begins to write or delete (!!!) something.
|
|
||||||
|
|
||||||
new Notice("6/7 Remotely Save Sync data exchanging!");
|
|
||||||
|
|
||||||
this.syncStatus = "syncing";
|
|
||||||
await doActualSync(
|
|
||||||
client,
|
|
||||||
this.db,
|
|
||||||
this.app.vault,
|
|
||||||
syncPlan,
|
|
||||||
this.settings.password
|
|
||||||
);
|
);
|
||||||
|
|
||||||
new Notice("7/7 Remotely Save finish!");
|
this.addCommand({
|
||||||
this.syncStatus = "finish";
|
id: "start-sync",
|
||||||
this.syncStatus = "idle";
|
name: "start sync",
|
||||||
} catch (error) {
|
icon: iconNameSyncWait,
|
||||||
const msg = `Remotely Save error while ${this.syncStatus}`;
|
callback: async () => {
|
||||||
log.info(msg);
|
this.syncRun("manual");
|
||||||
log.info(error);
|
},
|
||||||
new Notice(msg);
|
|
||||||
new Notice(error.message);
|
|
||||||
this.syncStatus = "idle";
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addSettingTab(new RemotelySaveSettingTab(this.app, this));
|
this.addSettingTab(new RemotelySaveSettingTab(this.app, this));
|
||||||
@@ -371,9 +480,17 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
// log.info("click", evt);
|
// log.info("click", evt);
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// this.registerInterval(
|
if (
|
||||||
// window.setInterval(() => log.info("setInterval"), 5 * 60 * 1000)
|
this.settings.autoRunEveryMilliseconds !== undefined &&
|
||||||
// );
|
this.settings.autoRunEveryMilliseconds !== null &&
|
||||||
|
this.settings.autoRunEveryMilliseconds > 0
|
||||||
|
) {
|
||||||
|
const intervalID = window.setInterval(() => {
|
||||||
|
this.syncRun("auto");
|
||||||
|
}, this.settings.autoRunEveryMilliseconds);
|
||||||
|
this.autoRunIntervalID = intervalID;
|
||||||
|
this.registerInterval(intervalID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
@@ -396,6 +513,9 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
if (this.settings.onedrive.authority === "") {
|
if (this.settings.onedrive.authority === "") {
|
||||||
this.settings.onedrive.authority = DEFAULT_SETTINGS.onedrive.authority;
|
this.settings.onedrive.authority = DEFAULT_SETTINGS.onedrive.authority;
|
||||||
}
|
}
|
||||||
|
if (this.settings.webdav.manualRecursive === undefined) {
|
||||||
|
this.settings.webdav.manualRecursive = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSettings() {
|
async saveSettings() {
|
||||||
@@ -472,11 +592,31 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkIfVaultIDAssigned() {
|
||||||
|
if (
|
||||||
|
this.settings.vaultRandomID === undefined ||
|
||||||
|
this.settings.vaultRandomID === ""
|
||||||
|
) {
|
||||||
|
this.settings.vaultRandomID = nanoid();
|
||||||
|
await this.saveSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async prepareDB() {
|
async prepareDB() {
|
||||||
this.db = await prepareDBs();
|
this.db = await prepareDBs(this.settings.vaultRandomID);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroyDBs() {
|
destroyDBs() {
|
||||||
/* destroyDBs(this.db); */
|
/* destroyDBs(this.db); */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setCurrSyncMsg(
|
||||||
|
i: number,
|
||||||
|
totalCount: number,
|
||||||
|
pathName: string,
|
||||||
|
decision: string
|
||||||
|
) {
|
||||||
|
const msg = `syncing progress=${i}/${totalCount},decision=${decision},path=${pathName}`;
|
||||||
|
this.currSyncMsg = msg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,19 +262,31 @@ const fromDriveItemToRemoteItem = (
|
|||||||
// pure english: /drive/root:/Apps/remotely-save/${vaultName}
|
// pure english: /drive/root:/Apps/remotely-save/${vaultName}
|
||||||
// or localized, e.g.: /drive/root:/应用/remotely-save/${vaultName}
|
// or localized, e.g.: /drive/root:/应用/remotely-save/${vaultName}
|
||||||
const FIRST_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/remotely-save\//g;
|
const FIRST_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/remotely-save\//g;
|
||||||
|
// or the root is absolute path /Livefolders,
|
||||||
|
// e.g.: /Livefolders/应用/remotely-save/${vaultName}
|
||||||
|
const SECOND_COMMON_PREFIX_REGEX = /^\/Livefolders\/[^\/]+\/remotely-save\//g;
|
||||||
|
|
||||||
// another possibile prefix
|
// another possibile prefix
|
||||||
const SECOND_COMMON_PREFIX_RAW = `/drive/items/`;
|
const THIRD_COMMON_PREFIX_RAW = `/drive/items/`;
|
||||||
|
|
||||||
const fullPathOriginal = `${x.parentReference.path}/${x.name}`;
|
const fullPathOriginal = `${x.parentReference.path}/${x.name}`;
|
||||||
const matchFirstPrefixRes = fullPathOriginal.match(FIRST_COMMON_PREFIX_REGEX);
|
const matchFirstPrefixRes = fullPathOriginal.match(FIRST_COMMON_PREFIX_REGEX);
|
||||||
|
const matchSecondPrefixRes = fullPathOriginal.match(
|
||||||
|
SECOND_COMMON_PREFIX_REGEX
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
matchFirstPrefixRes !== null &&
|
matchFirstPrefixRes !== null &&
|
||||||
fullPathOriginal.startsWith(`${matchFirstPrefixRes[0]}${vaultName}`)
|
fullPathOriginal.startsWith(`${matchFirstPrefixRes[0]}${vaultName}`)
|
||||||
) {
|
) {
|
||||||
const foundPrefix = `${matchFirstPrefixRes[0]}${vaultName}`;
|
const foundPrefix = `${matchFirstPrefixRes[0]}${vaultName}`;
|
||||||
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||||
} else if (x.parentReference.path.startsWith(SECOND_COMMON_PREFIX_RAW)) {
|
} else if (
|
||||||
|
matchSecondPrefixRes !== null &&
|
||||||
|
fullPathOriginal.startsWith(`${matchSecondPrefixRes[0]}${vaultName}`)
|
||||||
|
) {
|
||||||
|
const foundPrefix = `${matchSecondPrefixRes[0]}${vaultName}`;
|
||||||
|
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||||
|
} else if (x.parentReference.path.startsWith(THIRD_COMMON_PREFIX_RAW)) {
|
||||||
// it's something like
|
// it's something like
|
||||||
// /drive/items/<some_id>!<another_id>:/${vaultName}/<subfolder>
|
// /drive/items/<some_id>!<another_id>:/${vaultName}/<subfolder>
|
||||||
// with uri encoded!
|
// with uri encoded!
|
||||||
|
|||||||
+42
-1
@@ -2,6 +2,9 @@ import { Buffer } from "buffer";
|
|||||||
import { Vault } from "obsidian";
|
import { Vault } from "obsidian";
|
||||||
import type { FileStat, WebDAVClient } from "webdav/web";
|
import type { FileStat, WebDAVClient } from "webdav/web";
|
||||||
import { AuthType, BufferLike, createClient } from "webdav/web";
|
import { AuthType, BufferLike, createClient } from "webdav/web";
|
||||||
|
import { Queue } from "@fyears/tsqueue";
|
||||||
|
import chunk from "lodash/chunk";
|
||||||
|
import flatten from "lodash/flatten";
|
||||||
import type { RemoteItem, WebdavConfig } from "./baseTypes";
|
import type { RemoteItem, WebdavConfig } from "./baseTypes";
|
||||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||||
import { bufferToArrayBuffer, getPathFolder, mkdirpInVault } from "./misc";
|
import { bufferToArrayBuffer, getPathFolder, mkdirpInVault } from "./misc";
|
||||||
@@ -15,6 +18,7 @@ export const DEFAULT_WEBDAV_CONFIG = {
|
|||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
authType: "basic",
|
authType: "basic",
|
||||||
|
manualRecursive: false,
|
||||||
} as WebdavConfig;
|
} as WebdavConfig;
|
||||||
|
|
||||||
const getWebdavPath = (fileOrFolderPath: string, vaultName: string) => {
|
const getWebdavPath = (fileOrFolderPath: string, vaultName: string) => {
|
||||||
@@ -199,7 +203,43 @@ export const listFromRemote = async (
|
|||||||
throw Error("prefix not supported");
|
throw Error("prefix not supported");
|
||||||
}
|
}
|
||||||
await client.init();
|
await client.init();
|
||||||
const contents = (await client.client.getDirectoryContents(
|
|
||||||
|
let contents = [] as FileStat[];
|
||||||
|
if (client.webdavConfig.manualRecursive) {
|
||||||
|
// the remote doesn't support infinity propfind,
|
||||||
|
// we need to do a bfs here
|
||||||
|
const q = new Queue([`/${client.vaultName}`]);
|
||||||
|
const CHUNK_SIZE = 10;
|
||||||
|
while (q.length > 0) {
|
||||||
|
const itemsToFetch = [];
|
||||||
|
while (q.length > 0) {
|
||||||
|
itemsToFetch.push(q.pop());
|
||||||
|
}
|
||||||
|
const itemsToFetchChunks = chunk(itemsToFetch, CHUNK_SIZE);
|
||||||
|
// log.debug(itemsToFetchChunks);
|
||||||
|
const subContents = [] as FileStat[];
|
||||||
|
for (const singleChunk of itemsToFetchChunks) {
|
||||||
|
const r = singleChunk.map((x) => {
|
||||||
|
return client.client.getDirectoryContents(x, {
|
||||||
|
deep: false,
|
||||||
|
details: false /* no need for verbose details here */,
|
||||||
|
glob: "/**" /* avoid dot files by using glob */,
|
||||||
|
}) as Promise<FileStat[]>;
|
||||||
|
});
|
||||||
|
const r2 = flatten(await Promise.all(r));
|
||||||
|
subContents.push(...r2);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < subContents.length; ++i) {
|
||||||
|
const f = subContents[i];
|
||||||
|
contents.push(f);
|
||||||
|
if (f.type === "directory") {
|
||||||
|
q.push(f.filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// the remote supports infinity propfind
|
||||||
|
contents = (await client.client.getDirectoryContents(
|
||||||
`/${client.vaultName}`,
|
`/${client.vaultName}`,
|
||||||
{
|
{
|
||||||
deep: true,
|
deep: true,
|
||||||
@@ -207,6 +247,7 @@ export const listFromRemote = async (
|
|||||||
glob: "/**" /* avoid dot files by using glob */,
|
glob: "/**" /* avoid dot files by using glob */,
|
||||||
}
|
}
|
||||||
)) as FileStat[];
|
)) as FileStat[];
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
Contents: contents.map((x) =>
|
Contents: contents.map((x) =>
|
||||||
fromWebdavItemToRemoteItem(x, client.vaultName)
|
fromWebdavItemToRemoteItem(x, client.vaultName)
|
||||||
|
|||||||
+91
-2
@@ -1,6 +1,6 @@
|
|||||||
import { App, Modal, Notice, PluginSettingTab, Setting } from "obsidian";
|
import { App, Modal, Notice, PluginSettingTab, Setting } from "obsidian";
|
||||||
import type { SUPPORTED_SERVICES_TYPE, WebdavAuthType } from "./baseTypes";
|
import type { SUPPORTED_SERVICES_TYPE, WebdavAuthType } from "./baseTypes";
|
||||||
import { exportSyncPlansToFiles } from "./debugMode";
|
import { exportVaultSyncPlansToFiles } from "./debugMode";
|
||||||
import { exportQrCodeUri } from "./importExport";
|
import { exportQrCodeUri } from "./importExport";
|
||||||
import {
|
import {
|
||||||
clearAllSyncMetaMapping,
|
clearAllSyncMetaMapping,
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
DEFAULT_ONEDRIVE_CONFIG,
|
DEFAULT_ONEDRIVE_CONFIG,
|
||||||
getAuthUrlAndVerifier as getAuthUrlAndVerifierOnedrive,
|
getAuthUrlAndVerifier as getAuthUrlAndVerifierOnedrive,
|
||||||
} from "./remoteForOnedrive";
|
} from "./remoteForOnedrive";
|
||||||
|
import { messyConfigToNormal } from "./configPersist";
|
||||||
|
|
||||||
import * as origLog from "loglevel";
|
import * as origLog from "loglevel";
|
||||||
const log = origLog.getLogger("rs-default");
|
const log = origLog.getLogger("rs-default");
|
||||||
@@ -381,6 +382,46 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const scheduleDiv = generalDiv.createEl("div");
|
||||||
|
new Setting(scheduleDiv)
|
||||||
|
.setName("schedule for auto run")
|
||||||
|
.setDesc(
|
||||||
|
"The plugin trys to schedule the running after every interval. Battery may be impacted."
|
||||||
|
)
|
||||||
|
.addDropdown((dropdown) => {
|
||||||
|
dropdown.addOption("-1", "(no auto run)");
|
||||||
|
dropdown.addOption(`${1000 * 60 * 1}`, "every 1 minute");
|
||||||
|
dropdown.addOption(`${1000 * 60 * 5}`, "every 5 minutes");
|
||||||
|
dropdown.addOption(`${1000 * 60 * 10}`, "every 10 minutes");
|
||||||
|
dropdown.addOption(`${1000 * 60 * 30}`, "every 30 minutes");
|
||||||
|
|
||||||
|
dropdown
|
||||||
|
.setValue(`${this.plugin.settings.autoRunEveryMilliseconds}`)
|
||||||
|
.onChange(async (val: string) => {
|
||||||
|
const realVal = parseInt(val);
|
||||||
|
this.plugin.settings.autoRunEveryMilliseconds = realVal;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
if (
|
||||||
|
(realVal === undefined || realVal === null || realVal <= 0) &&
|
||||||
|
this.plugin.autoRunIntervalID !== undefined
|
||||||
|
) {
|
||||||
|
// clear
|
||||||
|
window.clearInterval(this.plugin.autoRunIntervalID);
|
||||||
|
this.plugin.autoRunIntervalID = undefined;
|
||||||
|
} else if (
|
||||||
|
realVal !== undefined &&
|
||||||
|
realVal !== null &&
|
||||||
|
realVal > 0
|
||||||
|
) {
|
||||||
|
const intervalID = window.setInterval(() => {
|
||||||
|
this.plugin.syncRun("auto");
|
||||||
|
}, realVal);
|
||||||
|
this.plugin.autoRunIntervalID = intervalID;
|
||||||
|
this.plugin.registerInterval(intervalID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
// below for general chooser (part 1/2)
|
// below for general chooser (part 1/2)
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
@@ -831,6 +872,32 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
new Setting(webdavDiv)
|
||||||
|
.setName("server supports infinity propfind or not")
|
||||||
|
.setDesc(
|
||||||
|
"The plugin needs to get all files and folders recursively using probfind. If your webdav server only supports depth='1' (such as NGINX), you need to adjust the setting here, then the plugin consumes more network requests, but better than not working."
|
||||||
|
)
|
||||||
|
.addDropdown((dropdown) => {
|
||||||
|
dropdown.addOption("infinity", "supports depth='infinity'");
|
||||||
|
dropdown.addOption("1", "only supports depth='1'");
|
||||||
|
|
||||||
|
type Depth = "1" | "infinity";
|
||||||
|
dropdown
|
||||||
|
.setValue(
|
||||||
|
this.plugin.settings.webdav.manualRecursive === false
|
||||||
|
? "infinity"
|
||||||
|
: "1"
|
||||||
|
)
|
||||||
|
.onChange(async (val: Depth) => {
|
||||||
|
if (val === "1") {
|
||||||
|
this.plugin.settings.webdav.manualRecursive = true;
|
||||||
|
} else if (val === "infinity") {
|
||||||
|
this.plugin.settings.webdav.manualRecursive = false;
|
||||||
|
}
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
new Setting(webdavDiv)
|
new Setting(webdavDiv)
|
||||||
.setName("check connectivity")
|
.setName("check connectivity")
|
||||||
.setDesc("check connectivity")
|
.setDesc("check connectivity")
|
||||||
@@ -946,6 +1013,24 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
log.info(`the log level is changed to ${val}`);
|
log.info(`the log level is changed to ${val}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
const outputCurrSettingsDiv = debugDiv.createDiv("div");
|
||||||
|
new Setting(outputCurrSettingsDiv)
|
||||||
|
.setName("output current settings from disk to console")
|
||||||
|
.setDesc(
|
||||||
|
"The settings save on disk in encoded. Click this to see the decoded settings in console."
|
||||||
|
)
|
||||||
|
.addButton(async (button) => {
|
||||||
|
button.setButtonText("Output");
|
||||||
|
button.onClick(async () => {
|
||||||
|
const c = messyConfigToNormal(await this.plugin.loadData());
|
||||||
|
if (c.currLogLevel === "debug") {
|
||||||
|
// no need to ouput it again because debug mode already output it
|
||||||
|
} else {
|
||||||
|
log.info(c);
|
||||||
|
}
|
||||||
|
new Notice("Finished outputing in console.");
|
||||||
|
});
|
||||||
|
});
|
||||||
const syncPlanDiv = debugDiv.createEl("div");
|
const syncPlanDiv = debugDiv.createEl("div");
|
||||||
new Setting(syncPlanDiv)
|
new Setting(syncPlanDiv)
|
||||||
.setName("export sync plans")
|
.setName("export sync plans")
|
||||||
@@ -955,7 +1040,11 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
.addButton(async (button) => {
|
.addButton(async (button) => {
|
||||||
button.setButtonText("Export");
|
button.setButtonText("Export");
|
||||||
button.onClick(async () => {
|
button.onClick(async () => {
|
||||||
await exportSyncPlansToFiles(this.plugin.db, this.app.vault);
|
await exportVaultSyncPlansToFiles(
|
||||||
|
this.plugin.db,
|
||||||
|
this.app.vault,
|
||||||
|
this.plugin.settings.vaultRandomID
|
||||||
|
);
|
||||||
new Notice("sync plans history exported");
|
new Notice("sync plans history exported");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+43
-15
@@ -9,9 +9,9 @@ import {
|
|||||||
} from "./encrypt";
|
} from "./encrypt";
|
||||||
import type { FileFolderHistoryRecord, InternalDBs } from "./localdb";
|
import type { FileFolderHistoryRecord, InternalDBs } from "./localdb";
|
||||||
import {
|
import {
|
||||||
clearDeleteRenameHistoryOfKey,
|
clearDeleteRenameHistoryOfKeyAndVault,
|
||||||
getSyncMetaMappingByRemoteKey,
|
getSyncMetaMappingByRemoteKeyAndVault,
|
||||||
upsertSyncMetaMappingData,
|
upsertSyncMetaMappingDataByVault,
|
||||||
} from "./localdb";
|
} from "./localdb";
|
||||||
import { isHiddenPath, isVaildText, mkdirpInVault } from "./misc";
|
import { isHiddenPath, isVaildText, mkdirpInVault } from "./misc";
|
||||||
import { RemoteClient } from "./remote";
|
import { RemoteClient } from "./remote";
|
||||||
@@ -170,6 +170,7 @@ const ensembleMixedStates = async (
|
|||||||
local: TAbstractFile[],
|
local: TAbstractFile[],
|
||||||
deleteHistory: FileFolderHistoryRecord[],
|
deleteHistory: FileFolderHistoryRecord[],
|
||||||
db: InternalDBs,
|
db: InternalDBs,
|
||||||
|
vaultRandomID: string,
|
||||||
remoteType: SUPPORTED_SERVICES_TYPE,
|
remoteType: SUPPORTED_SERVICES_TYPE,
|
||||||
password: string = ""
|
password: string = ""
|
||||||
) => {
|
) => {
|
||||||
@@ -190,12 +191,13 @@ const ensembleMixedStates = async (
|
|||||||
throw Error(`unexpected key=${remoteEncryptedKey}`);
|
throw Error(`unexpected key=${remoteEncryptedKey}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const backwardMapping = await getSyncMetaMappingByRemoteKey(
|
const backwardMapping = await getSyncMetaMappingByRemoteKeyAndVault(
|
||||||
remoteType,
|
remoteType,
|
||||||
db,
|
db,
|
||||||
key,
|
key,
|
||||||
entry.lastModified,
|
entry.lastModified,
|
||||||
entry.etag
|
entry.etag,
|
||||||
|
vaultRandomID
|
||||||
);
|
);
|
||||||
|
|
||||||
let r = {} as FileOrFolderMixedState;
|
let r = {} as FileOrFolderMixedState;
|
||||||
@@ -443,6 +445,7 @@ export const getSyncPlan = async (
|
|||||||
local: TAbstractFile[],
|
local: TAbstractFile[],
|
||||||
deleteHistory: FileFolderHistoryRecord[],
|
deleteHistory: FileFolderHistoryRecord[],
|
||||||
db: InternalDBs,
|
db: InternalDBs,
|
||||||
|
vaultRandomID: string,
|
||||||
remoteType: SUPPORTED_SERVICES_TYPE,
|
remoteType: SUPPORTED_SERVICES_TYPE,
|
||||||
password: string = ""
|
password: string = ""
|
||||||
) => {
|
) => {
|
||||||
@@ -451,6 +454,7 @@ export const getSyncPlan = async (
|
|||||||
local,
|
local,
|
||||||
deleteHistory,
|
deleteHistory,
|
||||||
db,
|
db,
|
||||||
|
vaultRandomID,
|
||||||
remoteType,
|
remoteType,
|
||||||
password
|
password
|
||||||
);
|
);
|
||||||
@@ -467,6 +471,7 @@ export const getSyncPlan = async (
|
|||||||
|
|
||||||
const dispatchOperationToActual = async (
|
const dispatchOperationToActual = async (
|
||||||
key: string,
|
key: string,
|
||||||
|
vaultRandomID: string,
|
||||||
state: FileOrFolderMixedState,
|
state: FileOrFolderMixedState,
|
||||||
client: RemoteClient,
|
client: RemoteClient,
|
||||||
db: InternalDBs,
|
db: InternalDBs,
|
||||||
@@ -493,6 +498,18 @@ const dispatchOperationToActual = async (
|
|||||||
throw Error(`unknown decision in ${JSON.stringify(state)}`);
|
throw Error(`unknown decision in ${JSON.stringify(state)}`);
|
||||||
} else if (state.decision === "skip") {
|
} else if (state.decision === "skip") {
|
||||||
// do nothing
|
// do nothing
|
||||||
|
} else if (
|
||||||
|
client.serviceType === "onedrive" &&
|
||||||
|
state.size_local === 0 &&
|
||||||
|
!state.key.endsWith("/") &&
|
||||||
|
password === "" &&
|
||||||
|
(state.decision === "upload" || state.decision === "upload_clearhist")
|
||||||
|
) {
|
||||||
|
// TODO: it's ugly, any other way to deal with empty file for onedrive?
|
||||||
|
// do nothing, skip empty file without encryption
|
||||||
|
// if it's empty folder, or it's encrypted file/folder, it continues to be uploaded.
|
||||||
|
// this branch should be earlier than normal upload / upload_clearhist branches.
|
||||||
|
log.debug(`skip empty file ${state.key} uploading for OneDrive`);
|
||||||
} else if (state.decision === "download_clearhist") {
|
} else if (state.decision === "download_clearhist") {
|
||||||
await client.downloadFromRemote(
|
await client.downloadFromRemote(
|
||||||
state.key,
|
state.key,
|
||||||
@@ -501,7 +518,7 @@ const dispatchOperationToActual = async (
|
|||||||
password,
|
password,
|
||||||
remoteEncryptedKey
|
remoteEncryptedKey
|
||||||
);
|
);
|
||||||
await clearDeleteRenameHistoryOfKey(db, state.key);
|
await clearDeleteRenameHistoryOfKeyAndVault(db, state.key, vaultRandomID);
|
||||||
} else if (state.decision === "upload_clearhist") {
|
} else if (state.decision === "upload_clearhist") {
|
||||||
const remoteObjMeta = await client.uploadToRemote(
|
const remoteObjMeta = await client.uploadToRemote(
|
||||||
state.key,
|
state.key,
|
||||||
@@ -511,7 +528,7 @@ const dispatchOperationToActual = async (
|
|||||||
remoteEncryptedKey,
|
remoteEncryptedKey,
|
||||||
foldersCreatedBefore
|
foldersCreatedBefore
|
||||||
);
|
);
|
||||||
await upsertSyncMetaMappingData(
|
await upsertSyncMetaMappingDataByVault(
|
||||||
client.serviceType,
|
client.serviceType,
|
||||||
db,
|
db,
|
||||||
state.key,
|
state.key,
|
||||||
@@ -520,9 +537,10 @@ const dispatchOperationToActual = async (
|
|||||||
state.key,
|
state.key,
|
||||||
remoteObjMeta.lastModified,
|
remoteObjMeta.lastModified,
|
||||||
remoteObjMeta.size,
|
remoteObjMeta.size,
|
||||||
remoteObjMeta.etag
|
remoteObjMeta.etag,
|
||||||
|
vaultRandomID
|
||||||
);
|
);
|
||||||
await clearDeleteRenameHistoryOfKey(db, state.key);
|
await clearDeleteRenameHistoryOfKeyAndVault(db, state.key, vaultRandomID);
|
||||||
} else if (state.decision === "download") {
|
} else if (state.decision === "download") {
|
||||||
await mkdirpInVault(state.key, vault);
|
await mkdirpInVault(state.key, vault);
|
||||||
await client.downloadFromRemote(
|
await client.downloadFromRemote(
|
||||||
@@ -534,7 +552,7 @@ const dispatchOperationToActual = async (
|
|||||||
);
|
);
|
||||||
} else if (state.decision === "delremote_clearhist") {
|
} else if (state.decision === "delremote_clearhist") {
|
||||||
await client.deleteFromRemote(state.key, password, remoteEncryptedKey);
|
await client.deleteFromRemote(state.key, password, remoteEncryptedKey);
|
||||||
await clearDeleteRenameHistoryOfKey(db, state.key);
|
await clearDeleteRenameHistoryOfKeyAndVault(db, state.key, vaultRandomID);
|
||||||
} else if (state.decision === "upload") {
|
} else if (state.decision === "upload") {
|
||||||
const remoteObjMeta = await client.uploadToRemote(
|
const remoteObjMeta = await client.uploadToRemote(
|
||||||
state.key,
|
state.key,
|
||||||
@@ -544,7 +562,7 @@ const dispatchOperationToActual = async (
|
|||||||
remoteEncryptedKey,
|
remoteEncryptedKey,
|
||||||
foldersCreatedBefore
|
foldersCreatedBefore
|
||||||
);
|
);
|
||||||
await upsertSyncMetaMappingData(
|
await upsertSyncMetaMappingDataByVault(
|
||||||
client.serviceType,
|
client.serviceType,
|
||||||
db,
|
db,
|
||||||
state.key,
|
state.key,
|
||||||
@@ -553,10 +571,11 @@ const dispatchOperationToActual = async (
|
|||||||
state.key,
|
state.key,
|
||||||
remoteObjMeta.lastModified,
|
remoteObjMeta.lastModified,
|
||||||
remoteObjMeta.size,
|
remoteObjMeta.size,
|
||||||
remoteObjMeta.etag
|
remoteObjMeta.etag,
|
||||||
|
vaultRandomID
|
||||||
);
|
);
|
||||||
} else if (state.decision === "clearhist") {
|
} else if (state.decision === "clearhist") {
|
||||||
await clearDeleteRenameHistoryOfKey(db, state.key);
|
await clearDeleteRenameHistoryOfKeyAndVault(db, state.key, vaultRandomID);
|
||||||
} else {
|
} else {
|
||||||
throw Error("this should never happen!");
|
throw Error("this should never happen!");
|
||||||
}
|
}
|
||||||
@@ -565,20 +584,29 @@ const dispatchOperationToActual = async (
|
|||||||
export const doActualSync = async (
|
export const doActualSync = async (
|
||||||
client: RemoteClient,
|
client: RemoteClient,
|
||||||
db: InternalDBs,
|
db: InternalDBs,
|
||||||
|
vaultRandomID: string,
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
syncPlan: SyncPlanType,
|
syncPlan: SyncPlanType,
|
||||||
password: string = ""
|
password: string = "",
|
||||||
|
callbackSyncProcess?: any
|
||||||
) => {
|
) => {
|
||||||
const keyStates = syncPlan.mixedStates;
|
const keyStates = syncPlan.mixedStates;
|
||||||
const foldersCreatedBefore = new Set<string>();
|
const foldersCreatedBefore = new Set<string>();
|
||||||
|
let i = 0;
|
||||||
|
const totalCount = Object.keys(keyStates).length || 0;
|
||||||
for (const [k, v] of Object.entries(keyStates).sort(
|
for (const [k, v] of Object.entries(keyStates).sort(
|
||||||
([k1, v1], [k2, v2]) => k2.length - k1.length
|
([k1, v1], [k2, v2]) => k2.length - k1.length
|
||||||
)) {
|
)) {
|
||||||
|
i += 1;
|
||||||
const k2 = k as string;
|
const k2 = k as string;
|
||||||
const v2 = v as FileOrFolderMixedState;
|
const v2 = v as FileOrFolderMixedState;
|
||||||
log.debug(`start syncing "${k2}" with plan ${JSON.stringify(v2)}`);
|
log.debug(`start syncing "${k2}" with plan ${JSON.stringify(v2)}`);
|
||||||
|
if (callbackSyncProcess !== undefined) {
|
||||||
|
await callbackSyncProcess(i, totalCount, k2, v2.decision);
|
||||||
|
}
|
||||||
await dispatchOperationToActual(
|
await dispatchOperationToActual(
|
||||||
k2,
|
k2,
|
||||||
|
vaultRandomID,
|
||||||
v2,
|
v2,
|
||||||
client,
|
client,
|
||||||
db,
|
db,
|
||||||
@@ -586,7 +614,7 @@ export const doActualSync = async (
|
|||||||
password,
|
password,
|
||||||
foldersCreatedBefore
|
foldersCreatedBefore
|
||||||
);
|
);
|
||||||
// log.info(`finished ${k}, with ${setToString(foldersCreatedBefore)}`);
|
log.info(`finished ${k2}`);
|
||||||
}
|
}
|
||||||
// await Promise.all(
|
// await Promise.all(
|
||||||
// Object.entries(keyStates)
|
// Object.entries(keyStates)
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"0.2.10": "0.12.15"
|
"0.2.13": "0.12.15"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user