105 lines
3.6 KiB
Bash
Executable File
105 lines
3.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# export_closets.sh — Privacy-safe export of wizard closets for fleet sync.
|
|
#
|
|
# Exports ONLY closet (summary) files from a wizard's local MemPalace to
|
|
# a bundle directory suitable for rsync to the shared Alpha fleet palace.
|
|
#
|
|
# POLICY: Raw drawers (full-text source content) NEVER leave the local VPS.
|
|
# Only closets (compressed summaries) are exported.
|
|
#
|
|
# Usage:
|
|
# ./mempalace/export_closets.sh [palace_dir] [export_dir]
|
|
#
|
|
# Defaults:
|
|
# palace_dir — $MEMPALACE_DIR or /root/wizards/bezalel/.mempalace/palace
|
|
# export_dir — /tmp/mempalace_export_closets
|
|
#
|
|
# After export, sync with:
|
|
# rsync -avz --delete /tmp/mempalace_export_closets/ alpha:/var/lib/mempalace/fleet/bezalel/
|
|
#
|
|
# Refs: #1083, #1075
|
|
|
|
set -euo pipefail
|
|
|
|
PALACE_DIR="${1:-${MEMPALACE_DIR:-/root/wizards/bezalel/.mempalace/palace}}"
|
|
EXPORT_DIR="${2:-/tmp/mempalace_export_closets}"
|
|
WIZARD="${MEMPALACE_WING:-bezalel}"
|
|
|
|
echo "[export_closets] Wizard: $WIZARD"
|
|
echo "[export_closets] Palace: $PALACE_DIR"
|
|
echo "[export_closets] Export: $EXPORT_DIR"
|
|
|
|
if [[ ! -d "$PALACE_DIR" ]]; then
|
|
echo "[export_closets] ERROR: palace not found: $PALACE_DIR" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Validate closets-only policy: abort if any raw drawer files are present in export scope.
|
|
# Closets are files named *.closet.json or stored under a closets/ subdirectory.
|
|
# Raw drawers are everything else (*.drawer.json, *.md source files, etc.).
|
|
|
|
DRAWER_COUNT=0
|
|
while IFS= read -r -d '' f; do
|
|
# Raw drawer check: any .json file that is NOT a closet
|
|
basename_f="$(basename "$f")"
|
|
if [[ "$basename_f" == *.drawer.json ]]; then
|
|
echo "[export_closets] POLICY VIOLATION: raw drawer found in export scope: $f" >&2
|
|
DRAWER_COUNT=$((DRAWER_COUNT + 1))
|
|
fi
|
|
done < <(find "$PALACE_DIR" -type f -name "*.json" -print0 2>/dev/null)
|
|
|
|
if [[ "$DRAWER_COUNT" -gt 0 ]]; then
|
|
echo "[export_closets] ABORT: $DRAWER_COUNT raw drawer(s) detected. Only closets may be exported." >&2
|
|
echo "[export_closets] Run mempalace compress to generate closets before exporting." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Also check for source_file metadata in closet JSON that would expose private paths.
|
|
SOURCE_FILE_LEAKS=0
|
|
while IFS= read -r -d '' f; do
|
|
if python3 -c "
|
|
import json, sys
|
|
try:
|
|
data = json.load(open('$f'))
|
|
drawers = data.get('drawers', []) if isinstance(data, dict) else []
|
|
for d in drawers:
|
|
if 'source_file' in d and not d.get('closet', False):
|
|
sys.exit(1)
|
|
except Exception:
|
|
pass
|
|
sys.exit(0)
|
|
" 2>/dev/null; then
|
|
:
|
|
else
|
|
echo "[export_closets] POLICY VIOLATION: source_file metadata in non-closet: $f" >&2
|
|
SOURCE_FILE_LEAKS=$((SOURCE_FILE_LEAKS + 1))
|
|
fi
|
|
done < <(find "$PALACE_DIR" -type f -name "*.closet.json" -print0 2>/dev/null)
|
|
|
|
if [[ "$SOURCE_FILE_LEAKS" -gt 0 ]]; then
|
|
echo "[export_closets] ABORT: $SOURCE_FILE_LEAKS file(s) contain private source_file paths." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Collect closet files
|
|
mkdir -p "$EXPORT_DIR/$WIZARD"
|
|
CLOSET_COUNT=0
|
|
while IFS= read -r -d '' f; do
|
|
rel_path="${f#$PALACE_DIR/}"
|
|
dest="$EXPORT_DIR/$WIZARD/$rel_path"
|
|
mkdir -p "$(dirname "$dest")"
|
|
cp "$f" "$dest"
|
|
CLOSET_COUNT=$((CLOSET_COUNT + 1))
|
|
done < <(find "$PALACE_DIR" -type f -name "*.closet.json" -print0 2>/dev/null)
|
|
|
|
if [[ "$CLOSET_COUNT" -eq 0 ]]; then
|
|
echo "[export_closets] WARNING: no closet files found in $PALACE_DIR" >&2
|
|
echo "[export_closets] Run 'mempalace compress' to generate closets from drawers." >&2
|
|
exit 0
|
|
fi
|
|
|
|
echo "[export_closets] Exported $CLOSET_COUNT closet(s) to $EXPORT_DIR/$WIZARD/"
|
|
echo "[export_closets] OK — ready for fleet sync."
|
|
echo ""
|
|
echo " rsync -avz --delete $EXPORT_DIR/$WIZARD/ alpha:/var/lib/mempalace/fleet/$WIZARD/"
|