Files
obsidian-wiki/scripts/package-linux-migration.sh
T
nac 777607dc29 chore(repo): establish project baseline
Track the source-of-truth code, skills, bootstrap rules, docs, and packaging files before adding new features.

Exclude local vault config, generated skill mirrors, temporary exports, virtual environments, and business data files from version control.

Validation: inspected staged file list and git ignored status before commit.
2026-07-05 11:03:40 +00:00

358 lines
9.0 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Build a portable Linux migration bundle for an obsidian-wiki installation.
#
# The bundle contains:
# - obsidian-wiki/ this repository, excluding local caches and symlink mirrors
# - vault/ the configured vault, including _sources/ and _meta/
# - deploy-linux-server.sh
# - server.env.example, README-LINUX-MIGRATION.md, bundle-info.env, SHA256SUMS
#
# It does not bundle the QMD cache. QMD is rebuilt on the server.
set -Eeuo pipefail
IFS=$'\n\t'
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEFAULT_REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
REPO_DIR="$DEFAULT_REPO_DIR"
VAULT_DIR=""
OUT_DIR="$DEFAULT_REPO_DIR/dist"
BUNDLE_NAME=""
REBUILD_FILE_INDEX=1
RUN_BOT_AUDIT=1
WITHOUT_SOURCES=0
KEEP_STAGING=0
DRY_RUN=0
usage() {
cat <<'EOF'
Usage:
scripts/package-linux-migration.sh [options]
Options:
--vault PATH Vault path to package. Defaults to OBSIDIAN_VAULT_PATH
from repo .env, then ~/.obsidian-wiki/config.
--repo PATH obsidian-wiki repo path. Defaults to this script's parent.
--out DIR Output directory for the .tar.gz bundle. Default: ./dist
--name NAME Bundle directory/archive name. Default: obsidian-wiki-linux-<UTC>
--skip-file-index Do not rebuild _meta/file-assets.sqlite before packaging.
--skip-bot-audit Do not run bot-view audit before packaging.
--without-sources Exclude vault/_sources/archive from the bundle. This is
only for small test bundles; citations/file lookup suffer.
--keep-staging Keep the temporary expanded bundle directory after tar.
--dry-run Print resolved paths and checks, but do not copy or tar.
-h, --help Show this help.
Example:
scripts/package-linux-migration.sh \
--vault "/Users/nactang/repo/knowlege Base/Knowlege Base" \
--out /Users/nactang/Desktop/wiki-migration
EOF
}
die() {
echo "ERROR: $*" >&2
exit 1
}
info() {
echo "[INFO] $*"
}
warn() {
echo "[WARN] $*" >&2
}
clean_config_value() {
local value="$1"
value="${value#\"}"
value="${value%\"}"
value="${value#\'}"
value="${value%\'}"
printf '%s' "$value"
}
expand_path() {
local path="$1"
case "$path" in
"~") printf '%s' "$HOME" ;;
"~/"*) printf '%s/%s' "$HOME" "${path#~/}" ;;
*) printf '%s' "$path" ;;
esac
}
read_config_key() {
local file="$1"
local key="$2"
[[ -f "$file" ]] || return 1
awk -v key="$key" '
index($0, key "=") == 1 {
sub("^[^=]*=", "", $0)
print $0
exit
}
' "$file"
}
resolve_vault_dir() {
if [[ -n "$VAULT_DIR" ]]; then
clean_config_value "$VAULT_DIR"
return
fi
local raw=""
raw="$(read_config_key "$REPO_DIR/.env" "OBSIDIAN_VAULT_PATH" || true)"
if [[ -z "$raw" ]]; then
raw="$(read_config_key "$HOME/.obsidian-wiki/config" "OBSIDIAN_VAULT_PATH" || true)"
fi
clean_config_value "$raw"
}
require_command() {
local cmd="$1"
command -v "$cmd" >/dev/null 2>&1 || die "missing command: $cmd"
}
human_size() {
du -sh "$1" 2>/dev/null | awk '{print $1}'
}
file_count() {
find "$1" -type f 2>/dev/null | wc -l | tr -d ' '
}
run_obsidian_wiki() {
(cd "$REPO_DIR" && PYTHONPATH="$REPO_DIR${PYTHONPATH:+:$PYTHONPATH}" python3 -m obsidian_wiki "$@")
}
while [[ $# -gt 0 ]]; do
case "$1" in
--vault)
[[ $# -ge 2 ]] || die "--vault requires a path"
VAULT_DIR="$2"
shift 2
;;
--repo)
[[ $# -ge 2 ]] || die "--repo requires a path"
REPO_DIR="$2"
shift 2
;;
--out)
[[ $# -ge 2 ]] || die "--out requires a directory"
OUT_DIR="$2"
shift 2
;;
--name)
[[ $# -ge 2 ]] || die "--name requires a value"
BUNDLE_NAME="$2"
shift 2
;;
--skip-file-index)
REBUILD_FILE_INDEX=0
shift
;;
--skip-bot-audit)
RUN_BOT_AUDIT=0
shift
;;
--without-sources)
WITHOUT_SOURCES=1
shift
;;
--keep-staging)
KEEP_STAGING=1
shift
;;
--dry-run)
DRY_RUN=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown option: $1"
;;
esac
done
REPO_DIR="$(expand_path "$REPO_DIR")"
REPO_DIR="$(cd "$REPO_DIR" && pwd)"
VAULT_DIR="$(resolve_vault_dir)"
VAULT_DIR="$(expand_path "$VAULT_DIR")"
OUT_DIR="$(expand_path "$OUT_DIR")"
[[ -d "$REPO_DIR/obsidian_wiki" ]] || die "repo path does not look like obsidian-wiki: $REPO_DIR"
[[ -n "$VAULT_DIR" ]] || die "vault path is not set; pass --vault PATH"
[[ -d "$VAULT_DIR" ]] || die "vault path does not exist: $VAULT_DIR"
[[ -f "$VAULT_DIR/index.md" ]] || warn "vault has no index.md: $VAULT_DIR"
[[ -d "$VAULT_DIR/_meta" ]] || warn "vault has no _meta directory: $VAULT_DIR"
[[ -d "$VAULT_DIR/_sources" ]] || warn "vault has no _sources directory: $VAULT_DIR"
require_command python3
require_command rsync
require_command tar
require_command shasum
if [[ -z "$BUNDLE_NAME" ]]; then
BUNDLE_NAME="obsidian-wiki-linux-$(date -u +%Y%m%dT%H%M%SZ)"
fi
mkdir -p "$OUT_DIR"
OUT_DIR="$(cd "$OUT_DIR" && pwd)"
info "repo: $REPO_DIR"
info "vault: $VAULT_DIR"
info "vault size: $(human_size "$VAULT_DIR")"
info "vault files: $(file_count "$VAULT_DIR")"
info "out: $OUT_DIR"
info "bundle: $BUNDLE_NAME.tar.gz"
if [[ "$WITHOUT_SOURCES" -eq 1 ]]; then
warn "--without-sources is enabled; _sources/archive will not be packaged."
fi
if [[ "$DRY_RUN" -eq 1 ]]; then
info "dry run complete; no files copied."
exit 0
fi
if [[ "$REBUILD_FILE_INDEX" -eq 1 ]]; then
info "rebuilding file asset index before packaging..."
run_obsidian_wiki file-index rebuild --vault "$VAULT_DIR"
fi
if [[ "$RUN_BOT_AUDIT" -eq 1 ]]; then
info "auditing bot-view ACLs before packaging..."
run_obsidian_wiki bot-view audit --vault "$VAULT_DIR"
fi
WORK_DIR="$OUT_DIR/.staging-$BUNDLE_NAME"
STAGE_DIR="$WORK_DIR/$BUNDLE_NAME"
rm -rf "$WORK_DIR"
mkdir -p "$STAGE_DIR"
info "copying repo..."
rsync -a \
--exclude '/.git/' \
--exclude '/.venv/' \
--exclude '/dist/' \
--exclude '/build/' \
--exclude '/.mypy_cache/' \
--exclude '/.pytest_cache/' \
--exclude '/__pycache__/' \
--exclude '*.pyc' \
--exclude '.DS_Store' \
--exclude '/.claude/skills/' \
--exclude '/.cursor/skills/' \
--exclude '/.windsurf/skills/' \
--exclude '/.agents/skills/' \
--exclude '/.pi/skills/' \
--exclude '/.kiro/skills/' \
"$REPO_DIR/" "$STAGE_DIR/obsidian-wiki/"
info "copying vault..."
VAULT_RSYNC_EXCLUDES=(
--exclude '.DS_Store'
--exclude '.trash/'
--exclude '.obsidian/workspace*'
)
if [[ "$WITHOUT_SOURCES" -eq 1 ]]; then
VAULT_RSYNC_EXCLUDES+=(--exclude '_sources/archive/')
fi
rsync -a "${VAULT_RSYNC_EXCLUDES[@]}" "$VAULT_DIR/" "$STAGE_DIR/vault/"
cp "$REPO_DIR/scripts/deploy-linux-server.sh" "$STAGE_DIR/deploy-linux-server.sh"
chmod +x "$STAGE_DIR/deploy-linux-server.sh"
cat > "$STAGE_DIR/server.env.example" <<'EOF'
# Copy values from here into deploy options if you want non-default paths.
INSTALL_DIR=/opt/obsidian-wiki
VAULT_DIR=/srv/bjd-vault
BOT_VIEW_DIR=/srv/bjd-bot-views
QMD_WIKI_COLLECTION=bjd-wiki
QMD_CLI_SEARCH_MODE=balanced
SOURCE_FILE_PROFILES=quality-bot,engineering-bot
EOF
cat > "$STAGE_DIR/README-LINUX-MIGRATION.md" <<'EOF'
# obsidian-wiki Linux migration bundle
This bundle is meant for a headless Linux server. Obsidian Desktop is not
required on the server.
## Deploy
Unpack the archive on the server, then run:
```bash
cd obsidian-wiki-linux-*
sudo bash deploy-linux-server.sh \
--install-dir /opt/obsidian-wiki \
--vault-dir /srv/bjd-vault \
--bot-view-dir /srv/bjd-bot-views
```
For a user-local deployment without sudo:
```bash
bash deploy-linux-server.sh \
--install-dir "$HOME/obsidian-wiki" \
--vault-dir "$HOME/bjd-vault" \
--bot-view-dir "$HOME/bjd-bot-views"
```
The deploy script copies the repo and vault, writes config, rebuilds the SQLite
file index, audits bot ACLs, exports bot views, and rebuilds QMD collections if
`qmd` is installed.
EOF
cat > "$STAGE_DIR/bundle-info.env" <<EOF
BUNDLE_CREATED_UTC="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
SOURCE_REPO="$REPO_DIR"
SOURCE_VAULT="$VAULT_DIR"
VAULT_SIZE_HUMAN="$(human_size "$VAULT_DIR")"
VAULT_FILE_COUNT="$(file_count "$VAULT_DIR")"
WITHOUT_SOURCES="$WITHOUT_SOURCES"
EOF
info "writing checksums..."
(
cd "$STAGE_DIR"
: > SHA256SUMS
find . -type f ! -name SHA256SUMS -print | LC_ALL=C sort | while IFS= read -r file; do
shasum -a 256 "$file"
done > SHA256SUMS
)
BUNDLE_TAR="$OUT_DIR/$BUNDLE_NAME.tar.gz"
rm -f "$BUNDLE_TAR" "$BUNDLE_TAR.sha256"
info "creating tarball..."
(
cd "$WORK_DIR"
COPYFILE_DISABLE=1 tar -czf "$BUNDLE_TAR" "$BUNDLE_NAME"
)
shasum -a 256 "$BUNDLE_TAR" > "$BUNDLE_TAR.sha256"
if [[ "$KEEP_STAGING" -eq 0 ]]; then
rm -rf "$WORK_DIR"
else
info "kept staging dir: $STAGE_DIR"
fi
info "bundle ready: $BUNDLE_TAR"
info "checksum: $BUNDLE_TAR.sha256"
cat <<EOF
Next:
scp "$BUNDLE_TAR" user@server:/tmp/
ssh user@server
cd /tmp && tar -xzf "$(basename "$BUNDLE_TAR")"
cd "$BUNDLE_NAME"
sudo bash deploy-linux-server.sh --install-dir /opt/obsidian-wiki --vault-dir /srv/bjd-vault --bot-view-dir /srv/bjd-bot-views
EOF