Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc082016d8 | ||
|
|
af93af9047 | ||
|
|
0b5b9cf51a | ||
|
|
915d027b12 | ||
|
|
479d2a4fac | ||
|
|
2d9610cd92 | ||
|
|
832b7e92f9 |
@@ -61,7 +61,9 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
||||
### S3
|
||||
|
||||
- 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.
|
||||
- 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`. 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.
|
||||
- About CORS:
|
||||
- If you are using Obsidian >= 0.13.25, 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`. 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.
|
||||
- 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.
|
||||
@@ -88,10 +90,12 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
||||
### webdav
|
||||
|
||||
- **webdav support is considered experimental.**
|
||||
- Currently only supports BASIC authorization method.
|
||||
- Currently 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- About CORS:
|
||||
- If you are using Obsidian >= 0.13.25, you can skip this CORS part.
|
||||
- If you are using Obsidian < 0.13.25:
|
||||
- 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.
|
||||
- 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.
|
||||
- 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**.
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ Technically, the plugin (or any plugin?) runs in the js environment provided by
|
||||
|
||||
Then some limitations are applied:
|
||||
|
||||
1. [The CORS policy.](./browser_env_cors.md)
|
||||
1. [The CORS issue (solved in the new Obsidian version).](./browser_env_cors.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).
|
||||
4. [No background running after Obsidian is closes.](./browser_env_no_background_after_closing.md)
|
||||
|
||||
@@ -4,18 +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)
|
||||
|
||||
To solve the issue, we have some options:
|
||||
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.
|
||||
|
||||
1. The server side needs to return the header `Access-Control-Allow-Origin` allowing the origins `app://obsidian.md` and `capacitor://localhost` and `http://localhost`. Sometimes in the future, the header `Access-Control-Expose-Headers` with some values being set might be also needed.
|
||||
|
||||
[Here is an example configuration for Amazon S3.](./s3_cors_configure.md)
|
||||
|
||||
However, some cloud services do not allow configuring or exposing these headers. (Notably most public WebDAV services.)
|
||||
|
||||
It's of course possible if the users build the services by themselves.
|
||||
|
||||
2. Obsidian implements and exposes a new api helping developers to bypass the CORS policy.
|
||||
|
||||
Currently (as of Feb 2022), an api `request()` indeed exists, but it only deals with text-like data, and does not support binary data or response headers reading yet.
|
||||
|
||||
Because this plugin allows uploading and downloading binary data, so a more feature-rich api is needed.
|
||||
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).
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 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.
|
||||
|
||||
Thanks to [@NAL100 in the Discussion](https://github.com/fyears/remotely-save/discussions/28).
|
||||
|
||||
Please try this config:
|
||||
|
||||
@@ -2,39 +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.
|
||||
|
||||
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 list is for information purposes only.
|
||||
|
||||
| Service | Connectable | by S3 | by WebDAV | by other protocol | More Info |
|
||||
| ------------------------------------------------------------------------------------- | ----------- | ----- | --------- | ----------------- | ------------------------------------------------------------ |
|
||||
| Amazon S3 | Yes | Yes | | | [CORS config needed.](./s3_cors_configure.md) |
|
||||
| Tencent Cloud - Cloud Object Storage (COS) 腾讯云对象存储 | Yes | Yes | | | CORS config needed. |
|
||||
| Alibaba Cloud - Object Storage Service 阿里云对象存储 | Yes | Yes | | | CORS config needed. |
|
||||
| Backblaze B2 Cloud Storage | No | No | | | Its CORS rules doesn't allow no-http(s) origins. |
|
||||
| [Wasabi](https://wasabi.com) | ? | ? | | | |
|
||||
| [filebase](https://filebase.com/) | Yes | Yes | | | CORS config needed. |
|
||||
| QingStor 青云 | ? | ? | | | |
|
||||
| [MinIO](https://min.io/) | ? | ? | | | |
|
||||
| [WsgiDAV](https://github.com/mar10/wsgidav) | Yes | | Yes | | CORS rules can be set. |
|
||||
| [Nginx `ngx_http_dav_module`](http://nginx.org/en/docs/http/ngx_http_dav_module.html) | ? | | ? | | Should be possible? |
|
||||
| NextCloud | No? | | No? | | No CORS config by default. |
|
||||
| OwnCloud | No? | | No? | | No CORS config by default. |
|
||||
| Seafile | ? | | ? | | |
|
||||
| `rclone serve webdav` | No | | No | | No CORS support. |
|
||||
| [Nutstore 坚果云](https://www.jianguoyun.com/) | No | | No | | No CORS support. |
|
||||
| [TeraCLOUD](https://teracloud.jp/en/) | No | | No | | No CORS support. |
|
||||
| Dropbox | Yes | | | Yes | |
|
||||
| OneDrive for personal | Yes | | | Yes | |
|
||||
| OneDrive for Business | In the plan | | | ? | |
|
||||
| Google Drive | In the plan | | | ? | |
|
||||
| [Box](https://www.box.com/) | ? | | | ? | 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. |
|
||||
| [OpenStack Storage (Swift)](https://github.com/openstack/swift) | ? | | | ? | May be possible but needs further development. |
|
||||
| https://put.io/ | ? | | | ? | |
|
||||
| Yandex Disk | ? | | | ? | |
|
||||
| FTP / FTPS | Never | | | No | Technically never possible to be implemented. |
|
||||
| SFTP | Never | | | No | Technically never possible to be implemented. |
|
||||
| Jottacloud | No | | | No | It seems that no open api is available. |
|
||||
| Mega | Never | | | No | No js api is available. |
|
||||
| Git | Never | | | No | Technically very hard, if not impossible, to be implemented. |
|
||||
| | | | | | |
|
||||
| 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 |
|
||||
| ------------------------------------------------------------------------------------- | ----------- | ----- | --------- | ---------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------ |
|
||||
| Amazon S3 | Yes | Yes | | | Yes | [CORS config needed.](./s3_cors_configure.md) |
|
||||
| Tencent Cloud - Cloud Object Storage (COS) 腾讯云对象存储 | 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. |
|
||||
| [Wasabi](https://wasabi.com) | ? | ? | | | | |
|
||||
| [filebase](https://filebase.com/) | Yes | Yes | | | Yes | CORS config needed. |
|
||||
| QingStor 青云 | ? | ? | | | | |
|
||||
| [MinIO](https://min.io/) | ? | ? | | | | |
|
||||
| [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? | ? |
|
||||
| NextCloud | Yes? | | Yes? | | Yes? | No CORS config by default. |
|
||||
| OwnCloud | Yes? | | Yes? | | Yes? | No CORS config by default. |
|
||||
| Seafile | Yes? | | Yes? | | Yes? | |
|
||||
| `rclone serve webdav` | 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. |
|
||||
| Dropbox | Yes | | | Yes | | |
|
||||
| OneDrive for personal | Yes | | | Yes | | |
|
||||
| OneDrive for Business | In the plan | | | ? | | |
|
||||
| Google Drive | In the plan | | | ? | | |
|
||||
| [Box](https://www.box.com/) | ? | | | 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. | | |
|
||||
| [OpenStack Storage (Swift)](https://github.com/openstack/swift) | ? | | | May be possible but needs further development. | | |
|
||||
| https://put.io/ | ? | | | ? | | |
|
||||
| Yandex Disk | ? | | | ? | | |
|
||||
| FTP / FTPS | 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. | | |
|
||||
| Mega | Never | | | No. No js api is available. | | |
|
||||
| Git | Never | | | No. Technically very hard, if not impossible, to be implemented. | | |
|
||||
| | | | | | | |
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "remotely-save",
|
||||
"name": "Remotely Save",
|
||||
"version": "0.3.5",
|
||||
"version": "0.3.6",
|
||||
"minAppVersion": "0.13.21",
|
||||
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
||||
"author": "fyears",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "remotely-save",
|
||||
"version": "0.3.5",
|
||||
"version": "0.3.6",
|
||||
"description": "This is yet another sync plugin for Obsidian app.",
|
||||
"scripts": {
|
||||
"dev2": "node esbuild.config.mjs",
|
||||
|
||||
+10
-1
@@ -26,13 +26,20 @@ export interface DropboxConfig {
|
||||
}
|
||||
|
||||
export type WebdavAuthType = "digest" | "basic";
|
||||
export type WebdavDepthType =
|
||||
| "auto_unknown"
|
||||
| "auto_1"
|
||||
| "auto_infinity"
|
||||
| "manual_1"
|
||||
| "manual_infinity";
|
||||
|
||||
export interface WebdavConfig {
|
||||
address: string;
|
||||
username: string;
|
||||
password: string;
|
||||
authType: WebdavAuthType;
|
||||
manualRecursive: boolean;
|
||||
manualRecursive: boolean; // deprecated in 0.3.6, use depth
|
||||
depth?: WebdavDepthType;
|
||||
}
|
||||
|
||||
export interface OnedriveConfig {
|
||||
@@ -60,6 +67,8 @@ export interface RemotelySavePluginSettings {
|
||||
initRunAfterMilliseconds?: number;
|
||||
agreeToUploadExtraMetadata?: boolean;
|
||||
concurrency?: number;
|
||||
syncConfigDir?: boolean;
|
||||
syncUnderscoreItems?: boolean;
|
||||
}
|
||||
|
||||
export interface RemoteItem {
|
||||
|
||||
+56
@@ -37,6 +37,7 @@ import { RemotelySaveSettingTab } from "./settings";
|
||||
import { fetchMetadataFile, parseRemoteItems, SyncStatusType } from "./sync";
|
||||
import { doActualSync, getSyncPlan, isPasswordOk } from "./sync";
|
||||
import { messyConfigToNormal, normalConfigToMessy } from "./configPersist";
|
||||
import { ObsConfigDirFileType, listFilesInObsFolder } from "./obsFolderLister";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
import { DeletionOnRemote, MetadataOnRemote } from "./metadataOnRemote";
|
||||
@@ -56,6 +57,8 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
||||
initRunAfterMilliseconds: -1,
|
||||
agreeToUploadExtraMetadata: false,
|
||||
concurrency: 5,
|
||||
syncConfigDir: false,
|
||||
syncUnderscoreItems: false,
|
||||
};
|
||||
|
||||
interface OAuth2Info {
|
||||
@@ -190,6 +193,14 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.db,
|
||||
this.settings.vaultRandomID
|
||||
);
|
||||
let localConfigDirContents: ObsConfigDirFileType[] = undefined;
|
||||
if (this.settings.syncConfigDir) {
|
||||
localConfigDirContents = await listFilesInObsFolder(
|
||||
this.app.vault.configDir,
|
||||
this.app.vault,
|
||||
this.manifest.id
|
||||
);
|
||||
}
|
||||
// log.info(local);
|
||||
// log.info(localHistory);
|
||||
|
||||
@@ -198,10 +209,14 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
const { plan, sortedKeys, deletions } = await getSyncPlan(
|
||||
remoteStates,
|
||||
local,
|
||||
localConfigDirContents,
|
||||
origMetadataOnRemote.deletions,
|
||||
localHistory,
|
||||
client.serviceType,
|
||||
this.app.vault,
|
||||
this.settings.syncConfigDir,
|
||||
this.app.vault.configDir,
|
||||
this.settings.syncUnderscoreItems,
|
||||
this.settings.password
|
||||
);
|
||||
log.info(plan.mixedStates); // for debugging
|
||||
@@ -300,6 +315,9 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
await this.checkIfOauthExpires();
|
||||
await this.checkIfVaultIDAssigned(); // MUST before prepareDB()
|
||||
|
||||
// no need to await this
|
||||
this.tryToAddIgnoreFile();
|
||||
|
||||
await this.prepareDB();
|
||||
|
||||
this.syncStatus = "idle";
|
||||
@@ -564,6 +582,9 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
if (this.settings.webdav.manualRecursive === undefined) {
|
||||
this.settings.webdav.manualRecursive = false;
|
||||
}
|
||||
if (this.settings.webdav.depth === undefined) {
|
||||
this.settings.webdav.depth = "auto_unknown";
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
@@ -708,4 +729,39 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
const msg = `syncing progress=${i}/${totalCount},decision=${decision},path=${pathName}`;
|
||||
this.currSyncMsg = msg;
|
||||
}
|
||||
|
||||
async tryToAddIgnoreFile() {
|
||||
const pluginConfigDir =
|
||||
this.manifest.dir ||
|
||||
`${this.app.vault.configDir}/plugins/${this.manifest.dir}`;
|
||||
const pluginConfigDirExists = await this.app.vault.adapter.exists(
|
||||
pluginConfigDir
|
||||
);
|
||||
if (!pluginConfigDirExists) {
|
||||
// what happened?
|
||||
return;
|
||||
}
|
||||
const ignoreFile = `${pluginConfigDir}/.gitignore`;
|
||||
const ignoreFileExists = await this.app.vault.adapter.exists(ignoreFile);
|
||||
|
||||
const contentText = "data.json\n";
|
||||
|
||||
try {
|
||||
if (ignoreFileExists) {
|
||||
// check empty, if empty, we can write it
|
||||
// if not empty, we do nothing
|
||||
const content = (await this.app.vault.adapter.read(ignoreFile)).trim();
|
||||
if (content === "") {
|
||||
// no need to await writing
|
||||
this.app.vault.adapter.write(ignoreFile, contentText);
|
||||
}
|
||||
} else {
|
||||
// not exists, directly create
|
||||
// no need to await
|
||||
this.app.vault.adapter.write(ignoreFile, contentText);
|
||||
}
|
||||
} catch (error) {
|
||||
// just skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-4
@@ -10,10 +10,18 @@ const log = origLog.getLogger("rs-default");
|
||||
/**
|
||||
* If any part of the file starts with '.' or '_' then it's a hidden file.
|
||||
* @param item
|
||||
* @param loose
|
||||
* @param dot
|
||||
* @param underscore
|
||||
* @returns
|
||||
*/
|
||||
export const isHiddenPath = (item: string, loose: boolean = true) => {
|
||||
export const isHiddenPath = (
|
||||
item: string,
|
||||
dot: boolean = true,
|
||||
underscore: boolean = true
|
||||
) => {
|
||||
if (!(dot || underscore)) {
|
||||
throw Error("parameter error for isHiddenPath");
|
||||
}
|
||||
const k = path.posix.normalize(item); // TODO: only unix path now
|
||||
const k2 = k.split("/"); // TODO: only unix path now
|
||||
// log.info(k2)
|
||||
@@ -21,10 +29,10 @@ export const isHiddenPath = (item: string, loose: boolean = true) => {
|
||||
if (singlePart === "." || singlePart === ".." || singlePart === "") {
|
||||
continue;
|
||||
}
|
||||
if (singlePart[0] === ".") {
|
||||
if (dot && singlePart[0] === ".") {
|
||||
return true;
|
||||
}
|
||||
if (loose && singlePart[0] === "_") {
|
||||
if (underscore && singlePart[0] === "_") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Vault, Stat, ListedFiles } from "obsidian";
|
||||
import { Queue } from "@fyears/tsqueue";
|
||||
import chunk from "lodash/chunk";
|
||||
import flatten from "lodash/flatten";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
export interface ObsConfigDirFileType {
|
||||
key: string;
|
||||
ctime: number;
|
||||
mtime: number;
|
||||
size: number;
|
||||
type: "folder" | "file";
|
||||
}
|
||||
|
||||
const isFolderToSkip = (x: string) => {
|
||||
let specialFolders = [".git", ".svn", "node_modules"];
|
||||
for (const iterator of specialFolders) {
|
||||
if (
|
||||
x === iterator ||
|
||||
x === `${iterator}/` ||
|
||||
x.endsWith(`/${iterator}`) ||
|
||||
x.endsWith(`/${iterator}/`)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isPluginDirItself = (x: string, pluginId: string) => {
|
||||
return (
|
||||
x === pluginId ||
|
||||
x === `${pluginId}/` ||
|
||||
x.endsWith(`/${pluginId}`) ||
|
||||
x.endsWith(`/${pluginId}/`)
|
||||
);
|
||||
};
|
||||
|
||||
const isLikelyPluginSubFiles = (x: string) => {
|
||||
const reqFiles = [
|
||||
"data.json",
|
||||
"main.js",
|
||||
"manifest.json",
|
||||
".gitignore",
|
||||
"styles.css",
|
||||
];
|
||||
for (const iterator of reqFiles) {
|
||||
if (x === iterator || x.endsWith(`/${iterator}`)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isInsideObsFolder = (x: string, configDir: string) => {
|
||||
if (!configDir.startsWith(".")) {
|
||||
throw Error(`configDir should starts with . but we get ${configDir}`);
|
||||
}
|
||||
return x === configDir || x.startsWith(`${configDir}/`);
|
||||
};
|
||||
|
||||
export const listFilesInObsFolder = async (
|
||||
configDir: string,
|
||||
vault: Vault,
|
||||
pluginId: string
|
||||
) => {
|
||||
const q = new Queue([configDir]);
|
||||
const CHUNK_SIZE = 10;
|
||||
const contents: ObsConfigDirFileType[] = [];
|
||||
while (q.length > 0) {
|
||||
const itemsToFetch = [];
|
||||
while (q.length > 0) {
|
||||
itemsToFetch.push(q.pop());
|
||||
}
|
||||
|
||||
const itemsToFetchChunks = chunk(itemsToFetch, CHUNK_SIZE);
|
||||
for (const singleChunk of itemsToFetchChunks) {
|
||||
const r = singleChunk.map(async (x) => {
|
||||
const statRes = await vault.adapter.stat(x);
|
||||
const isFolder = statRes.type === "folder";
|
||||
let children: ListedFiles = undefined;
|
||||
if (isFolder) {
|
||||
children = await vault.adapter.list(x);
|
||||
}
|
||||
|
||||
return {
|
||||
itself: {
|
||||
key: isFolder ? `${x}/` : x,
|
||||
...statRes,
|
||||
} as ObsConfigDirFileType,
|
||||
children: children,
|
||||
};
|
||||
});
|
||||
const r2 = flatten(await Promise.all(r));
|
||||
|
||||
for (const iter of r2) {
|
||||
contents.push(iter.itself);
|
||||
const isInsideSelfPlugin = isPluginDirItself(iter.itself.key, pluginId);
|
||||
if (iter.children !== undefined) {
|
||||
for (const iter2 of iter.children.folders) {
|
||||
if (isFolderToSkip(iter2)) {
|
||||
continue;
|
||||
}
|
||||
if (isInsideSelfPlugin && !isLikelyPluginSubFiles(iter2)) {
|
||||
// special treatment for remotely-save folder
|
||||
continue;
|
||||
}
|
||||
q.push(iter2);
|
||||
}
|
||||
for (const iter2 of iter.children.files) {
|
||||
if (isFolderToSkip(iter2)) {
|
||||
continue;
|
||||
}
|
||||
if (isInsideSelfPlugin && !isLikelyPluginSubFiles(iter2)) {
|
||||
// special treatment for remotely-save folder
|
||||
continue;
|
||||
}
|
||||
q.push(iter2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return contents;
|
||||
};
|
||||
+9
-3
@@ -41,11 +41,17 @@ export class RemoteClient {
|
||||
this.s3Config = s3Config;
|
||||
this.s3Client = s3.getS3Client(this.s3Config);
|
||||
} else if (serviceType === "webdav") {
|
||||
if (vaultName === undefined) {
|
||||
throw Error("remember to provide vault name while init webdav client");
|
||||
if (vaultName === undefined || saveUpdatedConfigFunc === undefined) {
|
||||
throw Error(
|
||||
"remember to provide vault name and callback while init webdav client"
|
||||
);
|
||||
}
|
||||
this.webdavConfig = webdavConfig;
|
||||
this.webdavClient = webdav.getWebdavClient(this.webdavConfig, vaultName);
|
||||
this.webdavClient = webdav.getWebdavClient(
|
||||
this.webdavConfig,
|
||||
vaultName,
|
||||
saveUpdatedConfigFunc
|
||||
);
|
||||
} else if (serviceType === "dropbox") {
|
||||
if (vaultName === undefined || saveUpdatedConfigFunc === undefined) {
|
||||
throw Error(
|
||||
|
||||
+76
-6
@@ -118,6 +118,7 @@ export const DEFAULT_WEBDAV_CONFIG = {
|
||||
password: "",
|
||||
authType: "basic",
|
||||
manualRecursive: false,
|
||||
depth: "auto_unknown",
|
||||
} as WebdavConfig;
|
||||
|
||||
const getWebdavPath = (fileOrFolderPath: string, vaultName: string) => {
|
||||
@@ -166,10 +167,16 @@ export class WrappedWebdavClient {
|
||||
vaultName: string;
|
||||
client: WebDAVClient;
|
||||
vaultFolderExists: boolean;
|
||||
constructor(webdavConfig: WebdavConfig, vaultName: string) {
|
||||
saveUpdatedConfigFunc: () => Promise<any>;
|
||||
constructor(
|
||||
webdavConfig: WebdavConfig,
|
||||
vaultName: string,
|
||||
saveUpdatedConfigFunc: () => Promise<any>
|
||||
) {
|
||||
this.webdavConfig = webdavConfig;
|
||||
this.vaultName = vaultName;
|
||||
this.vaultFolderExists = false;
|
||||
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
|
||||
}
|
||||
|
||||
init = async () => {
|
||||
@@ -208,14 +215,68 @@ export class WrappedWebdavClient {
|
||||
this.vaultFolderExists = true;
|
||||
}
|
||||
}
|
||||
|
||||
// adjust depth parameter
|
||||
if (this.webdavConfig.depth === "auto_unknown") {
|
||||
let testPassed = false;
|
||||
try {
|
||||
const res = await this.client.customRequest(`/${this.vaultName}`, {
|
||||
method: "PROPFIND",
|
||||
headers: {
|
||||
Depth: "Infinity",
|
||||
},
|
||||
responseType: "text",
|
||||
});
|
||||
if (res.status === 403) {
|
||||
throw Error("not support Infinity, get 403");
|
||||
} else {
|
||||
testPassed = true;
|
||||
this.webdavConfig.depth = "auto_infinity";
|
||||
this.webdavConfig.manualRecursive = false;
|
||||
}
|
||||
} catch (error) {
|
||||
testPassed = false;
|
||||
}
|
||||
if (!testPassed) {
|
||||
try {
|
||||
const res = await this.client.customRequest(`/${this.vaultName}`, {
|
||||
method: "PROPFIND",
|
||||
headers: {
|
||||
Depth: "1",
|
||||
},
|
||||
responseType: "text",
|
||||
});
|
||||
testPassed = true;
|
||||
this.webdavConfig.depth = "auto_1";
|
||||
this.webdavConfig.manualRecursive = true;
|
||||
} catch (error) {
|
||||
testPassed = false;
|
||||
}
|
||||
}
|
||||
if (testPassed) {
|
||||
// the depth option has been changed
|
||||
// save the setting
|
||||
if (this.saveUpdatedConfigFunc !== undefined) {
|
||||
await this.saveUpdatedConfigFunc();
|
||||
log.info(
|
||||
`webdav depth="auto_unknown" is changed to ${this.webdavConfig.depth}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getWebdavClient = (
|
||||
webdavConfig: WebdavConfig,
|
||||
vaultName: string
|
||||
vaultName: string,
|
||||
saveUpdatedConfigFunc: () => Promise<any>
|
||||
) => {
|
||||
return new WrappedWebdavClient(webdavConfig, vaultName);
|
||||
return new WrappedWebdavClient(
|
||||
webdavConfig,
|
||||
vaultName,
|
||||
saveUpdatedConfigFunc
|
||||
);
|
||||
};
|
||||
|
||||
export const getRemoteMeta = async (
|
||||
@@ -318,7 +379,10 @@ export const listFromRemote = async (
|
||||
await client.init();
|
||||
|
||||
let contents = [] as FileStat[];
|
||||
if (client.webdavConfig.manualRecursive) {
|
||||
if (
|
||||
client.webdavConfig.depth === "auto_1" ||
|
||||
client.webdavConfig.depth === "manual_1"
|
||||
) {
|
||||
// the remote doesn't support infinity propfind,
|
||||
// we need to do a bfs here
|
||||
const q = new Queue([`/${client.vaultName}`]);
|
||||
@@ -336,7 +400,10 @@ export const listFromRemote = async (
|
||||
return client.client.getDirectoryContents(x, {
|
||||
deep: false,
|
||||
details: false /* no need for verbose details here */,
|
||||
glob: "/**" /* avoid dot files by using glob */,
|
||||
// TODO: to support .obsidian,
|
||||
// we need to load all files including dot,
|
||||
// anyway to reduce the resources?
|
||||
// glob: "/**" /* avoid dot files by using glob */,
|
||||
}) as Promise<FileStat[]>;
|
||||
});
|
||||
const r2 = flatten(await Promise.all(r));
|
||||
@@ -357,7 +424,10 @@ export const listFromRemote = async (
|
||||
{
|
||||
deep: true,
|
||||
details: false /* no need for verbose details here */,
|
||||
glob: "/**" /* avoid dot files by using glob */,
|
||||
// TODO: to support .obsidian,
|
||||
// we need to load all files including dot,
|
||||
// anyway to reduce the resources?
|
||||
// glob: "/**" /* avoid dot files by using glob */,
|
||||
}
|
||||
)) as FileStat[];
|
||||
}
|
||||
|
||||
+174
-44
@@ -11,6 +11,7 @@ import {
|
||||
API_VER_REQURL,
|
||||
SUPPORTED_SERVICES_TYPE,
|
||||
WebdavAuthType,
|
||||
WebdavDepthType,
|
||||
} from "./baseTypes";
|
||||
import { exportVaultSyncPlansToFiles } from "./debugMode";
|
||||
import { exportQrCodeUri } from "./importExport";
|
||||
@@ -383,6 +384,59 @@ export class OnedriveRevokeAuthModal extends Modal {
|
||||
}
|
||||
}
|
||||
|
||||
class SyncConfigDirModal extends Modal {
|
||||
plugin: RemotelySavePlugin;
|
||||
saveDropdownFunc: () => void;
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: RemotelySavePlugin,
|
||||
saveDropdownFunc: () => void
|
||||
) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.saveDropdownFunc = saveDropdownFunc;
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
let { contentEl } = this;
|
||||
|
||||
const texts = [
|
||||
"Attention 1/3: This only syncs (copies) the whole Obsidian config dir, not other . folders or files. It also doesn't understand the inner structure of the config dir.",
|
||||
"Attention 2/3: After the config dir is synced, plugins settings might be corrupted, and Obsidian might need to be restarted to load the new settings.",
|
||||
"Attention 3/3: The deletion (uninstallation) operations of or inside Obsidian config dir cannot be tracked. So if you want to uninstall a plugin, you need to manually uninstall it on all device, before next sync.",
|
||||
"If you are agreed to take your own risk, please click the following second confirm button.",
|
||||
];
|
||||
for (const t of texts) {
|
||||
contentEl.createEl("p", {
|
||||
text: t,
|
||||
});
|
||||
}
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton((button) => {
|
||||
button.setButtonText("The Second Confirm To Enable.");
|
||||
button.onClick(async () => {
|
||||
this.plugin.settings.syncConfigDir = true;
|
||||
await this.plugin.saveSettings();
|
||||
this.saveDropdownFunc();
|
||||
new Notice("You've enabled syncing config folder!");
|
||||
this.close();
|
||||
});
|
||||
})
|
||||
.addButton((button) => {
|
||||
button.setButtonText("Go Back");
|
||||
button.onClick(() => {
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
let { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
class ExportSettingsQrCodeModal extends Modal {
|
||||
plugin: RemotelySavePlugin;
|
||||
constructor(app: App, plugin: RemotelySavePlugin) {
|
||||
@@ -551,30 +605,6 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
});
|
||||
});
|
||||
|
||||
const concurrencyDiv = generalDiv.createEl("div");
|
||||
new Setting(concurrencyDiv)
|
||||
.setName("Concurrency")
|
||||
.setDesc(
|
||||
"How many files do you want to download or upload in parallel at most? By default it's set to 5. If you meet any problems such as rate limit, you can reduce the concurrency to a lower value."
|
||||
)
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown.addOption("1", "1");
|
||||
dropdown.addOption("2", "2");
|
||||
dropdown.addOption("3", "3");
|
||||
dropdown.addOption("5", "5 (default)");
|
||||
dropdown.addOption("10", "10");
|
||||
dropdown.addOption("15", "15");
|
||||
dropdown.addOption("20", "20");
|
||||
|
||||
dropdown
|
||||
.setValue(`${this.plugin.settings.concurrency}`)
|
||||
.onChange(async (val) => {
|
||||
const realVal = parseInt(val);
|
||||
this.plugin.settings.concurrency = realVal;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// below for general chooser (part 1/2)
|
||||
//////////////////////////////////////////////////
|
||||
@@ -1073,29 +1103,41 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
});
|
||||
|
||||
new Setting(webdavDiv)
|
||||
.setName("server supports infinity propfind or not")
|
||||
.setName("depth header sent to servers")
|
||||
.setDesc(
|
||||
"The plugin needs to get all files and folders recursively using probfind. If your webdav server only supports depth='1' (such as NGINX), you need to adjust the setting here, then the plugin consumes more network requests, but better than not working."
|
||||
"Webdav servers should be configured to allow requests with header Depth being '1' or 'Infinity'. The plugin needs to know this info. If you are not sure what's this, choose \"auto\"."
|
||||
)
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown.addOption("infinity", "supports depth='infinity'");
|
||||
dropdown.addOption("1", "only supports depth='1'");
|
||||
dropdown.addOption("auto", "auto detect");
|
||||
dropdown.addOption("manual_1", "only supports depth='1'");
|
||||
dropdown.addOption("manual_infinity", "only supports depth='infinity'");
|
||||
|
||||
type Depth = "1" | "infinity";
|
||||
dropdown
|
||||
.setValue(
|
||||
this.plugin.settings.webdav.manualRecursive === false
|
||||
? "infinity"
|
||||
: "1"
|
||||
)
|
||||
.onChange(async (val: Depth) => {
|
||||
if (val === "1") {
|
||||
this.plugin.settings.webdav.manualRecursive = true;
|
||||
} else if (val === "infinity") {
|
||||
this.plugin.settings.webdav.manualRecursive = false;
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
let initVal = "auto";
|
||||
const autoOptions: Set<WebdavDepthType> = new Set([
|
||||
"auto_unknown",
|
||||
"auto_1",
|
||||
"auto_infinity",
|
||||
]);
|
||||
if (autoOptions.has(this.plugin.settings.webdav.depth)) {
|
||||
initVal = "auto";
|
||||
} else {
|
||||
initVal = this.plugin.settings.webdav.depth || "auto";
|
||||
}
|
||||
|
||||
type DepthOption = "auto" | "manual_1" | "manual_infinity";
|
||||
dropdown.setValue(initVal).onChange(async (val: DepthOption) => {
|
||||
if (val === "auto") {
|
||||
this.plugin.settings.webdav.depth = "auto_unknown";
|
||||
this.plugin.settings.webdav.manualRecursive = false;
|
||||
} else if (val === "manual_1") {
|
||||
this.plugin.settings.webdav.depth = "manual_1";
|
||||
this.plugin.settings.webdav.manualRecursive = true;
|
||||
} else if (val === "manual_infinity") {
|
||||
this.plugin.settings.webdav.depth = "manual_infinity";
|
||||
this.plugin.settings.webdav.manualRecursive = false;
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(webdavDiv)
|
||||
@@ -1105,13 +1147,15 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
button.setButtonText("Check");
|
||||
button.onClick(async () => {
|
||||
new Notice("Checking...");
|
||||
const self = this;
|
||||
const client = new RemoteClient(
|
||||
"webdav",
|
||||
undefined,
|
||||
this.plugin.settings.webdav,
|
||||
undefined,
|
||||
undefined,
|
||||
this.app.vault.getName()
|
||||
this.app.vault.getName(),
|
||||
() => self.plugin.saveSettings()
|
||||
);
|
||||
const res = await client.checkConnectivity();
|
||||
if (res) {
|
||||
@@ -1167,6 +1211,92 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
});
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// below for advanced settings
|
||||
//////////////////////////////////////////////////
|
||||
const advDiv = containerEl.createEl("div");
|
||||
advDiv.createEl("h2", {
|
||||
text: "Advanced Settings",
|
||||
});
|
||||
|
||||
const concurrencyDiv = advDiv.createEl("div");
|
||||
new Setting(concurrencyDiv)
|
||||
.setName("Concurrency")
|
||||
.setDesc(
|
||||
"How many files do you want to download or upload in parallel at most? By default it's set to 5. If you meet any problems such as rate limit, you can reduce the concurrency to a lower value."
|
||||
)
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown.addOption("1", "1");
|
||||
dropdown.addOption("2", "2");
|
||||
dropdown.addOption("3", "3");
|
||||
dropdown.addOption("5", "5 (default)");
|
||||
dropdown.addOption("10", "10");
|
||||
dropdown.addOption("15", "15");
|
||||
dropdown.addOption("20", "20");
|
||||
|
||||
dropdown
|
||||
.setValue(`${this.plugin.settings.concurrency}`)
|
||||
.onChange(async (val) => {
|
||||
const realVal = parseInt(val);
|
||||
this.plugin.settings.concurrency = realVal;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
const syncUnderscoreItemsDiv = advDiv.createEl("div");
|
||||
new Setting(syncUnderscoreItemsDiv)
|
||||
.setName("sync _ files or folders")
|
||||
.setDesc(`Sync files or folders startting with _ ("underscore") or not.`)
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown.addOption("disable", "disable");
|
||||
dropdown.addOption("enable", "enable");
|
||||
dropdown
|
||||
.setValue(
|
||||
`${this.plugin.settings.syncUnderscoreItems ? "enable" : "disable"}`
|
||||
)
|
||||
.onChange(async (val) => {
|
||||
this.plugin.settings.syncUnderscoreItems = val === "enable";
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
const syncConfigDirDiv = advDiv.createEl("div");
|
||||
new Setting(syncConfigDirDiv)
|
||||
.setName("sync config dir (experimental)")
|
||||
.setDesc(
|
||||
`Sync config dir ${this.app.vault.configDir} or not. Please be aware that this may impact all your plugins' or Obsidian's settings, and may require you restart Obsidian after sync. Enable this at your own risk.`
|
||||
)
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown.addOption("disable", "disable");
|
||||
dropdown.addOption("enable", "enable");
|
||||
|
||||
const bridge = {
|
||||
secondConfirm: false,
|
||||
};
|
||||
dropdown
|
||||
.setValue(
|
||||
`${this.plugin.settings.syncConfigDir ? "enable" : "disable"}`
|
||||
)
|
||||
.onChange(async (val) => {
|
||||
if (val === "enable" && !bridge.secondConfirm) {
|
||||
dropdown.setValue("disable");
|
||||
const modal = new SyncConfigDirModal(
|
||||
this.app,
|
||||
this.plugin,
|
||||
() => {
|
||||
bridge.secondConfirm = true;
|
||||
dropdown.setValue("enable");
|
||||
}
|
||||
);
|
||||
modal.open();
|
||||
} else {
|
||||
bridge.secondConfirm = false;
|
||||
this.plugin.settings.syncConfigDir = false;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// below for import and export functions
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
+86
-33
@@ -46,6 +46,7 @@ import {
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
import { padEnd } from "lodash";
|
||||
import { isInsideObsFolder, ObsConfigDirFileType } from "./obsFolderLister";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
export type SyncStatusType =
|
||||
@@ -272,18 +273,39 @@ export const fetchMetadataFile = async (
|
||||
return metadata;
|
||||
};
|
||||
|
||||
const isSkipItem = (
|
||||
key: string,
|
||||
syncConfigDir: boolean,
|
||||
syncUnderscoreItems: boolean,
|
||||
configDir: string
|
||||
) => {
|
||||
if (syncConfigDir && isInsideObsFolder(key, configDir)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
isHiddenPath(key, true, false) ||
|
||||
(!syncUnderscoreItems && isHiddenPath(key, false, true)) ||
|
||||
key === DEFAULT_FILE_NAME_FOR_METADATAONREMOTE ||
|
||||
key === DEFAULT_FILE_NAME_FOR_METADATAONREMOTE2
|
||||
);
|
||||
};
|
||||
|
||||
const ensembleMixedStates = async (
|
||||
remoteStates: FileOrFolderMixedState[],
|
||||
local: TAbstractFile[],
|
||||
localConfigDirContents: ObsConfigDirFileType[] | undefined,
|
||||
remoteDeleteHistory: DeletionOnRemote[],
|
||||
localDeleteHistory: FileFolderHistoryRecord[]
|
||||
localDeleteHistory: FileFolderHistoryRecord[],
|
||||
syncConfigDir: boolean,
|
||||
configDir: string,
|
||||
syncUnderscoreItems: boolean
|
||||
) => {
|
||||
const results = {} as Record<string, FileOrFolderMixedState>;
|
||||
|
||||
for (const r of remoteStates) {
|
||||
const key = r.key;
|
||||
|
||||
if (isHiddenPath(key)) {
|
||||
if (isSkipItem(key, syncConfigDir, syncUnderscoreItems, configDir)) {
|
||||
continue;
|
||||
}
|
||||
results[key] = r;
|
||||
@@ -316,9 +338,10 @@ const ensembleMixedStates = async (
|
||||
throw Error(`unexpected ${entry}`);
|
||||
}
|
||||
|
||||
if (isHiddenPath(key)) {
|
||||
if (isSkipItem(key, syncConfigDir, syncUnderscoreItems, configDir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (results.hasOwnProperty(key)) {
|
||||
results[key].key = r.key;
|
||||
results[key].existLocal = r.existLocal;
|
||||
@@ -330,6 +353,28 @@ const ensembleMixedStates = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (syncConfigDir && localConfigDirContents !== undefined) {
|
||||
for (const entry of localConfigDirContents) {
|
||||
const key = entry.key;
|
||||
const r: FileOrFolderMixedState = {
|
||||
key: key,
|
||||
existLocal: true,
|
||||
mtimeLocal: Math.max(entry.mtime, entry.ctime),
|
||||
sizeLocal: entry.size,
|
||||
};
|
||||
|
||||
if (results.hasOwnProperty(key)) {
|
||||
results[key].key = r.key;
|
||||
results[key].existLocal = r.existLocal;
|
||||
results[key].mtimeLocal = r.mtimeLocal;
|
||||
results[key].sizeLocal = r.sizeLocal;
|
||||
} else {
|
||||
results[key] = r;
|
||||
results[key].existRemote = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of remoteDeleteHistory) {
|
||||
const key = entry.key;
|
||||
const r = {
|
||||
@@ -337,6 +382,10 @@ const ensembleMixedStates = async (
|
||||
deltimeRemote: entry.actionWhen,
|
||||
} as FileOrFolderMixedState;
|
||||
|
||||
if (isSkipItem(key, syncConfigDir, syncUnderscoreItems, configDir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (results.hasOwnProperty(key)) {
|
||||
results[key].key = r.key;
|
||||
results[key].deltimeRemote = r.deltimeRemote;
|
||||
@@ -365,9 +414,10 @@ const ensembleMixedStates = async (
|
||||
deltimeLocal: entry.actionWhen,
|
||||
} as FileOrFolderMixedState;
|
||||
|
||||
if (isHiddenPath(key)) {
|
||||
if (isSkipItem(key, syncConfigDir, syncUnderscoreItems, configDir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (results.hasOwnProperty(key)) {
|
||||
results[key].key = r.key;
|
||||
results[key].deltimeLocal = r.deltimeLocal;
|
||||
@@ -540,39 +590,34 @@ const assignOperationToFolderInplace = async (
|
||||
// if it was created after deletion, we should keep it as is
|
||||
if (requireApiVersion(API_VER_STAT_FOLDER)) {
|
||||
if (r.existLocal) {
|
||||
try {
|
||||
const { ctime, mtime } = await vault.adapter.stat(r.key);
|
||||
const cmtime = Math.max(ctime, mtime);
|
||||
if (
|
||||
cmtime > 0 &&
|
||||
cmtime >= deltimeLocal &&
|
||||
cmtime >= deltimeRemote
|
||||
) {
|
||||
keptFolder.add(getParentFolder(r.key));
|
||||
if (r.existLocal && r.existRemote) {
|
||||
r.decision = "skipFolder";
|
||||
r.decisionBranch = 14;
|
||||
} else if (r.existLocal || r.existRemote) {
|
||||
r.decision = "createFolder";
|
||||
r.decisionBranch = 15;
|
||||
} else {
|
||||
throw Error(
|
||||
`Error: Folder ${r.key} doesn't exist locally and remotely but is marked must be kept. Abort.`
|
||||
);
|
||||
}
|
||||
const { ctime, mtime } = await vault.adapter.stat(r.key);
|
||||
const cmtime = Math.max(ctime, mtime);
|
||||
if (cmtime > 0 && cmtime >= deltimeLocal && cmtime >= deltimeRemote) {
|
||||
keptFolder.add(getParentFolder(r.key));
|
||||
if (r.existLocal && r.existRemote) {
|
||||
r.decision = "skipFolder";
|
||||
r.decisionBranch = 14;
|
||||
} else if (r.existLocal || r.existRemote) {
|
||||
r.decision = "createFolder";
|
||||
r.decisionBranch = 15;
|
||||
} else {
|
||||
throw Error(
|
||||
`Error: Folder ${r.key} doesn't exist locally and remotely but is marked must be kept. Abort.`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deltimeLocal > 0 && deltimeLocal > deltimeRemote) {
|
||||
r.decision = "uploadLocalDelHistToRemoteFolder";
|
||||
r.decisionBranch = 8;
|
||||
} else {
|
||||
r.decision = "keepRemoteDelHistFolder";
|
||||
r.decisionBranch = 9;
|
||||
if (r.decision === undefined) {
|
||||
// not yet decided by the above reason
|
||||
if (deltimeLocal > 0 && deltimeLocal > deltimeRemote) {
|
||||
r.decision = "uploadLocalDelHistToRemoteFolder";
|
||||
r.decisionBranch = 8;
|
||||
} else {
|
||||
r.decision = "keepRemoteDelHistFolder";
|
||||
r.decisionBranch = 9;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// it does not have any deletion commands
|
||||
@@ -623,17 +668,25 @@ const DELETION_DECISIONS: Set<DecisionType> = new Set([
|
||||
export const getSyncPlan = async (
|
||||
remoteStates: FileOrFolderMixedState[],
|
||||
local: TAbstractFile[],
|
||||
localConfigDirContents: ObsConfigDirFileType[] | undefined,
|
||||
remoteDeleteHistory: DeletionOnRemote[],
|
||||
localDeleteHistory: FileFolderHistoryRecord[],
|
||||
remoteType: SUPPORTED_SERVICES_TYPE,
|
||||
vault: Vault,
|
||||
syncConfigDir: boolean,
|
||||
configDir: string,
|
||||
syncUnderscoreItems: boolean,
|
||||
password: string = ""
|
||||
) => {
|
||||
const mixedStates = await ensembleMixedStates(
|
||||
remoteStates,
|
||||
local,
|
||||
localConfigDirContents,
|
||||
remoteDeleteHistory,
|
||||
localDeleteHistory
|
||||
localDeleteHistory,
|
||||
syncConfigDir,
|
||||
configDir,
|
||||
syncUnderscoreItems
|
||||
);
|
||||
|
||||
const sortedKeys = Object.keys(mixedStates).sort(
|
||||
|
||||
+14
-3
@@ -21,7 +21,7 @@ describe("Misc: hidden file", () => {
|
||||
|
||||
item = "_hidden_loose";
|
||||
expect(misc.isHiddenPath(item)).to.be.true;
|
||||
expect(misc.isHiddenPath(item, false)).to.be.false;
|
||||
expect(misc.isHiddenPath(item, true, false)).to.be.false;
|
||||
|
||||
item = "/sdd/_hidden_loose";
|
||||
expect(misc.isHiddenPath(item)).to.be.true;
|
||||
@@ -30,10 +30,21 @@ describe("Misc: hidden file", () => {
|
||||
expect(misc.isHiddenPath(item)).to.be.true;
|
||||
|
||||
item = "what/../_hidden_loose/what/what/what";
|
||||
expect(misc.isHiddenPath(item, false)).to.be.false;
|
||||
expect(misc.isHiddenPath(item, true, false)).to.be.false;
|
||||
|
||||
item = "what/../_hidden_loose/../.hidden/what/what/what";
|
||||
expect(misc.isHiddenPath(item, false)).to.be.true;
|
||||
expect(misc.isHiddenPath(item, true, false)).to.be.true;
|
||||
|
||||
item = "what/../_hidden_loose/../.hidden/what/what/what";
|
||||
expect(misc.isHiddenPath(item, false, true)).to.be.false;
|
||||
|
||||
item = "what/_hidden_loose/what/what/what";
|
||||
expect(misc.isHiddenPath(item, false, true)).to.be.true;
|
||||
expect(misc.isHiddenPath(item, true, false)).to.be.false;
|
||||
|
||||
item = "what/.hidden/what/what/what";
|
||||
expect(misc.isHiddenPath(item, false, true)).to.be.false;
|
||||
expect(misc.isHiddenPath(item, true, false)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"0.3.2": "0.12.15",
|
||||
"0.3.5": "0.13.21"
|
||||
"0.3.6": "0.13.21"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user