Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bebc7226d | ||
|
|
c9b86fc42d | ||
|
|
5d11801c28 | ||
|
|
1f8754633f | ||
|
|
6cdbc5b32e | ||
|
|
a78a2a88cf | ||
|
|
4035218706 | ||
|
|
b0cb1248a5 | ||
|
|
f936704a6a | ||
|
|
04f3a0c97c | ||
|
|
c637038d95 | ||
|
|
e7c24b55a3 | ||
|
|
9a66e3181d | ||
|
|
510755d7f0 | ||
|
|
27fe428fe4 | ||
|
|
ffabaacaf1 | ||
|
|
6790bb01d3 | ||
|
|
131f3f70e1 | ||
|
|
b551e0c1b5 | ||
|
|
c65c0aa258 | ||
|
|
1f0030b514 | ||
|
|
07896f92a3 | ||
|
|
9eddfb02d6 | ||
|
|
f0dd3bb5ef | ||
|
|
b91c0e694c | ||
|
|
84b03d2c2b | ||
|
|
313eff6a14 | ||
|
|
8671ba4660 | ||
|
|
3b195f33c8 | ||
|
|
65e18aa4b7 | ||
|
|
2cedef8168 | ||
|
|
22bc74f4d7 | ||
|
|
277793ccf4 | ||
|
|
9a1b11b811 | ||
|
|
3d6746ed99 | ||
|
|
2a797f8c40 |
@@ -0,0 +1,3 @@
|
||||
DROPBOX_APP_KEY=
|
||||
ONEDRIVE_CLIENT_ID=
|
||||
ONEDRIVE_AUTHORITY=https://
|
||||
@@ -15,6 +15,11 @@ jobs:
|
||||
|
||||
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:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
main.js
|
||||
data.json
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
This is yet another unofficial sync plugin for Obsidian. If you like it or find it useful, please consider give it a [star ](https://github.com/fyears/remotely-save) on Github.
|
||||
|
||||
[](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml) [](https://github.com/fyears/remotely-save/releases) [](https://github.com/fyears/remotely-save/releases)
|
||||
[](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml)
|
||||
|
||||
[](https://github.com/fyears/remotely-save/releases)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
@@ -17,10 +19,11 @@ As of Jan 2022, the plugin is considered in BETA stage. **DO NOT USE IT for any
|
||||
- Supports:
|
||||
- Amazon S3 or S3-compatible
|
||||
- Dropbox
|
||||
- OneDrive
|
||||
- OneDrive for personal
|
||||
- Webdav
|
||||
- **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.
|
||||
- **Scheduled auto sync supported.**
|
||||
- **[Minimal Intrusive](./docs/minimal_intrusive_design.md).**
|
||||
- **Fully open source under [Apache-2.0 License](./LICENSE).**
|
||||
- **[Sync Algorithm open](./docs/sync_algorithm.md) for discussion.**
|
||||
@@ -45,7 +48,7 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
||||
|
||||
## 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 #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.
|
||||
@@ -59,7 +62,7 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
||||
- Download and enable 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.
|
||||
- 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".
|
||||
- **Be patient while syncing.** Especially in the first-time sync.
|
||||
|
||||
### Dropbox
|
||||
@@ -70,10 +73,11 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
||||
- If you decide to authorize this plugin to connect to Dropbox, please go to plugin's settings, and choose Dropbox then follow the instructions.
|
||||
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
||||
|
||||
### OneDrive
|
||||
### OneDrive for personal
|
||||
|
||||
- **This plugin's function for OneDrive is not as mature as functions for S3.**
|
||||
- **This plugin is NOT an official Microsoft / OneDrive product.** The plugin just uses Microsoft's [OneDrive's public API](https://docs.microsoft.com/en-us/onedrive/developer/rest-api).
|
||||
- This plugin only works for "OneDrive for personal", and not works for "OneDrive for Business" (yet). See [#11](https://github.com/fyears/remotely-save/issues/11) to further details.
|
||||
- After the authorization, the plugin can read your name and email, and read and write files in your OneDrive's `/Apps/remotely-save` folder.
|
||||
- If you decide to authorize this plugin to connect to OneDrive, please go to plugin's settings, and choose OneDrive then follow the instructions.
|
||||
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
||||
@@ -88,22 +92,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.
|
||||
- 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:
|
||||
|
||||
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.
|
||||
See [here](./docs/import_export_some_settings.md) for more details.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a9bf1cc2403c3a32e7a6f5d5829bb065573922dc050ae2fdb8b3d34b4c87ecf5
|
||||
size 8296
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6990a598379c457442345e50c3f056c4598a121523bc10980835c08fc7447a54
|
||||
size 10119
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:432198e63fa9676541a41436917e025d1ea6af3772139c1dea450fcc0155cfd0
|
||||
size 13054
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fe3b5b8bf9c32e57c0af366c455b11ae68eb9fb8a32f371c9c445b3e7e89cc02
|
||||
size 1120
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f68df7bae94eb4c4cddfa6af47d7819d01a6482d4f6afe6ef3236f72f9adf15f
|
||||
size 1523
|
||||
@@ -0,0 +1 @@
|
||||
This work is licensed under CC BY-SA 4.0. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/
|
||||
@@ -0,0 +1,75 @@
|
||||
# the logo
|
||||
|
||||
Some different sizes' logos are needed.
|
||||
|
||||
## license
|
||||
|
||||
<a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"><img alt="Creative Commons License" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/88x31.png" /></a><br />This work is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International License</a>.
|
||||
|
||||
## the process of creation
|
||||
|
||||
No professional designers are here. Thus the following steps involve many programer styles. :-)
|
||||
|
||||
1. use excalidraw and export png and svg.
|
||||
|
||||
```bash
|
||||
# results
|
||||
logo.excalidraw
|
||||
logo.png
|
||||
logo.svg
|
||||
```
|
||||
|
||||
2. manually edit the `logo.svg` and make background transparent.
|
||||
|
||||
```bash
|
||||
# results
|
||||
logo-transparent.svg
|
||||
```
|
||||
|
||||
3. use python library [`svgutils`](https://github.com/btel/svg_utils) to make a strictly square figure. The [doc](https://svgutils.readthedocs.io/en/latest/tutorials/composing_multipanel_figures.html) is very useful.
|
||||
|
||||
```python
|
||||
from svgutils.compose import *
|
||||
def get_standard_300x300(file_name):
|
||||
fig = Figure(300, 300,
|
||||
Panel(
|
||||
SVG(file_name),
|
||||
).move(-3, 12),
|
||||
)
|
||||
return fig
|
||||
|
||||
get_standard_300x300('logo-transparent.svg').save('300x300.svg')
|
||||
|
||||
# def get_other_size_from_standard(file_name, px):
|
||||
# fig = Figure(px, px,
|
||||
# Panel(
|
||||
# SVG(file_name).scale(px/300.0),
|
||||
# ).move(-3*px/300.0, 12*px/300.0),
|
||||
# )
|
||||
# return fig
|
||||
|
||||
# get_other_size_from_standard('logo.svg',256).save('256x256.svg')
|
||||
```
|
||||
|
||||
```bash
|
||||
# results
|
||||
300x300.svg
|
||||
```
|
||||
|
||||
4. use `inkscape` command line to get different sizes' `.png` files.
|
||||
|
||||
```bash
|
||||
inkscape 300x300.svg -o 300x300.png
|
||||
|
||||
inkscape 300x300.svg -o 50x50.png -w 50 -h 50
|
||||
|
||||
inkscape 300x300.svg -o 64x64.png -w 64 -h 64
|
||||
inkscape 300x300.svg -o 256x256.png -w 256 -h 256
|
||||
```
|
||||
|
||||
```bash
|
||||
# results
|
||||
50x50.png
|
||||
64x64.png
|
||||
256x256.png
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:daedd262170200cb5e8cc288898b40892771b3962cf2bc17dc443f1c82a4aa94
|
||||
size 12886
|
||||
@@ -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 DEFAULT_DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY || "";
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "remotely-save",
|
||||
"name": "Remotely Save",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.11",
|
||||
"minAppVersion": "0.12.15",
|
||||
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
||||
"author": "fyears",
|
||||
|
||||
+8
-3
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "remotely-save",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.11",
|
||||
"description": "This is yet another sync plugin for Obsidian app.",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"dev2": "node esbuild.config.mjs",
|
||||
"build2": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"build": "webpack --mode production",
|
||||
"dev": "webpack --mode development --watch",
|
||||
"format": "npx prettier --write .",
|
||||
"clean": "npx rimraf main.js",
|
||||
"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",
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/chai-as-promised": "^7.1.4",
|
||||
"@types/feather-icons": "^4.7.0",
|
||||
"@types/jsdom": "^16.2.13",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
@@ -35,7 +37,7 @@
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^10.0.0",
|
||||
"esbuild": "^0.14.0",
|
||||
"esbuild": "^0.14.3",
|
||||
"jsdom": "^19.0.0",
|
||||
"mocha": "^9.1.3",
|
||||
"prettier": "^2.4.1",
|
||||
@@ -60,9 +62,12 @@
|
||||
"codemirror": "^5.63.1",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"dropbox": "^10.22.0",
|
||||
"feather-icons": "^4.28.0",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.8.0",
|
||||
"mime-types": "^2.1.33",
|
||||
"nanoid": "^3.1.30",
|
||||
"obsidian": "^0.12.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"process": "^0.11.10",
|
||||
|
||||
@@ -52,6 +52,9 @@ export interface RemotelySavePluginSettings {
|
||||
onedrive: OnedriveConfig;
|
||||
password: string;
|
||||
serviceType: SUPPORTED_SERVICES_TYPE;
|
||||
currLogLevel?: string;
|
||||
vaultRandomID?: string;
|
||||
autoRunEveryMilliseconds?: number;
|
||||
}
|
||||
|
||||
export interface RemoteItem {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { base64, base64url } from "rfc4648";
|
||||
import { reverseString } from "./misc";
|
||||
|
||||
import type { RemotelySavePluginSettings } from "./baseTypes";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
const DEFAULT_README: string =
|
||||
"Do NOT modify this manually. It's generated automatically.";
|
||||
|
||||
interface MessyConfigType {
|
||||
readme: string;
|
||||
d: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* this should accept the result after loadData();
|
||||
*/
|
||||
export const messyConfigToNormal = (
|
||||
x: MessyConfigType | RemotelySavePluginSettings | null | undefined
|
||||
): RemotelySavePluginSettings | null | undefined => {
|
||||
log.debug("loading, original config on disk:");
|
||||
log.debug(x);
|
||||
if (x === null || x === undefined) {
|
||||
log.debug("the messy config is null or undefined, skip");
|
||||
return x as any;
|
||||
}
|
||||
if ("readme" in x && "d" in x) {
|
||||
// we should decode
|
||||
const y = JSON.parse(
|
||||
(
|
||||
base64url.parse(reverseString(x["d"]), {
|
||||
out: Buffer.allocUnsafe as any,
|
||||
loose: true,
|
||||
}) as Buffer
|
||||
).toString("utf-8")
|
||||
);
|
||||
log.debug("loading, parsed config is:");
|
||||
log.debug(y);
|
||||
return y;
|
||||
} else {
|
||||
// return as is
|
||||
log.debug("loading, parsed config is the same");
|
||||
return x;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* this should accept the result of original config
|
||||
*/
|
||||
export const normalConfigToMessy = (
|
||||
x: RemotelySavePluginSettings | null | undefined
|
||||
) => {
|
||||
if (x === null || x === undefined) {
|
||||
log.debug("the normal config is null or undefined, skip");
|
||||
return x;
|
||||
}
|
||||
const y = {
|
||||
readme: DEFAULT_README,
|
||||
d: reverseString(
|
||||
base64url.stringify(Buffer.from(JSON.stringify(x), "utf-8"), {
|
||||
pad: false,
|
||||
})
|
||||
),
|
||||
};
|
||||
log.debug("encoding, encoded config is:");
|
||||
log.debug(y);
|
||||
return y;
|
||||
};
|
||||
+12
-5
@@ -1,17 +1,24 @@
|
||||
import { TAbstractFile, TFolder, TFile, Vault } from "obsidian";
|
||||
|
||||
import type { SyncPlanType } from "./sync";
|
||||
import { readAllSyncPlanRecordTexts } from "./localdb";
|
||||
import { readAllSyncPlanRecordTextsByVault } from "./localdb";
|
||||
import type { InternalDBs } from "./localdb";
|
||||
import { mkdirpInVault } from "./misc";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
const DEFAULT_DEBUG_FOLDER = "_debug_remotely_save/";
|
||||
const DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX = "sync_plans_hist_exported_on_";
|
||||
|
||||
export const exportSyncPlansToFiles = async (db: InternalDBs, vault: Vault) => {
|
||||
console.log("exporting");
|
||||
export const exportVaultSyncPlansToFiles = async (
|
||||
db: InternalDBs,
|
||||
vault: Vault,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
log.info("exporting");
|
||||
await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault);
|
||||
const records = await readAllSyncPlanRecordTexts(db);
|
||||
const records = await readAllSyncPlanRecordTextsByVault(db, vaultRandomID);
|
||||
let md = "";
|
||||
if (records.length === 0) {
|
||||
md = "No sync plans history found";
|
||||
@@ -25,5 +32,5 @@ export const exportSyncPlansToFiles = async (db: InternalDBs, vault: Vault) => {
|
||||
await vault.create(filePath, md, {
|
||||
mtime: ts,
|
||||
});
|
||||
console.log("finish exporting");
|
||||
log.info("finish exporting");
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { base32, base64url } from "rfc4648";
|
||||
import { bufferToArrayBuffer, hexStringToTypedArray } from "./misc";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
const DEFAULT_ITER = 20000;
|
||||
|
||||
// base32.stringify(Buffer.from('Salted__'))
|
||||
|
||||
+5
-1
@@ -7,6 +7,9 @@ import {
|
||||
RemotelySavePluginSettings,
|
||||
} from "./baseTypes";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
export const exportQrCodeUri = async (
|
||||
settings: RemotelySavePluginSettings,
|
||||
currentVaultName: string,
|
||||
@@ -15,11 +18,12 @@ export const exportQrCodeUri = async (
|
||||
const settings2 = cloneDeep(settings);
|
||||
delete settings2.dropbox;
|
||||
delete settings2.onedrive;
|
||||
delete settings2.vaultRandomID;
|
||||
const data = encodeURIComponent(JSON.stringify(settings2));
|
||||
const vault = encodeURIComponent(currentVaultName);
|
||||
const version = encodeURIComponent(pluginVersion);
|
||||
const rawUri = `obsidian://${COMMAND_URI}?func=settings&version=${version}&vault=${vault}&data=${data}`;
|
||||
// console.log(uri)
|
||||
// log.info(uri)
|
||||
const imgUri = await QRCode.toDataURL(rawUri);
|
||||
return {
|
||||
rawUri,
|
||||
|
||||
+169
-38
@@ -6,7 +6,11 @@ import type { SyncPlanType } from "./sync";
|
||||
|
||||
export type LocalForage = typeof localforage;
|
||||
|
||||
export const DEFAULT_DB_VERSION_NUMBER: number = 20211114;
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
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_TBL_VERSION = "schemaversion";
|
||||
export const DEFAULT_TBL_DELETE_HISTORY = "filefolderoperationhistory";
|
||||
@@ -22,6 +26,7 @@ export interface FileFolderHistoryRecord {
|
||||
actionType: "delete" | "rename";
|
||||
keyType: "folder" | "file";
|
||||
renameTo: string;
|
||||
vaultRandomID: string;
|
||||
}
|
||||
|
||||
interface SyncMetaMappingRecord {
|
||||
@@ -34,12 +39,14 @@ interface SyncMetaMappingRecord {
|
||||
remoteExtraKey: string;
|
||||
remoteType: SUPPORTED_SERVICES_TYPE;
|
||||
keyType: "folder" | "file";
|
||||
vaultRandomID: string;
|
||||
}
|
||||
|
||||
interface SyncPlanRecord {
|
||||
ts: number;
|
||||
remoteType: string;
|
||||
syncPlan: string;
|
||||
vaultRandomID: string;
|
||||
}
|
||||
|
||||
export interface InternalDBs {
|
||||
@@ -49,7 +56,103 @@ export interface InternalDBs {
|
||||
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 = {
|
||||
versionTbl: localforage.createInstance({
|
||||
name: DEFAULT_DB_NAME,
|
||||
@@ -71,14 +174,25 @@ export const prepareDBs = async () => {
|
||||
|
||||
const originalVersion = (await db.versionTbl.getItem("version")) as number;
|
||||
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);
|
||||
} else if (originalVersion === DEFAULT_DB_VERSION_NUMBER) {
|
||||
// do nothing
|
||||
} 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
|
||||
);
|
||||
}
|
||||
|
||||
console.log("db connected");
|
||||
log.info("db connected");
|
||||
return db;
|
||||
};
|
||||
|
||||
@@ -86,10 +200,10 @@ export const destroyDBs = async () => {
|
||||
// await localforage.dropInstance({
|
||||
// name: DEFAULT_DB_NAME,
|
||||
// });
|
||||
// console.log("db deleted");
|
||||
// log.info("db deleted");
|
||||
const req = indexedDB.deleteDatabase(DEFAULT_DB_NAME);
|
||||
req.onsuccess = (event) => {
|
||||
console.log("db deleted");
|
||||
log.info("db deleted");
|
||||
};
|
||||
req.onblocked = (event) => {
|
||||
console.warn("trying to delete db but it was blocked");
|
||||
@@ -100,35 +214,34 @@ export const destroyDBs = async () => {
|
||||
};
|
||||
};
|
||||
|
||||
const migrateDBs = async (db: InternalDBs, oldVer: number, newVer: number) => {
|
||||
if (oldVer === newVer) {
|
||||
return;
|
||||
}
|
||||
// not implemented
|
||||
throw Error(`not supported internal db changes from ${oldVer} to ${newVer}`);
|
||||
};
|
||||
|
||||
export const loadDeleteRenameHistoryTable = async (db: InternalDBs) => {
|
||||
export const loadDeleteRenameHistoryTableByVault = async (
|
||||
db: InternalDBs,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
const records = [] as FileFolderHistoryRecord[];
|
||||
await db.deleteHistoryTbl.iterate((value, key, iterationNumber) => {
|
||||
records.push(value as FileFolderHistoryRecord);
|
||||
if (key.startsWith(`${vaultRandomID}\t`)) {
|
||||
records.push(value as FileFolderHistoryRecord);
|
||||
}
|
||||
});
|
||||
records.sort((a, b) => a.actionWhen - b.actionWhen); // ascending
|
||||
return records;
|
||||
};
|
||||
|
||||
export const clearDeleteRenameHistoryOfKey = async (
|
||||
export const clearDeleteRenameHistoryOfKeyAndVault = async (
|
||||
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,
|
||||
fileOrFolder: TAbstractFile
|
||||
fileOrFolder: TAbstractFile,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
// console.log(fileOrFolder);
|
||||
// log.info(fileOrFolder);
|
||||
let k: FileFolderHistoryRecord;
|
||||
if (fileOrFolder instanceof TFile) {
|
||||
k = {
|
||||
@@ -140,6 +253,7 @@ export const insertDeleteRecord = async (
|
||||
actionType: "delete",
|
||||
keyType: "file",
|
||||
renameTo: "",
|
||||
vaultRandomID: vaultRandomID,
|
||||
};
|
||||
} else if (fileOrFolder instanceof TFolder) {
|
||||
// key should endswith "/"
|
||||
@@ -155,17 +269,19 @@ export const insertDeleteRecord = async (
|
||||
actionType: "delete",
|
||||
keyType: "folder",
|
||||
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,
|
||||
fileOrFolder: TAbstractFile,
|
||||
oldPath: string
|
||||
oldPath: string,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
// console.log(fileOrFolder);
|
||||
// log.info(fileOrFolder);
|
||||
let k: FileFolderHistoryRecord;
|
||||
if (fileOrFolder instanceof TFile) {
|
||||
k = {
|
||||
@@ -177,6 +293,7 @@ export const insertRenameRecord = async (
|
||||
actionType: "rename",
|
||||
keyType: "file",
|
||||
renameTo: fileOrFolder.path,
|
||||
vaultRandomID: vaultRandomID,
|
||||
};
|
||||
} else if (fileOrFolder instanceof TFolder) {
|
||||
const key = oldPath.endsWith("/") ? oldPath : `${oldPath}/`;
|
||||
@@ -192,12 +309,13 @@ export const insertRenameRecord = async (
|
||||
actionType: "rename",
|
||||
keyType: "folder",
|
||||
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,
|
||||
db: InternalDBs,
|
||||
localKey: string,
|
||||
@@ -206,7 +324,8 @@ export const upsertSyncMetaMappingData = async (
|
||||
remoteKey: string,
|
||||
remoteMTime: number,
|
||||
remoteSize: number,
|
||||
remoteExtraKey: string /* ETag from s3 */
|
||||
remoteExtraKey: string,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
const aggregratedInfo: SyncMetaMappingRecord = {
|
||||
localKey: localKey,
|
||||
@@ -218,19 +337,24 @@ export const upsertSyncMetaMappingData = async (
|
||||
remoteExtraKey: remoteExtraKey,
|
||||
remoteType: serviceType,
|
||||
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,
|
||||
db: InternalDBs,
|
||||
remoteKey: string,
|
||||
remoteMTime: number,
|
||||
remoteExtraKey: string
|
||||
remoteExtraKey: string,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
const potentialItem = (await db.syncMappingTbl.getItem(
|
||||
remoteKey
|
||||
`${vaultRandomID}\t${remoteKey}`
|
||||
)) as SyncMetaMappingRecord;
|
||||
|
||||
if (potentialItem === null) {
|
||||
@@ -255,26 +379,33 @@ export const clearAllSyncMetaMapping = async (db: InternalDBs) => {
|
||||
await db.syncMappingTbl.clear();
|
||||
};
|
||||
|
||||
export const insertSyncPlanRecord = async (
|
||||
export const insertSyncPlanRecordByVault = async (
|
||||
db: InternalDBs,
|
||||
syncPlan: SyncPlanType
|
||||
syncPlan: SyncPlanType,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
const record = {
|
||||
ts: syncPlan.ts,
|
||||
vaultRandomID: vaultRandomID,
|
||||
remoteType: syncPlan.remoteType,
|
||||
syncPlan: JSON.stringify(syncPlan /* directly stringify */, null, 2),
|
||||
} as SyncPlanRecord;
|
||||
await db.syncPlansTbl.setItem(`${syncPlan.ts}`, record);
|
||||
await db.syncPlansTbl.setItem(`${vaultRandomID}\t${syncPlan.ts}`, record);
|
||||
};
|
||||
|
||||
export const clearAllSyncPlanRecords = async (db: InternalDBs) => {
|
||||
await db.syncPlansTbl.clear();
|
||||
};
|
||||
|
||||
export const readAllSyncPlanRecordTexts = async (db: InternalDBs) => {
|
||||
export const readAllSyncPlanRecordTextsByVault = async (
|
||||
db: InternalDBs,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
const records = [] as SyncPlanRecord[];
|
||||
await db.syncPlansTbl.iterate((value, key, iterationNumber) => {
|
||||
records.push(value as SyncPlanRecord);
|
||||
if (key.startsWith(`${vaultRandomID}\t`)) {
|
||||
records.push(value as SyncPlanRecord);
|
||||
}
|
||||
});
|
||||
records.sort((a, b) => -(a.ts - b.ts)); // descending
|
||||
|
||||
|
||||
+242
-103
@@ -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 { nanoid } from "nanoid";
|
||||
import feather from "feather-icons";
|
||||
import type { RemotelySavePluginSettings } from "./baseTypes";
|
||||
import {
|
||||
COMMAND_CALLBACK,
|
||||
@@ -10,10 +12,10 @@ import {
|
||||
import { importQrCodeUri } from "./importExport";
|
||||
import type { InternalDBs } from "./localdb";
|
||||
import {
|
||||
insertDeleteRecord,
|
||||
insertRenameRecord,
|
||||
insertSyncPlanRecord,
|
||||
loadDeleteRenameHistoryTable,
|
||||
insertDeleteRecordByVault,
|
||||
insertRenameRecordByVault,
|
||||
insertSyncPlanRecordByVault,
|
||||
loadDeleteRenameHistoryTableByVault,
|
||||
prepareDBs,
|
||||
} from "./localdb";
|
||||
import { RemoteClient } from "./remote";
|
||||
@@ -34,6 +36,10 @@ import { DEFAULT_WEBDAV_CONFIG } from "./remoteForWebdav";
|
||||
import { RemotelySaveSettingTab } from "./settings";
|
||||
import type { SyncStatusType } from "./sync";
|
||||
import { doActualSync, getSyncPlan, isPasswordOk } from "./sync";
|
||||
import { messyConfigToNormal, normalConfigToMessy } from "./configPersist";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
||||
s3: DEFAULT_S3_CONFIG,
|
||||
@@ -42,6 +48,9 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
||||
onedrive: DEFAULT_ONEDRIVE_CONFIG,
|
||||
password: "",
|
||||
serviceType: "s3",
|
||||
currLogLevel: "info",
|
||||
vaultRandomID: "",
|
||||
autoRunEveryMilliseconds: -1,
|
||||
};
|
||||
|
||||
interface OAuth2Info {
|
||||
@@ -52,15 +61,182 @@ interface OAuth2Info {
|
||||
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 {
|
||||
settings: RemotelySavePluginSettings;
|
||||
// cm: CodeMirror.Editor;
|
||||
db: InternalDBs;
|
||||
syncStatus: SyncStatusType;
|
||||
oauth2Info: OAuth2Info;
|
||||
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() {
|
||||
console.log(`loading plugin ${this.manifest.id}`);
|
||||
log.info(`loading plugin ${this.manifest.id}`);
|
||||
|
||||
addIcon(iconNameSyncWait, iconSvgSyncWait);
|
||||
addIcon(iconNameSyncRunning, iconSvgSyncRunning);
|
||||
|
||||
this.oauth2Info = {
|
||||
verifier: "",
|
||||
@@ -70,8 +246,16 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
revokeAuthSetting: undefined,
|
||||
}; // init
|
||||
|
||||
this.currSyncMsg = "";
|
||||
|
||||
await this.loadSettings();
|
||||
|
||||
if (this.settings.currLogLevel !== undefined) {
|
||||
log.setLevel(this.settings.currLogLevel as any);
|
||||
}
|
||||
|
||||
await this.checkIfOauthExpires();
|
||||
await this.checkIfVaultIDAssigned(); // MUST before prepareDB()
|
||||
|
||||
await this.prepareDB();
|
||||
|
||||
@@ -79,13 +263,22 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
|
||||
this.registerEvent(
|
||||
this.app.vault.on("delete", async (fileOrFolder) => {
|
||||
await insertDeleteRecord(this.db, fileOrFolder);
|
||||
await insertDeleteRecordByVault(
|
||||
this.db,
|
||||
fileOrFolder,
|
||||
this.settings.vaultRandomID
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
this.registerEvent(
|
||||
this.app.vault.on("rename", async (fileOrFolder, oldPath) => {
|
||||
await insertRenameRecord(this.db, fileOrFolder, oldPath);
|
||||
await insertRenameRecordByVault(
|
||||
this.db,
|
||||
fileOrFolder,
|
||||
oldPath,
|
||||
this.settings.vaultRandomID
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -266,107 +459,33 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
}
|
||||
);
|
||||
|
||||
this.addRibbonIcon("switch", "Remotely Save", async () => {
|
||||
if (this.syncStatus !== "idle") {
|
||||
new Notice(
|
||||
`Remotely Save already running in stage ${this.syncStatus}!`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
//console.log(`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();
|
||||
// console.log(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);
|
||||
// console.log(local);
|
||||
// console.log(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
|
||||
);
|
||||
console.log(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.syncStatus = "finish";
|
||||
this.syncStatus = "idle";
|
||||
} catch (error) {
|
||||
const msg = `Remotely Save error while ${this.syncStatus}`;
|
||||
console.log(msg);
|
||||
console.log(error);
|
||||
new Notice(msg);
|
||||
new Notice(error.message);
|
||||
this.syncStatus = "idle";
|
||||
}
|
||||
});
|
||||
this.syncRibbon = this.addRibbonIcon(
|
||||
iconNameSyncWait,
|
||||
`${this.manifest.name}`,
|
||||
async () => this.syncRun("manual")
|
||||
);
|
||||
|
||||
this.addSettingTab(new RemotelySaveSettingTab(this.app, this));
|
||||
|
||||
// this.registerDomEvent(document, "click", (evt: MouseEvent) => {
|
||||
// console.log("click", evt);
|
||||
// log.info("click", evt);
|
||||
// });
|
||||
|
||||
// this.registerInterval(
|
||||
// window.setInterval(() => console.log("setInterval"), 5 * 60 * 1000)
|
||||
// );
|
||||
if (
|
||||
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() {
|
||||
console.log(`unloading plugin ${this.manifest.id}`);
|
||||
log.info(`unloading plugin ${this.manifest.id}`);
|
||||
this.destroyDBs();
|
||||
}
|
||||
|
||||
@@ -374,7 +493,7 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.settings = Object.assign(
|
||||
{},
|
||||
cloneDeep(DEFAULT_SETTINGS),
|
||||
await this.loadData()
|
||||
messyConfigToNormal(await this.loadData())
|
||||
);
|
||||
if (this.settings.dropbox.clientID === "") {
|
||||
this.settings.dropbox.clientID = DEFAULT_SETTINGS.dropbox.clientID;
|
||||
@@ -388,7 +507,7 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
await this.saveData(normalConfigToMessy(this.settings));
|
||||
}
|
||||
|
||||
async checkIfOauthExpires() {
|
||||
@@ -461,11 +580,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() {
|
||||
this.db = await prepareDBs();
|
||||
this.db = await prepareDBs(this.settings.vaultRandomID);
|
||||
}
|
||||
|
||||
destroyDBs() {
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
|
||||
+17
-5
@@ -4,6 +4,9 @@ import * as path from "path";
|
||||
import { base32, base64url } from "rfc4648";
|
||||
import XRegExp from "xregexp";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
/**
|
||||
* If any part of the file starts with '.' or '_' then it's a hidden file.
|
||||
* @param item
|
||||
@@ -13,7 +16,7 @@ import XRegExp from "xregexp";
|
||||
export const isHiddenPath = (item: string, loose: boolean = true) => {
|
||||
const k = path.posix.normalize(item); // TODO: only unix path now
|
||||
const k2 = k.split("/"); // TODO: only unix path now
|
||||
// console.log(k2)
|
||||
// log.info(k2)
|
||||
for (const singlePart of k2) {
|
||||
if (singlePart === "." || singlePart === ".." || singlePart === "") {
|
||||
continue;
|
||||
@@ -54,14 +57,14 @@ export const getFolderLevels = (x: string) => {
|
||||
};
|
||||
|
||||
export const mkdirpInVault = async (thePath: string, vault: Vault) => {
|
||||
// console.log(thePath);
|
||||
// log.info(thePath);
|
||||
const foldersToBuild = getFolderLevels(thePath);
|
||||
// console.log(foldersToBuild);
|
||||
// log.info(foldersToBuild);
|
||||
for (const folder of foldersToBuild) {
|
||||
const r = await vault.adapter.exists(folder);
|
||||
// console.log(r);
|
||||
// log.info(r);
|
||||
if (!r) {
|
||||
console.log(`mkdir ${folder}`);
|
||||
log.info(`mkdir ${folder}`);
|
||||
await vault.adapter.mkdir(folder);
|
||||
}
|
||||
}
|
||||
@@ -196,3 +199,12 @@ export const getRandomArrayBuffer = (byteLength: number) => {
|
||||
const k = window.crypto.getRandomValues(new Uint8Array(byteLength));
|
||||
return bufferToArrayBuffer(k);
|
||||
};
|
||||
|
||||
/**
|
||||
* https://stackoverflow.com/questions/958908
|
||||
* @param x
|
||||
* @returns
|
||||
*/
|
||||
export const reverseString = (x: string) => {
|
||||
return [...x].reverse().join("");
|
||||
};
|
||||
|
||||
@@ -11,6 +11,9 @@ import * as onedrive from "./remoteForOnedrive";
|
||||
import * as s3 from "./remoteForS3";
|
||||
import * as webdav from "./remoteForWebdav";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
export class RemoteClient {
|
||||
readonly serviceType: SUPPORTED_SERVICES_TYPE;
|
||||
readonly s3Client?: s3.S3Client;
|
||||
|
||||
+13
-10
@@ -12,6 +12,9 @@ import { bufferToArrayBuffer, getFolderLevels, mkdirpInVault } from "./misc";
|
||||
|
||||
export { Dropbox } from "dropbox";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
export const DEFAULT_DROPBOX_CONFIG: DropboxConfig = {
|
||||
accessToken: "",
|
||||
clientID: process.env.DEFAULT_DROPBOX_APP_KEY,
|
||||
@@ -208,7 +211,7 @@ export const sendRefreshTokenReq = async (
|
||||
appKey: string,
|
||||
refreshToken: string
|
||||
) => {
|
||||
console.log("start auto getting refreshed Dropbox access token.");
|
||||
log.info("start auto getting refreshed Dropbox access token.");
|
||||
const resp1 = await fetch("https://api.dropboxapi.com/oauth2/token", {
|
||||
method: "POST",
|
||||
body: new URLSearchParams({
|
||||
@@ -218,7 +221,7 @@ export const sendRefreshTokenReq = async (
|
||||
}),
|
||||
});
|
||||
const resp2 = (await resp1.json()) as DropboxSuccessAuthRes;
|
||||
console.log("finish auto getting refreshed Dropbox access token.");
|
||||
log.info("finish auto getting refreshed Dropbox access token.");
|
||||
return resp2;
|
||||
};
|
||||
|
||||
@@ -227,7 +230,7 @@ export const setConfigBySuccessfullAuthInplace = async (
|
||||
authRes: DropboxSuccessAuthRes,
|
||||
saveUpdatedConfigFunc: () => Promise<any> | undefined
|
||||
) => {
|
||||
console.log("start updating local info of Dropbox token");
|
||||
log.info("start updating local info of Dropbox token");
|
||||
|
||||
config.accessToken = authRes.access_token;
|
||||
config.accessTokenExpiresInSeconds = parseInt(authRes.expires_in);
|
||||
@@ -249,7 +252,7 @@ export const setConfigBySuccessfullAuthInplace = async (
|
||||
await saveUpdatedConfigFunc();
|
||||
}
|
||||
|
||||
console.log("finish updating local info of Dropbox token");
|
||||
log.info("finish updating local info of Dropbox token");
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -308,9 +311,9 @@ export class WrappedDropboxClient {
|
||||
}
|
||||
|
||||
// check vault folder
|
||||
// console.log(`checking remote has folder /${this.vaultName}`);
|
||||
// log.info(`checking remote has folder /${this.vaultName}`);
|
||||
if (this.vaultFolderExists) {
|
||||
// console.log(`already checked, /${this.vaultName} exist before`)
|
||||
// log.info(`already checked, /${this.vaultName} exist before`)
|
||||
} else {
|
||||
const res = await this.dropbox.filesListFolder({
|
||||
path: "",
|
||||
@@ -323,14 +326,14 @@ export class WrappedDropboxClient {
|
||||
}
|
||||
}
|
||||
if (!this.vaultFolderExists) {
|
||||
console.log(`remote does not have folder /${this.vaultName}`);
|
||||
log.info(`remote does not have folder /${this.vaultName}`);
|
||||
await this.dropbox.filesCreateFolderV2({
|
||||
path: `/${this.vaultName}`,
|
||||
});
|
||||
console.log(`remote folder /${this.vaultName} created`);
|
||||
log.info(`remote folder /${this.vaultName} created`);
|
||||
this.vaultFolderExists = true;
|
||||
} else {
|
||||
// console.log(`remote folder /${this.vaultName} exists`);
|
||||
// log.info(`remote folder /${this.vaultName} exists`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,7 +492,7 @@ export const listFromRemote = async (
|
||||
if (res.status !== 200) {
|
||||
throw Error(JSON.stringify(res));
|
||||
}
|
||||
// console.log(res);
|
||||
// log.info(res);
|
||||
|
||||
const contents = res.result.entries;
|
||||
const unifiedContents = contents
|
||||
|
||||
+54
-31
@@ -28,6 +28,9 @@ import {
|
||||
mkdirpInVault,
|
||||
} from "./misc";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
const SCOPES = ["User.Read", "Files.ReadWrite.AppFolder", "offline_access"];
|
||||
const REDIRECT_URI = `obsidian://${COMMAND_CALLBACK_ONEDRIVE}`;
|
||||
|
||||
@@ -117,8 +120,8 @@ export const sendAuthReq = async (
|
||||
// code: authCode,
|
||||
// codeVerifier: verifier, // PKCE Code Verifier
|
||||
// });
|
||||
// console.log('authResponse')
|
||||
// console.log(authResponse)
|
||||
// log.info('authResponse')
|
||||
// log.info(authResponse)
|
||||
// return authResponse;
|
||||
|
||||
// Because of the CORS problem,
|
||||
@@ -142,7 +145,7 @@ export const sendAuthReq = async (
|
||||
});
|
||||
|
||||
const rsp2 = JSON.parse(rsp1);
|
||||
// console.log(rsp2);
|
||||
// log.info(rsp2);
|
||||
|
||||
if (rsp2.error !== undefined) {
|
||||
return rsp2 as AccessCodeResponseFailedType;
|
||||
@@ -171,7 +174,7 @@ export const sendRefreshTokenReq = async (
|
||||
});
|
||||
|
||||
const rsp2 = JSON.parse(rsp1);
|
||||
// console.log(rsp2);
|
||||
// log.info(rsp2);
|
||||
|
||||
if (rsp2.error !== undefined) {
|
||||
return rsp2 as AccessCodeResponseFailedType;
|
||||
@@ -185,7 +188,7 @@ export const setConfigBySuccessfullAuthInplace = async (
|
||||
authRes: AccessCodeResponseSuccessfulType,
|
||||
saveUpdatedConfigFunc: () => Promise<any> | undefined
|
||||
) => {
|
||||
console.log("start updating local info of OneDrive token");
|
||||
log.info("start updating local info of OneDrive token");
|
||||
config.accessToken = authRes.access_token;
|
||||
config.accessTokenExpiresAtTime =
|
||||
Date.now() + authRes.expires_in - 5 * 60 * 1000;
|
||||
@@ -200,7 +203,7 @@ export const setConfigBySuccessfullAuthInplace = async (
|
||||
await saveUpdatedConfigFunc();
|
||||
}
|
||||
|
||||
console.log("finish updating local info of Onedrive token");
|
||||
log.info("finish updating local info of Onedrive token");
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -245,19 +248,33 @@ const getNormPath = (fileOrFolderPath: string, vaultName: string) => {
|
||||
return fileOrFolderPath.slice(`${prefix}/`.length);
|
||||
};
|
||||
|
||||
const constructFromDriveItemToRemoteItemError = (x: DriveItem) => {
|
||||
return `parentPath="${x.parentReference.path}", selfName="${x.name}"`;
|
||||
};
|
||||
|
||||
const fromDriveItemToRemoteItem = (
|
||||
x: DriveItem,
|
||||
vaultName: string
|
||||
): RemoteItem => {
|
||||
let key = "";
|
||||
|
||||
const COMMON_PREFIX = `/drive/root:/Apps/remotely-save/${vaultName}`;
|
||||
const COMMON_PREFIX_OTHERS = `/drive/items/`;
|
||||
if (`${x.parentReference.path}/${x.name}`.startsWith(COMMON_PREFIX)) {
|
||||
key = `${x.parentReference.path}/${x.name}`.substring(
|
||||
COMMON_PREFIX.length + 1
|
||||
);
|
||||
} else if (x.parentReference.path.startsWith(COMMON_PREFIX_OTHERS)) {
|
||||
// possible prefix:
|
||||
// pure english: /drive/root:/Apps/remotely-save/${vaultName}
|
||||
// or localized, e.g.: /drive/root:/应用/remotely-save/${vaultName}
|
||||
const FIRST_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/remotely-save\//g;
|
||||
|
||||
// another possibile prefix
|
||||
const SECOND_COMMON_PREFIX_RAW = `/drive/items/`;
|
||||
|
||||
const fullPathOriginal = `${x.parentReference.path}/${x.name}`;
|
||||
const matchFirstPrefixRes = fullPathOriginal.match(FIRST_COMMON_PREFIX_REGEX);
|
||||
if (
|
||||
matchFirstPrefixRes !== null &&
|
||||
fullPathOriginal.startsWith(`${matchFirstPrefixRes[0]}${vaultName}`)
|
||||
) {
|
||||
const foundPrefix = `${matchFirstPrefixRes[0]}${vaultName}`;
|
||||
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||
} else if (x.parentReference.path.startsWith(SECOND_COMMON_PREFIX_RAW)) {
|
||||
// it's something like
|
||||
// /drive/items/<some_id>!<another_id>:/${vaultName}/<subfolder>
|
||||
// with uri encoded!
|
||||
@@ -270,14 +287,14 @@ const fromDriveItemToRemoteItem = (
|
||||
key = x.name;
|
||||
} else {
|
||||
throw Error(
|
||||
`we meet file/folder and do not know how to deal with it:\n${JSON.stringify(
|
||||
`we meet file/folder and do not know how to deal with it:\n${constructFromDriveItemToRemoteItemError(
|
||||
x
|
||||
)}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw Error(
|
||||
`we meet file/folder and do not know how to deal with it:\n${JSON.stringify(
|
||||
`we meet file/folder and do not know how to deal with it:\n${constructFromDriveItemToRemoteItemError(
|
||||
x
|
||||
)}`
|
||||
);
|
||||
@@ -338,7 +355,7 @@ class MyAuthProvider implements AuthenticationProvider {
|
||||
this.onedriveConfig.accessTokenExpiresAtTime =
|
||||
currentTs + r2.expires_in * 1000 - 60 * 2 * 1000;
|
||||
await this.saveUpdatedConfigFunc();
|
||||
console.log("Onedrive accessToken updated");
|
||||
log.info("Onedrive accessToken updated");
|
||||
return this.onedriveConfig.accessToken;
|
||||
}
|
||||
};
|
||||
@@ -374,26 +391,26 @@ export class WrappedOnedriveClient {
|
||||
}
|
||||
|
||||
// check vault folder
|
||||
// console.log(`checking remote has folder /${this.vaultName}`);
|
||||
// log.info(`checking remote has folder /${this.vaultName}`);
|
||||
if (this.vaultFolderExists) {
|
||||
// console.log(`already checked, /${this.vaultName} exist before`)
|
||||
// log.info(`already checked, /${this.vaultName} exist before`)
|
||||
} else {
|
||||
const k = await this.client.api("/drive/special/approot/children").get();
|
||||
// console.log(k);
|
||||
// log.info(k);
|
||||
this.vaultFolderExists =
|
||||
(k.value as DriveItem[]).filter((x) => x.name === this.vaultName)
|
||||
.length > 0;
|
||||
if (!this.vaultFolderExists) {
|
||||
console.log(`remote does not have folder /${this.vaultName}`);
|
||||
log.info(`remote does not have folder /${this.vaultName}`);
|
||||
await this.client.api("/drive/special/approot/children").post({
|
||||
name: `${this.vaultName}`,
|
||||
folder: {},
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
});
|
||||
console.log(`remote folder /${this.vaultName} created`);
|
||||
log.info(`remote folder /${this.vaultName} created`);
|
||||
this.vaultFolderExists = true;
|
||||
} else {
|
||||
// console.log(`remote folder /${this.vaultName} exists`);
|
||||
// log.info(`remote folder /${this.vaultName} exists`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -431,7 +448,7 @@ export const listFromRemote = async (
|
||||
let res = await client.client
|
||||
.api(`/drive/special/approot:/${client.vaultName}:/delta`)
|
||||
.get();
|
||||
const driveItems = res.value as DriveItem[];
|
||||
let driveItems = res.value as DriveItem[];
|
||||
|
||||
while (NEXT_LINK_KEY in res) {
|
||||
res = await client.client.api(res[NEXT_LINK_KEY]).get();
|
||||
@@ -444,6 +461,12 @@ export const listFromRemote = async (
|
||||
await client.saveUpdatedConfigFunc();
|
||||
}
|
||||
|
||||
driveItems = driveItems.map((x) => {
|
||||
const y = cloneDeep(x);
|
||||
y.parentReference.path = y.parentReference.path.replace("/Apps", "/应用");
|
||||
return y;
|
||||
});
|
||||
|
||||
// unify everything to RemoteItem
|
||||
const unifiedContents = driveItems
|
||||
.map((x) => fromDriveItemToRemoteItem(x, client.vaultName))
|
||||
@@ -459,15 +482,15 @@ export const getRemoteMeta = async (
|
||||
) => {
|
||||
await client.init();
|
||||
const remotePath = getOnedrivePath(fileOrFolderPath, client.vaultName);
|
||||
// console.log(`remotePath=${remotePath}`);
|
||||
// log.info(`remotePath=${remotePath}`);
|
||||
const rsp = await client.client
|
||||
.api(remotePath)
|
||||
.select("cTag,eTag,fileSystemInfo,folder,file,name,parentReference,size")
|
||||
.get();
|
||||
// console.log(rsp);
|
||||
// log.info(rsp);
|
||||
const driveItem = rsp as DriveItem;
|
||||
const res = fromDriveItemToRemoteItem(driveItem, client.vaultName);
|
||||
// console.log(res);
|
||||
// log.info(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
@@ -487,7 +510,7 @@ export const uploadToRemote = async (
|
||||
uploadFile = remoteEncryptedKey;
|
||||
}
|
||||
uploadFile = getOnedrivePath(uploadFile, client.vaultName);
|
||||
// console.log(`uploadFile=${uploadFile}`);
|
||||
// log.info(`uploadFile=${uploadFile}`);
|
||||
|
||||
const isFolder = fileOrFolderPath.endsWith("/");
|
||||
|
||||
@@ -547,7 +570,7 @@ export const uploadToRemote = async (
|
||||
uploadEventHandlers: {
|
||||
progress: (range?: Range) => {
|
||||
// Handle progress event
|
||||
// console.log(
|
||||
// log.info(
|
||||
// `uploading ${range.minValue}-${range.maxValue} of ${fileOrFolderPath}`
|
||||
// );
|
||||
},
|
||||
@@ -555,7 +578,7 @@ export const uploadToRemote = async (
|
||||
} as LargeFileUploadTaskOptions
|
||||
);
|
||||
const uploadResult: UploadResult = await task.upload();
|
||||
// console.log(uploadResult)
|
||||
// log.info(uploadResult)
|
||||
const res = await getRemoteMeta(client, uploadFile);
|
||||
return res;
|
||||
}
|
||||
@@ -574,7 +597,7 @@ export const uploadToRemote = async (
|
||||
// so use LargeFileUploadTask instead of OneDriveLargeFileUploadTask
|
||||
const progress = (range?: Range) => {
|
||||
// Handle progress event
|
||||
// console.log(
|
||||
// log.info(
|
||||
// `uploading ${range.minValue}-${range.maxValue} of ${fileOrFolderPath}`
|
||||
// );
|
||||
};
|
||||
@@ -611,7 +634,7 @@ export const uploadToRemote = async (
|
||||
options
|
||||
);
|
||||
const uploadResult: UploadResult = await task.upload();
|
||||
// console.log(uploadResult)
|
||||
// log.info(uploadResult)
|
||||
const res = await getRemoteMeta(client, uploadFile);
|
||||
return res;
|
||||
}
|
||||
|
||||
+4
-1
@@ -25,6 +25,9 @@ import {
|
||||
|
||||
export { S3Client } from "@aws-sdk/client-s3";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
export const DEFAULT_S3_CONFIG = {
|
||||
s3Endpoint: "",
|
||||
s3Region: "",
|
||||
@@ -150,7 +153,7 @@ export const uploadToRemote = async (
|
||||
},
|
||||
});
|
||||
upload.on("httpUploadProgress", (progress) => {
|
||||
// console.log(progress);
|
||||
// log.info(progress);
|
||||
});
|
||||
await upload.done();
|
||||
|
||||
|
||||
+12
-9
@@ -7,6 +7,9 @@ import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||
import { bufferToArrayBuffer, getPathFolder, mkdirpInVault } from "./misc";
|
||||
export type { WebDAVClient } from "webdav/web";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
export const DEFAULT_WEBDAV_CONFIG = {
|
||||
address: "",
|
||||
username: "",
|
||||
@@ -82,7 +85,7 @@ export class WrappedWebdavClient {
|
||||
: AuthType.Password,
|
||||
});
|
||||
} else {
|
||||
console.log("no password");
|
||||
log.info("no password");
|
||||
this.client = createClient(this.webdavConfig.address);
|
||||
}
|
||||
}
|
||||
@@ -93,12 +96,12 @@ export class WrappedWebdavClient {
|
||||
} else {
|
||||
const res = await this.client.exists(`/${this.vaultName}`);
|
||||
if (res) {
|
||||
// console.log("remote vault folder exits!");
|
||||
// log.info("remote vault folder exits!");
|
||||
this.vaultFolderExists = true;
|
||||
} else {
|
||||
console.log("remote vault folder not exists, creating");
|
||||
log.info("remote vault folder not exists, creating");
|
||||
await this.client.createDirectory(`/${this.vaultName}`);
|
||||
console.log("remote vault folder created!");
|
||||
log.info("remote vault folder created!");
|
||||
this.vaultFolderExists = true;
|
||||
}
|
||||
}
|
||||
@@ -118,7 +121,7 @@ export const getRemoteMeta = async (
|
||||
) => {
|
||||
await client.init();
|
||||
const remotePath = getWebdavPath(fileOrFolderPath, client.vaultName);
|
||||
// console.log(`remotePath = ${remotePath}`);
|
||||
// log.info(`remotePath = ${remotePath}`);
|
||||
const res = (await client.client.stat(remotePath, {
|
||||
details: false,
|
||||
})) as FileStat;
|
||||
@@ -158,7 +161,7 @@ export const uploadToRemote = async (
|
||||
await client.client.putFileContents(uploadFile, "", {
|
||||
overwrite: true,
|
||||
onUploadProgress: (progress) => {
|
||||
// console.log(`Uploaded ${progress.loaded} bytes of ${progress.total}`);
|
||||
// log.info(`Uploaded ${progress.loaded} bytes of ${progress.total}`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -180,7 +183,7 @@ export const uploadToRemote = async (
|
||||
await client.client.putFileContents(uploadFile, remoteContent, {
|
||||
overwrite: true,
|
||||
onUploadProgress: (progress) => {
|
||||
console.log(`Uploaded ${progress.loaded} bytes of ${progress.total}`);
|
||||
log.info(`Uploaded ${progress.loaded} bytes of ${progress.total}`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -282,10 +285,10 @@ export const deleteFromRemote = async (
|
||||
await client.init();
|
||||
try {
|
||||
await client.client.deleteFile(remoteFileName);
|
||||
// console.log(`delete ${remoteFileName} succeeded`);
|
||||
// log.info(`delete ${remoteFileName} succeeded`);
|
||||
} catch (err) {
|
||||
console.error("some error while deleting");
|
||||
console.log(err);
|
||||
log.info(err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+131
-8
@@ -1,6 +1,6 @@
|
||||
import { App, Modal, Notice, PluginSettingTab, Setting } from "obsidian";
|
||||
import type { SUPPORTED_SERVICES_TYPE, WebdavAuthType } from "./baseTypes";
|
||||
import { exportSyncPlansToFiles } from "./debugMode";
|
||||
import { exportVaultSyncPlansToFiles } from "./debugMode";
|
||||
import { exportQrCodeUri } from "./importExport";
|
||||
import {
|
||||
clearAllSyncMetaMapping,
|
||||
@@ -19,6 +19,10 @@ import {
|
||||
DEFAULT_ONEDRIVE_CONFIG,
|
||||
getAuthUrlAndVerifier as getAuthUrlAndVerifierOnedrive,
|
||||
} from "./remoteForOnedrive";
|
||||
import { messyConfigToNormal } from "./configPersist";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
class PasswordModal extends Modal {
|
||||
plugin: RemotelySavePlugin;
|
||||
@@ -117,6 +121,21 @@ class DropboxAuthModal extends Modal {
|
||||
contentEl.createEl("p", {
|
||||
text: "Finally you should be redirected to Obsidian.",
|
||||
});
|
||||
|
||||
const div2 = contentEl.createDiv();
|
||||
div2.createEl(
|
||||
"button",
|
||||
{
|
||||
text: "Click to copy the auth url",
|
||||
},
|
||||
(el) => {
|
||||
el.onclick = async () => {
|
||||
await navigator.clipboard.writeText(authUrl);
|
||||
new Notice("the auth url copied to clipboard!");
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
contentEl.createEl("p").createEl("a", {
|
||||
href: authUrl,
|
||||
text: authUrl,
|
||||
@@ -157,12 +176,31 @@ export class OnedriveAuthModal extends Modal {
|
||||
);
|
||||
this.plugin.oauth2Info.verifier = verifier;
|
||||
|
||||
contentEl.createEl("p", {
|
||||
text: "Currently only OneDrive for personal is supported. OneDrive for Business is NOT supported (yet).",
|
||||
});
|
||||
|
||||
contentEl.createEl("p", {
|
||||
text: "Visit the address in a browser, and follow the steps.",
|
||||
});
|
||||
contentEl.createEl("p", {
|
||||
text: "Finally you should be redirected to Obsidian.",
|
||||
});
|
||||
|
||||
const div2 = contentEl.createDiv();
|
||||
div2.createEl(
|
||||
"button",
|
||||
{
|
||||
text: "Click to copy the auth url",
|
||||
},
|
||||
(el) => {
|
||||
el.onclick = async () => {
|
||||
await navigator.clipboard.writeText(authUrl);
|
||||
new Notice("the auth url copied to clipboard!");
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
contentEl.createEl("p").createEl("a", {
|
||||
href: authUrl,
|
||||
text: authUrl,
|
||||
@@ -344,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)
|
||||
//////////////////////////////////////////////////
|
||||
@@ -365,7 +443,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
});
|
||||
|
||||
s3Div.createEl("p", {
|
||||
text: "Disclaimer: The information is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your bucket, please immediately delete the access key on your AWS (or other S3-service provider) settings.",
|
||||
text: "Disclaimer: The information is stored in locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your bucket, please immediately delete the access key on your AWS (or other S3-service provider) settings.",
|
||||
cls: "s3-disclaimer",
|
||||
});
|
||||
|
||||
@@ -491,7 +569,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
cls: "dropbox-disclaimer",
|
||||
});
|
||||
dropboxDiv.createEl("p", {
|
||||
text: "Disclaimer: The information is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your Dropbox, please immediately disconnect this app on https://www.dropbox.com/account/connected_apps .",
|
||||
text: "Disclaimer: The information is stored in locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your Dropbox, please immediately disconnect this app on https://www.dropbox.com/account/connected_apps .",
|
||||
cls: "dropbox-disclaimer",
|
||||
});
|
||||
dropboxDiv.createEl("p", {
|
||||
@@ -614,13 +692,13 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
"onedrive-hide",
|
||||
this.plugin.settings.serviceType !== "onedrive"
|
||||
);
|
||||
onedriveDiv.createEl("h2", { text: "Remote For Onedrive" });
|
||||
onedriveDiv.createEl("h2", { text: "Remote For Onedrive (for personal)" });
|
||||
onedriveDiv.createEl("p", {
|
||||
text: "Disclaimer: This app is NOT an official Microsoft / Onedrive product.",
|
||||
cls: "onedrive-disclaimer",
|
||||
});
|
||||
onedriveDiv.createEl("p", {
|
||||
text: "Disclaimer: The information is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your Onedrive, please immediately disconnect this app on https://microsoft.com/consent .",
|
||||
text: "Disclaimer: The information is stored locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your Onedrive, please immediately disconnect this app on https://microsoft.com/consent .",
|
||||
cls: "onedrive-disclaimer",
|
||||
});
|
||||
onedriveDiv.createEl("p", {
|
||||
@@ -629,6 +707,10 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
}/${this.app.vault.getName()} on your Onedrive.`,
|
||||
});
|
||||
|
||||
onedriveDiv.createEl("p", {
|
||||
text: "Currently only OneDrive for personal is supported. OneDrive for Business is NOT supported (yet).",
|
||||
});
|
||||
|
||||
const onedriveSelectAuthDiv = onedriveDiv.createDiv();
|
||||
const onedriveAuthDiv = onedriveSelectAuthDiv.createDiv({
|
||||
cls: "onedrive-auth-button-hide",
|
||||
@@ -724,7 +806,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
webdavDiv.createEl("h2", { text: "Remote For Webdav" });
|
||||
|
||||
webdavDiv.createEl("p", {
|
||||
text: "Disclaimer: The information is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins may read the info. If you see any unintentional access to your webdav server, please immediately change the username and password.",
|
||||
text: "Disclaimer: The information is stored in locally. Other malicious/harmful/faulty plugins may read the info. If you see any unintentional access to your webdav server, please immediately change the username and password.",
|
||||
cls: "webdav-disclaimer",
|
||||
});
|
||||
|
||||
@@ -829,7 +911,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
dropdown.addOption("s3", "S3 or compatible");
|
||||
dropdown.addOption("dropbox", "Dropbox");
|
||||
dropdown.addOption("webdav", "Webdav (beta)");
|
||||
dropdown.addOption("onedrive", "OneDrive (alpha)");
|
||||
dropdown.addOption("onedrive", "OneDrive for personal (alpha)");
|
||||
dropdown
|
||||
.setValue(this.plugin.settings.serviceType)
|
||||
.onChange(async (val: SUPPORTED_SERVICES_TYPE) => {
|
||||
@@ -886,6 +968,43 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
|
||||
const debugDiv = containerEl.createEl("div");
|
||||
debugDiv.createEl("h2", { text: "Debug" });
|
||||
|
||||
const setConsoleLogLevelDiv = debugDiv.createDiv("div");
|
||||
new Setting(setConsoleLogLevelDiv)
|
||||
.setName("alter console log level")
|
||||
.setDesc(
|
||||
'By default the log level is "info". You can change to "debug" to get verbose infomation in console.'
|
||||
)
|
||||
.addDropdown(async (dropdown) => {
|
||||
dropdown.addOption("info", "info");
|
||||
dropdown.addOption("debug", "debug");
|
||||
dropdown
|
||||
.setValue(this.plugin.settings.currLogLevel)
|
||||
.onChange(async (val: string) => {
|
||||
this.plugin.settings.currLogLevel = val;
|
||||
log.setLevel(val as any);
|
||||
await this.plugin.saveSettings();
|
||||
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");
|
||||
new Setting(syncPlanDiv)
|
||||
.setName("export sync plans")
|
||||
@@ -895,7 +1014,11 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText("Export");
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
+37
-17
@@ -9,13 +9,16 @@ import {
|
||||
} from "./encrypt";
|
||||
import type { FileFolderHistoryRecord, InternalDBs } from "./localdb";
|
||||
import {
|
||||
clearDeleteRenameHistoryOfKey,
|
||||
getSyncMetaMappingByRemoteKey,
|
||||
upsertSyncMetaMappingData,
|
||||
clearDeleteRenameHistoryOfKeyAndVault,
|
||||
getSyncMetaMappingByRemoteKeyAndVault,
|
||||
upsertSyncMetaMappingDataByVault,
|
||||
} from "./localdb";
|
||||
import { isHiddenPath, isVaildText, mkdirpInVault } from "./misc";
|
||||
import { RemoteClient } from "./remote";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
export type SyncStatusType =
|
||||
| "idle"
|
||||
| "preparing"
|
||||
@@ -167,6 +170,7 @@ const ensembleMixedStates = async (
|
||||
local: TAbstractFile[],
|
||||
deleteHistory: FileFolderHistoryRecord[],
|
||||
db: InternalDBs,
|
||||
vaultRandomID: string,
|
||||
remoteType: SUPPORTED_SERVICES_TYPE,
|
||||
password: string = ""
|
||||
) => {
|
||||
@@ -187,12 +191,13 @@ const ensembleMixedStates = async (
|
||||
throw Error(`unexpected key=${remoteEncryptedKey}`);
|
||||
}
|
||||
}
|
||||
const backwardMapping = await getSyncMetaMappingByRemoteKey(
|
||||
const backwardMapping = await getSyncMetaMappingByRemoteKeyAndVault(
|
||||
remoteType,
|
||||
db,
|
||||
key,
|
||||
entry.lastModified,
|
||||
entry.etag
|
||||
entry.etag,
|
||||
vaultRandomID
|
||||
);
|
||||
|
||||
let r = {} as FileOrFolderMixedState;
|
||||
@@ -440,6 +445,7 @@ export const getSyncPlan = async (
|
||||
local: TAbstractFile[],
|
||||
deleteHistory: FileFolderHistoryRecord[],
|
||||
db: InternalDBs,
|
||||
vaultRandomID: string,
|
||||
remoteType: SUPPORTED_SERVICES_TYPE,
|
||||
password: string = ""
|
||||
) => {
|
||||
@@ -448,6 +454,7 @@ export const getSyncPlan = async (
|
||||
local,
|
||||
deleteHistory,
|
||||
db,
|
||||
vaultRandomID,
|
||||
remoteType,
|
||||
password
|
||||
);
|
||||
@@ -464,6 +471,7 @@ export const getSyncPlan = async (
|
||||
|
||||
const dispatchOperationToActual = async (
|
||||
key: string,
|
||||
vaultRandomID: string,
|
||||
state: FileOrFolderMixedState,
|
||||
client: RemoteClient,
|
||||
db: InternalDBs,
|
||||
@@ -498,7 +506,7 @@ const dispatchOperationToActual = async (
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
await clearDeleteRenameHistoryOfKey(db, state.key);
|
||||
await clearDeleteRenameHistoryOfKeyAndVault(db, state.key, vaultRandomID);
|
||||
} else if (state.decision === "upload_clearhist") {
|
||||
const remoteObjMeta = await client.uploadToRemote(
|
||||
state.key,
|
||||
@@ -508,7 +516,7 @@ const dispatchOperationToActual = async (
|
||||
remoteEncryptedKey,
|
||||
foldersCreatedBefore
|
||||
);
|
||||
await upsertSyncMetaMappingData(
|
||||
await upsertSyncMetaMappingDataByVault(
|
||||
client.serviceType,
|
||||
db,
|
||||
state.key,
|
||||
@@ -517,9 +525,10 @@ const dispatchOperationToActual = async (
|
||||
state.key,
|
||||
remoteObjMeta.lastModified,
|
||||
remoteObjMeta.size,
|
||||
remoteObjMeta.etag
|
||||
remoteObjMeta.etag,
|
||||
vaultRandomID
|
||||
);
|
||||
await clearDeleteRenameHistoryOfKey(db, state.key);
|
||||
await clearDeleteRenameHistoryOfKeyAndVault(db, state.key, vaultRandomID);
|
||||
} else if (state.decision === "download") {
|
||||
await mkdirpInVault(state.key, vault);
|
||||
await client.downloadFromRemote(
|
||||
@@ -531,7 +540,7 @@ const dispatchOperationToActual = async (
|
||||
);
|
||||
} else if (state.decision === "delremote_clearhist") {
|
||||
await client.deleteFromRemote(state.key, password, remoteEncryptedKey);
|
||||
await clearDeleteRenameHistoryOfKey(db, state.key);
|
||||
await clearDeleteRenameHistoryOfKeyAndVault(db, state.key, vaultRandomID);
|
||||
} else if (state.decision === "upload") {
|
||||
const remoteObjMeta = await client.uploadToRemote(
|
||||
state.key,
|
||||
@@ -541,7 +550,7 @@ const dispatchOperationToActual = async (
|
||||
remoteEncryptedKey,
|
||||
foldersCreatedBefore
|
||||
);
|
||||
await upsertSyncMetaMappingData(
|
||||
await upsertSyncMetaMappingDataByVault(
|
||||
client.serviceType,
|
||||
db,
|
||||
state.key,
|
||||
@@ -550,10 +559,11 @@ const dispatchOperationToActual = async (
|
||||
state.key,
|
||||
remoteObjMeta.lastModified,
|
||||
remoteObjMeta.size,
|
||||
remoteObjMeta.etag
|
||||
remoteObjMeta.etag,
|
||||
vaultRandomID
|
||||
);
|
||||
} else if (state.decision === "clearhist") {
|
||||
await clearDeleteRenameHistoryOfKey(db, state.key);
|
||||
await clearDeleteRenameHistoryOfKeyAndVault(db, state.key, vaultRandomID);
|
||||
} else {
|
||||
throw Error("this should never happen!");
|
||||
}
|
||||
@@ -562,27 +572,37 @@ const dispatchOperationToActual = async (
|
||||
export const doActualSync = async (
|
||||
client: RemoteClient,
|
||||
db: InternalDBs,
|
||||
vaultRandomID: string,
|
||||
vault: Vault,
|
||||
syncPlan: SyncPlanType,
|
||||
password: string = ""
|
||||
password: string = "",
|
||||
callbackSyncProcess?: any
|
||||
) => {
|
||||
const keyStates = syncPlan.mixedStates;
|
||||
const foldersCreatedBefore = new Set<string>();
|
||||
let i = 0;
|
||||
const totalCount = Object.keys(keyStates).length || 0;
|
||||
for (const [k, v] of Object.entries(keyStates).sort(
|
||||
([k1, v1], [k2, v2]) => k2.length - k1.length
|
||||
)) {
|
||||
i += 1;
|
||||
const k2 = k as string;
|
||||
const v2 = v as FileOrFolderMixedState;
|
||||
log.debug(`start syncing "${k2}" with plan ${JSON.stringify(v2)}`);
|
||||
if (callbackSyncProcess !== undefined) {
|
||||
await callbackSyncProcess(i, totalCount, k2, v2.decision);
|
||||
}
|
||||
await dispatchOperationToActual(
|
||||
k as string,
|
||||
v as FileOrFolderMixedState,
|
||||
k2,
|
||||
vaultRandomID,
|
||||
v2,
|
||||
client,
|
||||
db,
|
||||
vault,
|
||||
password,
|
||||
foldersCreatedBefore
|
||||
);
|
||||
// console.log(`finished ${k}, with ${setToString(foldersCreatedBefore)}`);
|
||||
log.info(`finished ${k2}`);
|
||||
}
|
||||
// await Promise.all(
|
||||
// Object.entries(keyStates)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as chai from "chai";
|
||||
import chaiAsPromised from "chai-as-promised";
|
||||
|
||||
import { RemotelySavePluginSettings } from "../src/baseTypes";
|
||||
import { messyConfigToNormal, normalConfigToMessy } from "../src/configPersist";
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
const expect = chai.expect;
|
||||
|
||||
const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
||||
s3: {
|
||||
s3AccessKeyID: "acc",
|
||||
} as any,
|
||||
webdav: {
|
||||
address: "addr",
|
||||
} as any,
|
||||
dropbox: {
|
||||
username: "测试中文",
|
||||
} as any,
|
||||
onedrive: {
|
||||
username: "test 🍎 emoji",
|
||||
} as any,
|
||||
password: "password",
|
||||
serviceType: "s3",
|
||||
currLogLevel: "info",
|
||||
};
|
||||
|
||||
describe("Config Persist tests", () => {
|
||||
it("should encrypt go back and forth conrrectly", async () => {
|
||||
const k = DEFAULT_SETTINGS;
|
||||
const k2 = normalConfigToMessy(k);
|
||||
const k3 = messyConfigToNormal(k2);
|
||||
expect(k3).to.deep.equal(k);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"0.2.6": "0.12.15"
|
||||
"0.2.11": "0.12.15"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user