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.
358 lines
9.0 KiB
Bash
Executable File
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
|