Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3eb2166aa | ||
|
|
7ecce29940 | ||
|
|
581c6237cc | ||
|
|
257c995090 | ||
|
|
fd095a9093 | ||
|
|
acc9233611 | ||
|
|
4456db11cf | ||
|
|
54449dd207 | ||
|
|
822941f1c3 |
@@ -92,6 +92,7 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
|||||||
- If you decide to authorize this plugin to connect to OneDrive, please go to plugin's settings, and choose OneDrive then follow the instructions.
|
- If you decide to authorize this plugin to connect to OneDrive, please go to plugin's settings, and choose OneDrive then follow the instructions.
|
||||||
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
||||||
- If you want to sync the files across multiple devices, **your vault name should be the same** while using default settings.
|
- If you want to sync the files across multiple devices, **your vault name should be the same** while using default settings.
|
||||||
|
- You might also want to checkout [faq for OneDrive](./docs/remote_services/onedrive/README.md).
|
||||||
|
|
||||||
### webdav
|
### webdav
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# OneDrive
|
||||||
|
|
||||||
|
- **This plugin is NOT an official Microsoft / OneDrive product.** The plugin just uses Microsoft's [OneDrive's public API](https://docs.microsoft.com/en-us/onedrive/developer/rest-api).
|
||||||
|
- After the authorization, the plugin can read your name and email, and read and write files in your OneDrive's `/Apps/remotely-save` folder.
|
||||||
|
- If you decide to authorize this plugin to connect to OneDrive, please go to plugin's settings, and choose OneDrive then follow the instructions.
|
||||||
|
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
||||||
|
- If you want to sync the files across multiple devices, **your vault name should be the same** while using default settings.
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### How about OneDrive for Business?
|
||||||
|
|
||||||
|
This plugin only works for "OneDrive for personal", and not works for "OneDrive for Business" (yet). See [#11](https://github.com/fyears/remotely-save/issues/11) to further details.
|
||||||
|
|
||||||
|
### I cannot find `/Apps/remotely-save` folder
|
||||||
|
|
||||||
|
Mystically some users report that their OneDrive generate `/Application/Graph` instead of `/Apps/remotely-save`. See [#517](https://github.com/remotely-save/remotely-save/issues/517).
|
||||||
|
|
||||||
|
The solution is simple:
|
||||||
|
|
||||||
|
1. Backup your vault manually.
|
||||||
|
2. Go to onedrive website (<https://onedrive.live.com/>), and rename `/Application/Graph` to `/Application/remotely-save` (right click on the folder and you will see rename option)
|
||||||
|
3. Come back to Obsidian and try to sync!
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "remotely-save",
|
"id": "remotely-save",
|
||||||
"name": "Remotely Save",
|
"name": "Remotely Save",
|
||||||
"version": "0.4.4",
|
"version": "0.4.8",
|
||||||
"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",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "remotely-save",
|
"name": "remotely-save",
|
||||||
"version": "0.4.4",
|
"version": "0.4.8",
|
||||||
"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 --watch",
|
"dev2": "node esbuild.config.mjs --watch",
|
||||||
|
|||||||
+7
-10
@@ -509,17 +509,14 @@ export const stringToFragment = (string: string) => {
|
|||||||
* @param op
|
* @param op
|
||||||
*/
|
*/
|
||||||
export const changeMobileStatusBar = (op: "enable" | "disable") => {
|
export const changeMobileStatusBar = (op: "enable" | "disable") => {
|
||||||
|
const bar = document.querySelector(
|
||||||
|
".is-mobile .app-container .status-bar"
|
||||||
|
) as HTMLElement;
|
||||||
if (op === "enable") {
|
if (op === "enable") {
|
||||||
(
|
bar.style.setProperty("display", "flex");
|
||||||
document.querySelector(
|
bar.style.setProperty("margin-bottom", "40px");
|
||||||
".is-mobile .app-container .status-bar"
|
|
||||||
) as HTMLElement
|
|
||||||
).style.setProperty("display", "flex");
|
|
||||||
} else {
|
} else {
|
||||||
(
|
bar.style.removeProperty("display");
|
||||||
document.querySelector(
|
bar.style.removeProperty("margin-bottom");
|
||||||
".is-mobile .app-container .status-bar"
|
|
||||||
) as HTMLElement
|
|
||||||
).style.setProperty("display", "none");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+55
-72
@@ -1,5 +1,5 @@
|
|||||||
import { Buffer } from "buffer";
|
import { Buffer } from "buffer";
|
||||||
import { Vault, requestUrl } from "obsidian";
|
import { Platform, Vault, requestUrl } from "obsidian";
|
||||||
|
|
||||||
import { Queue } from "@fyears/tsqueue";
|
import { Queue } from "@fyears/tsqueue";
|
||||||
import chunk from "lodash/chunk";
|
import chunk from "lodash/chunk";
|
||||||
@@ -26,43 +26,70 @@ function onlyAscii(str: string) {
|
|||||||
return !/[^\u0000-\u00ff]/g.test(str);
|
return !/[^\u0000-\u00ff]/g.test(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://stackoverflow.com/questions/12539574/
|
||||||
|
* @param obj
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function objKeyToLower(obj: Record<string, string>) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(obj).map(([k, v]) => [k.toLowerCase(), v])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { getPatcher } from "webdav/dist/web/index.js";
|
import { getPatcher } from "webdav/dist/web/index.js";
|
||||||
if (VALID_REQURL) {
|
if (VALID_REQURL) {
|
||||||
getPatcher().patch(
|
getPatcher().patch(
|
||||||
"request",
|
"request",
|
||||||
async (options: RequestOptionsWithState): Promise<Response> => {
|
async (options: RequestOptionsWithState): Promise<Response> => {
|
||||||
const transformedHeaders = { ...options.headers };
|
const transformedHeaders = objKeyToLower({ ...options.headers });
|
||||||
delete transformedHeaders["host"];
|
delete transformedHeaders["host"];
|
||||||
delete transformedHeaders["Host"];
|
|
||||||
delete transformedHeaders["content-length"];
|
delete transformedHeaders["content-length"];
|
||||||
delete transformedHeaders["Content-Length"];
|
|
||||||
|
|
||||||
const r = await requestUrl({
|
const reqContentType =
|
||||||
|
transformedHeaders["accept"] ?? transformedHeaders["content-type"];
|
||||||
|
|
||||||
|
console.debug(`before request:`);
|
||||||
|
console.debug(`url: ${options.url}`);
|
||||||
|
console.debug(`method: ${options.method}`);
|
||||||
|
console.debug(`headers: ${JSON.stringify(transformedHeaders, null, 2)}`);
|
||||||
|
console.debug(`reqContentType: ${reqContentType}`);
|
||||||
|
|
||||||
|
let r = await requestUrl({
|
||||||
url: options.url,
|
url: options.url,
|
||||||
method: options.method,
|
method: options.method,
|
||||||
body: options.data as string | ArrayBuffer,
|
body: options.data as string | ArrayBuffer,
|
||||||
headers: transformedHeaders,
|
headers: transformedHeaders,
|
||||||
|
contentType: reqContentType,
|
||||||
throw: false,
|
throw: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
let contentType: string | undefined =
|
if (
|
||||||
r.headers["Content-Type"] || r.headers["content-type"];
|
r.status === 401 &&
|
||||||
if (options.headers !== undefined) {
|
Platform.isIosApp &&
|
||||||
contentType =
|
!options.url.endsWith("/") &&
|
||||||
contentType ||
|
!options.url.endsWith(".md") &&
|
||||||
options.headers["Content-Type"] ||
|
options.method.toUpperCase() === "PROPFIND"
|
||||||
options.headers["content-type"] ||
|
) {
|
||||||
options.headers["Accept"] ||
|
// don't ask me why,
|
||||||
options.headers["accept"];
|
// some webdav servers have some mysterious behaviours,
|
||||||
|
// if a folder doesn't exist without slash, the servers return 401 instead of 404
|
||||||
|
// here is a dirty hack that works
|
||||||
|
console.debug(`so we have 401, try appending request url with slash`);
|
||||||
|
r = await requestUrl({
|
||||||
|
url: `${options.url}/`,
|
||||||
|
method: options.method,
|
||||||
|
body: options.data as string | ArrayBuffer,
|
||||||
|
headers: transformedHeaders,
|
||||||
|
contentType: reqContentType,
|
||||||
|
throw: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentType !== undefined) {
|
console.debug(`after request:`);
|
||||||
contentType = contentType.toLowerCase();
|
const rspHeaders = objKeyToLower({ ...r.headers });
|
||||||
}
|
console.debug(`rspHeaders: ${JSON.stringify(rspHeaders, null, 2)}`);
|
||||||
const rspHeaders = { ...r.headers };
|
|
||||||
console.log("rspHeaders");
|
|
||||||
console.log(rspHeaders);
|
|
||||||
for (let key in rspHeaders) {
|
for (let key in rspHeaders) {
|
||||||
if (rspHeaders.hasOwnProperty(key)) {
|
if (rspHeaders.hasOwnProperty(key)) {
|
||||||
// avoid the error:
|
// avoid the error:
|
||||||
@@ -79,74 +106,28 @@ if (VALID_REQURL) {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
if (!onlyAscii(rspHeaders[key])) {
|
if (!onlyAscii(rspHeaders[key])) {
|
||||||
|
console.debug(`rspHeaders[key] needs encode: ${key}`);
|
||||||
rspHeaders[key] = encodeURIComponent(rspHeaders[key]);
|
rspHeaders[key] = encodeURIComponent(rspHeaders[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// console.info(`requesting url=${options.url}`);
|
|
||||||
// console.info(`contentType=${contentType}`);
|
|
||||||
// console.info(`rspHeaders=${JSON.stringify(rspHeaders)}`)
|
|
||||||
|
|
||||||
// let r2: Response = undefined;
|
|
||||||
// if (contentType.includes("xml")) {
|
|
||||||
// r2 = new Response(r.text, {
|
|
||||||
// status: r.status,
|
|
||||||
// statusText: getReasonPhrase(r.status),
|
|
||||||
// headers: rspHeaders,
|
|
||||||
// });
|
|
||||||
// } else if (
|
|
||||||
// contentType.includes("json") ||
|
|
||||||
// contentType.includes("javascript")
|
|
||||||
// ) {
|
|
||||||
// console.info('inside json branch');
|
|
||||||
// // const j = r.json;
|
|
||||||
// // console.info(j);
|
|
||||||
// r2 = new Response(
|
|
||||||
// r.text, // yea, here is the text because Response constructor expects a text
|
|
||||||
// {
|
|
||||||
// status: r.status,
|
|
||||||
// statusText: getReasonPhrase(r.status),
|
|
||||||
// headers: rspHeaders,
|
|
||||||
// });
|
|
||||||
// } else if (contentType.includes("text")) {
|
|
||||||
// // avoid text/json,
|
|
||||||
// // so we split this out from the above xml or json branch
|
|
||||||
// r2 = new Response(r.text, {
|
|
||||||
// status: r.status,
|
|
||||||
// statusText: getReasonPhrase(r.status),
|
|
||||||
// headers: rspHeaders,
|
|
||||||
// });
|
|
||||||
// } else if (
|
|
||||||
// contentType.includes("octet-stream") ||
|
|
||||||
// contentType.includes("binary") ||
|
|
||||||
// contentType.includes("buffer")
|
|
||||||
// ) {
|
|
||||||
// // application/octet-stream
|
|
||||||
// r2 = new Response(r.arrayBuffer, {
|
|
||||||
// status: r.status,
|
|
||||||
// statusText: getReasonPhrase(r.status),
|
|
||||||
// headers: rspHeaders,
|
|
||||||
// });
|
|
||||||
// } else {
|
|
||||||
// throw Error(
|
|
||||||
// `do not know how to deal with requested content type = ${contentType}`
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
let r2: Response | undefined = undefined;
|
let r2: Response | undefined = undefined;
|
||||||
|
const statusText = getReasonPhrase(r.status);
|
||||||
|
console.debug(`statusText: ${statusText}`);
|
||||||
if ([101, 103, 204, 205, 304].includes(r.status)) {
|
if ([101, 103, 204, 205, 304].includes(r.status)) {
|
||||||
// A null body status is a status that is 101, 103, 204, 205, or 304.
|
// A null body status is a status that is 101, 103, 204, 205, or 304.
|
||||||
// https://fetch.spec.whatwg.org/#statuses
|
// https://fetch.spec.whatwg.org/#statuses
|
||||||
// fix this: Failed to construct 'Response': Response with null body status cannot have body
|
// fix this: Failed to construct 'Response': Response with null body status cannot have body
|
||||||
r2 = new Response(null, {
|
r2 = new Response(null, {
|
||||||
status: r.status,
|
status: r.status,
|
||||||
statusText: getReasonPhrase(r.status),
|
statusText: statusText,
|
||||||
headers: rspHeaders,
|
headers: rspHeaders,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
r2 = new Response(r.arrayBuffer, {
|
r2 = new Response(r.arrayBuffer, {
|
||||||
status: r.status,
|
status: r.status,
|
||||||
statusText: getReasonPhrase(r.status),
|
statusText: statusText,
|
||||||
headers: rspHeaders,
|
headers: rspHeaders,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -158,6 +139,7 @@ if (VALID_REQURL) {
|
|||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { AuthType, BufferLike, createClient } from "webdav/dist/web/index.js";
|
import { AuthType, BufferLike, createClient } from "webdav/dist/web/index.js";
|
||||||
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
export type { WebDAVClient } from "webdav";
|
export type { WebDAVClient } from "webdav";
|
||||||
|
|
||||||
export const DEFAULT_WEBDAV_CONFIG = {
|
export const DEFAULT_WEBDAV_CONFIG = {
|
||||||
@@ -229,7 +211,8 @@ export class WrappedWebdavClient {
|
|||||||
remoteBaseDir: string,
|
remoteBaseDir: string,
|
||||||
saveUpdatedConfigFunc: () => Promise<any>
|
saveUpdatedConfigFunc: () => Promise<any>
|
||||||
) {
|
) {
|
||||||
this.webdavConfig = webdavConfig;
|
this.webdavConfig = cloneDeep(webdavConfig);
|
||||||
|
this.webdavConfig.address = encodeURI(this.webdavConfig.address);
|
||||||
this.remoteBaseDir = remoteBaseDir;
|
this.remoteBaseDir = remoteBaseDir;
|
||||||
this.vaultFolderExists = false;
|
this.vaultFolderExists = false;
|
||||||
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
|
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
|
||||||
|
|||||||
Reference in New Issue
Block a user