mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-06-18 12:20:15 +00:00
Compare commits
22 Commits
0.25.76-cli
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c6c4044f3c | |||
| 2bd7832d71 | |||
| 72033472f3 | |||
| 93dc03e86f | |||
| dae8443fe8 | |||
| 88a8bcbd5a | |||
| 4a5283543d | |||
| 7895336189 | |||
| 2d5cdccf7d | |||
| 28cdf40fcd | |||
| 497fd04081 | |||
| 38142aaf1c | |||
| ae9c46f8f0 | |||
| dcd10cd690 | |||
| 5a35b71339 | |||
| 18a59219f7 | |||
| c9095738e6 | |||
| bed415fc6b | |||
| 4eeb93b9e4 | |||
| ebbac96b16 | |||
| ed0e67e853 | |||
| f99d7dc00f |
@@ -1 +1,24 @@
|
||||
# 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
|
||||
|
||||
@@ -12,6 +12,17 @@ on:
|
||||
- main
|
||||
tags:
|
||||
- "*.*.*-cli"
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "*.md"
|
||||
- "images/**"
|
||||
- "assets/**"
|
||||
- "instruction_images/**"
|
||||
- "src/apps/webapp/**"
|
||||
- "src/apps/webpeer/**"
|
||||
- ".github/workflows/release.yml"
|
||||
- ".github/workflows/unit-ci.yml"
|
||||
- ".github/workflows/harness-ci.yml"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
# Sequence of patterns matched against refs/tags
|
||||
tags:
|
||||
- '*' # Push events to matching any tag format, i.e. 1.0, 20.15.10
|
||||
- '!*-cli' # Exclude command-line interface tags
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# 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
|
||||
@@ -0,0 +1,349 @@
|
||||
# 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. |
|
||||
@@ -0,0 +1,26 @@
|
||||
# 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# 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
|
||||
@@ -0,0 +1,30 @@
|
||||
; 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
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"TCP": {
|
||||
"443": {
|
||||
"HTTPS": true
|
||||
}
|
||||
},
|
||||
"Web": {
|
||||
"${TS_CERT_DOMAIN}:443": {
|
||||
"Handlers": {
|
||||
"/": {
|
||||
"Proxy": "http://127.0.0.1:5984"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"AllowFunnel": {
|
||||
"${TS_CERT_DOMAIN}:443": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
# 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
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/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}"
|
||||
+35
-2
@@ -19,11 +19,15 @@ const packageJson = JSON.parse(fs.readFileSync("./package.json") + "");
|
||||
const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + "");
|
||||
|
||||
const PATHS_TEST_INSTALL = process.env?.PATHS_TEST_INSTALL || "";
|
||||
const PATH_TEST_INSTALL = PATHS_TEST_INSTALL.split(path.delimiter).map(p => p.trim()).filter(p => p.length);
|
||||
const PATH_TEST_INSTALL = PATHS_TEST_INSTALL.split(path.delimiter)
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => p.length);
|
||||
if (PATH_TEST_INSTALL) {
|
||||
console.log(`Built files will be copied to ${PATH_TEST_INSTALL}`);
|
||||
} else {
|
||||
console.log("Development build: You can install the plug-in to Obsidian for testing by exporting the PATHS_TEST_INSTALL environment variable with the paths to your vault plugins directories separated by your system path delimiter (':' on Unix, ';' on Windows).");
|
||||
console.log(
|
||||
"Development build: You can install the plug-in to Obsidian for testing by exporting the PATHS_TEST_INSTALL environment variable with the paths to your vault plugins directories separated by your system path delimiter (':' on Unix, ';' on Windows)."
|
||||
);
|
||||
}
|
||||
|
||||
const moduleAliasPlugin = {
|
||||
@@ -66,6 +70,34 @@ const moduleAliasPlugin = {
|
||||
},
|
||||
};
|
||||
|
||||
const removePragmaCommentsPlugin = {
|
||||
name: "remove-pragma-comments",
|
||||
setup(build) {
|
||||
// Filter target extensions (e.g., JavaScript and TypeScript)
|
||||
build.onLoad({ filter: /\.[jt]s?$/ }, async (args) => {
|
||||
const source = await fs.promises.readFile(args.path, "utf8");
|
||||
|
||||
// Regex targeting both single-line and multi-line comments
|
||||
// This regex looks for:
|
||||
// - /* eslint ... */ (multi-line)
|
||||
// const esLintPragmaRegexBlock = /\/\*[\s\S]*?eslint[\s\S]*?\*\/|([^\\:]|^)\/\/.*eslint.*$/gm;
|
||||
// - // eslint-disable-next-line
|
||||
let cleanedSource = source;
|
||||
const tsIgnoreRegex = /\/\*\s*@ts-ignore\s*\*\/|([^\\:]|^)\/\/.*?@ts-ignore.*$/gm;
|
||||
const esLintPragmaRegexLine = /([^\\:]|^)\/\/.*?eslint-.*$/gm;
|
||||
const exps = [tsIgnoreRegex, esLintPragmaRegexLine];
|
||||
for (const exp of exps) {
|
||||
cleanedSource = cleanedSource.replace(exp, "$1");
|
||||
}
|
||||
|
||||
return {
|
||||
contents: cleanedSource,
|
||||
loader: args.path.endsWith("ts") ? "ts" : "js",
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/** @type esbuild.Plugin[] */
|
||||
const plugins = [
|
||||
{
|
||||
@@ -177,6 +209,7 @@ const context = await esbuild.context({
|
||||
preprocess: sveltePreprocess(),
|
||||
compilerOptions: { css: "injected", preserveComments: false },
|
||||
}),
|
||||
removePragmaCommentsPlugin,
|
||||
...plugins,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
const restrictedGlobalsOptions = [
|
||||
{
|
||||
name: "app",
|
||||
message: "Avoid using the global app object. Instead use the reference provided by your plugin instance.",
|
||||
},
|
||||
"warn",
|
||||
{
|
||||
name: "fetch",
|
||||
message: "Use the built-in `requestUrl` function instead of `fetch` for network requests in Obsidian.",
|
||||
},
|
||||
{
|
||||
name: "localStorage",
|
||||
message:
|
||||
"Prefer `App#saveLocalStorage` / `App#loadLocalStorage` functions to write / read localStorage data that's unique to a vault.",
|
||||
},
|
||||
];
|
||||
const restrictedImportsOptions = [
|
||||
{
|
||||
name: "axios",
|
||||
message: "Use the built-in `requestUrl` function instead of `axios`.",
|
||||
},
|
||||
{
|
||||
name: "superagent",
|
||||
message: "Use the built-in `requestUrl` function instead of `superagent`.",
|
||||
},
|
||||
{
|
||||
name: "got",
|
||||
message: "Use the built-in `requestUrl` function instead of `got`.",
|
||||
},
|
||||
{
|
||||
name: "ofetch",
|
||||
message: "Use the built-in `requestUrl` function instead of `ofetch`.",
|
||||
},
|
||||
{
|
||||
name: "ky",
|
||||
message: "Use the built-in `requestUrl` function instead of `ky`.",
|
||||
},
|
||||
{
|
||||
name: "node-fetch",
|
||||
message: "Use the built-in `requestUrl` function instead of `node-fetch`.",
|
||||
},
|
||||
{
|
||||
name: "moment",
|
||||
message: "The 'moment' package is bundled with Obsidian. Please import it from 'obsidian' instead.",
|
||||
},
|
||||
];
|
||||
|
||||
const warnWhileDev = "off";
|
||||
|
||||
/**
|
||||
* @type {import("eslint").Linter.RulesRecord}
|
||||
*/
|
||||
export const baseRules = {
|
||||
// -- Base rules (turned off in favour of TS specific versions or explicitly disabled).
|
||||
"no-unused-vars": "off",
|
||||
"no-unused-labels": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"require-await": "off",
|
||||
// -- TypeScript specific rules (Gradual adoption of stricter rules, currently set to 'warn' for a while).
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-redundant-type-constituents": "warn",
|
||||
// -- TypeScript specific rules
|
||||
// @typescript-eslint/no-unsafe-* rules and @typescript-eslint/no-explicit-any:
|
||||
// This project contains a lot of library-sh code where the use of `any` is often necessary and justified.
|
||||
// Rules is now set to 'off' for a while.
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
// -- Reasonable rules.
|
||||
"@typescript-eslint/no-deprecated": warnWhileDev,
|
||||
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/require-await": "error",
|
||||
"@typescript-eslint/no-misused-promises": "error",
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
|
||||
// -- General rules
|
||||
"no-async-promise-executor": warnWhileDev,
|
||||
"no-constant-condition": ["error", { checkLoops: false }],
|
||||
// -- Disabled rules
|
||||
// no-undef: This option breaks the global declarations for the library files and is not worth the effort to fix at this time.
|
||||
"no-undef": "off",
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import("eslint").Linter.RulesRecord}
|
||||
*/
|
||||
export const obsidianRules = {
|
||||
// -- Obsidian rules
|
||||
// obsidianmd/no-unsupported-api: usually this project checks for API support at runtime, so this rule is not critical but can be helpful to catch potential issues.
|
||||
"obsidianmd/no-unsupported-api": warnWhileDev,
|
||||
|
||||
// -- Plugin specific overrides
|
||||
"obsidianmd/rule-custom-message": "off",
|
||||
"obsidianmd/ui/sentence-case": "off",
|
||||
"obsidianmd/no-plugin-as-component": "off",
|
||||
|
||||
// -- Temporary overrides for migration
|
||||
"obsidianmd/no-static-styles-assignment": "off",
|
||||
};
|
||||
/**
|
||||
* @type {(base:string) => import("eslint").Linter.RulesRecord}
|
||||
*/
|
||||
export const ImportAliasRules = (base) => ({
|
||||
"@dword-design/import-alias/prefer-alias": [
|
||||
"error",
|
||||
{
|
||||
aliasForSubpaths: true,
|
||||
alias: {
|
||||
"@": `${base}/src`,
|
||||
"@lib": `${base}/src/lib/src`,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
/**
|
||||
* @type {import("eslint").Linter.RulesRecord}
|
||||
*/
|
||||
export const CommunityReviewRecommendedRules = {
|
||||
"no-unused-vars": "off",
|
||||
"no-prototype-bultins": "off",
|
||||
"no-self-compare": "warn",
|
||||
"no-eval": "error",
|
||||
"no-implied-eval": "error",
|
||||
"prefer-const": "off",
|
||||
"no-implicit-globals": "error",
|
||||
"no-console": "off", // overridden by obsidianmd/rule-custom-message
|
||||
"no-restricted-globals": ["error", ...restrictedGlobalsOptions],
|
||||
"no-restricted-imports": ["error", ...restrictedImportsOptions],
|
||||
"no-alert": "error",
|
||||
"no-undef": "error",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-deprecated": "error",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
|
||||
"@typescript-eslint/require-await": "off",
|
||||
"@typescript-eslint/no-explicit-any": ["error", { fixToUnknown: true }],
|
||||
// "import/no-nodejs-modules": "off",
|
||||
// "import/no-extraneous-dependencies": "error",
|
||||
};
|
||||
+16
-48
@@ -4,6 +4,8 @@ import globals from "globals";
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import * as sveltePlugin from "eslint-plugin-svelte";
|
||||
import svelteParser from "svelte-eslint-parser";
|
||||
import importAlias from "@dword-design/eslint-plugin-import-alias";
|
||||
import { baseRules, ImportAliasRules, obsidianRules } from "./eslint.config.common.mjs";
|
||||
const warnWhileDev = "off"; // Change to "warn" to enable warnings for rules that are currently disabled.
|
||||
export default defineConfig([
|
||||
globalIgnores([
|
||||
@@ -18,11 +20,11 @@ export default defineConfig([
|
||||
"**/*.json",
|
||||
"**/.eslintrc.js.bak",
|
||||
// Files from linked dependencies (those files should not exist for most people).
|
||||
"modules/octagonal-wheels/dist/**/*",
|
||||
"modules/octagonal-wheels/dist",
|
||||
|
||||
// Sub-projects (Exclude from root linting as they have different environments)
|
||||
"src/apps/**/*",
|
||||
"utils/**/*",
|
||||
"src/apps",
|
||||
"utils",
|
||||
|
||||
// Specific exclusions from common library (src/lib)
|
||||
"src/lib/coverage",
|
||||
@@ -54,6 +56,7 @@ export default defineConfig([
|
||||
]),
|
||||
...sveltePlugin.configs["flat/base"],
|
||||
...obsidianmd.configs.recommended,
|
||||
importAlias.configs.recommended,
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
// ignores:["src/lib/**/*.ts"], // Exclude library files from root linting (they have different environments and rules).
|
||||
@@ -62,64 +65,29 @@ export default defineConfig([
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
rootDir: "./",
|
||||
},
|
||||
},
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: false,
|
||||
},
|
||||
rules: {
|
||||
// -- Base rules (turned off in favour of TS specific versions or explicitly disabled).
|
||||
"no-unused-vars": "off",
|
||||
"no-unused-labels": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"require-await": "off",
|
||||
// -- TypeScript specific rules
|
||||
// @typescript-eslint/no-unsafe-* rules and @typescript-eslint/no-explicit-any:
|
||||
// This project contains a lot of library-sh code where the use of `any` is often necessary and justified.
|
||||
// Rules is now set to 'off' for a while.
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
// -- Reasonable rules.
|
||||
"@typescript-eslint/no-deprecated": warnWhileDev,
|
||||
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/require-await": "error",
|
||||
"@typescript-eslint/no-misused-promises": "error",
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
|
||||
// -- Obsidian rules
|
||||
// obsidianmd/no-unsupported-api: usually this project checks for API support at runtime, so this rule is not critical but can be helpful to catch potential issues.
|
||||
"obsidianmd/no-unsupported-api": warnWhileDev,
|
||||
|
||||
// -- General rules
|
||||
"no-async-promise-executor": warnWhileDev,
|
||||
"no-constant-condition": ["error", { checkLoops: false }],
|
||||
// -- Disabled rules
|
||||
// no-undef: This option breaks the global declarations for the library files and is not worth the effort to fix at this time.
|
||||
"no-undef": "off",
|
||||
|
||||
// -- Plugin specific overrides
|
||||
"obsidianmd/rule-custom-message": "off",
|
||||
"obsidianmd/ui/sentence-case": "off",
|
||||
"obsidianmd/no-plugin-as-component": "off",
|
||||
|
||||
// -- Temporary overrides for migration
|
||||
"obsidianmd/no-static-styles-assignment": "off",
|
||||
...baseRules,
|
||||
...obsidianRules,
|
||||
// -- Project specific rules
|
||||
...ImportAliasRules("."),
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/*.svelte"],
|
||||
languageOptions: {
|
||||
globals: { ...globals.browser, PouchDB: "readonly" },
|
||||
parser: svelteParser,
|
||||
parserOptions: {
|
||||
parser: tsParser,
|
||||
extraFileExtensions: [".svelte"],
|
||||
project: "./tsconfig.json",
|
||||
rootDir: "./",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
@@ -127,8 +95,8 @@ export default defineConfig([
|
||||
// Svelte template's declarations have a lot of false positives and the rule is not worth the effort to fix at this time.
|
||||
// it may improve in the future with some options as like ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],]
|
||||
"no-unused-vars": "off",
|
||||
"obsidianmd/no-plugin-as-component": "off",
|
||||
"obsidianmd/ui/sentence-case": "off",
|
||||
...obsidianRules,
|
||||
...ImportAliasRules("."),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
Generated
+1461
-1726
File diff suppressed because it is too large
Load Diff
+24
-21
@@ -57,15 +57,17 @@
|
||||
"test:docker-all:stop": "npm run test:docker-all:down",
|
||||
"test:full": "npm run test:docker-all:start && vitest run --coverage && npm run test:docker-all:stop",
|
||||
"test:p2p": "bash test/suitep2p/run-p2p-tests.sh",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
"update-workspaces": "node update-workspaces.mjs",
|
||||
"version": "node version-bump.mjs && node update-workspaces.mjs && git add manifest.json versions.json src/apps/cli/package.json src/apps/webpeer/package.json src/apps/webapp/package.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "vorotamoroz",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@chialab/esbuild-plugin-worker": "^0.19.0",
|
||||
"@dword-design/eslint-plugin-import-alias": "^8.1.8",
|
||||
"@eslint/js": "^9.39.3",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||
"@tsconfig/svelte": "^5.0.8",
|
||||
"@types/deno": "^2.5.0",
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
@@ -80,7 +82,6 @@
|
||||
"@types/pouchdb-mapreduce": "^6.1.10",
|
||||
"@types/pouchdb-replication": "^6.4.7",
|
||||
"@types/transform-pouch": "^1.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "8.56.1",
|
||||
"@typescript-eslint/parser": "8.56.1",
|
||||
"@vitest/browser": "^4.1.8",
|
||||
"@vitest/browser-playwright": "^4.1.8",
|
||||
@@ -91,12 +92,11 @@
|
||||
"esbuild-svelte": "^0.9.4",
|
||||
"eslint": "^9.39.3",
|
||||
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||
"eslint-plugin-svelte": "^3.15.0",
|
||||
"eslint-plugin-svelte": "^3.19.0",
|
||||
"events": "^3.3.0",
|
||||
"globals": "^14.0.0",
|
||||
"playwright": "^1.58.2",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-load-config": "^6.0.1",
|
||||
"pouchdb-adapter-http": "^9.0.0",
|
||||
"pouchdb-adapter-idb": "^9.0.0",
|
||||
"pouchdb-adapter-indexeddb": "^9.0.0",
|
||||
@@ -110,20 +110,22 @@
|
||||
"pouchdb-utils": "^9.0.0",
|
||||
"prettier": "3.8.1",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"svelte": "5.41.1",
|
||||
"svelte-check": "^4.4.3",
|
||||
"svelte": "5.56.3",
|
||||
"svelte-check": "^4.6.0",
|
||||
"svelte-eslint-parser": "^1.8.0",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"terser": "^5.39.0",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"transform-pouch": "^2.0.0",
|
||||
"tslib": "^2.8.1",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-istanbul": "^8.0.0",
|
||||
"typescript-eslint": "^8.61.0",
|
||||
"vite": "^8.0.16",
|
||||
"vitest": "^4.1.8",
|
||||
"webdriverio": "^9.27.0",
|
||||
"yaml": "^2.8.2"
|
||||
"yaml": "^2.8.2",
|
||||
"@emnapi/core": "1.11.1",
|
||||
"@emnapi/runtime": "1.11.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.808.0",
|
||||
@@ -132,21 +134,22 @@
|
||||
"@smithy/middleware-apply-body-checksum": "^4.3.9",
|
||||
"@smithy/protocol-http": "^5.3.9",
|
||||
"@smithy/querystring-builder": "^4.2.9",
|
||||
"@smithy/types": "^4.14.3",
|
||||
"@smithy/util-retry": "^4.4.5",
|
||||
"@trystero-p2p/nostr": "^0.24.0",
|
||||
"chokidar": "^4.0.0",
|
||||
"commander": "^14.0.3",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"fflate": "^0.8.2",
|
||||
"idb": "^8.0.3",
|
||||
"markdown-it": "^14.1.1",
|
||||
"micromatch": "^4.0.0",
|
||||
"minimatch": "^10.2.2",
|
||||
"obsidian": "^1.12.3",
|
||||
"markdown-it": "^14.2.0",
|
||||
"minimatch": "^10.2.5",
|
||||
"obsidian": "^1.13.1",
|
||||
"octagonal-wheels": "^0.1.46",
|
||||
"pouchdb-adapter-leveldb": "^9.0.0",
|
||||
"qrcode-generator": "^1.4.4",
|
||||
"werift": "^0.23.0",
|
||||
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
||||
}
|
||||
},
|
||||
"workspaces": [
|
||||
"src/apps/cli",
|
||||
"src/apps/webpeer",
|
||||
"src/apps/webapp"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -82,8 +82,8 @@ RUN apt-get update \
|
||||
|
||||
WORKDIR /deps
|
||||
|
||||
# runtime-package.json lists only the packages that Vite leaves external
|
||||
COPY src/apps/cli/runtime-package.json ./package.json
|
||||
# package.json lists only the packages that the CLI requires
|
||||
COPY src/apps/cli/package.json ./package.json
|
||||
RUN npm install --omit=dev
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
+12
-5
@@ -118,19 +118,26 @@ git submodule update --init --recursive
|
||||
# Install dependencies from the repository root
|
||||
npm install
|
||||
|
||||
# Build the CLI from its package directory
|
||||
# Build the CLI from the repository root
|
||||
npm run build -w self-hosted-livesync-cli
|
||||
|
||||
# Or from the package directory
|
||||
cd src/apps/cli
|
||||
npm run build
|
||||
```
|
||||
|
||||
If `src/lib` is missing, `npm run build` now stops early with a targeted message
|
||||
instead of a low-level Vite `ENOENT` error.
|
||||
If `src/lib` is missing, the build process stops early with a targeted message instead of a low-level Vite `ENOENT` error.
|
||||
|
||||
Run the CLI:
|
||||
|
||||
```bash
|
||||
# Run with npm script (from repository root)
|
||||
npm run --silent cli -- [database-path] [command] [args...]
|
||||
# Run with npm workspace script (from repository root)
|
||||
npm run cli -w self-hosted-livesync-cli -- [database-path] [command] [args...]
|
||||
|
||||
# Or from the package directory
|
||||
cd src/apps/cli
|
||||
npm run cli -- [database-path] [command] [args...]
|
||||
|
||||
# Run the built executable directly
|
||||
node src/apps/cli/dist/index.cjs [database-path] [command] [args...]
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as path from "path";
|
||||
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
||||
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { NodeFile, NodeFolder } from "./NodeTypes";
|
||||
import { path } from "../node-compat";
|
||||
|
||||
/**
|
||||
* Conversion adapter implementation for Node.js
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
import type { FilePath, UXStat } from "@lib/common/types";
|
||||
import type { IFileSystemAdapter } from "@lib/serviceModules/adapters";
|
||||
import { NodePathAdapter } from "./NodePathAdapter";
|
||||
@@ -8,6 +6,7 @@ import { NodeConversionAdapter } from "./NodeConversionAdapter";
|
||||
import { NodeStorageAdapter } from "./NodeStorageAdapter";
|
||||
import { NodeVaultAdapter } from "./NodeVaultAdapter";
|
||||
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes";
|
||||
import { fsPromises as fs, path } from "../node-compat";
|
||||
|
||||
/**
|
||||
* Complete file system adapter implementation for Node.js
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as path from "path";
|
||||
import type { FilePath } from "@lib/common/types";
|
||||
import type { IPathAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { NodeFile } from "./NodeTypes";
|
||||
import { path } from "../node-compat";
|
||||
|
||||
/**
|
||||
* Path adapter implementation for Node.js
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
import type { UXDataWriteOptions } from "@lib/common/types";
|
||||
import type { IStorageAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { NodeStat } from "./NodeTypes";
|
||||
import { fsPromises as fs, path } from "../node-compat";
|
||||
|
||||
/**
|
||||
* Storage adapter implementation for Node.js
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
import type { UXDataWriteOptions } from "@lib/common/types";
|
||||
import type { IVaultAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes";
|
||||
import type { NodeFile, NodeFolder } from "./NodeTypes";
|
||||
import { fsPromises as fs, path } from "../node-compat";
|
||||
|
||||
/**
|
||||
* Vault adapter implementation for Node.js
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import type { LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import { P2P_DEFAULT_SETTINGS } from "@lib/common/types";
|
||||
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import { addP2PEventHandlers } from "@lib/replication/trystero/addP2PEventHandlers";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
|
||||
type CLIP2PPeer = {
|
||||
peerId: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
return new Promise((resolve) => compatGlobal.setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function parseTimeoutSeconds(value: string, commandName: string): number {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
import { decodeSettingsFromSetupURI } from "@lib/API/processSetting";
|
||||
import { configURIBase } from "@lib/common/models/shared.const";
|
||||
import {
|
||||
@@ -18,6 +16,8 @@ import { promptForPassphrase, readStdinAsUtf8, toArrayBuffer, toDatabaseRelative
|
||||
import { collectPeers, openP2PHost, parseTimeoutSeconds, syncWithPeer } from "./p2p";
|
||||
import { performFullScan } from "@lib/serviceFeatures/offlineScanner";
|
||||
import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
import { fsPromises as fs, path } from "../node-compat";
|
||||
|
||||
function redactConnectionString(uri: string): string {
|
||||
return uri.replace(/\/\/([^@/]+)@/u, "//***@");
|
||||
@@ -150,11 +150,11 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
||||
);
|
||||
}
|
||||
}
|
||||
pollTimer = setTimeout(poll, currentIntervalMs);
|
||||
pollTimer = compatGlobal.setTimeout(poll, currentIntervalMs);
|
||||
};
|
||||
let pollTimer: ReturnType<typeof setTimeout> = setTimeout(poll, currentIntervalMs);
|
||||
let pollTimer = compatGlobal.setTimeout(poll, currentIntervalMs);
|
||||
core.services.appLifecycle.onUnload.addHandler(async () => {
|
||||
clearTimeout(pollTimer);
|
||||
compatGlobal.clearTimeout(pollTimer);
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
|
||||
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as path from "path";
|
||||
import * as readline from "node:readline/promises";
|
||||
import { path, readline } from "../node-compat";
|
||||
|
||||
export function toArrayBuffer(data: Buffer): ArrayBuffer {
|
||||
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// eslint-disable -- This is the entry point for the CLI application.
|
||||
import * as polyfill from "werift";
|
||||
import { main } from "./main";
|
||||
|
||||
|
||||
+8
-14
@@ -1,17 +1,10 @@
|
||||
/**
|
||||
* Self-hosted LiveSync CLI
|
||||
* Command-line version of Self-hosted LiveSync plugin for syncing vaults without Obsidian
|
||||
*/
|
||||
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
import { NodeServiceContext, NodeServiceHub } from "./services/NodeServiceHub";
|
||||
import { configureNodeLocalStorage, ensureGlobalNodeLocalStorage } from "./services/NodeLocalStorage";
|
||||
import { LiveSyncBaseCore } from "../../LiveSyncBaseCore";
|
||||
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import { initialiseServiceModulesCLI } from "./serviceModules/CLIServiceModules";
|
||||
import { DEFAULT_SETTINGS, LOG_LEVEL_VERBOSE, type LOG_LEVEL, type ObsidianLiveSyncSettings } from "@lib/common/types";
|
||||
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub";
|
||||
import type { InjectableSettingService } from "@/lib/src/services/implements/injectable/InjectableSettingService";
|
||||
import type { InjectableSettingService } from "@lib/services/implements/injectable/InjectableSettingService";
|
||||
import {
|
||||
LOG_LEVEL_DEBUG,
|
||||
setGlobalLogFunction,
|
||||
@@ -26,7 +19,8 @@ import type { CLICommand, CLIOptions } from "./commands/types";
|
||||
import { getPathFromUXFileInfo } from "@lib/common/typeUtils";
|
||||
import { stripAllPrefixes } from "@lib/string_and_binary/path";
|
||||
import { IgnoreRules } from "./serviceModules/IgnoreRules";
|
||||
import { useP2PReplicatorFeature } from "@/lib/src/replication/trystero/useP2PReplicatorFeature";
|
||||
import { useP2PReplicatorFeature } from "@lib/replication/trystero/useP2PReplicatorFeature";
|
||||
import { fsPromises as fs, path, fs as fsSync } from "./node-compat";
|
||||
|
||||
const SETTINGS_FILE = ".livesync/settings.json";
|
||||
ensureGlobalNodeLocalStorage();
|
||||
@@ -485,8 +479,8 @@ export async function main() {
|
||||
}
|
||||
};
|
||||
|
||||
process.on("SIGINT", () => shutdown("SIGINT"));
|
||||
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
||||
process.on("SIGINT", () => void shutdown("SIGINT"));
|
||||
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
||||
|
||||
// Save the settings file before any lifecycle events can mutate and persist them.
|
||||
// suspendAllSync and other lifecycle hooks clobber sync settings in memory, and
|
||||
@@ -499,8 +493,8 @@ export async function main() {
|
||||
if (settingsBackup) {
|
||||
const tmpPath = settingsPath + ".tmp";
|
||||
try {
|
||||
require("fs").writeFileSync(tmpPath, settingsBackup, "utf-8");
|
||||
require("fs").renameSync(tmpPath, settingsPath);
|
||||
fsSync.writeFileSync(tmpPath, settingsBackup, "utf-8");
|
||||
fsSync.renameSync(tmpPath, settingsPath);
|
||||
} catch (err) {
|
||||
console.error("[Settings] Failed to restore settings on exit:", err);
|
||||
}
|
||||
|
||||
@@ -10,12 +10,10 @@ import type {
|
||||
IStorageEventWatchHandlers,
|
||||
} from "@lib/managers/adapters";
|
||||
import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
|
||||
import type { NodeFile, NodeFolder } from "../adapters/NodeTypes";
|
||||
import type { Stats } from "fs";
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
import type { NodeFile, NodeFolder } from "@/apps/cli/adapters/NodeTypes";
|
||||
import { watch as chokidarWatch, type FSWatcher } from "chokidar";
|
||||
import type { IgnoreRules } from "../serviceModules/IgnoreRules";
|
||||
import type { IgnoreRules } from "@/apps/cli/serviceModules/IgnoreRules";
|
||||
import { fsPromises as fs, path, type Stats } from "../node-compat";
|
||||
|
||||
/**
|
||||
* CLI-specific type guard adapter
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import type { IStorageEventWatchHandlers } from "@lib/managers/adapters";
|
||||
import type { NodeFile } from "../adapters/NodeTypes";
|
||||
import type { NodeFile } from "@/apps/cli/adapters/NodeTypes";
|
||||
|
||||
// ── chokidar mock ──────────────────────────────────────────────────────────────
|
||||
// Must be hoisted before imports that pull in chokidar.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@lib/managers/StorageEventManager";
|
||||
import { CLIStorageEventManagerAdapter } from "./CLIStorageEventManagerAdapter";
|
||||
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
|
||||
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||
import type { IgnoreRules } from "../serviceModules/IgnoreRules";
|
||||
import type { IgnoreRules } from "@/apps/cli/serviceModules/IgnoreRules";
|
||||
// import type { IMinimumLiveSyncCommands } from "@lib/services/base/IService";
|
||||
|
||||
export class StorageEventManagerCLI extends StorageEventManagerBase<CLIStorageEventManagerAdapter> {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/* eslint-disable obsidianmd/no-nodejs-builtins */
|
||||
import * as nodeFs from "node:fs";
|
||||
import * as nodeFsPromises from "node:fs/promises";
|
||||
import * as nodePath from "node:path";
|
||||
import * as nodeReadlinePromises from "node:readline/promises";
|
||||
import type { Stats } from "node:fs";
|
||||
export {
|
||||
nodeFs as fs,
|
||||
nodeFsPromises as fsPromises,
|
||||
nodePath as path,
|
||||
nodeReadlinePromises as readline,
|
||||
type Stats,
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "self-hosted-livesync-cli",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.25.76-cli",
|
||||
"main": "dist/index.cjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -38,6 +38,26 @@
|
||||
"test:e2e:docker:p2p-sync": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-p2p-sync-linux.sh",
|
||||
"test:e2e:docker:all": "export RUN_BUILD=0 && npm run test:e2e:docker:setup-put-cat && npm run test:e2e:docker:push-pull && npm run test:e2e:docker:sync-two-local && npm run test:e2e:docker:mirror && npm run test:e2e:docker:remote-commands"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"minimatch": "^10.2.5",
|
||||
"octagonal-wheels": "^0.1.46",
|
||||
"pouchdb-adapter-http": "^9.0.0",
|
||||
"pouchdb-adapter-leveldb": "^9.0.0",
|
||||
"pouchdb-core": "^9.0.0",
|
||||
"pouchdb-errors": "^9.0.0",
|
||||
"pouchdb-find": "^9.0.0",
|
||||
"pouchdb-mapreduce": "^9.0.0",
|
||||
"pouchdb-merge": "^9.0.0",
|
||||
"pouchdb-replication": "^9.0.0",
|
||||
"pouchdb-utils": "^9.0.0",
|
||||
"transform-pouch": "^2.0.0",
|
||||
"werift": "^0.23.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "^8.0.16",
|
||||
"vitest": "^4.1.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"name": "livesync-cli-runtime",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"description": "Runtime dependencies for Self-hosted LiveSync CLI Docker image",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"commander": "^14.0.3",
|
||||
"werift": "^0.22.9",
|
||||
"pouchdb-adapter-http": "^9.0.0",
|
||||
"pouchdb-adapter-idb": "^9.0.0",
|
||||
"pouchdb-adapter-indexeddb": "^9.0.0",
|
||||
"pouchdb-adapter-leveldb": "^9.0.0",
|
||||
"pouchdb-adapter-memory": "^9.0.0",
|
||||
"pouchdb-core": "^9.0.0",
|
||||
"pouchdb-errors": "^9.0.0",
|
||||
"pouchdb-find": "^9.0.0",
|
||||
"pouchdb-mapreduce": "^9.0.0",
|
||||
"pouchdb-merge": "^9.0.0",
|
||||
"pouchdb-replication": "^9.0.0",
|
||||
"pouchdb-utils": "^9.0.0",
|
||||
"pouchdb-wrappers": "*",
|
||||
"transform-pouch": "^2.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub";
|
||||
import { ServiceRebuilder } from "@lib/serviceModules/Rebuilder";
|
||||
import { ServiceFileHandler } from "../../../serviceModules/FileHandler";
|
||||
import { ServiceFileHandler } from "@/serviceModules/FileHandler";
|
||||
import { StorageAccessManager } from "@lib/managers/StorageProcessingManager";
|
||||
import type { LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||
import { FileAccessCLI } from "./FileAccessCLI";
|
||||
import { ServiceFileAccessCLI } from "./ServiceFileAccessImpl";
|
||||
import { ServiceDatabaseFileAccessCLI } from "./DatabaseFileAccess";
|
||||
import { StorageEventManagerCLI } from "../managers/StorageEventManagerCLI";
|
||||
import { StorageEventManagerCLI } from "@/apps/cli/managers/StorageEventManagerCLI";
|
||||
import type { ServiceModules } from "@lib/interfaces/ServiceModule";
|
||||
import type { IgnoreRules } from "./IgnoreRules";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase";
|
||||
import { NodeFileSystemAdapter } from "../adapters/NodeFileSystemAdapter";
|
||||
import { NodeFileSystemAdapter } from "@/apps/cli/adapters/NodeFileSystemAdapter";
|
||||
|
||||
/**
|
||||
* CLI-specific implementation of FileAccessBase
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
|
||||
import { minimatch } from "minimatch";
|
||||
import { fsPromises as fs, path } from "../node-compat";
|
||||
|
||||
/**
|
||||
* Loads and evaluates ignore rules from `.livesync/ignore` inside the vault.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@lib/serviceModules/ServiceFileAccessBase";
|
||||
import { NodeFileSystemAdapter } from "../adapters/NodeFileSystemAdapter";
|
||||
import { NodeFileSystemAdapter } from "@/apps/cli/adapters/NodeFileSystemAdapter";
|
||||
|
||||
/**
|
||||
* CLI-specific implementation of ServiceFileAccess
|
||||
|
||||
@@ -7,8 +7,7 @@ import type { InjectableDatabaseEventService } from "@lib/services/implements/in
|
||||
import type { IVaultService } from "@lib/services/base/IService";
|
||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||
import { createInstanceLogFunction } from "@lib/services/lib/logUtils";
|
||||
import * as nodeFs from "node:fs";
|
||||
import * as nodePath from "node:path";
|
||||
import { fs as nodeFs, path as nodePath } from "../node-compat";
|
||||
|
||||
const NODE_KV_TYPED_KEY = "__nodeKvType";
|
||||
const NODE_KV_VALUES_KEY = "values";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as nodeFs from "node:fs";
|
||||
import * as nodePath from "node:path";
|
||||
import { fs as nodeFs, path as nodePath } from "../node-compat";
|
||||
|
||||
type LocalStorageShape = {
|
||||
getItem(key: string): string | null;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { AppLifecycleService, AppLifecycleServiceDependencies } from "@lib/services/base/AppLifecycleService";
|
||||
import { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||
import * as nodePath from "node:path";
|
||||
import { ConfigServiceBrowserCompat } from "@lib/services/implements/browser/ConfigServiceBrowserCompat";
|
||||
import { SvelteDialogManagerBase, type ComponentHasResult } from "@lib/services/implements/base/SvelteDialog";
|
||||
import { UIService } from "@lib/services/implements/base/UIService";
|
||||
@@ -24,7 +23,8 @@ import type { ServiceInstances } from "@lib/services/ServiceHub";
|
||||
import { NodeKeyValueDBService } from "./NodeKeyValueDBService";
|
||||
import { NodeSettingService } from "./NodeSettingService";
|
||||
import { DatabaseService } from "@lib/services/base/DatabaseService";
|
||||
import type { ObsidianLiveSyncSettings } from "@/lib/src/common/types";
|
||||
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
|
||||
import { path as nodePath } from "../node-compat";
|
||||
|
||||
export class NodeServiceContext extends ServiceContext {
|
||||
databasePath: string;
|
||||
|
||||
@@ -77,7 +77,9 @@ 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 as number) >= 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");
|
||||
});
|
||||
|
||||
@@ -15,18 +15,18 @@
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": false,
|
||||
"strict": true,
|
||||
// "noImplicitAny": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// "rootDir": "../../../",
|
||||
/* Path mapping */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["../../*"],
|
||||
"@lib/*": ["../../lib/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["*.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules", "dist", "test", "testdeno"]
|
||||
}
|
||||
|
||||
@@ -35,8 +35,15 @@ npm install
|
||||
|
||||
### Development
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
npm run dev -w livesync-webapp
|
||||
```
|
||||
|
||||
Or from the package directory:
|
||||
|
||||
```bash
|
||||
# Build the project (ensure you are in `src/apps/webapp` directory)
|
||||
cd src/apps/webapp
|
||||
npm run dev
|
||||
```
|
||||
@@ -45,8 +52,15 @@ This will start a development server at `http://localhost:3000`.
|
||||
|
||||
### Build
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
npm run build -w livesync-webapp
|
||||
```
|
||||
|
||||
Or from the package directory:
|
||||
|
||||
```bash
|
||||
# Build the project (ensure you are in `src/apps/webapp` directory)
|
||||
cd src/apps/webapp
|
||||
npm run build
|
||||
```
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { LiveSyncWebApp } from "./main";
|
||||
import { VaultHistoryStore, type VaultHistoryItem } from "./vaultSelector";
|
||||
import { compatGlobal, _activeDocument } from "@lib/common/coreEnvFunctions.ts";
|
||||
|
||||
const historyStore = new VaultHistoryStore();
|
||||
let app: LiveSyncWebApp | null = null;
|
||||
|
||||
function getRequiredElement<T extends HTMLElement>(id: string): T {
|
||||
const element = document.getElementById(id);
|
||||
const element = _activeDocument.getElementById(id);
|
||||
if (!element) {
|
||||
throw new Error(`Missing element: #${id}`);
|
||||
}
|
||||
@@ -22,7 +23,7 @@ function setBusyState(isBusy: boolean): void {
|
||||
const pickNewBtn = getRequiredElement<HTMLButtonElement>("pick-new-vault");
|
||||
pickNewBtn.disabled = isBusy;
|
||||
|
||||
const historyButtons = document.querySelectorAll<HTMLButtonElement>(".vault-item button");
|
||||
const historyButtons = _activeDocument.querySelectorAll<HTMLButtonElement>(".vault-item button");
|
||||
historyButtons.forEach((button) => {
|
||||
button.disabled = isBusy;
|
||||
});
|
||||
@@ -45,24 +46,24 @@ async function renderHistoryList(): Promise<VaultHistoryItem[]> {
|
||||
emptyEl.classList.toggle("is-hidden", items.length > 0);
|
||||
|
||||
for (const item of items) {
|
||||
const row = document.createElement("div");
|
||||
const row = _activeDocument.createElement("div");
|
||||
row.className = "vault-item";
|
||||
|
||||
const info = document.createElement("div");
|
||||
const info = _activeDocument.createElement("div");
|
||||
info.className = "vault-item-info";
|
||||
|
||||
const name = document.createElement("div");
|
||||
const name = _activeDocument.createElement("div");
|
||||
name.className = "vault-item-name";
|
||||
name.textContent = item.name;
|
||||
|
||||
const meta = document.createElement("div");
|
||||
const meta = _activeDocument.createElement("div");
|
||||
meta.className = "vault-item-meta";
|
||||
const label = item.id === lastUsedId ? "Last used" : "Used";
|
||||
meta.textContent = `${label}: ${formatLastUsed(item.lastUsedAt)}`;
|
||||
|
||||
info.append(name, meta);
|
||||
|
||||
const useButton = document.createElement("button");
|
||||
const useButton = _activeDocument.createElement("button");
|
||||
useButton.type = "button";
|
||||
useButton.textContent = "Use this vault";
|
||||
useButton.addEventListener("click", () => {
|
||||
@@ -120,7 +121,7 @@ async function initializeVaultSelector(): Promise<void> {
|
||||
await renderHistoryList();
|
||||
}
|
||||
|
||||
window.addEventListener("load", async () => {
|
||||
compatGlobal.addEventListener("load", async () => {
|
||||
try {
|
||||
await initializeVaultSelector();
|
||||
} catch (error) {
|
||||
@@ -129,11 +130,11 @@ window.addEventListener("load", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
compatGlobal.addEventListener("beforeunload", () => {
|
||||
void app?.shutdown();
|
||||
});
|
||||
|
||||
(window as any).livesyncApp = {
|
||||
(compatGlobal as any).livesyncApp = {
|
||||
getApp: () => app,
|
||||
historyStore,
|
||||
};
|
||||
|
||||
+10
-9
@@ -17,8 +17,9 @@ import { useSetupURIFeature } from "@lib/serviceFeatures/setupObsidian/setupUri"
|
||||
import { useRemoteConfiguration } from "@lib/serviceFeatures/remoteConfig";
|
||||
import { SetupManager } from "@/modules/features/SetupManager";
|
||||
import { useSetupManagerHandlersFeature } from "@/serviceFeatures/setupObsidian/setupManagerHandlers";
|
||||
import { useP2PReplicatorCommands } from "@/lib/src/replication/trystero/useP2PReplicatorCommands";
|
||||
import { useP2PReplicatorFeature } from "@/lib/src/replication/trystero/useP2PReplicatorFeature";
|
||||
import { useP2PReplicatorCommands } from "@lib/replication/trystero/useP2PReplicatorCommands";
|
||||
import { useP2PReplicatorFeature } from "@lib/replication/trystero/useP2PReplicatorFeature";
|
||||
import { compatGlobal, _activeDocument } from "@lib/common/coreEnvFunctions.ts";
|
||||
|
||||
const SETTINGS_DIR = ".livesync";
|
||||
const SETTINGS_FILE = "settings.json";
|
||||
@@ -91,7 +92,7 @@ class LiveSyncWebApp {
|
||||
console.log("[Settings] Loaded from .livesync/settings.json");
|
||||
return { ...DEFAULT_SETTINGS, ...data } as ObsidianLiveSyncSettings;
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.log("[Settings] Failed to load, using defaults");
|
||||
}
|
||||
return DEFAULT_SETTINGS as ObsidianLiveSyncSettings;
|
||||
@@ -102,8 +103,8 @@ class LiveSyncWebApp {
|
||||
console.log("[AppLifecycle] Restart requested");
|
||||
await this.shutdown();
|
||||
await this.initialize();
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
compatGlobal.setTimeout(() => {
|
||||
compatGlobal.location.reload();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
@@ -169,7 +170,7 @@ class LiveSyncWebApp {
|
||||
const file = await fileHandle.getFile();
|
||||
const text = await file.text();
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// File doesn't exist yet
|
||||
return null;
|
||||
}
|
||||
@@ -235,7 +236,7 @@ class LiveSyncWebApp {
|
||||
}
|
||||
|
||||
private showError(message: string) {
|
||||
const statusEl = document.getElementById("status");
|
||||
const statusEl = _activeDocument.getElementById("status");
|
||||
if (statusEl) {
|
||||
statusEl.className = "error";
|
||||
statusEl.textContent = `Error: ${message}`;
|
||||
@@ -243,7 +244,7 @@ class LiveSyncWebApp {
|
||||
}
|
||||
|
||||
private showWarning(message: string) {
|
||||
const statusEl = document.getElementById("status");
|
||||
const statusEl = _activeDocument.getElementById("status");
|
||||
if (statusEl) {
|
||||
statusEl.className = "warning";
|
||||
statusEl.textContent = `Warning: ${message}`;
|
||||
@@ -251,7 +252,7 @@ class LiveSyncWebApp {
|
||||
}
|
||||
|
||||
private showSuccess(message: string) {
|
||||
const statusEl = document.getElementById("status");
|
||||
const statusEl = _activeDocument.getElementById("status");
|
||||
if (statusEl) {
|
||||
statusEl.className = "success";
|
||||
statusEl.textContent = message;
|
||||
|
||||
@@ -10,7 +10,8 @@ import type {
|
||||
IStorageEventWatchHandlers,
|
||||
} from "@lib/managers/adapters";
|
||||
import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
|
||||
import type { FSAPIFile, FSAPIFolder } from "../adapters/FSAPITypes";
|
||||
import type { FSAPIFile, FSAPIFolder } from "@/apps/webapp/adapters/FSAPITypes";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
|
||||
/**
|
||||
* FileSystem API-specific type guard adapter
|
||||
@@ -149,14 +150,14 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
||||
|
||||
async beginWatch(handlers: IStorageEventWatchHandlers): Promise<void> {
|
||||
// Use FileSystemObserver if available (Chrome 124+)
|
||||
if (typeof (window as any).FileSystemObserver === "undefined") {
|
||||
if (typeof (compatGlobal as any).FileSystemObserver === "undefined") {
|
||||
console.log("[FSAPIWatchAdapter] FileSystemObserver not available, file watching disabled");
|
||||
console.log("[FSAPIWatchAdapter] Consider using Chrome 124+ for real-time file watching");
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
try {
|
||||
const FileSystemObserver = (window as any).FileSystemObserver;
|
||||
const FileSystemObserver = (compatGlobal as any).FileSystemObserver;
|
||||
|
||||
this.observer = new FileSystemObserver(async (records: any[]) => {
|
||||
for (const record of records) {
|
||||
@@ -181,7 +182,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
||||
if (changedHandle && changedHandle.kind === "file") {
|
||||
const file = await changedHandle.getFile();
|
||||
const fileInfo = {
|
||||
path: relativePath as any,
|
||||
path: relativePath,
|
||||
stat: {
|
||||
size: file.size,
|
||||
mtime: file.lastModified,
|
||||
@@ -199,7 +200,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
||||
}
|
||||
} else if (type === "disappeared") {
|
||||
const fileInfo = {
|
||||
path: relativePath as any,
|
||||
path: relativePath,
|
||||
stat: {
|
||||
size: 0,
|
||||
mtime: Date.now(),
|
||||
@@ -216,7 +217,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
||||
if (changedHandle && changedHandle.kind === "file") {
|
||||
const file = await changedHandle.getFile();
|
||||
const fileInfo = {
|
||||
path: relativePath as any,
|
||||
path: relativePath,
|
||||
stat: {
|
||||
size: file.size,
|
||||
mtime: file.lastModified,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "livesync-webapp",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"version": "0.25.76-webapp",
|
||||
"type": "module",
|
||||
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
|
||||
"scripts": {
|
||||
@@ -11,9 +11,16 @@
|
||||
"run:docker": "docker run -p 8002:80 livesync-webapp",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"octagonal-wheels": "^0.1.46"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||
"playwright": "^1.58.2",
|
||||
"svelte": "5.56.3",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
"vite": "^8.0.16",
|
||||
"vite-plugin-istanbul": "^9.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||
import { FileAccessFSAPI } from "./FileAccessFSAPI";
|
||||
import { ServiceFileAccessFSAPI } from "./ServiceFileAccessImpl";
|
||||
import { ServiceDatabaseFileAccessFSAPI } from "./DatabaseFileAccess";
|
||||
import { StorageEventManagerFSAPI } from "../managers/StorageEventManagerFSAPI";
|
||||
import { StorageEventManagerFSAPI } from "@/apps/webapp/managers/StorageEventManagerFSAPI";
|
||||
import type { ServiceModules } from "@lib/interfaces/ServiceModule";
|
||||
import { ServiceFileHandler } from "@/serviceModules/FileHandler";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase";
|
||||
import { FSAPIFileSystemAdapter } from "../adapters/FSAPIFileSystemAdapter";
|
||||
import { FSAPIFileSystemAdapter } from "@/apps/webapp/adapters/FSAPIFileSystemAdapter";
|
||||
|
||||
/**
|
||||
* FileSystem API-specific implementation of FileAccessBase
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@lib/serviceModules/ServiceFileAccessBase";
|
||||
import { FSAPIFileSystemAdapter } from "../adapters/FSAPIFileSystemAdapter";
|
||||
import { FSAPIFileSystemAdapter } from "@/apps/webapp/adapters/FSAPIFileSystemAdapter";
|
||||
|
||||
/**
|
||||
* FileSystem API-specific implementation of ServiceFileAccess
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { LiveSyncWebApp } from "./main";
|
||||
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
|
||||
import type { FilePathWithPrefix } from "@lib/common/types";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Internal state – one app instance per page / browser context
|
||||
@@ -41,7 +42,7 @@ async function waitForIdle(core: any, timeoutMs = 60_000): Promise<void> {
|
||||
(core.services?.fileProcessing?.processing?.value ?? 0) +
|
||||
(core.services?.replication?.storageApplyingCount?.value ?? 0);
|
||||
if (q === 0) return;
|
||||
await new Promise<void>((r) => setTimeout(r, 300));
|
||||
await new Promise<void>((r) => compatGlobal.setTimeout(r, 300));
|
||||
}
|
||||
throw new Error(`waitForIdle timed out after ${timeoutMs} ms`);
|
||||
}
|
||||
@@ -116,7 +117,7 @@ export interface LiveSyncTestAPI {
|
||||
const livesyncTest: LiveSyncTestAPI = {
|
||||
async init(vaultName: string, settings: Partial<ObsidianLiveSyncSettings>): Promise<void> {
|
||||
// Clean up any stale OPFS data from previous runs.
|
||||
const opfsRoot = await navigator.storage.getDirectory();
|
||||
const opfsRoot = await compatGlobal.navigator.storage.getDirectory();
|
||||
try {
|
||||
await opfsRoot.removeEntry(vaultName, { recursive: true });
|
||||
} catch {
|
||||
@@ -200,4 +201,4 @@ const livesyncTest: LiveSyncTestAPI = {
|
||||
};
|
||||
|
||||
// Expose on window for Playwright page.evaluate() calls.
|
||||
(window as any).livesyncTest = livesyncTest;
|
||||
(compatGlobal as any).livesyncTest = livesyncTest;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
|
||||
import { test, expect, type BrowserContext, type Page, type TestInfo } from "@playwright/test";
|
||||
import type { LiveSyncTestAPI } from "../test-entry";
|
||||
import type { LiveSyncTestAPI } from "@/apps/webapp/test-entry";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Path mapping */
|
||||
"baseUrl": ".",
|
||||
// "baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["../../*"],
|
||||
"@lib/*": ["../../lib/src/*"]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
|
||||
const HANDLE_DB_NAME = "livesync-webapp-handles";
|
||||
const HANDLE_STORE_NAME = "handles";
|
||||
const LAST_USED_KEY = "meta:lastUsedVaultId";
|
||||
@@ -89,7 +91,7 @@ export class VaultHistoryStore {
|
||||
|
||||
async getVaultHistory(): Promise<VaultHistoryItem[]> {
|
||||
return this.withStore("readonly", async (store) => {
|
||||
const keys = (await this.requestAsPromise(store.getAllKeys())) as IDBValidKey[];
|
||||
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++) {
|
||||
@@ -170,7 +172,7 @@ export class VaultHistoryStore {
|
||||
}
|
||||
|
||||
async pickNewVault(): Promise<FileSystemDirectoryHandle> {
|
||||
const picker = (window as any).showDirectoryPicker;
|
||||
const picker = (compatGlobal as any).showDirectoryPicker;
|
||||
if (typeof picker !== "function") {
|
||||
throw new Error("FileSystem API showDirectoryPicker is not supported in this browser");
|
||||
}
|
||||
|
||||
@@ -13,13 +13,20 @@ This pseudo client actually receives the data from other devices, and sends if s
|
||||
|
||||
## How to use it?
|
||||
|
||||
We can build the application by running the following command:
|
||||
We can build the application from the repository root by running the following command:
|
||||
|
||||
```bash
|
||||
$ deno task build
|
||||
npm run build -w webpeer
|
||||
```
|
||||
|
||||
Then, open the `dist/index.html` in the browser. It can be configured as the same as the Self-hosted LiveSync (Same components are used[^1]).
|
||||
Or from the package directory:
|
||||
|
||||
```bash
|
||||
cd src/apps/webpeer
|
||||
npm run build
|
||||
```
|
||||
|
||||
Then, open `dist/index.html` in the browser. It can be configured in the same way as Self-hosted LiveSync (the same components are used[^1]).
|
||||
|
||||
## Some notes
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "webpeer",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.25.76-webpeer",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -11,15 +11,17 @@
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
|
||||
},
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"octagonal-wheels": "^0.1.46"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-plugin-svelte": "^3.15.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"eslint-plugin-svelte": "^3.19.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||
"@tsconfig/svelte": "^5.0.8",
|
||||
"svelte": "5.41.1",
|
||||
"svelte-check": "^443.3",
|
||||
"svelte": "5.56.3",
|
||||
"svelte-check": "^4.6.0",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
"vite": "^8.0.16"
|
||||
},
|
||||
"imports": {
|
||||
"../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts",
|
||||
|
||||
@@ -17,7 +17,9 @@ import {
|
||||
type PeerStatus,
|
||||
type PluginShim,
|
||||
} from "@lib/replication/trystero/P2PReplicatorPaneCommon";
|
||||
import { P2PLogCollector, type P2PReplicatorBase, useP2PReplicator } from "@lib/replication/trystero/P2PReplicatorCore";
|
||||
import { useP2PReplicator } from "@lib/replication/trystero/P2PReplicatorCore";
|
||||
import { P2PLogCollector } from "@lib/replication/trystero/P2PLogCollector";
|
||||
import type { P2PReplicatorBase } from "@lib/replication/trystero/P2PReplicatorBase.ts";
|
||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||
import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
|
||||
import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents";
|
||||
@@ -28,9 +30,10 @@ import { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||
import type { InjectableServiceHub } from "@lib/services/InjectableServices";
|
||||
import { Menu } from "@lib/services/implements/browser/Menu";
|
||||
import { SimpleStoreIDBv2 } from "octagonal-wheels/databases/SimpleStoreIDBv2";
|
||||
import type { BrowserAPIService } from "@/lib/src/services/implements/browser/BrowserAPIService";
|
||||
import type { InjectableSettingService } from "@/lib/src/services/implements/injectable/InjectableSettingService";
|
||||
import type { BrowserAPIService } from "@lib/services/implements/browser/BrowserAPIService";
|
||||
import type { InjectableSettingService } from "@lib/services/implements/injectable/InjectableSettingService";
|
||||
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
|
||||
function addToList(item: string, list: string) {
|
||||
return unique(
|
||||
@@ -137,7 +140,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
|
||||
|
||||
this._initP2PReplicator();
|
||||
|
||||
setTimeout(() => {
|
||||
compatGlobal.setTimeout(() => {
|
||||
if (this.settings.P2P_AutoStart && this.settings.P2P_Enabled) {
|
||||
void this.open();
|
||||
}
|
||||
@@ -164,12 +167,12 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
|
||||
getConfig(key: string) {
|
||||
const vaultName = this.services.vault.getVaultName();
|
||||
const dbKey = `${vaultName}-${key}`;
|
||||
return localStorage.getItem(dbKey);
|
||||
return compatGlobal.localStorage.getItem(dbKey);
|
||||
}
|
||||
setConfig(key: string, value: string) {
|
||||
const vaultName = this.services.vault.getVaultName();
|
||||
const dbKey = `${vaultName}-${key}`;
|
||||
localStorage.setItem(dbKey, value);
|
||||
compatGlobal.localStorage.setItem(dbKey, value);
|
||||
}
|
||||
|
||||
getDeviceName(): string {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Menu } from "@/lib/src/services/implements/browser/Menu";
|
||||
import { Menu } from "@lib/services/implements/browser/Menu";
|
||||
import { getDialogContext } from "@lib/services/implements/base/SvelteDialog";
|
||||
let result = $state<string | boolean>("");
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { mount } from "svelte";
|
||||
import "./app.css";
|
||||
import App from "./App.svelte";
|
||||
import { _activeDocument } from "@lib/common/coreEnvFunctions.ts";
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.getElementById("app")!,
|
||||
target: _activeDocument.getElementById("app")!,
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { mount } from "svelte";
|
||||
import "./app.css";
|
||||
import App from "./UITest.svelte";
|
||||
import { _activeDocument } from "@lib/common/coreEnvFunctions.ts";
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.getElementById("app")!,
|
||||
target: _activeDocument.getElementById("app")!,
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"sourceRoot": "../",
|
||||
// "sourceRoot": "../",
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
@@ -15,11 +15,13 @@
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"isolatedModules": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"moduleDetection": "force",
|
||||
"paths": {
|
||||
"@/*": ["../../*"],
|
||||
"@lib/*": ["../../lib/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { deleteDB, type IDBPDatabase, openDB } from "idb";
|
||||
import type { KeyValueDatabase } from "../lib/src/interfaces/KeyValueDatabase.ts";
|
||||
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> } = {};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LOG_LEVEL_VERBOSE, Logger } from "@/lib/src/common/logger";
|
||||
import type { KeyValueDatabase } from "@/lib/src/interfaces/KeyValueDatabase";
|
||||
import { LOG_LEVEL_VERBOSE, Logger } from "@lib/common/logger";
|
||||
import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase";
|
||||
import { deleteDB, openDB, type IDBPDatabase } from "idb";
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { eventHub } from "../lib/src/hub/hub";
|
||||
import { eventHub } from "@lib/hub/hub";
|
||||
// import type ObsidianLiveSyncPlugin from "../main";
|
||||
|
||||
export const EVENT_PLUGIN_LOADED = "plugin-loaded";
|
||||
@@ -43,5 +43,5 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export * from "../lib/src/events/coreEvents.ts";
|
||||
export * from "@lib/events/coreEvents.ts";
|
||||
export { eventHub };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TFile } from "../deps";
|
||||
import type { FilePathWithPrefix, LoadedEntry } from "../lib/src/common/types";
|
||||
import type { TFile } from "@/deps";
|
||||
import type { FilePathWithPrefix, LoadedEntry } from "@lib/common/types";
|
||||
|
||||
export const EVENT_REQUEST_SHOW_HISTORY = "show-history";
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function generateReport(settings: ObsidianLiveSyncSettings, core: L
|
||||
const r = await requestToCouchDBWithCredentials(
|
||||
settings.couchDB_URI,
|
||||
credential,
|
||||
window.origin,
|
||||
compatGlobal.origin,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
import { type PluginManifest, TFile } from "../deps.ts";
|
||||
import { type DatabaseEntry, type EntryBody, type FilePath } from "../lib/src/common/types.ts";
|
||||
export type { CacheData, FileEventItem } from "../lib/src/common/types.ts";
|
||||
import { type PluginManifest, TFile } from "@/deps.ts";
|
||||
import { type DatabaseEntry, type EntryBody, type FilePath } from "@lib/common/types.ts";
|
||||
export type { CacheData, FileEventItem } from "@lib/common/types.ts";
|
||||
|
||||
export interface PluginDataEntry extends DatabaseEntry {
|
||||
deviceVaultName: string;
|
||||
@@ -51,7 +51,7 @@ export type queueItem = {
|
||||
|
||||
export const FileWatchEventQueueMax = 10;
|
||||
|
||||
export { configURIBase, configURIBaseQR } from "../lib/src/common/types.ts";
|
||||
export { configURIBase, configURIBaseQR } from "@lib/common/types.ts";
|
||||
|
||||
export {
|
||||
CHeader,
|
||||
@@ -61,4 +61,4 @@ export {
|
||||
ICHeaderEnd,
|
||||
ICHeaderLength,
|
||||
ICXHeader,
|
||||
} from "../lib/src/common/models/fileaccess.const.ts";
|
||||
} from "@lib/common/models/fileaccess.const.ts";
|
||||
|
||||
+9
-9
@@ -1,4 +1,4 @@
|
||||
import { normalizePath, Platform, TAbstractFile, type RequestUrlParam, requestUrl } from "../deps.ts";
|
||||
import { normalizePath, Platform, TAbstractFile, type RequestUrlParam, requestUrl } from "@/deps.ts";
|
||||
import {
|
||||
path2id_base,
|
||||
id2path_base,
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
isValidFilenameInWidows,
|
||||
isValidFilenameInAndroid,
|
||||
stripAllPrefixes,
|
||||
} from "../lib/src/string_and_binary/path.ts";
|
||||
} from "@lib/string_and_binary/path.ts";
|
||||
|
||||
import { Logger } from "../lib/src/common/logger.ts";
|
||||
import { Logger } from "@lib/common/logger.ts";
|
||||
import {
|
||||
LOG_LEVEL_INFO,
|
||||
LOG_LEVEL_NOTICE,
|
||||
@@ -22,14 +22,14 @@ import {
|
||||
type FilePathWithPrefix,
|
||||
type UXFileInfo,
|
||||
type UXFileInfoStub,
|
||||
} from "../lib/src/common/types.ts";
|
||||
} from "@lib/common/types.ts";
|
||||
export { ICHeader, ICXHeader } from "./types.ts";
|
||||
import { writeString } from "../lib/src/string_and_binary/convert.ts";
|
||||
import { writeString } from "@lib/string_and_binary/convert.ts";
|
||||
import { sameChangePairs } from "./stores.ts";
|
||||
|
||||
import { scheduleTask } from "octagonal-wheels/concurrency/task";
|
||||
import { AuthorizationHeaderGenerator } from "../lib/src/replication/httplib.ts";
|
||||
import type { KeyValueDatabase } from "../lib/src/interfaces/KeyValueDatabase.ts";
|
||||
import { AuthorizationHeaderGenerator } from "@lib/replication/httplib.ts";
|
||||
import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase.ts";
|
||||
|
||||
export { scheduleTask, cancelTask, cancelAllTasks } from "octagonal-wheels/concurrency/task";
|
||||
|
||||
@@ -132,7 +132,7 @@ export const _requestToCouchDBFetch = async (
|
||||
method?: string
|
||||
) => {
|
||||
const utf8str = String.fromCharCode.apply(null, [...writeString(`${username}:${password}`)]);
|
||||
const encoded = window.btoa(utf8str);
|
||||
const encoded = compatGlobal.btoa(utf8str);
|
||||
const authHeader = "Basic " + encoded;
|
||||
const transformedHeaders: Record<string, string> = {
|
||||
authorization: authHeader,
|
||||
@@ -214,7 +214,7 @@ import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@lib/common/models/shared.cons
|
||||
export { BASE_IS_NEW, EVEN, TARGET_IS_NEW };
|
||||
// Why 2000? : ZIP FILE Does not have enough resolution.
|
||||
import { compareMTime } from "@lib/common/utils.ts";
|
||||
import { _fetch } from "@/lib/src/common/coreEnvFunctions.ts";
|
||||
import { _fetch, compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
export { compareMTime };
|
||||
function getKey(file: AnyEntry | string | UXFileInfoStub) {
|
||||
const key = typeof file == "string" ? file : stripAllPrefixes(file.path);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
diff_match_patch,
|
||||
Platform,
|
||||
addIcon,
|
||||
} from "../../deps.ts";
|
||||
} from "@/deps.ts";
|
||||
|
||||
import type {
|
||||
EntryDoc,
|
||||
@@ -19,7 +19,7 @@ import type {
|
||||
AnyEntry,
|
||||
SavingEntry,
|
||||
diff_result,
|
||||
} from "../../lib/src/common/types.ts";
|
||||
} from "@lib/common/types.ts";
|
||||
import {
|
||||
CANCELLED,
|
||||
LEAVE_TO_SUBSEQUENT,
|
||||
@@ -29,8 +29,8 @@ import {
|
||||
LOG_LEVEL_VERBOSE,
|
||||
MODE_SELECTIVE,
|
||||
MODE_SHINY,
|
||||
} from "../../lib/src/common/types.ts";
|
||||
import { ICXHeader, PERIODIC_PLUGIN_SWEEP } from "../../common/types.ts";
|
||||
} from "@lib/common/types.ts";
|
||||
import { ICXHeader, PERIODIC_PLUGIN_SWEEP } from "@/common/types.ts";
|
||||
import {
|
||||
createBlob,
|
||||
createSavingEntryFromLoadedEntry,
|
||||
@@ -42,12 +42,12 @@ import {
|
||||
isDocContentSame,
|
||||
isLoadedEntry,
|
||||
isObjectDifferent,
|
||||
} from "../../lib/src/common/utils.ts";
|
||||
import { digestHash } from "../../lib/src/string_and_binary/hash.ts";
|
||||
import { arrayBufferToBase64, decodeBinary, readString } from "../../lib/src/string_and_binary/convert.ts";
|
||||
} from "@lib/common/utils.ts";
|
||||
import { digestHash } from "@lib/string_and_binary/hash.ts";
|
||||
import { arrayBufferToBase64, decodeBinary, readString } from "@lib/string_and_binary/convert.ts";
|
||||
import { serialized, shareRunningResult } from "octagonal-wheels/concurrency/lock";
|
||||
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
|
||||
import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
|
||||
import { LiveSyncCommands } from "@/features/LiveSyncCommands.ts";
|
||||
import { stripAllPrefixes } from "@lib/string_and_binary/path.ts";
|
||||
import {
|
||||
EVEN,
|
||||
disposeMemoObject,
|
||||
@@ -57,20 +57,20 @@ import {
|
||||
memoObject,
|
||||
retrieveMemoObject,
|
||||
scheduleTask,
|
||||
} from "../../common/utils.ts";
|
||||
} from "@/common/utils.ts";
|
||||
import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts";
|
||||
import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts";
|
||||
import { JsonResolveModal } from "@/features/HiddenFileCommon/JsonResolveModal.ts";
|
||||
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
||||
import { pluginScanningCount } from "../../lib/src/mock_and_interop/stores.ts";
|
||||
import type ObsidianLiveSyncPlugin from "../../main.ts";
|
||||
import { pluginScanningCount } from "@lib/mock_and_interop/stores.ts";
|
||||
import type ObsidianLiveSyncPlugin from "@/main.ts";
|
||||
import { base64ToArrayBuffer, base64ToString } from "octagonal-wheels/binary/base64";
|
||||
import { ConflictResolveModal } from "../../modules/features/InteractiveConflictResolving/ConflictResolveModal.ts";
|
||||
import { ConflictResolveModal } from "@/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts";
|
||||
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||
import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from "../../common/events.ts";
|
||||
import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from "@/common/events.ts";
|
||||
import { PluginDialogModal } from "./PluginDialogModal.ts";
|
||||
import { $msg } from "@/lib/src/common/i18n.ts";
|
||||
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
|
||||
import type { LiveSyncCore } from "../../main.ts";
|
||||
import { $msg } from "@lib/common/i18n.ts";
|
||||
import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
import { LiveSyncError } from "@lib/common/LSError.ts";
|
||||
|
||||
const d = "\u200b";
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
type IPluginDataExDisplay,
|
||||
type PluginDataExFile,
|
||||
} from "./CmdConfigSync.ts";
|
||||
import { Logger } from "../../lib/src/common/logger";
|
||||
import { type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../lib/src/common/types";
|
||||
import { getDocData, timeDeltaToHumanReadable, unique } from "../../lib/src/common/utils";
|
||||
import type ObsidianLiveSyncPlugin from "../../main";
|
||||
import { Logger } from "@lib/common/logger";
|
||||
import { type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@lib/common/types";
|
||||
import { getDocData, timeDeltaToHumanReadable, unique } from "@lib/common/utils";
|
||||
import type ObsidianLiveSyncPlugin from "@/main";
|
||||
// import { askString } from "../../common/utils";
|
||||
import { Menu } from "@/deps.ts";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mount, unmount } from "svelte";
|
||||
import { App, Modal } from "../../deps.ts";
|
||||
import ObsidianLiveSyncPlugin from "../../main.ts";
|
||||
import { App, Modal } from "@/deps.ts";
|
||||
import ObsidianLiveSyncPlugin from "@/main.ts";
|
||||
import PluginPane from "./PluginPane.svelte";
|
||||
export class PluginDialogModal extends Modal {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
@@ -16,9 +16,11 @@ export class PluginDialogModal extends Modal {
|
||||
|
||||
override onOpen() {
|
||||
const { contentEl } = this;
|
||||
this.contentEl.style.overflow = "auto";
|
||||
this.contentEl.style.display = "flex";
|
||||
this.contentEl.style.flexDirection = "column";
|
||||
this.contentEl.setCssStyles({
|
||||
overflow: "auto",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
});
|
||||
this.titleEl.setText("Customization Sync (Beta3)");
|
||||
if (!this.component) {
|
||||
this.component = mount(PluginPane, {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import ObsidianLiveSyncPlugin from "../../main";
|
||||
import ObsidianLiveSyncPlugin from "@/main";
|
||||
import {
|
||||
ConfigSync,
|
||||
type IPluginDataExDisplay,
|
||||
@@ -11,16 +11,16 @@
|
||||
} from "./CmdConfigSync.ts";
|
||||
import PluginCombo from "./PluginCombo.svelte";
|
||||
import { Menu, type PluginManifest } from "@/deps.ts";
|
||||
import { unique } from "../../lib/src/common/utils";
|
||||
import { unique } from "@lib/common/utils";
|
||||
import {
|
||||
MODE_SELECTIVE,
|
||||
MODE_AUTOMATIC,
|
||||
MODE_PAUSED,
|
||||
type SYNC_MODE,
|
||||
MODE_SHINY,
|
||||
} from "../../lib/src/common/types";
|
||||
import { normalizePath } from "../../deps";
|
||||
import { HiddenFileSync } from "../HiddenFileSync/CmdHiddenFileSync.ts";
|
||||
} from "@lib/common/types";
|
||||
import { normalizePath } from "@/deps";
|
||||
import { HiddenFileSync } from "@/features/HiddenFileSync/CmdHiddenFileSync.ts";
|
||||
import { LOG_LEVEL_NOTICE, Logger } from "octagonal-wheels/common/logger";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
|
||||
export let plugin: ObsidianLiveSyncPlugin;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { App, Modal } from "../../deps.ts";
|
||||
import { type FilePath, type LoadedEntry } from "../../lib/src/common/types.ts";
|
||||
import { App, Modal } from "@/deps.ts";
|
||||
import { type FilePath, type LoadedEntry } from "@lib/common/types.ts";
|
||||
import JsonResolvePane from "./JsonResolvePane.svelte";
|
||||
import { waitForSignal } from "../../lib/src/common/utils.ts";
|
||||
import { waitForSignal } from "@lib/common/utils.ts";
|
||||
import { mount, unmount } from "svelte";
|
||||
|
||||
export class JsonResolveModal extends Modal {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { type Diff, DIFF_DELETE, DIFF_INSERT, diff_match_patch } from "../../deps.ts";
|
||||
import type { FilePath, LoadedEntry } from "../../lib/src/common/types.ts";
|
||||
import { decodeBinary, readString } from "../../lib/src/string_and_binary/convert.ts";
|
||||
import { getDocData, isObjectDifferent, mergeObject } from "../../lib/src/common/utils.ts";
|
||||
import { type Diff, DIFF_DELETE, DIFF_INSERT, diff_match_patch } from "@/deps.ts";
|
||||
import type { FilePath, LoadedEntry } from "@lib/common/types.ts";
|
||||
import { decodeBinary, readString } from "@lib/string_and_binary/convert.ts";
|
||||
import { getDocData, isObjectDifferent, mergeObject } from "@lib/common/utils.ts";
|
||||
|
||||
interface Props {
|
||||
docs?: LoadedEntry[];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type PluginManifest, type ListedFiles } from "../../deps.ts";
|
||||
import { type PluginManifest, type ListedFiles } from "@/deps.ts";
|
||||
import {
|
||||
type LoadedEntry,
|
||||
type FilePathWithPrefix,
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
LOG_LEVEL_DEBUG,
|
||||
type MetaEntry,
|
||||
type UXDataWriteOptions,
|
||||
} from "../../lib/src/common/types.ts";
|
||||
import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "../../common/types.ts";
|
||||
} from "@lib/common/types.ts";
|
||||
import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "@/common/types.ts";
|
||||
import {
|
||||
readAsBlob,
|
||||
isDocContentSame,
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
fireAndForget,
|
||||
type CustomRegExp,
|
||||
getFileRegExp,
|
||||
} from "../../lib/src/common/utils.ts";
|
||||
} from "@lib/common/utils.ts";
|
||||
import {
|
||||
compareMTime,
|
||||
isInternalMetadata,
|
||||
@@ -39,17 +39,17 @@ import {
|
||||
BASE_IS_NEW,
|
||||
EVEN,
|
||||
displayRev,
|
||||
} from "../../common/utils.ts";
|
||||
} from "@/common/utils.ts";
|
||||
import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts";
|
||||
import { serialized, skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
|
||||
import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts";
|
||||
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
|
||||
import { addPrefix, stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
|
||||
import { JsonResolveModal } from "@/features/HiddenFileCommon/JsonResolveModal.ts";
|
||||
import { LiveSyncCommands } from "@/features/LiveSyncCommands.ts";
|
||||
import { addPrefix, stripAllPrefixes } from "@lib/string_and_binary/path.ts";
|
||||
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
||||
import { hiddenFilesEventCount, hiddenFilesProcessingCount } from "../../lib/src/mock_and_interop/stores.ts";
|
||||
import { EVENT_SETTING_SAVED, eventHub } from "../../common/events.ts";
|
||||
import { hiddenFilesEventCount, hiddenFilesProcessingCount } from "@lib/mock_and_interop/stores.ts";
|
||||
import { EVENT_SETTING_SAVED, eventHub } from "@/common/events.ts";
|
||||
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||
import type { LiveSyncCore } from "../../main.ts";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
import { tryGetFilePath } from "@lib/common/utils.doc.ts";
|
||||
type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce";
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@ import {
|
||||
type FilePath,
|
||||
type FilePathWithPrefix,
|
||||
type LOG_LEVEL,
|
||||
} from "../lib/src/common/types.ts";
|
||||
import type ObsidianLiveSyncPlugin from "../main.ts";
|
||||
import { MARK_DONE } from "../modules/features/ModuleLog.ts";
|
||||
import type { LiveSyncCore } from "../main.ts";
|
||||
import { __$checkInstanceBinding } from "../lib/src/dev/checks.ts";
|
||||
import { createInstanceLogFunction } from "@/lib/src/services/lib/logUtils.ts";
|
||||
} from "@lib/common/types.ts";
|
||||
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 { createInstanceLogFunction } from "@lib/services/lib/logUtils.ts";
|
||||
|
||||
let noticeIndex = 0;
|
||||
export abstract class LiveSyncCommands {
|
||||
|
||||
@@ -9,14 +9,14 @@ import {
|
||||
type EntryLeaf,
|
||||
type FilePathWithPrefix,
|
||||
type MetaEntry,
|
||||
} from "../../lib/src/common/types";
|
||||
import { getNoFromRev } from "../../lib/src/pouchdb/LiveSyncLocalDB";
|
||||
import { LiveSyncCommands } from "../LiveSyncCommands";
|
||||
} from "@lib/common/types";
|
||||
import { getNoFromRev } from "@lib/pouchdb/LiveSyncLocalDB";
|
||||
import { LiveSyncCommands } from "@/features/LiveSyncCommands";
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock_v2";
|
||||
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/src/replication/couchdb/LiveSyncReplicator";
|
||||
import { delay } from "@/lib/src/common/utils";
|
||||
import type { LiveSyncCouchDBReplicator } from "@lib/replication/couchdb/LiveSyncReplicator";
|
||||
import { delay } from "@lib/common/utils";
|
||||
// import { _requestToCouchDB } from "@/common/utils";
|
||||
const DB_KEY_SEQ = "gc-seq";
|
||||
const DB_KEY_CHUNK_SET = "chunk-set";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { App, Modal } from "@/deps.ts";
|
||||
import P2POpenReplicationPane from "./P2POpenReplicationPane.svelte";
|
||||
import { mount, unmount } from "svelte";
|
||||
import type { LiveSyncTrysteroReplicator } from "@/lib/src/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
|
||||
export type P2POpenReplicationModalCallback = {
|
||||
onSync: (peerId: string) => Promise<void>;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
// import type { TrysteroReplicator } from "@lib/replication/trystero/TrysteroReplicator";
|
||||
import { LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "@lib/common/types";
|
||||
import { Logger } from "@lib/common/logger";
|
||||
import type { LiveSyncTrysteroReplicator } from "@/lib/src/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import { delay, fireAndForget } from "octagonal-wheels/promises";
|
||||
import P2PServerStatusCard from "./P2PServerStatusCard.svelte";
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { onMount, setContext } from "svelte";
|
||||
import { AutoAccepting, DEFAULT_SETTINGS, type P2PSyncSetting } from "../../../lib/src/common/types";
|
||||
import { AutoAccepting, DEFAULT_SETTINGS, type P2PSyncSetting } from "@lib/common/types";
|
||||
import {
|
||||
AcceptedStatus,
|
||||
ConnectionStatus,
|
||||
type PeerStatus,
|
||||
} from "@lib/replication/trystero/P2PReplicatorPaneCommon";
|
||||
import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import PeerStatusRow from "../P2PReplicator/PeerStatusRow.svelte";
|
||||
import PeerStatusRow from "@/features/P2PSync/P2PReplicator/PeerStatusRow.svelte";
|
||||
import { EVENT_LAYOUT_READY, eventHub } from "@/common/events";
|
||||
import {
|
||||
type PeerInfo,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { LOG_LEVEL_NOTICE, REMOTE_P2P } from "@lib/common/types.ts";
|
||||
import { Logger } from "@lib/common/logger.ts";
|
||||
import { EVENT_P2P_PEER_SHOW_EXTRA_MENU, type PeerStatus } from "@lib/replication/trystero/P2PReplicatorPaneCommon.ts";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
|
||||
import type { P2PPaneParams } from "@/lib/src/replication/trystero/UseP2PReplicatorResult";
|
||||
import type { P2PPaneParams } from "@lib/replication/trystero/UseP2PReplicatorResult";
|
||||
export const VIEW_TYPE_P2P = "p2p-replicator";
|
||||
|
||||
function addToList(item: string, list: string) {
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
EVENT_P2P_REPLICATOR_STATUS,
|
||||
} from "@lib/replication/trystero/TrysteroReplicatorP2PServer";
|
||||
import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents";
|
||||
import type { LiveSyncTrysteroReplicator } from "@/lib/src/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import type { P2PReplicatorStatus } from "@/lib/src/replication/trystero/TrysteroReplicator";
|
||||
import { extractP2PRoomSuffix } from "@/lib/src/common/utils";
|
||||
import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import type { P2PReplicatorStatus } from "@lib/replication/trystero/TrysteroReplicator";
|
||||
import { extractP2PRoomSuffix } from "@lib/common/utils";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { WorkspaceLeaf } from "@/deps.ts";
|
||||
import { mount } from "svelte";
|
||||
import { SvelteItemView } from "@/common/SvelteItemView.ts";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
|
||||
import type { P2PPaneParams } from "@/lib/src/replication/trystero/UseP2PReplicatorResult";
|
||||
import type { P2PPaneParams } from "@lib/replication/trystero/UseP2PReplicatorResult";
|
||||
import P2PServerStatusPane from "./P2PServerStatusPane.svelte";
|
||||
|
||||
export const VIEW_TYPE_P2P_SERVER_STATUS = "p2p-server-status";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
import { AcceptedStatus, type PeerStatus } from "../../../lib/src/replication/trystero/P2PReplicatorPaneCommon";
|
||||
import type { LiveSyncTrysteroReplicator } from "../../../lib/src/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import { eventHub } from "../../../common/events";
|
||||
import { EVENT_P2P_PEER_SHOW_EXTRA_MENU } from "../../../lib/src/replication/trystero/P2PReplicatorPaneCommon";
|
||||
import { AcceptedStatus, type PeerStatus } from "@lib/replication/trystero/P2PReplicatorPaneCommon";
|
||||
import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import { eventHub } from "@/common/events";
|
||||
import { EVENT_P2P_PEER_SHOW_EXTRA_MENU } from "@lib/replication/trystero/P2PReplicatorPaneCommon";
|
||||
|
||||
interface Props {
|
||||
peerStatus: PeerStatus;
|
||||
|
||||
+1
-1
Submodule src/lib updated: 25d23bf738...c926417f82
@@ -3,7 +3,7 @@ import type { AnyEntry, FilePathWithPrefix } from "@lib/common/types";
|
||||
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import { stripAllPrefixes } from "@lib/string_and_binary/path";
|
||||
import { createInstanceLogFunction } from "@lib/services/lib/logUtils";
|
||||
import type { ServiceContext } from "@/lib/src/services/base/ServiceBase";
|
||||
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||
|
||||
export abstract class AbstractModule<
|
||||
T extends LiveSyncBaseCore<ServiceContext, IMinimumLiveSyncCommands> = LiveSyncBaseCore<
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type Prettify } from "../lib/src/common/types";
|
||||
import type { LiveSyncCore } from "../main";
|
||||
import type ObsidianLiveSyncPlugin from "../main";
|
||||
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";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Prettify } from "../lib/src/common/types";
|
||||
import type { LiveSyncCore } from "../main";
|
||||
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];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PeriodicProcessor } from "@/common/PeriodicProcessor";
|
||||
import type { LiveSyncCore } from "../../main";
|
||||
import { AbstractModule } from "../AbstractModule";
|
||||
import type { LiveSyncCore } from "@/main";
|
||||
import { AbstractModule } from "@/modules/AbstractModule";
|
||||
|
||||
export class ModulePeriodicProcess extends AbstractModule {
|
||||
periodicSyncProcessor = new PeriodicProcessor(this.core, async () => await this.services.replication.replicate());
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import type PouchDB from "pouchdb-core";
|
||||
import { fireAndForget } from "octagonal-wheels/promises";
|
||||
import { AbstractModule } from "../AbstractModule";
|
||||
import { AbstractModule } from "@/modules/AbstractModule";
|
||||
import { Logger, LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "octagonal-wheels/common/logger";
|
||||
import { skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
|
||||
import { balanceChunkPurgedDBs } from "@lib/pouchdb/chunks";
|
||||
import { purgeUnreferencedChunks } from "@lib/pouchdb/chunks";
|
||||
import { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator";
|
||||
import { type EntryDoc, type RemoteType } from "../../lib/src/common/types";
|
||||
import { LiveSyncCouchDBReplicator } from "@lib/replication/couchdb/LiveSyncReplicator";
|
||||
import { type EntryDoc, type RemoteType } from "@lib/common/types";
|
||||
|
||||
import { scheduleTask } from "octagonal-wheels/concurrency/task";
|
||||
import { EVENT_FILE_SAVED, EVENT_SETTING_SAVED, eventHub } from "../../common/events";
|
||||
import { EVENT_FILE_SAVED, EVENT_SETTING_SAVED, eventHub } from "@/common/events";
|
||||
|
||||
import { $msg } from "../../lib/src/common/i18n";
|
||||
import type { LiveSyncCore } from "../../main";
|
||||
import { $msg } from "@lib/common/i18n";
|
||||
import type { LiveSyncCore } from "@/main";
|
||||
import { ReplicateResultProcessor } from "./ReplicateResultProcessor";
|
||||
import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager";
|
||||
import { clearHandlers } from "@lib/replication/SyncParamsHandler";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { fireAndForget } from "octagonal-wheels/promises";
|
||||
import { REMOTE_MINIO, REMOTE_P2P, type RemoteDBSettings } from "../../lib/src/common/types";
|
||||
import { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator";
|
||||
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
|
||||
import { AbstractModule } from "../AbstractModule";
|
||||
import type { LiveSyncCore } from "../../main";
|
||||
import { REMOTE_MINIO, REMOTE_P2P, type RemoteDBSettings } from "@lib/common/types";
|
||||
import { LiveSyncCouchDBReplicator } from "@lib/replication/couchdb/LiveSyncReplicator";
|
||||
import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator";
|
||||
import { AbstractModule } from "@/modules/AbstractModule";
|
||||
import type { LiveSyncCore } from "@/main";
|
||||
|
||||
export class ModuleReplicatorCouchDB extends AbstractModule {
|
||||
_anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator | false> {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { REMOTE_MINIO, type RemoteDBSettings } from "../../lib/src/common/types";
|
||||
import { LiveSyncJournalReplicator } from "../../lib/src/replication/journal/LiveSyncJournalReplicator";
|
||||
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
|
||||
import type { LiveSyncCore } from "../../main";
|
||||
import { AbstractModule } from "../AbstractModule";
|
||||
import { REMOTE_MINIO, type RemoteDBSettings } from "@lib/common/types";
|
||||
import { LiveSyncJournalReplicator } from "@lib/replication/journal/LiveSyncJournalReplicator";
|
||||
import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator";
|
||||
import type { LiveSyncCore } from "@/main";
|
||||
import { AbstractModule } from "@/modules/AbstractModule";
|
||||
|
||||
export class ModuleReplicatorMinIO extends AbstractModule {
|
||||
_anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator | false> {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
type MetaEntry,
|
||||
} from "@lib/common/types";
|
||||
import type { ModuleReplicator } from "./ModuleReplicator";
|
||||
import { isChunk } from "@/lib/src/common/typeUtils";
|
||||
import { isChunk } from "@lib/common/typeUtils";
|
||||
import {
|
||||
LOG_LEVEL_DEBUG,
|
||||
LOG_LEVEL_INFO,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { AbstractModule } from "../AbstractModule.ts";
|
||||
import { LOG_LEVEL_NOTICE, type FilePathWithPrefix } from "../../lib/src/common/types";
|
||||
import { AbstractModule } from "@/modules/AbstractModule.ts";
|
||||
import { LOG_LEVEL_NOTICE, type FilePathWithPrefix } from "@lib/common/types";
|
||||
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
|
||||
import { sendValue } from "octagonal-wheels/messagepassing/signal";
|
||||
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
|
||||
import type { LiveSyncCore } from "../../main.ts";
|
||||
import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
|
||||
export class ModuleConflictChecker extends AbstractModule {
|
||||
async _queueConflictCheckIfOpen(file: FilePathWithPrefix): Promise<void> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||
import { AbstractModule } from "../AbstractModule.ts";
|
||||
import { AbstractModule } from "@/modules/AbstractModule.ts";
|
||||
import {
|
||||
AUTO_MERGED,
|
||||
CANCELLED,
|
||||
@@ -10,15 +10,15 @@ import {
|
||||
NOT_CONFLICTED,
|
||||
type diff_check_result,
|
||||
type FilePathWithPrefix,
|
||||
} from "../../lib/src/common/types";
|
||||
} from "@lib/common/types";
|
||||
import { isCustomisationSyncMetadata, isPluginMetadata } from "@lib/common/typeUtils.ts";
|
||||
import { TARGET_IS_NEW } from "@lib/common/models/shared.const.symbols.ts";
|
||||
import { compareMTime, displayRev } from "@lib/common/utils.ts";
|
||||
import diff_match_patch from "diff-match-patch";
|
||||
import { stripAllPrefixes, isPlainText } from "../../lib/src/string_and_binary/path";
|
||||
import { eventHub } from "../../common/events.ts";
|
||||
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
|
||||
import type { LiveSyncCore } from "../../main.ts";
|
||||
import { stripAllPrefixes, isPlainText } from "@lib/string_and_binary/path";
|
||||
import { eventHub } from "@/common/events.ts";
|
||||
import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
|
||||
declare global {
|
||||
interface LSEvents {
|
||||
@@ -182,9 +182,9 @@ export class ModuleConflictResolver extends AbstractModule {
|
||||
revs.map(async (rev) => {
|
||||
const leaf = await this.core.databaseFileAccess.fetchEntryMeta(filename, rev);
|
||||
if (leaf == false) {
|
||||
return [0, rev] as [number, string];
|
||||
return [0, rev];
|
||||
}
|
||||
return [leaf.mtime, rev] as [number, string];
|
||||
return [leaf.mtime, rev];
|
||||
})
|
||||
)),
|
||||
] as [number, string][]
|
||||
|
||||
@@ -10,12 +10,12 @@ import {
|
||||
type RemoteDBSettings,
|
||||
IncompatibleChangesInSpecificPattern,
|
||||
CompatibleButLossyChanges,
|
||||
} from "../../lib/src/common/types.ts";
|
||||
import { escapeMarkdownValue } from "../../lib/src/common/utils.ts";
|
||||
import { AbstractModule } from "../AbstractModule.ts";
|
||||
import { $msg } from "../../lib/src/common/i18n.ts";
|
||||
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
|
||||
import type { LiveSyncCore } from "../../main.ts";
|
||||
} from "@lib/common/types.ts";
|
||||
import { escapeMarkdownValue } from "@lib/common/utils.ts";
|
||||
import { AbstractModule } from "@/modules/AbstractModule.ts";
|
||||
import { $msg } from "@lib/common/i18n.ts";
|
||||
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) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ButtonComponent } from "@/deps.ts";
|
||||
import { App, FuzzySuggestModal, MarkdownRenderer, Modal, Plugin, Setting } from "../../../deps.ts";
|
||||
import { EVENT_PLUGIN_UNLOADED, eventHub } from "../../../common/events.ts";
|
||||
import { App, FuzzySuggestModal, MarkdownRenderer, Modal, Plugin, Setting } from "@/deps.ts";
|
||||
import { EVENT_PLUGIN_UNLOADED, eventHub } from "@/common/events.ts";
|
||||
import { compatGlobal, type CompatIntervalHandle } from "@lib/common/coreEnvFunctions.ts";
|
||||
|
||||
class AutoClosableModal extends Modal {
|
||||
@@ -192,8 +192,10 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
|
||||
const { contentEl } = this;
|
||||
this.titleEl.setText(this.title);
|
||||
const div = contentEl.createDiv();
|
||||
div.style.userSelect = "text";
|
||||
div.style["webkitUserSelect"] = "text";
|
||||
div.setCssStyles({
|
||||
userSelect: "text",
|
||||
webkitUserSelect: "text",
|
||||
});
|
||||
void MarkdownRenderer.render(this.plugin.app, this.contentMd, div, "/", this.plugin);
|
||||
const buttonSetting = new Setting(contentEl);
|
||||
const labelWrapper = contentEl.createDiv();
|
||||
@@ -202,21 +204,23 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
|
||||
labelEl.addClass("sls-dialogue-note-countdown");
|
||||
if (!this.timeout || !this.timer) {
|
||||
labelWrapper.empty();
|
||||
labelWrapper.style.display = "none";
|
||||
labelWrapper.setCssStyles({ display: "none" });
|
||||
}
|
||||
|
||||
buttonSetting.infoEl.style.display = "none";
|
||||
buttonSetting.controlEl.style.flexWrap = "wrap";
|
||||
buttonSetting.infoEl.setCssStyles({ display: "none" });
|
||||
buttonSetting.controlEl.setCssStyles({ flexWrap: "wrap" });
|
||||
if (this.wideButton) {
|
||||
buttonSetting.controlEl.style.flexDirection = "column";
|
||||
buttonSetting.controlEl.style.alignItems = "center";
|
||||
buttonSetting.controlEl.style.justifyContent = "center";
|
||||
buttonSetting.controlEl.style.flexGrow = "1";
|
||||
buttonSetting.controlEl.setCssStyles({
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexGrow: "1",
|
||||
});
|
||||
}
|
||||
contentEl.addEventListener("click", () => {
|
||||
if (this.timer) {
|
||||
labelWrapper.empty();
|
||||
labelWrapper.style.display = "none";
|
||||
labelWrapper.setCssStyles({ display: "none" });
|
||||
compatGlobal.clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
this.defaultButtonComponent?.setButtonText(`${this.defaultAction}`);
|
||||
@@ -238,8 +242,10 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
|
||||
btn.setCta();
|
||||
}
|
||||
if (this.wideButton) {
|
||||
btn.buttonEl.style.flexGrow = "1";
|
||||
btn.buttonEl.style.width = "100%";
|
||||
btn.buttonEl.setCssStyles({
|
||||
flexGrow: "1",
|
||||
width: "100%",
|
||||
});
|
||||
}
|
||||
return btn;
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user