Sync Obsidian both ways so notes Simba writes reach your devices #86

Merged
ryan merged 1 commits from feat/obsidian-write-sync into main 2026-10-13 08:11:47 -04:00
6 changed files with 44 additions and 20 deletions
+4
View File
@@ -195,6 +195,10 @@ OBSIDIAN_E2E_PASSWORD=
OBSIDIAN_DEVICE_NAME=simbarag OBSIDIAN_DEVICE_NAME=simbarag
# Set to true to run continuous sync in the background # Set to true to run continuous sync in the background
OBSIDIAN_CONTINUOUS_SYNC=false OBSIDIAN_CONTINUOUS_SYNC=false
# bidirectional: notes and journal tasks Simba writes sync back to your devices.
# pull-only: download only, Simba's writes stay in the container's copy.
# mirror-remote: download only and revert local changes.
OBSIDIAN_SYNC_MODE=bidirectional
OBSIDIAN_SYNC_INTERVAL=60 OBSIDIAN_SYNC_INTERVAL=60
# Local path to Obsidian vault (where files are synced) # Local path to Obsidian vault (where files are synced)
OBSIDIAN_VAULT_PATH=/app/data/obsidian OBSIDIAN_VAULT_PATH=/app/data/obsidian
+8
View File
@@ -225,6 +225,14 @@ def validate_configuration(env: Mapping[str, str], *, production: bool = True) -
problem(flag, "signature validation must remain enabled in production") problem(flag, "signature validation must remain enabled in production")
for name in ("METRICS_TOKEN", "CRON_SECRET", "EMAIL_HMAC_SECRET"): for name in ("METRICS_TOKEN", "CRON_SECRET", "EMAIL_HMAC_SECRET"):
secret(name, minimum=32) secret(name, minimum=32)
if env.get("OBSIDIAN_SYNC_MODE", "bidirectional") not in {
"bidirectional",
"pull-only",
"mirror-remote",
}:
problem(
"OBSIDIAN_SYNC_MODE", "must be bidirectional, pull-only or mirror-remote"
)
if env.get("OBSIDIAN_CONTINUOUS_SYNC") == "true": if env.get("OBSIDIAN_CONTINUOUS_SYNC") == "true":
for name in ("OBSIDIAN_EMAIL", "OBSIDIAN_PASSWORD", "OBSIDIAN_VAULT_ID"): for name in ("OBSIDIAN_EMAIL", "OBSIDIAN_PASSWORD", "OBSIDIAN_VAULT_ID"):
if not value(name): if not value(name):
+1
View File
@@ -61,6 +61,7 @@ x-raggr-common: &raggr-common
- OBSIDIAN_E2E_PASSWORD=${OBSIDIAN_E2E_PASSWORD} - OBSIDIAN_E2E_PASSWORD=${OBSIDIAN_E2E_PASSWORD}
- OBSIDIAN_DEVICE_NAME=${OBSIDIAN_DEVICE_NAME} - OBSIDIAN_DEVICE_NAME=${OBSIDIAN_DEVICE_NAME}
- OBSIDIAN_CONTINUOUS_SYNC=${OBSIDIAN_CONTINUOUS_SYNC:-false} - OBSIDIAN_CONTINUOUS_SYNC=${OBSIDIAN_CONTINUOUS_SYNC:-false}
- OBSIDIAN_SYNC_MODE=${OBSIDIAN_SYNC_MODE:-bidirectional}
- OBSIDIAN_VAULT_PATH=${OBSIDIAN_VAULT_PATH:-/app/data/obsidian} - OBSIDIAN_VAULT_PATH=${OBSIDIAN_VAULT_PATH:-/app/data/obsidian}
- OBSIDIAN_VAULT_NAME=${OBSIDIAN_VAULT_NAME:-} - OBSIDIAN_VAULT_NAME=${OBSIDIAN_VAULT_NAME:-}
- S3_ENDPOINT_URL=${S3_ENDPOINT_URL} - S3_ENDPOINT_URL=${S3_ENDPOINT_URL}
+1 -1
View File
@@ -39,7 +39,7 @@ make logs
make migrate-history make migrate-history
``` ```
Startup validates configuration, runs `aerich upgrade`, optionally sets up Obsidian pull-only sync, and replaces the shell with the Python process. **A migration failure stops startup; the application is never launched.** Obsidian login/setup failures still log a warning and allow the application to continue. Startup validates configuration, runs `aerich upgrade`, optionally starts Obsidian sync in the background (`OBSIDIAN_SYNC_MODE`, default `bidirectional` so notes and tasks Simba writes reach your devices; containers take turns via a lock file in the shared vault), and replaces the shell with the Python process. **A migration failure stops startup; the application is never launched.** Obsidian login/setup failures still log a warning and allow the application to continue.
An empty database without Aerich metadata may need explicit initialization. Inspect its state first. Run the migration tool in a one-off container because the app cannot start until migrations pass: An empty database without Aerich metadata may need explicit initialization. Inspect its state first. Run the migration tool in a one-off container because the app cannot start until migrations pass:
+28 -19
View File
@@ -19,27 +19,36 @@ if [ "${OBSIDIAN_CONTINUOUS_SYNC:-false}" = "true" ]; then
if [ -z "${OBSIDIAN_EMAIL:-}" ] || [ -z "${OBSIDIAN_PASSWORD:-}" ] || [ -z "${OBSIDIAN_VAULT_ID:-}" ]; then if [ -z "${OBSIDIAN_EMAIL:-}" ] || [ -z "${OBSIDIAN_PASSWORD:-}" ] || [ -z "${OBSIDIAN_VAULT_ID:-}" ]; then
echo "WARNING: OBSIDIAN_EMAIL, OBSIDIAN_PASSWORD, or OBSIDIAN_VAULT_ID not set. Skipping sync." echo "WARNING: OBSIDIAN_EMAIL, OBSIDIAN_PASSWORD, or OBSIDIAN_VAULT_ID not set. Skipping sync."
else else
echo "Setting up Obsidian sync..."
VAULT_PATH="${OBSIDIAN_VAULT_PATH:-/app/data/obsidian}" VAULT_PATH="${OBSIDIAN_VAULT_PATH:-/app/data/obsidian}"
SYNC_MODE="${OBSIDIAN_SYNC_MODE:-bidirectional}"
mkdir -p "${VAULT_PATH}"
# Login and setup sync (foreground, must complete before sync starts) # Blue and green share the vault volume. flock on a file in it works
if ob login --email "${OBSIDIAN_EMAIL}" --password "${OBSIDIAN_PASSWORD}" && \ # across containers, so this container only sets up and runs sync once
ob sync-setup \ # the previous container's sync process has exited. Two clients writing
--vault "${OBSIDIAN_VAULT_ID}" \ # one vault and its sync state would otherwise race. Setup runs in the
--path "${VAULT_PATH}" \ # background so the app starts while it waits.
--password "${OBSIDIAN_E2E_PASSWORD:-}" \ (
--device-name "${OBSIDIAN_DEVICE_NAME:-simbarag}"; then flock 9
# Remove stale lock from previous container run echo "Setting up Obsidian sync (${SYNC_MODE})..."
rm -rf "${VAULT_PATH}/.obsidian/.sync.lock" if ob login --email "${OBSIDIAN_EMAIL}" --password "${OBSIDIAN_PASSWORD}" && \
# Set sync to pull-only (read-only) mode ob sync-setup \
ob sync-config --mode pull-only --path "${VAULT_PATH}" --vault "${OBSIDIAN_VAULT_ID}" \
# Start continuous sync in background --path "${VAULT_PATH}" \
echo "Starting Obsidian continuous sync (pull-only)..." --password "${OBSIDIAN_E2E_PASSWORD:-}" \
ob sync --continuous --path "${VAULT_PATH}" & --device-name "${OBSIDIAN_DEVICE_NAME:-simbarag}"; then
else # Safe now: the lock means no other container's sync is running.
echo "WARNING: Obsidian sync setup failed. Continuing without sync." rm -rf "${VAULT_PATH}/.obsidian/.sync.lock"
fi if ob sync-config --mode "${SYNC_MODE}" --conflict-strategy merge \
--path "${VAULT_PATH}"; then
echo "Starting Obsidian continuous sync (${SYNC_MODE})..."
exec ob sync --continuous --path "${VAULT_PATH}"
fi
echo "WARNING: Obsidian sync-config failed. Continuing without sync."
else
echo "WARNING: Obsidian sync setup failed. Continuing without sync."
fi
) 9>"${VAULT_PATH}/.raggr-sync.lock" &
fi fi
fi fi
+2
View File
@@ -63,6 +63,7 @@ def test_missing_required_settings(production_env, name):
("CHECKPOINT_DB_PATH", "/data/invalid\x00.db"), ("CHECKPOINT_DB_PATH", "/data/invalid\x00.db"),
("CONVERSATION_SUMMARY_DB_PATH", ""), ("CONVERSATION_SUMMARY_DB_PATH", ""),
("OBSIDIAN_CONTINUOUS_SYNC", "yes"), ("OBSIDIAN_CONTINUOUS_SYNC", "yes"),
("OBSIDIAN_SYNC_MODE", "push-only"),
], ],
) )
def test_malformed_and_unsafe_settings(production_env, name, value): def test_malformed_and_unsafe_settings(production_env, name, value):
@@ -202,6 +203,7 @@ def test_compose_forwards_runtime_settings():
"CHECKPOINT_DB_PATH", "CHECKPOINT_DB_PATH",
"CONVERSATION_SUMMARY_DB_PATH", "CONVERSATION_SUMMARY_DB_PATH",
"OBSIDIAN_SYNC_INTERVAL", "OBSIDIAN_SYNC_INTERVAL",
"OBSIDIAN_SYNC_MODE",
"OIDC_AUTHORIZATION_ENDPOINT", "OIDC_AUTHORIZATION_ENDPOINT",
"OIDC_TOKEN_ENDPOINT", "OIDC_TOKEN_ENDPOINT",
"OIDC_USERINFO_ENDPOINT", "OIDC_USERINFO_ENDPOINT",