mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-06-18 12:20:15 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cb76bba72 | |||
| 866a49204c | |||
| fb93511ae7 |
@@ -1,24 +1 @@
|
||||
# Always checkout shell scripts with LF line endings (never CRLF)
|
||||
*.sh text eol=lf
|
||||
|
||||
# Standard text files — auto normalize on checkout
|
||||
*.md text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.ini text eol=lf
|
||||
*.env text eol=lf
|
||||
*.json text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.js text eol=lf
|
||||
*.mjs text eol=lf
|
||||
*.css text eol=lf
|
||||
|
||||
# Binary files — no line ending conversion
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.woff2 binary
|
||||
*.woff binary
|
||||
*.sh text eol=lf
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# Self-hosted LiveSync — Environment Variables
|
||||
# Copy this file to .env and fill in your values.
|
||||
# NEVER commit .env to version control.
|
||||
|
||||
# =============================================================================
|
||||
# REQUIRED — CouchDB credentials
|
||||
# =============================================================================
|
||||
|
||||
# Admin username for CouchDB
|
||||
COUCHDB_USER=admin
|
||||
|
||||
# Admin password — use a strong random password (min 16 chars recommended)
|
||||
COUCHDB_PASSWORD=change_me_use_a_strong_password
|
||||
|
||||
# Name of the database the Obsidian plugin will use
|
||||
COUCHDB_DATABASE=obsidiannotes
|
||||
|
||||
# Host port CouchDB is exposed on (default: 5984)
|
||||
# For tunnel-only deployments you can set this to 127.0.0.1:5984 to block external access
|
||||
COUCHDB_PORT=5984
|
||||
|
||||
# =============================================================================
|
||||
# PROFILE: caddy (--profile caddy)
|
||||
# =============================================================================
|
||||
|
||||
# Your public domain pointing to this server (A record)
|
||||
# Example: couchdb.yourdomain.com
|
||||
COUCHDB_DOMAIN=couchdb.yourdomain.com
|
||||
|
||||
# Email for Let's Encrypt TLS certificate notifications
|
||||
ACME_EMAIL=you@yourdomain.com
|
||||
|
||||
# =============================================================================
|
||||
# PROFILE: tailscale (--profile tailscale)
|
||||
# =============================================================================
|
||||
|
||||
# Tailscale OAuth key (not a regular auth key — must be OAuth for persistent use)
|
||||
# Generate at: https://login.tailscale.com/admin/settings/oauth
|
||||
# Scopes needed: devices:write
|
||||
TS_AUTHKEY=tskey-auth-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Hostname this node will have on your tailnet (becomes <hostname>.<tailnet>.ts.net)
|
||||
TS_HOSTNAME=livesync
|
||||
|
||||
# =============================================================================
|
||||
# PROFILE: cloudflare (--profile cloudflare)
|
||||
# =============================================================================
|
||||
|
||||
# Tunnel token from Cloudflare Zero Trust dashboard
|
||||
# Create at: https://one.dash.cloudflare.com/ → Networks → Tunnels → Create tunnel
|
||||
# Copy the token from the "Install connector" step
|
||||
CF_TUNNEL_TOKEN=eyJhIjoixxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
@@ -1,349 +0,0 @@
|
||||
# Self-hosted LiveSync — Docker Setup
|
||||
|
||||
A fully self-hosted CouchDB stack for the [obsidian-livesync](https://github.com/vrtmrz/obsidian-livesync) plugin.
|
||||
**No fly.io. No IBM Cloudant. No cloud accounts required for basic use.**
|
||||
|
||||
> ✅ **Tested on Docker Desktop for Windows (Docker 29.2, Compose v5, WSL2 backend)** — full init, CORS, auth, and idempotent restart verified.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Obsidian (desktop / iOS / Android)
|
||||
│ CouchDB Replication Protocol (HTTPS)
|
||||
▼
|
||||
[ Reverse Proxy / Tunnel ] ◄── Choose ONE profile below
|
||||
│
|
||||
▼
|
||||
[ CouchDB container ] ◄── The only required service
|
||||
│ initialized once by couchdb-init container
|
||||
▼
|
||||
[ Named Docker Volume ] ◄── All vault data stored here
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Prerequisites
|
||||
|
||||
- [Docker Desktop](https://docs.docker.com/desktop/) (Windows/Mac/Linux) or Docker Engine + Compose plugin
|
||||
- A machine that Obsidian devices can reach over HTTPS (see profiles below)
|
||||
|
||||
### 2. Configure
|
||||
|
||||
```bash
|
||||
cd docker/
|
||||
cp .env.example .env
|
||||
# Edit .env — at minimum set COUCHDB_USER and a strong COUCHDB_PASSWORD
|
||||
```
|
||||
|
||||
### 3. Launch
|
||||
|
||||
```bash
|
||||
# Default: CouchDB only (LAN / localhost, no TLS)
|
||||
docker compose up -d
|
||||
|
||||
# With Caddy (public domain + auto Let's Encrypt)
|
||||
docker compose --profile caddy up -d
|
||||
|
||||
# With Tailscale (no domain needed, private mesh or public Funnel)
|
||||
docker compose --profile tailscale up -d
|
||||
|
||||
# With Cloudflare Tunnel (Cloudflare account required)
|
||||
docker compose --profile cloudflare up -d
|
||||
```
|
||||
|
||||
### 4. Verify
|
||||
|
||||
```bash
|
||||
# Should return {"status":"ok"}
|
||||
curl -u admin:yourpassword http://localhost:5984/_up
|
||||
|
||||
# Check CORS headers
|
||||
curl -v -H "Origin: app://obsidian.md" \
|
||||
-u admin:yourpassword \
|
||||
http://localhost:5984/
|
||||
```
|
||||
|
||||
### 5. Connect Obsidian
|
||||
|
||||
In the Obsidian plugin settings (**Self-hosted LiveSync**):
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| URI | `https://your-domain-or-ts-hostname:5984` (or `http://localhost:5984` for LAN-only) |
|
||||
| Username | value of `COUCHDB_USER` |
|
||||
| Password | value of `COUCHDB_PASSWORD` |
|
||||
| Database name | value of `COUCHDB_DATABASE` (default: `obsidiannotes`) |
|
||||
| End-to-end passphrase | *your own chosen passphrase — never stored server-side* |
|
||||
|
||||
---
|
||||
|
||||
## Profile Details
|
||||
|
||||
### Default (no profile) — LAN / Localhost only
|
||||
|
||||
CouchDB is exposed on `http://localhost:5984` (or LAN IP).
|
||||
**Desktop Obsidian works over HTTP.** Mobile Obsidian requires HTTPS — use a tunnel profile.
|
||||
|
||||
### `--profile caddy` — Public Domain + Auto TLS
|
||||
|
||||
**Requires**:
|
||||
- A domain with an A record pointing to this server's public IP
|
||||
- Ports 80 and 443 open in your firewall/router
|
||||
|
||||
**Set in `.env`**:
|
||||
```
|
||||
COUCHDB_DOMAIN=couchdb.yourdomain.com
|
||||
ACME_EMAIL=you@example.com
|
||||
```
|
||||
|
||||
Caddy automatically issues a Let's Encrypt certificate. No manual cert management.
|
||||
|
||||
### `--profile tailscale` — No Domain Required ✅ Recommended for privacy
|
||||
|
||||
**Requires**:
|
||||
- Free [Tailscale account](https://login.tailscale.com/)
|
||||
- Install the Tailscale app on all your Obsidian devices
|
||||
- Generate an **OAuth key** at: https://login.tailscale.com/admin/settings/oauth
|
||||
(Scopes: `devices:write`)
|
||||
|
||||
**Set in `.env`**:
|
||||
```
|
||||
TS_AUTHKEY=tskey-auth-...
|
||||
TS_HOSTNAME=livesync
|
||||
```
|
||||
|
||||
**Two sub-modes**:
|
||||
- **VPN mode** (default): CouchDB accessible only to devices on your Tailnet at
|
||||
`https://livesync.<tailnet>.ts.net` — completely private
|
||||
- **Funnel mode**: public HTTPS at `https://livesync.<tailnet>.ts.net` — no domain purchase
|
||||
Enable in your [Tailscale ACL](https://login.tailscale.com/admin/acls):
|
||||
```json
|
||||
"nodeAttrs": [{"target": ["tag:container"], "attr": ["funnel"]}]
|
||||
```
|
||||
|
||||
> **Note on Windows Docker Desktop**: If `/dev/net/tun` is unavailable, add `TS_USERSPACE=true`
|
||||
> to the tailscale service environment in `docker-compose.yml`.
|
||||
|
||||
### `--profile cloudflare` — Cloudflare Tunnel
|
||||
|
||||
**Requires**:
|
||||
- Free [Cloudflare account](https://www.cloudflare.com/)
|
||||
- A domain managed by Cloudflare DNS (can transfer existing domain for free)
|
||||
- Cloudflare Zero Trust account (free)
|
||||
|
||||
#### Step 1: Create a Cloudflare Tunnel
|
||||
|
||||
1. Log in to [Cloudflare Zero Trust](https://one.dash.cloudflare.com/)
|
||||
2. Navigate to **Networks → Tunnels**
|
||||
3. Click **Create a tunnel**
|
||||
4. Choose **Cloudflared** as tunnel type
|
||||
5. Name your tunnel (e.g., `obsidian-livesync`)
|
||||
6. Click **Save tunnel**
|
||||
7. **Copy the tunnel token** — it looks like `eyJhIjoiZX...` (very long, ~400 characters)
|
||||
|
||||
#### Step 2: Configure Environment
|
||||
|
||||
Edit `docker/.env`:
|
||||
```env
|
||||
CF_TUNNEL_TOKEN=eyJhIjoiZX... # Paste the full token from Step 1
|
||||
COUCHDB_DOMAIN=sync.yourdomain.com # Must be a domain managed by Cloudflare
|
||||
```
|
||||
|
||||
#### Step 3: Add Public Hostname Route
|
||||
|
||||
🚨 **CRITICAL**: Token-based tunnels ignore the local `cloudflared.yml` config file. All routing is controlled from the dashboard.
|
||||
|
||||
Back in the Zero Trust dashboard, **in the same tunnel creation flow** (or edit your tunnel later):
|
||||
|
||||
1. Go to the **Public Hostname** tab
|
||||
2. Click **Add a public hostname**
|
||||
3. Configure:
|
||||
- **Subdomain**: `sync` (or your preferred subdomain)
|
||||
- **Domain**: Select your Cloudflare domain from dropdown
|
||||
- **Type**: `HTTP`
|
||||
- **URL**: `couchdb:5984` ← **Do NOT use `localhost`!**
|
||||
|
||||
**Why `couchdb:5984` not `localhost:5984`?**
|
||||
- The `cloudflared` container runs inside Docker on the same network as `couchdb`
|
||||
- Docker's internal DNS resolves `couchdb` to the correct container
|
||||
- Using `localhost` would look inside the `cloudflared` container (nothing there)
|
||||
|
||||
4. Under **Additional application settings** (expand):
|
||||
- **No TLS Verify**: Leave **OFF** (CouchDB uses plain HTTP internally, that's fine)
|
||||
- Leave other settings at defaults
|
||||
5. Click **Save hostname**
|
||||
|
||||
#### Step 4: Start the Stack
|
||||
|
||||
```bash
|
||||
cd docker/
|
||||
docker compose --profile cloudflare up -d
|
||||
```
|
||||
|
||||
Verify containers are running:
|
||||
```bash
|
||||
docker ps --filter "name=livesync"
|
||||
```
|
||||
|
||||
You should see:
|
||||
- `livesync-couchdb` — Status: Up (healthy)
|
||||
- `livesync-cloudflared` — Status: Up
|
||||
- `livesync-init` — Status: Exited (0)
|
||||
|
||||
#### Step 5: Test the Connection
|
||||
|
||||
```bash
|
||||
# Should return 401 Unauthorized (proves CouchDB auth is working)
|
||||
curl -I https://sync.yourdomain.com
|
||||
|
||||
# Should return {"couchdb":"Welcome",...}
|
||||
curl -u admin:yourpassword https://sync.yourdomain.com
|
||||
```
|
||||
|
||||
If you get **404**, see Troubleshooting below.
|
||||
|
||||
#### Step 6: Configure Obsidian Plugin
|
||||
|
||||
In Obsidian → Settings → **Self-hosted LiveSync**:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| URI | `https://sync.yourdomain.com` |
|
||||
| Username | value of `COUCHDB_USER` from `.env` |
|
||||
| Password | value of `COUCHDB_PASSWORD` from `.env` |
|
||||
| Database name | value of `COUCHDB_DATABASE` from `.env` (default: `obsidiannotes`) |
|
||||
| End-to-end passphrase | *Choose your own* — never stored server-side |
|
||||
|
||||
Under **Remote Database Configuration → Advanced**:
|
||||
- Enable: ✅ **Use Request API to avoid inevitable CORS problem**
|
||||
(See "Known Issue" below for why this is critical)
|
||||
|
||||
---
|
||||
|
||||
#### 🔧 Troubleshooting Cloudflare Tunnel
|
||||
|
||||
**Problem: 404 Error / Cloud flare Generic Error Page**
|
||||
|
||||
**Diagnosis**:
|
||||
```bash
|
||||
# Check if cloudflared is running
|
||||
docker logs livesync-cloudflared --tail 20
|
||||
|
||||
# Look for: "Registered tunnel connection"
|
||||
# If you see the connector ID, the tunnel is connected but routing is wrong
|
||||
```
|
||||
|
||||
**Fix**: The public hostname rule is missing or incorrect.
|
||||
|
||||
1. Go to Zero Trust → Networks → Tunnels → your tunnel → **Edit**
|
||||
2. Click **Public Hostname** tab
|
||||
3. Verify a hostname exists with:
|
||||
- Service Type: `HTTP`
|
||||
- URL: `couchdb:5984` (NOT `localhost:5984`)
|
||||
4. If no hostname exists, add it (see Step 3 above)
|
||||
5. Wait 30 seconds for changes to propagate, then test again
|
||||
|
||||
**Problem: Connection immediately closes / 502 Bad Gateway**
|
||||
|
||||
**Diagnosis**: CouchDB is not healthy or not on the same Docker network as cloudflared.
|
||||
|
||||
```bash
|
||||
docker ps --filter "name=livesync-couchdb"
|
||||
# Status should be: Up (healthy)
|
||||
|
||||
docker inspect livesync-couchdb -f '{{.NetworkSettings.Networks}}'
|
||||
# Should show: livesync-net
|
||||
|
||||
docker inspect livesync-cloudflared -f '{{.NetworkSettings.Networks}}'
|
||||
# Should also show: livesync-net
|
||||
```
|
||||
|
||||
**Fix**: If CouchDB is unhealthy, check logs:
|
||||
```bash
|
||||
docker logs livesync-couchdb --tail 50
|
||||
```
|
||||
|
||||
**Problem: 524 Timeout Errors During Sync**
|
||||
|
||||
**Root cause**: Cloudflare's proxy has a **100-second idle timeout**. CouchDB's replication protocol uses long-polling on the `_changes` feed, which can idle for longer during quiet periods.
|
||||
|
||||
**Fix**: Switch to short-polling mode in the Obsidian plugin:
|
||||
1. Obsidian → Settings → Self-hosted LiveSync
|
||||
2. **Remote Database Configuration → Advanced**
|
||||
3. Enable: ✅ **Use Request API to avoid inevitable CORS problem**
|
||||
4. Save and restart sync
|
||||
|
||||
This keeps all requests under 100 seconds.
|
||||
|
||||
**Alternative**: Use Tailscale or Caddy profiles instead — neither has aggressive timeouts.
|
||||
|
||||
---
|
||||
|
||||
## Data & Backup
|
||||
|
||||
All vault data lives in the `couchdb-data` Docker named volume.
|
||||
|
||||
```bash
|
||||
# Backup
|
||||
docker run --rm -v obsidian-livesync_couchdb-data:/data \
|
||||
-v $(pwd)/backup:/backup alpine \
|
||||
tar czf /backup/couchdb-backup-$(date +%Y%m%d).tar.gz -C /data .
|
||||
|
||||
# Restore
|
||||
docker run --rm -v obsidian-livesync_couchdb-data:/data \
|
||||
-v $(pwd)/backup:/backup alpine \
|
||||
tar xzf /backup/couchdb-backup-20260218.tar.gz -C /data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
- CouchDB requires authentication for **all** requests (configured by `livesync.ini`)
|
||||
- Enable **End-to-End Encryption** passphrase in the Obsidian plugin — vault data is
|
||||
encrypted before it ever leaves your device
|
||||
- The init container runs once and exits — it has no persistent access
|
||||
- Never expose CouchDB's admin interface (`/_utils`) to the public internet;
|
||||
use a firewall rule or the path-based obfuscation trick from
|
||||
[self-hosted-livesync-server](https://github.com/vrtmrz/self-hosted-livesync-server)
|
||||
|
||||
---
|
||||
|
||||
## Useful Commands
|
||||
|
||||
```bash
|
||||
# View logs
|
||||
docker compose logs -f couchdb
|
||||
docker compose logs couchdb-init
|
||||
|
||||
# Re-run init (e.g. after changing credentials)
|
||||
docker compose restart couchdb-init
|
||||
|
||||
# Stop without removing data
|
||||
docker compose down
|
||||
|
||||
# Stop AND remove all data volumes (DESTRUCTIVE)
|
||||
docker compose down -v
|
||||
|
||||
# Open CouchDB admin UI (Fauxton) in browser
|
||||
open http://localhost:5984/_utils
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---|---|
|
||||
| Init container keeps restarting | CouchDB not healthy yet — wait 30s, check `docker compose logs couchdb` |
|
||||
| `curl: (52) Empty reply` | CouchDB not fully started — the healthcheck should gate this |
|
||||
| Mobile can't connect | Needs HTTPS — use tailscale or caddy profile |
|
||||
| 524 errors with Cloudflare | Enable "Use Request API" toggle in Obsidian plugin |
|
||||
| `Permission denied` on volumes | Run `docker compose down -v` and retry — first-run volume ownership issue |
|
||||
| CORS errors in browser | Confirm CouchDB headers: `curl -v -H "Origin: app://obsidian.md" http://localhost:5984/` |
|
||||
| CouchDB exits immediately, zero logs (Windows) | **Do not add `:ro`** to the `livesync.ini` volume mount. CouchDB's entrypoint runs `chmod 0644` on all files in `/opt/couchdb/etc` — read-only bind mounts cause a silent EPERM crash on Docker Desktop for Windows (WSL2). The compose file is already correct; do not modify it. |
|
||||
| Settings in `livesync.ini` seem ignored | Settings requiring restart (e.g. bind_address) load at start. Runtime-only settings (require_valid_user, enable_cors) are set by the init container via REST API and take effect immediately without restart. |
|
||||
@@ -1,26 +0,0 @@
|
||||
# Caddy config for Self-hosted LiveSync CouchDB
|
||||
# =============================================================================
|
||||
# IMPORTANT: CouchDB handles CORS itself.
|
||||
# Do NOT add CORS headers here — they will conflict with CouchDB's own headers.
|
||||
# Do NOT intercept OPTIONS requests.
|
||||
# =============================================================================
|
||||
|
||||
{
|
||||
# Email used for Let's Encrypt certificate notifications
|
||||
email {$ACME_EMAIL}
|
||||
}
|
||||
|
||||
{$COUCHDB_DOMAIN} {
|
||||
# Forward all traffic to CouchDB, preserving Host and forwarded-for headers
|
||||
reverse_proxy couchdb:5984 {
|
||||
header_up Host {host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
|
||||
# Logging
|
||||
log {
|
||||
output stdout
|
||||
level WARN
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
# cloudflared tunnel configuration for Self-hosted LiveSync
|
||||
# =============================================================================
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. Create a tunnel in Cloudflare Zero Trust → Networks → Tunnels
|
||||
# 2. Copy the tunnel token to CF_TUNNEL_TOKEN in your .env
|
||||
# 3. Add a public hostname in the tunnel config:
|
||||
# Hostname : couchdb.yourdomain.com (or whatever you set COUCHDB_DOMAIN to)
|
||||
# Service : http://couchdb:5984
|
||||
#
|
||||
# Known issue: Cloudflare's 100-second proxy timeout can interrupt CouchDB's
|
||||
# long-polling replication change feed, causing 524 errors.
|
||||
# MITIGATION: In the Obsidian plugin settings, enable:
|
||||
# "Use Request API to avoid inevitable CORS problem"
|
||||
# This switches from long-poll to short-poll mode.
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
tunnel: ${CF_TUNNEL_ID}
|
||||
credentials-file: /etc/cloudflared/credentials.json
|
||||
|
||||
ingress:
|
||||
- hostname: ${COUCHDB_DOMAIN}
|
||||
service: http://couchdb:5984
|
||||
originRequest:
|
||||
# Increase timeouts for CouchDB replication streams
|
||||
connectTimeout: 30s
|
||||
keepAliveTimeout: 90s
|
||||
keepAliveConnections: 100
|
||||
noTLSVerify: false
|
||||
- service: http_status:404
|
||||
@@ -1,30 +0,0 @@
|
||||
; CouchDB local configuration for Self-hosted LiveSync
|
||||
; This file is volume-mounted into /opt/couchdb/etc/local.d/livesync.ini
|
||||
;
|
||||
; IMPORTANT: Do NOT set require_valid_user here.
|
||||
; CouchDB needs to start without auth to complete its first-run cluster setup
|
||||
; (_users, _replicator databases must be created first).
|
||||
; The couchdb-init service applies auth lockdown via REST API after first-run.
|
||||
|
||||
[couchdb]
|
||||
; Max size per document (50MB). Large enough for binary attachments.
|
||||
max_document_size = 50000000
|
||||
|
||||
[chttpd]
|
||||
; Bind on all interfaces.
|
||||
bind_address = 0.0.0.0
|
||||
port = 5984
|
||||
; 4 GB max request (handles very large vaults)
|
||||
max_http_request_size = 4294967296
|
||||
|
||||
[httpd]
|
||||
WWW-Authenticate = Basic realm="couchdb"
|
||||
|
||||
[cors]
|
||||
; These are the exact app origins Obsidian uses on desktop + mobile
|
||||
credentials = true
|
||||
origins = app://obsidian.md,capacitor://localhost,http://localhost
|
||||
|
||||
[log]
|
||||
; Reduce noise in Docker logs — set to "debug" if troubleshooting
|
||||
level = warning
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"TCP": {
|
||||
"443": {
|
||||
"HTTPS": true
|
||||
}
|
||||
},
|
||||
"Web": {
|
||||
"${TS_CERT_DOMAIN}:443": {
|
||||
"Handlers": {
|
||||
"/": {
|
||||
"Proxy": "http://127.0.0.1:5984"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"AllowFunnel": {
|
||||
"${TS_CERT_DOMAIN}:443": true
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
# Self-hosted LiveSync — Docker Compose
|
||||
# =============================================================================
|
||||
# PROFILES
|
||||
# --------
|
||||
# (default) CouchDB only — LAN/localhost access, no TLS
|
||||
# Suitable for desktop-only use or testing.
|
||||
#
|
||||
# --profile caddy CouchDB + Caddy reverse proxy
|
||||
# Auto TLS via Let's Encrypt. Needs public domain + ports 80/443.
|
||||
#
|
||||
# --profile tailscale CouchDB + Tailscale sidecar
|
||||
# No domain required. HTTPS via *.ts.net PKI.
|
||||
# Needs a Tailscale account (free tier works).
|
||||
#
|
||||
# --profile cloudflare CouchDB + cloudflared tunnel daemon
|
||||
# Free public HTTPS via Cloudflare. Needs a CF account + tunnel token.
|
||||
# NOTE: Enable "Use Request API" in the Obsidian plugin to avoid 524 timeouts.
|
||||
#
|
||||
# QUICK START (local test):
|
||||
# cp .env.example .env && edit .env
|
||||
# docker compose up -d
|
||||
# curl -u admin:yourpassword http://localhost:5984/_up
|
||||
# =============================================================================
|
||||
|
||||
name: obsidian-livesync
|
||||
|
||||
services:
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CouchDB — the only required service
|
||||
# ---------------------------------------------------------------------------
|
||||
couchdb:
|
||||
image: couchdb:latest
|
||||
container_name: livesync-couchdb
|
||||
restart: unless-stopped
|
||||
# NOTE: Do NOT set user: here — the CouchDB entrypoint starts as root to
|
||||
# write docker.ini (from env vars), then drops to uid 5984 automatically.
|
||||
environment:
|
||||
COUCHDB_USER: ${COUCHDB_USER:?Set COUCHDB_USER in .env}
|
||||
COUCHDB_PASSWORD: ${COUCHDB_PASSWORD:?Set COUCHDB_PASSWORD in .env}
|
||||
volumes:
|
||||
- couchdb-data:/opt/couchdb/data
|
||||
# Mount to /opt/couchdb/etc/local.ini (NOT into local.d/).
|
||||
# Do NOT use :ro — the CouchDB entrypoint runs chmod on this file at startup
|
||||
# and will crash with EPERM if the file is read-only. The file is only read
|
||||
# at startup; runtime changes go via the REST API into local.d/docker.ini.
|
||||
- ./config/livesync.ini:/opt/couchdb/etc/local.ini
|
||||
ports:
|
||||
# Exposes CouchDB on the host for LAN/localhost access.
|
||||
# The tunnel profiles (caddy/tailscale/cloudflare) provide HTTPS on top.
|
||||
# You can remove this port mapping once a tunnel profile is in use.
|
||||
- "${COUCHDB_PORT:-5984}:5984"
|
||||
healthcheck:
|
||||
# Test with admin credentials — ensures both CouchDB is up AND auth is ready.
|
||||
# ${COUCHDB_USER} / ${COUCHDB_PASSWORD} are expanded by Docker Compose here.
|
||||
test:
|
||||
- "CMD-SHELL"
|
||||
- "curl -sf -u ${COUCHDB_USER}:${COUCHDB_PASSWORD} http://localhost:5984/_session | grep -q ok || exit 1"
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 24
|
||||
start_period: 20s
|
||||
networks:
|
||||
- livesync-net
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# One-shot init container — runs couchdb-init.sh after CouchDB is healthy.
|
||||
# Sets single-node cluster, auth requirements, CORS, size limits, creates DB.
|
||||
# Restarts on failure (e.g. race at first boot) but won't re-run if already done.
|
||||
# ---------------------------------------------------------------------------
|
||||
couchdb-init:
|
||||
image: curlimages/curl:latest
|
||||
container_name: livesync-init
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
couchdb:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
COUCHDB_INTERNAL_URL: http://couchdb:5984
|
||||
COUCHDB_USER: ${COUCHDB_USER}
|
||||
COUCHDB_PASSWORD: ${COUCHDB_PASSWORD}
|
||||
COUCHDB_DATABASE: ${COUCHDB_DATABASE:-obsidiannotes}
|
||||
volumes:
|
||||
- ./scripts/couchdb-init.sh:/couchdb-init.sh:ro
|
||||
entrypoint: ["sh", "/couchdb-init.sh"]
|
||||
networks:
|
||||
- livesync-net
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PROFILE: caddy — Caddy reverse proxy with automatic Let's Encrypt TLS
|
||||
# Requirements: public domain, ports 80 + 443 open to internet
|
||||
# Usage: docker compose --profile caddy up -d
|
||||
# ---------------------------------------------------------------------------
|
||||
caddy:
|
||||
image: caddy:latest
|
||||
container_name: livesync-caddy
|
||||
profiles: [caddy]
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
couchdb:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
COUCHDB_DOMAIN: ${COUCHDB_DOMAIN:?Set COUCHDB_DOMAIN in .env for caddy profile}
|
||||
ACME_EMAIL: ${ACME_EMAIL:?Set ACME_EMAIL in .env for caddy profile}
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./config/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
networks:
|
||||
- livesync-net
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PROFILE: tailscale — Tailscale sidecar for mesh VPN + optional Funnel
|
||||
# Requirements: Tailscale account (free), OAuth key, Funnel enabled in ACL
|
||||
# Usage: docker compose --profile tailscale up -d
|
||||
# The CouchDB port mapping above can be removed for tailscale-only deployments.
|
||||
# ---------------------------------------------------------------------------
|
||||
tailscale:
|
||||
image: tailscale/tailscale:latest
|
||||
container_name: livesync-tailscale
|
||||
profiles: [tailscale]
|
||||
restart: unless-stopped
|
||||
hostname: ${TS_HOSTNAME:-livesync}
|
||||
environment:
|
||||
TS_AUTHKEY: ${TS_AUTHKEY:?Set TS_AUTHKEY in .env for tailscale profile}
|
||||
TS_STATE_DIR: /var/lib/tailscale
|
||||
TS_SERVE_CONFIG: /config/serve.json
|
||||
TS_USERSPACE: "false"
|
||||
TS_ACCEPT_DNS: "false"
|
||||
TS_EXTRA_ARGS: ""
|
||||
volumes:
|
||||
- tailscale-state:/var/lib/tailscale
|
||||
- ./config/ts-serve.json:/config/serve.json:ro
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
# Share CouchDB's network namespace so Tailscale can reach it on localhost
|
||||
network_mode: service:couchdb
|
||||
depends_on:
|
||||
- couchdb
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PROFILE: cloudflare — cloudflared tunnel daemon
|
||||
# Requirements: Cloudflare account, tunnel token from CF Zero Trust dashboard
|
||||
# Usage: docker compose --profile cloudflare up -d
|
||||
# NOTE: Enable "Use Request API" toggle in the Obsidian LiveSync plugin settings
|
||||
# to avoid Cloudflare's 100-second proxy timeout (524 errors).
|
||||
# ---------------------------------------------------------------------------
|
||||
cloudflared:
|
||||
image: cloudflare/cloudflared:latest
|
||||
container_name: livesync-cloudflared
|
||||
profiles: [cloudflare]
|
||||
restart: unless-stopped
|
||||
command: tunnel --no-autoupdate run
|
||||
environment:
|
||||
TUNNEL_TOKEN: ${CF_TUNNEL_TOKEN:?Set CF_TUNNEL_TOKEN in .env for cloudflare profile}
|
||||
volumes:
|
||||
- ./config/cloudflared.yml:/etc/cloudflared/config.yml:ro
|
||||
depends_on:
|
||||
couchdb:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- livesync-net
|
||||
|
||||
# =============================================================================
|
||||
# Volumes
|
||||
# =============================================================================
|
||||
volumes:
|
||||
couchdb-data:
|
||||
driver: local
|
||||
caddy-data:
|
||||
driver: local
|
||||
caddy-config:
|
||||
driver: local
|
||||
tailscale-state:
|
||||
driver: local
|
||||
|
||||
# =============================================================================
|
||||
# Networks
|
||||
# =============================================================================
|
||||
networks:
|
||||
livesync-net:
|
||||
driver: bridge
|
||||
@@ -1,79 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Self-hosted LiveSync — CouchDB Initialization Script
|
||||
# Runs once on first startup via the couchdb-init service.
|
||||
# Configures single-node cluster, auth, CORS, and size limits.
|
||||
|
||||
set -e
|
||||
|
||||
hostname="${COUCHDB_INTERNAL_URL:-http://couchdb:5984}"
|
||||
username="${COUCHDB_USER:?COUCHDB_USER is required}"
|
||||
password="${COUCHDB_PASSWORD:?COUCHDB_PASSWORD is required}"
|
||||
node="${COUCHDB_NODE:-_local}"
|
||||
|
||||
echo "==> Waiting for CouchDB at ${hostname} ..."
|
||||
# _up is publicly accessible (no auth required) — safe pre-auth wait
|
||||
until curl -sf "${hostname}/_up" 2>/dev/null | grep -q '"status":"ok"'; do
|
||||
printf '.'
|
||||
sleep 2
|
||||
done
|
||||
echo ""
|
||||
echo "==> CouchDB is up. Initializing..."
|
||||
|
||||
# 1. Enable single-node cluster
|
||||
curl -sf -X POST "${hostname}/_cluster_setup" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"action\":\"enable_single_node\",\"username\":\"${username}\",\"password\":\"${password}\",\"bind_address\":\"0.0.0.0\",\"port\":5984,\"singlenode\":true}" \
|
||||
--user "${username}:${password}" && echo "[OK] cluster_setup"
|
||||
|
||||
# 2. Require valid user on both http interfaces
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/chttpd/require_valid_user" \
|
||||
-H "Content-Type: application/json" -d '"true"' --user "${username}:${password}" && echo "[OK] chttpd/require_valid_user"
|
||||
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/chttpd_auth/require_valid_user" \
|
||||
-H "Content-Type: application/json" -d '"true"' --user "${username}:${password}" && echo "[OK] chttpd_auth/require_valid_user"
|
||||
|
||||
# 3. HTTP auth challenge header
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/httpd/WWW-Authenticate" \
|
||||
-H "Content-Type: application/json" -d '"Basic realm=\"couchdb\""' --user "${username}:${password}" && echo "[OK] httpd/WWW-Authenticate"
|
||||
|
||||
# 4. Enable CORS on both http listeners
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/httpd/enable_cors" \
|
||||
-H "Content-Type: application/json" -d '"true"' --user "${username}:${password}" && echo "[OK] httpd/enable_cors"
|
||||
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/chttpd/enable_cors" \
|
||||
-H "Content-Type: application/json" -d '"true"' --user "${username}:${password}" && echo "[OK] chttpd/enable_cors"
|
||||
|
||||
# 5. Increase size limits for large vaults
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/chttpd/max_http_request_size" \
|
||||
-H "Content-Type: application/json" -d '"4294967296"' --user "${username}:${password}" && echo "[OK] chttpd/max_http_request_size"
|
||||
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/couchdb/max_document_size" \
|
||||
-H "Content-Type: application/json" -d '"50000000"' --user "${username}:${password}" && echo "[OK] couchdb/max_document_size"
|
||||
|
||||
# 6. CORS configuration — allow Obsidian app origins
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/cors/credentials" \
|
||||
-H "Content-Type: application/json" -d '"true"' --user "${username}:${password}" && echo "[OK] cors/credentials"
|
||||
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/cors/origins" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '"app://obsidian.md,capacitor://localhost,http://localhost"' \
|
||||
--user "${username}:${password}" && echo "[OK] cors/origins"
|
||||
|
||||
# 7. Create the vault database if it doesn't exist
|
||||
db="${COUCHDB_DATABASE:-obsidiannotes}"
|
||||
set +e
|
||||
status=$(curl -sf -o /dev/null -w "%{http_code}" --user "${username}:${password}" "${hostname}/${db}" 2>/dev/null)
|
||||
curl_exit=$?
|
||||
set -e
|
||||
|
||||
if [ "$status" = "200" ]; then
|
||||
echo "[OK] database '${db}' already exists"
|
||||
else
|
||||
curl -sf -X PUT "${hostname}/${db}" --user "${username}:${password}" && echo "[OK] database '${db}' created" || echo "[WARN] database creation returned non-200 — may already exist"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "==> CouchDB initialization complete!"
|
||||
echo " URL : ${hostname}"
|
||||
echo " Database : ${db}"
|
||||
echo " Username : ${username}"
|
||||
@@ -97,7 +97,7 @@ export const obsidianRules = {
|
||||
// -- Plugin specific overrides
|
||||
"obsidianmd/rule-custom-message": "off",
|
||||
"obsidianmd/ui/sentence-case": "off",
|
||||
"obsidianmd/no-plugin-as-component": "off",
|
||||
"obsidianmd/no-plugin-as-component": "warn",
|
||||
|
||||
// -- Temporary overrides for migration
|
||||
"obsidianmd/no-static-styles-assignment": "off",
|
||||
|
||||
+1
-2
@@ -86,8 +86,6 @@ export default defineConfig([
|
||||
parserOptions: {
|
||||
parser: tsParser,
|
||||
extraFileExtensions: [".svelte"],
|
||||
project: "./tsconfig.json",
|
||||
rootDir: "./",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
@@ -96,6 +94,7 @@ export default defineConfig([
|
||||
// it may improve in the future with some options as like ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],]
|
||||
"no-unused-vars": "off",
|
||||
...obsidianRules,
|
||||
"obsidianmd/no-plugin-as-component": "off",
|
||||
...ImportAliasRules("."),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain";
|
||||
import type { ServiceModules } from "./lib/src/interfaces/ServiceModule";
|
||||
import { ModuleBasicMenu } from "./modules/essential/ModuleBasicMenu";
|
||||
import { usePrepareDatabaseForUse } from "./lib/src/serviceFeatures/prepareDatabaseForUse";
|
||||
import type { Constructor } from "@lib/common/utils.type";
|
||||
|
||||
export class LiveSyncBaseCore<
|
||||
T extends ServiceContext = ServiceContext,
|
||||
@@ -120,7 +121,7 @@ export class LiveSyncBaseCore<
|
||||
* @param constructor
|
||||
* @returns
|
||||
*/
|
||||
getModule<T extends AbstractModule>(constructor: new (...args: any[]) => T): T {
|
||||
getModule<T extends AbstractModule>(constructor: Constructor<T>): T {
|
||||
for (const module of this.modules) {
|
||||
if (module.constructor === constructor) return module as T;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# Self-hosted LiveSync CLI
|
||||
Command-line version of Self-hosted LiveSync plugin for syncing vaults without Obsidian.
|
||||
Command-line version of Self-hosted LiveSync plug-in for syncing vaults without Obsidian.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Sync Obsidian vaults using CouchDB without running Obsidian
|
||||
- ✅ Compatible with Self-hosted LiveSync plugin settings
|
||||
- ✅ Compatible with Self-hosted LiveSync plug-in settings
|
||||
- ✅ Supports all core sync features (encryption, conflict resolution, etc.)
|
||||
- ✅ Lightweight and headless operation
|
||||
- ✅ Cross-platform (Windows, macOS, Linux)
|
||||
|
||||
## Architecture
|
||||
|
||||
This CLI version is built using the same core as the Obsidian plugin:
|
||||
This CLI version is built using the same core as the Obsidian plug-in:
|
||||
|
||||
```
|
||||
CLI Main
|
||||
@@ -290,7 +290,7 @@ livesync-cli /path/to/your-local-database --settings /path/to/settings.json unlo
|
||||
|
||||
### Configuration
|
||||
|
||||
The CLI uses the same settings format as the Obsidian plugin. Create a `.livesync/settings.json` file in your vault directory:
|
||||
The CLI uses the same settings format as the Obsidian plug-in. Create a `.livesync/settings.json` file in your vault directory:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -60,6 +60,7 @@ export class NodeStorageAdapter implements IStorageAdapter<NodeStat> {
|
||||
|
||||
async readBinary(p: string): Promise<ArrayBuffer> {
|
||||
const buffer = await fs.readFile(this.resolvePath(p));
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- required in environments where Buffer.buffer is ArrayBufferLike
|
||||
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) as ArrayBuffer;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ export class NodeVaultAdapter implements IVaultAdapter<NodeFile> {
|
||||
const buffer = await fs.readFile(this.resolvePath(file.path));
|
||||
// Same correction as read() — ensure stat.size matches actual byte length.
|
||||
file.stat.size = buffer.length;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- required in environments where Buffer.buffer is ArrayBufferLike
|
||||
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) as ArrayBuffer;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { path, readline } from "../node-compat";
|
||||
|
||||
export function toArrayBuffer(data: Buffer): ArrayBuffer {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- required in environments where Buffer.buffer is ArrayBufferLike
|
||||
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;
|
||||
}
|
||||
|
||||
|
||||
@@ -303,7 +303,7 @@ export async function main() {
|
||||
console.error(`Error: ${databasePath} is not a directory`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.error(`Error: Database directory ${databasePath} does not exist`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -324,7 +324,7 @@ export async function main() {
|
||||
? path.resolve(options.commandArgs[0])
|
||||
: options.vaultPath
|
||||
? path.resolve(options.vaultPath)
|
||||
: databasePath!;
|
||||
: databasePath;
|
||||
|
||||
// Check if vault directory exists
|
||||
try {
|
||||
@@ -333,7 +333,7 @@ export async function main() {
|
||||
console.error(`Error: Vault path ${vaultPath} is not a directory`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.error(`Error: Vault directory ${vaultPath} does not exist`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -415,7 +415,7 @@ export async function main() {
|
||||
// Force disable IndexedDB adapter in CLI environment
|
||||
data.useIndexedDBAdapter = false;
|
||||
return data;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
if (options.verbose) {
|
||||
console.error(`[Settings] File not found, using defaults`);
|
||||
}
|
||||
@@ -434,7 +434,7 @@ export async function main() {
|
||||
() => [], // No add-ons
|
||||
(core) => {
|
||||
// Register P2P replicator feature.
|
||||
const _replicator = useP2PReplicatorFeature(core);
|
||||
useP2PReplicatorFeature(core);
|
||||
// Add target filter to prevent internal files are handled
|
||||
core.services.vault.isTargetFile.addHandler(async (target) => {
|
||||
const targetPath = stripAllPrefixes(getPathFromUXFileInfo(target));
|
||||
@@ -458,8 +458,8 @@ export async function main() {
|
||||
if (rules.shouldIgnore(targetPath)) {
|
||||
return false;
|
||||
}
|
||||
// undefined = pass through to next handler in chain
|
||||
return undefined;
|
||||
// At least this handler think it is a target file, but other handlers may still veto it.
|
||||
return true;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
/* eslint-disable obsidianmd/no-nodejs-builtins */
|
||||
// eslint-disable-next-line obsidianmd/no-nodejs-builtins -- This file is used to provide Node.js built-in modules in the CLI environment, which is not running in a browser context.
|
||||
import * as nodeFs from "node:fs";
|
||||
// eslint-disable-next-line obsidianmd/no-nodejs-builtins -- This file is used to provide Node.js built-in modules in the CLI environment, which is not running in a browser context.
|
||||
import * as nodeFsPromises from "node:fs/promises";
|
||||
// eslint-disable-next-line obsidianmd/no-nodejs-builtins -- This file is used to provide Node.js built-in modules in the CLI environment, which is not running in a browser context.
|
||||
import * as nodePath from "node:path";
|
||||
// eslint-disable-next-line obsidianmd/no-nodejs-builtins -- This file is used to provide Node.js built-in modules in the CLI environment, which is not running in a browser context.
|
||||
import * as nodeReadlinePromises from "node:readline/promises";
|
||||
// eslint-disable-next-line obsidianmd/no-nodejs-builtins -- This file is used to provide Node.js built-in modules in the CLI environment, which is not running in a browser context.
|
||||
import type { Stats } from "node:fs";
|
||||
export {
|
||||
nodeFs as fs,
|
||||
nodeFsPromises as fsPromises,
|
||||
nodePath as path,
|
||||
nodeReadlinePromises as readline,
|
||||
type Stats,
|
||||
};
|
||||
export { nodeFs as fs, nodeFsPromises as fsPromises, nodePath as path, nodeReadlinePromises as readline, type Stats };
|
||||
|
||||
@@ -77,9 +77,7 @@ export class BackgroundCliProcess {
|
||||
if (this.combined.includes(needle)) return;
|
||||
const status = await Promise.race([
|
||||
this.child.status.then((s) => ({ type: "status" as const, status: s })),
|
||||
new Promise<{ type: "tick" }>((resolve) =>
|
||||
setTimeout(() => resolve({ type: "tick" }), 100)
|
||||
),
|
||||
new Promise<{ type: "tick" }>((resolve) => setTimeout(() => resolve({ type: "tick" }), 100)),
|
||||
]);
|
||||
if (status.type === "status") {
|
||||
throw new Error(
|
||||
|
||||
@@ -132,7 +132,7 @@ Deno.test("CLI file operations: push / cat / ls / info / rm / resolve / cat-rev
|
||||
assertEquals(data.path, REMOTE_PATH, "info .path mismatch");
|
||||
assertEquals(data.filename, REMOTE_PATH.split("/").at(-1), "info .filename mismatch");
|
||||
assert(typeof data.size === "number" && data.size >= 0, `info .size invalid: ${data.size}`);
|
||||
assert(typeof data.chunks === "number" && (data.chunks) >= 1, `info .chunks invalid: ${data.chunks}`);
|
||||
assert(typeof data.chunks === "number" && data.chunks >= 1, `info .chunks invalid: ${data.chunks}`);
|
||||
assertEquals(data.conflicts, "N/A", "info .conflicts should be N/A");
|
||||
console.log("[PASS] info output format matched");
|
||||
});
|
||||
|
||||
@@ -2,9 +2,12 @@ import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import path from "node:path";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const resolve = (...args: string[]) => path.resolve(...args).replace(/\\/g, "/");
|
||||
const packageJson = JSON.parse(readFileSync("../../../package.json", "utf-8"));
|
||||
const manifestJson = JSON.parse(readFileSync("../../../manifest.json", "utf-8"));
|
||||
const repoRoot = path.resolve(__dirname, "../../..");
|
||||
const packageJson = JSON.parse(readFileSync(path.resolve(repoRoot, "package.json"), "utf-8"));
|
||||
const manifestJson = JSON.parse(readFileSync(path.resolve(repoRoot, "manifest.json"), "utf-8"));
|
||||
// https://vite.dev/config/
|
||||
const defaultExternal = [
|
||||
"obsidian",
|
||||
|
||||
@@ -6,7 +6,7 @@ Note: (I vrtmrz have not tested this so much yet).
|
||||
|
||||
- 🌐 Runs entirely in the browser
|
||||
- 📁 Uses FileSystem API to access your local vault
|
||||
- 🔄 Syncs with CouchDB, Object Storage server (compatible with Self-hosted LiveSync plugin)
|
||||
- 🔄 Syncs with CouchDB, Object Storage server (compatible with Self-hosted LiveSync plug-in)
|
||||
- 🚫 No server-side code required!!
|
||||
- 💾 Settings stored in `.livesync/settings.json` within your vault
|
||||
- 👁️ Real-time file watching (Chrome 124+ with FileSystemObserver)
|
||||
@@ -127,7 +127,7 @@ webapp/
|
||||
1. **Adapters**: Implement `IFileSystemAdapter` interface using FileSystem API
|
||||
2. **Managers**: Handle storage events and file watching
|
||||
3. **Service Modules**: Integrate with LiveSyncBaseCore
|
||||
4. **Main**: Application initialization and lifecycle management
|
||||
4. **Main**: Application initialisation and lifecycle management
|
||||
|
||||
### Service Hub
|
||||
|
||||
@@ -154,11 +154,11 @@ Uses `BrowserServiceHub` which provides:
|
||||
- Settings stored in `.livesync/settings.json` in vault
|
||||
- Real-time file watching only with FileSystemObserver (Chrome 124+)
|
||||
|
||||
## Differences from Obsidian Plugin
|
||||
## Differences from Obsidian Plug-in
|
||||
|
||||
- No Obsidian-specific modules (UI, settings dialog, etc.)
|
||||
- No Obsidian-specific modules (UI, settings dialogue, etc.)
|
||||
- Simplified configuration
|
||||
- No plugin/theme sync features
|
||||
- No plug-in/theme sync features
|
||||
- No internal file handling (`.obsidian` folder)
|
||||
|
||||
## Development Notes
|
||||
|
||||
@@ -23,7 +23,6 @@ import { compatGlobal, _activeDocument } from "@lib/common/coreEnvFunctions.ts";
|
||||
|
||||
const SETTINGS_DIR = ".livesync";
|
||||
const SETTINGS_FILE = "settings.json";
|
||||
const DB_NAME = "livesync-webapp";
|
||||
|
||||
/**
|
||||
* Default settings for the webapp
|
||||
@@ -65,7 +64,6 @@ class LiveSyncWebApp {
|
||||
console.log(`Vault directory: ${this.rootHandle.name}`);
|
||||
|
||||
// Create service context and hub
|
||||
const context = new ServiceContext();
|
||||
this.serviceHub = new BrowserServiceHub<ServiceContext>();
|
||||
|
||||
// Setup API service
|
||||
|
||||
@@ -161,7 +161,6 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
||||
|
||||
this.observer = new FileSystemObserver(async (records: any[]) => {
|
||||
for (const record of records) {
|
||||
const handle = record.root;
|
||||
const changedHandle = record.changedHandle;
|
||||
const relativePathComponents = record.relativePathComponents;
|
||||
const type = record.type; // "appeared", "disappeared", "modified", "moved", "unknown", "errored"
|
||||
|
||||
@@ -27,6 +27,6 @@
|
||||
"@lib/*": ["../../lib/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["*.ts", "**/*.ts", "**/*.tsx"],
|
||||
"include": ["*.ts", "**/*.ts", "**/*.tsx", "**/*.svelte"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export class VaultHistoryStore {
|
||||
|
||||
async getVaultHistory(): Promise<VaultHistoryItem[]> {
|
||||
return this.withStore("readonly", async (store) => {
|
||||
const keys = (await this.requestAsPromise(store.getAllKeys()));
|
||||
const keys = await this.requestAsPromise(store.getAllKeys());
|
||||
const values = (await this.requestAsPromise(store.getAll())) as unknown[];
|
||||
const items: VaultHistoryItem[] = [];
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
|
||||
@@ -3,10 +3,12 @@ import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import istanbul from "vite-plugin-istanbul";
|
||||
import path from "node:path";
|
||||
import { readFileSync } from "node:fs";
|
||||
const packageJson = JSON.parse(readFileSync("../../../package.json", "utf-8"));
|
||||
const manifestJson = JSON.parse(readFileSync("../../../manifest.json", "utf-8"));
|
||||
const enableCoverage = process.env.PW_COVERAGE === "1";
|
||||
import { fileURLToPath } from "node:url";
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(__dirname, "../../..");
|
||||
const packageJson = JSON.parse(readFileSync(path.resolve(repoRoot, "package.json"), "utf-8"));
|
||||
const manifestJson = JSON.parse(readFileSync(path.resolve(repoRoot, "manifest.json"), "utf-8"));
|
||||
const enableCoverage = process.env.PW_COVERAGE === "1";
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
|
||||
@@ -11,7 +11,6 @@ import { eventHub } from "@lib/hub/hub";
|
||||
|
||||
import type { Confirm } from "@lib/interfaces/Confirm";
|
||||
import { LOG_LEVEL_NOTICE, Logger } from "@lib/common/logger";
|
||||
import { storeP2PStatusLine } from "./CommandsShim";
|
||||
import {
|
||||
EVENT_P2P_PEER_SHOW_EXTRA_MENU,
|
||||
type PeerStatus,
|
||||
@@ -87,7 +86,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
|
||||
this._liveSyncReplicator = replicator;
|
||||
this.p2pLogCollector = p2pLogCollector;
|
||||
p2pLogCollector.p2pReplicationLine.onChanged((line) => {
|
||||
storeP2PStatusLine.set(line.value);
|
||||
p2pStatusLine.value = line.value;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { deleteDB, type IDBPDatabase, openDB } from "idb";
|
||||
import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase.ts";
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||
import { Logger } from "octagonal-wheels/common/logger";
|
||||
const databaseCache: { [key: string]: IDBPDatabase<any> } = {};
|
||||
const databaseCache: { [key: string]: IDBPDatabase<unknown> } = {};
|
||||
export { OpenKeyValueDatabase } from "./KeyValueDBv2.ts";
|
||||
|
||||
export const _OpenKeyValueDatabase = async (dbKey: string): Promise<KeyValueDatabase> => {
|
||||
@@ -11,7 +11,7 @@ export const _OpenKeyValueDatabase = async (dbKey: string): Promise<KeyValueData
|
||||
delete databaseCache[dbKey];
|
||||
}
|
||||
const storeKey = dbKey;
|
||||
let db: IDBPDatabase<any> | null = null;
|
||||
let db: IDBPDatabase<unknown> | null = null;
|
||||
const _openDB = () => {
|
||||
return serialized("keyvaluedb-" + dbKey, async () => {
|
||||
const dbInstance = await openDB(dbKey, 1, {
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function OpenKeyValueDatabase(dbKey: string): Promise<KeyValueDatab
|
||||
}
|
||||
|
||||
export class IDBKeyValueDatabase implements KeyValueDatabase {
|
||||
protected _dbPromise: Promise<IDBPDatabase<any>> | null = null;
|
||||
protected _dbPromise: Promise<IDBPDatabase<unknown>> | null = null;
|
||||
protected dbKey: string;
|
||||
protected storeKey: string;
|
||||
protected _isDestroyed: boolean = false;
|
||||
@@ -104,7 +104,7 @@ export class IDBKeyValueDatabase implements KeyValueDatabase {
|
||||
this.destroyedPromise = Promise.resolve();
|
||||
}
|
||||
}
|
||||
get DB(): Promise<IDBPDatabase<any>> {
|
||||
get DB(): Promise<IDBPDatabase<unknown>> {
|
||||
if (this._isDestroyed) {
|
||||
return Promise.reject(new Error("Database is destroyed"));
|
||||
}
|
||||
@@ -117,7 +117,7 @@ export class IDBKeyValueDatabase implements KeyValueDatabase {
|
||||
}
|
||||
async get<U>(key: IDBValidKey): Promise<U> {
|
||||
const db = await this.DB;
|
||||
return await db.get(this.storeKey, key);
|
||||
return (await db.get(this.storeKey, key)) as U;
|
||||
}
|
||||
async set<U>(key: IDBValidKey, value: U): Promise<IDBValidKey> {
|
||||
const db = await this.DB;
|
||||
|
||||
@@ -4,10 +4,10 @@ import { eventHub, EVENT_PLUGIN_UNLOADED } from "./events";
|
||||
import type { NecessaryServices } from "@lib/interfaces/ServiceModule";
|
||||
type PeriodicProcessorHost = NecessaryServices<"API" | "control", never>;
|
||||
export class PeriodicProcessor {
|
||||
_process: () => Promise<any>;
|
||||
_process: () => Promise<unknown>;
|
||||
_timer?: number = undefined;
|
||||
_core: PeriodicProcessorHost;
|
||||
constructor(core: PeriodicProcessorHost, process: () => Promise<any>) {
|
||||
constructor(core: PeriodicProcessorHost, process: () => Promise<unknown>) {
|
||||
// this._plugin = plugin;
|
||||
this._core = core;
|
||||
this._process = process;
|
||||
|
||||
@@ -9,16 +9,16 @@ import { isCloudantURI } from "@lib/pouchdb/utils_couchdb";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions";
|
||||
import { manifestVersion, packageVersion } from "@lib/common/coreEnvVars";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
function redactObject(obj: Record<string, any>, dotted: string, redactedValue = "REDACTED") {
|
||||
function redactObject(obj: Record<string, unknown>, dotted: string, redactedValue = "REDACTED") {
|
||||
const keys = dotted.split(".");
|
||||
let current = obj;
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!(key in current)) {
|
||||
current[key] = {} as Record<string, any>;
|
||||
current[key] = {};
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
current = current[key];
|
||||
current = current[key] as Record<string, unknown>;
|
||||
}
|
||||
const lastKey = keys[keys.length - 1];
|
||||
if (lastKey in current) {
|
||||
@@ -27,7 +27,7 @@ function redactObject(obj: Record<string, any>, dotted: string, redactedValue =
|
||||
return obj;
|
||||
}
|
||||
export async function generateReport(settings: ObsidianLiveSyncSettings, core: LiveSyncBaseCore) {
|
||||
let responseConfig: Record<string, any> = {};
|
||||
let responseConfig: Record<string, unknown> = {};
|
||||
const REDACTED = "𝑅𝐸𝐷𝐴𝐶𝑇𝐸𝐷";
|
||||
if (settings.remoteType == REMOTE_COUCHDB) {
|
||||
try {
|
||||
@@ -42,7 +42,7 @@ export async function generateReport(settings: ObsidianLiveSyncSettings, core: L
|
||||
undefined,
|
||||
customHeaders
|
||||
);
|
||||
responseConfig = r.json as Record<string, any>;
|
||||
responseConfig = r.json as Record<string, unknown>;
|
||||
redactObject(responseConfig, "couch_httpd_auth.secret");
|
||||
redactObject(responseConfig, "couch_httpd_auth.authentication_db");
|
||||
redactObject(responseConfig, "couch_httpd_auth.authentication_redirect");
|
||||
|
||||
+13
-13
@@ -72,7 +72,7 @@ import {
|
||||
} from "@lib/common/typeUtils.ts";
|
||||
export { isInternalFile, getPathFromUXFileInfo, getStoragePathFromUXFileInfo, getDatabasePathFromUXFileInfo };
|
||||
|
||||
const memos: { [key: string]: any } = {};
|
||||
const memos: { [key: string]: unknown } = {};
|
||||
export function memoObject<T>(key: string, obj: T): T {
|
||||
memos[key] = obj;
|
||||
return memos[key] as T;
|
||||
@@ -87,7 +87,7 @@ export async function memoIfNotExist<T>(key: string, func: () => T | Promise<T>)
|
||||
}
|
||||
export function retrieveMemoObject<T>(key: string): T | false {
|
||||
if (key in memos) {
|
||||
return memos[key];
|
||||
return memos[key] as T;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -128,7 +128,7 @@ export const _requestToCouchDBFetch = async (
|
||||
username: string,
|
||||
password: string,
|
||||
path?: string,
|
||||
body?: any,
|
||||
body?: unknown,
|
||||
method?: string
|
||||
) => {
|
||||
const utf8str = String.fromCharCode.apply(null, [...writeString(`${username}:${password}`)]);
|
||||
@@ -154,7 +154,7 @@ export const _requestToCouchDB = async (
|
||||
credentials: CouchDBCredentials,
|
||||
origin: string,
|
||||
path?: string,
|
||||
body?: any,
|
||||
body?: unknown,
|
||||
method?: string,
|
||||
customHeaders?: Record<string, string>
|
||||
) => {
|
||||
@@ -263,27 +263,27 @@ export function compareFileFreshness(
|
||||
const _cached = new Map<
|
||||
string,
|
||||
{
|
||||
value: any;
|
||||
context: Map<string, any>;
|
||||
value: unknown;
|
||||
context: Map<string, unknown>;
|
||||
}
|
||||
>();
|
||||
|
||||
export type MemoOption = {
|
||||
key: string;
|
||||
forceUpdate?: boolean;
|
||||
validator?: (context: Map<string, any>) => boolean;
|
||||
validator?: (context: Map<string, unknown>) => boolean;
|
||||
};
|
||||
|
||||
export function useMemo<T>(
|
||||
{ key, forceUpdate, validator }: MemoOption,
|
||||
updateFunc: (context: Map<string, any>, prev: T) => T
|
||||
updateFunc: (context: Map<string, unknown>, prev: T) => T
|
||||
): T {
|
||||
const cached = _cached.get(key);
|
||||
const context = cached?.context || new Map<string, any>();
|
||||
const context = cached?.context || new Map<string, unknown>();
|
||||
if (cached && !forceUpdate && (!validator || (validator && !validator(context)))) {
|
||||
return cached.value;
|
||||
return cached.value as T;
|
||||
}
|
||||
const value = updateFunc(context, cached?.value);
|
||||
const value = updateFunc(context, cached?.value as T);
|
||||
if (value !== cached?.value) {
|
||||
_cached.set(key, { value, context });
|
||||
}
|
||||
@@ -294,7 +294,7 @@ export function useMemo<T>(
|
||||
const _staticObj = new Map<
|
||||
string,
|
||||
{
|
||||
value: any;
|
||||
value: unknown;
|
||||
}
|
||||
>();
|
||||
|
||||
@@ -390,7 +390,7 @@ export async function autosaveCache<K, V>(db: KeyValueDatabase, mapKey: string):
|
||||
};
|
||||
}
|
||||
|
||||
export function onlyInNTimes(n: number, proc: (progress: number) => any) {
|
||||
export function onlyInNTimes(n: number, proc: (progress: number) => unknown) {
|
||||
let counter = 0;
|
||||
return function () {
|
||||
if (counter++ % n == 0) {
|
||||
|
||||
@@ -31,6 +31,7 @@ export {
|
||||
TextComponent,
|
||||
ToggleComponent,
|
||||
DropdownComponent,
|
||||
Component,
|
||||
} from "obsidian";
|
||||
export type {
|
||||
DataWriteOptions,
|
||||
@@ -41,6 +42,8 @@ export type {
|
||||
ListedFiles,
|
||||
ValueComponent,
|
||||
Stat,
|
||||
Command,
|
||||
ViewCreator,
|
||||
} from "obsidian";
|
||||
import { normalizePath as normalizePath_ } from "obsidian";
|
||||
const normalizePath = normalizePath_ as <T extends string | FilePath>(from: T) => T;
|
||||
|
||||
@@ -1101,7 +1101,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
this._log(`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[];
|
||||
const manifests = Object.values(this.app.plugins.manifests) as unknown as PluginManifest[];
|
||||
//@ts-ignore
|
||||
const enabledPlugins = this.app.plugins.enabledPlugins as Set<string>;
|
||||
const pluginManifest = manifests.find(
|
||||
|
||||
@@ -1225,7 +1225,7 @@ Offline Changed files: ${files.length}`;
|
||||
this.queuedNotificationFiles.clear();
|
||||
try {
|
||||
//@ts-ignore
|
||||
const manifests = Object.values(this.app.plugins.manifests) as any as PluginManifest[];
|
||||
const manifests = Object.values(this.app.plugins.manifests) as unknown as PluginManifest[];
|
||||
//@ts-ignore
|
||||
const enabledPlugins = this.app.plugins.enabledPlugins as Set<string>;
|
||||
const enabledPluginManifests = manifests.filter((e) => enabledPlugins.has(e.id));
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import type ObsidianLiveSyncPlugin from "@/main.ts";
|
||||
import { MARK_DONE } from "@/modules/features/ModuleLog.ts";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
import { __$checkInstanceBinding } from "@lib/dev/checks.ts";
|
||||
// import { __$checkInstanceBinding } from "@lib/dev/checks.ts";
|
||||
import { createInstanceLogFunction } from "@lib/services/lib/logUtils.ts";
|
||||
|
||||
let noticeIndex = 0;
|
||||
@@ -50,7 +50,7 @@ export abstract class LiveSyncCommands {
|
||||
this.core = core;
|
||||
this.onBindFunction(this.core, this.core.services);
|
||||
this._log = createInstanceLogFunction(this.constructor.name, this.services.API);
|
||||
__$checkInstanceBinding(this);
|
||||
// __$checkInstanceBinding(this);
|
||||
}
|
||||
abstract onunload(): void;
|
||||
abstract onload(): void | Promise<void>;
|
||||
@@ -67,24 +67,24 @@ export abstract class LiveSyncCommands {
|
||||
|
||||
_log: ReturnType<typeof createInstanceLogFunction>;
|
||||
|
||||
_verbose = (msg: any, key?: string) => {
|
||||
_verbose = (msg: unknown, key?: string) => {
|
||||
this._log(msg, LOG_LEVEL_VERBOSE, key);
|
||||
};
|
||||
|
||||
_info = (msg: any, key?: string) => {
|
||||
_info = (msg: unknown, key?: string) => {
|
||||
this._log(msg, LOG_LEVEL_INFO, key);
|
||||
};
|
||||
|
||||
_notice = (msg: any, key?: string) => {
|
||||
_notice = (msg: unknown, key?: string) => {
|
||||
this._log(msg, LOG_LEVEL_NOTICE, key);
|
||||
};
|
||||
_progress = (prefix: string = "", level: LOG_LEVEL = LOG_LEVEL_NOTICE) => {
|
||||
const key = `keepalive-progress-${noticeIndex++}`;
|
||||
return {
|
||||
log: (msg: any) => {
|
||||
log: (msg: string) => {
|
||||
this._log(prefix + msg, level, key);
|
||||
},
|
||||
once: (msg: any) => {
|
||||
once: (msg: string) => {
|
||||
this._log(prefix + msg, level);
|
||||
},
|
||||
done: (msg: string = "Done") => {
|
||||
@@ -93,7 +93,7 @@ export abstract class LiveSyncCommands {
|
||||
};
|
||||
};
|
||||
|
||||
_debug = (msg: any, key?: string) => {
|
||||
_debug = (msg: unknown, key?: string) => {
|
||||
this._log(msg, LOG_LEVEL_VERBOSE, key);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import { arrayToChunkedArray } from "octagonal-wheels/collection";
|
||||
import { EVENT_ANALYSE_DB_USAGE, EVENT_REQUEST_PERFORM_GC_V3, eventHub } from "@/common/events";
|
||||
import type { LiveSyncCouchDBReplicator } from "@lib/replication/couchdb/LiveSyncReplicator";
|
||||
import { delay } from "@lib/common/utils";
|
||||
import { isNotFoundError } from "@/lib/src/common/utils.doc";
|
||||
// import { _requestToCouchDB } from "@/common/utils";
|
||||
const DB_KEY_SEQ = "gc-seq";
|
||||
const DB_KEY_CHUNK_SET = "chunk-set";
|
||||
@@ -394,7 +395,7 @@ Note: **Make sure to synchronise all devices before deletion.**
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
if ((ex as any)?.status == 404) {
|
||||
if (isNotFoundError(ex)) {
|
||||
this._log(`No revisions found for ${doc._id}`, LOG_LEVEL_VERBOSE);
|
||||
} else {
|
||||
this._log(`Error finding revisions for ${doc._id}`);
|
||||
@@ -474,14 +475,14 @@ Are you ready to delete unused chunks?`;
|
||||
include_docs: true,
|
||||
});
|
||||
for (const chunk of deleteChunks.rows) {
|
||||
if ((chunk as any)?.value?.deleted) {
|
||||
if ((chunk as { value?: { deleted?: boolean } })?.value?.deleted) {
|
||||
chunkSet.delete(chunk.key as DocumentID);
|
||||
}
|
||||
}
|
||||
const deleteDocs = deleteChunks.rows
|
||||
.filter((e) => "doc" in e)
|
||||
.map((e) => ({
|
||||
...(e as any).doc!,
|
||||
...(e as { doc?: EntryLeaf }).doc!,
|
||||
_deleted: true,
|
||||
}));
|
||||
|
||||
@@ -490,7 +491,7 @@ Are you ready to delete unused chunks?`;
|
||||
let successCount = 0;
|
||||
let errored = 0;
|
||||
for (const batch of deleteChunkBatch) {
|
||||
const results = await this.database.bulkDocs(batch as EntryLeaf[]);
|
||||
const results = await this.database.bulkDocs(batch);
|
||||
for (const result of results) {
|
||||
if ("ok" in result) {
|
||||
chunkSet.delete(result.id as DocumentID);
|
||||
@@ -698,7 +699,7 @@ Success: ${successCount}, Errored: ${errored}`;
|
||||
sharedChunkCount: 0,
|
||||
uniqueChunkSize: orphanChunkSize,
|
||||
sharedChunkSize: 0,
|
||||
} as any);
|
||||
} as const);
|
||||
|
||||
const csvSrc = result.map((e) => {
|
||||
return [
|
||||
@@ -737,7 +738,7 @@ Success: ${successCount}, Errored: ${errored}`;
|
||||
});
|
||||
// Probably no need to wait, but just in case.
|
||||
let timeout = 2 * 60 * 1000; // 2 minutes
|
||||
do {
|
||||
for (;;) {
|
||||
const status = await remote.db.info();
|
||||
if ("compact_running" in status && status?.compact_running) {
|
||||
this._notice("Compaction in progress on remote database...", "gc-compact");
|
||||
@@ -750,7 +751,7 @@ Success: ${successCount}, Errored: ${errored}`;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
if (compactResult && "ok" in compactResult) {
|
||||
this._notice("Compaction on remote database completed successfully.", "gc-compact");
|
||||
} else {
|
||||
|
||||
+1
-1
Submodule src/lib updated: c926417f82...9aeab513b0
+2
-2
@@ -4,7 +4,7 @@ setGetLanguage(getLanguage);
|
||||
import { LiveSyncCommands } from "./features/LiveSyncCommands.ts";
|
||||
import { HiddenFileSync } from "./features/HiddenFileSync/CmdHiddenFileSync.ts";
|
||||
import { ConfigSync } from "./features/ConfigSync/CmdConfigSync.ts";
|
||||
import { ModuleDev } from "./modules/extras/ModuleDev.ts";
|
||||
// import { ModuleDev } from "./modules/extras/ModuleDev.ts";
|
||||
|
||||
import { ModuleInteractiveConflictResolver } from "./modules/features/ModuleInteractiveConflictResolver.ts";
|
||||
import { ModuleLog } from "./modules/features/ModuleLog.ts";
|
||||
@@ -153,7 +153,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
new ModuleObsidianDocumentHistory(this, core),
|
||||
new ModuleInteractiveConflictResolver(this, core),
|
||||
new ModuleObsidianGlobalHistory(this, core),
|
||||
new ModuleDev(this, core),
|
||||
// new ModuleDev(this, core),
|
||||
new SetupManager(core), // this should be moved to core?
|
||||
new ModuleMigration(core),
|
||||
];
|
||||
|
||||
@@ -20,7 +20,7 @@ import { InternalFileToUXFileInfoStub, TFileToUXFileInfoStub } from "@/modules/c
|
||||
* Obsidian-specific type guard adapter
|
||||
*/
|
||||
class ObsidianTypeGuardAdapter implements IStorageEventTypeGuardAdapter<TFile, TFolder> {
|
||||
isFile(file: any): file is TFile {
|
||||
isFile(file: unknown): file is TFile {
|
||||
if (file instanceof TFile) {
|
||||
return true;
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class ObsidianTypeGuardAdapter implements IStorageEventTypeGuardAdapter<TFile, T
|
||||
return false;
|
||||
}
|
||||
|
||||
isFolder(item: any): item is TFolder {
|
||||
isFolder(item: unknown): item is TFolder {
|
||||
if (item instanceof TFolder) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -61,21 +61,21 @@ export abstract class AbstractModule<
|
||||
return this.testDone(false);
|
||||
}
|
||||
|
||||
async _test(key: string, process: () => Promise<any>) {
|
||||
this._log(`Testing ${key}`, LOG_LEVEL_VERBOSE);
|
||||
try {
|
||||
const ret = await process();
|
||||
if (ret !== true) {
|
||||
this.addTestResult(key, false, ret.toString());
|
||||
return this.testFail(`${key} failed: ${ret}`);
|
||||
}
|
||||
this.addTestResult(key, true, "");
|
||||
} catch (ex: any) {
|
||||
this.addTestResult(key, false, "Failed by Exception", ex.toString());
|
||||
return this.testFail(`${key} failed: ${ex}`);
|
||||
}
|
||||
return this.testDone();
|
||||
}
|
||||
// async _test(key: string, process: () => Promise<any>) {
|
||||
// this._log(`Testing ${key}`, LOG_LEVEL_VERBOSE);
|
||||
// try {
|
||||
// const ret = await process();
|
||||
// if (ret !== true) {
|
||||
// this.addTestResult(key, false, ret.toString());
|
||||
// return this.testFail(`${key} failed: ${ret}`);
|
||||
// }
|
||||
// this.addTestResult(key, true, "");
|
||||
// } catch (ex: any) {
|
||||
// this.addTestResult(key, false, "Failed by Exception", ex.toString());
|
||||
// return this.testFail(`${key} failed: ${ex}`);
|
||||
// }
|
||||
// return this.testDone();
|
||||
// }
|
||||
|
||||
isMainReady() {
|
||||
return this.services.appLifecycle.isReady();
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { type Prettify } from "@lib/common/types";
|
||||
import type { LiveSyncCore } from "@/main";
|
||||
import type ObsidianLiveSyncPlugin from "@/main";
|
||||
import { AbstractModule } from "./AbstractModule.ts";
|
||||
import type { ChainableExecuteFunction, OverridableFunctionsKeys } from "./ModuleTypes";
|
||||
|
||||
export type IObsidianModuleBase = OverridableFunctionsKeys<ObsidianLiveSyncPlugin>;
|
||||
export type IObsidianModule = Prettify<Partial<IObsidianModuleBase>>;
|
||||
export type ModuleKeys = keyof IObsidianModule;
|
||||
export type ChainableModuleProps = ChainableExecuteFunction<ObsidianLiveSyncPlugin>;
|
||||
|
||||
export abstract class AbstractObsidianModule extends AbstractModule {
|
||||
get app() {
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import type { Prettify } from "@lib/common/types";
|
||||
import type { LiveSyncCore } from "@/main";
|
||||
|
||||
export type OverridableFunctionsKeys<T> = {
|
||||
[K in keyof T as K extends `$${string}` ? K : never]: T[K];
|
||||
};
|
||||
|
||||
export type ChainableExecuteFunction<T> = {
|
||||
[K in keyof T as K extends `$${string}`
|
||||
? T[K] extends (...args: any) => ChainableFunctionResult
|
||||
? K
|
||||
: never
|
||||
: never]: T[K];
|
||||
};
|
||||
|
||||
export type ICoreModuleBase = OverridableFunctionsKeys<LiveSyncCore>;
|
||||
export type ICoreModule = Prettify<Partial<ICoreModuleBase>>;
|
||||
export type CoreModuleKeys = keyof ICoreModule;
|
||||
|
||||
export type ChainableFunctionResult =
|
||||
| Promise<boolean | undefined | string>
|
||||
| Promise<boolean | undefined>
|
||||
| Promise<boolean>
|
||||
| Promise<void>;
|
||||
export type ChainableFunctionResultOrAll = Promise<boolean | undefined | string | void>;
|
||||
|
||||
type AllExecuteFunction<T> = {
|
||||
[K in keyof T as K extends `$all${string}`
|
||||
? T[K] extends (...args: any[]) => ChainableFunctionResultOrAll
|
||||
? K
|
||||
: never
|
||||
: never]: T[K];
|
||||
};
|
||||
type EveryExecuteFunction<T> = {
|
||||
[K in keyof T as K extends `$every${string}`
|
||||
? T[K] extends (...args: any[]) => ChainableFunctionResult
|
||||
? K
|
||||
: never
|
||||
: never]: T[K];
|
||||
};
|
||||
type AnyExecuteFunction<T> = {
|
||||
[K in keyof T as K extends `$any${string}`
|
||||
? T[K] extends (...args: any[]) => ChainableFunctionResult
|
||||
? K
|
||||
: never
|
||||
: never]: T[K];
|
||||
};
|
||||
type InjectableFunction<T> = {
|
||||
[K in keyof T as K extends `$$${string}` ? (T[K] extends (...args: any[]) => any ? K : never) : never]: T[K];
|
||||
};
|
||||
export type AllExecuteProps = AllExecuteFunction<LiveSyncCore>;
|
||||
export type EveryExecuteProps = EveryExecuteFunction<LiveSyncCore>;
|
||||
export type AnyExecuteProps = AnyExecuteFunction<LiveSyncCore>;
|
||||
|
||||
export type AllInjectableProps = InjectableFunction<LiveSyncCore>;
|
||||
@@ -21,7 +21,7 @@ import { MARK_LOG_NETWORK_ERROR } from "@lib/services/lib/logUtils";
|
||||
|
||||
function isOnlineAndCanReplicate(
|
||||
errorManager: UnresolvedErrorManager,
|
||||
host: NecessaryServices<"API", any>,
|
||||
host: NecessaryServices<"API", never>,
|
||||
showMessage: boolean
|
||||
): Promise<boolean> {
|
||||
const errorMessage = "Network is offline";
|
||||
@@ -34,7 +34,7 @@ function isOnlineAndCanReplicate(
|
||||
}
|
||||
async function canReplicateWithPBKDF2(
|
||||
errorManager: UnresolvedErrorManager,
|
||||
host: NecessaryServices<"replicator" | "setting", any>,
|
||||
host: NecessaryServices<"replicator" | "setting", never>,
|
||||
showMessage: boolean
|
||||
): Promise<boolean> {
|
||||
const currentSettings = host.services.setting.currentSettings();
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Semaphore } from "octagonal-wheels/concurrency/semaphore_v2";
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||
import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import { isNotFoundError } from "@lib/common/utils.doc";
|
||||
|
||||
const KV_KEY_REPLICATION_RESULT_PROCESSOR_SNAPSHOT = "replicationResultProcessorSnapshot";
|
||||
type ReplicateResultProcessorState = {
|
||||
@@ -39,7 +40,7 @@ export class ReplicateResultProcessor {
|
||||
private log(message: string, level: LOG_LEVEL = LOG_LEVEL_INFO) {
|
||||
Logger(`[ReplicateResultProcessor] ${message}`, level);
|
||||
}
|
||||
private logError(e: any) {
|
||||
private logError(e: unknown) {
|
||||
Logger(e, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
private replicator: ModuleReplicator;
|
||||
@@ -466,8 +467,8 @@ export class ReplicateResultProcessor {
|
||||
return false; // This means that the document already processed (While no conflict existed).
|
||||
}
|
||||
return true; // This mostly should not happen, but we have to process it just in case.
|
||||
} catch (e: any) {
|
||||
if ("status" in e && e.status == 404) {
|
||||
} catch (e) {
|
||||
if (isNotFoundError(e)) {
|
||||
// getRaw failed due to not existing, it may not be happened normally especially on replication.
|
||||
// If the process caused by some other reason, we **probably** have to process it.
|
||||
// Note that this is not a common case.
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
import { REMOTE_P2P } from "@lib/common/models/setting.const.ts";
|
||||
|
||||
function valueToString(value: any) {
|
||||
function valueToString(value: string | number | boolean | object | undefined): string {
|
||||
if (typeof value === "boolean") {
|
||||
return value ? "true" : "false";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ButtonComponent } from "@/deps.ts";
|
||||
import { App, FuzzySuggestModal, MarkdownRenderer, Modal, Plugin, Setting } from "@/deps.ts";
|
||||
import { App, FuzzySuggestModal, MarkdownRenderer, Modal, Plugin, Setting, Component } from "@/deps.ts";
|
||||
import { EVENT_PLUGIN_UNLOADED, eventHub } from "@/common/events.ts";
|
||||
import { compatGlobal, type CompatIntervalHandle } from "@lib/common/coreEnvFunctions.ts";
|
||||
|
||||
@@ -148,6 +148,7 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
|
||||
wideButton: boolean;
|
||||
|
||||
onSubmit: (result: string | false) => void;
|
||||
component: Component = new Component();
|
||||
|
||||
constructor(
|
||||
plugin: Plugin,
|
||||
@@ -189,6 +190,7 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
|
||||
}
|
||||
|
||||
override onOpen() {
|
||||
this.component.load();
|
||||
const { contentEl } = this;
|
||||
this.titleEl.setText(this.title);
|
||||
const div = contentEl.createDiv();
|
||||
@@ -196,7 +198,7 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
|
||||
userSelect: "text",
|
||||
webkitUserSelect: "text",
|
||||
});
|
||||
void MarkdownRenderer.render(this.plugin.app, this.contentMd, div, "/", this.plugin);
|
||||
void MarkdownRenderer.render(this.plugin.app, this.contentMd, div, "/", this.component);
|
||||
const buttonSetting = new Setting(contentEl);
|
||||
const labelWrapper = contentEl.createDiv();
|
||||
labelWrapper.addClass("sls-dialogue-note-wrapper");
|
||||
@@ -254,6 +256,7 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
|
||||
|
||||
override onClose() {
|
||||
super.onClose();
|
||||
this.component.unload();
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
if (this.timer) {
|
||||
|
||||
@@ -80,7 +80,7 @@ export class ObsHttpHandler extends FetchHttpHandler {
|
||||
contentType = transformedHeaders["content-type"];
|
||||
}
|
||||
|
||||
let transformedBody: any = body;
|
||||
let transformedBody = body;
|
||||
if (ArrayBuffer.isView(body)) {
|
||||
transformedBody = new Uint8Array(body.buffer).buffer;
|
||||
}
|
||||
|
||||
@@ -36,10 +36,11 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
|
||||
this.services.appLifecycle.performRestart();
|
||||
}
|
||||
|
||||
initialCallback: any;
|
||||
initialCallback: (() => void) | undefined = undefined;
|
||||
|
||||
swapSaveCommand() {
|
||||
this._log("Modifying callback of the save command", LOG_LEVEL_VERBOSE);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Editor Tweaking
|
||||
const saveCommandDefinition = (this.app as any).commands?.commands?.["editor:save-file"];
|
||||
const save = saveCommandDefinition?.callback;
|
||||
if (typeof save === "function") {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { delay, fireAndForget } from "octagonal-wheels/promises";
|
||||
import { delay } from "octagonal-wheels/promises";
|
||||
import { __onMissingTranslation } from "@lib/common/i18n";
|
||||
import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts";
|
||||
import { LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||
import { eventHub } from "@/common/events";
|
||||
import { enableTestFunction } from "./devUtil/testUtils.ts";
|
||||
// import { enableTestFunction } from "./devUtil/testUtils.ts";
|
||||
import { TestPaneView, VIEW_TYPE_TEST } from "./devUtil/TestPaneView.ts";
|
||||
import { writable } from "svelte/store";
|
||||
import type { FilePathWithPrefix } from "@lib/common/types.ts";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
import type { WorkspaceLeaf } from "@/deps.ts";
|
||||
export class ModuleDev extends AbstractObsidianModule {
|
||||
_everyOnloadStart(): Promise<boolean> {
|
||||
__onMissingTranslation(() => {});
|
||||
@@ -37,57 +37,7 @@ export class ModuleDev extends AbstractObsidianModule {
|
||||
|
||||
private _everyOnloadAfterLoadSettings(): Promise<boolean> {
|
||||
if (!this.settings.enableDebugTools) return Promise.resolve(true);
|
||||
this.onMissingTranslation = this.onMissingTranslation.bind(this);
|
||||
__onMissingTranslation((key) => {
|
||||
void this.onMissingTranslation(key);
|
||||
});
|
||||
type STUB = {
|
||||
toc: Set<string>;
|
||||
stub: { [key: string]: { [key: string]: Map<string, Record<string, string>> } };
|
||||
};
|
||||
eventHub.onEvent("document-stub-created", (detail: STUB) => {
|
||||
fireAndForget(async () => {
|
||||
const stub = detail.stub;
|
||||
const toc = detail.toc;
|
||||
|
||||
const stubDocX = Object.entries(stub)
|
||||
.map(([key, value]) => {
|
||||
return [
|
||||
`## ${key}`,
|
||||
Object.entries(value)
|
||||
.map(([key2, value2]) => {
|
||||
return [
|
||||
`### ${key2}`,
|
||||
[...value2.entries()].map(([key3, value3]) => {
|
||||
// return `#### ${key3}` + "\n" + JSON.stringify(value3);
|
||||
const isObsolete = value3["is_obsolete"] ? " (obsolete)" : "";
|
||||
const desc = value3["desc"] ?? "";
|
||||
const key = value3["key"] ? "Setting key: " + value3["key"] + "\n" : "";
|
||||
return `#### ${key3}${isObsolete}\n${key}${desc}\n`;
|
||||
}),
|
||||
].flat();
|
||||
})
|
||||
.flat(),
|
||||
].flat();
|
||||
})
|
||||
.flat();
|
||||
const stubDocMD =
|
||||
`
|
||||
| Icon | Description |
|
||||
| :---: | ----------------------------------------------------------------- |
|
||||
` +
|
||||
[...toc.values()].map((e) => `${e}`).join("\n") +
|
||||
"\n\n" +
|
||||
stubDocX.join("\n");
|
||||
await this.core.storageAccess.writeHiddenFileAuto(
|
||||
this.app.vault.configDir + "/ls-debug/stub-doc.md",
|
||||
stubDocMD
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
enableTestFunction(this.plugin);
|
||||
this.registerView(VIEW_TYPE_TEST, (leaf) => new TestPaneView(leaf, this.plugin, this));
|
||||
this.registerView(VIEW_TYPE_TEST, (leaf: WorkspaceLeaf) => new TestPaneView(leaf, this.plugin, this));
|
||||
this.addCommand({
|
||||
id: "view-test",
|
||||
name: "Open Test dialogue",
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { fireAndForget } from "@lib/common/utils.ts";
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||
import type ObsidianLiveSyncPlugin from "@/main.ts";
|
||||
|
||||
let plugin: ObsidianLiveSyncPlugin;
|
||||
export function enableTestFunction(plugin_: ObsidianLiveSyncPlugin) {
|
||||
plugin = plugin_;
|
||||
}
|
||||
export function addDebugFileLog(message: any, stackLog = false) {
|
||||
fireAndForget(
|
||||
serialized("debug-log", async () => {
|
||||
const now = new Date();
|
||||
const filename = `debug-log`;
|
||||
const time = now.toISOString().split("T")[0];
|
||||
const outFile = `${filename}${time}.jsonl`;
|
||||
// const messageContent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2);
|
||||
const timestamp = now.toLocaleString();
|
||||
const timestampEpoch = now;
|
||||
let out = { timestamp: timestamp, epoch: timestampEpoch } as Record<string, any>;
|
||||
if (message instanceof Error) {
|
||||
// debugger;
|
||||
// console.dir(message.stack);
|
||||
out = { ...out, message };
|
||||
} else if (stackLog) {
|
||||
if (stackLog) {
|
||||
const stackE = new Error();
|
||||
const stack = stackE.stack;
|
||||
out = { ...out, stack };
|
||||
}
|
||||
}
|
||||
if (typeof message == "object") {
|
||||
out = { ...out, ...message };
|
||||
} else {
|
||||
out = {
|
||||
result: message,
|
||||
};
|
||||
}
|
||||
// const out = "--" + timestamp + "--\n" + messageContent + " " + (stack || "");
|
||||
// const out
|
||||
try {
|
||||
await plugin.core.storageAccess.appendHiddenFile(
|
||||
plugin.core.services.API.getSystemConfigDir() + "/ls-debug/" + outFile,
|
||||
JSON.stringify(out) + "\n"
|
||||
);
|
||||
} catch {
|
||||
//NO OP
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -99,9 +99,11 @@ export class DocumentHistoryModal extends Modal {
|
||||
if (!file && id) {
|
||||
this.file = this.services.path.id2path(id);
|
||||
}
|
||||
// eslint-disable-next-line obsidianmd/no-unsupported-api -- loadLocalStorage is supported in Obsidian 1.7.2+
|
||||
if (this.app.loadLocalStorage("ols-history-highlightdiff") == "1") {
|
||||
this.showDiff = true;
|
||||
}
|
||||
// eslint-disable-next-line obsidianmd/no-unsupported-api -- loadLocalStorage is supported in Obsidian 1.7.2+
|
||||
if (this.app.loadLocalStorage("ols-history-diffonly") == "1") {
|
||||
this.diffOnly = true;
|
||||
}
|
||||
@@ -139,7 +141,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
this.range.value = `${this.revs_info.length - 1 - rIndex}`;
|
||||
}
|
||||
}
|
||||
const index = this.revs_info.length - 1 - (this.range.value as any) / 1;
|
||||
const index = this.revs_info.length - 1 - (Number(this.range.value) || 0);
|
||||
const rev = this.revs_info[index];
|
||||
await this.showExactRev(rev.rev);
|
||||
}
|
||||
@@ -251,7 +253,7 @@ export class DocumentHistoryModal extends Modal {
|
||||
}
|
||||
let rendered = false;
|
||||
if (this.showDiff) {
|
||||
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
|
||||
const prevRevIdx = this.revs_info.length - 1 - ((Number(this.range.value) || 0) - 1);
|
||||
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
|
||||
const oldRev = this.revs_info[prevRevIdx].rev;
|
||||
const w2 = await db.getDBEntry(this.file, { rev: oldRev }, false, false, true);
|
||||
@@ -550,8 +552,9 @@ export class DocumentHistoryModal extends Modal {
|
||||
if (this.showDiff) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
checkbox.addEventListener("input", (evt: any) => {
|
||||
checkbox.addEventListener("input", (evt: Event) => {
|
||||
this.showDiff = checkbox.checked;
|
||||
// eslint-disable-next-line obsidianmd/no-unsupported-api -- saveLocalStorage is supported in Obsidian 1.7.2+
|
||||
this.app.saveLocalStorage("ols-history-highlightdiff", this.showDiff == true ? "1" : null);
|
||||
this.updateDiffNavVisibility();
|
||||
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
||||
@@ -565,8 +568,9 @@ export class DocumentHistoryModal extends Modal {
|
||||
if (this.diffOnly) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
checkbox.addEventListener("input", (evt: any) => {
|
||||
checkbox.addEventListener("input", (evt: Event) => {
|
||||
this.diffOnly = checkbox.checked;
|
||||
// eslint-disable-next-line obsidianmd/no-unsupported-api -- saveLocalStorage is supported in Obsidian 1.7.2+
|
||||
this.app.saveLocalStorage("ols-history-diffonly", this.diffOnly == true ? "1" : null);
|
||||
void scheduleOnceIfDuplicated("loadRevs", () => this.loadRevs());
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts";
|
||||
import { VIEW_TYPE_GLOBAL_HISTORY, GlobalHistoryView } from "./GlobalHistory/GlobalHistoryView.ts";
|
||||
import type { WorkspaceLeaf } from "@/deps.ts";
|
||||
|
||||
export class ModuleObsidianGlobalHistory extends AbstractObsidianModule {
|
||||
_everyOnloadStart(): Promise<boolean> {
|
||||
@@ -11,7 +12,7 @@ export class ModuleObsidianGlobalHistory extends AbstractObsidianModule {
|
||||
},
|
||||
});
|
||||
|
||||
this.registerView(VIEW_TYPE_GLOBAL_HISTORY, (leaf) => new GlobalHistoryView(leaf, this.plugin));
|
||||
this.registerView(VIEW_TYPE_GLOBAL_HISTORY, (leaf: WorkspaceLeaf) => new GlobalHistoryView(leaf, this.plugin));
|
||||
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ import { generateReport } from "@/common/reportTool.ts";
|
||||
|
||||
// DI the log again.
|
||||
const recentLogEntries = reactiveSource<LogEntry[]>([]);
|
||||
const globalLogFunction = (message: any, level?: number, key?: string) => {
|
||||
const globalLogFunction = (message: unknown, level?: number, key?: string) => {
|
||||
const messageX =
|
||||
message instanceof Error
|
||||
? new LiveSyncError("[Error Logged]: " + message.message, { cause: message })
|
||||
@@ -501,7 +501,7 @@ ${stringifyYaml(info)}
|
||||
})
|
||||
);
|
||||
}
|
||||
__addLog(message: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key = ""): void {
|
||||
__addLog(message: unknown, level: LOG_LEVEL = LOG_LEVEL_INFO, key = ""): void {
|
||||
if (level == LOG_LEVEL_DEBUG && !showDebugLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "@/deps.ts";
|
||||
import { unique } from "octagonal-wheels/collection";
|
||||
import { LEVEL_ADVANCED, LEVEL_POWER_USER, statusDisplay, type ConfigurationItem } from "@lib/common/types.ts";
|
||||
import { createStub, type ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import { type ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import {
|
||||
type AllSettingItemKey,
|
||||
getConfig,
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
type AllBooleanItemKey,
|
||||
} from "./settingConstants.ts";
|
||||
import { $msg } from "@lib/common/i18n.ts";
|
||||
import { findAttrFromParent, wrapMemo, type AutoWireOption, type OnUpdateResult } from "./SettingPane.ts";
|
||||
import { wrapMemo, type AutoWireOption, type OnUpdateResult } from "./SettingPane.ts";
|
||||
|
||||
export class LiveSyncSetting extends Setting {
|
||||
autoWiredComponent?: TextComponent | ToggleComponent | DropdownComponent | ButtonComponent | TextAreaComponent;
|
||||
@@ -35,37 +35,19 @@ export class LiveSyncSetting extends Setting {
|
||||
hasPassword: boolean = false;
|
||||
|
||||
invalidateValue?: () => void;
|
||||
setValue?: (value: any) => void;
|
||||
setValue?: (value: unknown) => void;
|
||||
constructor(containerEl: HTMLElement) {
|
||||
super(containerEl);
|
||||
LiveSyncSetting.env.settingComponents.push(this);
|
||||
}
|
||||
|
||||
_createDocStub(key: string, value: string | DocumentFragment) {
|
||||
DEV: {
|
||||
const paneName = findAttrFromParent(this.settingEl, "data-pane");
|
||||
const panelName = findAttrFromParent(this.settingEl, "data-panel");
|
||||
const itemName =
|
||||
typeof this.nameBuf == "string" ? this.nameBuf : (this.nameBuf.textContent?.toString() ?? "");
|
||||
const strValue = typeof value == "string" ? value : (value.textContent?.toString() ?? "");
|
||||
|
||||
createStub(itemName, key, strValue, panelName, paneName);
|
||||
}
|
||||
}
|
||||
|
||||
override setDesc(desc: string | DocumentFragment): this {
|
||||
this.descBuf = desc;
|
||||
DEV: {
|
||||
this._createDocStub("desc", desc);
|
||||
}
|
||||
super.setDesc(desc);
|
||||
return this;
|
||||
}
|
||||
override setName(name: string | DocumentFragment): this {
|
||||
this.nameBuf = name;
|
||||
DEV: {
|
||||
this._createDocStub("name", name);
|
||||
}
|
||||
super.setName(name);
|
||||
return this;
|
||||
}
|
||||
@@ -84,11 +66,6 @@ export class LiveSyncSetting extends Setting {
|
||||
if (conf.desc) {
|
||||
this.setDesc(conf.desc);
|
||||
}
|
||||
DEV: {
|
||||
this._createDocStub("key", key);
|
||||
if (conf.obsolete) this._createDocStub("is_obsolete", "true");
|
||||
if (conf.level) this._createDocStub("level", conf.level);
|
||||
}
|
||||
|
||||
this.holdValue = opt?.holdValue || this.holdValue;
|
||||
this.selfKey = key;
|
||||
@@ -102,7 +79,7 @@ export class LiveSyncSetting extends Setting {
|
||||
}
|
||||
return conf;
|
||||
}
|
||||
autoWireComponent(component: ValueComponent<any>, conf?: ConfigurationItem, opt?: AutoWireOption) {
|
||||
autoWireComponent<T>(component: ValueComponent<T>, conf?: ConfigurationItem, opt?: AutoWireOption) {
|
||||
this.placeHolderBuf = conf?.placeHolder || opt?.placeHolder || "";
|
||||
if (conf?.level == LEVEL_ADVANCED) {
|
||||
this.settingEl.toggleClass("sls-setting-advanced", true);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { App, PluginSettingTab } from "@/deps.ts";
|
||||
import { App, Component, PluginSettingTab } from "@/deps.ts";
|
||||
import {
|
||||
type ObsidianLiveSyncSettings,
|
||||
type RemoteDBSettings,
|
||||
@@ -40,12 +40,13 @@ import { JournalSyncMinio } from "@lib/replication/journal/objectstore/JournalSy
|
||||
import { paneChangeLog } from "./PaneChangeLog.ts";
|
||||
import {
|
||||
enableOnly,
|
||||
findAttrFromParent,
|
||||
getLevelStr,
|
||||
// findAttrFromParent,
|
||||
// getLevelStr,
|
||||
setLevelClass,
|
||||
setStyle,
|
||||
visibleOnly,
|
||||
type OnSavedHandler,
|
||||
type OnSavedHandlerFunc,
|
||||
type OnUpdateFunc,
|
||||
type OnUpdateResult,
|
||||
type PageFunctions,
|
||||
@@ -65,28 +66,14 @@ import { paneMaintenance } from "./PaneMaintenance.ts";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
|
||||
// For creating a document
|
||||
const toc = new Set<string>();
|
||||
const stubs = {} as {
|
||||
[key: string]: { [key: string]: Map<string, Record<string, string>> };
|
||||
};
|
||||
export function createStub(name: string, key: string, value: string, panel: string, pane: string) {
|
||||
DEV: {
|
||||
if (!(pane in stubs)) {
|
||||
stubs[pane] = {};
|
||||
}
|
||||
if (!(panel in stubs[pane])) {
|
||||
stubs[pane][panel] = new Map<string, Record<string, string>>();
|
||||
}
|
||||
const old = stubs[pane][panel].get(name) ?? {};
|
||||
stubs[pane][panel].set(name, { ...old, [key]: value });
|
||||
scheduleTask("update-stub", 100, () => {
|
||||
eventHub.emitEvent("document-stub-created", { toc: toc, stub: stubs });
|
||||
});
|
||||
}
|
||||
}
|
||||
// const toc = new Set<string>();
|
||||
|
||||
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
private _lifetimeComponent: Component = new Component();
|
||||
get lifetimeComponent(): Component {
|
||||
return this._lifetimeComponent;
|
||||
}
|
||||
get core() {
|
||||
return this.plugin.core;
|
||||
}
|
||||
@@ -181,7 +168,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
// if (runOnSaved) {
|
||||
const handlers = this.onSavedHandlers
|
||||
.filter((e) => appliedKeys.indexOf(e.key) !== -1)
|
||||
.map((e) => Promise.resolve(e.handler(this.editingSettings[e.key as AllSettingItemKey])));
|
||||
.map((e) => Promise.resolve(e.handler(this.editingSettings[e.key])));
|
||||
await Promise.all(handlers);
|
||||
// }
|
||||
keys.forEach((e) => this.refreshSetting(e));
|
||||
@@ -287,7 +274,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
// UI Element Wrapper -->
|
||||
settingComponents = [] as Setting[];
|
||||
controlledElementFunc = [] as UpdateFunction[];
|
||||
onSavedHandlers = [] as OnSavedHandler<any>[];
|
||||
onSavedHandlers = [] as OnSavedHandler<AllSettingItemKey>[];
|
||||
|
||||
inWizard: boolean = false;
|
||||
|
||||
@@ -370,8 +357,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
return Promise.resolve(elm);
|
||||
}
|
||||
|
||||
addOnSaved<T extends AllSettingItemKey>(key: T, func: (value: AllSettings[T]) => Promise<void> | void) {
|
||||
this.onSavedHandlers.push({ key, handler: func });
|
||||
addOnSaved<T extends AllSettingItemKey>(key: T, func: OnSavedHandlerFunc<T>) {
|
||||
const newHandler = { key, handler: func } as OnSavedHandler<AllSettingItemKey>;
|
||||
this.onSavedHandlers.push(newHandler);
|
||||
}
|
||||
resetEditingSettings() {
|
||||
this._editingSettings = undefined;
|
||||
@@ -379,6 +367,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
|
||||
override hide() {
|
||||
super.hide();
|
||||
this._lifetimeComponent.unload();
|
||||
this.isShown = false;
|
||||
}
|
||||
isShown: boolean = false;
|
||||
@@ -663,8 +653,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
}
|
||||
|
||||
display(): void {
|
||||
override display(): void {
|
||||
const changeDisplay = this.changeDisplay.bind(this);
|
||||
// Make sure lifetime component is loaded for markdown rendering in panes.
|
||||
this._lifetimeComponent.load();
|
||||
const { containerEl } = this;
|
||||
this.settingComponents.length = 0;
|
||||
this.controlledElementFunc.length = 0;
|
||||
@@ -718,7 +710,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
visibleOnly(() => this.isNeedRebuildLocal() || this.isNeedRebuildRemote())
|
||||
);
|
||||
|
||||
let paneNo = 0;
|
||||
// let paneNo = 0;
|
||||
const addPane = (
|
||||
parentEl: HTMLElement,
|
||||
title: string,
|
||||
@@ -728,20 +720,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
level?: ConfigLevel
|
||||
) => {
|
||||
const el = this.createEl(parentEl, "div", { text: "" });
|
||||
DEV: {
|
||||
const mdTitle = `${paneNo++}. ${title}${getLevelStr(level ?? "")}`;
|
||||
el.setAttribute("data-pane", mdTitle);
|
||||
toc.add(
|
||||
`| ${icon} | [${mdTitle}](#${mdTitle
|
||||
.toLowerCase()
|
||||
.replace(/ /g, "-")
|
||||
.replace(/[^\w\s-]/g, "")}) | `
|
||||
);
|
||||
}
|
||||
|
||||
setLevelClass(el, level);
|
||||
// TODO: Refactor to use Obsidian's recommended way to create heading.
|
||||
// eslint-disable-next-line obsidianmd/settings-tab/no-manual-html-headings
|
||||
el.createEl("h3", { text: title, cls: "sls-setting-pane-title" });
|
||||
new Setting(el).setName(title).setHeading().setClass("sls-setting-pane-title");
|
||||
if (this.menuEl) {
|
||||
this.menuEl.createEl(
|
||||
"label",
|
||||
@@ -772,7 +753,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
// });
|
||||
return p;
|
||||
};
|
||||
const panelNoMap = {} as { [key: string]: number };
|
||||
// const panelNoMap = {} as { [key: string]: number };
|
||||
const addPanel = (
|
||||
parentEl: HTMLElement,
|
||||
title: string,
|
||||
@@ -781,15 +762,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
level?: ConfigLevel
|
||||
) => {
|
||||
const el = this.createEl(parentEl, "div", { text: "" }, callback, func);
|
||||
DEV: {
|
||||
const paneNo = findAttrFromParent(parentEl, "data-pane");
|
||||
if (!(paneNo in panelNoMap)) {
|
||||
panelNoMap[paneNo] = 0;
|
||||
}
|
||||
panelNoMap[paneNo] += 1;
|
||||
const panelNo = panelNoMap[paneNo];
|
||||
el.setAttribute("data-panel", `${panelNo}. ${title}${getLevelStr(level ?? "")}`);
|
||||
}
|
||||
setLevelClass(el, level);
|
||||
this.createEl(el, "h4", { text: title, cls: "sls-setting-panel-title" });
|
||||
const p = Promise.resolve(el);
|
||||
@@ -823,6 +795,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
return callback;
|
||||
};
|
||||
|
||||
// Add panes
|
||||
|
||||
// TODO: Refactor to new API style.
|
||||
void addPane(containerEl, $msg("obsidianLiveSyncSettingTab.panelChangeLog"), "💬", 100, false).then(
|
||||
bindPane(paneChangeLog)
|
||||
);
|
||||
|
||||
@@ -20,7 +20,6 @@ export function paneChangeLog(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElem
|
||||
undefined,
|
||||
visibleOnly(() => !this.isConfiguredAs("versionUpFlash", ""))
|
||||
);
|
||||
|
||||
this.createEl(
|
||||
cx,
|
||||
"div",
|
||||
@@ -58,6 +57,6 @@ export function paneChangeLog(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElem
|
||||
});
|
||||
}
|
||||
fireAndForget(() =>
|
||||
MarkdownRenderer.render(this.plugin.app, updateInformation, informationDivEl, "/", this.plugin)
|
||||
MarkdownRenderer.render(this.plugin.app, updateInformation, informationDivEl, "/", this.lifetimeComponent)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type LoadedEntry,
|
||||
type MetaEntry,
|
||||
type FilePath,
|
||||
type EntryDoc,
|
||||
} from "@lib/common/types.ts";
|
||||
import { createBlob, getFileRegExp, isDocContentSame, readAsBlob } from "@lib/common/utils.ts";
|
||||
import { Logger } from "@lib/common/logger.ts";
|
||||
@@ -441,8 +442,8 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement,
|
||||
const newData = entriesToDelete.rows.map((e) => ({
|
||||
...e.doc,
|
||||
_deleted: true,
|
||||
}));
|
||||
const r = await this.core.localDatabase.bulkDocsRaw(newData as any[]);
|
||||
})) as EntryDoc[];
|
||||
const r = await this.core.localDatabase.bulkDocsRaw(newData);
|
||||
// Do not care about the result.
|
||||
Logger(
|
||||
`${r.length} items have been removed, to confirm how many items are left, please perform it again.`,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type ObsidianLiveSyncSettings,
|
||||
LOG_LEVEL_VERBOSE,
|
||||
} from "@lib/common/types.ts";
|
||||
import { Menu } from "@/deps.ts";
|
||||
import { Menu, type ButtonComponent } from "@/deps.ts";
|
||||
import { $msg } from "@lib/common/i18n.ts";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
@@ -37,9 +37,9 @@ import { syncActivatedRemoteSettings } from "./remoteConfigBuffer.ts";
|
||||
|
||||
function getSettingsFromEditingSettings(editingSettings: AllSettings): ObsidianLiveSyncSettings {
|
||||
const workObj = { ...editingSettings } as ObsidianLiveSyncSettings;
|
||||
const keys = Object.keys(OnDialogSettingsDefault);
|
||||
const keys = Object.keys(OnDialogSettingsDefault) as (keyof ObsidianLiveSyncSettings)[];
|
||||
for (const k of keys) {
|
||||
delete (workObj as any)[k];
|
||||
delete workObj[k];
|
||||
}
|
||||
return workObj;
|
||||
}
|
||||
@@ -72,7 +72,7 @@ function serializeRemoteConfiguration(settings: ObsidianLiveSyncSettings): strin
|
||||
return ConnectionStringParser.serialize({ type: "couchdb", settings });
|
||||
}
|
||||
|
||||
function setEmojiButton(button: any, emoji: string, tooltip: string) {
|
||||
function setEmojiButton(button: ButtonComponent, emoji: string, tooltip: string) {
|
||||
button.setButtonText(emoji);
|
||||
button.setTooltip(tooltip, { delay: 10, placement: "top" });
|
||||
// button.buttonEl.addClass("clickable-icon");
|
||||
|
||||
@@ -14,6 +14,7 @@ import { visibleOnly } from "./SettingPane.ts";
|
||||
import { DEFAULT_SETTINGS } from "@lib/common/types.ts";
|
||||
import { request } from "@/deps.ts";
|
||||
import { SetupManager, UserMode } from "@/modules/features/SetupManager.ts";
|
||||
import { LiveSyncError } from "@/lib/src/common/LSError.ts";
|
||||
export function paneSetup(
|
||||
this: ObsidianLiveSyncSettingTab,
|
||||
paneEl: HTMLElement,
|
||||
@@ -145,8 +146,9 @@ export function paneSetup(
|
||||
let remoteTroubleShootMDSrc = "";
|
||||
try {
|
||||
remoteTroubleShootMDSrc = await request(`${rawRepoURI}${basePath}/${filename}`);
|
||||
} catch (ex: any) {
|
||||
remoteTroubleShootMDSrc = `${$msg("obsidianLiveSyncSettingTab.logErrorOccurred")}\n${ex.toString()}`;
|
||||
} catch (ex) {
|
||||
const err = LiveSyncError.fromError(ex);
|
||||
remoteTroubleShootMDSrc = `${$msg("obsidianLiveSyncSettingTab.logErrorOccurred")}\n${err.toString()}`;
|
||||
}
|
||||
const remoteTroubleShootMD = remoteTroubleShootMDSrc.replace(
|
||||
/\((.*?(.png)|(.jpg))\)/g,
|
||||
@@ -158,7 +160,7 @@ export function paneSetup(
|
||||
`<a class='sls-troubleshoot-anchor'></a> [${$msg("obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting")}](${topPath}) [${$msg("obsidianLiveSyncSettingTab.linkPageTop")}](${filename})\n\n${remoteTroubleShootMD}`,
|
||||
troubleShootEl,
|
||||
`${rawRepoURI}`,
|
||||
this.plugin
|
||||
this.lifetimeComponent
|
||||
);
|
||||
// Menu
|
||||
troubleShootEl.querySelector<HTMLAnchorElement>(".sls-troubleshoot-anchor")?.parentElement?.setCssStyles({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { type Writable, writable, get } from "svelte/store";
|
||||
* Props passed to Svelte panels, containing a writable port
|
||||
* to communicate with the panel
|
||||
*/
|
||||
export type SveltePanelProps<T = any> = {
|
||||
export type SveltePanelProps<T = unknown> = {
|
||||
port: Writable<T | undefined>;
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ export type SveltePanelProps<T = any> = {
|
||||
* A class to manage a Svelte panel within Obsidian
|
||||
* Especially useful for settings panels
|
||||
*/
|
||||
export class SveltePanel<T = any> {
|
||||
export class SveltePanel<T = unknown> {
|
||||
private _mountedComponent: ReturnType<typeof mount>;
|
||||
private _componentValue = writable<T | undefined>(undefined);
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@lib/common/utils";
|
||||
import { getConfig, type AllSettingItemKey } from "./settingConstants";
|
||||
import { LOG_LEVEL_NOTICE, Logger } from "octagonal-wheels/common/logger";
|
||||
import { isNotFoundError } from "@lib/common/utils.doc";
|
||||
|
||||
/**
|
||||
* Generates a summary of P2P configuration settings
|
||||
@@ -90,10 +91,10 @@ export function getSummaryFromPartialSettings(setting: Partial<ObsidianLiveSyncS
|
||||
export async function copyMigrationDocs(docName: string, dbFrom: PouchDB.Database, dbTo: PouchDB.Database) {
|
||||
try {
|
||||
const doc = await dbFrom.get(docName);
|
||||
delete (doc as any)._rev;
|
||||
delete (doc as { _rev?: string })._rev;
|
||||
await dbTo.put(doc);
|
||||
} catch (e) {
|
||||
if ((e as any).status === 404) {
|
||||
if (isNotFoundError(e)) {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { fireAndForget, parseHeaderValues } from "@lib/common/utils";
|
||||
import { isCloudantURI } from "@lib/pouchdb/utils_couchdb";
|
||||
import { generateCredentialObject } from "@lib/replication/httplib";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
import { isUnauthorizedError } from "@lib/common/utils.doc";
|
||||
|
||||
export const checkConfig = async (
|
||||
checkResultDiv: HTMLDivElement | undefined,
|
||||
@@ -260,8 +261,8 @@ export const checkConfig = async (
|
||||
addResult($msg("obsidianLiveSyncSettingTab.msgDone"), ["ob-btn-config-head"]);
|
||||
addResult($msg("obsidianLiveSyncSettingTab.msgConnectionProxyNote"), ["ob-btn-config-info"]);
|
||||
Logger($msg("obsidianLiveSyncSettingTab.logCheckingConfigDone"), LOG_LEVEL_INFO);
|
||||
} catch (ex: any) {
|
||||
if (ex?.status == 401) {
|
||||
} catch (ex) {
|
||||
if (isUnauthorizedError(ex)) {
|
||||
isSuccessful = false;
|
||||
addResult($msg("obsidianLiveSyncSettingTab.errAccessForbidden"));
|
||||
addResult($msg("obsidianLiveSyncSettingTab.errCannotContinueTest"));
|
||||
|
||||
@@ -22,17 +22,17 @@
|
||||
detectedIssues.push({ message: `Error during testAndFixSettings: ${e}`, result: "error", classes: [] });
|
||||
}
|
||||
}
|
||||
function isErrorResult(result: ConfigCheckResult): result is ResultError | ResultErrorMessage {
|
||||
function isErrorResult(result: ConfigCheckResult): result is ResultError<unknown> | ResultErrorMessage {
|
||||
return "result" in result && result.result === "error";
|
||||
}
|
||||
function isFixableError(result: ConfigCheckResult): result is ResultError {
|
||||
function isFixableError(result: ConfigCheckResult): result is ResultError<unknown> {
|
||||
return isErrorResult(result) && "fix" in result && typeof result.fix === "function";
|
||||
}
|
||||
function isSuccessResult(result: ConfigCheckResult): result is { message: string; result: "ok"; value?: any } {
|
||||
return "result" in result && result.result === "ok";
|
||||
}
|
||||
let processing = $state(false);
|
||||
async function fixIssue(issue: ResultError) {
|
||||
async function fixIssue(issue: ResultError<unknown>) {
|
||||
try {
|
||||
processing = true;
|
||||
await issue.fix();
|
||||
|
||||
@@ -6,12 +6,17 @@ import { parseHeaderValues } from "@lib/common/utils";
|
||||
import { isCloudantURI } from "@lib/pouchdb/utils_couchdb";
|
||||
import { generateCredentialObject } from "@lib/replication/httplib";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
import { isUnauthorizedError } from "@lib/common/utils.doc";
|
||||
|
||||
export type ResultMessage = { message: string; classes: string[] };
|
||||
export type ResultErrorMessage = { message: string; result: "error"; classes: string[] };
|
||||
export type ResultOk = { message: string; result: "ok"; value?: any };
|
||||
export type ResultError = { message: string; result: "error"; value: any; fixMessage: string; fix(): Promise<void> };
|
||||
export type ConfigCheckResult = ResultOk | ResultError | ResultMessage | ResultErrorMessage;
|
||||
export type ResultOk<T> = { message: string; result: "ok"; value?: T };
|
||||
export type ResultError<T> = { message: string; result: "error"; value: T; fixMessage: string; fix(): Promise<void> };
|
||||
export type ConfigCheckResult<T = unknown, U = unknown> =
|
||||
| ResultOk<T>
|
||||
| ResultError<U>
|
||||
| ResultMessage
|
||||
| ResultErrorMessage;
|
||||
/**
|
||||
* Compares two version strings to determine if the baseVersion is greater than or equal to the version.
|
||||
* @param baseVersion a.b.c format
|
||||
@@ -37,7 +42,11 @@ function isGreaterThanOrEqual(baseVersion: string, version: string) {
|
||||
* @param value setting value to update
|
||||
* @returns true if the update was successful, false otherwise
|
||||
*/
|
||||
async function updateRemoteSetting(setting: ObsidianLiveSyncSettings, key: string, value: any) {
|
||||
async function updateRemoteSetting(
|
||||
setting: ObsidianLiveSyncSettings,
|
||||
key: string,
|
||||
value: string
|
||||
): Promise<true | string> {
|
||||
const customHeaders = parseHeaderValues(setting.couchDB_CustomHeaders);
|
||||
const credential = generateCredentialObject(setting);
|
||||
const res = await requestToCouchDBWithCredentials(
|
||||
@@ -62,24 +71,29 @@ async function updateRemoteSetting(setting: ObsidianLiveSyncSettings, key: strin
|
||||
* @returns Array of ConfigCheckResult
|
||||
*/
|
||||
export const checkConfig = async (editingSettings: ObsidianLiveSyncSettings) => {
|
||||
const result = [] as ConfigCheckResult[];
|
||||
const result = [] as ConfigCheckResult<unknown, unknown>[];
|
||||
const addMessage = (msg: string, classes: string[] = []) => {
|
||||
result.push({ message: msg, classes });
|
||||
};
|
||||
const addSuccess = (msg: string, value?: any) => {
|
||||
const addSuccess = <T>(msg: string, value?: T) => {
|
||||
result.push({ message: msg, result: "ok", value });
|
||||
};
|
||||
const _addError = (message: string, fixMessage: string, fix: () => Promise<void>, value?: any) => {
|
||||
const _addError = <T>(message: string, fixMessage: string, fix: () => Promise<void>, value?: T) => {
|
||||
result.push({ message, result: "error", fixMessage, fix, value });
|
||||
};
|
||||
const addErrorMessage = (msg: string, classes: string[] = []) => {
|
||||
result.push({ message: msg, result: "error", classes });
|
||||
};
|
||||
|
||||
const addError = (message: string, fixMessage: string, key: string, expected: any) => {
|
||||
_addError(message, fixMessage, async () => {
|
||||
const addError = (message: string, fixMessage: string, key: string, expected: string) => {
|
||||
_addError(
|
||||
message,
|
||||
fixMessage,
|
||||
async () => {
|
||||
await updateRemoteSetting(editingSettings, key, expected);
|
||||
});
|
||||
},
|
||||
expected
|
||||
);
|
||||
};
|
||||
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.logCheckingDbConfig"));
|
||||
@@ -281,8 +295,8 @@ export const checkConfig = async (editingSettings: ObsidianLiveSyncSettings) =>
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.msgDone"), ["ob-btn-config-head"]);
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.msgConnectionProxyNote"), ["ob-btn-config-info"]);
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.logCheckingConfigDone"));
|
||||
} catch (ex: any) {
|
||||
if (ex?.status == 401) {
|
||||
} catch (ex) {
|
||||
if (isUnauthorizedError(ex)) {
|
||||
addErrorMessage($msg("obsidianLiveSyncSettingTab.errAccessForbidden"));
|
||||
addErrorMessage($msg("obsidianLiveSyncSettingTab.errCannotContinueTest"));
|
||||
addMessage($msg("obsidianLiveSyncSettingTab.logCheckingConfigDone"));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { InjectableAPIService } from "@lib/services/implements/injectable/InjectableAPIService";
|
||||
import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext";
|
||||
import { Platform, type Command, type ViewCreator } from "obsidian";
|
||||
import { Platform, type Command, type ViewCreator } from "@/deps.ts";
|
||||
import { ObsHttpHandler } from "@/modules/essentialObsidian/APILib/ObsHttpHandler";
|
||||
import { ObsidianConfirm } from "./ObsidianConfirm";
|
||||
import type { Confirm } from "@lib/interfaces/Confirm";
|
||||
@@ -122,14 +122,15 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
|
||||
return this.context.plugin.addCommand(command) as TCommand;
|
||||
}
|
||||
|
||||
registerWindow(type: string, factory: ViewCreator): void {
|
||||
return this.context.plugin.registerView(type, factory);
|
||||
registerWindow<T>(type: string, factory: (leaf: T) => unknown): void {
|
||||
return this.context.plugin.registerView(type, factory as ViewCreator);
|
||||
}
|
||||
addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => any): HTMLElement {
|
||||
|
||||
addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => unknown): HTMLElement {
|
||||
return this.context.plugin.addRibbonIcon(icon, title, callback);
|
||||
}
|
||||
|
||||
registerProtocolHandler(action: string, handler: (params: Record<string, string>) => any): void {
|
||||
registerProtocolHandler(action: string, handler: (params: Record<string, string>) => unknown): void {
|
||||
return this.context.plugin.registerObsidianProtocolHandler(action, handler);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ export async function deleteFlagFile(host: NecessaryServices<never, "storageAcce
|
||||
}
|
||||
const REMOTE_KEEP_CURRENT = "Use active remote";
|
||||
const REMOTE_CANCEL = "Cancel";
|
||||
async function askAndActivateRemoteDatabase(host: NecessaryServices<"UI" | "setting", any>, log: LogFunction) {
|
||||
async function askAndActivateRemoteDatabase(host: NecessaryServices<"UI" | "setting", never>, log: LogFunction) {
|
||||
const settings = host.services.setting.currentSettings();
|
||||
if (settings.remoteConfigurations && Object.keys(settings.remoteConfigurations).length > 1) {
|
||||
const message =
|
||||
@@ -216,7 +216,7 @@ export function createFetchAllFlagHandler(
|
||||
* @returns updated configuration if applied, otherwise null.
|
||||
*/
|
||||
export async function adjustSettingToRemote(
|
||||
host: NecessaryServices<"tweakValue" | "UI" | "setting", any>,
|
||||
host: NecessaryServices<"tweakValue" | "UI" | "setting", never>,
|
||||
log: LogFunction,
|
||||
config: ObsidianLiveSyncSettings
|
||||
) {
|
||||
@@ -243,7 +243,7 @@ export async function adjustSettingToRemote(
|
||||
const necessary = extractObject(TweakValuesShouldMatchedTemplate, remoteTweaks);
|
||||
// Check if any necessary tweak value is different from current config.
|
||||
const differentItems = Object.entries(necessary).filter(([key, value]) => {
|
||||
return (config as any)[key] !== value;
|
||||
return config[key as keyof ObsidianLiveSyncSettings] !== value;
|
||||
});
|
||||
if (differentItems.length === 0) {
|
||||
log("Remote configuration matches local configuration. No changes applied.", LOG_LEVEL_NOTICE);
|
||||
@@ -260,7 +260,7 @@ export async function adjustSettingToRemote(
|
||||
|
||||
config = {
|
||||
...config,
|
||||
...Object.fromEntries(differentItems),
|
||||
...(Object.fromEntries(differentItems) as Partial<ObsidianLiveSyncSettings>),
|
||||
} satisfies ObsidianLiveSyncSettings;
|
||||
await host.services.setting.applyExternalSettings(config, true);
|
||||
log("Remote configuration applied.", LOG_LEVEL_NOTICE);
|
||||
@@ -277,7 +277,7 @@ export async function adjustSettingToRemote(
|
||||
* @param config current configuration to retrieve remote preferred config
|
||||
*/
|
||||
export async function adjustSettingToRemoteIfNeeded(
|
||||
host: NecessaryServices<"tweakValue" | "UI" | "setting", any>,
|
||||
host: NecessaryServices<"tweakValue" | "UI" | "setting", never>,
|
||||
log: LogFunction,
|
||||
extra: { preventFetchingConfig: boolean },
|
||||
config: ObsidianLiveSyncSettings
|
||||
@@ -309,7 +309,7 @@ export async function adjustSettingToRemoteIfNeeded(
|
||||
* @returns result of the process, or false if error occurs.
|
||||
*/
|
||||
export async function processVaultInitialisation(
|
||||
host: NecessaryServices<"setting", any>,
|
||||
host: NecessaryServices<"setting", never>,
|
||||
log: LogFunction,
|
||||
proc: () => Promise<boolean>,
|
||||
keepSuspending = false
|
||||
@@ -341,7 +341,7 @@ export async function processVaultInitialisation(
|
||||
}
|
||||
|
||||
export async function verifyAndUnlockSuspension(
|
||||
host: NecessaryServices<"setting" | "appLifecycle" | "UI", any>,
|
||||
host: NecessaryServices<"setting" | "appLifecycle" | "UI", never>,
|
||||
log: LogFunction
|
||||
) {
|
||||
if (!host.services.setting.currentSettings().suspendFileWatching) {
|
||||
|
||||
@@ -6,11 +6,11 @@ import { TFile, TFolder } from "obsidian";
|
||||
*/
|
||||
|
||||
export class ObsidianTypeGuardAdapter implements ITypeGuardAdapter<TFile, TFolder> {
|
||||
isFile(file: any): file is TFile {
|
||||
isFile(file: unknown): file is TFile {
|
||||
return file instanceof TFile;
|
||||
}
|
||||
|
||||
isFolder(item: any): item is TFolder {
|
||||
isFolder(item: unknown): item is TFolder {
|
||||
return item instanceof TFolder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { TFile, App, TFolder } from "obsidian";
|
||||
/**
|
||||
* Vault adapter implementation for Obsidian
|
||||
*/
|
||||
export class ObsidianVaultAdapter implements IVaultAdapter<TFile> {
|
||||
export class ObsidianVaultAdapter implements IVaultAdapter<TFile, TFolder> {
|
||||
constructor(private app: App) {}
|
||||
|
||||
async read(file: TFile): Promise<string> {
|
||||
@@ -38,24 +38,24 @@ export class ObsidianVaultAdapter implements IVaultAdapter<TFile> {
|
||||
}
|
||||
|
||||
async delete(file: TFile | TFolder, force = false): Promise<void> {
|
||||
// if ("trashFile" in this.app.fileManager) {
|
||||
// // eslint-disable-next-line obsidianmd/no-unsupported-api
|
||||
// return await this.app.fileManager.trashFile(file);
|
||||
// }
|
||||
//TODO: need fix
|
||||
if ("trashFile" in this.app.fileManager) {
|
||||
// eslint-disable-next-line obsidianmd/no-unsupported-api
|
||||
return await this.app.fileManager.trashFile(file);
|
||||
}
|
||||
// eslint-disable-next-line obsidianmd/prefer-file-manager-trash-file -- Fallback for older versions of Obsidian without trashFile support
|
||||
return await this.app.vault.delete(file, force);
|
||||
}
|
||||
|
||||
async trash(file: TFile | TFolder, force = false): Promise<void> {
|
||||
// if ("trashFile" in this.app.fileManager) {
|
||||
// // eslint-disable-next-line obsidianmd/no-unsupported-api
|
||||
// return await this.app.fileManager.trashFile(file);
|
||||
// }
|
||||
//TODO: need fix
|
||||
if ("trashFile" in this.app.fileManager) {
|
||||
// eslint-disable-next-line obsidianmd/no-unsupported-api
|
||||
return await this.app.fileManager.trashFile(file);
|
||||
}
|
||||
// eslint-disable-next-line obsidianmd/prefer-file-manager-trash-file -- Fallback for older versions of Obsidian without trashFile support
|
||||
return await this.app.vault.trash(file, force);
|
||||
}
|
||||
|
||||
trigger(name: string, ...data: any[]): any {
|
||||
trigger(name: string, ...data: unknown[]): void {
|
||||
return this.app.vault.trigger(name, ...data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,14 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
|
||||
|
||||
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
|
||||
|
||||
## Unreleased
|
||||
|
||||
18th June, 2026
|
||||
|
||||
### Improved
|
||||
|
||||
- File deletion now respects the user's deletion preferences (by utilising the `FileManager.trashFile` API) on Obsidian v1.7.2 or newer, regardless of the plug-in's internal trashbin setting.
|
||||
|
||||
## 0.25.76
|
||||
|
||||
15th June, 2026
|
||||
|
||||
@@ -26,11 +26,7 @@ for (const sourceFile of project.getSourceFiles()) {
|
||||
const posixFilePath = toPosixPath(filePath);
|
||||
|
||||
if (!posixFilePath.startsWith(posixSrc)) continue;
|
||||
if (
|
||||
posixFilePath.includes("/_test/") ||
|
||||
posixFilePath.endsWith(".spec.ts") ||
|
||||
posixFilePath.endsWith(".test.ts")
|
||||
) {
|
||||
if (posixFilePath.includes("/_test/") || posixFilePath.endsWith(".spec.ts") || posixFilePath.endsWith(".test.ts")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,8 @@ for (const sourceFile of project.getSourceFiles()) {
|
||||
const posixFilePath = toPosixPath(filePath);
|
||||
|
||||
if (!posixFilePath.startsWith(posixSrc)) continue;
|
||||
if (posixFilePath.includes("/_test/") || posixFilePath.endsWith(".spec.ts") || posixFilePath.endsWith(".test.ts")) continue;
|
||||
if (posixFilePath.includes("/_test/") || posixFilePath.endsWith(".spec.ts") || posixFilePath.endsWith(".test.ts"))
|
||||
continue;
|
||||
|
||||
// Find AsExpression (expr as Type) and TypeAssertion (<Type>expr)
|
||||
const asExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.AsExpression);
|
||||
|
||||
@@ -38,7 +38,8 @@ for (const sourceFile of project.getSourceFiles()) {
|
||||
const posixFilePath = toPosixPath(filePath);
|
||||
|
||||
if (!posixFilePath.startsWith(posixSrc)) continue;
|
||||
if (posixFilePath.includes("/_test/") || posixFilePath.endsWith(".spec.ts") || posixFilePath.endsWith(".test.ts")) continue;
|
||||
if (posixFilePath.includes("/_test/") || posixFilePath.endsWith(".spec.ts") || posixFilePath.endsWith(".test.ts"))
|
||||
continue;
|
||||
|
||||
let fileModified = false;
|
||||
|
||||
@@ -51,10 +52,11 @@ for (const sourceFile of project.getSourceFiles()) {
|
||||
if (varDec) {
|
||||
const varName = varDec.getName();
|
||||
// Count references within the catch clause itself
|
||||
const count = catchClause.getDescendantsOfKind(SyntaxKind.Identifier)
|
||||
.filter((id) => id.getText() === varName)
|
||||
.length;
|
||||
if (count === 1) { // Only the declaration itself
|
||||
const count = catchClause
|
||||
.getDescendantsOfKind(SyntaxKind.Identifier)
|
||||
.filter((id) => id.getText() === varName).length;
|
||||
if (count === 1) {
|
||||
// Only the declaration itself
|
||||
catchVarsToRemove.push(varDec);
|
||||
}
|
||||
}
|
||||
@@ -86,10 +88,11 @@ for (const sourceFile of project.getSourceFiles()) {
|
||||
for (const namedImport of namedImports) {
|
||||
const importName = namedImport.getAliasNode()?.getText() ?? namedImport.getName();
|
||||
// Count references in the entire file
|
||||
const count = sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)
|
||||
.filter((id) => id.getText() === importName)
|
||||
.length;
|
||||
if (count === 1) { // Only the import specifier itself
|
||||
const count = sourceFile
|
||||
.getDescendantsOfKind(SyntaxKind.Identifier)
|
||||
.filter((id) => id.getText() === importName).length;
|
||||
if (count === 1) {
|
||||
// Only the import specifier itself
|
||||
importsToRemove.push({ namedImport, impDecl });
|
||||
}
|
||||
}
|
||||
|
||||
+10
-3
@@ -9,10 +9,17 @@ import fs from "node:fs";
|
||||
import { platform } from "node:process";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
function readJson(filePath: string) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return {};
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||
}
|
||||
|
||||
const manifestJson = JSON.parse(fs.readFileSync("./manifest.json") + "");
|
||||
const packageJson = JSON.parse(fs.readFileSync("./package.json") + "");
|
||||
const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + "");
|
||||
const manifestJson = readJson(path.resolve(__dirname, "manifest.json"));
|
||||
const packageJson = readJson(path.resolve(__dirname, "package.json"));
|
||||
const updatesPath = path.resolve(__dirname, "updates.md");
|
||||
const updateInfo = JSON.stringify(fs.existsSync(updatesPath) ? fs.readFileSync(updatesPath, "utf-8") : "");
|
||||
|
||||
// const moduleAliasPlugin = {
|
||||
// name: "module-alias",
|
||||
|
||||
Reference in New Issue
Block a user