mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-08-29 08:13:31 +02:00
* full commit obsidian * updates on readme and clarification for addons community for easy tag on issues * becouse of https://github.com/alexbelgium/hassio-addons/issues/2966 * restore build.yaml for obsidian addons --------- Co-authored-by: ToledoEM <8144940+ToledoEM@users.noreply.github.com>
225 lines
9.0 KiB
Bash
Executable File
225 lines
9.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# shellcheck shell=bash
|
|
set -Eeuo pipefail
|
|
|
|
# Obsidian LiveSync sync server (CouchDB) as a Home Assistant add-on.
|
|
#
|
|
# CouchDB alone is not usable as a LiveSync backend: the plugin needs a
|
|
# single-node cluster, CORS opened to Obsidian's app origins, authentication
|
|
# required, and raised request/document size limits. This script applies that
|
|
# configuration on every start. The settings mirror the upstream provisioning
|
|
# tool (vrtmrz/obsidian-livesync, utils/couchdb/provision.ts), which is the
|
|
# authoritative source for what LiveSync expects.
|
|
|
|
OPTIONS_JSON="/data/options.json"
|
|
ADDON_DIR="/config/obsidian-syncserver"
|
|
PASSWORD_FILE="${ADDON_DIR}/admin_password"
|
|
DATA_DIR="${ADDON_DIR}/data"
|
|
LOCAL_D="/opt/couchdb/etc/local.d"
|
|
COUCH_URL="http://127.0.0.1:5984"
|
|
|
|
# Retry budget matches provision.ts: CouchDB on a Raspberry Pi can take a
|
|
# while to open its listener on first boot.
|
|
READY_RETRIES=12
|
|
READY_DELAY=5
|
|
|
|
log() { echo "[obsidian-syncserver] $*"; }
|
|
warn() { echo "[obsidian-syncserver] WARN: $*" >&2; }
|
|
die() {
|
|
echo "[obsidian-syncserver] ERROR: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
read_opt() {
|
|
jq -er --arg k "$1" '.[$k]' "$OPTIONS_JSON" 2> /dev/null || true
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Step 1: Read add-on options
|
|
# ---------------------------------------------------------------------------
|
|
[[ -f "$OPTIONS_JSON" ]] || die "Missing options file at ${OPTIONS_JSON}"
|
|
|
|
USERNAME="$(read_opt username)"
|
|
USERNAME="${USERNAME:-admin}"
|
|
PASSWORD="$(read_opt password)"
|
|
DATABASE="$(read_opt database)"
|
|
DATABASE="${DATABASE:-obsidian}"
|
|
LOG_LEVEL="$(read_opt log_level)"
|
|
LOG_LEVEL="${LOG_LEVEL:-info}"
|
|
|
|
# CouchDB database names are restricted; a bad name only fails much later at
|
|
# the create step, with an opaque 400.
|
|
[[ "$DATABASE" =~ ^[a-z][a-z0-9_$()+/-]*$ ]] \
|
|
|| die "database '${DATABASE}' is invalid. Must start with a lowercase letter and contain only a-z 0-9 _ \$ ( ) + / -"
|
|
|
|
mkdir -p "$ADDON_DIR"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Step 2: Resolve admin credentials
|
|
#
|
|
# A blank password auto-generates one and persists it, so the add-on never
|
|
# ships a guessable default. It is reused on later starts, otherwise every
|
|
# restart would invalidate the credentials already configured in Obsidian.
|
|
# ---------------------------------------------------------------------------
|
|
if [[ -z "$PASSWORD" ]]; then
|
|
if [[ -f "$PASSWORD_FILE" ]]; then
|
|
PASSWORD="$(cat "$PASSWORD_FILE")"
|
|
log "Using previously generated admin password from ${PASSWORD_FILE}"
|
|
else
|
|
PASSWORD="$(openssl rand -base64 24)"
|
|
(
|
|
umask 077
|
|
printf '%s\n' "$PASSWORD" > "$PASSWORD_FILE"
|
|
)
|
|
warn "No password set. Generated one and saved it to ${PASSWORD_FILE}"
|
|
warn "Admin username: ${USERNAME}"
|
|
warn "Admin password: ${PASSWORD}"
|
|
warn "Set a password in the add-on options to choose your own."
|
|
fi
|
|
fi
|
|
|
|
export COUCHDB_USER="$USERNAME"
|
|
export COUCHDB_PASSWORD="$PASSWORD"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Step 3: Point CouchDB at persistent storage
|
|
#
|
|
# /data is wiped when the add-on is reinstalled, and is not included in a
|
|
# Home Assistant backup the way the add-on config directory is. The vault is
|
|
# the whole point of this add-on, so it lives under /config instead.
|
|
# ---------------------------------------------------------------------------
|
|
mkdir -p "$DATA_DIR" "${DATA_DIR}/.delayed" "$LOCAL_D"
|
|
|
|
COUCH_UID="$(id -u couchdb 2> /dev/null || echo 5984)"
|
|
COUCH_GID="$(id -g couchdb 2> /dev/null || echo 5984)"
|
|
chown -R "${COUCH_UID}:${COUCH_GID}" "$ADDON_DIR" 2> /dev/null \
|
|
|| warn "Could not chown ${ADDON_DIR}; CouchDB may fail to write to it"
|
|
|
|
cat > "${LOCAL_D}/10-addon-storage.ini" << EOF
|
|
; Managed by the Home Assistant add-on. Edits are overwritten on restart.
|
|
[couchdb]
|
|
database_dir = ${DATA_DIR}
|
|
view_index_dir = ${DATA_DIR}
|
|
|
|
[chttpd]
|
|
bind_address = 0.0.0.0
|
|
port = 5984
|
|
|
|
[log]
|
|
level = ${LOG_LEVEL}
|
|
EOF
|
|
|
|
# Flavour hook: _ssl overrides this to write 20-addon-ssl.ini after validating
|
|
# the user's certificates. No-op in this flavour.
|
|
if declare -f configure_tls > /dev/null 2>&1; then
|
|
configure_tls
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Step 4: Start CouchDB in the background
|
|
# ---------------------------------------------------------------------------
|
|
log "Starting CouchDB (user=${USERNAME}, database=${DATABASE}, log_level=${LOG_LEVEL})"
|
|
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
|
|
COUCH_PID=$!
|
|
|
|
# Without this, a CouchDB that dies during provisioning leaves the script
|
|
# retrying against a socket that will never come up.
|
|
trap 'kill -TERM "$COUCH_PID" 2>/dev/null || true' EXIT INT TERM
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Step 5: Wait for CouchDB to accept requests
|
|
# ---------------------------------------------------------------------------
|
|
ready=false
|
|
for i in $(seq 1 "$READY_RETRIES"); do
|
|
if curl -fsS -u "${USERNAME}:${PASSWORD}" "${COUCH_URL}/_up" > /dev/null 2>&1; then
|
|
ready=true
|
|
break
|
|
fi
|
|
kill -0 "$COUCH_PID" 2> /dev/null || die "CouchDB exited during startup. See the log above."
|
|
log "Waiting for CouchDB to come up (${i}/${READY_RETRIES})"
|
|
sleep "$READY_DELAY"
|
|
done
|
|
[[ "$ready" == "true" ]] || die "CouchDB did not become ready after $((READY_RETRIES * READY_DELAY))s"
|
|
|
|
log "CouchDB is up, applying Obsidian LiveSync configuration"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Step 6: Provision for LiveSync
|
|
#
|
|
# Every call below is idempotent, so this runs safely on each start and
|
|
# repairs configuration that was changed by hand in Fauxton.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Promotes the single node out of the uninitialised state. A node that is
|
|
# already set up answers 400/409 with "already"/"finished", which is success
|
|
# here, not an error.
|
|
cluster_body="$(jq -nc \
|
|
--arg u "$USERNAME" --arg p "$PASSWORD" \
|
|
'{action:"enable_single_node",username:$u,password:$p,bind_address:"0.0.0.0",port:5984,singlenode:true}')"
|
|
|
|
cluster_response="$(curl -sS -u "${USERNAME}:${PASSWORD}" \
|
|
-X POST "${COUCH_URL}/_cluster_setup" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$cluster_body" \
|
|
-w '\n%{http_code}' 2>&1 || true)"
|
|
cluster_code="$(printf '%s' "$cluster_response" | tail -n1)"
|
|
cluster_text="$(printf '%s' "$cluster_response" | sed '$d')"
|
|
|
|
case "$cluster_code" in
|
|
2*) log "Single-node cluster initialised" ;;
|
|
400 | 409)
|
|
if printf '%s' "$cluster_text" | rg -qi 'already|finished'; then
|
|
log "Single-node cluster already initialised"
|
|
else
|
|
die "Cluster setup failed (HTTP ${cluster_code}): ${cluster_text}"
|
|
fi
|
|
;;
|
|
*) die "Cluster setup failed (HTTP ${cluster_code}): ${cluster_text}" ;;
|
|
esac
|
|
|
|
# CORS origins are what let the Obsidian desktop app and the mobile app talk
|
|
# to CouchDB at all; without them the plugin fails with an opaque network
|
|
# error. Values are taken from provision.ts.
|
|
set_config() {
|
|
local label="$1" key="$2" value="$3" code
|
|
code="$(curl -sS -o /dev/null -w '%{http_code}' \
|
|
-u "${USERNAME}:${PASSWORD}" \
|
|
-X PUT "${COUCH_URL}/_node/_local/_config/${key}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$value" 2>&1 || true)"
|
|
case "$code" in
|
|
2*) log " set ${label}" ;;
|
|
*) die "Failed to ${label} (HTTP ${code}) at ${key}" ;;
|
|
esac
|
|
}
|
|
|
|
set_config "require authenticated HTTP users" "chttpd/require_valid_user" '"true"'
|
|
set_config "require authenticated HTTP users for authentication" "chttpd_auth/require_valid_user" '"true"'
|
|
set_config "the HTTP authentication challenge" "httpd/WWW-Authenticate" '"Basic realm=\"couchdb\""'
|
|
set_config "enable HTTP CORS" "httpd/enable_cors" '"true"'
|
|
set_config "enable clustered HTTP CORS" "chttpd/enable_cors" '"true"'
|
|
set_config "the maximum HTTP request size" "chttpd/max_http_request_size" '"4294967296"'
|
|
set_config "the maximum document size" "couchdb/max_document_size" '"50000000"'
|
|
set_config "enable CORS credentials" "cors/credentials" '"true"'
|
|
set_config "allowed CORS origins" "cors/origins" '"app://obsidian.md,capacitor://localhost,http://localhost"'
|
|
|
|
# 412 means the database is already there, which is the normal case on every
|
|
# start after the first.
|
|
db_code="$(curl -sS -o /dev/null -w '%{http_code}' \
|
|
-u "${USERNAME}:${PASSWORD}" \
|
|
-X PUT "${COUCH_URL}/$(printf '%s' "$DATABASE" | jq -sRr @uri)" 2>&1 || true)"
|
|
case "$db_code" in
|
|
2*) log "Created database '${DATABASE}'" ;;
|
|
412) log "Database '${DATABASE}' already exists" ;;
|
|
*) die "Failed to create database '${DATABASE}' (HTTP ${db_code})" ;;
|
|
esac
|
|
|
|
log "Ready. Point Obsidian Self-hosted LiveSync at this server."
|
|
log " database: ${DATABASE} username: ${USERNAME}"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Step 7: Hand the container's lifetime back to CouchDB
|
|
# ---------------------------------------------------------------------------
|
|
trap - EXIT
|
|
wait "$COUCH_PID"
|