Compare commits

..
17 Commits
Author SHA1 Message Date
fyears 48e5601a52 bump to 0.3.20
Release A New Version / build (16.x) (push) Failing after 37s
2022-04-30 15:45:23 +08:00
fyears 054a9a3c01 add more verbose hint about the json 2022-04-30 15:40:56 +08:00
fyears 66dcaf4e4c add debug log 2022-04-30 15:23:16 +08:00
fyears 6d29554033 force refresh req every time for s3 2022-04-30 15:22:52 +08:00
fyears 4ed0d735a2 update all packages to latest 2022-04-30 14:22:34 +08:00
fyears 26dff02daa format 2022-04-30 11:59:38 +08:00
fyears 1b1083d746 better esbuild 2022-04-30 11:59:18 +08:00
fyears 6db005018d Merge branch 'moritzlang-add-apache-example-conf' 2022-04-27 23:39:26 +08:00
fyears 8d25ddb636 Merge branch 'add-apache-example-conf' of https://github.com/moritzlang/remotely-save into moritzlang-add-apache-example-conf 2022-04-27 23:39:03 +08:00
fyears b0d41478a3 Merge branch 'n0k0m3-master' 2022-04-27 23:32:27 +08:00
fyears f69adf273a add special hints 2022-04-27 23:32:15 +08:00
n0k0m3 4b4a9b0989 Add CORS header guide for Nextcloud/Owncloud 2022-04-18 11:22:23 -04:00
fyears bfe11f44dc make more s3 req to lower 2022-04-18 21:30:02 +08:00
fyears d25303cfac update s3 pack 2022-04-18 21:29:38 +08:00
fyears ebd62b1581 new s3 every request to avoid request expired 2022-04-18 00:56:25 +08:00
fyears 13537d1481 insert dry run into plan records 2022-04-18 00:22:00 +08:00
Moritz 2eda5618df Add apache example configuration 2022-02-20 22:54:30 +01:00
14 changed files with 153 additions and 74 deletions
+2
View File
@@ -92,6 +92,8 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
- 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.
- 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.
- **Unofficial** workaround: NextCloud users can **evaluate the risk by themselves**, and if decide to accept the risk, they can install [WebAppPassword](https://apps.nextcloud.com/apps/webapppassword) app, and add `app://obsidian.md`, `capacitor://localhost`, `http://localhost` to `Allowed origins`
- **Unofficial** workaround: OwnCloud users can **evaluate the risk by themselves**, and if decide to accept the risk, they can download `.tar.gz` of `WebAppPassword` above and manually install and configure it on their instances.
- 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**.
+25
View File
@@ -0,0 +1,25 @@
# How To Configure Apache CORS Rules for Webdav
This method is [contributed by community](https://github.com/remotely-save/remotely-save/pull/31).
You should evaluate the risk by yourself before trying this method.
**You are strongly advised to backup your original Apache configuration files before making any changes.**
```apacheconf
<IfModule mod_headers.c>
# Obsidian webdav
SetEnvIf Origin "^app://obsidian.md$" IS_OBSIDIAN
SetEnvIf Origin "^capacitor://localhost$" IS_OBSIDIAN
SetEnvIf Origin "^http://localhost$" IS_OBSIDIAN
Header always set Access-Control-Allow-Origin "*" env=IS_OBSIDIAN
Header always set Access-Control-Allow-Methods "GET, HEAD, POST, PUT, OPTIONS, MOVE, DELETE, COPY, LOCK, UNLOCK, PROPFIND" env=IS_OBSIDIAN
Header always set Access-Control-Allow-Headers "Authorization, Depth, DNT, User-Agent, Keep-Alive, Content-Type, accept, origin, X-Requested-With" env=IS_OBSIDIAN
Header always set Access-Control-Expose-Headers "etag, dav" env=IS_OBSIDIAN
# Allow OPTION request without authentication and respond with status 200
RewriteCond %{ENV:IS_OBSIDIAN} 1
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=200,L]
</IfModule>
```
+7 -2
View File
@@ -4,6 +4,11 @@ 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)
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.
1. 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 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).
2. 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`.
Example configurations:
- [Amazon S3](./s3_cors_configure.md)
- [Apache](./apache_cors_configure.md) ([contributed by community](https://github.com/remotely-save/remotely-save/pull/31))
+4 -1
View File
@@ -31,11 +31,11 @@ esbuild
"obsidian",
"electron",
"fs",
"crypto",
"tls",
"net",
// ...builtins
],
inject: ["./esbuild.injecthelper.mjs"],
format: "cjs",
watch: !prod,
target: "es2016",
@@ -48,6 +48,9 @@ esbuild
"process.env.DEFAULT_DROPBOX_APP_KEY": `"${DEFAULT_DROPBOX_APP_KEY}"`,
"process.env.DEFAULT_ONEDRIVE_CLIENT_ID": `"${DEFAULT_ONEDRIVE_CLIENT_ID}"`,
"process.env.DEFAULT_ONEDRIVE_AUTHORITY": `"${DEFAULT_ONEDRIVE_AUTHORITY}"`,
global: "window",
"process.env.NODE_DEBUG": `undefined`, // ugly fix
"process.env.DEBUG": `undefined`, // ugly fix
},
})
.catch(() => process.exit(1));
+2
View File
@@ -0,0 +1,2 @@
export let Buffer = require("buffer").Buffer;
export let process = require("process/browser");
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "remotely-save",
"name": "Remotely Save",
"version": "0.3.19",
"version": "0.3.20",
"minAppVersion": "0.13.21",
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
"author": "fyears",
+39 -39
View File
@@ -1,6 +1,6 @@
{
"name": "remotely-save",
"version": "0.3.19",
"version": "0.3.20",
"description": "This is yet another sync plugin for Obsidian app.",
"scripts": {
"dev2": "node esbuild.config.mjs",
@@ -23,73 +23,73 @@
"author": "",
"license": "Apache-2.0",
"devDependencies": {
"@microsoft/microsoft-graph-types": "^2.11.0",
"@types/chai": "^4.2.22",
"@types/chai-as-promised": "^7.1.4",
"@types/jsdom": "^16.2.13",
"@types/lodash": "^4.14.178",
"@microsoft/microsoft-graph-types": "^2.19.0",
"@types/chai": "^4.3.1",
"@types/chai-as-promised": "^7.1.5",
"@types/jsdom": "^16.2.14",
"@types/lodash": "^4.14.182",
"@types/mime-types": "^2.1.1",
"@types/mocha": "^9.0.0",
"@types/mocha": "^9.1.1",
"@types/mustache": "^4.1.2",
"@types/node": "^14.14.37",
"@types/qrcode": "^1.4.1",
"@types/node": "^17.0.30",
"@types/qrcode": "^1.4.2",
"builtin-modules": "^3.2.0",
"chai": "^4.3.4",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"cross-env": "^7.0.3",
"dotenv": "^10.0.0",
"esbuild": "^0.14.27",
"dotenv": "^16.0.0",
"esbuild": "^0.14.38",
"jsdom": "^19.0.0",
"mocha": "^9.1.3",
"prettier": "^2.4.1",
"ts-loader": "^9.2.6",
"ts-node": "^10.4.0",
"tslib": "^2.2.0",
"typescript": "^4.4.4",
"mocha": "^9.2.2",
"prettier": "^2.6.2",
"ts-loader": "^9.2.9",
"ts-node": "^10.7.0",
"tslib": "^2.4.0",
"typescript": "^4.6.4",
"webdav-server": "^2.6.2",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1"
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.67.0",
"@aws-sdk/fetch-http-handler": "^3.58.0",
"@aws-sdk/lib-storage": "^3.67.0",
"@aws-sdk/protocol-http": "^3.58.0",
"@aws-sdk/querystring-builder": "^3.55.0",
"@aws-sdk/signature-v4-crt": "^3.66.0",
"@aws-sdk/types": "^3.55.0",
"@azure/msal-node": "^1.6.0",
"@aws-sdk/client-s3": "^3.81.0",
"@aws-sdk/fetch-http-handler": "^3.78.0",
"@aws-sdk/lib-storage": "^3.81.0",
"@aws-sdk/protocol-http": "^3.78.0",
"@aws-sdk/querystring-builder": "^3.78.0",
"@aws-sdk/signature-v4-crt": "^3.78.0",
"@aws-sdk/types": "^3.78.0",
"@azure/msal-node": "^1.8.0",
"@fyears/tsqueue": "^1.0.1",
"@microsoft/microsoft-graph-client": "^3.0.1",
"acorn": "^8.5.0",
"@microsoft/microsoft-graph-client": "^3.0.2",
"acorn": "^8.7.1",
"aggregate-error": "^4.0.0",
"assert": "^2.0.0",
"aws-crt": "^1.10.1",
"aws-crt": "^1.12.1",
"buffer": "^6.0.3",
"crypto-browserify": "^3.12.0",
"delay": "^5.0.0",
"dropbox": "^10.22.0",
"dropbox": "^10.28.0",
"emoji-regex": "^10.1.0",
"http-status-codes": "^2.2.0",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"loglevel": "^1.8.0",
"lucide": "^0.17.12",
"mime-types": "^2.1.33",
"lucide": "^0.35.0",
"mime-types": "^2.1.35",
"mustache": "^4.2.0",
"nanoid": "^3.1.30",
"obsidian": "^0.13.26",
"nanoid": "^3.3.3",
"obsidian": "^0.14.6",
"p-queue": "^7.2.0",
"path-browserify": "^1.0.1",
"process": "^0.11.10",
"qrcode": "^1.5.0",
"rfc4648": "^1.5.0",
"rfc4648": "^1.5.1",
"rimraf": "^3.0.2",
"stream-browserify": "^3.0.0",
"url": "^0.11.0",
"util": "^0.12.4",
"webdav": "^4.7.0",
"webdav-fs": "^4.0.0",
"webdav": "^4.9.0",
"webdav-fs": "^4.0.1",
"xregexp": "^5.1.0"
}
}
+2
View File
@@ -165,3 +165,5 @@ export const DEFAULT_DEBUG_FOLDER = "_debug_remotely_save/";
export const DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX =
"sync_plans_hist_exported_on_";
export const DEFAULT_LOG_HISTORY_FILE_PREFIX = "log_hist_exported_on_";
export type SyncTriggerSourceType = "manual" | "auto" | "dry" | "autoOnceInit";
+5 -5
View File
@@ -9,7 +9,10 @@ import {
} from "obsidian";
import cloneDeep from "lodash/cloneDeep";
import { createElement, RotateCcw, RefreshCcw, FileText } from "lucide";
import type { RemotelySavePluginSettings } from "./baseTypes";
import type {
RemotelySavePluginSettings,
SyncTriggerSourceType,
} from "./baseTypes";
import {
COMMAND_CALLBACK,
COMMAND_CALLBACK_ONEDRIVE,
@@ -89,8 +92,6 @@ interface OAuth2Info {
revokeAuthSetting?: Setting;
}
type SyncTriggerSourceType = "manual" | "auto" | "dry" | "autoOnceInit";
const iconNameSyncWait = `remotely-save-sync-wait`;
const iconNameSyncRunning = `remotely-save-sync-running`;
const iconNameLogs = `remotely-save-logs`;
@@ -286,6 +287,7 @@ export default class RemotelySavePlugin extends Plugin {
origMetadataOnRemote.deletions,
localHistory,
client.serviceType,
triggerSource,
this.app.vault,
this.settings.syncConfigDir,
this.app.vault.configDir,
@@ -293,9 +295,7 @@ export default class RemotelySavePlugin extends Plugin {
this.settings.password
);
log.info(plan.mixedStates); // for debugging
if (triggerSource !== "dry") {
await insertSyncPlanRecordByVault(this.db, plan, this.vaultRandomID);
}
// The operations above are almost read only and kind of safe.
// The operations below begins to write or delete (!!!) something.
+34 -8
View File
@@ -1,6 +1,7 @@
import isEqual from "lodash/isEqual";
import { base64url } from "rfc4648";
import { reverseString } from "./misc";
import { log } from "./moreOnLog";
const DEFAULT_README_FOR_METADATAONREMOTE =
"Do NOT edit or delete the file manually. This file is for the plugin remotely-save to store some necessary meta data on the remote services. Its content is slightly obfuscated.";
@@ -69,19 +70,44 @@ export const deserializeMetadataOnRemote = (x: string | ArrayBuffer) => {
} else {
y1 = new TextDecoder().decode(x);
}
const y2: any = JSON.parse(y1);
if (!("readme" in y2 && "d" in y2)) {
throw Error("invalid remote meta data file!");
let y2: any;
try {
y2 = JSON.parse(y1);
} catch (e) {
throw new Error(
`invalid remote meta data file with first few chars: ${y1.slice(0, 5)}`
);
}
const y3 = JSON.parse(
(
if (!("readme" in y2 && "d" in y2)) {
throw new Error(
'invalid remote meta data file (no "readme" or "d" fields)!'
);
}
let y3: string;
try {
y3 = (
base64url.parse(reverseString(y2["d"]), {
out: Buffer.allocUnsafe as any,
loose: true,
}) as Buffer
).toString("utf-8")
) as MetadataOnRemote;
return y3;
).toString("utf-8");
} catch (e) {
throw new Error('invalid remote meta data file (invalid "d" field)!');
}
let y4: MetadataOnRemote;
try {
y4 = JSON.parse(y3) as MetadataOnRemote;
} catch (e) {
throw new Error(
`invalid remote meta data file with \"d\" field with first few chars: ${y3.slice(
0,
5
)}`
);
}
return y4;
};
+10 -8
View File
@@ -15,7 +15,6 @@ import { log } from "./moreOnLog";
export class RemoteClient {
readonly serviceType: SUPPORTED_SERVICES_TYPE;
readonly s3Client?: s3.S3Client;
readonly s3Config?: S3Config;
readonly webdavClient?: webdav.WrappedWebdavClient;
readonly webdavConfig?: WebdavConfig;
@@ -38,7 +37,6 @@ export class RemoteClient {
// so we use a ref not copy of config here
if (serviceType === "s3") {
this.s3Config = s3Config;
this.s3Client = s3.getS3Client(this.s3Config);
} else if (serviceType === "webdav") {
if (vaultName === undefined || saveUpdatedConfigFunc === undefined) {
throw Error(
@@ -86,7 +84,7 @@ export class RemoteClient {
getRemoteMeta = async (fileOrFolderPath: string) => {
if (this.serviceType === "s3") {
return await s3.getRemoteMeta(
this.s3Client,
s3.getS3Client(this.s3Config),
this.s3Config,
fileOrFolderPath
);
@@ -116,7 +114,7 @@ export class RemoteClient {
) => {
if (this.serviceType === "s3") {
return await s3.uploadToRemote(
this.s3Client,
s3.getS3Client(this.s3Config),
this.s3Config,
fileOrFolderPath,
vault,
@@ -168,7 +166,11 @@ export class RemoteClient {
listFromRemote = async (prefix?: string) => {
if (this.serviceType === "s3") {
return await s3.listFromRemote(this.s3Client, this.s3Config, prefix);
return await s3.listFromRemote(
s3.getS3Client(this.s3Config),
this.s3Config,
prefix
);
} else if (this.serviceType === "webdav") {
return await webdav.listFromRemote(this.webdavClient, prefix);
} else if (this.serviceType === "dropbox") {
@@ -190,7 +192,7 @@ export class RemoteClient {
) => {
if (this.serviceType === "s3") {
return await s3.downloadFromRemote(
this.s3Client,
s3.getS3Client(this.s3Config),
this.s3Config,
fileOrFolderPath,
vault,
@@ -241,7 +243,7 @@ export class RemoteClient {
) => {
if (this.serviceType === "s3") {
return await s3.deleteFromRemote(
this.s3Client,
s3.getS3Client(this.s3Config),
this.s3Config,
fileOrFolderPath,
password,
@@ -276,7 +278,7 @@ export class RemoteClient {
checkConnectivity = async (callbackFunc?: any) => {
if (this.serviceType === "s3") {
return await s3.checkConnectivity(
this.s3Client,
s3.getS3Client(this.s3Config),
this.s3Config,
callbackFunc
);
+13 -5
View File
@@ -83,17 +83,25 @@ class ObsHttpHandler extends FetchHttpHandler {
const body =
method === "GET" || method === "HEAD" ? undefined : request.body;
const transformedHeaders = { ...request.headers };
delete transformedHeaders["host"];
delete transformedHeaders["Host"];
delete transformedHeaders["content-length"];
delete transformedHeaders["Content-Length"];
const transformedHeaders: Record<string, string> = {};
for (const key of Object.keys(request.headers)) {
const keyLower = key.toLowerCase();
if (keyLower === "host" || keyLower === "content-length") {
continue;
}
transformedHeaders[keyLower] = request.headers[key];
}
let contentType: string = undefined;
if (transformedHeaders["content-type"] !== undefined) {
contentType = transformedHeaders["content-type"];
}
if (transformedHeaders["cache-control"] === undefined) {
// every time is a new request
transformedHeaders["cache-control"] = "no-cache";
}
let transformedBody: any = body;
if (ArrayBuffer.isView(body)) {
transformedBody = bufferToArrayBuffer(body);
+7 -3
View File
@@ -7,13 +7,14 @@ import {
} from "obsidian";
import AggregateError from "aggregate-error";
import PQueue from "p-queue";
import {
import type {
RemoteItem,
SUPPORTED_SERVICES_TYPE,
SyncTriggerSourceType,
DecisionType,
FileOrFolderMixedState,
API_VER_STAT_FOLDER,
SUPPORTED_SERVICES_TYPE,
} from "./baseTypes";
import { API_VER_STAT_FOLDER } from "./baseTypes";
import {
decryptBase32ToString,
decryptBase64urlToString,
@@ -65,6 +66,7 @@ export type SyncStatusType =
export interface SyncPlanType {
ts: number;
tsFmt?: string;
syncTriggerSource?: SyncTriggerSourceType;
remoteType: SUPPORTED_SERVICES_TYPE;
mixedStates: Record<string, FileOrFolderMixedState>;
}
@@ -752,6 +754,7 @@ export const getSyncPlan = async (
remoteDeleteHistory: DeletionOnRemote[],
localFileHistory: FileFolderHistoryRecord[],
remoteType: SUPPORTED_SERVICES_TYPE,
triggerSource: SyncTriggerSourceType,
vault: Vault,
syncConfigDir: boolean,
configDir: string,
@@ -824,6 +827,7 @@ export const getSyncPlan = async (
ts: currTs,
tsFmt: currTsFmt,
remoteType: remoteType,
syncTriggerSource: triggerSource,
mixedStates: mixedStates,
} as SyncPlanType;
return {
+1 -1
View File
@@ -1,4 +1,4 @@
{
"0.3.2": "0.12.15",
"0.3.19": "0.13.21"
"0.3.20": "0.13.21"
}