mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-03-13 05:18:49 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cd12c85ed | ||
|
|
90651540f9 | ||
|
|
9e504d5002 | ||
|
|
faaa94423c | ||
|
|
a7c179fc86 | ||
|
|
ed1a670b9b | ||
|
|
6c3c265bd6 | ||
|
|
9d68025f2a | ||
|
|
e70972c8f9 | ||
|
|
7607be7729 | ||
|
|
db9d428ab4 | ||
|
|
0a2caea3c7 | ||
|
|
b1d1ba0e6b | ||
|
|
5e844372cb | ||
|
|
99c6911e96 | ||
|
|
dc880d7d4e | ||
|
|
c157fef76c | ||
|
|
2b2011dc49 | ||
|
|
ae451e005e |
@@ -1,4 +1,5 @@
|
||||
node_modules
|
||||
build
|
||||
.eslintrc.js.bak
|
||||
src/lib/src/patches/pouchdb-utils
|
||||
src/lib/src/patches/pouchdb-utils
|
||||
esbuild.config.mjs
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14.x' # You might need to adjust this value to your own version
|
||||
node-version: '18.x' # You might need to adjust this value to your own version
|
||||
# Get the version number and put it in a variable
|
||||
- name: Get Version
|
||||
id: version
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,6 +8,7 @@ package-lock.json
|
||||
|
||||
# build
|
||||
main.js
|
||||
main_org.js
|
||||
*.js.map
|
||||
|
||||
# obsidian
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Self-hosted LiveSync
|
||||
|
||||
[Japanese docs](./README_ja.md) [Chinese docs](./README_cn.md).
|
||||
[Japanese docs](./README_ja.md) - [Chinese docs](./README_cn.md).
|
||||
|
||||
Self-hosted LiveSync is a community-implemented synchronization plugin.
|
||||
A self-hosted or purchased CouchDB acts as the intermediate server. Available on every obsidian-compatible platform.
|
||||
|
||||
46
docker-compose.traefik.yml
Normal file
46
docker-compose.traefik.yml
Normal file
@@ -0,0 +1,46 @@
|
||||
# For details and other explanations about this file refer to:
|
||||
# https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/setup_own_server.md#traefik
|
||||
|
||||
version: "2.1"
|
||||
services:
|
||||
couchdb:
|
||||
image: couchdb:latest
|
||||
container_name: obsidian-livesync
|
||||
user: 1000:1000
|
||||
environment:
|
||||
- COUCHDB_USER=username
|
||||
- COUCHDB_PASSWORD=password
|
||||
volumes:
|
||||
- ./data:/opt/couchdb/data
|
||||
- ./local.ini:/opt/couchdb/etc/local.ini
|
||||
# Ports not needed when already passed to Traefik
|
||||
#ports:
|
||||
# - 5984:5984
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- proxy
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# The Traefik Network
|
||||
- "traefik.docker.network=proxy"
|
||||
# Don't forget to replace 'obsidian-livesync.example.org' with your own domain
|
||||
- "traefik.http.routers.obsidian-livesync.rule=Host(`obsidian-livesync.example.org`)"
|
||||
# The 'websecure' entryPoint is basically your HTTPS entrypoint. Check the next code snippet if you are encountering problems only; you probably have a working traefik configuration if this is not your first container you are reverse proxying.
|
||||
- "traefik.http.routers.obsidian-livesync.entrypoints=websecure"
|
||||
- "traefik.http.routers.obsidian-livesync.service=obsidian-livesync"
|
||||
- "traefik.http.services.obsidian-livesync.loadbalancer.server.port=5984"
|
||||
- "traefik.http.routers.obsidian-livesync.tls=true"
|
||||
# Replace the string 'letsencrypt' with your own certificate resolver
|
||||
- "traefik.http.routers.obsidian-livesync.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.obsidian-livesync.middlewares=obsidiancors"
|
||||
# The part needed for CORS to work on Traefik 2.x starts here
|
||||
- "traefik.http.middlewares.obsidiancors.headers.accesscontrolallowmethods=GET,PUT,POST,HEAD,DELETE"
|
||||
- "traefik.http.middlewares.obsidiancors.headers.accesscontrolallowheaders=accept,authorization,content-type,origin,referer"
|
||||
- "traefik.http.middlewares.obsidiancors.headers.accesscontrolalloworiginlist=app://obsidian.md,capacitor://localhost,http://localhost"
|
||||
- "traefik.http.middlewares.obsidiancors.headers.accesscontrolmaxage=3600"
|
||||
- "traefik.http.middlewares.obsidiancors.headers.addvaryheader=true"
|
||||
- "traefik.http.middlewares.obsidiancors.headers.accessControlAllowCredentials=true"
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
@@ -1,10 +1,22 @@
|
||||
# Setup a CouchDB server
|
||||
|
||||
## Table of Contents
|
||||
- [Configure](#configure)
|
||||
- [Run](#run)
|
||||
- [Docker CLI](#docker-cli)
|
||||
- [Docker Compose](#docker-compose)
|
||||
- [Access from a mobile device](#access-from-a-mobile-device)
|
||||
- [Testing from a mobile](#testing-from-a-mobile)
|
||||
- [Setting up your domain](#setting-up-your-domain)
|
||||
- [Reverse Proxies](#reverse-proxies)
|
||||
- [Traefik](#traefik)
|
||||
---
|
||||
|
||||
## Configure
|
||||
|
||||
The easiest way to set up a CouchDB instance is using the official [docker image](https://hub.docker.com/_/couchdb).
|
||||
|
||||
Some initial configuration is required. Create a `local.ini` to use Self-hosted LiveSync as follows:
|
||||
Some initial configuration is required. Create a `local.ini` to use Self-hosted LiveSync as follows ([CouchDB has to be version 3.2 or higher](https://docs.couchdb.org/en/latest/config/http.html#chttpd/enable_cors), if lower `enable_cors = true` has to be under section `[httpd]` ):
|
||||
|
||||
```ini
|
||||
[couchdb]
|
||||
@@ -14,6 +26,7 @@ max_document_size = 50000000
|
||||
[chttpd]
|
||||
require_valid_user = true
|
||||
max_http_request_size = 4294967296
|
||||
enable_cors = true
|
||||
|
||||
[chttpd_auth]
|
||||
require_valid_user = true
|
||||
@@ -21,13 +34,13 @@ authentication_redirect = /_utils/session.html
|
||||
|
||||
[httpd]
|
||||
WWW-Authenticate = Basic realm="couchdb"
|
||||
enable_cors = true
|
||||
bind_address = 0.0.0.0
|
||||
|
||||
[cors]
|
||||
origins = app://obsidian.md,capacitor://localhost,http://localhost
|
||||
origins = app://obsidian.md, capacitor://localhost, http://localhost
|
||||
credentials = true
|
||||
headers = accept, authorization, content-type, origin, referer
|
||||
methods = GET, PUT, POST, HEAD, DELETE
|
||||
methods = GET,PUT,POST,HEAD,DELETE
|
||||
max_age = 3600
|
||||
```
|
||||
|
||||
@@ -48,7 +61,7 @@ $ docker run -d --restart always -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=passw
|
||||
*Remember to replace the path with the path to your local.ini*
|
||||
|
||||
### Docker Compose
|
||||
Create a directory, place your `local.ini` within it, and create a `docker-compose.yml` alongside it. The directory structure should look similar to this:
|
||||
Create a directory, place your `local.ini` within it, and create a `docker-compose.yml` alongside it. Make sure to have write permissions for `local.ini` and the about to be created `data` folder after the container start. The directory structure should look similar to this:
|
||||
```
|
||||
obsidian-livesync
|
||||
├── docker-compose.yml
|
||||
@@ -127,6 +140,77 @@ Set the A record of your domain to point to your server, and host reverse proxy
|
||||
Note: Mounting CouchDB on the top directory is not recommended.
|
||||
Using Caddy is a handy way to serve the server with SSL automatically.
|
||||
|
||||
I have published [docker-compose.yml and ini files](https://github.com/vrtmrz/self-hosted-livesync-server) that launch Caddy and CouchDB at once. Please try it out.
|
||||
I have published [docker-compose.yml and ini files](https://github.com/vrtmrz/self-hosted-livesync-server) that launch Caddy and CouchDB at once. If you are using Traefik you can check the [Reverse Proxies](#reverse-proxies) section below.
|
||||
|
||||
And, be sure to check the server log and be careful of malicious access.
|
||||
|
||||
|
||||
## Reverse Proxies
|
||||
|
||||
### Traefik
|
||||
|
||||
If you are using Traefik, this [docker-compose.yml](https://github.com/vrtmrz/obsidian-livesync/blob/main/docker-compose.traefik.yml) file (also pasted below) has all the right CORS parameters set. It assumes you have an external network called `proxy`.
|
||||
|
||||
```yaml
|
||||
version: "2.1"
|
||||
services:
|
||||
couchdb:
|
||||
image: couchdb:latest
|
||||
container_name: obsidian-livesync
|
||||
user: 1000:1000
|
||||
environment:
|
||||
- COUCHDB_USER=username
|
||||
- COUCHDB_PASSWORD=password
|
||||
volumes:
|
||||
- ./data:/opt/couchdb/data
|
||||
- ./local.ini:/opt/couchdb/etc/local.ini
|
||||
# Ports not needed when already passed to Traefik
|
||||
#ports:
|
||||
# - 5984:5984
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- proxy
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# The Traefik Network
|
||||
- "traefik.docker.network=proxy"
|
||||
# Don't forget to replace 'obsidian-livesync.example.org' with your own domain
|
||||
- "traefik.http.routers.obsidian-livesync.rule=Host(`obsidian-livesync.example.org`)"
|
||||
# The 'websecure' entryPoint is basically your HTTPS entrypoint. Check the next code snippet if you are encountering problems only; you probably have a working traefik configuration if this is not your first container you are reverse proxying.
|
||||
- "traefik.http.routers.obsidian-livesync.entrypoints=websecure"
|
||||
- "traefik.http.routers.obsidian-livesync.service=obsidian-livesync"
|
||||
- "traefik.http.services.obsidian-livesync.loadbalancer.server.port=5984"
|
||||
- "traefik.http.routers.obsidian-livesync.tls=true"
|
||||
# Replace the string 'letsencrypt' with your own certificate resolver
|
||||
- "traefik.http.routers.obsidian-livesync.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.obsidian-livesync.middlewares=obsidiancors"
|
||||
# The part needed for CORS to work on Traefik 2.x starts here
|
||||
- "traefik.http.middlewares.obsidiancors.headers.accesscontrolallowmethods=GET,PUT,POST,HEAD,DELETE"
|
||||
- "traefik.http.middlewares.obsidiancors.headers.accesscontrolallowheaders=accept,authorization,content-type,origin,referer"
|
||||
- "traefik.http.middlewares.obsidiancors.headers.accesscontrolalloworiginlist=app://obsidian.md,capacitor://localhost,http://localhost"
|
||||
- "traefik.http.middlewares.obsidiancors.headers.accesscontrolmaxage=3600"
|
||||
- "traefik.http.middlewares.obsidiancors.headers.addvaryheader=true"
|
||||
- "traefik.http.middlewares.obsidiancors.headers.accessControlAllowCredentials=true"
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
```
|
||||
|
||||
Partial `traefik.yml` config file mentioned in above:
|
||||
```yml
|
||||
...
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
http:
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: "websecure"
|
||||
scheme: "https"
|
||||
websecure:
|
||||
address: ":443"
|
||||
|
||||
...
|
||||
```
|
||||
@@ -1,8 +1,19 @@
|
||||
# 在你自己的服务器上设置 CouchDB
|
||||
|
||||
## 目录
|
||||
- [配置 CouchDB](#配置-CouchDB)
|
||||
- [运行 CouchDB](#运行-CouchDB)
|
||||
- [Docker CLI](#docker-cli)
|
||||
- [Docker Compose](#docker-compose)
|
||||
- [创建数据库](#创建数据库)
|
||||
- [从移动设备访问](#从移动设备访问)
|
||||
- [移动设备测试](#移动设备测试)
|
||||
- [设置你的域名](#设置你的域名)
|
||||
---
|
||||
|
||||
> 注:提供了 [docker-compose.yml 和 ini 文件](https://github.com/vrtmrz/self-hosted-livesync-server) 可以同时启动 Caddy 和 CouchDB。推荐直接使用该 docker-compose 配置进行搭建。(若使用,请查阅链接中的文档,而不是这个文档)
|
||||
|
||||
## 安装 CouchDB 并从 PC 或 Mac 上访问
|
||||
## 配置 CouchDB
|
||||
|
||||
设置 CouchDB 的最简单方法是使用 [CouchDB docker image]((https://hub.docker.com/_/couchdb)).
|
||||
|
||||
@@ -33,17 +44,62 @@ methods = GET, PUT, POST, HEAD, DELETE
|
||||
max_age = 3600
|
||||
```
|
||||
|
||||
创建 `local.ini` 并用如下指令启动 CouchDB:
|
||||
```
|
||||
$ docker run --rm -it -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v .local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb
|
||||
```
|
||||
Note: 此时 local.ini 的文件所有者会变成 5984:5984。这是 docker 镜像的限制,请修改文件所有者后再编辑 local.ini。
|
||||
## 运行 CouchDB
|
||||
|
||||
在确定 Self-hosted LiveSync 可以和服务器同步后,可以后台启动 docker 镜像:
|
||||
### Docker CLI
|
||||
|
||||
你可以通过指定 `local.ini` 配置运行 CouchDB:
|
||||
|
||||
```
|
||||
$ docker run -d --restart always -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v .local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb
|
||||
$ docker run --rm -it -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v /path/to/local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb
|
||||
```
|
||||
*记得将上述命令中的 local.ini 挂载路径替换成实际的存放路径*
|
||||
|
||||
后台运行:
|
||||
```
|
||||
$ docker run -d --restart always -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v /path/to/local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb
|
||||
```
|
||||
*记得将上述命令中的 local.ini 挂载路径替换成实际的存放路径*
|
||||
|
||||
### Docker Compose
|
||||
创建一个文件夹, 将你的 `local.ini` 放在文件夹内, 然后在文件夹内创建 `docker-compose.yml`. 请确保对 `local.ini` 有读写权限并且确保在容器运行后能创建 `data` 文件夹. 文件夹结构大概如下:
|
||||
```
|
||||
obsidian-livesync
|
||||
├── docker-compose.yml
|
||||
└── local.ini
|
||||
```
|
||||
|
||||
可以参照以下内容编辑 `docker-compose.yml`:
|
||||
```yaml
|
||||
version: "2.1"
|
||||
services:
|
||||
couchdb:
|
||||
image: couchdb
|
||||
container_name: obsidian-livesync
|
||||
user: 1000:1000
|
||||
environment:
|
||||
- COUCHDB_USER=admin
|
||||
- COUCHDB_PASSWORD=password
|
||||
volumes:
|
||||
- ./data:/opt/couchdb/data
|
||||
- ./local.ini:/opt/couchdb/etc/local.ini
|
||||
ports:
|
||||
- 5984:5984
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
最后, 创建并启动容器:
|
||||
```
|
||||
# -d will launch detached so the container runs in background
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## 创建数据库
|
||||
|
||||
CouchDB 部署成功后, 需要手动创建一个数据库, 方便插件连接并同步.
|
||||
|
||||
1. 访问 `http://localhost:5984/_utils`, 输入帐号密码后进入管理页面
|
||||
2. 点击 Create Database, 然后根据个人喜好创建数据库
|
||||
|
||||
## 从移动设备访问
|
||||
如果你想要从移动设备访问 Self-hosted LiveSync,你需要一个合法的 SSL 证书。
|
||||
|
||||
@@ -1,46 +1,163 @@
|
||||
//@ts-check
|
||||
|
||||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
import sveltePlugin from "esbuild-svelte";
|
||||
import sveltePreprocess from "svelte-preprocess";
|
||||
import fs from "node:fs";
|
||||
// import terser from "terser";
|
||||
import { minify } from "terser";
|
||||
const banner = `/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD AND TERSER
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
|
||||
const prod = process.argv[2] === "production";
|
||||
const manifestJson = JSON.parse(fs.readFileSync("./manifest.json"));
|
||||
const packageJson = JSON.parse(fs.readFileSync("./package.json"));
|
||||
|
||||
const terserOpt = {
|
||||
sourceMap: (!prod ? {
|
||||
url: "inline"
|
||||
} : {}),
|
||||
format: {
|
||||
indent_level: 2,
|
||||
beautify: true,
|
||||
comments: "some",
|
||||
ecma: 2018,
|
||||
preamble: banner,
|
||||
webkit: true
|
||||
},
|
||||
parse: {
|
||||
// parse options
|
||||
},
|
||||
compress: {
|
||||
// compress options
|
||||
defaults: false,
|
||||
evaluate: true,
|
||||
inline: 3,
|
||||
join_vars: true,
|
||||
loops: true,
|
||||
passes: prod ? 4 : 1,
|
||||
reduce_vars: true,
|
||||
reduce_funcs: true,
|
||||
arrows: true,
|
||||
collapse_vars: true,
|
||||
comparisons: true,
|
||||
lhs_constants: true,
|
||||
hoist_props: true,
|
||||
side_effects: true,
|
||||
// if_return: true,
|
||||
},
|
||||
// mangle: {
|
||||
// // mangle options
|
||||
// keep_classnames: true,
|
||||
// keep_fnames: true,
|
||||
|
||||
// properties: {
|
||||
// // mangle property options
|
||||
|
||||
// }
|
||||
// },
|
||||
|
||||
ecma: 2018, // specify one of: 5, 2015, 2016, etc.
|
||||
enclose: false, // or specify true, or "args:values"
|
||||
keep_classnames: true,
|
||||
keep_fnames: true,
|
||||
ie8: false,
|
||||
module: false,
|
||||
// nameCache: null, // or specify a name cache object
|
||||
safari10: false,
|
||||
toplevel: false
|
||||
}
|
||||
|
||||
|
||||
const manifestJson = JSON.parse(fs.readFileSync("./manifest.json") + "");
|
||||
const packageJson = JSON.parse(fs.readFileSync("./package.json") + "");
|
||||
const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + "");
|
||||
esbuild
|
||||
.build({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
define: {
|
||||
"MANIFEST_VERSION": `"${manifestJson.version}"`,
|
||||
"PACKAGE_VERSION": `"${packageJson.version}"`,
|
||||
"UPDATE_INFO": `${updateInfo}`,
|
||||
"global":"window",
|
||||
},
|
||||
external: ["obsidian", "electron", "crypto"],
|
||||
format: "cjs",
|
||||
watch: !prod,
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
platform: "browser",
|
||||
plugins: [
|
||||
sveltePlugin({
|
||||
preprocess: sveltePreprocess(),
|
||||
compilerOptions: { css: true },
|
||||
}),
|
||||
],
|
||||
outfile: "main.js",
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
|
||||
/** @type esbuild.Plugin[] */
|
||||
const plugins = [{
|
||||
name: 'my-plugin',
|
||||
setup(build) {
|
||||
let count = 0;
|
||||
build.onEnd(async result => {
|
||||
if (count++ === 0) {
|
||||
console.log('first build:', result);
|
||||
|
||||
} else {
|
||||
console.log('subsequent build:');
|
||||
}
|
||||
if (prod) {
|
||||
console.log("Performing terser");
|
||||
const src = fs.readFileSync("./main_org.js").toString();
|
||||
// @ts-ignore
|
||||
const ret = await minify(src, terserOpt);
|
||||
if (ret && ret.code) {
|
||||
fs.writeFileSync("./main.js", ret.code);
|
||||
}
|
||||
console.log("Finished terser");
|
||||
} else {
|
||||
fs.copyFileSync("./main_org.js", "./main.js");
|
||||
}
|
||||
});
|
||||
},
|
||||
}];
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
define: {
|
||||
"MANIFEST_VERSION": `"${manifestJson.version}"`,
|
||||
"PACKAGE_VERSION": `"${packageJson.version}"`,
|
||||
"UPDATE_INFO": `${updateInfo}`,
|
||||
"global": "window",
|
||||
},
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"crypto",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr"],
|
||||
// minifyWhitespace: true,
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
platform: "browser",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main_org.js",
|
||||
minifyWhitespace: false,
|
||||
minifySyntax: false,
|
||||
minifyIdentifiers: false,
|
||||
minify: false,
|
||||
|
||||
// keepNames: true,
|
||||
plugins: [
|
||||
sveltePlugin({
|
||||
preprocess: sveltePreprocess(),
|
||||
compilerOptions: { css: true, preserveComments: true },
|
||||
}),
|
||||
...plugins
|
||||
],
|
||||
})
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.19.12",
|
||||
"version": "0.19.16",
|
||||
"minAppVersion": "0.9.12",
|
||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"author": "vorotamoroz",
|
||||
|
||||
3338
package-lock.json
generated
3338
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
42
package.json
42
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.19.12",
|
||||
"version": "0.19.16",
|
||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
@@ -13,41 +13,51 @@
|
||||
"author": "vorotamoroz",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@tsconfig/svelte": "^4.0.1",
|
||||
"@tsconfig/svelte": "^5.0.0",
|
||||
"@types/diff-match-patch": "^1.0.32",
|
||||
"@types/node": "^20.2.5",
|
||||
"@types/pouchdb": "^6.4.0",
|
||||
"@types/pouchdb-browser": "^6.1.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.54.0",
|
||||
"@typescript-eslint/parser": "^5.54.0",
|
||||
"@types/pouchdb-adapter-http": "^6.1.3",
|
||||
"@types/pouchdb-adapter-idb": "^6.1.4",
|
||||
"@types/pouchdb-core": "^7.0.11",
|
||||
"@types/pouchdb-mapreduce": "^6.1.7",
|
||||
"@types/pouchdb-replication": "^6.4.4",
|
||||
"@types/transform-pouch": "^1.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.1",
|
||||
"@typescript-eslint/parser": "^6.2.1",
|
||||
"builtin-modules": "^3.3.0",
|
||||
"esbuild": "0.15.15",
|
||||
"esbuild-svelte": "^0.7.3",
|
||||
"eslint": "^8.35.0",
|
||||
"esbuild": "0.18.17",
|
||||
"esbuild-svelte": "^0.7.4",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-import": "^2.28.0",
|
||||
"events": "^3.3.0",
|
||||
"obsidian": "^1.1.1",
|
||||
"postcss": "^8.4.21",
|
||||
"obsidian": "^1.3.5",
|
||||
"postcss": "^8.4.27",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"pouchdb-adapter-http": "^8.0.1",
|
||||
"pouchdb-adapter-idb": "^8.0.1",
|
||||
"pouchdb-adapter-indexeddb": "^8.0.1",
|
||||
"pouchdb-core": "^8.0.1",
|
||||
"pouchdb-errors": "^8.0.1",
|
||||
"pouchdb-find": "^8.0.1",
|
||||
"pouchdb-mapreduce": "^8.0.1",
|
||||
"pouchdb-merge": "^8.0.1",
|
||||
"pouchdb-replication": "^8.0.1",
|
||||
"pouchdb-utils": "^8.0.1",
|
||||
"svelte": "^3.59.1",
|
||||
"svelte-preprocess": "^5.0.3",
|
||||
"svelte": "^4.1.2",
|
||||
"svelte-preprocess": "^5.0.4",
|
||||
"terser": "^5.19.2",
|
||||
"transform-pouch": "^2.0.0",
|
||||
"tslib": "^2.5.0",
|
||||
"typescript": "^5.0.4"
|
||||
"tslib": "^2.6.1",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"idb": "^7.1.1",
|
||||
"xxhash-wasm": "^0.4.2",
|
||||
"minimatch": "^9.0.3",
|
||||
"xxhash-wasm": "0.4.2",
|
||||
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@ import { writable } from 'svelte/store';
|
||||
import { Notice, type PluginManifest, parseYaml } from "./deps";
|
||||
|
||||
import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, DocumentID, AnyEntry } from "./lib/src/types";
|
||||
import { LOG_LEVEL } from "./lib/src/types";
|
||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "./lib/src/types";
|
||||
import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "./types";
|
||||
import { Parallels, delay, getDocData } from "./lib/src/utils";
|
||||
import { delay, getDocData } from "./lib/src/utils";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { WrappedNotice } from "./lib/src/wrapper";
|
||||
import { base64ToArrayBuffer, arrayBufferToBase64, readString, crc32CKHash } from "./lib/src/strbin";
|
||||
@@ -139,7 +139,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
Logger("Scanning customizations : done");
|
||||
} catch (ex) {
|
||||
Logger("Scanning customizations : failed");
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -165,13 +165,14 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
await this.updatePluginList(showMessage);
|
||||
}
|
||||
async updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise<void> {
|
||||
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
|
||||
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||
// pluginList.set([]);
|
||||
if (!this.settings.usePluginSync) {
|
||||
this.pluginList = [];
|
||||
pluginList.set(this.pluginList)
|
||||
return;
|
||||
}
|
||||
await Promise.resolve(); // Just to prevent warning.
|
||||
scheduleTask("update-plugin-list-task", 200, async () => {
|
||||
await runWithLock("update-plugin-list", false, async () => {
|
||||
try {
|
||||
@@ -191,7 +192,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
try {
|
||||
count++;
|
||||
if (count % 10 == 0) Logger(`Enumerating files... ${count}`, logLevel, "get-plugins");
|
||||
Logger(`plugin-${path}`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`plugin-${path}`, LOG_LEVEL_VERBOSE);
|
||||
const wx = await this.localDatabase.getDBEntry(path, null, false, false);
|
||||
if (wx) {
|
||||
const data = deserialize(getDocData(wx.data), {}) as PluginDataEx;
|
||||
@@ -211,7 +212,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
// return entries;
|
||||
} catch (ex) {
|
||||
//TODO
|
||||
Logger(`Something happened at enumerating customization :${path}`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE);
|
||||
console.warn(ex);
|
||||
}
|
||||
return false;
|
||||
@@ -257,7 +258,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
const fileB = pluginDataB.files[0];
|
||||
const docAx = { ...docA, ...fileA } as LoadedEntry, docBx = { ...docB, ...fileB } as LoadedEntry
|
||||
return runWithLock("config:merge-data", false, () => new Promise((res) => {
|
||||
Logger("Opening data-merging dialog", LOG_LEVEL.VERBOSE);
|
||||
Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE);
|
||||
// const docs = [docA, docB];
|
||||
const path = stripAllPrefixes(docAx.path.split("/").slice(-1).join("/") as FilePath);
|
||||
const modal = new JsonResolveModal(this.app, path, [docAx, docBx], async (keep, result) => {
|
||||
@@ -266,7 +267,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
res(await this.applyData(pluginDataA, result));
|
||||
} catch (ex) {
|
||||
Logger("Could not apply merged file");
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
res(false);
|
||||
}
|
||||
}, "📡", "🛰️", "B");
|
||||
@@ -299,7 +300,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
|
||||
} catch (ex) {
|
||||
Logger(`Applying ${f.filename} of ${data.displayName || data.name}.. Failed`);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -307,7 +308,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
await this.storeCustomizationFiles(uPath);
|
||||
await this.updatePluginList(true, uPath);
|
||||
await delay(100);
|
||||
Logger(`Config ${data.displayName || data.name} has been applied`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Config ${data.displayName || data.name} has been applied`, LOG_LEVEL_NOTICE);
|
||||
if (data.category == "PLUGIN_DATA" || data.category == "PLUGIN_MAIN") {
|
||||
//@ts-ignore
|
||||
const manifests = Object.values(this.app.plugins.manifests) as any as PluginManifest[];
|
||||
@@ -315,12 +316,12 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
const enabledPlugins = this.app.plugins.enabledPlugins as Set<string>;
|
||||
const pluginManifest = manifests.find((manifest) => enabledPlugins.has(manifest.id) && manifest.dir == `${baseDir}/plugins/${data.name}`);
|
||||
if (pluginManifest) {
|
||||
Logger(`Unloading plugin: ${pluginManifest.name}`, LOG_LEVEL.NOTICE, "plugin-reload-" + pluginManifest.id);
|
||||
Logger(`Unloading plugin: ${pluginManifest.name}`, LOG_LEVEL_NOTICE, "plugin-reload-" + pluginManifest.id);
|
||||
// @ts-ignore
|
||||
await this.app.plugins.unloadPlugin(pluginManifest.id);
|
||||
// @ts-ignore
|
||||
await this.app.plugins.loadPlugin(pluginManifest.id);
|
||||
Logger(`Plugin reloaded: ${pluginManifest.name}`, LOG_LEVEL.NOTICE, "plugin-reload-" + pluginManifest.id);
|
||||
Logger(`Plugin reloaded: ${pluginManifest.name}`, LOG_LEVEL_NOTICE, "plugin-reload-" + pluginManifest.id);
|
||||
}
|
||||
} else if (data.category == "CONFIG") {
|
||||
scheduleTask("configReload", 250, async () => {
|
||||
@@ -333,7 +334,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
return true;
|
||||
} catch (ex) {
|
||||
Logger(`Applying ${data.displayName || data.name}.. Failed`);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -342,11 +343,11 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
if (data.documentPath) {
|
||||
await this.deleteConfigOnDatabase(data.documentPath);
|
||||
await this.updatePluginList(false, data.documentPath);
|
||||
Logger(`Delete: ${data.documentPath}`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Delete: ${data.documentPath}`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
return true;
|
||||
} catch (ex) {
|
||||
Logger(`Failed to delete: ${data.documentPath}`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Failed to delete: ${data.documentPath}`, LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
|
||||
}
|
||||
@@ -433,12 +434,12 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
displayName = `${json.name}`;
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger(`Configuration sync data: ${path} looks like manifest, but could not read the version`, LOG_LEVEL.INFO);
|
||||
Logger(`Configuration sync data: ${path} looks like manifest, but could not read the version`, LOG_LEVEL_INFO);
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger(`The file ${path} could not be encoded`);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
const mtime = stat.mtime;
|
||||
@@ -465,7 +466,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
async storeCustomizationFiles(path: FilePath, termOverRide?: string) {
|
||||
const term = termOverRide || this.plugin.deviceAndVaultName;
|
||||
if (term == "") {
|
||||
Logger("We have to configure the device name", LOG_LEVEL.NOTICE);
|
||||
Logger("We have to configure the device name", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
const vf = this.filenameToUnifiedKey(path, term);
|
||||
@@ -501,7 +502,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
for (const target of fileTargets) {
|
||||
const data = await this.makeEntryFromFile(target);
|
||||
if (data == false) {
|
||||
// Logger(`Config: skipped: ${target} `, LOG_LEVEL.VERBOSE);
|
||||
// Logger(`Config: skipped: ${target} `, LOG_LEVEL_VERBOSE);
|
||||
continue;
|
||||
}
|
||||
if (data.version) {
|
||||
@@ -543,7 +544,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
};
|
||||
} else {
|
||||
if (old.mtime == mtime) {
|
||||
// Logger(`STORAGE --> DB:${file.path}: (hidden) Not changed`, LOG_LEVEL.VERBOSE);
|
||||
// Logger(`STORAGE --> DB:${file.path}: (hidden) Not changed`, LOG_LEVEL_VERBOSE);
|
||||
return true;
|
||||
}
|
||||
saveData =
|
||||
@@ -564,7 +565,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
return ret;
|
||||
} catch (ex) {
|
||||
Logger(`STORAGE --> DB:${prefixedFileName}: (config) Failed`);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
@@ -591,11 +592,11 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
|
||||
|
||||
async scanAllConfigFiles(showMessage: boolean) {
|
||||
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
|
||||
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||
Logger("Scanning customizing files.", logLevel, "scan-all-config");
|
||||
const term = this.plugin.deviceAndVaultName;
|
||||
if (term == "") {
|
||||
Logger("We have to configure the device name", LOG_LEVEL.NOTICE);
|
||||
Logger("We have to configure the device name", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
const filesAll = await this.scanInternalFiles();
|
||||
@@ -643,7 +644,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
Logger(`STORAGE -x> DB:${prefixedFileName}: (config) Done`);
|
||||
} catch (ex) {
|
||||
Logger(`STORAGE -x> DB:${prefixedFileName}: (config) Failed`);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Notice, normalizePath, type PluginManifest } from "./deps";
|
||||
import { type EntryDoc, type LoadedEntry, LOG_LEVEL, type InternalFileEntry, type FilePathWithPrefix, type FilePath } from "./lib/src/types";
|
||||
import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "./lib/src/types";
|
||||
import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "./types";
|
||||
import { Parallels, delay, isDocContentSame } from "./lib/src/utils";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
@@ -44,7 +44,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
Logger("Synchronizing hidden files done");
|
||||
} catch (ex) {
|
||||
Logger("Synchronizing hidden files failed");
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,7 +161,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
const commonBase = revFrom._revs_info.filter(e => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo).first()?.rev ?? "";
|
||||
const result = await this.plugin.mergeObject(path, commonBase, doc._rev, conflictedRev);
|
||||
if (result) {
|
||||
Logger(`Object merge:${path}`, LOG_LEVEL.INFO);
|
||||
Logger(`Object merge:${path}`, LOG_LEVEL_INFO);
|
||||
const filename = stripAllPrefixes(path);
|
||||
const isExists = await this.app.vault.adapter.exists(filename);
|
||||
if (!isExists) {
|
||||
@@ -174,7 +174,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
await this.localDatabase.removeRaw(id, revB);
|
||||
return this.resolveConflictOnInternalFile(path);
|
||||
} else {
|
||||
Logger(`Object merge is not applicable.`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Object merge is not applicable.`, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
|
||||
const docAMerge = await this.localDatabase.getDBEntry(path, { rev: revA });
|
||||
@@ -203,7 +203,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
return this.resolveConflictOnInternalFile(path);
|
||||
} catch (ex) {
|
||||
Logger(`Failed to resolve conflict (Hidden): ${path}`);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -211,7 +211,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
//TODO: Tidy up. Even though it is experimental feature, So dirty...
|
||||
async syncInternalFilesAndDatabase(direction: "push" | "pull" | "safe" | "pullForce" | "pushForce", showMessage: boolean, files: InternalFileInfo[] | false = false, targetFiles: string[] | false = false) {
|
||||
await this.resolveConflictOnInternalFiles();
|
||||
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
|
||||
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||
Logger("Scanning hidden files.", logLevel, "sync_internal");
|
||||
const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns
|
||||
.replace(/\n| /g, "")
|
||||
@@ -273,6 +273,9 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
if (!filename) continue;
|
||||
if (ignorePatterns.some(e => filename.match(e)))
|
||||
continue;
|
||||
if (!await this.plugin.isIgnoredByIgnoreFiles(filename)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const fileOnStorage = filename in filesMap ? filesMap[filename] : undefined;
|
||||
const fileOnDatabase = filename in filesOnDBMap ? filesOnDBMap[filename] : undefined;
|
||||
@@ -355,12 +358,12 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
a.appendChild(a.createEl("a", null, (anchor) => {
|
||||
anchor.text = "HERE";
|
||||
anchor.addEventListener("click", async () => {
|
||||
Logger(`Unloading plugin: ${updatePluginName}`, LOG_LEVEL.NOTICE, "plugin-reload-" + updatePluginId);
|
||||
Logger(`Unloading plugin: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId);
|
||||
// @ts-ignore
|
||||
await this.app.plugins.unloadPlugin(updatePluginId);
|
||||
// @ts-ignore
|
||||
await this.app.plugins.loadPlugin(updatePluginId);
|
||||
Logger(`Plugin reloaded: ${updatePluginName}`, LOG_LEVEL.NOTICE, "plugin-reload-" + updatePluginId);
|
||||
Logger(`Plugin reloaded: ${updatePluginName}`, LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId);
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -391,7 +394,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger("Error on checking plugin status.");
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
|
||||
}
|
||||
|
||||
@@ -431,6 +434,9 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
}
|
||||
|
||||
async storeInternalFileToDatabase(file: InternalFileInfo, forceWrite = false) {
|
||||
if (!await this.plugin.isIgnoredByIgnoreFiles(file.path)) {
|
||||
return
|
||||
}
|
||||
const id = await this.path2id(file.path, ICHeader);
|
||||
const prefixedFileName = addPrefix(file.path, ICHeader);
|
||||
const contentBin = await this.app.vault.adapter.readBinary(file.path);
|
||||
@@ -439,7 +445,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
content = await arrayBufferToBase64(contentBin);
|
||||
} catch (ex) {
|
||||
Logger(`The file ${file.path} could not be encoded`);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
const mtime = file.mtime;
|
||||
@@ -462,7 +468,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
};
|
||||
} else {
|
||||
if (isDocContentSame(old.data, content) && !forceWrite) {
|
||||
// Logger(`STORAGE --> DB:${file.path}: (hidden) Not changed`, LOG_LEVEL.VERBOSE);
|
||||
// Logger(`STORAGE --> DB:${file.path}: (hidden) Not changed`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
saveData =
|
||||
@@ -482,7 +488,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
return ret;
|
||||
} catch (ex) {
|
||||
Logger(`STORAGE --> DB:${file.path}: (hidden) Failed`);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@@ -492,6 +498,9 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
const id = await this.path2id(filename, ICHeader);
|
||||
const prefixedFileName = addPrefix(filename, ICHeader);
|
||||
const mtime = new Date().getTime();
|
||||
if (!await this.plugin.isIgnoredByIgnoreFiles(filename)) {
|
||||
return
|
||||
}
|
||||
await runWithLock("file-" + prefixedFileName, false, async () => {
|
||||
try {
|
||||
const old = await this.localDatabase.getDBEntryMeta(prefixedFileName, null, true) as InternalFileEntry | false;
|
||||
@@ -526,7 +535,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
Logger(`STORAGE -x> DB:${filename}: (hidden) Done`);
|
||||
} catch (ex) {
|
||||
Logger(`STORAGE -x> DB:${filename}: (hidden) Failed`);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@@ -535,7 +544,9 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
async extractInternalFileFromDatabase(filename: FilePath, force = false) {
|
||||
const isExists = await this.app.vault.adapter.exists(filename);
|
||||
const prefixedFileName = addPrefix(filename, ICHeader);
|
||||
|
||||
if (!await this.plugin.isIgnoredByIgnoreFiles(filename)) {
|
||||
return;
|
||||
}
|
||||
return await runWithLock("file-" + prefixedFileName, false, async () => {
|
||||
try {
|
||||
// Check conflicted status
|
||||
@@ -545,7 +556,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
throw new Error(`File not found on database.:${filename}`);
|
||||
// Prevent overwrite for Prevent overwriting while some conflicted revision exists.
|
||||
if (fileOnDB?._conflicts?.length) {
|
||||
Logger(`Hidden file ${filename} has conflicted revisions, to keep in safe, writing to storage has been prevented`, LOG_LEVEL.INFO);
|
||||
Logger(`Hidden file ${filename} has conflicted revisions, to keep in safe, writing to storage has been prevented`, LOG_LEVEL_INFO);
|
||||
return;
|
||||
}
|
||||
const deleted = "deleted" in fileOnDB ? fileOnDB.deleted : false;
|
||||
@@ -559,8 +570,8 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
//@ts-ignore internalAPI
|
||||
await app.vault.adapter.reconcileInternalFile(filename);
|
||||
} catch (ex) {
|
||||
Logger("Failed to call internal API(reconcileInternalFile)", LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger("Failed to call internal API(reconcileInternalFile)", LOG_LEVEL_VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -572,8 +583,8 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
//@ts-ignore internalAPI
|
||||
await app.vault.adapter.reconcileInternalFile(filename);
|
||||
} catch (ex) {
|
||||
Logger("Failed to call internal API(reconcileInternalFile)", LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger("Failed to call internal API(reconcileInternalFile)", LOG_LEVEL_VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
Logger(`STORAGE <-- DB:${filename}: written (hidden,new${force ? ", force" : ""})`);
|
||||
return true;
|
||||
@@ -581,7 +592,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
const contentBin = await this.app.vault.adapter.readBinary(filename);
|
||||
const content = await arrayBufferToBase64(contentBin);
|
||||
if (content == fileOnDB.data && !force) {
|
||||
// Logger(`STORAGE <-- DB:${filename}: skipped (hidden) Not changed`, LOG_LEVEL.VERBOSE);
|
||||
// Logger(`STORAGE <-- DB:${filename}: skipped (hidden) Not changed`, LOG_LEVEL_VERBOSE);
|
||||
return true;
|
||||
}
|
||||
await this.app.vault.adapter.writeBinary(filename, base64ToArrayBuffer(fileOnDB.data), { mtime: fileOnDB.mtime, ctime: fileOnDB.ctime });
|
||||
@@ -589,8 +600,8 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
//@ts-ignore internalAPI
|
||||
await app.vault.adapter.reconcileInternalFile(filename);
|
||||
} catch (ex) {
|
||||
Logger("Failed to call internal API(reconcileInternalFile)", LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger("Failed to call internal API(reconcileInternalFile)", LOG_LEVEL_VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
Logger(`STORAGE <-- DB:${filename}: written (hidden, overwrite${force ? ", force" : ""})`);
|
||||
return true;
|
||||
@@ -598,7 +609,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger(`STORAGE <-- DB:${filename}: written (hidden, overwrite${force ? ", force" : ""}) Failed`);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@@ -608,7 +619,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
|
||||
showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry): Promise<boolean> {
|
||||
return runWithLock("conflict:merge-data", false, () => new Promise((res) => {
|
||||
Logger("Opening data-merging dialog", LOG_LEVEL.VERBOSE);
|
||||
Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE);
|
||||
const docs = [docA, docB];
|
||||
const path = stripAllPrefixes(docA.path);
|
||||
const modal = new JsonResolveModal(this.app, path, [docA, docB], async (keep, result) => {
|
||||
@@ -644,8 +655,8 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
//@ts-ignore internalAPI
|
||||
await app.vault.adapter.reconcileInternalFile(filename);
|
||||
} catch (ex) {
|
||||
Logger("Failed to call internal API(reconcileInternalFile)", LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger("Failed to call internal API(reconcileInternalFile)", LOG_LEVEL_VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
Logger(`STORAGE <-- DB:${filename}: written (hidden,merged)`);
|
||||
}
|
||||
@@ -656,7 +667,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
res(true);
|
||||
} catch (ex) {
|
||||
Logger("Could not merge conflicted json");
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
res(false);
|
||||
}
|
||||
});
|
||||
@@ -680,6 +691,9 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
const result: InternalFileInfo[] = [];
|
||||
for (const f of files) {
|
||||
const w = await f;
|
||||
if (!await this.plugin.isIgnoredByIgnoreFiles(w.path)) {
|
||||
continue
|
||||
}
|
||||
result.push({
|
||||
...w,
|
||||
...w.stat
|
||||
@@ -698,12 +712,18 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
) {
|
||||
|
||||
const w = await this.app.vault.adapter.list(path);
|
||||
let files = [
|
||||
const filesSrc = [
|
||||
...w.files
|
||||
.filter((e) => !ignoreList.some((ee) => e.endsWith(ee)))
|
||||
.filter((e) => !filter || filter.some((ee) => e.match(ee)))
|
||||
.filter((e) => !ignoreFilter || ignoreFilter.every((ee) => !e.match(ee))),
|
||||
];
|
||||
let files = [] as string[];
|
||||
for (const file of filesSrc) {
|
||||
if (!await this.plugin.isIgnoredByIgnoreFiles(file)) {
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
L1: for (const v of w.folders) {
|
||||
for (const ignore of ignoreList) {
|
||||
@@ -714,6 +734,9 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
if (ignoreFilter && ignoreFilter.some(e => v.match(e))) {
|
||||
continue L1;
|
||||
}
|
||||
if (!await this.plugin.isIgnoredByIgnoreFiles(v)) {
|
||||
continue L1;
|
||||
}
|
||||
files = files.concat(await this.getFiles(v, ignoreList, filter, ignoreFilter));
|
||||
}
|
||||
return files;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { normalizePath, type PluginManifest } from "./deps";
|
||||
import type { DocumentID, EntryDoc, FilePathWithPrefix, LoadedEntry } from "./lib/src/types";
|
||||
import { LOG_LEVEL } from "./lib/src/types";
|
||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "./lib/src/types";
|
||||
import { type PluginDataEntry, PERIODIC_PLUGIN_SWEEP, type PluginList, type DevicePluginList, PSCHeader, PSCHeaderEnd } from "./types";
|
||||
import { getDocData, isDocContentSame } from "./lib/src/utils";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
@@ -79,7 +79,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||
Logger("Scanning plugins done");
|
||||
} catch (ex) {
|
||||
Logger("Scanning plugins failed");
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -148,7 +148,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||
});
|
||||
NewNotice(fragment, 10000);
|
||||
} else {
|
||||
Logger("Everything is up to date.", LOG_LEVEL.NOTICE);
|
||||
Logger("Everything is up to date.", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,9 +165,9 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||
specificPlugin = manifests.find(e => e.dir.endsWith("/" + specificPluginPath))?.id ?? "";
|
||||
}
|
||||
await runWithLock("sweepplugin", true, async () => {
|
||||
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
|
||||
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||
if (!this.deviceAndVaultName) {
|
||||
Logger("You have to set your device name.", LOG_LEVEL.NOTICE);
|
||||
Logger("You have to set your device name.", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
Logger("Scanning plugins", logLevel);
|
||||
@@ -176,7 +176,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||
endkey: `ps:${this.deviceAndVaultName}-${specificPlugin}\u{10ffff}`,
|
||||
include_docs: true,
|
||||
});
|
||||
// Logger("OLD DOCS.", LOG_LEVEL.VERBOSE);
|
||||
// Logger("OLD DOCS.", LOG_LEVEL_VERBOSE);
|
||||
// sweep current plugin.
|
||||
const procs = manifests.map(async (m) => {
|
||||
const pluginDataEntryID = `ps:${this.deviceAndVaultName}-${m.id}` as DocumentID;
|
||||
@@ -184,7 +184,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||
if (specificPlugin && m.id != specificPlugin) {
|
||||
return;
|
||||
}
|
||||
Logger(`Reading plugin:${m.name}(${m.id})`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Reading plugin:${m.name}(${m.id})`, LOG_LEVEL_VERBOSE);
|
||||
const path = normalizePath(m.dir) + "/";
|
||||
const adapter = this.app.vault.adapter;
|
||||
const files = ["manifest.json", "main.js", "styles.css", "data.json"];
|
||||
@@ -222,7 +222,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||
datatype: "plain",
|
||||
type: "plain"
|
||||
};
|
||||
Logger(`check diff:${m.name}(${m.id})`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`check diff:${m.name}(${m.id})`, LOG_LEVEL_VERBOSE);
|
||||
await runWithLock("plugin-" + m.id, false, async () => {
|
||||
const old = await this.localDatabase.getDBEntry(p._id as string as FilePathWithPrefix /* This also should be explained */, null, false, false);
|
||||
if (old !== false) {
|
||||
@@ -237,7 +237,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||
Logger(`Plugin saved:${m.name}`, logLevel);
|
||||
});
|
||||
} catch (ex) {
|
||||
Logger(`Plugin save failed:${m.name}`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Plugin save failed:${m.name}`, LOG_LEVEL_NOTICE);
|
||||
} finally {
|
||||
oldDocs.rows = oldDocs.rows.filter((e) => e.id != pluginDataEntryID);
|
||||
}
|
||||
@@ -259,7 +259,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||
}
|
||||
return e.doc;
|
||||
});
|
||||
Logger(`Deleting old plugin:(${delDocs.length})`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Deleting old plugin:(${delDocs.length})`, LOG_LEVEL_VERBOSE);
|
||||
await this.localDatabase.bulkDocsRaw(delDocs);
|
||||
Logger(`Scan plugin done.`, logLevel);
|
||||
});
|
||||
@@ -274,15 +274,15 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||
if (stat) {
|
||||
// @ts-ignore
|
||||
await this.app.plugins.unloadPlugin(plugin.manifest.id);
|
||||
Logger(`Unload plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Unload plugin:${plugin.manifest.id}`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
if (plugin.dataJson)
|
||||
await adapter.write(pluginTargetFolderPath + "data.json", plugin.dataJson);
|
||||
Logger("wrote:" + pluginTargetFolderPath + "data.json", LOG_LEVEL.NOTICE);
|
||||
Logger("wrote:" + pluginTargetFolderPath + "data.json", LOG_LEVEL_NOTICE);
|
||||
if (stat) {
|
||||
// @ts-ignore
|
||||
await this.app.plugins.loadPlugin(plugin.manifest.id);
|
||||
Logger(`Load plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Load plugin:${plugin.manifest.id}`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -294,7 +294,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||
if (stat) {
|
||||
// @ts-ignore
|
||||
await this.app.plugins.unloadPlugin(plugin.manifest.id);
|
||||
Logger(`Unload plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Unload plugin:${plugin.manifest.id}`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
|
||||
const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/";
|
||||
@@ -309,7 +309,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||
if (stat) {
|
||||
// @ts-ignore
|
||||
await this.app.plugins.loadPlugin(plugin.manifest.id);
|
||||
Logger(`Load plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Load plugin:${plugin.manifest.id}`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type EntryDoc, type ObsidianLiveSyncSettings, LOG_LEVEL, DEFAULT_SETTINGS } from "./lib/src/types";
|
||||
import { type EntryDoc, type ObsidianLiveSyncSettings, DEFAULT_SETTINGS, LOG_LEVEL_NOTICE } from "./lib/src/types";
|
||||
import { configURIBase } from "./types";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
||||
@@ -8,6 +8,7 @@ import { LiveSyncCommands } from "./LiveSyncCommands";
|
||||
import { delay } from "./lib/src/utils";
|
||||
import { confirmWithMessage } from "./dialogs";
|
||||
import { Platform } from "./deps";
|
||||
import { fetchAllUsedChunks } from "./lib/src/utils_couchdb";
|
||||
|
||||
export class SetupLiveSync extends LiveSyncCommands {
|
||||
onunload() { }
|
||||
@@ -54,7 +55,7 @@ export class SetupLiveSync extends LiveSyncCommands {
|
||||
const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase, false));
|
||||
const uri = `${configURIBase}${encryptedSetting}`;
|
||||
await navigator.clipboard.writeText(uri);
|
||||
Logger("Setup URI copied to clipboard", LOG_LEVEL.NOTICE);
|
||||
Logger("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
async command_copySetupURIFull() {
|
||||
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "", true);
|
||||
@@ -64,14 +65,14 @@ export class SetupLiveSync extends LiveSyncCommands {
|
||||
const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase, false));
|
||||
const uri = `${configURIBase}${encryptedSetting}`;
|
||||
await navigator.clipboard.writeText(uri);
|
||||
Logger("Setup URI copied to clipboard", LOG_LEVEL.NOTICE);
|
||||
Logger("Setup URI copied to clipboard", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
async command_openSetupURI() {
|
||||
const setupURI = await askString(this.app, "Easy setup", "Set up URI", `${configURIBase}aaaaa`);
|
||||
if (setupURI === false)
|
||||
return;
|
||||
if (!setupURI.startsWith(`${configURIBase}`)) {
|
||||
Logger("Set up URI looks wrong.", LOG_LEVEL.NOTICE);
|
||||
Logger("Set up URI looks wrong.", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
const config = decodeURIComponent(setupURI.substring(configURIBase.length));
|
||||
@@ -102,6 +103,11 @@ export class SetupLiveSync extends LiveSyncCommands {
|
||||
const setupManually = "Leave everything to me";
|
||||
newSettingW.syncInternalFiles = false;
|
||||
newSettingW.usePluginSync = false;
|
||||
// Migrate completely obsoleted configuration.
|
||||
if (!newSettingW.useIndexedDBAdapter) {
|
||||
newSettingW.useIndexedDBAdapter = true;
|
||||
}
|
||||
|
||||
const setupType = await askSelectString(this.app, "How would you like to set it up?", [setupAsNew, setupAgain, setupJustImport, setupManually]);
|
||||
if (setupType == setupJustImport) {
|
||||
this.plugin.settings = newSettingW;
|
||||
@@ -134,13 +140,13 @@ export class SetupLiveSync extends LiveSyncCommands {
|
||||
await this.plugin.replicate(true);
|
||||
await this.plugin.markRemoteUnlocked();
|
||||
}
|
||||
Logger("Configuration loaded.", LOG_LEVEL.NOTICE);
|
||||
Logger("Configuration loaded.", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
if (keepLocalDB == "no" && keepRemoteDB == "no") {
|
||||
const reset = await askYesNo(this.app, "Drop everything?");
|
||||
if (reset != "yes") {
|
||||
Logger("Cancelled", LOG_LEVEL.NOTICE);
|
||||
Logger("Cancelled", LOG_LEVEL_NOTICE);
|
||||
this.plugin.settings = oldConf;
|
||||
return;
|
||||
}
|
||||
@@ -175,17 +181,17 @@ export class SetupLiveSync extends LiveSyncCommands {
|
||||
}
|
||||
}
|
||||
|
||||
Logger("Configuration loaded.", LOG_LEVEL.NOTICE);
|
||||
Logger("Configuration loaded.", LOG_LEVEL_NOTICE);
|
||||
} else {
|
||||
Logger("Cancelled.", LOG_LEVEL.NOTICE);
|
||||
Logger("Cancelled.", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger("Couldn't parse or decrypt configuration uri.", LOG_LEVEL.NOTICE);
|
||||
Logger("Couldn't parse or decrypt configuration uri.", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
}
|
||||
|
||||
suspendExtraSync() {
|
||||
Logger("Hidden files and plugin synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL.NOTICE)
|
||||
Logger("Hidden files and plugin synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL_NOTICE)
|
||||
this.plugin.settings.syncInternalFiles = false;
|
||||
this.plugin.settings.usePluginSync = false;
|
||||
this.plugin.settings.autoSweepPlugins = false;
|
||||
@@ -230,7 +236,7 @@ Of course, we are able to disable these features.`
|
||||
return;
|
||||
}
|
||||
if (mode != "CUSTOMIZE") {
|
||||
Logger("Gathering files for enabling Hidden File Sync", LOG_LEVEL.NOTICE);
|
||||
Logger("Gathering files for enabling Hidden File Sync", LOG_LEVEL_NOTICE);
|
||||
if (mode == "FETCH") {
|
||||
await this.plugin.addOnHiddenFileSync.syncInternalFilesAndDatabase("pullForce", true);
|
||||
} else if (mode == "OVERWRITE") {
|
||||
@@ -240,7 +246,7 @@ Of course, we are able to disable these features.`
|
||||
}
|
||||
this.plugin.settings.syncInternalFiles = true;
|
||||
await this.plugin.saveSettings();
|
||||
Logger(`Done! Restarting the app is strongly recommended!`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Done! Restarting the app is strongly recommended!`, LOG_LEVEL_NOTICE);
|
||||
} else if (mode == "CUSTOMIZE") {
|
||||
if (!this.plugin.deviceAndVaultName) {
|
||||
let name = await askString(this.app, "Device name", "Please set this device name", `desktop`);
|
||||
@@ -284,6 +290,26 @@ Of course, we are able to disable these features.`
|
||||
this.plugin.settings.syncAfterMerge = false;
|
||||
//this.suspendExtraSync();
|
||||
}
|
||||
async suspendReflectingDatabase() {
|
||||
if (this.plugin.settings.doNotSuspendOnFetching) return;
|
||||
Logger(`Suspending reflection: Database and storage changes will not be reflected in each other until completely finished the fetching.`, LOG_LEVEL_NOTICE);
|
||||
this.plugin.settings.suspendParseReplicationResult = true;
|
||||
this.plugin.settings.suspendFileWatching = true;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
async resumeReflectingDatabase() {
|
||||
if (this.plugin.settings.doNotSuspendOnFetching) return;
|
||||
Logger(`Database and storage reflection has been resumed!`, LOG_LEVEL_NOTICE);
|
||||
this.plugin.settings.suspendParseReplicationResult = false;
|
||||
this.plugin.settings.suspendFileWatching = false;
|
||||
await this.plugin.saveSettings();
|
||||
if (this.plugin.settings.readChunksOnline) {
|
||||
await this.plugin.syncAllFiles(true);
|
||||
await this.plugin.loadQueuedFiles();
|
||||
// Start processing
|
||||
this.plugin.procQueuedFiles();
|
||||
}
|
||||
}
|
||||
async askUseNewAdapter() {
|
||||
if (!this.plugin.settings.useIndexedDBAdapter) {
|
||||
const message = `Now this plugin has been configured to use the old database adapter for keeping compatibility. Do you want to deactivate it?`;
|
||||
@@ -297,9 +323,22 @@ Of course, we are able to disable these features.`
|
||||
}
|
||||
}
|
||||
}
|
||||
async fetchRemoteChunks() {
|
||||
if (!this.plugin.settings.doNotSuspendOnFetching && this.plugin.settings.readChunksOnline) {
|
||||
Logger(`Fetching chunks`, LOG_LEVEL_NOTICE);
|
||||
const remoteDB = await this.plugin.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.plugin.getIsMobile(), true);
|
||||
if (typeof remoteDB == "string") {
|
||||
Logger(remoteDB, LOG_LEVEL_NOTICE);
|
||||
} else {
|
||||
await fetchAllUsedChunks(this.localDatabase.localDatabase, remoteDB.db);
|
||||
}
|
||||
Logger(`Fetching chunks done`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
}
|
||||
async fetchLocal() {
|
||||
this.suspendExtraSync();
|
||||
this.askUseNewAdapter();
|
||||
await this.suspendReflectingDatabase();
|
||||
await this.plugin.realizeSettingSyncMode();
|
||||
await this.plugin.resetLocalDatabase();
|
||||
await delay(1000);
|
||||
@@ -310,6 +349,8 @@ Of course, we are able to disable these features.`
|
||||
await this.plugin.replicateAllFromServer(true);
|
||||
await delay(1000);
|
||||
await this.plugin.replicateAllFromServer(true);
|
||||
await this.fetchRemoteChunks();
|
||||
await this.resumeReflectingDatabase();
|
||||
await this.askHiddenFileConfiguration({ enableFetch: true });
|
||||
}
|
||||
async rebuildRemote() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { App, Modal } from "./deps";
|
||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
||||
import { diff_result } from "./lib/src/types";
|
||||
import { type diff_result } from "./lib/src/types";
|
||||
import { escapeStringToHTML } from "./lib/src/strbin";
|
||||
|
||||
export class ConflictResolveModal extends Modal {
|
||||
@@ -30,11 +30,11 @@ export class ConflictResolveModal extends Modal {
|
||||
const x1 = v[0];
|
||||
const x2 = v[1];
|
||||
if (x1 == DIFF_DELETE) {
|
||||
diff += "<span class='deleted'>" + escapeStringToHTML(x2) + "</span>";
|
||||
diff += "<span class='deleted'>" + escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") + "</span>";
|
||||
} else if (x1 == DIFF_EQUAL) {
|
||||
diff += "<span class='normal'>" + escapeStringToHTML(x2) + "</span>";
|
||||
diff += "<span class='normal'>" + escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") + "</span>";
|
||||
} else if (x1 == DIFF_INSERT) {
|
||||
diff += "<span class='added'>" + escapeStringToHTML(x2) + "</span>";
|
||||
diff += "<span class='added'>" + escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") + "</span>";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,23 +48,26 @@ export class ConflictResolveModal extends Modal {
|
||||
`;
|
||||
contentEl.createEl("button", { text: "Keep A" }, (e) => {
|
||||
e.addEventListener("click", async () => {
|
||||
await this.callback(this.result.right.rev);
|
||||
const callback = this.callback;
|
||||
this.callback = null;
|
||||
this.close();
|
||||
await callback(this.result.right.rev);
|
||||
});
|
||||
});
|
||||
contentEl.createEl("button", { text: "Keep B" }, (e) => {
|
||||
e.addEventListener("click", async () => {
|
||||
await this.callback(this.result.left.rev);
|
||||
const callback = this.callback;
|
||||
this.callback = null;
|
||||
this.close();
|
||||
await callback(this.result.left.rev);
|
||||
});
|
||||
});
|
||||
contentEl.createEl("button", { text: "Concat both" }, (e) => {
|
||||
e.addEventListener("click", async () => {
|
||||
await this.callback("");
|
||||
const callback = this.callback;
|
||||
this.callback = null;
|
||||
this.close();
|
||||
await callback("");
|
||||
});
|
||||
});
|
||||
contentEl.createEl("button", { text: "Not now" }, (e) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TFile, Modal, App, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_pat
|
||||
import { getPathFromTFile, isValidPath } from "./utils";
|
||||
import { base64ToArrayBuffer, base64ToString, escapeStringToHTML } from "./lib/src/strbin";
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
import { type DocumentID, type FilePathWithPrefix, type LoadedEntry, LOG_LEVEL } from "./lib/src/types";
|
||||
import { type DocumentID, type FilePathWithPrefix, type LoadedEntry, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "./lib/src/types";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { isErrorOfMissingDoc } from "./lib/src/utils_couchdb";
|
||||
import { getDocData } from "./lib/src/utils";
|
||||
@@ -60,7 +60,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
this.contentView.setText(`History of this file was not recorded.`);
|
||||
} else {
|
||||
this.contentView.setText(`Error occurred.`);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,7 +178,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
e.addClass("mod-cta");
|
||||
e.addEventListener("click", async () => {
|
||||
await navigator.clipboard.writeText(this.currentText);
|
||||
Logger(`Old content copied to clipboard`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Old content copied to clipboard`, LOG_LEVEL_NOTICE);
|
||||
});
|
||||
});
|
||||
async function focusFile(path: string) {
|
||||
@@ -189,7 +189,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
const leaf = app.workspace.getLeaf(false);
|
||||
await leaf.openFile(targetFile);
|
||||
} else {
|
||||
Logger("The file could not view on the editor", LOG_LEVEL.NOTICE)
|
||||
Logger("The file could not view on the editor", LOG_LEVEL_NOTICE)
|
||||
}
|
||||
}
|
||||
buttons.createEl("button", { text: "Back to this revision" }, (e) => {
|
||||
@@ -198,7 +198,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
// const pathToWrite = this.plugin.id2path(this.id, true);
|
||||
const pathToWrite = stripPrefix(this.file);
|
||||
if (!isValidPath(pathToWrite)) {
|
||||
Logger("Path is not valid to write content.", LOG_LEVEL.INFO);
|
||||
Logger("Path is not valid to write content.", LOG_LEVEL_INFO);
|
||||
}
|
||||
if (this.currentDoc?.datatype == "plain") {
|
||||
await this.app.vault.adapter.write(pathToWrite, getDocData(this.currentDoc.data));
|
||||
@@ -210,7 +210,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
this.close();
|
||||
} else {
|
||||
|
||||
Logger(`Could not parse entry`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Could not parse entry`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ export class GlobalHistoryView extends ItemView {
|
||||
return "Vault history";
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-await
|
||||
async onOpen() {
|
||||
this.component = new GlobalHistoryComponent({
|
||||
target: this.contentEl,
|
||||
@@ -41,6 +42,7 @@ export class GlobalHistoryView extends ItemView {
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-await
|
||||
async onClose() {
|
||||
this.component.$destroy();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { App, Modal } from "./deps";
|
||||
import { FilePath, LoadedEntry } from "./lib/src/types";
|
||||
import { type FilePath, type LoadedEntry } from "./lib/src/types";
|
||||
import JsonResolvePane from "./JsonResolvePane.svelte";
|
||||
|
||||
export class JsonResolveModal extends Modal {
|
||||
@@ -41,7 +41,7 @@ export class JsonResolveModal extends Modal {
|
||||
nameA: this.nameA,
|
||||
nameB: this.nameB,
|
||||
defaultSelect: this.defaultSelect,
|
||||
callback: (keepRev, mergedStr) => this.UICallback(keepRev, mergedStr),
|
||||
callback: (keepRev: string, mergedStr: string) => this.UICallback(keepRev, mergedStr),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { deleteDB, IDBPDatabase, openDB } from "idb";
|
||||
import { deleteDB, type IDBPDatabase, openDB } from "idb";
|
||||
export interface KeyValueDatabase {
|
||||
get<T>(key: string): Promise<T>;
|
||||
set<T>(key: string, value: T): Promise<IDBValidKey>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AnyEntry, DocumentID, EntryDoc, EntryHasPath, FilePath, FilePathWithPrefix } from "./lib/src/types";
|
||||
import { type AnyEntry, type DocumentID, type EntryDoc, type EntryHasPath, type FilePath, type FilePathWithPrefix } from "./lib/src/types";
|
||||
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
||||
import type ObsidianLiveSyncPlugin from "./main";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import LogPaneComponent from "./LogPane.svelte";
|
||||
import type ObsidianLiveSyncPlugin from "./main";
|
||||
export const VIEW_TYPE_LOG = "log-log";
|
||||
// Show notes as like scroll.
|
||||
//Log view
|
||||
export class LogPaneView extends ItemView {
|
||||
|
||||
component: LogPaneComponent;
|
||||
@@ -32,6 +32,7 @@ export class LogPaneView extends ItemView {
|
||||
return "Self-hosted LiveSync Log";
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-await
|
||||
async onOpen() {
|
||||
this.component = new LogPaneComponent({
|
||||
target: this.contentEl,
|
||||
@@ -40,6 +41,7 @@ export class LogPaneView extends ItemView {
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-await
|
||||
async onClose() {
|
||||
this.component.$destroy();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, TextAreaComponent, MarkdownRenderer, stringifyYaml } from "./deps";
|
||||
import { DEFAULT_SETTINGS, LOG_LEVEL, type ObsidianLiveSyncSettings, type ConfigPassphraseStore, type RemoteDBSettings, type FilePathWithPrefix, type HashAlgorithm, type DocumentID } from "./lib/src/types";
|
||||
import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings, type ConfigPassphraseStore, type RemoteDBSettings, type FilePathWithPrefix, type HashAlgorithm, type DocumentID, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "./lib/src/types";
|
||||
import { delay } from "./lib/src/utils";
|
||||
import { Semaphore } from "./lib/src/semaphore";
|
||||
import { versionNumberString2Number } from "./lib/src/strbin";
|
||||
@@ -7,7 +7,7 @@ import { Logger } from "./lib/src/logger";
|
||||
import { checkSyncInfo, isCloudantURI } from "./lib/src/utils_couchdb.js";
|
||||
import { testCrypt } from "./lib/src/e2ee_v2";
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
import { balanceChunks, localDatabaseCleanUp, performRebuildDB, remoteDatabaseCleanup, requestToCouchDB } from "./utils";
|
||||
import { askYesNo, performRebuildDB, requestToCouchDB, scheduleTask } from "./utils";
|
||||
|
||||
|
||||
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
@@ -21,10 +21,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
async testConnection(): Promise<void> {
|
||||
const db = await this.plugin.replicator.connectRemoteCouchDBWithSetting(this.plugin.settings, this.plugin.isMobile, true);
|
||||
if (typeof db === "string") {
|
||||
this.plugin.addLog(`could not connect to ${this.plugin.settings.couchDB_URI} : ${this.plugin.settings.couchDB_DBNAME} \n(${db})`, LOG_LEVEL.NOTICE);
|
||||
this.plugin.addLog(`could not connect to ${this.plugin.settings.couchDB_URI} : ${this.plugin.settings.couchDB_DBNAME} \n(${db})`, LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
this.plugin.addLog(`Connected to ${db.info.db_name}`, LOG_LEVEL.NOTICE);
|
||||
this.plugin.addLog(`Connected to ${db.info.db_name}`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
@@ -111,7 +111,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
}
|
||||
|
||||
MarkdownRenderer.renderMarkdown(updateInformation, informationDivEl, "/", this.plugin);
|
||||
MarkdownRenderer.render(this.plugin.app, updateInformation, informationDivEl, "/", this.plugin);
|
||||
|
||||
|
||||
addScreenElement("100", containerInformationEl);
|
||||
@@ -133,12 +133,13 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
new Setting(setupWizardEl)
|
||||
.setName("Discard the existing configuration and set up")
|
||||
.addButton((text) => {
|
||||
// eslint-disable-next-line require-await
|
||||
text.setButtonText("Next").onClick(async () => {
|
||||
if (JSON.stringify(this.plugin.settings) != JSON.stringify(DEFAULT_SETTINGS)) {
|
||||
this.plugin.replicator.closeReplication();
|
||||
this.plugin.settings = { ...DEFAULT_SETTINGS };
|
||||
this.plugin.saveSettings();
|
||||
Logger("Configuration has been flushed, please open it again", LOG_LEVEL.NOTICE)
|
||||
Logger("Configuration has been flushed, please open it again", LOG_LEVEL_NOTICE)
|
||||
// @ts-ignore
|
||||
this.plugin.app.setting.close()
|
||||
} else {
|
||||
@@ -302,7 +303,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
const checkConfig = async () => {
|
||||
try {
|
||||
if (isCloudantURI(this.plugin.settings.couchDB_URI)) {
|
||||
Logger("This feature cannot be used with IBM Cloudant.", LOG_LEVEL.NOTICE);
|
||||
Logger("This feature cannot be used with IBM Cloudant.", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -334,11 +335,11 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
const res = await requestToCouchDB(this.plugin.settings.couchDB_URI, this.plugin.settings.couchDB_USER, this.plugin.settings.couchDB_PASSWORD, undefined, key, value);
|
||||
console.dir(res);
|
||||
if (res.status == 200) {
|
||||
Logger(`${title} successfully updated`, LOG_LEVEL.NOTICE);
|
||||
Logger(`${title} successfully updated`, LOG_LEVEL_NOTICE);
|
||||
checkResultDiv.removeChild(x);
|
||||
checkConfig();
|
||||
} else {
|
||||
Logger(`${title} failed`, LOG_LEVEL.NOTICE);
|
||||
Logger(`${title} failed`, LOG_LEVEL_NOTICE);
|
||||
Logger(res.text);
|
||||
}
|
||||
});
|
||||
@@ -451,7 +452,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
addResult("--Done--", ["ob-btn-config-head"]);
|
||||
addResult("If you have some trouble with Connection-check even though all Config-check has been passed, Please check your reverse proxy's configuration.", ["ob-btn-config-info"]);
|
||||
} catch (ex) {
|
||||
Logger(`Checking configuration failed`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Checking configuration failed`, LOG_LEVEL_NOTICE);
|
||||
Logger(ex);
|
||||
}
|
||||
};
|
||||
@@ -609,25 +610,25 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
console.dir(settingForCheck);
|
||||
const db = await this.plugin.replicator.connectRemoteCouchDBWithSetting(settingForCheck, this.plugin.isMobile, true);
|
||||
if (typeof db === "string") {
|
||||
Logger("Could not connect to the database.", LOG_LEVEL.NOTICE);
|
||||
Logger("Could not connect to the database.", LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
} else {
|
||||
if (await checkSyncInfo(db.db)) {
|
||||
// Logger("Database connected", LOG_LEVEL.NOTICE);
|
||||
// Logger("Database connected", LOG_LEVEL_NOTICE);
|
||||
return true;
|
||||
} else {
|
||||
Logger("Failed to read remote database", LOG_LEVEL.NOTICE);
|
||||
Logger("Failed to read remote database", LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
const applyEncryption = async (sendToServer: boolean) => {
|
||||
if (encrypt && passphrase == "") {
|
||||
Logger("If you enable encryption, you have to set the passphrase", LOG_LEVEL.NOTICE);
|
||||
Logger("If you enable encryption, you have to set the passphrase", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
if (encrypt && !(await testCrypt())) {
|
||||
Logger("WARNING! Your device would not support encryption.", LOG_LEVEL.NOTICE);
|
||||
Logger("WARNING! Your device would not support encryption.", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
if (!(await checkWorkingPassphrase()) && !sendToServer) {
|
||||
@@ -654,11 +655,11 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
const rebuildDB = async (method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice") => {
|
||||
if (encrypt && passphrase == "") {
|
||||
Logger("If you enable encryption, you have to set the passphrase", LOG_LEVEL.NOTICE);
|
||||
Logger("If you enable encryption, you have to set the passphrase", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
if (encrypt && !(await testCrypt())) {
|
||||
Logger("WARNING! Your device would not support encryption.", LOG_LEVEL.NOTICE);
|
||||
Logger("WARNING! Your device would not support encryption.", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
if (!encrypt) {
|
||||
@@ -670,7 +671,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
this.plugin.settings.passphrase = passphrase;
|
||||
this.plugin.settings.useDynamicIterationCount = useDynamicIterationCount;
|
||||
this.plugin.settings.usePathObfuscation = usePathObfuscation;
|
||||
Logger("All synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL.NOTICE)
|
||||
Logger("All synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL_NOTICE)
|
||||
await this.plugin.saveSettings();
|
||||
updateE2EControls();
|
||||
applyDisplayEnabled();
|
||||
@@ -882,7 +883,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
if (currentPreset == "") {
|
||||
Logger("Select any preset.", LOG_LEVEL.NOTICE);
|
||||
Logger("Select any preset.", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
const presetAllDisabled = {
|
||||
@@ -913,15 +914,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
...this.plugin.settings,
|
||||
...presetLiveSync
|
||||
}
|
||||
Logger("Synchronization setting configured as LiveSync.", LOG_LEVEL.NOTICE);
|
||||
Logger("Synchronization setting configured as LiveSync.", LOG_LEVEL_NOTICE);
|
||||
} else if (currentPreset == "PERIODIC") {
|
||||
this.plugin.settings = {
|
||||
...this.plugin.settings,
|
||||
...presetPeriodic
|
||||
}
|
||||
Logger("Synchronization setting configured as Periodic sync with batch database update.", LOG_LEVEL.NOTICE);
|
||||
Logger("Synchronization setting configured as Periodic sync with batch database update.", LOG_LEVEL_NOTICE);
|
||||
} else {
|
||||
Logger("All synchronization disabled.", LOG_LEVEL.NOTICE);
|
||||
Logger("All synchronization disabled.", LOG_LEVEL_NOTICE);
|
||||
this.plugin.settings = {
|
||||
...this.plugin.settings,
|
||||
...presetAllDisabled
|
||||
@@ -943,7 +944,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
await this.plugin.replicate(true);
|
||||
|
||||
Logger("All done! Please set up subsequent devices with 'Copy setup URI' and 'Open setup URI'.", LOG_LEVEL.NOTICE);
|
||||
Logger("All done! Please set up subsequent devices with 'Copy setup URI' and 'Open setup URI'.", LOG_LEVEL_NOTICE);
|
||||
// @ts-ignore
|
||||
this.plugin.app.commands.executeCommandById("obsidian-livesync:livesync-copysetupuri")
|
||||
}
|
||||
@@ -1330,7 +1331,38 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
return text;
|
||||
}
|
||||
);
|
||||
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("(Beta) Use ignore files")
|
||||
.setDesc("If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.")
|
||||
.setClass("wizardHidden")
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.useIgnoreFiles)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.useIgnoreFiles = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
return toggle;
|
||||
}
|
||||
);
|
||||
if (this.plugin.settings.useIgnoreFiles) {
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Ignore files")
|
||||
.setDesc("We can use multiple ignore files, e.g.) `.gitignore, .dockerignore`")
|
||||
.setClass("wizardHidden")
|
||||
.addTextArea((text) => {
|
||||
text
|
||||
.setValue(this.plugin.settings.ignoreFiles)
|
||||
.setPlaceholder(".gitignore, .dockerignore")
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.ignoreFiles = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
return text;
|
||||
}
|
||||
);
|
||||
}
|
||||
containerSyncSettingEl.createEl("h4", {
|
||||
text: sanitizeHTMLToDom(`Advanced settings`),
|
||||
}).addClass("wizardHidden");
|
||||
@@ -1468,7 +1500,7 @@ ${stringifyYaml(responseConfig)}
|
||||
${stringifyYaml(pluginConfig)}`;
|
||||
console.log(msgConfig);
|
||||
await navigator.clipboard.writeText(msgConfig);
|
||||
Logger(`Information has been copied to clipboard`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Information has been copied to clipboard`, LOG_LEVEL_NOTICE);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1521,11 +1553,11 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
Logger(`UPDATE DATABASE ${file.path}`);
|
||||
await this.plugin.updateIntoDB(file, false, null, true);
|
||||
i++;
|
||||
Logger(`${i}/${files.length}\n${file.path}`, LOG_LEVEL.NOTICE, "verify");
|
||||
Logger(`${i}/${files.length}\n${file.path}`, LOG_LEVEL_NOTICE, "verify");
|
||||
|
||||
} catch (ex) {
|
||||
i++;
|
||||
Logger(`Error while verifyAndRepair`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Error while verifyAndRepair`, LOG_LEVEL_NOTICE);
|
||||
Logger(ex);
|
||||
} finally {
|
||||
releaser();
|
||||
@@ -1533,7 +1565,7 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
}
|
||||
)(e));
|
||||
await Promise.all(processes);
|
||||
Logger("done", LOG_LEVEL.NOTICE, "verify");
|
||||
Logger("done", LOG_LEVEL_NOTICE, "verify");
|
||||
})
|
||||
);
|
||||
new Setting(containerHatchEl)
|
||||
@@ -1573,35 +1605,35 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
}
|
||||
const ret = await this.plugin.localDatabase.putRaw(newDoc, { force: true });
|
||||
if (ret.ok) {
|
||||
Logger(`${docName} has been converted as conflicted document`, LOG_LEVEL.NOTICE);
|
||||
Logger(`${docName} has been converted as conflicted document`, LOG_LEVEL_NOTICE);
|
||||
doc._deleted = true;
|
||||
if ((await this.plugin.localDatabase.putRaw(doc)).ok) {
|
||||
Logger(`Old ${docName} has been deleted`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
await this.plugin.showIfConflicted(docName as FilePathWithPrefix);
|
||||
} else {
|
||||
Logger(`Converting ${docName} Failed!`, LOG_LEVEL.NOTICE);
|
||||
Logger(ret, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Converting ${docName} Failed!`, LOG_LEVEL_NOTICE);
|
||||
Logger(ret, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
} catch (ex) {
|
||||
if (ex?.status == 404) {
|
||||
// We can perform this safely
|
||||
if ((await this.plugin.localDatabase.putRaw(newDoc)).ok) {
|
||||
Logger(`${docName} has been converted`, LOG_LEVEL.NOTICE);
|
||||
Logger(`${docName} has been converted`, LOG_LEVEL_NOTICE);
|
||||
doc._deleted = true;
|
||||
if ((await this.plugin.localDatabase.putRaw(doc)).ok) {
|
||||
Logger(`Old ${docName} has been deleted`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger(`Something went wrong on converting ${docName}`, LOG_LEVEL.NOTICE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Something went wrong on converting ${docName}`, LOG_LEVEL_NOTICE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
// Something wrong.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Logger(`Converting finished`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Converting finished`, LOG_LEVEL_NOTICE);
|
||||
}));
|
||||
new Setting(containerHatchEl)
|
||||
.setName("Suspend file watching")
|
||||
@@ -1610,6 +1642,27 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
toggle.setValue(this.plugin.settings.suspendFileWatching).onChange(async (value) => {
|
||||
this.plugin.settings.suspendFileWatching = value;
|
||||
await this.plugin.saveSettings();
|
||||
scheduleTask("configReload", 250, async () => {
|
||||
if (await askYesNo(this.app, "Do you want to restart and reload Obsidian now?") == "yes") {
|
||||
// @ts-ignore
|
||||
this.app.commands.executeCommandById("app:reload")
|
||||
}
|
||||
})
|
||||
})
|
||||
);
|
||||
new Setting(containerHatchEl)
|
||||
.setName("Suspend database reflecting")
|
||||
.setDesc("Stop reflecting database changes to storage files.")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.suspendParseReplicationResult).onChange(async (value) => {
|
||||
this.plugin.settings.suspendParseReplicationResult = value;
|
||||
await this.plugin.saveSettings();
|
||||
scheduleTask("configReload", 250, async () => {
|
||||
if (await askYesNo(this.app, "Do you want to restart and reload Obsidian now?") == "yes") {
|
||||
// @ts-ignore
|
||||
this.app.commands.executeCommandById("app:reload")
|
||||
}
|
||||
})
|
||||
})
|
||||
);
|
||||
new Setting(containerHatchEl)
|
||||
@@ -1706,12 +1759,12 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
button.setButtonText("Change")
|
||||
.onClick(async () => {
|
||||
if (this.plugin.settings.additionalSuffixOfDatabaseName == newDatabaseName) {
|
||||
Logger("Suffix was not changed.", LOG_LEVEL.NOTICE);
|
||||
Logger("Suffix was not changed.", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
this.plugin.settings.additionalSuffixOfDatabaseName = newDatabaseName;
|
||||
await this.plugin.saveSettings();
|
||||
Logger("Suffix has been changed. Reopening database...", LOG_LEVEL.NOTICE);
|
||||
Logger("Suffix has been changed. Reopening database...", LOG_LEVEL_NOTICE);
|
||||
await this.plugin.initializeDatabase();
|
||||
})
|
||||
})
|
||||
@@ -1731,6 +1784,16 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
)
|
||||
.setClass("wizardHidden");
|
||||
|
||||
|
||||
new Setting(containerHatchEl)
|
||||
.setName("Fetch database with previous behaviour")
|
||||
.setDesc("")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.doNotSuspendOnFetching).onChange(async (value) => {
|
||||
this.plugin.settings.doNotSuspendOnFetching = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
addScreenElement("50", containerHatchEl);
|
||||
|
||||
|
||||
@@ -1755,11 +1818,11 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
vaultName.setDisabled(this.plugin.settings.usePluginSync);
|
||||
// vaultName.setTooltip(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic ? "You could not change when you enabling auto scan." : "");
|
||||
};
|
||||
updateDisabledOfDeviceAndVaultName
|
||||
updateDisabledOfDeviceAndVaultName();
|
||||
new Setting(containerPluginSettings).setName("Enable customization sync").addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.usePluginSync).onChange(async (value) => {
|
||||
if (value && this.plugin.deviceAndVaultName.trim() == "") {
|
||||
Logger("We have to configure `Device name` to use this feature.", LOG_LEVEL.NOTICE);
|
||||
Logger("We have to configure `Device name` to use this feature.", LOG_LEVEL_NOTICE);
|
||||
toggle.setValue(false);
|
||||
return false;
|
||||
}
|
||||
@@ -1851,26 +1914,6 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
})
|
||||
)
|
||||
|
||||
new Setting(containerMaintenanceEl)
|
||||
.setName("(Beta) Clean the remote database")
|
||||
.setDesc("")
|
||||
.addButton((button) =>
|
||||
button.setButtonText("Count")
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await remoteDatabaseCleanup(this.plugin, true);
|
||||
})
|
||||
).addButton((button) =>
|
||||
button.setButtonText("Perform cleaning")
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
// @ts-ignore
|
||||
this.plugin.app.setting.close()
|
||||
await remoteDatabaseCleanup(this.plugin, false);
|
||||
await balanceChunks(this.plugin, false);
|
||||
})
|
||||
);
|
||||
|
||||
containerMaintenanceEl.createEl("h4", { text: "The local database" });
|
||||
|
||||
@@ -1887,26 +1930,6 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
})
|
||||
)
|
||||
|
||||
new Setting(containerMaintenanceEl)
|
||||
.setName("(Beta) Clean the local database")
|
||||
.setDesc("This feature requires disabling 'Use an old adapter for compatibility'")
|
||||
.addButton((button) =>
|
||||
button.setButtonText("Count")
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await localDatabaseCleanUp(this.plugin, false, true);
|
||||
})
|
||||
).addButton((button) =>
|
||||
button.setButtonText("Perform cleaning")
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
// @ts-ignore
|
||||
this.plugin.app.setting.close()
|
||||
await localDatabaseCleanUp(this.plugin, false, false);
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerMaintenanceEl)
|
||||
.setName("Discard local database to reset or uninstall Self-hosted LiveSync")
|
||||
.addButton((button) =>
|
||||
@@ -1922,6 +1945,25 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
|
||||
containerMaintenanceEl.createEl("h4", { text: "Both databases" });
|
||||
|
||||
new Setting(containerMaintenanceEl)
|
||||
.setName("(Beta2) Clean up databases")
|
||||
.setDesc("Delete unused chunks to shrink the database. This feature requires disabling 'Use an old adapter for compatibility'")
|
||||
.addButton((button) =>
|
||||
button.setButtonText("DryRun")
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await this.plugin.dryRunGC();
|
||||
})
|
||||
).addButton((button) =>
|
||||
button.setButtonText("Perform cleaning")
|
||||
.setDisabled(false)
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
// @ts-ignore
|
||||
this.plugin.app.setting.close()
|
||||
await this.plugin.dbGC();
|
||||
})
|
||||
);
|
||||
new Setting(containerMaintenanceEl)
|
||||
.setName("Rebuild everything")
|
||||
.setDesc("Rebuild local and remote database with local files.")
|
||||
@@ -1934,19 +1976,6 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
await rebuildDB("rebuildBothByThisDevice");
|
||||
})
|
||||
)
|
||||
|
||||
new Setting(containerMaintenanceEl)
|
||||
.setName("(Beta) Complement each other with possible missing chunks.")
|
||||
.setDesc("")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Balance")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await balanceChunks(this.plugin, false);
|
||||
})
|
||||
)
|
||||
applyDisplayEnabled();
|
||||
addScreenElement("70", containerMaintenanceEl);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { PluginDataExDisplay } from "./CmdConfigSync";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { versionNumberString2Number } from "./lib/src/strbin";
|
||||
import { type FilePath, LOG_LEVEL } from "./lib/src/types";
|
||||
import { type FilePath, LOG_LEVEL_NOTICE } from "./lib/src/types";
|
||||
import { getDocData } from "./lib/src/utils";
|
||||
import type ObsidianLiveSyncPlugin from "./main";
|
||||
import { askString, scheduleTask } from "./utils";
|
||||
@@ -229,7 +229,7 @@
|
||||
const duplicateTermName = await askString(plugin.app, "Duplicate", "device name", "");
|
||||
if (duplicateTermName) {
|
||||
if (duplicateTermName.contains("/")) {
|
||||
Logger(`We can not use "/" to the device name`, LOG_LEVEL.NOTICE);
|
||||
Logger(`We can not use "/" to the device name`, LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
const key = `${plugin.app.vault.configDir}/${local.files[0].filename}`;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Plugin_2, TAbstractFile, TFile, TFolder } from "./deps";
|
||||
import { Plugin, TAbstractFile, TFile, TFolder } from "./deps";
|
||||
import { isPlainText, shouldBeIgnored } from "./lib/src/path";
|
||||
import { getGlobalStore } from "./lib/src/store";
|
||||
import { FilePath, ObsidianLiveSyncSettings } from "./lib/src/types";
|
||||
import { FileEventItem, FileEventType, FileInfo, InternalFileInfo, queueItem } from "./types";
|
||||
import { type FilePath, type ObsidianLiveSyncSettings } from "./lib/src/types";
|
||||
import { type FileEventItem, type FileEventType, type FileInfo, type InternalFileInfo, type queueItem } from "./types";
|
||||
import { recentlyTouched } from "./utils";
|
||||
|
||||
|
||||
@@ -12,12 +12,13 @@ export abstract class StorageEventManager {
|
||||
abstract getQueueLength(): number;
|
||||
}
|
||||
|
||||
type LiveSyncForStorageEventManager = Plugin_2 &
|
||||
type LiveSyncForStorageEventManager = Plugin &
|
||||
{
|
||||
settings: ObsidianLiveSyncSettings
|
||||
ignoreFiles: string[],
|
||||
} & {
|
||||
isTargetFile: (file: string | TAbstractFile) => boolean,
|
||||
procFileEvent: (applyBatch?: boolean) => Promise<boolean>
|
||||
isTargetFile: (file: string | TAbstractFile) => Promise<boolean>,
|
||||
procFileEvent: (applyBatch?: boolean) => Promise<boolean>,
|
||||
};
|
||||
|
||||
|
||||
@@ -35,12 +36,12 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
this.watchVaultDelete = this.watchVaultDelete.bind(this);
|
||||
this.watchVaultRename = this.watchVaultRename.bind(this);
|
||||
this.watchVaultRawEvents = this.watchVaultRawEvents.bind(this);
|
||||
plugin.registerEvent(app.vault.on("modify", this.watchVaultChange));
|
||||
plugin.registerEvent(app.vault.on("delete", this.watchVaultDelete));
|
||||
plugin.registerEvent(app.vault.on("rename", this.watchVaultRename));
|
||||
plugin.registerEvent(app.vault.on("create", this.watchVaultCreate));
|
||||
plugin.registerEvent(plugin.app.vault.on("modify", this.watchVaultChange));
|
||||
plugin.registerEvent(plugin.app.vault.on("delete", this.watchVaultDelete));
|
||||
plugin.registerEvent(plugin.app.vault.on("rename", this.watchVaultRename));
|
||||
plugin.registerEvent(plugin.app.vault.on("create", this.watchVaultCreate));
|
||||
//@ts-ignore : Internal API
|
||||
plugin.registerEvent(app.vault.on("raw", this.watchVaultRawEvents));
|
||||
plugin.registerEvent(plugin.app.vault.on("raw", this.watchVaultRawEvents));
|
||||
}
|
||||
|
||||
watchVaultCreate(file: TAbstractFile, ctx?: any) {
|
||||
@@ -64,9 +65,18 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
}
|
||||
// Watch raw events (Internal API)
|
||||
watchVaultRawEvents(path: FilePath) {
|
||||
if (this.plugin.settings.useIgnoreFiles && this.plugin.ignoreFiles.some(e => path.endsWith(e.trim()))) {
|
||||
// If it is one of ignore files, refresh the cached one.
|
||||
this.plugin.isTargetFile(path).then(() => this._watchVaultRawEvents(path));
|
||||
} else {
|
||||
this._watchVaultRawEvents(path);
|
||||
}
|
||||
}
|
||||
|
||||
_watchVaultRawEvents(path: FilePath) {
|
||||
if (!this.plugin.settings.syncInternalFiles && !this.plugin.settings.usePluginSync) return;
|
||||
if (!this.plugin.settings.watchInternalFileChanges) return;
|
||||
if (!path.startsWith(app.vault.configDir)) return;
|
||||
if (!path.startsWith(this.plugin.app.vault.configDir)) return;
|
||||
const ignorePatterns = this.plugin.settings.syncInternalFilesIgnorePatterns
|
||||
.replace(/\n| /g, "")
|
||||
.split(",").filter(e => e).map(e => new RegExp(e, "i"));
|
||||
@@ -77,7 +87,6 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
file: { path, mtime: 0, ctime: 0, size: 0 }
|
||||
}], null);
|
||||
}
|
||||
|
||||
// Cache file and waiting to can be proceed.
|
||||
async appendWatchEvent(params: { type: FileEventType, file: TAbstractFile | InternalFileInfo, oldPath?: string }[], ctx?: any) {
|
||||
let forcePerform = false;
|
||||
@@ -90,7 +99,7 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
const file = param.file;
|
||||
const oldPath = param.oldPath;
|
||||
if (file instanceof TFolder) continue;
|
||||
if (!this.plugin.isTargetFile(file.path)) continue;
|
||||
if (!await this.plugin.isTargetFile(file.path)) continue;
|
||||
if (this.plugin.settings.suspendFileWatching) continue;
|
||||
|
||||
let cache: null | string | ArrayBuffer;
|
||||
@@ -100,11 +109,11 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
continue;
|
||||
}
|
||||
if (!isPlainText(file.name)) {
|
||||
cache = await app.vault.readBinary(file);
|
||||
cache = await this.plugin.app.vault.readBinary(file);
|
||||
} else {
|
||||
// cache = await this.app.vault.read(file);
|
||||
cache = await app.vault.cachedRead(file);
|
||||
if (!cache) cache = await app.vault.read(file);
|
||||
cache = await this.plugin.app.vault.cachedRead(file);
|
||||
if (!cache) cache = await this.plugin.app.vault.read(file);
|
||||
}
|
||||
}
|
||||
if (type == "DELETE" || type == "RENAME") {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FilePath } from "./lib/src/types";
|
||||
|
||||
export {
|
||||
addIcon, App, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, Notice, Platform, Plugin, PluginSettingTab, Plugin_2, requestUrl, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder,
|
||||
addIcon, App, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, Notice, Platform, Plugin, PluginSettingTab, requestUrl, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder,
|
||||
parseYaml, ItemView, WorkspaceLeaf
|
||||
} from "obsidian";
|
||||
export type { DataWriteOptions, PluginManifest, RequestUrlParam, RequestUrlResponse } from "obsidian";
|
||||
|
||||
@@ -183,8 +183,9 @@ export class MessageBox extends Modal {
|
||||
})
|
||||
contentEl.createEl("h1", { text: this.title });
|
||||
const div = contentEl.createDiv();
|
||||
MarkdownRenderer.renderMarkdown(this.contentMd, div, "/", null);
|
||||
MarkdownRenderer.render(this.plugin.app, this.contentMd, div, "/", this.plugin);
|
||||
const buttonSetting = new Setting(contentEl);
|
||||
buttonSetting.controlEl.style.flexWrap = "wrap";
|
||||
for (const button of this.buttons) {
|
||||
buttonSetting.addButton((btn) => {
|
||||
btn
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: aacf942453...6efd115e0e
442
src/main.ts
442
src/main.ts
@@ -2,7 +2,7 @@ const isDebug = false;
|
||||
|
||||
import { type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "./deps";
|
||||
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, type RequestUrlParam, type RequestUrlResponse, requestUrl } from "./deps";
|
||||
import { type EntryDoc, type LoadedEntry, type ObsidianLiveSyncSettings, type diff_check_result, type diff_result_leaf, type EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, type diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, SALT_OF_PASSPHRASE, type ConfigPassphraseStore, type CouchDBConnection, FLAGMD_REDFLAG2, FLAGMD_REDFLAG3, PREFIXMD_LOGFILE, type DatabaseConnectingStatus, type EntryHasPath, type DocumentID, type FilePathWithPrefix, type FilePath, type AnyEntry } from "./lib/src/types";
|
||||
import { type EntryDoc, type LoadedEntry, type ObsidianLiveSyncSettings, type diff_check_result, type diff_result_leaf, type EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, type diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, SALT_OF_PASSPHRASE, type ConfigPassphraseStore, type CouchDBConnection, FLAGMD_REDFLAG2, FLAGMD_REDFLAG3, PREFIXMD_LOGFILE, type DatabaseConnectingStatus, type EntryHasPath, type DocumentID, type FilePathWithPrefix, type FilePath, type AnyEntry, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT, LOG_LEVEL_VERBOSE } from "./lib/src/types";
|
||||
import { type InternalFileInfo, type queueItem, type CacheData, type FileEventItem, FileWatchEventQueueMax } from "./types";
|
||||
import { arrayToChunkedArray, getDocData, isDocContentSame } from "./lib/src/utils";
|
||||
import { Logger, setGlobalLogFunction } from "./lib/src/logger";
|
||||
@@ -10,15 +10,15 @@ import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
||||
import { ConflictResolveModal } from "./ConflictResolveModal";
|
||||
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
||||
import { applyPatch, cancelAllPeriodicTask, cancelAllTasks, cancelTask, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, flattenObject, path2id, scheduleTask, tryParseJSON, createFile, modifyFile, isValidPath, getAbstractFileByPath, touch, recentlyTouched, isInternalMetadata, isPluginMetadata, stripInternalMetadataPrefix, isChunk, askSelectString, askYesNo, askString, PeriodicProcessor, clearTouched, getPath, getPathWithoutPrefix, getPathFromTFile, localDatabaseCleanUp, balanceChunks, performRebuildDB } from "./utils";
|
||||
import { applyPatch, cancelAllPeriodicTask, cancelAllTasks, cancelTask, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, flattenObject, path2id, scheduleTask, tryParseJSON, createFile, modifyFile, isValidPath, getAbstractFileByPath, touch, recentlyTouched, isInternalMetadata, isPluginMetadata, stripInternalMetadataPrefix, isChunk, askSelectString, askYesNo, askString, PeriodicProcessor, clearTouched, getPath, getPathWithoutPrefix, getPathFromTFile, performRebuildDB } from "./utils";
|
||||
import { encrypt, tryDecrypt } from "./lib/src/e2ee_v2";
|
||||
import { enableEncryption, isCloudantURI, isErrorOfMissingDoc, isValidRemoteCouchDBURI } from "./lib/src/utils_couchdb";
|
||||
import { balanceChunkPurgedDBs, enableEncryption, isCloudantURI, isErrorOfMissingDoc, isValidRemoteCouchDBURI, purgeUnreferencedChunks } from "./lib/src/utils_couchdb";
|
||||
import { getGlobalStore, ObservableStore, observeStores } from "./lib/src/store";
|
||||
import { lockStore, logMessageStore, logStore, type LogEntry } from "./lib/src/stores";
|
||||
import { setNoticeClass } from "./lib/src/wrapper";
|
||||
import { base64ToString, versionNumberString2Number, base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin";
|
||||
import { addPrefix, isPlainText, shouldBeIgnored, stripAllPrefixes } from "./lib/src/path";
|
||||
import { runWithLock } from "./lib/src/lock";
|
||||
import { addPrefix, isAcceptedAll, isPlainText, shouldBeIgnored, stripAllPrefixes } from "./lib/src/path";
|
||||
import { isLockAcquired, runWithLock } from "./lib/src/lock";
|
||||
import { Semaphore } from "./lib/src/semaphore";
|
||||
import { StorageEventManager, StorageEventManagerObsidian } from "./StorageEventManager";
|
||||
import { LiveSyncLocalDB, type LiveSyncLocalDBEnv } from "./lib/src/LiveSyncLocalDB";
|
||||
@@ -32,6 +32,7 @@ import { confirmWithMessage } from "./dialogs";
|
||||
import { GlobalHistoryView, VIEW_TYPE_GLOBAL_HISTORY } from "./GlobalHistoryView";
|
||||
import { LogPaneView, VIEW_TYPE_LOG } from "./LogPaneView";
|
||||
import { mapAllTasksWithConcurrencyLimit, processAllTasksWithConcurrencyLimit } from "./lib/src/task";
|
||||
import { LRUCache } from "./lib/src/LRUCache";
|
||||
|
||||
setNoticeClass(Notice);
|
||||
|
||||
@@ -124,7 +125,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
// over 10MB
|
||||
if (isCloudantURI(uri)) {
|
||||
this.last_successful_post = false;
|
||||
Logger("This request should fail on IBM Cloudant.", LOG_LEVEL.VERBOSE);
|
||||
Logger("This request should fail on IBM Cloudant.", LOG_LEVEL_VERBOSE);
|
||||
throw new Error("This request should fail on IBM Cloudant.");
|
||||
}
|
||||
}
|
||||
@@ -156,7 +157,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
} else {
|
||||
this.last_successful_post = true;
|
||||
}
|
||||
Logger(`HTTP:${method}${size} to:${localURL} -> ${r.status}`, LOG_LEVEL.DEBUG);
|
||||
Logger(`HTTP:${method}${size} to:${localURL} -> ${r.status}`, LOG_LEVEL_DEBUG);
|
||||
|
||||
return new Response(r.arrayBuffer, {
|
||||
headers: r.headers,
|
||||
@@ -164,7 +165,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
statusText: `${r.status}`,
|
||||
});
|
||||
} catch (ex) {
|
||||
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL_VERBOSE);
|
||||
// limit only in bulk_docs.
|
||||
if (url.toString().indexOf("_bulk_docs") !== -1) {
|
||||
this.last_successful_post = false;
|
||||
@@ -183,10 +184,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
} else {
|
||||
this.last_successful_post = true;
|
||||
}
|
||||
Logger(`HTTP:${method}${size} to:${localURL} -> ${response.status}`, LOG_LEVEL.DEBUG);
|
||||
Logger(`HTTP:${method}${size} to:${localURL} -> ${response.status}`, LOG_LEVEL_DEBUG);
|
||||
return response;
|
||||
} catch (ex) {
|
||||
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL_VERBOSE);
|
||||
// limit only in bulk_docs.
|
||||
if (url.toString().indexOf("_bulk_docs") !== -1) {
|
||||
this.last_successful_post = false;
|
||||
@@ -213,7 +214,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
if (ex.name == "TypeError" && ex.message == "Failed to fetch") {
|
||||
msg += "\n**Note** This error caused by many reasons. The only sure thing is you didn't touch the server.\nTo check details, open inspector.";
|
||||
}
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
@@ -240,6 +241,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
createPouchDBInstance<T>(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database<T> {
|
||||
if (this.settings.useIndexedDBAdapter) {
|
||||
options.adapter = "indexeddb";
|
||||
//@ts-ignore :missing def
|
||||
options.purged_infos_limit = 1;
|
||||
return new PouchDB(name + "-indexeddb", options);
|
||||
}
|
||||
return new PouchDB(name, options);
|
||||
@@ -343,7 +346,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
notes.sort((a, b) => b.mtime - a.mtime);
|
||||
const notesList = notes.map(e => e.dispPath);
|
||||
if (notesList.length == 0) {
|
||||
Logger("There are no conflicted documents", LOG_LEVEL.NOTICE);
|
||||
Logger("There are no conflicted documents", LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
}
|
||||
const target = await askSelectString(this.app, "File to view History", notesList);
|
||||
@@ -395,7 +398,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
async onLayoutReady() {
|
||||
this.registerFileWatchEvents();
|
||||
if (!this.localDatabase.isReady) {
|
||||
Logger(`Something went wrong! The local database is not ready`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Something went wrong! The local database is not ready`, LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -407,7 +410,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
this.settings.suspendFileWatching = true;
|
||||
await this.saveSettings();
|
||||
if (this.isRedFlag2Raised()) {
|
||||
Logger(`${FLAGMD_REDFLAG2} has been detected! Self-hosted LiveSync suspends all sync and rebuild everything.`, LOG_LEVEL.NOTICE);
|
||||
Logger(`${FLAGMD_REDFLAG2} has been detected! Self-hosted LiveSync suspends all sync and rebuild everything.`, LOG_LEVEL_NOTICE);
|
||||
await this.addOnSetup.rebuildEverything();
|
||||
await this.deleteRedFlag2();
|
||||
if (await askYesNo(this.app, "Do you want to disable Suspend file watching and restart obsidian now?") == "yes") {
|
||||
@@ -417,25 +420,30 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
this.app.commands.executeCommandById("app:reload")
|
||||
}
|
||||
} else if (this.isRedFlag3Raised()) {
|
||||
Logger(`${FLAGMD_REDFLAG3} has been detected! Self-hosted LiveSync will discard the local database and fetch everything from the remote once again.`, LOG_LEVEL.NOTICE);
|
||||
Logger(`${FLAGMD_REDFLAG3} has been detected! Self-hosted LiveSync will discard the local database and fetch everything from the remote once again.`, LOG_LEVEL_NOTICE);
|
||||
await this.addOnSetup.fetchLocal();
|
||||
await this.deleteRedFlag3();
|
||||
if (await askYesNo(this.app, "Do you want to disable Suspend file watching and restart obsidian now?") == "yes") {
|
||||
this.settings.suspendFileWatching = false;
|
||||
await this.saveSettings();
|
||||
// @ts-ignore
|
||||
this.app.commands.executeCommandById("app:reload")
|
||||
if (this.settings.suspendFileWatching) {
|
||||
if (await askYesNo(this.app, "Do you want to disable Suspend file watching and restart obsidian now?") == "yes") {
|
||||
this.settings.suspendFileWatching = false;
|
||||
await this.saveSettings();
|
||||
// @ts-ignore
|
||||
this.app.commands.executeCommandById("app:reload")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.settings.writeLogToTheFile = true;
|
||||
await this.openDatabase();
|
||||
const warningMessage = "The red flag is raised! The whole initialize steps are skipped, and any file changes are not captured.";
|
||||
Logger(warningMessage, LOG_LEVEL.NOTICE);
|
||||
Logger(warningMessage, LOG_LEVEL_NOTICE);
|
||||
this.setStatusBarText(warningMessage);
|
||||
}
|
||||
} else {
|
||||
if (this.settings.suspendFileWatching) {
|
||||
Logger("'Suspend file watching' turned on. Are you sure this is what you intended? Every modification on the vault will be ignored.", LOG_LEVEL.NOTICE);
|
||||
Logger("'Suspend file watching' turned on. Are you sure this is what you intended? Every modification on the vault will be ignored.", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
if (this.settings.suspendParseReplicationResult) {
|
||||
Logger("'Suspend database reflecting' turned on. Are you sure this is what you intended? Every replicated change will be postponed until disabling this option.", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
const isInitialized = await this.initializeDatabase(false, false);
|
||||
if (!isInitialized) {
|
||||
@@ -450,8 +458,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
}
|
||||
this.scanStat();
|
||||
} catch (ex) {
|
||||
Logger("Error while loading Self-hosted LiveSync", LOG_LEVEL.NOTICE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger("Error while loading Self-hosted LiveSync", LOG_LEVEL_NOTICE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,20 +468,20 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
*/
|
||||
async scanStat() {
|
||||
const notes: { path: string, mtime: number }[] = [];
|
||||
Logger(`Additional safety scan..`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Additional safety scan..`, LOG_LEVEL_VERBOSE);
|
||||
for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) {
|
||||
if (!("_conflicts" in doc)) continue;
|
||||
notes.push({ path: this.getPath(doc), mtime: doc.mtime });
|
||||
}
|
||||
if (notes.length > 0) {
|
||||
Logger(`Some files have been left conflicted! Please resolve them by "Pick a file to resolve conflict". The list is written in the log.`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Some files have been left conflicted! Please resolve them by "Pick a file to resolve conflict". The list is written in the log.`, LOG_LEVEL_NOTICE);
|
||||
for (const note of notes) {
|
||||
Logger(`Conflicted: ${note.path}`);
|
||||
}
|
||||
} else {
|
||||
Logger(`There are no conflicted files`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`There are no conflicted files`, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
Logger(`Additional safety scan done`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Additional safety scan done`, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
|
||||
async onload() {
|
||||
@@ -494,7 +502,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
|
||||
const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000);
|
||||
if (lastVersion > this.settings.lastReadUpdates) {
|
||||
Logger("Self-hosted LiveSync has undergone a major upgrade. Please open the setting dialog, and check the information pane.", LOG_LEVEL.NOTICE);
|
||||
Logger("Self-hosted LiveSync has undergone a major upgrade. Please open the setting dialog, and check the information pane.", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
//@ts-ignore
|
||||
if (this.app.isMobile) {
|
||||
@@ -582,10 +590,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
callback: async () => {
|
||||
if (this.settings.liveSync) {
|
||||
this.settings.liveSync = false;
|
||||
Logger("LiveSync Disabled.", LOG_LEVEL.NOTICE);
|
||||
Logger("LiveSync Disabled.", LOG_LEVEL_NOTICE);
|
||||
} else {
|
||||
this.settings.liveSync = true;
|
||||
Logger("LiveSync Enabled.", LOG_LEVEL.NOTICE);
|
||||
Logger("LiveSync Enabled.", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
await this.realizeSettingSyncMode();
|
||||
this.saveSettings();
|
||||
@@ -597,10 +605,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
callback: async () => {
|
||||
if (this.suspended) {
|
||||
this.suspended = false;
|
||||
Logger("Self-hosted LiveSync resumed", LOG_LEVEL.NOTICE);
|
||||
Logger("Self-hosted LiveSync resumed", LOG_LEVEL_NOTICE);
|
||||
} else {
|
||||
this.suspended = true;
|
||||
Logger("Self-hosted LiveSync suspended", LOG_LEVEL.NOTICE);
|
||||
Logger("Self-hosted LiveSync suspended", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
await this.realizeSettingSyncMode();
|
||||
this.saveSettings();
|
||||
@@ -765,7 +773,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
|
||||
const passphrase = await this.getPassphrase(settings);
|
||||
if (passphrase === false) {
|
||||
Logger("Could not determine passphrase to save data.json! You probably make the configuration sure again!", LOG_LEVEL.URGENT);
|
||||
Logger("Could not determine passphrase to save data.json! You probably make the configuration sure again!", LOG_LEVEL_URGENT);
|
||||
return "";
|
||||
}
|
||||
const dec = await encrypt(src, passphrase + SALT_OF_PASSPHRASE, false);
|
||||
@@ -781,7 +789,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
const settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()) as ObsidianLiveSyncSettings;
|
||||
const passphrase = await this.getPassphrase(settings);
|
||||
if (passphrase === false) {
|
||||
Logger("Could not determine passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", LOG_LEVEL.URGENT);
|
||||
Logger("Could not determine passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", LOG_LEVEL_URGENT);
|
||||
} else {
|
||||
if (settings.encryptedCouchDBConnection) {
|
||||
const keys = ["couchDB_URI", "couchDB_USER", "couchDB_PASSWORD", "couchDB_DBNAME"] as (keyof CouchDBConnection)[];
|
||||
@@ -793,7 +801,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger("Could not decrypt passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", LOG_LEVEL.URGENT);
|
||||
Logger("Could not decrypt passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", LOG_LEVEL_URGENT);
|
||||
for (const key of keys) {
|
||||
settings[key] = "";
|
||||
}
|
||||
@@ -805,7 +813,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
if (decrypted) {
|
||||
settings.passphrase = decrypted;
|
||||
} else {
|
||||
Logger("Could not decrypt passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", LOG_LEVEL.URGENT);
|
||||
Logger("Could not decrypt passphrase for reading data.json! DO NOT synchronize with the remote before making sure your configuration is!", LOG_LEVEL_URGENT);
|
||||
settings.passphrase = "";
|
||||
}
|
||||
}
|
||||
@@ -833,10 +841,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
}
|
||||
}
|
||||
if (isCloudantURI(this.settings.couchDB_URI) && this.settings.customChunkSize != 0) {
|
||||
Logger("Configuration verification founds problems with your configuration. This has been fixed automatically. But you may already have data that cannot be synchronised. If this is the case, please rebuild everything.", LOG_LEVEL.NOTICE)
|
||||
Logger("Configuration verification founds problems with your configuration. This has been fixed automatically. But you may already have data that cannot be synchronised. If this is the case, please rebuild everything.", LOG_LEVEL_NOTICE)
|
||||
this.settings.customChunkSize = 0;
|
||||
}
|
||||
this.deviceAndVaultName = localStorage.getItem(lsKey) || "";
|
||||
this.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
|
||||
}
|
||||
|
||||
triggerRealizeSettingSyncMode() {
|
||||
@@ -847,9 +856,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.getVaultName();
|
||||
|
||||
localStorage.setItem(lsKey, this.deviceAndVaultName || "");
|
||||
|
||||
const settings = { ...this.settings };
|
||||
if (this.usedPassphrase == "" && !await this.getPassphrase(settings)) {
|
||||
Logger("Could not determine passphrase for saving data.json! Our data.json have insecure items!", LOG_LEVEL.NOTICE);
|
||||
Logger("Could not determine passphrase for saving data.json! Our data.json have insecure items!", LOG_LEVEL_NOTICE);
|
||||
} else {
|
||||
if (settings.couchDB_PASSWORD != "" || settings.couchDB_URI != "" || settings.couchDB_USER != "" || settings.couchDB_DBNAME) {
|
||||
const connectionSetting: CouchDBConnection = {
|
||||
@@ -871,6 +881,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
}
|
||||
await this.saveData(settings);
|
||||
this.localDatabase.settings = this.settings;
|
||||
this.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
|
||||
this.triggerRealizeSettingSyncMode();
|
||||
}
|
||||
|
||||
@@ -960,12 +971,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
} else {
|
||||
const targetFile = this.app.vault.getAbstractFileByPath(file.path);
|
||||
if (!(targetFile instanceof TFile)) {
|
||||
Logger(`Target file was not found: ${file.path}`, LOG_LEVEL.INFO);
|
||||
Logger(`Target file was not found: ${file.path}`, LOG_LEVEL_INFO);
|
||||
continue;
|
||||
}
|
||||
//TODO: check from cache time.
|
||||
if (file.mtime == last) {
|
||||
Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -974,7 +985,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
const keyD1 = `file-last-proc-DELETED-${file.path}`;
|
||||
await this.kvDB.set(keyD1, mtime);
|
||||
if (!await this.updateIntoDB(targetFile, false, cache)) {
|
||||
Logger(`DB -> STORAGE: failed, cancel the relative operations: ${targetFile.path}`, LOG_LEVEL.INFO);
|
||||
Logger(`DB -> STORAGE: failed, cancel the relative operations: ${targetFile.path}`, LOG_LEVEL_INFO);
|
||||
// cancel running queues and remove one of atomic operation
|
||||
this.vaultManager.cancelRelativeEvent(queue);
|
||||
continue;
|
||||
@@ -1031,7 +1042,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
}
|
||||
|
||||
async watchVaultRenameAsync(file: TFile, oldFile: any, cache?: CacheData) {
|
||||
Logger(`${oldFile} renamed to ${file.path}`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`${oldFile} renamed to ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||
if (file instanceof TFile) {
|
||||
try {
|
||||
// Logger(`RENAMING.. ${file.path} into db`);
|
||||
@@ -1039,7 +1050,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
// Logger(`deleted ${oldFile} from db`);
|
||||
await this.deleteFromDBbyPath(oldFile);
|
||||
} else {
|
||||
Logger(`Could not save new file: ${file.path} `, LOG_LEVEL.NOTICE);
|
||||
Logger(`Could not save new file: ${file.path} `, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
} catch (ex) {
|
||||
Logger(ex);
|
||||
@@ -1052,14 +1063,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
|
||||
lastLog = "";
|
||||
// eslint-disable-next-line require-await
|
||||
async addLog(message: any, level: LOG_LEVEL = LOG_LEVEL.INFO, key = "") {
|
||||
if (level == LOG_LEVEL.DEBUG && !isDebug) {
|
||||
async addLog(message: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key = "") {
|
||||
if (level == LOG_LEVEL_DEBUG && !isDebug) {
|
||||
return;
|
||||
}
|
||||
if (level < LOG_LEVEL.INFO && this.settings && this.settings.lessInformationInLog) {
|
||||
if (level < LOG_LEVEL_INFO && this.settings && this.settings.lessInformationInLog) {
|
||||
return;
|
||||
}
|
||||
if (this.settings && !this.settings.showVerboseLog && level == LOG_LEVEL.VERBOSE) {
|
||||
if (this.settings && !this.settings.showVerboseLog && level == LOG_LEVEL_VERBOSE) {
|
||||
return;
|
||||
}
|
||||
const vaultName = this.getVaultName();
|
||||
@@ -1085,7 +1096,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
logMessageStore.apply(e => [...e, newMessage].slice(-100));
|
||||
this.setStatusBarText(null, messageContent);
|
||||
|
||||
if (level >= LOG_LEVEL.NOTICE) {
|
||||
if (level >= LOG_LEVEL_NOTICE) {
|
||||
if (!key) key = messageContent;
|
||||
if (key in this.notifies) {
|
||||
// @ts-ignore
|
||||
@@ -1152,13 +1163,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
if (shouldBeIgnored(path)) {
|
||||
return;
|
||||
}
|
||||
if (!this.isTargetFile(path)) return;
|
||||
if (!await this.isTargetFile(path)) return;
|
||||
if (docEntry._deleted || docEntry.deleted) {
|
||||
// This occurs not only when files are deleted, but also when conflicts are resolved.
|
||||
// We have to check no other revisions are left.
|
||||
const lastDocs = await this.localDatabase.getDBEntry(path);
|
||||
if (path != file.path) {
|
||||
Logger(`delete skipped: ${file.path} :Not exactly matched`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`delete skipped: ${file.path} :Not exactly matched`, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
if (lastDocs === false) {
|
||||
await this.deleteVaultItem(file);
|
||||
@@ -1166,7 +1177,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
// it perhaps delete some revisions.
|
||||
// may be we have to reload this
|
||||
await this.pullFile(path, null, true);
|
||||
Logger(`delete skipped:${file.path}`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`delete skipped:${file.path}`, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1177,12 +1188,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
if (doc === false) return;
|
||||
const msg = `DB -> STORAGE (${mode}${force ? ",force" : ""},${doc.datatype}) `;
|
||||
if (doc.datatype != "newnote" && doc.datatype != "plain") {
|
||||
Logger(msg + "ERROR, Invalid datatype: " + path + "(" + doc.datatype + ")", LOG_LEVEL.NOTICE);
|
||||
Logger(msg + "ERROR, Invalid datatype: " + path + "(" + doc.datatype + ")", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
if (!force && localMtime >= docMtime) return;
|
||||
if (!isValidPath(path)) {
|
||||
Logger(msg + "ERROR, invalid path: " + path, LOG_LEVEL.NOTICE);
|
||||
Logger(msg + "ERROR, invalid path: " + path, LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
const writeData = doc.datatype == "newnote" ? base64ToArrayBuffer(doc.data) : getDocData(doc.data);
|
||||
@@ -1200,14 +1211,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
this.app.vault.trigger(mode, outFile);
|
||||
|
||||
} catch (ex) {
|
||||
Logger(msg + "ERROR, Could not write: " + path, LOG_LEVEL.NOTICE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(msg + "ERROR, Could not write: " + path, LOG_LEVEL_NOTICE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteVaultItem(file: TFile | TFolder) {
|
||||
if (file instanceof TFile) {
|
||||
if (!this.isTargetFile(file)) return;
|
||||
if (!await this.isTargetFile(file)) return;
|
||||
}
|
||||
const dir = file.parent;
|
||||
if (this.settings.trashInsteadDelete) {
|
||||
@@ -1251,7 +1262,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
try {
|
||||
const releaser = await semaphore.acquire(1);
|
||||
runWithLock(`dbchanged-${path}`, false, async () => {
|
||||
Logger(`Applying ${path} (${entry._id}: ${entry._rev}) change...`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Applying ${path} (${entry._id}: ${entry._rev}) change...`, LOG_LEVEL_VERBOSE);
|
||||
await this.handleDBChangedAsync(entry);
|
||||
Logger(`Applied ${path} (${entry._id}:${entry._rev}) change...`);
|
||||
}).finally(() => { releaser(); });
|
||||
@@ -1297,7 +1308,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
await this.doc2storage(doc, file);
|
||||
} else {
|
||||
if (!queueConflictCheck()) {
|
||||
Logger(`${this.getPath(change)} is conflicted, write to the storage has been pended.`, LOG_LEVEL.NOTICE);
|
||||
Logger(`${this.getPath(change)} is conflicted, write to the storage has been pended.`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1316,12 +1327,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
localStorage.setItem(lsKey, saveData);
|
||||
}
|
||||
async loadQueuedFiles() {
|
||||
const lsKey = "obsidian-livesync-queuefiles-" + this.getVaultName();
|
||||
const ids = JSON.parse(localStorage.getItem(lsKey) || "[]") as string[];
|
||||
const ret = await this.localDatabase.allDocsRaw<EntryDoc>({ keys: ids, include_docs: true });
|
||||
for (const doc of ret.rows) {
|
||||
if (doc.doc && !this.queuedFiles.some((e) => e.entry._id == doc.doc._id)) {
|
||||
await this.parseIncomingDoc(doc.doc as PouchDB.Core.ExistingDocument<EntryBody & PouchDB.Core.AllDocsMeta>);
|
||||
if (!this.settings.suspendParseReplicationResult) {
|
||||
const lsKey = "obsidian-livesync-queuefiles-" + this.getVaultName();
|
||||
const ids = [...new Set(JSON.parse(localStorage.getItem(lsKey) || "[]"))] as string[];
|
||||
const ret = await this.localDatabase.allDocsRaw<EntryDoc>({ keys: ids, include_docs: true });
|
||||
for (const doc of ret.rows) {
|
||||
if (doc.doc && !this.queuedFiles.some((e) => e.entry._id == doc.doc._id)) {
|
||||
await this.parseIncomingDoc(doc.doc as PouchDB.Core.ExistingDocument<EntryBody & PouchDB.Core.AllDocsMeta>);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1341,10 +1354,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
} else if (isValidPath(this.getPath(queue.entry))) {
|
||||
this.handleDBChanged(queue.entry);
|
||||
} else {
|
||||
Logger(`Skipped: ${queue.entry._id}`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Skipped: ${queue.entry._id}`, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
} else if (now > queue.timeout) {
|
||||
if (!queue.warned) Logger(`Timed out: ${queue.entry._id} could not collect ${queue.missingChildren.length} chunks. plugin keeps watching, but you have to check the file after the replication.`, LOG_LEVEL.NOTICE);
|
||||
if (!queue.warned) Logger(`Timed out: ${queue.entry._id} could not collect ${queue.missingChildren.length} chunks. plugin keeps watching, but you have to check the file after the replication.`, LOG_LEVEL_NOTICE);
|
||||
queue.warned = true;
|
||||
continue;
|
||||
}
|
||||
@@ -1376,13 +1389,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
}
|
||||
async parseIncomingDoc(doc: PouchDB.Core.ExistingDocument<EntryBody>) {
|
||||
const path = this.getPath(doc);
|
||||
if (!this.isTargetFile(path)) return;
|
||||
if (!await this.isTargetFile(path)) return;
|
||||
const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary.
|
||||
// Do not handle internal files if the feature has not been enabled.
|
||||
if (isInternalMetadata(doc._id) && !this.settings.syncInternalFiles) return;
|
||||
// It is better for your own safety, not to handle the following files
|
||||
const ignoreFiles = [
|
||||
"_design/replicate",
|
||||
"_design/chunks",
|
||||
FLAGMD_REDFLAG,
|
||||
FLAGMD_REDFLAG2,
|
||||
FLAGMD_REDFLAG3
|
||||
@@ -1399,7 +1413,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
const docMtime = ~~(doc.mtime / 1000);
|
||||
//TODO: some margin required.
|
||||
if (localMtime >= docMtime) {
|
||||
Logger(`${path} (${doc._id}, ${doc._rev}) Skipped, older than storage.`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`${path} (${doc._id}, ${doc._rev}) Skipped, older than storage.`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1415,7 +1429,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
if ((!this.settings.readChunksOnline) && "children" in doc) {
|
||||
const c = await this.localDatabase.collectChunksWithCache(doc.children as DocumentID[]);
|
||||
const missing = c.filter((e) => e.chunk === false).map((e) => e.id);
|
||||
if (missing.length > 0) Logger(`${path} (${doc._id}, ${doc._rev}) Queued (waiting ${missing.length} items)`, LOG_LEVEL.VERBOSE);
|
||||
if (missing.length > 0) Logger(`${path} (${doc._id}, ${doc._rev}) Queued (waiting ${missing.length} items)`, LOG_LEVEL_VERBOSE);
|
||||
newQueue.missingChildren = missing;
|
||||
this.queuedFiles.push(newQueue);
|
||||
} else {
|
||||
@@ -1431,25 +1445,45 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
L1:
|
||||
for (const change of docsSorted) {
|
||||
if (isChunk(change._id)) {
|
||||
await this.parseIncomingChunk(change);
|
||||
if (!this.settings.suspendParseReplicationResult) {
|
||||
await this.parseIncomingChunk(change);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (const proc of this.addOns) {
|
||||
if (await proc.parseReplicationResultItem(change)) {
|
||||
continue L1;
|
||||
if (!this.settings.suspendParseReplicationResult) {
|
||||
for (const proc of this.addOns) {
|
||||
if (await proc.parseReplicationResultItem(change)) {
|
||||
continue L1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (change._id == SYNCINFO_ID) {
|
||||
continue;
|
||||
}
|
||||
if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo") {
|
||||
await this.parseIncomingDoc(change);
|
||||
if (change._id.startsWith("_design")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo") {
|
||||
if (this.settings.suspendParseReplicationResult) {
|
||||
const newQueue = {
|
||||
entry: change,
|
||||
missingChildren: [] as string[],
|
||||
timeout: 0,
|
||||
};
|
||||
Logger(`Processing scheduled: ${change.path}`, LOG_LEVEL_INFO);
|
||||
this.queuedFiles.push(newQueue);
|
||||
this.saveQueuedFiles();
|
||||
continue;
|
||||
} else {
|
||||
await this.parseIncomingDoc(change);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (change.type == "versioninfo") {
|
||||
if (change.version > VER) {
|
||||
this.replicator.closeReplication();
|
||||
Logger(`Remote database updated to incompatible version. update your self-hosted-livesync plugin.`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Remote database updated to incompatible version. update your self-hosted-livesync plugin.`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1566,19 +1600,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
const newMsg = typeof message == "string" ? message : this.lastMessage;
|
||||
const newLog = typeof log == "string" ? log : this.lastLog;
|
||||
if (`${this.lastMessage}-${this.lastLog}` != `${newMsg}-${newLog}`) {
|
||||
scheduleTask("update-display", 50, () => {
|
||||
this.statusBar.setText(newMsg.split("\n")[0]);
|
||||
// scheduleTask("update-display", 50, () => {
|
||||
this.statusBar.setText(newMsg.split("\n")[0]);
|
||||
|
||||
if (this.settings.showStatusOnEditor) {
|
||||
const root = activeDocument.documentElement;
|
||||
const q = root.querySelectorAll(`.CodeMirror-wrap,.cm-s-obsidian>.cm-editor,.canvas-wrapper`);
|
||||
q.forEach(e => e.setAttr("data-log", '' + (newMsg + "\n" + newLog) + ''))
|
||||
} else {
|
||||
const root = activeDocument.documentElement;
|
||||
const q = root.querySelectorAll(`.CodeMirror-wrap,.cm-s-obsidian>.cm-editor,.canvas-wrapper`);
|
||||
q.forEach(e => e.setAttr("data-log", ''))
|
||||
}
|
||||
}, true);
|
||||
if (this.settings.showStatusOnEditor) {
|
||||
const root = activeDocument.documentElement;
|
||||
const q = root.querySelectorAll(`.CodeMirror-wrap,.cm-s-obsidian>.cm-editor,.canvas-wrapper`);
|
||||
q.forEach(e => e.setAttr("data-log", '' + (newMsg + "\n" + newLog) + ''))
|
||||
} else {
|
||||
const root = activeDocument.documentElement;
|
||||
const q = root.querySelectorAll(`.CodeMirror-wrap,.cm-s-obsidian>.cm-editor,.canvas-wrapper`);
|
||||
q.forEach(e => e.setAttr("data-log", ''))
|
||||
}
|
||||
// }, true);
|
||||
scheduleTask("log-hide", 3000, () => this.setStatusBarText(null, ""));
|
||||
this.lastMessage = newMsg;
|
||||
this.lastLog = newLog;
|
||||
@@ -1588,8 +1622,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
|
||||
async replicate(showMessage?: boolean) {
|
||||
if (!this.isReady) return;
|
||||
if (isLockAcquired("cleanup")) {
|
||||
Logger("Database cleaning up is in process. replication has been cancelled", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
if (this.settings.versionUpFlash != "") {
|
||||
Logger("Open settings and check message, please.", LOG_LEVEL.NOTICE);
|
||||
Logger("Open settings and check message, please. replication has been cancelled.", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
await this.applyBatchChange();
|
||||
@@ -1598,23 +1636,44 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
const ret = await this.replicator.openReplication(this.settings, false, showMessage);
|
||||
if (!ret) {
|
||||
if (this.replicator.remoteLockedAndDeviceNotAccepted) {
|
||||
if (this.replicator.remoteCleaned) {
|
||||
const message = `
|
||||
The remote database has been cleaned up.
|
||||
To synchronize, this device must also be cleaned up or fetch everything again once.
|
||||
Fetching may takes some time. Cleaning up is not stable yet but fast.
|
||||
`
|
||||
const CHOICE_CLEANUP = "Clean up";
|
||||
const CHOICE_FETCH = "Fetch again";
|
||||
const CHOICE_DISMISS = "Dismiss";
|
||||
const ret = await confirmWithMessage(this, "Locked", message, [CHOICE_CLEANUP, CHOICE_FETCH, CHOICE_DISMISS], CHOICE_DISMISS, 10);
|
||||
if (ret == CHOICE_CLEANUP) {
|
||||
await localDatabaseCleanUp(this, true, false);
|
||||
await balanceChunks(this, false);
|
||||
}
|
||||
if (ret == CHOICE_FETCH) {
|
||||
await performRebuildDB(this, "localOnly");
|
||||
}
|
||||
if (this.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
|
||||
Logger(`The remote database has been cleaned.`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
|
||||
await runWithLock("cleanup", true, async () => {
|
||||
const count = await purgeUnreferencedChunks(this.localDatabase.localDatabase, true);
|
||||
const message = `The remote database has been cleaned up.
|
||||
To synchronize, this device must be also cleaned up. ${count} chunk(s) will be erased from this device.
|
||||
However, If there are many chunks to be deleted, maybe fetching again is faster.
|
||||
We will lose the history of this device if we fetch the remote database again.
|
||||
Even if you choose to clean up, you will see this option again if you exit Obsidian and then synchronise again.`
|
||||
const CHOICE_FETCH = "Fetch again";
|
||||
const CHOICE_CLEAN = "Cleanup";
|
||||
const CHOICE_DISMISS = "Dismiss";
|
||||
const ret = await confirmWithMessage(this, "Cleaned", message, [CHOICE_FETCH, CHOICE_CLEAN, CHOICE_DISMISS], CHOICE_DISMISS, 30);
|
||||
if (ret == CHOICE_FETCH) {
|
||||
await performRebuildDB(this, "localOnly");
|
||||
}
|
||||
if (ret == CHOICE_CLEAN) {
|
||||
const remoteDB = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.getIsMobile(), true);
|
||||
if (typeof remoteDB == "string") {
|
||||
Logger(remoteDB, LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
}
|
||||
|
||||
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
|
||||
this.localDatabase.hashCaches.clear();
|
||||
// Perform the synchronisation once.
|
||||
if (await this.replicator.openReplication(this.settings, false, showMessage, true)) {
|
||||
await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db);
|
||||
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
|
||||
this.localDatabase.hashCaches.clear();
|
||||
await this.getReplicator().markRemoteResolved(this.settings);
|
||||
Logger("The local database has been cleaned up.", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO)
|
||||
} else {
|
||||
Logger("Replication has been cancelled. Please try it again.", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO)
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const message = `
|
||||
The remote database has been rebuilt.
|
||||
@@ -1678,28 +1737,36 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
// synchronize all files between database and storage.
|
||||
let initialScan = false;
|
||||
if (showingNotice) {
|
||||
Logger("Initializing", LOG_LEVEL.NOTICE, "syncAll");
|
||||
Logger("Initializing", LOG_LEVEL_NOTICE, "syncAll");
|
||||
}
|
||||
|
||||
Logger("Initialize and checking database files");
|
||||
Logger("Checking deleted files");
|
||||
await this.collectDeletedFiles();
|
||||
|
||||
Logger("Collecting local files on the storage", LOG_LEVEL.VERBOSE);
|
||||
const filesStorage = this.app.vault.getFiles().filter(e => this.isTargetFile(e));
|
||||
Logger("Collecting local files on the storage", LOG_LEVEL_VERBOSE);
|
||||
const filesStorageSrc = this.app.vault.getFiles();
|
||||
|
||||
const filesStorage = [] as typeof filesStorageSrc;
|
||||
for (const f of filesStorageSrc) {
|
||||
if (await this.isTargetFile(f.path)) {
|
||||
filesStorage.push(f);
|
||||
}
|
||||
}
|
||||
|
||||
const filesStorageName = filesStorage.map((e) => e.path);
|
||||
Logger("Collecting local files on the DB", LOG_LEVEL.VERBOSE);
|
||||
Logger("Collecting local files on the DB", LOG_LEVEL_VERBOSE);
|
||||
const filesDatabase = [] as FilePathWithPrefix[]
|
||||
let count = 0;
|
||||
for await (const doc of this.localDatabase.findAllNormalDocs()) {
|
||||
count++;
|
||||
if (count % 25 == 0) Logger(`Collecting local files on the DB: ${count}`, showingNotice ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO, "syncAll");
|
||||
if (count % 25 == 0) Logger(`Collecting local files on the DB: ${count}`, showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, "syncAll");
|
||||
const path = getPath(doc);
|
||||
if (isValidPath(path) && this.isTargetFile(path)) {
|
||||
if (isValidPath(path) && await this.isTargetFile(path)) {
|
||||
filesDatabase.push(path);
|
||||
}
|
||||
}
|
||||
Logger("Opening the key-value database", LOG_LEVEL.VERBOSE);
|
||||
Logger("Opening the key-value database", LOG_LEVEL_VERBOSE);
|
||||
const isInitialized = await (this.kvDB.get<boolean>("initialized")) || false;
|
||||
// Make chunk bigger if it is the initial scan. There must be non-active docs.
|
||||
if (filesDatabase.length == 0 && !isInitialized) {
|
||||
@@ -1723,8 +1790,8 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
await callback(e);
|
||||
return true;
|
||||
} catch (ex) {
|
||||
Logger(`Error while ${procedureName}`, LOG_LEVEL.NOTICE);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Error while ${procedureName}`, LOG_LEVEL_NOTICE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1753,7 +1820,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
await this.pullFile(e, filesStorage, false, null, false);
|
||||
Logger(`Check or pull from db:${e} OK`);
|
||||
} else if (w) {
|
||||
Logger(`Deletion history skipped: ${e}`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Deletion history skipped: ${e}`, LOG_LEVEL_VERBOSE);
|
||||
} else {
|
||||
Logger(`entry not found: ${e}`);
|
||||
}
|
||||
@@ -1784,7 +1851,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
await (this.kvDB.set("initialized", true))
|
||||
}
|
||||
if (showingNotice) {
|
||||
Logger("Initialize done!", LOG_LEVEL.NOTICE, "syncAll");
|
||||
Logger("Initialize done!", LOG_LEVEL_NOTICE, "syncAll");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1935,7 +2002,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
// except insertion, the line should not be different.
|
||||
if (rightItem[1] != leftItem[1]) {
|
||||
//TODO: SHOULD BE PANIC.
|
||||
Logger(`MERGING PANIC:${leftItem[0]},${leftItem[1]} == ${rightItem[0]},${rightItem[1]}`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`MERGING PANIC:${leftItem[0]},${leftItem[1]} == ${rightItem[0]},${rightItem[1]}`, LOG_LEVEL_VERBOSE);
|
||||
autoMerge = false;
|
||||
break LOOP_MERGE;
|
||||
}
|
||||
@@ -1959,12 +2026,12 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
break LOOP_MERGE;
|
||||
}
|
||||
}
|
||||
Logger(`Weird condition:${leftItem[0]},${leftItem[1]} == ${rightItem[0]},${rightItem[1]}`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Weird condition:${leftItem[0]},${leftItem[1]} == ${rightItem[0]},${rightItem[1]}`, LOG_LEVEL_VERBOSE);
|
||||
// here is the exception
|
||||
break LOOP_MERGE;
|
||||
} while (leftIdx < diffLeft.length || rightIdx < diffRight.length);
|
||||
if (autoMerge) {
|
||||
Logger(`Sensibly merge available`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Sensibly merge available`, LOG_LEVEL_VERBOSE);
|
||||
return merged;
|
||||
} else {
|
||||
return false;
|
||||
@@ -2016,7 +2083,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
return JSON.stringify(newObj.data);
|
||||
} catch (ex) {
|
||||
Logger("Could not merge object");
|
||||
Logger(ex, LOG_LEVEL.VERBOSE)
|
||||
Logger(ex, LOG_LEVEL_VERBOSE)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2046,18 +2113,18 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
if (result) {
|
||||
p = result.filter(e => e[0] != DIFF_DELETE).map((e) => e[1]).join("");
|
||||
// can be merged.
|
||||
Logger(`Sensible merge:${path}`, LOG_LEVEL.INFO);
|
||||
Logger(`Sensible merge:${path}`, LOG_LEVEL_INFO);
|
||||
} else {
|
||||
Logger(`Sensible merge is not applicable.`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Sensible merge is not applicable.`, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
} else if (isObjectMargeApplicable(path)) {
|
||||
// can be merged.
|
||||
const result = await this.mergeObject(path, commonBase, test._rev, conflictedRev);
|
||||
if (result) {
|
||||
Logger(`Object merge:${path}`, LOG_LEVEL.INFO);
|
||||
Logger(`Object merge:${path}`, LOG_LEVEL_INFO);
|
||||
p = result;
|
||||
} else {
|
||||
Logger(`Object merge is not applicable.`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Object merge is not applicable.`, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2074,7 +2141,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
await this.updateIntoDB(newFile);
|
||||
}
|
||||
await this.pullFile(path);
|
||||
Logger(`Automatically merged (sensible) :${path}`, LOG_LEVEL.INFO);
|
||||
Logger(`Automatically merged (sensible) :${path}`, LOG_LEVEL_INFO);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2084,14 +2151,14 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
const rightLeaf = await this.getConflictedDoc(path, conflicts[0]);
|
||||
if (leftLeaf == false) {
|
||||
// what's going on..
|
||||
Logger(`could not get current revisions:${path}`, LOG_LEVEL.NOTICE);
|
||||
Logger(`could not get current revisions:${path}`, LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
}
|
||||
if (rightLeaf == false) {
|
||||
// Conflicted item could not load, delete this.
|
||||
await this.localDatabase.deleteDBEntry(path, { rev: conflicts[0] });
|
||||
await this.pullFile(path, null, true);
|
||||
Logger(`could not get old revisions, automatically used newer one:${path}`, LOG_LEVEL.NOTICE);
|
||||
Logger(`could not get old revisions, automatically used newer one:${path}`, LOG_LEVEL_NOTICE);
|
||||
return true;
|
||||
}
|
||||
// first, check for same contents and deletion status.
|
||||
@@ -2114,7 +2181,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
}
|
||||
await this.localDatabase.deleteDBEntry(path, { rev: loser.rev });
|
||||
await this.pullFile(path, null, true);
|
||||
Logger(`Automatically merged (newerFileResolve) :${path}`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Automatically merged (newerFileResolve) :${path}`, LOG_LEVEL_NOTICE);
|
||||
return true;
|
||||
}
|
||||
// make diff.
|
||||
@@ -2132,15 +2199,15 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
showMergeDialog(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise<boolean> {
|
||||
return runWithLock("resolve-conflict:" + filename, false, () =>
|
||||
new Promise((res, rej) => {
|
||||
Logger("open conflict dialog", LOG_LEVEL.VERBOSE);
|
||||
Logger("open conflict dialog", LOG_LEVEL_VERBOSE);
|
||||
new ConflictResolveModal(this.app, filename, conflictCheckResult, async (selected) => {
|
||||
const testDoc = await this.localDatabase.getDBEntry(filename, { conflicts: true }, false, false, true);
|
||||
if (testDoc === false) {
|
||||
Logger("Missing file..", LOG_LEVEL.VERBOSE);
|
||||
Logger("Missing file..", LOG_LEVEL_VERBOSE);
|
||||
return res(true);
|
||||
}
|
||||
if (!testDoc._conflicts) {
|
||||
Logger("Nothing have to do with this conflict", LOG_LEVEL.VERBOSE);
|
||||
Logger("Nothing have to do with this conflict", LOG_LEVEL_VERBOSE);
|
||||
return res(true);
|
||||
}
|
||||
const toDelete = selected;
|
||||
@@ -2166,7 +2233,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
setTimeout(() => {
|
||||
//resolved, check again.
|
||||
this.showIfConflicted(filename);
|
||||
}, 500);
|
||||
}, 50);
|
||||
} else if (toDelete == null) {
|
||||
Logger("Leave it still conflicted");
|
||||
} else {
|
||||
@@ -2179,7 +2246,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
setTimeout(() => {
|
||||
//resolved, check again.
|
||||
this.showIfConflicted(filename);
|
||||
}, 500);
|
||||
}, 50);
|
||||
}
|
||||
|
||||
return res(true);
|
||||
@@ -2223,7 +2290,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
Logger("conflict:Automatically merged, but we have to check it again");
|
||||
setTimeout(() => {
|
||||
this.showIfConflicted(filename);
|
||||
}, 500);
|
||||
}, 50);
|
||||
return;
|
||||
}
|
||||
//there conflicts, and have to resolve ;
|
||||
@@ -2233,7 +2300,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
|
||||
async pullFile(filename: FilePathWithPrefix, fileList?: TFile[], force?: boolean, rev?: string, waitForReady = true) {
|
||||
const targetFile = getAbstractFileByPath(stripAllPrefixes(filename));
|
||||
if (!this.isTargetFile(filename)) return;
|
||||
if (!await this.isTargetFile(filename)) return;
|
||||
if (targetFile == null) {
|
||||
//have to create;
|
||||
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
||||
@@ -2276,7 +2343,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
const dK = `${file.path}-diff`;
|
||||
const isLastDiff = dK in caches ? caches[dK] : { storageMtime: 0, docMtime: 0 };
|
||||
if (isLastDiff.docMtime == docMtime && isLastDiff.storageMtime == storageMtime) {
|
||||
// Logger("STORAGE .. DB :" + file.path, LOG_LEVEL.VERBOSE);
|
||||
// Logger("STORAGE .. DB :" + file.path, LOG_LEVEL_VERBOSE);
|
||||
caches[dK] = { storageMtime, docMtime };
|
||||
return caches;
|
||||
}
|
||||
@@ -2300,14 +2367,14 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
caches[dK] = { storageMtime, docMtime };
|
||||
return caches;
|
||||
}
|
||||
Logger("STORAGE == DB :" + file.path + "", LOG_LEVEL.VERBOSE);
|
||||
Logger("STORAGE == DB :" + file.path + "", LOG_LEVEL_VERBOSE);
|
||||
caches[dK] = { storageMtime, docMtime };
|
||||
return caches;
|
||||
|
||||
}
|
||||
|
||||
async updateIntoDB(file: TFile, initialScan?: boolean, cache?: CacheData, force?: boolean) {
|
||||
if (!this.isTargetFile(file)) return true;
|
||||
if (!await this.isTargetFile(file)) return true;
|
||||
if (shouldBeIgnored(file.path)) {
|
||||
return true;
|
||||
}
|
||||
@@ -2315,14 +2382,14 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
let datatype: "plain" | "newnote" = "newnote";
|
||||
if (!cache) {
|
||||
if (!isPlainText(file.name)) {
|
||||
Logger(`Reading : ${file.path}`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Reading : ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||
const contentBin = await this.app.vault.readBinary(file);
|
||||
Logger(`Processing: ${file.path}`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Processing: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||
try {
|
||||
content = await arrayBufferToBase64(contentBin);
|
||||
} catch (ex) {
|
||||
Logger(`The file ${file.path} could not be encoded`);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
datatype = "newnote";
|
||||
@@ -2332,12 +2399,12 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
}
|
||||
} else {
|
||||
if (cache instanceof ArrayBuffer) {
|
||||
Logger(`Processing: ${file.path}`, LOG_LEVEL.VERBOSE);
|
||||
Logger(`Processing: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||
try {
|
||||
content = await arrayBufferToBase64(cache);
|
||||
} catch (ex) {
|
||||
Logger(`The file ${file.path} could not be encoded`);
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
datatype = "newnote"
|
||||
@@ -2372,15 +2439,15 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
const newData = { data: d.data, deleted: d._deleted || d.deleted };
|
||||
if (oldData.deleted != newData.deleted) return false;
|
||||
if (!isDocContentSame(old.data, newData.data)) return false;
|
||||
Logger(msg + "Skipped (not changed) " + fullPath + ((d._deleted || d.deleted) ? " (deleted)" : ""), LOG_LEVEL.VERBOSE);
|
||||
Logger(msg + "Skipped (not changed) " + fullPath + ((d._deleted || d.deleted) ? " (deleted)" : ""), LOG_LEVEL_VERBOSE);
|
||||
return true;
|
||||
// d._rev = old._rev;
|
||||
}
|
||||
} catch (ex) {
|
||||
if (force) {
|
||||
Logger(msg + "Error, Could not check the diff for the old one." + (force ? "force writing." : "") + fullPath + ((d._deleted || d.deleted) ? " (deleted)" : ""), LOG_LEVEL.VERBOSE);
|
||||
Logger(msg + "Error, Could not check the diff for the old one." + (force ? "force writing." : "") + fullPath + ((d._deleted || d.deleted) ? " (deleted)" : ""), LOG_LEVEL_VERBOSE);
|
||||
} else {
|
||||
Logger(msg + "Error, Could not check the diff for the old one." + fullPath + ((d._deleted || d.deleted) ? " (deleted)" : ""), LOG_LEVEL.VERBOSE);
|
||||
Logger(msg + "Error, Could not check the diff for the old one." + fullPath + ((d._deleted || d.deleted) ? " (deleted)" : ""), LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
return !force;
|
||||
}
|
||||
@@ -2398,7 +2465,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
}
|
||||
|
||||
async deleteFromDB(file: TFile) {
|
||||
if (!this.isTargetFile(file)) return;
|
||||
if (!await this.isTargetFile(file)) return;
|
||||
const fullPath = getPathFromTFile(file);
|
||||
Logger(`deleteDB By path:${fullPath}`);
|
||||
await this.deleteFromDBbyPath(fullPath);
|
||||
@@ -2481,15 +2548,78 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
isTargetFile(file: string | TAbstractFile) {
|
||||
if (file instanceof TFile) {
|
||||
return this.localDatabase.isTargetFile(file.path);
|
||||
} else if (typeof file == "string") {
|
||||
return this.localDatabase.isTargetFile(file);
|
||||
ignoreFileCache = new LRUCache<string, string[] | false>(300, 250000, true);
|
||||
ignoreFiles = [] as string[]
|
||||
async readIgnoreFile(path: string) {
|
||||
try {
|
||||
const file = await this.app.vault.adapter.read(path);
|
||||
const gitignore = file.split(/\r?\n/g);
|
||||
this.ignoreFileCache.set(path, gitignore);
|
||||
return gitignore;
|
||||
} catch (ex) {
|
||||
this.ignoreFileCache.set(path, false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async getIgnoreFile(path: string) {
|
||||
if (this.ignoreFileCache.has(path)) {
|
||||
return this.ignoreFileCache.get(path);
|
||||
} else {
|
||||
return await this.readIgnoreFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
async isIgnoredByIgnoreFiles(file: string | TAbstractFile) {
|
||||
if (!this.settings.useIgnoreFiles) {
|
||||
return true;
|
||||
}
|
||||
const filepath = file instanceof TFile ? file.path : file as string;
|
||||
if (this.ignoreFileCache.has(filepath)) {
|
||||
// Renew
|
||||
await this.readIgnoreFile(filepath);
|
||||
}
|
||||
if (!await isAcceptedAll(stripAllPrefixes(filepath as FilePathWithPrefix), this.ignoreFiles, (filename) => this.getIgnoreFile(filename))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async isTargetFile(file: string | TAbstractFile) {
|
||||
const filepath = file instanceof TFile ? file.path : file as string;
|
||||
if (this.settings.useIgnoreFiles && !await this.isIgnoredByIgnoreFiles(file)) {
|
||||
return false;
|
||||
}
|
||||
return this.localDatabase.isTargetFile(filepath);
|
||||
}
|
||||
async dryRunGC() {
|
||||
await runWithLock("cleanup", true, async () => {
|
||||
const remoteDBConn = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.isMobile)
|
||||
if (typeof (remoteDBConn) == "string") {
|
||||
Logger(remoteDBConn);
|
||||
return;
|
||||
}
|
||||
await purgeUnreferencedChunks(remoteDBConn.db, true, this.settings, false);
|
||||
await purgeUnreferencedChunks(this.localDatabase.localDatabase, true);
|
||||
this.localDatabase.hashCaches.clear();
|
||||
});
|
||||
}
|
||||
|
||||
async dbGC() {
|
||||
// Lock the remote completely once.
|
||||
await runWithLock("cleanup", true, async () => {
|
||||
this.getReplicator().markRemoteLocked(this.settings, true, true);
|
||||
const remoteDBConn = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.isMobile)
|
||||
if (typeof (remoteDBConn) == "string") {
|
||||
Logger(remoteDBConn);
|
||||
return;
|
||||
}
|
||||
await purgeUnreferencedChunks(remoteDBConn.db, false, this.settings, true);
|
||||
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
|
||||
this.localDatabase.hashCaches.clear();
|
||||
await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDBConn.db);
|
||||
this.localDatabase.refreshSettings();
|
||||
Logger("The remote database has been cleaned up! Other devices will be cleaned up on the next synchronisation.")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PluginManifest, TFile } from "./deps";
|
||||
import { DatabaseEntry, EntryBody, FilePath } from "./lib/src/types";
|
||||
import { type PluginManifest, TFile } from "./deps";
|
||||
import { type DatabaseEntry, type EntryBody, type FilePath } from "./lib/src/types";
|
||||
|
||||
export interface PluginDataEntry extends DatabaseEntry {
|
||||
deviceVaultName: string;
|
||||
|
||||
268
src/utils.ts
268
src/utils.ts
@@ -1,12 +1,11 @@
|
||||
import { type DataWriteOptions, normalizePath, TFile, Platform, TAbstractFile, App, Plugin_2, type RequestUrlParam, requestUrl } from "./deps";
|
||||
import { type DataWriteOptions, normalizePath, TFile, Platform, TAbstractFile, App, Plugin, type RequestUrlParam, requestUrl } from "./deps";
|
||||
import { path2id_base, id2path_base, isValidFilenameInLinux, isValidFilenameInDarwin, isValidFilenameInWidows, isValidFilenameInAndroid, stripAllPrefixes } from "./lib/src/path";
|
||||
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { type AnyEntry, type DocumentID, type EntryDoc, type EntryHasPath, type FilePath, type FilePathWithPrefix, LOG_LEVEL, type NewEntry } from "./lib/src/types";
|
||||
import { LOG_LEVEL_VERBOSE, type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix } from "./lib/src/types";
|
||||
import { CHeader, ICHeader, ICHeaderLength, PSCHeader } from "./types";
|
||||
import { InputStringDialog, PopoverSelectString } from "./dialogs";
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
import { runWithLock } from "./lib/src/lock";
|
||||
|
||||
// For backward compatibility, using the path for determining id.
|
||||
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
|
||||
@@ -328,7 +327,7 @@ export function isValidPath(filename: string) {
|
||||
if (Platform.isAndroidApp) return isValidFilenameInAndroid(filename);
|
||||
if (Platform.isIosApp) return isValidFilenameInDarwin(filename);
|
||||
//Fallback
|
||||
Logger("Could not determine platform for checking filename", LOG_LEVEL.VERBOSE);
|
||||
Logger("Could not determine platform for checking filename", LOG_LEVEL_VERBOSE);
|
||||
return isValidFilenameInWidows(filename);
|
||||
}
|
||||
|
||||
@@ -416,8 +415,8 @@ export const askString = (app: App, title: string, key: string, placeholder: str
|
||||
export class PeriodicProcessor {
|
||||
_process: () => Promise<any>;
|
||||
_timer?: number;
|
||||
_plugin: Plugin_2;
|
||||
constructor(plugin: Plugin_2, process: () => Promise<any>) {
|
||||
_plugin: Plugin;
|
||||
constructor(plugin: Plugin, process: () => Promise<any>) {
|
||||
this._plugin = plugin;
|
||||
this._process = process;
|
||||
}
|
||||
@@ -431,20 +430,15 @@ export class PeriodicProcessor {
|
||||
enable(interval: number) {
|
||||
this.disable();
|
||||
if (interval == 0) return;
|
||||
this._timer = window.setInterval(() => this._process().then(() => { }), interval);
|
||||
this._timer = window.setInterval(() => this.process().then(() => { }), interval);
|
||||
this._plugin.registerInterval(this._timer);
|
||||
}
|
||||
disable() {
|
||||
if (this._timer) clearInterval(this._timer);
|
||||
if (this._timer !== undefined) window.clearInterval(this._timer);
|
||||
this._timer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function sizeToHumanReadable(size: number | undefined) {
|
||||
if (!size) return "-";
|
||||
const i = Math.floor(Math.log(size) / Math.log(1024));
|
||||
return Number.parseInt((size / Math.pow(1024, i)).toFixed(2)) + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
|
||||
}
|
||||
|
||||
export const _requestToCouchDBFetch = async (baseUri: string, username: string, password: string, path?: string, body?: string | any, method?: string) => {
|
||||
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${username}:${password}`));
|
||||
const encoded = window.btoa(utf8str);
|
||||
@@ -492,249 +486,3 @@ export async function performRebuildDB(plugin: ObsidianLiveSyncPlugin, method: "
|
||||
await plugin.addOnSetup.rebuildEverything();
|
||||
}
|
||||
}
|
||||
|
||||
export const gatherChunkUsage = async (db: PouchDB.Database<EntryDoc>) => {
|
||||
const used = new Map();
|
||||
const unreferenced = new Map();
|
||||
const removed = new Map();
|
||||
const missing = new Map();
|
||||
const xx = await db.allDocs({ startkey: "h:", endkey: `h:\u{10ffff}` });
|
||||
for (const xxd of xx.rows) {
|
||||
const chunk = xxd.id
|
||||
unreferenced.set(chunk, xxd.value.rev);
|
||||
}
|
||||
|
||||
const x = await db.find({ limit: 999999999, selector: { children: { $exists: true, $type: "array" } }, fields: ["_id", "path", "mtime", "children"] });
|
||||
for (const temp of x.docs) {
|
||||
for (const chunk of (temp as NewEntry).children) {
|
||||
used.set(chunk, (used.has(chunk) ? used.get(chunk) : 0) + 1);
|
||||
if (unreferenced.has(chunk)) {
|
||||
removed.set(chunk, unreferenced.get(chunk));
|
||||
unreferenced.delete(chunk);
|
||||
} else {
|
||||
if (!removed.has(chunk)) {
|
||||
if (!missing.has(temp._id)) {
|
||||
missing.set(temp._id, []);
|
||||
}
|
||||
missing.get(temp._id).push(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { used, unreferenced, missing };
|
||||
}
|
||||
|
||||
export const localDatabaseCleanUp = async (plugin: ObsidianLiveSyncPlugin, force: boolean, dryRun: boolean) => {
|
||||
|
||||
await runWithLock("clean-up:local", true, async () => {
|
||||
const db = plugin.localDatabase.localDatabase;
|
||||
if ((db as any)?.adapter != "indexeddb") {
|
||||
if (force && !dryRun) {
|
||||
Logger("Fetch from the remote database", LOG_LEVEL.NOTICE, "clean-up-db");
|
||||
await performRebuildDB(plugin, "localOnly");
|
||||
return;
|
||||
} else {
|
||||
Logger("This feature requires disabling `Use an old adapter for compatibility`.", LOG_LEVEL.NOTICE, "clean-up-db");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Logger(`The remote database has been locked for garbage collection`, LOG_LEVEL.NOTICE, "clean-up-db");
|
||||
Logger(`Gathering chunk usage information`, LOG_LEVEL.NOTICE, "clean-up-db");
|
||||
|
||||
const { unreferenced, missing } = await gatherChunkUsage(db);
|
||||
if (missing.size != 0) {
|
||||
Logger(`Some chunks are not found! We have to rescue`, LOG_LEVEL.NOTICE);
|
||||
Logger(missing, LOG_LEVEL.VERBOSE);
|
||||
} else {
|
||||
Logger(`All chunks are OK`, LOG_LEVEL.NOTICE);
|
||||
}
|
||||
const payload = {} as Record<string, string[]>;
|
||||
for (const [id, rev] of unreferenced) {
|
||||
payload[id] = [rev];
|
||||
}
|
||||
const removeItems = Object.keys(payload).length;
|
||||
if (removeItems == 0) {
|
||||
Logger(`No unreferenced chunks found (Local)`, LOG_LEVEL.NOTICE);
|
||||
await plugin.markRemoteResolved();
|
||||
}
|
||||
if (dryRun) {
|
||||
Logger(`There are ${removeItems} unreferenced chunks (Local)`, LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
}
|
||||
Logger(`Deleting unreferenced chunks: ${removeItems}`, LOG_LEVEL.NOTICE, "clean-up-db");
|
||||
for (const [id, rev] of unreferenced) {
|
||||
//@ts-ignore
|
||||
const ret = await db.purge(id, rev);
|
||||
Logger(ret, LOG_LEVEL.VERBOSE);
|
||||
}
|
||||
plugin.localDatabase.refreshSettings();
|
||||
Logger(`Compacting local database...`, LOG_LEVEL.NOTICE, "clean-up-db");
|
||||
await db.compact();
|
||||
await plugin.markRemoteResolved();
|
||||
Logger("Done!", LOG_LEVEL.NOTICE, "clean-up-db");
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const balanceChunks = async (plugin: ObsidianLiveSyncPlugin, dryRun: boolean) => {
|
||||
|
||||
await runWithLock("clean-up:balance", true, async () => {
|
||||
const localDB = plugin.localDatabase.localDatabase;
|
||||
Logger(`Gathering chunk usage information`, LOG_LEVEL.NOTICE, "clean-up-db");
|
||||
|
||||
const ret = await plugin.replicator.connectRemoteCouchDBWithSetting(plugin.settings, plugin.isMobile);
|
||||
if (typeof ret === "string") {
|
||||
Logger(`Connect error: ${ret}`, LOG_LEVEL.NOTICE, "clean-up-db");
|
||||
return;
|
||||
}
|
||||
const localChunks = new Map<string, string>();
|
||||
const xx = await localDB.allDocs({ startkey: "h:", endkey: `h:\u{10ffff}` });
|
||||
for (const xxd of xx.rows) {
|
||||
const chunk = xxd.id
|
||||
localChunks.set(chunk, xxd.value.rev);
|
||||
}
|
||||
// const info = ret.info;
|
||||
const remoteDB = ret.db;
|
||||
const remoteChunks = new Map<string, string>();
|
||||
const xxr = await remoteDB.allDocs({ startkey: "h:", endkey: `h:\u{10ffff}` });
|
||||
for (const xxd of xxr.rows) {
|
||||
const chunk = xxd.id
|
||||
remoteChunks.set(chunk, xxd.value.rev);
|
||||
}
|
||||
const localToRemote = new Map<string, string>([...localChunks]);
|
||||
const remoteToLocal = new Map<string, string>([...remoteChunks]);
|
||||
for (const id of new Set([...localChunks.keys(), ...remoteChunks.keys()])) {
|
||||
if (remoteChunks.has(id)) {
|
||||
localToRemote.delete(id);
|
||||
}
|
||||
if (localChunks.has(id)) {
|
||||
remoteToLocal.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
function arrayToChunkedArray<T>(src: T[], size = 25) {
|
||||
const ret = [] as T[][];
|
||||
let i = 0;
|
||||
while (i < src.length) {
|
||||
ret.push(src.slice(i, i += size));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (localToRemote.size == 0) {
|
||||
Logger(`No chunks need to be sent`, LOG_LEVEL.NOTICE);
|
||||
} else {
|
||||
Logger(`${localToRemote.size} chunks need to be sent`, LOG_LEVEL.NOTICE);
|
||||
if (!dryRun) {
|
||||
const w = arrayToChunkedArray([...localToRemote]);
|
||||
for (const chunk of w) {
|
||||
for (const [id,] of chunk) {
|
||||
const queryRet = await localDB.allDocs({ keys: [id], include_docs: true });
|
||||
const docs = queryRet.rows.filter(e => !("error" in e)).map(x => x.doc);
|
||||
|
||||
const ret = await remoteDB.bulkDocs(docs, { new_edits: false });
|
||||
Logger(ret, LOG_LEVEL.VERBOSE);
|
||||
}
|
||||
}
|
||||
Logger(`Done! ${remoteToLocal.size} chunks have been sent`, LOG_LEVEL.NOTICE);
|
||||
}
|
||||
}
|
||||
if (remoteToLocal.size == 0) {
|
||||
Logger(`No chunks need to be retrieved`, LOG_LEVEL.NOTICE);
|
||||
} else {
|
||||
Logger(`${remoteToLocal.size} chunks need to be retrieved`, LOG_LEVEL.NOTICE);
|
||||
if (!dryRun) {
|
||||
const w = arrayToChunkedArray([...remoteToLocal]);
|
||||
for (const chunk of w) {
|
||||
for (const [id,] of chunk) {
|
||||
const queryRet = await remoteDB.allDocs({ keys: [id], include_docs: true });
|
||||
const docs = queryRet.rows.filter(e => !("error" in e)).map(x => x.doc);
|
||||
|
||||
const ret = await localDB.bulkDocs(docs, { new_edits: false });
|
||||
Logger(ret, LOG_LEVEL.VERBOSE);
|
||||
}
|
||||
}
|
||||
Logger(`Done! ${remoteToLocal.size} chunks have been retrieved`, LOG_LEVEL.NOTICE);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const remoteDatabaseCleanup = async (plugin: ObsidianLiveSyncPlugin, dryRun: boolean) => {
|
||||
const getSize = function (info: PouchDB.Core.DatabaseInfo, key: "active" | "external" | "file") {
|
||||
return Number.parseInt((info as any)?.sizes?.[key] ?? 0);
|
||||
}
|
||||
await runWithLock("clean-up:remote", true, async () => {
|
||||
const CHUNK_SIZE = 100;
|
||||
function makeChunkedArrayFromArray<T>(items: T[]): T[][] {
|
||||
const chunked = [];
|
||||
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
|
||||
chunked.push(items.slice(i, i + CHUNK_SIZE));
|
||||
}
|
||||
return chunked;
|
||||
}
|
||||
try {
|
||||
const ret = await plugin.replicator.connectRemoteCouchDBWithSetting(plugin.settings, plugin.isMobile);
|
||||
if (typeof ret === "string") {
|
||||
Logger(`Connect error: ${ret}`, LOG_LEVEL.NOTICE, "clean-up-db");
|
||||
return;
|
||||
}
|
||||
const info = ret.info;
|
||||
Logger(JSON.stringify(info), LOG_LEVEL.VERBOSE, "clean-up-db");
|
||||
Logger(`Database active-size: ${sizeToHumanReadable(getSize(info, "active"))}, external-size:${sizeToHumanReadable(getSize(info, "external"))}, file-size: ${sizeToHumanReadable(getSize(info, "file"))}`, LOG_LEVEL.NOTICE);
|
||||
|
||||
if (!dryRun) {
|
||||
Logger(`The remote database has been locked for garbage collection`, LOG_LEVEL.NOTICE, "clean-up-db");
|
||||
await plugin.markRemoteLocked(true);
|
||||
}
|
||||
Logger(`Gathering chunk usage information`, LOG_LEVEL.NOTICE, "clean-up-db");
|
||||
const db = ret.db;
|
||||
|
||||
const { unreferenced, missing } = await gatherChunkUsage(db);
|
||||
if (missing.size != 0) {
|
||||
Logger(`Some chunks are not found! We have to rescue`, LOG_LEVEL.NOTICE);
|
||||
Logger(missing, LOG_LEVEL.VERBOSE);
|
||||
} else {
|
||||
Logger(`All chunks are OK`, LOG_LEVEL.NOTICE);
|
||||
}
|
||||
const payload = {} as Record<string, string[]>;
|
||||
for (const [id, rev] of unreferenced) {
|
||||
payload[id] = [rev];
|
||||
}
|
||||
const removeItems = Object.keys(payload).length;
|
||||
if (removeItems == 0) {
|
||||
Logger(`No unreferenced chunk found (Remote)`, LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
}
|
||||
if (dryRun) {
|
||||
Logger(`There are ${removeItems} unreferenced chunks (Remote)`, LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
}
|
||||
Logger(`Deleting unreferenced chunks: ${removeItems}`, LOG_LEVEL.NOTICE, "clean-up-db");
|
||||
const buffer = makeChunkedArrayFromArray(Object.entries(payload));
|
||||
for (const chunkedPayload of buffer) {
|
||||
const rets = await _requestToCouchDBFetch(
|
||||
`${plugin.settings.couchDB_URI}/${plugin.settings.couchDB_DBNAME}`,
|
||||
plugin.settings.couchDB_USER,
|
||||
plugin.settings.couchDB_PASSWORD,
|
||||
"_purge",
|
||||
chunkedPayload.reduce((p, c) => ({ ...p, [c[0]]: c[1] }), {}), "POST");
|
||||
// const result = await rets();
|
||||
Logger(JSON.stringify(await rets.json()), LOG_LEVEL.VERBOSE);
|
||||
}
|
||||
Logger(`Compacting database...`, LOG_LEVEL.NOTICE, "clean-up-db");
|
||||
await db.compact();
|
||||
const endInfo = await db.info();
|
||||
|
||||
Logger(`Processed database active-size: ${sizeToHumanReadable(getSize(endInfo, "active"))}, external-size:${sizeToHumanReadable(getSize(endInfo, "external"))}, file-size: ${sizeToHumanReadable(getSize(endInfo, "file"))}`, LOG_LEVEL.NOTICE);
|
||||
Logger(`Reduced sizes: active-size: ${sizeToHumanReadable(getSize(info, "active") - getSize(endInfo, "active"))}, external-size:${sizeToHumanReadable(getSize(info, "external") - getSize(endInfo, "external"))}, file-size: ${sizeToHumanReadable(getSize(info, "file") - getSize(endInfo, "file"))}`, LOG_LEVEL.NOTICE);
|
||||
Logger(JSON.stringify(endInfo), LOG_LEVEL.VERBOSE, "clean-up-db");
|
||||
Logger(`Local database cleaning up...`);
|
||||
await localDatabaseCleanUp(plugin, true, false);
|
||||
} catch (ex) {
|
||||
Logger("Failed to clean up db.")
|
||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
11
styles.css
11
styles.css
@@ -260,3 +260,14 @@ div.sls-setting-menu-btn {
|
||||
.password-input > .setting-item-control >input {
|
||||
-webkit-text-security: disc;
|
||||
}
|
||||
|
||||
span.ls-mark-cr::after {
|
||||
user-select: none;
|
||||
content: "↲";
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.deleted span.ls-mark-cr::after {
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
76
updates.md
76
updates.md
@@ -14,32 +14,58 @@ I hope you will give it a try.
|
||||
|
||||
#### Minors
|
||||
|
||||
- 0.19.1 to 0.19.6 has been moved into the updates_old.md
|
||||
- 0.19.7
|
||||
- Fixed:
|
||||
- The initial pane of Setting dialogue is now changed to General Settings.
|
||||
- The Setup Wizard is now able to flush existing settings and get into the mode again.
|
||||
- 0.19.8
|
||||
- New feature:
|
||||
- Vault history: A tab has been implemented to give a birds-eye view of the changes that have occurred in the vault.
|
||||
- 0.19.1 to 0.19.11 has been moved into the updates_old.md
|
||||
- 0.19.12
|
||||
- Improved:
|
||||
- Now the passphrases on the dialogue masked out. Thank you @antoKeinanen!
|
||||
- Log dialogue is now shown as one of tabs.
|
||||
- Boot-up performance has been improved.
|
||||
- Customisation sync performance has been improved.
|
||||
- Synchronising performance has been improved.
|
||||
- 0.19.13
|
||||
- Implemented:
|
||||
- Database clean-up is now in beta 2!
|
||||
We can shrink the remote database by deleting unused chunks, with keeping history.
|
||||
Note: Local database is not cleaned up totally. We have to `Fetch` again to let it done.
|
||||
**Note2**: Still in beta. Please back your vault up anything before.
|
||||
- Fixed:
|
||||
- Some minor issues has been fixed.
|
||||
- 0.19.9
|
||||
- New feature (For fixing a problem):
|
||||
- We can fix the database obfuscated and plain paths that have been mixed up.
|
||||
- Improvements
|
||||
- Customisation Sync performance has been improved.
|
||||
- 0.19.10
|
||||
- Fixed
|
||||
- Fixed the issue about fixing the database.
|
||||
- 0.19.11
|
||||
- Improvements:
|
||||
- Hashing ChunkID has been improved.
|
||||
- Logging keeps 400 lines now.
|
||||
- Refactored:
|
||||
- Import statement has been fixed about types.
|
||||
- The log updates are not thinned out now.
|
||||
- 0.19.14
|
||||
- Fixed:
|
||||
- Internal documents are now ignored.
|
||||
- Merge dialogue now respond immediately to button pressing.
|
||||
- Periodic processing now works fine.
|
||||
- The checking interval of detecting conflicted has got shorter.
|
||||
- Replication is now cancelled while cleaning up.
|
||||
- The database locking by the cleaning up is now carefully unlocked.
|
||||
- Missing chunks message is correctly reported.
|
||||
- New feature:
|
||||
- Suspend database reflecting has been implemented.
|
||||
- This can be disabled by `Fetch database with previous behaviour`.
|
||||
- Now fetch suspends the reflecting database and storage changes temporarily to improve the performance.
|
||||
- We can choose the action when the remote database has been cleaned
|
||||
- Merge dialogue now show `↲` before the new line.
|
||||
- Improved:
|
||||
- Now progress is reported while the cleaning up and fetch process.
|
||||
- Cancelled replication is now detected.
|
||||
- 0.19.15
|
||||
- Fixed:
|
||||
- Now storing files after cleaning up is correct works.
|
||||
- Improved:
|
||||
- Cleaning the local database up got incredibly fastened.
|
||||
Now we can clean instead of fetching again when synchronising with the remote which has been cleaned up.
|
||||
- 0.19.16
|
||||
- Many upgrades on this release. I have tried not to let that happen, if something got corrupted, please feel free to notify me.
|
||||
- New feature:
|
||||
- (Beta) ignore files handling
|
||||
We can use `.gitignore`, `.dockerignore`, and anything you like to filter the synchronising files.
|
||||
- Fixed:
|
||||
- Buttons on lock-detected-dialogue now can be shown in narrow-width devices.
|
||||
- Improved:
|
||||
- Some constant has been flattened to be evaluated.
|
||||
- The usage of the deprecated API of obsidian has been reduced.
|
||||
- Now the indexedDB adapter will be enabled while the importing configuration.
|
||||
- Misc:
|
||||
- Compiler, framework, and dependencies have been upgraded.
|
||||
- Due to standing for these impacts (especially in esbuild and svelte,) terser has been introduced.
|
||||
Feel free to notify your opinion to me! I do not like to obfuscate the code too.
|
||||
|
||||
... To continue on to `updates_old.md`.
|
||||
|
||||
@@ -56,7 +56,32 @@ I hope you will give it a try.
|
||||
- Items that have no meaning in the settings are no longer displayed.
|
||||
- Some items have been reordered for clarity.
|
||||
- Each configuration has been grouped.
|
||||
|
||||
- 0.19.7
|
||||
- Fixed:
|
||||
- The initial pane of Setting dialogue is now changed to General Settings.
|
||||
- The Setup Wizard is now able to flush existing settings and get into the mode again.
|
||||
- 0.19.8
|
||||
- New feature:
|
||||
- Vault history: A tab has been implemented to give a birds-eye view of the changes that have occurred in the vault.
|
||||
- Improved:
|
||||
- Now the passphrases on the dialogue masked out. Thank you @antoKeinanen!
|
||||
- Log dialogue is now shown as one of tabs.
|
||||
- Fixed:
|
||||
- Some minor issues has been fixed.
|
||||
- 0.19.9
|
||||
- New feature (For fixing a problem):
|
||||
- We can fix the database obfuscated and plain paths that have been mixed up.
|
||||
- Improvements
|
||||
- Customisation Sync performance has been improved.
|
||||
- 0.19.10
|
||||
- Fixed
|
||||
- Fixed the issue about fixing the database.
|
||||
- 0.19.11
|
||||
- Improvements:
|
||||
- Hashing ChunkID has been improved.
|
||||
- Logging keeps 400 lines now.
|
||||
- Refactored:
|
||||
- Import statement has been fixed about types.
|
||||
### 0.18.0
|
||||
|
||||
#### Now, paths of files in the database can now be obfuscated. (Experimental Feature)
|
||||
|
||||
Reference in New Issue
Block a user