#!/bin/bash
#
# Hypernote Restore — Import notes and media from a portable backup
#
# Usage:
# ./scripts/restore.sh [BACKUP_DIR] [BASE_URL]
#
# Reads the backup structure created by backup.sh and pushes everything
# back into hypernote via the API.
set -euo pipefail
BACKUP="${1:-$HOME/hypernote-backups/2026-04-06-pre-compaction}"
BASE_URL="${2:-http://localhost:4040/hypernote}"
BASE_URL="${BASE_URL%/}"
echo "=== Hypernote Restore ==="
echo "Backup: $BACKUP"
echo "Target: $BASE_URL"
echo ""
if [ ! -d "$BACKUP/notebooks" ]; then
echo "ERROR: No notebooks directory in $BACKUP"
exit 1
fi
if ! curl -sf "$BASE_URL/api/health" > /dev/null 2>&1; then
echo "ERROR: Cannot reach $BASE_URL/api/health"
exit 1
fi
echo "WARNING: This will create notebooks and notes that don't already exist."
echo "Existing notes with the same ID will be SKIPPED (not overwritten)."
echo ""
read -p "Continue? [y/N] " confirm
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
echo "Aborted."
exit 0
fi
TOTAL_NOTES=0
TOTAL_MEDIA=0
SKIPPED=0
for NB_DIR in "$BACKUP"/notebooks/*/; do
SLUG=$(basename "$NB_DIR")
echo ""
echo "=== Notebook: $SLUG ==="
# Read notebook metadata
META_FILE="$NB_DIR/metadata.json"
if [ -f "$META_FILE" ]; then
NAME=$(python3 -c "import json; m=json.load(open('$META_FILE')); print(m.get('name','$SLUG'))" 2>/dev/null || echo "$SLUG")
DESC=$(python3 -c "import json; m=json.load(open('$META_FILE')); print(m.get('description',''))" 2>/dev/null || echo "")
VIS=$(python3 -c "import json; m=json.load(open('$META_FILE')); print(m.get('visibility','public'))" 2>/dev/null || echo "public")
else
NAME="$SLUG"
DESC=""
VIS="public"
fi
# Check if notebook exists
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" "$BASE_URL/api/notebooks/$SLUG" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" != "200" ]; then
echo " Creating notebook: $SLUG"
curl -sf -X POST "$BASE_URL/api/notebooks" \
-H "Content-Type: application/json" \
-d "{\"slug\":\"$SLUG\",\"name\":$(python3 -c "import json; print(json.dumps('$NAME'))"),\"description\":$(python3 -c "import json; print(json.dumps('$DESC'))"),\"visibility\":\"$VIS\"}" \
> /dev/null 2>&1 || echo " WARN: Failed to create notebook $SLUG (may need auth)"
else
echo " Notebook exists: $SLUG"
fi
# Restore notes
if [ -d "$NB_DIR/notes" ]; then
for META in "$NB_DIR"/notes/*.meta.json; do
[ -f "$META" ] || continue
NOTE_ID=$(python3 -c "import json; print(json.load(open('$META')).get('id',''))" 2>/dev/null)
NOTE_SLUG=$(python3 -c "import json; print(json.load(open('$META')).get('slug',''))" 2>/dev/null)
TITLE=$(python3 -c "import json; print(json.load(open('$META')).get('title','Untitled'))" 2>/dev/null)
TYPE=$(python3 -c "import json; print(json.load(open('$META')).get('type','markdown'))" 2>/dev/null)
TAGS=$(python3 -c "import json; print(json.dumps(json.load(open('$META')).get('tags',[])))" 2>/dev/null)
# Check if note already exists
if [ -n "$NOTE_SLUG" ]; then
EXISTS=$(curl -sf -o /dev/null -w "%{http_code}" "$BASE_URL/api/notebooks/$SLUG/notes/$NOTE_SLUG" 2>/dev/null || echo "000")
elif [ -n "$NOTE_ID" ]; then
EXISTS=$(curl -sf -o /dev/null -w "%{http_code}" "$BASE_URL/api/notebooks/$SLUG/notes/$NOTE_ID" 2>/dev/null || echo "000")
else
EXISTS="000"
fi
if [ "$EXISTS" = "200" ]; then
SKIPPED=$((SKIPPED + 1))
continue
fi
# Read content file
BASE_NAME=$(basename "$META" .meta.json)
if [ -f "$NB_DIR/notes/$BASE_NAME.md" ]; then
CONTENT_FILE="$NB_DIR/notes/$BASE_NAME.md"
elif [ -f "$NB_DIR/notes/$BASE_NAME.json" ]; then
CONTENT_FILE="$NB_DIR/notes/$BASE_NAME.json"
else
echo " WARN: No content file for $BASE_NAME"
continue
fi
# Create note via API
python3 -c "
import json, sys
meta = json.load(open('$META'))
with open('$CONTENT_FILE') as f:
content = f.read()
payload = {
'title': meta.get('title', 'Untitled'),
'content': content,
'tags': meta.get('tags', []),
'type': meta.get('type', 'markdown')
}
print(json.dumps(payload))
" 2>/dev/null | curl -sf -X POST "$BASE_URL/api/notebooks/$SLUG/notes" \
-H "Content-Type: application/json" \
-d @- > /dev/null 2>&1
if [ $? -eq 0 ]; then
TOTAL_NOTES=$((TOTAL_NOTES + 1))
echo " + $TITLE"
else
echo " FAIL: $TITLE (may need auth)"
fi
done
fi
# Restore media
if [ -d "$NB_DIR/media" ]; then
for MEDIA_FILE in "$NB_DIR"/media/*; do
[ -f "$MEDIA_FILE" ] || continue
FILENAME=$(basename "$MEDIA_FILE")
HASH="${FILENAME%.*}" # Strip extension
# Check if already exists
EXISTS=$(curl -sf -o /dev/null -w "%{http_code}" "$BASE_URL/api/notebooks/$SLUG/media/$HASH" 2>/dev/null || echo "000")
if [ "$EXISTS" = "200" ]; then continue; fi
# Detect content type from extension
case "$FILENAME" in
*.png) CT="image/png" ;;
*.jpg) CT="image/jpeg" ;;
*.gif) CT="image/gif" ;;
*.webp) CT="image/webp" ;;
*.mp4) CT="video/mp4" ;;
*.webm) CT="video/webm" ;;
*.mp3) CT="audio/mpeg" ;;
*) CT="application/octet-stream" ;;
esac
curl -sf -X POST "$BASE_URL/api/notebooks/$SLUG/media" \
-H "Content-Type: $CT" \
--data-binary "@$MEDIA_FILE" > /dev/null 2>&1
if [ $? -eq 0 ]; then
TOTAL_MEDIA=$((TOTAL_MEDIA + 1))
fi
done
fi
done
# Also restore from global media-all directory
if [ -d "$BACKUP/media-all" ]; then
echo ""
echo "=== Restoring global media ==="
# Pick first notebook as target for orphan media
FIRST_SLUG=$(ls "$BACKUP/notebooks/" | head -1)
for MEDIA_FILE in "$BACKUP"/media-all/*; do
[ -f "$MEDIA_FILE" ] || continue
FILENAME=$(basename "$MEDIA_FILE")
HASH="${FILENAME%.*}"
EXISTS=$(curl -sf -o /dev/null -w "%{http_code}" "$BASE_URL/api/notebooks/$FIRST_SLUG/media/$HASH" 2>/dev/null || echo "000")
if [ "$EXISTS" = "200" ]; then continue; fi
case "$FILENAME" in
*.png) CT="image/png" ;;
*.jpg) CT="image/jpeg" ;;
*.gif) CT="image/gif" ;;
*.webp) CT="image/webp" ;;
*.mp4) CT="video/mp4" ;;
*.webm) CT="video/webm" ;;
*) CT="application/octet-stream" ;;
esac
curl -sf -X POST "$BASE_URL/api/notebooks/$FIRST_SLUG/media" \
-H "Content-Type: $CT" \
--data-binary "@$MEDIA_FILE" > /dev/null 2>&1 && TOTAL_MEDIA=$((TOTAL_MEDIA + 1))
done
fi
echo ""
echo "=== Restore Complete ==="
echo "Notes created: $TOTAL_NOTES"
echo "Notes skipped: $SKIPPED (already exist)"
echo "Media uploaded: $TOTAL_MEDIA"