Compare commits

...
26 Commits
Author SHA1 Message Date
fyears b13bd8708b bump to 0.3.14 2022-03-27 00:55:12 +08:00
fyears 83c3aac317 some logic to move away vaultRandomID from data.json 2022-03-27 00:52:10 +08:00
fyears 712979fb0d reset depth settings of webdav 2022-03-26 22:58:57 +08:00
fyears e460c563af skip extra checking folders for webdav 2022-03-26 22:41:10 +08:00
fyears 580397fbf9 update text 2022-03-26 15:11:41 +08:00
fyears 537fae1a2b add tracking for local movement 2022-03-26 15:06:34 +08:00
fyears dbc9e02ff3 update bug report tmpl 2022-03-26 11:24:20 +08:00
fyears 6474faad51 fix memory. Squashed commit of the following:
commit 1c24e48f2528238aeac417866ccec9ee3deced5f
Author: fyears <1142836+fyears@users.noreply.github.com>
Date:   Fri Mar 25 02:08:05 2022 +0800

    just some trying
2022-03-26 09:45:48 +08:00
fyears a5e415aeaf Merge branch 'master' of https://github.com/fyears/remotely-save 2022-03-26 00:34:13 +08:00
fyears 3e75e31696 change wording in config file 2022-03-26 00:34:00 +08:00
fyears 9c8cc1c8b8 update esbuild 2022-03-25 23:41:29 +08:00
fyears cccc4969cd Update browser_env.md 2022-03-23 23:38:48 +08:00
fyears 1ba1591c75 Update README.md 2022-03-23 23:37:49 +08:00
fyears 1581cb3822 bump to 0.3.13 2022-03-23 22:57:16 +08:00
fyears 99926adbbd more hints for webdav 2022-03-23 22:56:42 +08:00
fyears 4edd8caaea bump to 0.3.12 2022-03-23 10:02:35 +08:00
fyears 005b698862 fix folder 2022-03-23 10:02:02 +08:00
fyears e76a5e4c12 add version desc 2022-03-23 10:01:08 +08:00
fyears ea3dcc6533 bump to 0.3.11 2022-03-22 00:56:29 +08:00
fyears c8bd10a855 fix typo 2022-03-22 00:55:44 +08:00
fyears 996074e431 bump to 0.3.10 2022-03-22 00:36:09 +08:00
fyears 275224cf73 full outout for error file 2022-03-22 00:35:18 +08:00
fyears 575a27db26 err msg stays longer 2022-03-22 00:31:17 +08:00
fyears 76127dd131 fix auto depth 2022-03-21 23:34:18 +08:00
fyears e2e6f6a12c a little help for webdav addr 2022-03-21 23:26:09 +08:00
fyears 4f3e8803da more verbose reason for conn fail 2022-03-21 23:04:56 +08:00
21 changed files with 580 additions and 214 deletions
+9 -1
View File
@@ -1,5 +1,5 @@
name: Bug Report name: Bug Report
description: File a bug report description: Fire a bug report
title: "[Bug]: " title: "[Bug]: "
labels: ["bug"] labels: ["bug"]
body: body:
@@ -55,6 +55,14 @@ body:
description: What version of Obsidian are you running? description: What version of Obsidian are you running?
validations: validations:
required: false required: false
- type: checkboxes
id: using-password
attributes:
label: Using password or not
description: Are you using password (end-to-end encryption) or not?
options:
- label: Yes.
required: false
- type: checkboxes - type: checkboxes
id: ensure-no-sensitive-information id: ensure-no-sensitive-information
attributes: attributes:
+4 -4
View File
@@ -61,8 +61,8 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
- Prepare your S3 (-compatible) service information: [endpoint, region](https://docs.aws.amazon.com/general/latest/gr/s3.html), [access key id, secret access key](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-your-credentials.html), bucket name. The bucket should be empty and solely for syncing a vault. - Prepare your S3 (-compatible) service information: [endpoint, region](https://docs.aws.amazon.com/general/latest/gr/s3.html), [access key id, secret access key](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-your-credentials.html), bucket name. The bucket should be empty and solely for syncing a vault.
- About CORS: - About CORS:
- If you are using Obsidian >= 0.13.25, you can skip this CORS part. - If you are using Obsidian desktop >= 0.13.25 or mobile >= 1.1.1, you can skip this CORS part.
- If you are using Obsidian < 0.13.25, you need to configure (enable) [CORS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html) for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`, and add at least `ETag` into exposed headers. Full example is [here](./docs/s3_cors_configure.md). It's unfortunately required, because the plugin sends requests from a browser-like envirement. And those addresses are tested and found on desktop and ios and android. - If you are using Obsidian desktop < 0.13.25 or mobile < 1.1.1, you need to configure (enable) [CORS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html) for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`, and add at least `ETag` into exposed headers. Full example is [here](./docs/s3_cors_configure.md). It's unfortunately required, because the plugin sends requests from a browser-like envirement. And those addresses are tested and found on desktop and ios and android.
- 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.
@@ -87,8 +87,8 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
### webdav ### webdav
- About CORS: - About CORS:
- If you are using Obsidian >= 0.13.25, you can skip this CORS part. - If you are using Obsidian desktop >= 0.13.25 or iOS >= 1.1.1, you can skip this CORS part.
- If you are using Obsidian < 0.13.25: - If you are using Obsidian desktop < 0.13.25 or iOS < 1.1.1 or any Android version:
- The webdav server has to be enabled CORS for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`, **AND** all webdav HTTP methods, **AND** all webdav headers. These are required, because Obsidian mobile works like a browser and mobile plugins are limited by CORS policies unless under a upgraded Obsidian version. - The webdav server has to be enabled CORS for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`, **AND** all webdav HTTP methods, **AND** all webdav headers. These are required, because Obsidian mobile works like a browser and mobile plugins are limited by CORS policies unless under a upgraded Obsidian version.
- Popular software NextCloud, OwnCloud, `rclone serve webdav` do **NOT** enable CORS by default. If you are using any of them, you should evaluate the risk, and find a way to enable CORS, before using this plugin, or use a upgraded Obsidian version. - Popular software NextCloud, OwnCloud, `rclone serve webdav` do **NOT** enable CORS by default. If you are using any of them, you should evaluate the risk, and find a way to enable CORS, before using this plugin, or use a upgraded Obsidian version.
- The plugin is tested successfully under python package [`wsgidav` (version 4.0)](https://github.com/mar10/wsgidav). See [this issue](https://github.com/mar10/wsgidav/issues/239) for some details. - The plugin is tested successfully under python package [`wsgidav` (version 4.0)](https://github.com/mar10/wsgidav). See [this issue](https://github.com/mar10/wsgidav/issues/239) for some details.
+1 -1
View File
@@ -6,7 +6,7 @@ Technically, the plugin (or any plugin?) runs in the js environment provided by
Then some limitations are applied: Then some limitations are applied:
1. [The CORS issue (solved in the new Obsidian version).](./browser_env_cors.md) 1. [The CORS issue (solved in the new Obsidian version on some platforms).](./browser_env_cors.md)
2. [No Node.js environment.](./browser_env_no_nodejs.md) 2. [No Node.js environment.](./browser_env_no_nodejs.md)
3. If the cloud service uses OAuth flow, it needs to support PKCE. More details are [here](./browser_env_oauth2_pkce.md). 3. If the cloud service uses OAuth flow, it needs to support PKCE. More details are [here](./browser_env_oauth2_pkce.md).
4. [No background running after Obsidian is closes.](./browser_env_no_background_after_closing.md) 4. [No background running after Obsidian is closes.](./browser_env_no_background_after_closing.md)
+2 -2
View File
@@ -4,6 +4,6 @@ The plugin is developed for the browser environment. The "fake" browser behind t
[MDN has a doc about CORS.](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) [MDN has a doc about CORS.](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
From Obsidian version >= insider 0.13.25, Obsidian [provides a new API `requiestUrl`](https://forum.obsidian.md/t/obsidian-release-v0-13-25-insider-build/32701), that allows the plugin to fully bypass the CORS issue. As of Mar 2022, the latest public-released Obsidian desktop has supported this API, but the Obsidian mobile still stays in insider. From Obsidian desktop >= 0.13.25 or mobile >= 1.1.1, Obsidian [provides a new API `requiestUrl`](https://forum.obsidian.md/t/obsidian-release-v0-13-25-insider-build/32701), that allows the plugin to fully bypass the CORS issue. As of Mar 2022, the latest public-released Obsidian desktop has supported this API, but the Obsidian mobile still stays in insider.
For using this plugin in Obsidian version < 0.13.25, we need to configure the server side to return the header `Access-Control-Allow-Origin` allowing the origins `app://obsidian.md` and `capacitor://localhost` and `http://localhost`. Here is an example [configuration for Amazon S3](./s3_cors_configure.md). For using this plugin in Obsidian desktop < 0.13.25 or mobile < 1.1.1, we need to configure the server side to return the header `Access-Control-Allow-Origin` allowing the origins `app://obsidian.md` and `capacitor://localhost` and `http://localhost`. Here is an example [configuration for Amazon S3](./s3_cors_configure.md).
+1 -1
View File
@@ -1,6 +1,6 @@
# How To Configure S3 CORS Rules # How To Configure S3 CORS Rules
If you are using the latest version of this plugin AND Obsidian >= 0.13.25, you do not need to configure it any more. If you are using Obsidian < 0.13.25, you are required to configure the rules as following. If you are using the latest version of this plugin AND Obsidian desktop >= 0.13.25, mobile >= 1.1.1, you do not need to configure it any more. If you are using Obsidian desktop < 0.13.25, moble < 1.1.1, you are required to configure the rules as following.
Thanks to [@NAL100 in the Discussion](https://github.com/fyears/remotely-save/discussions/28). Thanks to [@NAL100 in the Discussion](https://github.com/fyears/remotely-save/discussions/28).
+35 -35
View File
@@ -2,41 +2,41 @@
Here is an overview of the connectability ("connectable" or "not connectable" or "in the plan" or "never") to some services by this plugin. Here is an overview of the connectability ("connectable" or "not connectable" or "in the plan" or "never") to some services by this plugin.
The plugin works under the browser environment in Obsidian, so CORS is an issue. Obsidian starts to provide a rich API `requestUrl` for version >= 0.13.25 to bypass the CORS issue. But if the users are still using an older version of Obsidian, they need to configure CORS on server. The plugin works under the browser environment in Obsidian, so CORS is an issue. Obsidian starts to provide a rich API `requestUrl` for desktop version >= 0.13.25, mobile >= 1.1.1 to bypass the CORS issue. But if the users are still using an older version of Obsidian, they need to configure CORS on server.
The list is for information purposes only. The list is for information purposes only.
| Service | Connectable | by S3 | by WebDAV | by other protocol | can bypass CORS issue in Obsidian>=0.13.25 | need CORS config in Obsidian<0.13.25 | | Service | Connectable | by S3 | by WebDAV | by other protocol | can bypass CORS issue in latest Obsidian | need CORS config in old Obsidian |
| ------------------------------------------------------------------------------------- | ----------- | ----- | --------- | ---------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------ | | ------------------------------------------------------------------------------------- | ----------- | ----- | --------- | ---------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------ |
| Amazon S3 | Yes | Yes | | | Yes | [CORS config needed.](./s3_cors_configure.md) | | Amazon S3 | Yes | Yes | | | Yes | [CORS config needed.](./s3_cors_configure.md) |
| Tencent Cloud - Cloud Object Storage (COS) 腾讯云对象存储 | Yes | Yes | | | Yes | CORS config needed. | | Tencent Cloud - Cloud Object Storage (COS) 腾讯云对象存储 | Yes | Yes | | | Yes | CORS config needed. |
| Alibaba Cloud - Object Storage Service 阿里云对象存储 | Yes | Yes | | | Yes | CORS config needed. | | Alibaba Cloud - Object Storage Service 阿里云对象存储 | Yes | Yes | | | Yes | CORS config needed. |
| Backblaze B2 Cloud Storage | Yes | Yes | | | Yes | Its CORS rules doesn't allow no-http(s) origins. | | Backblaze B2 Cloud Storage | Yes | Yes | | | Yes | Its CORS rules doesn't allow no-http(s) origins. |
| [Wasabi](https://wasabi.com) | ? | ? | | | | | | [Wasabi](https://wasabi.com) | ? | ? | | | | |
| [filebase](https://filebase.com/) | Yes | Yes | | | Yes | CORS config needed. | | [filebase](https://filebase.com/) | Yes | Yes | | | Yes | CORS config needed. |
| QingStor 青云 | ? | ? | | | | | | QingStor 青云 | ? | ? | | | | |
| [MinIO](https://min.io/) | ? | ? | | | | | | [MinIO](https://min.io/) | ? | ? | | | | |
| [WsgiDAV](https://github.com/mar10/wsgidav) | Yes | | Yes | | Yes | CORS rules can be set. | | [WsgiDAV](https://github.com/mar10/wsgidav) | Yes | | Yes | | Yes | CORS rules can be set. |
| [Nginx `ngx_http_dav_module`](http://nginx.org/en/docs/http/ngx_http_dav_module.html) | Yes? | | Yes? | | Yes? | ? | | [Nginx `ngx_http_dav_module`](http://nginx.org/en/docs/http/ngx_http_dav_module.html) | Yes? | | Yes? | | Yes? | ? |
| NextCloud | Yes? | | Yes? | | Yes? | No CORS config by default. | | NextCloud | Yes? | | Yes? | | Yes? | No CORS config by default. |
| OwnCloud | Yes? | | Yes? | | Yes? | No CORS config by default. | | OwnCloud | Yes? | | Yes? | | Yes? | No CORS config by default. |
| Seafile | Yes? | | Yes? | | Yes? | | | Seafile | Yes? | | Yes? | | Yes? | |
| `rclone serve webdav` | Yes | | Yes | | Yes | No CORS support. | | `rclone serve webdav` | Yes | | Yes | | Yes | No CORS support. |
| [Nutstore 坚果云](https://www.jianguoyun.com/) | Yes | | Yes | | Yes | No CORS support. | | [Nutstore 坚果云](https://www.jianguoyun.com/) | Yes | | Yes | | Yes | No CORS support. |
| [TeraCLOUD](https://teracloud.jp/en/) | Yes | | Yes | | Yes | No CORS support. | | [TeraCLOUD](https://teracloud.jp/en/) | Yes | | Yes | | Yes | No CORS support. |
| Dropbox | Yes | | | Yes | | | | Dropbox | Yes | | | Yes | | |
| OneDrive for personal | Yes | | | Yes | | | | OneDrive for personal | Yes | | | Yes | | |
| OneDrive for Business | In the plan | | | ? | | | | OneDrive for Business | In the plan | | | ? | | |
| Google Drive | In the plan | | | ? | | | | Google Drive | In the plan | | | ? | | |
| [Box](https://www.box.com/) | ? | | | May be possible but needs further development. | | | | [Box](https://www.box.com/) | ? | | | May be possible but needs further development. | | |
| Google Cloud Storage | ? | | | May be possible but needs further development. | | | | Google Cloud Storage | ? | | | May be possible but needs further development. | | |
| Microsoft Azure Blob Storage | ? | | | May be possible but needs further development. | | | | Microsoft Azure Blob Storage | ? | | | May be possible but needs further development. | | |
| [OpenStack Storage (Swift)](https://github.com/openstack/swift) | ? | | | May be possible but needs further development. | | | | [OpenStack Storage (Swift)](https://github.com/openstack/swift) | ? | | | May be possible but needs further development. | | |
| https://put.io/ | ? | | | ? | | | | https://put.io/ | ? | | | ? | | |
| Yandex Disk | ? | | | ? | | | | Yandex Disk | ? | | | ? | | |
| FTP / FTPS | Never | | | Technically never possible to be implemented. | | | | FTP / FTPS | Never | | | Technically never possible to be implemented. | | |
| SFTP | Never | | | Technically never possible to be implemented. | | | | SFTP | Never | | | Technically never possible to be implemented. | | |
| Jottacloud | No | | | No. It seems that no open api is available. | | | | Jottacloud | No | | | No. It seems that no open api is available. | | |
| Mega | Never | | | No. No js api is available. | | | | Mega | Never | | | No. No js api is available. | | |
| Git | Never | | | No. Technically very hard, if not impossible, to be implemented. | | | | Git | Never | | | No. Technically very hard, if not impossible, to be implemented. | | |
| | | | | | | | | | | | | | | |
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "remotely-save", "id": "remotely-save",
"name": "Remotely Save", "name": "Remotely Save",
"version": "0.3.9", "version": "0.3.14",
"minAppVersion": "0.13.21", "minAppVersion": "0.13.21",
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.", "description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
"author": "fyears", "author": "fyears",
+2 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "remotely-save", "name": "remotely-save",
"version": "0.3.9", "version": "0.3.14",
"description": "This is yet another sync plugin for Obsidian app.", "description": "This is yet another sync plugin for Obsidian app.",
"scripts": { "scripts": {
"dev2": "node esbuild.config.mjs", "dev2": "node esbuild.config.mjs",
@@ -38,7 +38,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.3", "esbuild": "^0.14.27",
"jsdom": "^19.0.0", "jsdom": "^19.0.0",
"mocha": "^9.1.3", "mocha": "^9.1.3",
"prettier": "^2.4.1", "prettier": "^2.4.1",
@@ -65,7 +65,6 @@
"assert": "^2.0.0", "assert": "^2.0.0",
"aws-crt": "^1.10.1", "aws-crt": "^1.10.1",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"codemirror": "^5.63.1",
"crypto-browserify": "^3.12.0", "crypto-browserify": "^3.12.0",
"dropbox": "^10.22.0", "dropbox": "^10.22.0",
"http-status-codes": "^2.2.0", "http-status-codes": "^2.2.0",
+7 -2
View File
@@ -66,7 +66,6 @@ export interface RemotelySavePluginSettings {
password: string; password: string;
serviceType: SUPPORTED_SERVICES_TYPE; serviceType: SUPPORTED_SERVICES_TYPE;
currLogLevel?: string; currLogLevel?: string;
vaultRandomID?: string;
autoRunEveryMilliseconds?: number; autoRunEveryMilliseconds?: number;
initRunAfterMilliseconds?: number; initRunAfterMilliseconds?: number;
agreeToUploadExtraMetadata?: boolean; agreeToUploadExtraMetadata?: boolean;
@@ -74,6 +73,11 @@ export interface RemotelySavePluginSettings {
syncConfigDir?: boolean; syncConfigDir?: boolean;
syncUnderscoreItems?: boolean; syncUnderscoreItems?: boolean;
lang?: LangTypeAndAuto; lang?: LangTypeAndAuto;
/**
* @deprecated
*/
vaultRandomID?: string;
} }
export interface RemoteItem { export interface RemoteItem {
@@ -124,7 +128,8 @@ export interface FileOrFolderMixedState {
deltimeRemote?: number; deltimeRemote?: number;
sizeLocal?: number; sizeLocal?: number;
sizeRemote?: number; sizeRemote?: number;
changeMtimeUsingMapping?: boolean; changeRemoteMtimeUsingMapping?: boolean;
changeLocalMtimeUsingMapping?: boolean;
decision?: DecisionType; decision?: DecisionType;
decisionBranch?: number; decisionBranch?: number;
syncDone?: "done"; syncDone?: "done";
+1 -1
View File
@@ -7,7 +7,7 @@ import * as origLog from "loglevel";
const log = origLog.getLogger("rs-default"); const log = origLog.getLogger("rs-default");
const DEFAULT_README: string = const DEFAULT_README: string =
"Do NOT modify this manually. It's generated automatically."; "The file contains sensitive info, so DO NOT take screenshot of, copy, or share it to anyone! It's also generated automatically, so do not edit it manually.";
interface MessyConfigType { interface MessyConfigType {
readme: string; readme: string;
+163 -33
View File
@@ -1,21 +1,23 @@
import localforage from "localforage"; import localforage from "localforage";
import { TAbstractFile, TFile, TFolder } from "obsidian"; import { requireApiVersion, TAbstractFile, TFile, TFolder } from "obsidian";
import type { SUPPORTED_SERVICES_TYPE } from "./baseTypes"; import { API_VER_STAT_FOLDER, SUPPORTED_SERVICES_TYPE } from "./baseTypes";
import type { SyncPlanType } from "./sync"; import type { SyncPlanType } from "./sync";
export type LocalForage = typeof localforage; export type LocalForage = typeof localforage;
import * as origLog from "loglevel"; import * as origLog from "loglevel";
import { nanoid } from "nanoid";
const log = origLog.getLogger("rs-default"); const log = origLog.getLogger("rs-default");
const DB_VERSION_NUMBER_IN_HISTORY = [20211114, 20220108]; const DB_VERSION_NUMBER_IN_HISTORY = [20211114, 20220108, 20220326];
export const DEFAULT_DB_VERSION_NUMBER: number = 20220108; export const DEFAULT_DB_VERSION_NUMBER: number = 20220326;
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_FILE_HISTORY = "filefolderoperationhistory";
export const DEFAULT_TBL_SYNC_MAPPING = "syncmetadatahistory"; export const DEFAULT_TBL_SYNC_MAPPING = "syncmetadatahistory";
export const DEFAULT_SYNC_PLANS_HISTORY = "syncplanshistory"; export const DEFAULT_SYNC_PLANS_HISTORY = "syncplanshistory";
export const DEFAULT_TBL_VAULT_RANDOM_ID_MAPPING = "vaultrandomidmapping";
export interface FileFolderHistoryRecord { export interface FileFolderHistoryRecord {
key: string; key: string;
@@ -23,7 +25,7 @@ export interface FileFolderHistoryRecord {
mtime: number; mtime: number;
size: number; size: number;
actionWhen: number; actionWhen: number;
actionType: "delete" | "rename"; actionType: "delete" | "rename" | "renameDestination";
keyType: "folder" | "file"; keyType: "folder" | "file";
renameTo: string; renameTo: string;
vaultRandomID: string; vaultRandomID: string;
@@ -51,9 +53,10 @@ interface SyncPlanRecord {
export interface InternalDBs { export interface InternalDBs {
versionTbl: LocalForage; versionTbl: LocalForage;
deleteHistoryTbl: LocalForage; fileHistoryTbl: LocalForage;
syncMappingTbl: LocalForage; syncMappingTbl: LocalForage;
syncPlansTbl: LocalForage; syncPlansTbl: LocalForage;
vaultRandomIDMappingTbl: LocalForage;
} }
/** /**
@@ -72,12 +75,12 @@ const migrateDBsFrom20211114To20220108 = async (
const allPromisesToWait: Promise<any>[] = []; const allPromisesToWait: Promise<any>[] = [];
log.debug("assign vault id to any delete history"); log.debug("assign vault id to any delete history");
const keysInDeleteHistoryTbl = await db.deleteHistoryTbl.keys(); const keysInDeleteHistoryTbl = await db.fileHistoryTbl.keys();
for (const key of keysInDeleteHistoryTbl) { for (const key of keysInDeleteHistoryTbl) {
if (key.startsWith(vaultRandomID)) { if (key.startsWith(vaultRandomID)) {
continue; continue;
} }
const value = (await db.deleteHistoryTbl.getItem( const value = (await db.fileHistoryTbl.getItem(
key key
)) as FileFolderHistoryRecord; )) as FileFolderHistoryRecord;
if (value === null || value === undefined) { if (value === null || value === undefined) {
@@ -87,8 +90,8 @@ const migrateDBsFrom20211114To20220108 = async (
value.vaultRandomID = vaultRandomID; value.vaultRandomID = vaultRandomID;
} }
const newKey = `${vaultRandomID}\t${key}`; const newKey = `${vaultRandomID}\t${key}`;
allPromisesToWait.push(db.deleteHistoryTbl.setItem(newKey, value)); allPromisesToWait.push(db.fileHistoryTbl.setItem(newKey, value));
allPromisesToWait.push(db.deleteHistoryTbl.removeItem(key)); allPromisesToWait.push(db.fileHistoryTbl.removeItem(key));
} }
log.debug("assign vault id to any sync mapping"); log.debug("assign vault id to any sync mapping");
@@ -136,6 +139,23 @@ const migrateDBsFrom20211114To20220108 = async (
log.debug(`finish upgrading internal db from ${oldVer} to ${newVer}`); log.debug(`finish upgrading internal db from ${oldVer} to ${newVer}`);
}; };
/**
* no need to do anything except changing version
* we just add more file operations in db, and no schema is changed.
* @param db
* @param vaultRandomID
*/
const migrateDBsFrom20220108To20220326 = async (
db: InternalDBs,
vaultRandomID: string
) => {
const oldVer = 20220108;
const newVer = 20220326;
log.debug(`start upgrading internal db from ${oldVer} to ${newVer}`);
await db.versionTbl.setItem("version", newVer);
log.debug(`finish upgrading internal db from ${oldVer} to ${newVer}`);
};
const migrateDBs = async ( const migrateDBs = async (
db: InternalDBs, db: InternalDBs,
oldVer: number, oldVer: number,
@@ -148,19 +168,36 @@ const migrateDBs = async (
if (oldVer === 20211114 && newVer === 20220108) { if (oldVer === 20211114 && newVer === 20220108) {
return await migrateDBsFrom20211114To20220108(db, vaultRandomID); return await migrateDBsFrom20211114To20220108(db, vaultRandomID);
} }
if (oldVer === 20220108 && newVer === 20220326) {
return await migrateDBsFrom20220108To20220326(db, vaultRandomID);
}
if (oldVer === 20211114 && newVer === 20220326) {
// TODO: more steps with more versions in the future
await migrateDBsFrom20211114To20220108(db, vaultRandomID);
await migrateDBsFrom20220108To20220326(db, vaultRandomID);
return;
}
if (newVer < oldVer) {
throw Error(
"You've installed a new version, but then downgrade to an old version. Stop working!"
);
}
// not implemented // not implemented
throw Error(`not supported internal db changes from ${oldVer} to ${newVer}`); throw Error(`not supported internal db changes from ${oldVer} to ${newVer}`);
}; };
export const prepareDBs = async (vaultRandomID: string) => { export const prepareDBs = async (
vaultBasePath: string,
vaultRandomIDFromOldConfigFile: string
) => {
const db = { const db = {
versionTbl: localforage.createInstance({ versionTbl: localforage.createInstance({
name: DEFAULT_DB_NAME, name: DEFAULT_DB_NAME,
storeName: DEFAULT_TBL_VERSION, storeName: DEFAULT_TBL_VERSION,
}), }),
deleteHistoryTbl: localforage.createInstance({ fileHistoryTbl: localforage.createInstance({
name: DEFAULT_DB_NAME, name: DEFAULT_DB_NAME,
storeName: DEFAULT_TBL_DELETE_HISTORY, storeName: DEFAULT_TBL_FILE_HISTORY,
}), }),
syncMappingTbl: localforage.createInstance({ syncMappingTbl: localforage.createInstance({
name: DEFAULT_DB_NAME, name: DEFAULT_DB_NAME,
@@ -170,9 +207,42 @@ export const prepareDBs = async (vaultRandomID: string) => {
name: DEFAULT_DB_NAME, name: DEFAULT_DB_NAME,
storeName: DEFAULT_SYNC_PLANS_HISTORY, storeName: DEFAULT_SYNC_PLANS_HISTORY,
}), }),
vaultRandomIDMappingTbl: localforage.createInstance({
name: DEFAULT_DB_NAME,
storeName: DEFAULT_TBL_VAULT_RANDOM_ID_MAPPING,
}),
} as InternalDBs; } as InternalDBs;
const originalVersion = (await db.versionTbl.getItem("version")) as number; // try to get vaultRandomID firstly
let vaultRandomID = "";
const vaultRandomIDInDB: string | null =
await db.vaultRandomIDMappingTbl.getItem(`path2id\t${vaultBasePath}`);
if (vaultRandomIDInDB === null) {
if (vaultRandomIDFromOldConfigFile !== "") {
// reuse the old config id
vaultRandomID = vaultRandomIDFromOldConfigFile;
} else {
// no old config id, we create a random one
vaultRandomID = nanoid();
}
// save the id back
await db.vaultRandomIDMappingTbl.setItem(
`path2id\t${vaultBasePath}`,
vaultRandomID
);
await db.vaultRandomIDMappingTbl.setItem(
`id2path\t${vaultRandomID}`,
vaultBasePath
);
} else {
vaultRandomID = vaultRandomIDInDB;
}
if (vaultRandomID === "") {
throw Error("no vaultRandomID found or generated");
}
const originalVersion: number | null = await db.versionTbl.getItem("version");
if (originalVersion === null) { if (originalVersion === null) {
log.debug( log.debug(
`no internal db version, setting it to ${DEFAULT_DB_VERSION_NUMBER}` `no internal db version, setting it to ${DEFAULT_DB_VERSION_NUMBER}`
@@ -193,7 +263,10 @@ export const prepareDBs = async (vaultRandomID: string) => {
} }
log.info("db connected"); log.info("db connected");
return db; return {
db: db,
vaultRandomID: vaultRandomID,
};
}; };
export const destroyDBs = async () => { export const destroyDBs = async () => {
@@ -206,20 +279,20 @@ export const destroyDBs = async () => {
log.info("db deleted"); log.info("db deleted");
}; };
req.onblocked = (event) => { req.onblocked = (event) => {
console.warn("trying to delete db but it was blocked"); log.warn("trying to delete db but it was blocked");
}; };
req.onerror = (event) => { req.onerror = (event) => {
console.error("tried to delete db but something bad!"); log.error("tried to delete db but something goes wrong!");
console.error(event); log.error(event);
}; };
}; };
export const loadDeleteRenameHistoryTableByVault = async ( export const loadFileHistoryTableByVault = async (
db: InternalDBs, db: InternalDBs,
vaultRandomID: string vaultRandomID: string
) => { ) => {
const records = [] as FileFolderHistoryRecord[]; const records = [] as FileFolderHistoryRecord[];
await db.deleteHistoryTbl.iterate((value, key, iterationNumber) => { await db.fileHistoryTbl.iterate((value, key, iterationNumber) => {
if (key.startsWith(`${vaultRandomID}\t`)) { if (key.startsWith(`${vaultRandomID}\t`)) {
records.push(value as FileFolderHistoryRecord); records.push(value as FileFolderHistoryRecord);
} }
@@ -233,7 +306,16 @@ export const clearDeleteRenameHistoryOfKeyAndVault = async (
key: string, key: string,
vaultRandomID: string vaultRandomID: string
) => { ) => {
await db.deleteHistoryTbl.removeItem(`${vaultRandomID}\t${key}`); const fullKey = `${vaultRandomID}\t${key}`;
const item: FileFolderHistoryRecord | null = await db.fileHistoryTbl.getItem(
fullKey
);
if (
item !== null &&
(item.actionType === "delete" || item.actionType === "rename")
) {
await db.fileHistoryTbl.removeItem(fullKey);
}
}; };
export const insertDeleteRecordByVault = async ( export const insertDeleteRecordByVault = async (
@@ -260,10 +342,12 @@ export const insertDeleteRecordByVault = async (
const key = fileOrFolder.path.endsWith("/") const key = fileOrFolder.path.endsWith("/")
? fileOrFolder.path ? fileOrFolder.path
: `${fileOrFolder.path}/`; : `${fileOrFolder.path}/`;
const ctime = 0; // they are deleted, so no way to get ctime, mtime
const mtime = 0; // they are deleted, so no way to get ctime, mtime
k = { k = {
key: key, key: key,
ctime: 0, ctime: ctime,
mtime: 0, mtime: mtime,
size: 0, size: 0,
actionWhen: Date.now(), actionWhen: Date.now(),
actionType: "delete", actionType: "delete",
@@ -272,9 +356,19 @@ export const insertDeleteRecordByVault = async (
vaultRandomID: vaultRandomID, vaultRandomID: vaultRandomID,
}; };
} }
await db.deleteHistoryTbl.setItem(`${vaultRandomID}\t${k.key}`, k); await db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k.key}`, k);
}; };
/**
* A file/folder is renamed from A to B
* We insert two records:
* A with actionType="rename"
* B with actionType="renameDestination"
* @param db
* @param fileOrFolder
* @param oldPath
* @param vaultRandomID
*/
export const insertRenameRecordByVault = async ( export const insertRenameRecordByVault = async (
db: InternalDBs, db: InternalDBs,
fileOrFolder: TAbstractFile, fileOrFolder: TAbstractFile,
@@ -282,37 +376,73 @@ export const insertRenameRecordByVault = async (
vaultRandomID: string vaultRandomID: string
) => { ) => {
// log.info(fileOrFolder); // log.info(fileOrFolder);
let k: FileFolderHistoryRecord; let k1: FileFolderHistoryRecord;
let k2: FileFolderHistoryRecord;
const actionWhen = Date.now();
if (fileOrFolder instanceof TFile) { if (fileOrFolder instanceof TFile) {
k = { k1 = {
key: oldPath, key: oldPath,
ctime: fileOrFolder.stat.ctime, ctime: fileOrFolder.stat.ctime,
mtime: fileOrFolder.stat.mtime, mtime: fileOrFolder.stat.mtime,
size: fileOrFolder.stat.size, size: fileOrFolder.stat.size,
actionWhen: Date.now(), actionWhen: actionWhen,
actionType: "rename", actionType: "rename",
keyType: "file", keyType: "file",
renameTo: fileOrFolder.path, renameTo: fileOrFolder.path,
vaultRandomID: vaultRandomID, vaultRandomID: vaultRandomID,
}; };
k2 = {
key: fileOrFolder.path,
ctime: fileOrFolder.stat.ctime,
mtime: fileOrFolder.stat.mtime,
size: fileOrFolder.stat.size,
actionWhen: actionWhen,
actionType: "renameDestination",
keyType: "file",
renameTo: "", // itself is the destination, so no need to set this field
vaultRandomID: vaultRandomID,
};
} else if (fileOrFolder instanceof TFolder) { } else if (fileOrFolder instanceof TFolder) {
const key = oldPath.endsWith("/") ? oldPath : `${oldPath}/`; const key = oldPath.endsWith("/") ? oldPath : `${oldPath}/`;
const renameTo = fileOrFolder.path.endsWith("/") const renameTo = fileOrFolder.path.endsWith("/")
? fileOrFolder.path ? fileOrFolder.path
: `${fileOrFolder.path}/`; : `${fileOrFolder.path}/`;
k = { let ctime = 0;
let mtime = 0;
if (requireApiVersion(API_VER_STAT_FOLDER)) {
// TAbstractFile does not contain these info
// but from API_VER_STAT_FOLDER we can manually stat them by path.
const s = await fileOrFolder.vault.adapter.stat(fileOrFolder.path);
ctime = s.ctime;
mtime = s.mtime;
}
k1 = {
key: key, key: key,
ctime: 0, ctime: ctime,
mtime: 0, mtime: mtime,
size: 0, size: 0,
actionWhen: Date.now(), actionWhen: actionWhen,
actionType: "rename", actionType: "rename",
keyType: "folder", keyType: "folder",
renameTo: renameTo, renameTo: renameTo,
vaultRandomID: vaultRandomID, vaultRandomID: vaultRandomID,
}; };
k2 = {
key: renameTo,
ctime: ctime,
mtime: mtime,
size: 0,
actionWhen: actionWhen,
actionType: "renameDestination",
keyType: "folder",
renameTo: "", // itself is the destination, so no need to set this field
vaultRandomID: vaultRandomID,
};
} }
await db.deleteHistoryTbl.setItem(`${vaultRandomID}\t${k.key}`, k); await Promise.all([
db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k1.key}`, k1),
db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k2.key}`, k2),
]);
}; };
export const upsertSyncMetaMappingDataByVault = async ( export const upsertSyncMetaMappingDataByVault = async (
+99 -45
View File
@@ -1,6 +1,13 @@
import { Modal, Notice, Plugin, Setting, addIcon, setIcon } from "obsidian"; import {
Modal,
Notice,
Plugin,
Setting,
addIcon,
setIcon,
FileSystemAdapter,
} from "obsidian";
import cloneDeep from "lodash/cloneDeep"; import cloneDeep from "lodash/cloneDeep";
import { nanoid } from "nanoid";
import { createElement, RotateCcw, RefreshCcw } from "lucide"; import { createElement, RotateCcw, RefreshCcw } from "lucide";
import type { RemotelySavePluginSettings } from "./baseTypes"; import type { RemotelySavePluginSettings } from "./baseTypes";
import { import {
@@ -10,13 +17,13 @@ import {
COMMAND_URI, COMMAND_URI,
} from "./baseTypes"; } from "./baseTypes";
import { importQrCodeUri } from "./importExport"; import { importQrCodeUri } from "./importExport";
import type { InternalDBs } from "./localdb";
import { import {
insertDeleteRecordByVault, insertDeleteRecordByVault,
insertRenameRecordByVault, insertRenameRecordByVault,
insertSyncPlanRecordByVault, insertSyncPlanRecordByVault,
loadDeleteRenameHistoryTableByVault, loadFileHistoryTableByVault,
prepareDBs, prepareDBs,
InternalDBs,
} from "./localdb"; } from "./localdb";
import { RemoteClient } from "./remote"; import { RemoteClient } from "./remote";
import { import {
@@ -54,7 +61,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
password: "", password: "",
serviceType: "s3", serviceType: "s3",
currLogLevel: "info", currLogLevel: "info",
vaultRandomID: "", // vaultRandomID: "", // deprecated
autoRunEveryMilliseconds: -1, autoRunEveryMilliseconds: -1,
initRunAfterMilliseconds: -1, initRunAfterMilliseconds: -1,
agreeToUploadExtraMetadata: false, agreeToUploadExtraMetadata: false,
@@ -77,16 +84,25 @@ type SyncTriggerSourceType = "manual" | "auto" | "dry" | "autoOnceInit";
const iconNameSyncWait = `remotely-save-sync-wait`; const iconNameSyncWait = `remotely-save-sync-wait`;
const iconNameSyncRunning = `remotely-save-sync-running`; const iconNameSyncRunning = `remotely-save-sync-running`;
const iconSvgSyncWait = createElement(RotateCcw); const getIconSvg = () => {
iconSvgSyncWait.setAttribute("width", "100"); const iconSvgSyncWait = createElement(RotateCcw);
iconSvgSyncWait.setAttribute("height", "100"); iconSvgSyncWait.setAttribute("width", "100");
const iconSvgSyncRunning = createElement(RefreshCcw); iconSvgSyncWait.setAttribute("height", "100");
iconSvgSyncRunning.setAttribute("width", "100"); const iconSvgSyncRunning = createElement(RefreshCcw);
iconSvgSyncRunning.setAttribute("height", "100"); iconSvgSyncRunning.setAttribute("width", "100");
iconSvgSyncRunning.setAttribute("height", "100");
const res = {
iconSvgSyncWait: iconSvgSyncWait.outerHTML,
iconSvgSyncRunning: iconSvgSyncRunning.outerHTML,
};
iconSvgSyncWait.empty();
iconSvgSyncRunning.empty();
return res;
};
export default class RemotelySavePlugin extends Plugin { export default class RemotelySavePlugin extends Plugin {
settings: RemotelySavePluginSettings; settings: RemotelySavePluginSettings;
// cm: CodeMirror.Editor;
db: InternalDBs; db: InternalDBs;
syncStatus: SyncStatusType; syncStatus: SyncStatusType;
oauth2Info: OAuth2Info; oauth2Info: OAuth2Info;
@@ -95,17 +111,18 @@ export default class RemotelySavePlugin extends Plugin {
syncRibbon?: HTMLElement; syncRibbon?: HTMLElement;
autoRunIntervalID?: number; autoRunIntervalID?: number;
i18n: I18n; i18n: I18n;
vaultRandomID: string;
async syncRun(triggerSource: SyncTriggerSourceType = "manual") { async syncRun(triggerSource: SyncTriggerSourceType = "manual") {
const t = (x: TransItemType, vars?: any) => { const t = (x: TransItemType, vars?: any) => {
return this.i18n.t(x, vars); return this.i18n.t(x, vars);
}; };
const getNotice = (x: string) => { const getNotice = (x: string, timeout?: number) => {
// only show notices in manual mode // only show notices in manual mode
// no notice in auto mode // no notice in auto mode
if (triggerSource === "manual" || triggerSource === "dry") { if (triggerSource === "manual" || triggerSource === "dry") {
new Notice(x); new Notice(x, timeout);
} }
}; };
if (this.syncStatus !== "idle") { if (this.syncStatus !== "idle") {
@@ -207,7 +224,7 @@ export default class RemotelySavePlugin extends Plugin {
const { remoteStates, metadataFile } = await parseRemoteItems( const { remoteStates, metadataFile } = await parseRemoteItems(
remoteRsp.Contents, remoteRsp.Contents,
this.db, this.db,
this.settings.vaultRandomID, this.vaultRandomID,
client.serviceType, client.serviceType,
this.settings.password this.settings.password
); );
@@ -225,9 +242,9 @@ export default class RemotelySavePlugin extends Plugin {
); );
this.syncStatus = "getting_local_meta"; this.syncStatus = "getting_local_meta";
const local = this.app.vault.getAllLoadedFiles(); const local = this.app.vault.getAllLoadedFiles();
const localHistory = await loadDeleteRenameHistoryTableByVault( const localHistory = await loadFileHistoryTableByVault(
this.db, this.db,
this.settings.vaultRandomID this.vaultRandomID
); );
let localConfigDirContents: ObsConfigDirFileType[] = undefined; let localConfigDirContents: ObsConfigDirFileType[] = undefined;
if (this.settings.syncConfigDir) { if (this.settings.syncConfigDir) {
@@ -261,11 +278,7 @@ export default class RemotelySavePlugin extends Plugin {
); );
log.info(plan.mixedStates); // for debugging log.info(plan.mixedStates); // for debugging
if (triggerSource !== "dry") { if (triggerSource !== "dry") {
await insertSyncPlanRecordByVault( await insertSyncPlanRecordByVault(this.db, plan, this.vaultRandomID);
this.db,
plan,
this.settings.vaultRandomID
);
} }
// The operations above are almost read only and kind of safe. // The operations above are almost read only and kind of safe.
@@ -282,7 +295,7 @@ export default class RemotelySavePlugin extends Plugin {
await doActualSync( await doActualSync(
client, client,
this.db, this.db,
this.settings.vaultRandomID, this.vaultRandomID,
this.app.vault, this.app.vault,
plan, plan,
sortedKeys, sortedKeys,
@@ -331,8 +344,8 @@ export default class RemotelySavePlugin extends Plugin {
}); });
log.info(msg); log.info(msg);
log.info(error); log.info(error);
getNotice(msg); getNotice(msg, 10 * 1000);
getNotice(error.message); getNotice(error.message, 10 * 1000);
this.syncStatus = "idle"; this.syncStatus = "idle";
if (this.syncRibbon !== undefined) { if (this.syncRibbon !== undefined) {
setIcon(this.syncRibbon, iconNameSyncWait); setIcon(this.syncRibbon, iconNameSyncWait);
@@ -344,8 +357,10 @@ export default class RemotelySavePlugin extends Plugin {
async onload() { async onload() {
log.info(`loading plugin ${this.manifest.id}`); log.info(`loading plugin ${this.manifest.id}`);
addIcon(iconNameSyncWait, iconSvgSyncWait.outerHTML); const { iconSvgSyncWait, iconSvgSyncRunning } = getIconSvg();
addIcon(iconNameSyncRunning, iconSvgSyncRunning.outerHTML);
addIcon(iconNameSyncWait, iconSvgSyncWait);
addIcon(iconNameSyncRunning, iconSvgSyncRunning);
this.oauth2Info = { this.oauth2Info = {
verifier: "", verifier: "",
@@ -373,12 +388,27 @@ export default class RemotelySavePlugin extends Plugin {
} }
await this.checkIfOauthExpires(); await this.checkIfOauthExpires();
await this.checkIfVaultIDAssigned(); // MUST before prepareDB()
// MUST before prepareDB()
// And, it's also possible to be an empty string,
// which means the vaultRandomID is read from db later!
const vaultRandomIDFromOldConfigFile =
await this.getVaultRandomIDFromOldConfigFile();
// no need to await this // no need to await this
this.tryToAddIgnoreFile(); this.tryToAddIgnoreFile();
await this.prepareDB(); const vaultBasePath = this.getVaultBasePath();
try {
await this.prepareDBAndVaultRandomID(
vaultBasePath,
vaultRandomIDFromOldConfigFile
);
} catch (err) {
new Notice(err.message, 10 * 1000);
throw err;
}
this.syncStatus = "idle"; this.syncStatus = "idle";
@@ -387,7 +417,7 @@ export default class RemotelySavePlugin extends Plugin {
await insertDeleteRecordByVault( await insertDeleteRecordByVault(
this.db, this.db,
fileOrFolder, fileOrFolder,
this.settings.vaultRandomID this.vaultRandomID
); );
}) })
); );
@@ -398,7 +428,7 @@ export default class RemotelySavePlugin extends Plugin {
this.db, this.db,
fileOrFolder, fileOrFolder,
oldPath, oldPath,
this.settings.vaultRandomID this.vaultRandomID
); );
}) })
); );
@@ -629,9 +659,13 @@ export default class RemotelySavePlugin extends Plugin {
} }
} }
onunload() { async onunload() {
log.info(`unloading plugin ${this.manifest.id}`); log.info(`unloading plugin ${this.manifest.id}`);
this.destroyDBs(); this.syncRibbon = undefined;
if (this.oauth2Info !== undefined) {
this.oauth2Info.helperModal = undefined;
this.oauth2Info = undefined;
}
} }
async loadSettings() { async loadSettings() {
@@ -737,14 +771,20 @@ export default class RemotelySavePlugin extends Plugin {
} }
} }
async checkIfVaultIDAssigned() { async getVaultRandomIDFromOldConfigFile() {
if ( let vaultRandomID = "";
this.settings.vaultRandomID === undefined || if (this.settings.vaultRandomID !== undefined) {
this.settings.vaultRandomID === "" // In old version, the vault id is saved in data.json
) { // But we want to store it in localForage later
this.settings.vaultRandomID = nanoid(); if (this.settings.vaultRandomID !== "") {
// a real string was assigned before
vaultRandomID = this.settings.vaultRandomID;
}
log.debug("vaultRandomID is no longer saved in data.json");
delete this.settings.vaultRandomID;
await this.saveSettings(); await this.saveSettings();
} }
return vaultRandomID;
} }
async trash(x: string) { async trash(x: string) {
@@ -753,8 +793,26 @@ export default class RemotelySavePlugin extends Plugin {
} }
} }
async prepareDB() { getVaultBasePath() {
this.db = await prepareDBs(this.settings.vaultRandomID); if (this.app.vault.adapter instanceof FileSystemAdapter) {
// in desktop
return this.app.vault.adapter.getBasePath().split("?")[0];
} else {
// in mobile
return this.app.vault.adapter.getResourcePath("").split("?")[0];
}
}
async prepareDBAndVaultRandomID(
vaultBasePath: string,
vaultRandomIDFromOldConfigFile: string
) {
const { db, vaultRandomID } = await prepareDBs(
vaultBasePath,
vaultRandomIDFromOldConfigFile
);
this.db = db;
this.vaultRandomID = vaultRandomID;
} }
enableAutoSyncIfSet() { enableAutoSyncIfSet() {
@@ -792,10 +850,6 @@ export default class RemotelySavePlugin extends Plugin {
await this.saveSettings(); await this.saveSettings();
} }
destroyDBs() {
/* destroyDBs(this.db); */
}
async setCurrSyncMsg( async setCurrSyncMsg(
i: number, i: number,
totalCount: number, totalCount: number,
+12 -5
View File
@@ -271,15 +271,22 @@ export class RemoteClient {
} }
}; };
checkConnectivity = async () => { checkConnectivity = async (callbackFunc?: any) => {
if (this.serviceType === "s3") { if (this.serviceType === "s3") {
return await s3.checkConnectivity(this.s3Client, this.s3Config); return await s3.checkConnectivity(
this.s3Client,
this.s3Config,
callbackFunc
);
} else if (this.serviceType === "webdav") { } else if (this.serviceType === "webdav") {
return await webdav.checkConnectivity(this.webdavClient); return await webdav.checkConnectivity(this.webdavClient, callbackFunc);
} else if (this.serviceType === "dropbox") { } else if (this.serviceType === "dropbox") {
return await dropbox.checkConnectivity(this.dropboxClient); return await dropbox.checkConnectivity(this.dropboxClient, callbackFunc);
} else if (this.serviceType === "onedrive") { } else if (this.serviceType === "onedrive") {
return await onedrive.checkConnectivity(this.onedriveClient); return await onedrive.checkConnectivity(
this.onedriveClient,
callbackFunc
);
} else { } else {
throw Error(`not supported service type ${this.serviceType}`); throw Error(`not supported service type ${this.serviceType}`);
} }
+8 -3
View File
@@ -636,7 +636,10 @@ export const deleteFromRemote = async (
} }
}; };
export const checkConnectivity = async (client: WrappedDropboxClient) => { export const checkConnectivity = async (
client: WrappedDropboxClient,
callbackFunc?: any
) => {
try { try {
const results = await getRemoteMeta(client, "/"); const results = await getRemoteMeta(client, "/");
if (results === undefined) { if (results === undefined) {
@@ -644,8 +647,10 @@ export const checkConnectivity = async (client: WrappedDropboxClient) => {
} }
return true; return true;
} catch (err) { } catch (err) {
console.error("dropbox connectivity error:"); log.debug(err);
console.error(err); if (callbackFunc !== undefined) {
callbackFunc(err);
}
return false; return false;
} }
}; };
+8 -1
View File
@@ -866,11 +866,18 @@ export const deleteFromRemote = async (
await client.deleteJson(remoteFileName); await client.deleteJson(remoteFileName);
}; };
export const checkConnectivity = async (client: WrappedOnedriveClient) => { export const checkConnectivity = async (
client: WrappedOnedriveClient,
callbackFunc?: any
) => {
try { try {
const k = await getUserDisplayName(client); const k = await getUserDisplayName(client);
return k !== "<unknown display name>"; return k !== "<unknown display name>";
} catch (err) { } catch (err) {
log.debug(err);
if (callbackFunc !== undefined) {
callbackFunc(err);
}
return false; return false;
} }
}; };
+11 -1
View File
@@ -498,7 +498,8 @@ export const deleteFromRemote = async (
*/ */
export const checkConnectivity = async ( export const checkConnectivity = async (
s3Client: S3Client, s3Client: S3Client,
s3Config: S3Config s3Config: S3Config,
callbackFunc?: any
) => { ) => {
try { try {
const results = await s3Client.send( const results = await s3Client.send(
@@ -509,10 +510,19 @@ export const checkConnectivity = async (
results.$metadata === undefined || results.$metadata === undefined ||
results.$metadata.httpStatusCode === undefined results.$metadata.httpStatusCode === undefined
) { ) {
const err = "results or $metadata or httStatusCode is undefined";
log.debug(err);
if (callbackFunc !== undefined) {
callbackFunc(err);
}
return false; return false;
} }
return results.$metadata.httpStatusCode === 200; return results.$metadata.httpStatusCode === 200;
} catch (err) { } catch (err) {
log.debug(err);
if (callbackFunc !== undefined) {
callbackFunc(err);
}
return false; return false;
} }
}; };
+48 -14
View File
@@ -1,5 +1,13 @@
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import { Vault, request, requestUrl, requireApiVersion } from "obsidian"; import {
Vault,
request,
requestUrl,
requireApiVersion,
Platform,
Notice,
RequestUrlResponse,
} from "obsidian";
import { Queue } from "@fyears/tsqueue"; import { Queue } from "@fyears/tsqueue";
import chunk from "lodash/chunk"; import chunk from "lodash/chunk";
@@ -20,7 +28,7 @@ import type {
ResponseDataDetailed, ResponseDataDetailed,
} from "webdav/web"; } from "webdav/web";
import { getPatcher } from "webdav/web"; import { getPatcher } from "webdav/web";
if (requireApiVersion(API_VER_REQURL)) { if (requireApiVersion(API_VER_REQURL) && !Platform.isAndroidApp) {
getPatcher().patch( getPatcher().patch(
"request", "request",
async ( async (
@@ -204,13 +212,13 @@ export class WrappedWebdavClient {
if (this.vaultFolderExists) { if (this.vaultFolderExists) {
// pass // pass
} else { } else {
const res = await this.client.exists(`/${this.vaultName}`); const res = await this.client.exists(`/${this.vaultName}/`);
if (res) { if (res) {
// log.info("remote vault folder exits!"); // log.info("remote vault folder exits!");
this.vaultFolderExists = true; this.vaultFolderExists = true;
} else { } else {
log.info("remote vault folder not exists, creating"); log.info("remote vault folder not exists, creating");
await this.client.createDirectory(`/${this.vaultName}`); await this.client.createDirectory(`/${this.vaultName}/`);
log.info("remote vault folder created!"); log.info("remote vault folder created!");
this.vaultFolderExists = true; this.vaultFolderExists = true;
} }
@@ -220,10 +228,10 @@ export class WrappedWebdavClient {
if (this.webdavConfig.depth === "auto_unknown") { if (this.webdavConfig.depth === "auto_unknown") {
let testPassed = false; let testPassed = false;
try { try {
const res = await this.client.customRequest(`/${this.vaultName}`, { const res = await this.client.customRequest(`/${this.vaultName}/`, {
method: "PROPFIND", method: "PROPFIND",
headers: { headers: {
Depth: "Infinity", Depth: "infinity",
}, },
responseType: "text", responseType: "text",
}); });
@@ -239,7 +247,7 @@ export class WrappedWebdavClient {
} }
if (!testPassed) { if (!testPassed) {
try { try {
const res = await this.client.customRequest(`/${this.vaultName}`, { const res = await this.client.customRequest(`/${this.vaultName}/`, {
method: "PROPFIND", method: "PROPFIND",
headers: { headers: {
Depth: "1", Depth: "1",
@@ -321,7 +329,7 @@ export const uploadToRemote = async (
if (password === "") { if (password === "") {
// if not encrypted, mkdir a remote folder // if not encrypted, mkdir a remote folder
await client.client.createDirectory(uploadFile, { await client.client.createDirectory(uploadFile, {
recursive: true, recursive: false, // the sync algo should guarantee no need to recursive
}); });
const res = await getRemoteMeta(client, uploadFile); const res = await getRemoteMeta(client, uploadFile);
return res; return res;
@@ -353,11 +361,12 @@ export const uploadToRemote = async (
if (password !== "") { if (password !== "") {
remoteContent = await encryptArrayBuffer(localContent, password); remoteContent = await encryptArrayBuffer(localContent, password);
} }
// we need to create folders before uploading // updated 20220326: the algorithm guarantee this
const dir = getPathFolder(uploadFile); // // we need to create folders before uploading
if (dir !== "/" && dir !== "") { // const dir = getPathFolder(uploadFile);
await client.client.createDirectory(dir, { recursive: true }); // if (dir !== "/" && dir !== "") {
} // await client.client.createDirectory(dir, { recursive: false });
// }
await client.client.putFileContents(uploadFile, remoteContent, { await client.client.putFileContents(uploadFile, remoteContent, {
overwrite: true, overwrite: true,
onUploadProgress: (progress: any) => { onUploadProgress: (progress: any) => {
@@ -523,15 +532,40 @@ export const deleteFromRemote = async (
} }
}; };
export const checkConnectivity = async (client: WrappedWebdavClient) => { export const checkConnectivity = async (
client: WrappedWebdavClient,
callbackFunc?: any
) => {
if (
!(
client.webdavConfig.address.startsWith("http://") ||
client.webdavConfig.address.startsWith("https://")
)
) {
const err = "Error: the url should start with http(s):// but it does not!";
log.debug(err);
if (callbackFunc !== undefined) {
callbackFunc(err);
}
return false;
}
try { try {
await client.init(); await client.init();
const results = await getRemoteMeta(client, "/"); const results = await getRemoteMeta(client, "/");
if (results === undefined) { if (results === undefined) {
const err = "results is undefined";
log.debug(err);
if (callbackFunc !== undefined) {
callbackFunc(err);
}
return false; return false;
} }
return true; return true;
} catch (err) { } catch (err) {
log.debug(err);
if (callbackFunc !== undefined) {
callbackFunc(err);
}
return false; return false;
} }
}; };
+81 -29
View File
@@ -826,11 +826,15 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
button.onClick(async () => { button.onClick(async () => {
new Notice(t("settings_checkonnectivity_checking")); new Notice(t("settings_checkonnectivity_checking"));
const client = new RemoteClient("s3", this.plugin.settings.s3); const client = new RemoteClient("s3", this.plugin.settings.s3);
const res = await client.checkConnectivity(); const errors = { msg: "" };
const res = await client.checkConnectivity((err: any) => {
errors.msg = err;
});
if (res) { if (res) {
new Notice(t("settings_s3_connect_succ")); new Notice(t("settings_s3_connect_succ"));
} else { } else {
new Notice(t("settings_s3_connect_fail")); new Notice(t("settings_s3_connect_fail"));
new Notice(errors.msg);
} }
}); });
}); });
@@ -959,11 +963,15 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
() => self.plugin.saveSettings() () => self.plugin.saveSettings()
); );
const res = await client.checkConnectivity(); const errors = { msg: "" };
const res = await client.checkConnectivity((err: any) => {
errors.msg = `${err}`;
});
if (res) { if (res) {
new Notice(t("settings_dropbox_connect_succ")); new Notice(t("settings_dropbox_connect_succ"));
} else { } else {
new Notice(t("settings_dropbox_connect_fail")); new Notice(t("settings_dropbox_connect_fail"));
new Notice(errors.msg);
} }
}); });
}); });
@@ -1074,11 +1082,15 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
() => self.plugin.saveSettings() () => self.plugin.saveSettings()
); );
const res = await client.checkConnectivity(); const errors = { msg: "" };
const res = await client.checkConnectivity((err: any) => {
errors.msg = `${err}`;
});
if (res) { if (res) {
new Notice(t("settings_onedrive_connect_succ")); new Notice(t("settings_onedrive_connect_succ"));
} else { } else {
new Notice(t("settings_onedrive_connect_fail")); new Notice(t("settings_onedrive_connect_fail"));
new Notice(errors.msg);
} }
}); });
}); });
@@ -1093,6 +1105,9 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
this.plugin.settings.serviceType !== "webdav" this.plugin.settings.serviceType !== "webdav"
); );
const webdavReq =
requireApiVersion(API_VER_REQURL) && !Platform.isAndroidApp;
webdavDiv.createEl("h2", { text: t("settings_webdav") }); webdavDiv.createEl("h2", { text: t("settings_webdav") });
webdavDiv.createEl("p", { webdavDiv.createEl("p", {
@@ -1100,7 +1115,17 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
cls: "webdav-disclaimer", cls: "webdav-disclaimer",
}); });
if (!requireApiVersion(API_VER_REQURL)) { if (!webdavReq) {
if (Platform.isAndroidApp) {
webdavDiv.createEl("p", {
text: t("settings_webdav_cors_android"),
});
} else {
webdavDiv.createEl("p", {
text: t("settings_webdav_cors_otheros"),
});
}
webdavDiv.createEl("p", { webdavDiv.createEl("p", {
text: t("settings_webdav_cors"), text: t("settings_webdav_cors"),
}); });
@@ -1121,6 +1146,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.webdav.address) .setValue(this.plugin.settings.webdav.address)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.webdav.address = value.trim(); this.plugin.settings.webdav.address = value.trim();
if (
this.plugin.settings.webdav.depth === "auto_1" ||
this.plugin.settings.webdav.depth === "auto_infinity"
) {
this.plugin.settings.webdav.depth = "auto_unknown";
}
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
@@ -1134,6 +1165,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.webdav.username) .setValue(this.plugin.settings.webdav.username)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.webdav.username = value.trim(); this.plugin.settings.webdav.username = value.trim();
if (
this.plugin.settings.webdav.depth === "auto_1" ||
this.plugin.settings.webdav.depth === "auto_infinity"
) {
this.plugin.settings.webdav.depth = "auto_unknown";
}
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
@@ -1147,6 +1184,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.webdav.password) .setValue(this.plugin.settings.webdav.password)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.webdav.password = value.trim(); this.plugin.settings.webdav.password = value.trim();
if (
this.plugin.settings.webdav.depth === "auto_1" ||
this.plugin.settings.webdav.depth === "auto_infinity"
) {
this.plugin.settings.webdav.depth = "auto_unknown";
}
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
@@ -1156,15 +1199,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setDesc(t("settings_webdav_auth_desc")) .setDesc(t("settings_webdav_auth_desc"))
.addDropdown(async (dropdown) => { .addDropdown(async (dropdown) => {
dropdown.addOption("basic", "basic"); dropdown.addOption("basic", "basic");
if (requireApiVersion(API_VER_REQURL)) { if (webdavReq) {
dropdown.addOption("digest", "digest"); dropdown.addOption("digest", "digest");
} }
// new version config, copied to old version, we need to reset it // new version config, copied to old version, we need to reset it
if ( if (!webdavReq && this.plugin.settings.webdav.authType !== "basic") {
!requireApiVersion(API_VER_REQURL) &&
this.plugin.settings.webdav.authType !== "basic"
) {
this.plugin.settings.webdav.authType = "basic"; this.plugin.settings.webdav.authType = "basic";
await this.plugin.saveSettings(); await this.plugin.saveSettings();
} }
@@ -1230,20 +1270,19 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
this.app.vault.getName(), this.app.vault.getName(),
() => self.plugin.saveSettings() () => self.plugin.saveSettings()
); );
const res = await client.checkConnectivity(); const errors = { msg: "" };
const res = await client.checkConnectivity((err: any) => {
errors.msg = `${err}`;
});
if (res) { if (res) {
new Notice(t("settings_webdav_connect_succ")); new Notice(t("settings_webdav_connect_succ"));
} else { } else {
let corsErrMsg = "/CORS"; if (webdavReq) {
if (requireApiVersion(API_VER_REQURL)) { new Notice(t("settings_webdav_connect_fail"));
corsErrMsg = ""; } else {
new Notice(t("settings_webdav_connect_fail_withcors"));
} }
new Notice(errors.msg);
new Notice(
t("settings_webdav_connect_fail", {
corsErrMsg: corsErrMsg,
})
);
} }
}); });
}); });
@@ -1355,15 +1394,10 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.onChange(async (val) => { .onChange(async (val) => {
if (val === "enable" && !bridge.secondConfirm) { if (val === "enable" && !bridge.secondConfirm) {
dropdown.setValue("disable"); dropdown.setValue("disable");
const modal = new SyncConfigDirModal( new SyncConfigDirModal(this.app, this.plugin, () => {
this.app, bridge.secondConfirm = true;
this.plugin, dropdown.setValue("enable");
() => { }).open();
bridge.secondConfirm = true;
dropdown.setValue("enable");
}
);
modal.open();
} else { } else {
bridge.secondConfirm = false; bridge.secondConfirm = false;
this.plugin.settings.syncConfigDir = false; this.plugin.settings.syncConfigDir = false;
@@ -1445,7 +1479,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
await exportVaultSyncPlansToFiles( await exportVaultSyncPlansToFiles(
this.plugin.db, this.plugin.db,
this.app.vault, this.app.vault,
this.plugin.settings.vaultRandomID this.plugin.vaultRandomID
); );
new Notice(t("settings_syncplans_notice")); new Notice(t("settings_syncplans_notice"));
}); });
@@ -1473,6 +1507,18 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
}); });
}); });
const outputCurrBasePathVaultIDDiv = debugDiv.createDiv("div");
new Setting(outputCurrBasePathVaultIDDiv)
.setName(t("settings_outputbasepathvaultid"))
.setDesc(t("settings_outputbasepathvaultid_desc"))
.addButton(async (button) => {
button.setButtonText(t("settings_outputbasepathvaultid_button"));
button.onClick(async () => {
new Notice(this.plugin.getVaultBasePath());
new Notice(this.plugin.vaultRandomID);
});
});
const dbsResetDiv = debugDiv.createEl("div"); const dbsResetDiv = debugDiv.createEl("div");
new Setting(dbsResetDiv) new Setting(dbsResetDiv)
.setName(t("settings_resetcache")) .setName(t("settings_resetcache"))
@@ -1485,4 +1531,10 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
}); });
}); });
} }
hide() {
let { containerEl } = this;
containerEl.empty();
super.hide();
}
} }
+85 -30
View File
@@ -218,7 +218,7 @@ export const parseRemoteItems = async (
mtimeRemote: backwardMapping.localMtime || entry.lastModified, mtimeRemote: backwardMapping.localMtime || entry.lastModified,
sizeRemote: backwardMapping.localSize || entry.size, sizeRemote: backwardMapping.localSize || entry.size,
remoteEncryptedKey: remoteEncryptedKey, remoteEncryptedKey: remoteEncryptedKey,
changeMtimeUsingMapping: true, changeRemoteMtimeUsingMapping: true,
}; };
} else { } else {
r = { r = {
@@ -227,7 +227,7 @@ export const parseRemoteItems = async (
mtimeRemote: entry.lastModified, mtimeRemote: entry.lastModified,
sizeRemote: entry.size, sizeRemote: entry.size,
remoteEncryptedKey: remoteEncryptedKey, remoteEncryptedKey: remoteEncryptedKey,
changeMtimeUsingMapping: false, changeRemoteMtimeUsingMapping: false,
}; };
} }
@@ -295,7 +295,7 @@ const ensembleMixedStates = async (
local: TAbstractFile[], local: TAbstractFile[],
localConfigDirContents: ObsConfigDirFileType[] | undefined, localConfigDirContents: ObsConfigDirFileType[] | undefined,
remoteDeleteHistory: DeletionOnRemote[], remoteDeleteHistory: DeletionOnRemote[],
localDeleteHistory: FileFolderHistoryRecord[], localFileHistory: FileFolderHistoryRecord[],
syncConfigDir: boolean, syncConfigDir: boolean,
configDir: string, configDir: string,
syncUnderscoreItems: boolean syncUnderscoreItems: boolean
@@ -323,7 +323,7 @@ const ensembleMixedStates = async (
r = { r = {
key: entry.path, key: entry.path,
existLocal: true, existLocal: true,
mtimeLocal: Math.max(entry.stat.mtime, entry.stat.ctime), mtimeLocal: Math.max(entry.stat.mtime || 0, entry.stat.ctime || 0),
sizeLocal: entry.stat.size, sizeLocal: entry.stat.size,
}; };
} else if (entry instanceof TFolder) { } else if (entry instanceof TFolder) {
@@ -397,7 +397,7 @@ const ensembleMixedStates = async (
} }
} }
for (const entry of localDeleteHistory) { for (const entry of localFileHistory) {
let key = entry.key; let key = entry.key;
if (entry.keyType === "folder") { if (entry.keyType === "folder") {
if (!entry.key.endsWith("/")) { if (!entry.key.endsWith("/")) {
@@ -409,23 +409,45 @@ const ensembleMixedStates = async (
throw Error(`unexpected ${entry}`); throw Error(`unexpected ${entry}`);
} }
const r = {
key: key,
deltimeLocal: entry.actionWhen,
} as FileOrFolderMixedState;
if (isSkipItem(key, syncConfigDir, syncUnderscoreItems, configDir)) { if (isSkipItem(key, syncConfigDir, syncUnderscoreItems, configDir)) {
continue; continue;
} }
if (results.hasOwnProperty(key)) { if (entry.actionType === "delete" || entry.actionType === "rename") {
results[key].key = r.key; const r = {
results[key].deltimeLocal = r.deltimeLocal; key: key,
} else { deltimeLocal: entry.actionWhen,
results[key] = r; } as FileOrFolderMixedState;
results[key].existLocal = false; if (results.hasOwnProperty(key)) {
results[key].existRemote = false; results[key].deltimeLocal = r.deltimeLocal;
} else {
results[key] = r;
results[key].existLocal = false; // we have already checked local
results[key].existRemote = false; // we have already checked remote
}
} else if (entry.actionType === "renameDestination") {
const r = {
key: key,
mtimeLocal: entry.actionWhen,
changeLocalMtimeUsingMapping: true,
};
if (results.hasOwnProperty(key)) {
results[key].mtimeLocal = Math.max(
r.mtimeLocal || 0,
results[key].mtimeLocal || 0
);
results[key].changeLocalMtimeUsingMapping =
r.changeLocalMtimeUsingMapping;
} else {
results[key] = r;
results[key].existLocal = false; // we have already checked local
results[key].existRemote = false; // we have already checked remote
}
} else {
throw Error(
`do not know how to deal with local file history ${entry.key} with ${entry.actionType}`
);
} }
} }
@@ -450,34 +472,42 @@ const assignOperationToFileInplace = (
// 0. find anything inconsistent // 0. find anything inconsistent
if (r.existLocal && (r.mtimeLocal === undefined || r.mtimeLocal <= 0)) { if (r.existLocal && (r.mtimeLocal === undefined || r.mtimeLocal <= 0)) {
throw Error( throw Error(
`Error: File ${r.key} has a last modified time <=0 or undefined in the local file system. It's abnormal and the plugin stops.` `Error: Abnormal last modified time locally: ${JSON.stringify(
r,
null,
2
)}`
); );
} }
if (r.existRemote && (r.mtimeRemote === undefined || r.mtimeRemote <= 0)) { if (r.existRemote && (r.mtimeRemote === undefined || r.mtimeRemote <= 0)) {
throw Error( throw Error(
`Error: File ${r.key} has a last modified time <=0 or undefined on the remote service. It's abnormal and the plugin stops.` `Error: Abnormal last modified time remotely: ${JSON.stringify(
r,
null,
2
)}`
); );
} }
if (r.deltimeLocal !== undefined && r.deltimeLocal <= 0) { if (r.deltimeLocal !== undefined && r.deltimeLocal <= 0) {
throw Error( throw Error(
`Error: File ${r.key} has a local deletion time <=0. It's abnormal and the plugin stops.` `Error: Abnormal deletion time locally: ${JSON.stringify(r, null, 2)}`
); );
} }
if (r.deltimeRemote !== undefined && r.deltimeRemote <= 0) { if (r.deltimeRemote !== undefined && r.deltimeRemote <= 0) {
throw Error( throw Error(
`Error: File ${r.key} has a remote deletion time <=0. It's abnormal and the plugin stops.` `Error: Abnormal deletion time remotely: ${JSON.stringify(r, null, 2)}`
); );
} }
// 1. mtimeLocal // 1. mtimeLocal
if (r.existLocal) { if (r.existLocal) {
const mtimeRemote = r.existRemote ? r.mtimeRemote : -1; const mtimeRemote = r.existRemote ? r.mtimeRemote : -1;
const deltime_remote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1; const deltimeRemote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1;
const deltimeLocal = r.deltimeLocal !== undefined ? r.deltimeLocal : -1; const deltimeLocal = r.deltimeLocal !== undefined ? r.deltimeLocal : -1;
if ( if (
r.mtimeLocal >= mtimeRemote && r.mtimeLocal >= mtimeRemote &&
r.mtimeLocal >= deltimeLocal && r.mtimeLocal >= deltimeLocal &&
r.mtimeLocal >= deltime_remote r.mtimeLocal >= deltimeRemote
) { ) {
if (r.mtimeLocal === r.mtimeRemote) { if (r.mtimeLocal === r.mtimeRemote) {
// mtime the same // mtime the same
@@ -508,12 +538,12 @@ const assignOperationToFileInplace = (
// 2. mtimeRemote // 2. mtimeRemote
if (r.existRemote) { if (r.existRemote) {
const mtimeLocal = r.existLocal ? r.mtimeLocal : -1; const mtimeLocal = r.existLocal ? r.mtimeLocal : -1;
const deltime_remote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1; const deltimeRemote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1;
const deltimeLocal = r.deltimeLocal !== undefined ? r.deltimeLocal : -1; const deltimeLocal = r.deltimeLocal !== undefined ? r.deltimeLocal : -1;
if ( if (
r.mtimeRemote > mtimeLocal && r.mtimeRemote > mtimeLocal &&
r.mtimeRemote >= deltimeLocal && r.mtimeRemote >= deltimeLocal &&
r.mtimeRemote >= deltime_remote r.mtimeRemote >= deltimeRemote
) { ) {
r.decision = "downloadRemoteToLocal"; r.decision = "downloadRemoteToLocal";
r.decisionBranch = 5; r.decisionBranch = 5;
@@ -526,11 +556,11 @@ const assignOperationToFileInplace = (
if (r.deltimeLocal !== undefined && r.deltimeLocal !== 0) { if (r.deltimeLocal !== undefined && r.deltimeLocal !== 0) {
const mtimeLocal = r.existLocal ? r.mtimeLocal : -1; const mtimeLocal = r.existLocal ? r.mtimeLocal : -1;
const mtimeRemote = r.existRemote ? r.mtimeRemote : -1; const mtimeRemote = r.existRemote ? r.mtimeRemote : -1;
const deltime_remote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1; const deltimeRemote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1;
if ( if (
r.deltimeLocal >= mtimeLocal && r.deltimeLocal >= mtimeLocal &&
r.deltimeLocal >= mtimeRemote && r.deltimeLocal >= mtimeRemote &&
r.deltimeLocal >= deltime_remote r.deltimeLocal >= deltimeRemote
) { ) {
r.decision = "uploadLocalDelHistToRemote"; r.decision = "uploadLocalDelHistToRemote";
r.decisionBranch = 6; r.decisionBranch = 6;
@@ -541,7 +571,7 @@ const assignOperationToFileInplace = (
} }
} }
// 4. deltime_remote // 4. deltimeRemote
if (r.deltimeRemote !== undefined && r.deltimeRemote !== 0) { if (r.deltimeRemote !== undefined && r.deltimeRemote !== 0) {
const mtimeLocal = r.existLocal ? r.mtimeLocal : -1; const mtimeLocal = r.existLocal ? r.mtimeLocal : -1;
const mtimeRemote = r.existRemote ? r.mtimeRemote : -1; const mtimeRemote = r.existRemote ? r.mtimeRemote : -1;
@@ -609,6 +639,31 @@ const assignOperationToFolderInplace = async (
} }
} }
// If it was moved to here, after deletion, we should keep it as is.
// The logic not necessarily needs API_VER_STAT_FOLDER.
// The folder needs this logic because it's also determined by file children.
// But the file do not need this logic because the mtimeLocal is checked firstly.
if (
r.existLocal &&
r.changeLocalMtimeUsingMapping &&
r.mtimeLocal > 0 &&
r.mtimeLocal > deltimeLocal &&
r.mtimeLocal > deltimeRemote
) {
keptFolder.add(getParentFolder(r.key));
if (r.existLocal && r.existRemote) {
r.decision = "skipFolder";
r.decisionBranch = 16;
} else if (r.existLocal || r.existRemote) {
r.decision = "createFolder";
r.decisionBranch = 17;
} else {
throw Error(
`Error: Folder ${r.key} doesn't exist locally and remotely but is marked must be kept. Abort.`
);
}
}
if (r.decision === undefined) { if (r.decision === undefined) {
// not yet decided by the above reason // not yet decided by the above reason
if (deltimeLocal > 0 && deltimeLocal > deltimeRemote) { if (deltimeLocal > 0 && deltimeLocal > deltimeRemote) {
@@ -670,7 +725,7 @@ export const getSyncPlan = async (
local: TAbstractFile[], local: TAbstractFile[],
localConfigDirContents: ObsConfigDirFileType[] | undefined, localConfigDirContents: ObsConfigDirFileType[] | undefined,
remoteDeleteHistory: DeletionOnRemote[], remoteDeleteHistory: DeletionOnRemote[],
localDeleteHistory: FileFolderHistoryRecord[], localFileHistory: FileFolderHistoryRecord[],
remoteType: SUPPORTED_SERVICES_TYPE, remoteType: SUPPORTED_SERVICES_TYPE,
vault: Vault, vault: Vault,
syncConfigDir: boolean, syncConfigDir: boolean,
@@ -683,7 +738,7 @@ export const getSyncPlan = async (
local, local,
localConfigDirContents, localConfigDirContents,
remoteDeleteHistory, remoteDeleteHistory,
localDeleteHistory, localFileHistory,
syncConfigDir, syncConfigDir,
configDir, configDir,
syncUnderscoreItems syncUnderscoreItems
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"0.3.2": "0.12.15", "0.3.2": "0.12.15",
"0.3.9": "0.13.21" "0.3.14": "0.13.21"
} }