Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e39edcea70 | ||
|
|
9ac7ecf828 | ||
|
|
50c08400ff | ||
|
|
7bc2fad88b | ||
|
|
6dc1c28361 | ||
|
|
2220f511e3 |
@@ -122,6 +122,7 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
|||||||
- [Open Media Vault](./docs/remote_services/webdav_openmediavault/README.md)
|
- [Open Media Vault](./docs/remote_services/webdav_openmediavault/README.md)
|
||||||
- [Nginx (`ngx_http_dav_module`, `nginx-dav-ext-module`, with Docker)](./docs/remote_services/webdav_nginx/README.md)
|
- [Nginx (`ngx_http_dav_module`, `nginx-dav-ext-module`, with Docker)](./docs/remote_services/webdav_nginx/README.md)
|
||||||
- [Apache (with Docker)](./docs/remote_services/webdav_apache/README.md)
|
- [Apache (with Docker)](./docs/remote_services/webdav_apache/README.md)
|
||||||
|
- [Caddy with `http.handlers.webdav` module](./docs/remote_services/webdav_caddy/README.md)
|
||||||
- Very old version of Obsidian needs [configuring CORS](./docs/remote_services/webdav_general/webav_cors.md).
|
- Very old version of Obsidian needs [configuring CORS](./docs/remote_services/webdav_general/webav_cors.md).
|
||||||
- Your data would be synced to a `${vaultName}` sub folder on your webdav server.
|
- 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**.
|
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
- [Open Media Vault](./docs/remote_services/webdav_openmediavault/README.md)
|
- [Open Media Vault](./docs/remote_services/webdav_openmediavault/README.md)
|
||||||
- [Nginx (`ngx_http_dav_module`, `nginx-dav-ext-module`, with Docker)](./docs/remote_services/webdav_nginx/README.md)
|
- [Nginx (`ngx_http_dav_module`, `nginx-dav-ext-module`, with Docker)](./docs/remote_services/webdav_nginx/README.md)
|
||||||
- [Apache (with Docker)](./docs/remote_services/webdav_apache/README.md)
|
- [Apache (with Docker)](./docs/remote_services/webdav_apache/README.md)
|
||||||
|
- [Caddy with `http.handlers.webdav` module](./docs/remote_services/webdav_caddy/README.md)
|
||||||
- 非常旧版本的Obsidian需要[配置 CORS](./docs/remote_services/webdav_general/webav_cors.md)。
|
- 非常旧版本的Obsidian需要[配置 CORS](./docs/remote_services/webdav_general/webav_cors.md)。
|
||||||
- 你的数据会同步到你的webdav服务器上的 `${vaultName}` 子文件夹。
|
- 你的数据会同步到你的webdav服务器上的 `${vaultName}` 子文件夹。
|
||||||
- 基于密码的端到端加密也是可以的。但请注意,**vault 名称本身未加密**。
|
- 基于密码的端到端加密也是可以的。但请注意,**vault 名称本身未加密**。
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Caddy with `http.handlers.webdav` module
|
||||||
|
|
||||||
|
> modified from the instruction from @cyruz-git in https://github.com/remotely-save/remotely-save/issues/825
|
||||||
|
|
||||||
|
## Link
|
||||||
|
|
||||||
|
<https://caddyserver.com/download?package=github.com%2Fmholt%2Fcaddy-webdav>
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Download caddy with webdav module from <https://caddyserver.com/download?package=github.com%2Fmholt%2Fcaddy-webdav>. Or you can install Caddy then install the plugins.
|
||||||
|
2. Create a folder for storing webdav server files. Like `/usr/local/mywebdav`.
|
||||||
|
3. Create a `Caddyfile` (yeah the file name itself is `Caddyfile`.) like this:
|
||||||
|
```caddy
|
||||||
|
:8080 {
|
||||||
|
route /dav/* {
|
||||||
|
root /usr/local/mywebdav
|
||||||
|
basicauth {
|
||||||
|
# Username "Bob", password "hiccup"
|
||||||
|
Bob $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG
|
||||||
|
}
|
||||||
|
uri strip_prefix /dav
|
||||||
|
webdav
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The password hash is generated like [this](https://caddyserver.com/docs/caddyfile/directives/basic_auth).
|
||||||
|
4. In Remotely Save, setup:
|
||||||
|
* address `http://localhost:8080/dav/`
|
||||||
|
* username `Bob`
|
||||||
|
* password `hiccup`
|
||||||
|
* auth type: `basic`
|
||||||
|
5. Check the connection and sync!
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "remotely-save",
|
"id": "remotely-save",
|
||||||
"name": "Remotely Save",
|
"name": "Remotely Save",
|
||||||
"version": "0.5.23",
|
"version": "0.5.25",
|
||||||
"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,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "remotely-save",
|
"id": "remotely-save",
|
||||||
"name": "Remotely Save",
|
"name": "Remotely Save",
|
||||||
"version": "0.5.23",
|
"version": "0.5.25",
|
||||||
"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.5.23",
|
"version": "0.5.25",
|
||||||
"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",
|
||||||
|
|||||||
+8
-1
@@ -305,7 +305,14 @@ export const getSkipItemsByList = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we deal with explictly allow list secondly, apply them to PARENTS if possible
|
// we deal with explictly allow list secondly, apply them to PARENTS if possible
|
||||||
const enableAllowMode = skipOrNotResults[allPotentialKeys[0]].enableAllowMode;
|
let enableAllowMode = false;
|
||||||
|
if (
|
||||||
|
allPotentialKeys.length > 0 &&
|
||||||
|
allPotentialKeys[0] !== undefined &&
|
||||||
|
skipOrNotResults[allPotentialKeys[0]] !== undefined
|
||||||
|
) {
|
||||||
|
enableAllowMode = skipOrNotResults[allPotentialKeys[0]].enableAllowMode;
|
||||||
|
}
|
||||||
if (enableAllowMode) {
|
if (enableAllowMode) {
|
||||||
for (let index = 0; index < sortedKeys.length; index++) {
|
for (let index = 0; index < sortedKeys.length; index++) {
|
||||||
// reverse order, long(deep) to short(shadow), ascending
|
// reverse order, long(deep) to short(shadow), ascending
|
||||||
|
|||||||
@@ -252,6 +252,10 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
|||||||
// why?? /drive/root:/Apps/Graph
|
// why?? /drive/root:/Apps/Graph
|
||||||
const FIFTH_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/Graph\//g;
|
const FIFTH_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/Graph\//g;
|
||||||
|
|
||||||
|
// why again?? /drive/root:/Apps/Graph 1
|
||||||
|
const SIXTH_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/Graph 1\//g;
|
||||||
|
const SIXTH_COMMON_PREFIX_REGEX_V2 = /^\/drive\/root:\/[^\/]+\/Graph%201\//g;
|
||||||
|
|
||||||
// or the root is absolute path /Livefolders,
|
// or the root is absolute path /Livefolders,
|
||||||
// e.g.: /Livefolders/应用/remotely-save/${remoteBaseDir}
|
// e.g.: /Livefolders/应用/remotely-save/${remoteBaseDir}
|
||||||
const SECOND_COMMON_PREFIX_REGEX = /^\/Livefolders\/[^\/]+\/remotely-save\//g;
|
const SECOND_COMMON_PREFIX_REGEX = /^\/Livefolders\/[^\/]+\/remotely-save\//g;
|
||||||
@@ -278,6 +282,11 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
|||||||
const fullPathOriginal = `${x.parentReference.path}/${x.name}`;
|
const fullPathOriginal = `${x.parentReference.path}/${x.name}`;
|
||||||
const matchFirstPrefixRes = fullPathOriginal.match(FIRST_COMMON_PREFIX_REGEX);
|
const matchFirstPrefixRes = fullPathOriginal.match(FIRST_COMMON_PREFIX_REGEX);
|
||||||
const matchFifthPrefixRes = fullPathOriginal.match(FIFTH_COMMON_PREFIX_REGEX);
|
const matchFifthPrefixRes = fullPathOriginal.match(FIFTH_COMMON_PREFIX_REGEX);
|
||||||
|
const matchSixthPrefixRes = fullPathOriginal.match(SIXTH_COMMON_PREFIX_REGEX);
|
||||||
|
const matchSixthV2PrefixRes = fullPathOriginal.match(
|
||||||
|
SIXTH_COMMON_PREFIX_REGEX_V2
|
||||||
|
);
|
||||||
|
|
||||||
const matchSecondPrefixRes = fullPathOriginal.match(
|
const matchSecondPrefixRes = fullPathOriginal.match(
|
||||||
SECOND_COMMON_PREFIX_REGEX
|
SECOND_COMMON_PREFIX_REGEX
|
||||||
);
|
);
|
||||||
@@ -317,6 +326,40 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
|||||||
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sixth
|
||||||
|
else if (
|
||||||
|
matchSixthPrefixRes !== null &&
|
||||||
|
fullPathOriginal.startsWith(`${matchSixthPrefixRes[0]}${remoteBaseDir}`)
|
||||||
|
) {
|
||||||
|
const foundPrefix = `${matchSixthPrefixRes[0]}${remoteBaseDir}`;
|
||||||
|
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||||
|
} else if (
|
||||||
|
matchSixthPrefixRes !== null &&
|
||||||
|
fullPathOriginal.startsWith(
|
||||||
|
`${matchSixthPrefixRes[0]}${remoteBaseDirEncoded}`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const foundPrefix = `${matchSixthPrefixRes[0]}${remoteBaseDirEncoded}`;
|
||||||
|
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sixth v2
|
||||||
|
else if (
|
||||||
|
matchSixthV2PrefixRes !== null &&
|
||||||
|
fullPathOriginal.startsWith(`${matchSixthV2PrefixRes[0]}${remoteBaseDir}`)
|
||||||
|
) {
|
||||||
|
const foundPrefix = `${matchSixthV2PrefixRes[0]}${remoteBaseDir}`;
|
||||||
|
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||||
|
} else if (
|
||||||
|
matchSixthV2PrefixRes !== null &&
|
||||||
|
fullPathOriginal.startsWith(
|
||||||
|
`${matchSixthV2PrefixRes[0]}${remoteBaseDirEncoded}`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const foundPrefix = `${matchSixthV2PrefixRes[0]}${remoteBaseDirEncoded}`;
|
||||||
|
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||||
|
}
|
||||||
|
|
||||||
// second
|
// second
|
||||||
else if (
|
else if (
|
||||||
matchSecondPrefixRes !== null &&
|
matchSecondPrefixRes !== null &&
|
||||||
@@ -374,6 +417,8 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
|||||||
fullPathOriginal=${fullPathOriginal}
|
fullPathOriginal=${fullPathOriginal}
|
||||||
matchFirstPrefixRes=${matchFirstPrefixRes}
|
matchFirstPrefixRes=${matchFirstPrefixRes}
|
||||||
matchFifthPrefixRes=${matchFifthPrefixRes}
|
matchFifthPrefixRes=${matchFifthPrefixRes}
|
||||||
|
matchSixthPrefixRes=${matchSixthPrefixRes}
|
||||||
|
matchSixthV2PrefixRes=${matchSixthV2PrefixRes}
|
||||||
matchSecondPrefixRes=${matchSecondPrefixRes}
|
matchSecondPrefixRes=${matchSecondPrefixRes}
|
||||||
matchThirdPrefixRes=${matchThirdPrefixRes}
|
matchThirdPrefixRes=${matchThirdPrefixRes}
|
||||||
${constructFromDriveItemToEntityError(x)}`
|
${constructFromDriveItemToEntityError(x)}`
|
||||||
@@ -388,6 +433,8 @@ ${constructFromDriveItemToEntityError(x)}`
|
|||||||
fullPathOriginal=${fullPathOriginal}
|
fullPathOriginal=${fullPathOriginal}
|
||||||
matchFirstPrefixRes=${matchFirstPrefixRes}
|
matchFirstPrefixRes=${matchFirstPrefixRes}
|
||||||
matchFifthPrefixRes=${matchFifthPrefixRes}
|
matchFifthPrefixRes=${matchFifthPrefixRes}
|
||||||
|
matchSixthPrefixRes=${matchSixthPrefixRes}
|
||||||
|
matchSixthV2PrefixRes=${matchSixthV2PrefixRes}
|
||||||
matchSecondPrefixRes=${matchSecondPrefixRes}
|
matchSecondPrefixRes=${matchSecondPrefixRes}
|
||||||
matchThirdPrefixRes=${matchThirdPrefixRes}
|
matchThirdPrefixRes=${matchThirdPrefixRes}
|
||||||
${constructFromDriveItemToEntityError(x)}`
|
${constructFromDriveItemToEntityError(x)}`
|
||||||
|
|||||||
+33
-12
@@ -138,7 +138,6 @@ if (VALID_REQURL) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
import isEqual from "lodash/isEqual";
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// biome-ignore lint: we want to ts-ignore the next line
|
// biome-ignore lint: we want to ts-ignore the next line
|
||||||
import { AuthType, BufferLike, createClient } from "webdav/dist/web/index.js";
|
import { AuthType, BufferLike, createClient } from "webdav/dist/web/index.js";
|
||||||
@@ -169,23 +168,37 @@ const getWebdavPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
|
|||||||
return key;
|
return key;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sometimes the path startswith /../../......
|
||||||
|
* we want to make sure the path is compatible
|
||||||
|
*/
|
||||||
|
const stripLeadingPath = (x: string) => {
|
||||||
|
let y = x;
|
||||||
|
while (y.startsWith("/..")) {
|
||||||
|
y = y.slice("/..".length);
|
||||||
|
}
|
||||||
|
return y;
|
||||||
|
};
|
||||||
|
|
||||||
const getNormPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
|
const getNormPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
|
||||||
|
const strippedFileOrFolderPath = stripLeadingPath(fileOrFolderPath);
|
||||||
if (
|
if (
|
||||||
!(
|
!(
|
||||||
fileOrFolderPath === `/${remoteBaseDir}` ||
|
strippedFileOrFolderPath === `/${remoteBaseDir}` ||
|
||||||
fileOrFolderPath.startsWith(`/${remoteBaseDir}/`)
|
strippedFileOrFolderPath.startsWith(`/${remoteBaseDir}/`)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
throw Error(
|
throw Error(
|
||||||
`"${fileOrFolderPath}" doesn't starts with "/${remoteBaseDir}/"`
|
`"${fileOrFolderPath}" after stripping doesn't starts with "/${remoteBaseDir}/"`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const result = strippedFileOrFolderPath.slice(`/${remoteBaseDir}/`.length);
|
||||||
return fileOrFolderPath.slice(`/${remoteBaseDir}/`.length);
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fromWebdavItemToEntity = (x: FileStat, remoteBaseDir: string): Entity => {
|
const fromWebdavItemToEntity = (x: FileStat, remoteBaseDir: string): Entity => {
|
||||||
let key = getNormPath(x.filename, remoteBaseDir);
|
let key = getNormPath(x.filename, remoteBaseDir);
|
||||||
|
|
||||||
if (x.type === "directory" && !key.endsWith("/")) {
|
if (x.type === "directory" && !key.endsWith("/")) {
|
||||||
key = `${key}/`;
|
key = `${key}/`;
|
||||||
}
|
}
|
||||||
@@ -447,15 +460,17 @@ export class FakeFsWebdav extends FakeFs {
|
|||||||
// console.debug(itemsToFetchChunks);
|
// console.debug(itemsToFetchChunks);
|
||||||
const subContents = [] as FileStat[];
|
const subContents = [] as FileStat[];
|
||||||
for (const singleChunk of itemsToFetchChunks) {
|
for (const singleChunk of itemsToFetchChunks) {
|
||||||
const r = singleChunk.map((x) => {
|
const r = singleChunk.map(async (x) => {
|
||||||
return this.client.getDirectoryContents(x, {
|
let k = (await this.client.getDirectoryContents(x, {
|
||||||
deep: false,
|
deep: false,
|
||||||
details: false /* no need for verbose details here */,
|
details: false /* no need for verbose details here */,
|
||||||
// TODO: to support .obsidian,
|
// TODO: to support .obsidian,
|
||||||
// we need to load all files including dot,
|
// we need to load all files including dot,
|
||||||
// anyway to reduce the resources?
|
// anyway to reduce the resources?
|
||||||
// glob: "/**" /* avoid dot files by using glob */,
|
// glob: "/**" /* avoid dot files by using glob */,
|
||||||
}) as Promise<FileStat[]>;
|
})) as FileStat[];
|
||||||
|
k = k.filter((sub) => stripLeadingPath(sub.filename) !== x);
|
||||||
|
return k;
|
||||||
});
|
});
|
||||||
const r3 = await Promise.all(r);
|
const r3 = await Promise.all(r);
|
||||||
for (const r4 of r3) {
|
for (const r4 of r3) {
|
||||||
@@ -477,7 +492,7 @@ export class FakeFsWebdav extends FakeFs {
|
|||||||
const f = subContents[i];
|
const f = subContents[i];
|
||||||
contents.push(f);
|
contents.push(f);
|
||||||
if (f.type === "directory") {
|
if (f.type === "directory") {
|
||||||
q.push(f.filename);
|
q.push(stripLeadingPath(f.filename));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,7 +510,11 @@ export class FakeFsWebdav extends FakeFs {
|
|||||||
}
|
}
|
||||||
)) as FileStat[];
|
)) as FileStat[];
|
||||||
}
|
}
|
||||||
return contents.map((x) => fromWebdavItemToEntity(x, this.remoteBaseDir));
|
|
||||||
|
const result = contents
|
||||||
|
.map((x) => fromWebdavItemToEntity(x, this.remoteBaseDir))
|
||||||
|
.filter((x) => x.keyRaw !== "/");
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async walkPartial(): Promise<Entity[]> {
|
async walkPartial(): Promise<Entity[]> {
|
||||||
@@ -508,7 +527,9 @@ export class FakeFsWebdav extends FakeFs {
|
|||||||
details: false /* no need for verbose details here */,
|
details: false /* no need for verbose details here */,
|
||||||
}
|
}
|
||||||
)) as FileStat[];
|
)) as FileStat[];
|
||||||
return contents.map((x) => fromWebdavItemToEntity(x, this.remoteBaseDir));
|
return contents
|
||||||
|
.map((x) => fromWebdavItemToEntity(x, this.remoteBaseDir))
|
||||||
|
.filter((x) => x.keyRaw !== "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
async stat(key: string): Promise<Entity> {
|
async stat(key: string): Promise<Entity> {
|
||||||
|
|||||||
Reference in New Issue
Block a user