Skip to content

Commit a802f2f

Browse files
astrostlclaude
andauthored
fix: pack sync — skip installed packs, silence failures, show progress summary (#453)
- Skip already-installed packs (verified via on-disk checksums) instead of re-downloading every time - Probe manifest URL before creating local directories — no cleanup needed on failure - Stop downloading remaining sounds on first failure (partial packs retry next run) - Replace noisy warnings with per-pack status: ✓ (installed), ✅ (downloaded), ⚠️ (partial), ❌ (unavailable) - Hide cursor during progress bars to prevent terminal rendering artifacts - Print summary with counts and itemized partial/failed packs - Show total disk usage at the end - Fix pre-existing bug: checksum lookup for filenames with spaces (e.g. `incoming (1).mp3`) used `split(' ', 1)` which broke on the first space, causing packs like tf2_spy to re-download every run Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8413d0d commit a802f2f

2 files changed

Lines changed: 160 additions & 53 deletions

File tree

scripts/pack-download.sh

Lines changed: 158 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
# Used by install.sh and `peon packs install`
44
set -euo pipefail
55

6+
# Restore cursor on exit (in case we hid it for progress bars)
7+
trap '[ -t 1 ] && printf "\033[?25h"' EXIT
8+
69
REGISTRY_URL="https://peonping.github.io/registry/index.json"
710

811
# MSYS2/MinGW: Windows Python can't read /c/... paths — convert to C:/... via cygpath
@@ -93,7 +96,7 @@ is_cached_valid() {
9396
[ -s "$filepath" ] || return 1
9497
[ -f "$checksums_file" ] || return 1
9598
local stored_hash current_hash
96-
stored_hash=$(grep -F "$filename " "$checksums_file" 2>/dev/null | head -1 | cut -d' ' -f2)
99+
stored_hash=$(grep -F "$filename " "$checksums_file" 2>/dev/null | head -1 | rev | cut -d' ' -f1 | rev)
97100
[ -n "$stored_hash" ] || return 1
98101
current_hash=$(file_sha256 "$filepath")
99102
[ "$stored_hash" = "$current_hash" ]
@@ -110,6 +113,7 @@ store_checksum() {
110113
mv "$checksums_file.tmp" "$checksums_file"
111114
}
112115

116+
113117
# --- Progress bar ---
114118

115119
draw_progress() {
@@ -134,7 +138,7 @@ draw_progress() {
134138
size_str="$bytes B"
135139
fi
136140

137-
printf "\r [%${idx_width}d/%d] %-20s [%s] %d/%d (%s)%-10s" \
141+
printf "\r [%${idx_width}d/%d] %-20s [%s] %d/%d (%s)%-16s" \
138142
"$pidx" "$ptotal" "$pname" "$bar" "$cur" "$total" "$size_str" ""
139143
}
140144

@@ -288,18 +292,60 @@ IS_TTY=false
288292
TOTAL_DOWNLOAD_FILES=0
289293
TOTAL_DOWNLOAD_BYTES=0
290294
TOTAL_DOWNLOAD_PACKS=0
295+
TOTAL_SKIPPED_PACKS=0
296+
TOTAL_FAILED_PACKS=0
297+
FAILED_PACK_NAMES=()
298+
PARTIAL_PACK_NAMES=()
299+
TOTAL_PARTIAL_PACKS=0
291300

292301
echo ""
293-
echo "Downloading packs..."
302+
echo "Syncing packs..."
294303
for pack in $PACKS; do
295304
if ! is_safe_pack_name "$pack"; then
296-
echo " Warning: skipping invalid pack name: $pack" >&2
297305
continue
298306
fi
299307

300308
PACK_INDEX=$((PACK_INDEX + 1))
301-
302-
mkdir -p "$PEON_DIR/packs/$pack/sounds"
309+
idx_width=${#TOTAL_PACKS}
310+
311+
# Skip packs where every manifest sound has a valid checksum
312+
if [ -s "$PEON_DIR/packs/$pack/openpeon.json" ] && [ -f "$PEON_DIR/packs/$pack/.checksums" ]; then
313+
manifest_check="$(py_path "$PEON_DIR/packs/$pack/openpeon.json")"
314+
checksums_check="$PEON_DIR/packs/$pack/.checksums"
315+
pack_complete=$(CHECKSUMS="$checksums_check" PACKS_DIR="$(py_path "$PEON_DIR/packs/$pack")" python3 -c "
316+
import json, os, posixpath, hashlib
317+
m = json.load(open('$manifest_check'))
318+
checksums = {}
319+
cf = os.environ['CHECKSUMS']
320+
packs_dir = os.environ['PACKS_DIR']
321+
if os.path.isfile(cf):
322+
for line in open(cf):
323+
parts = line.strip().rsplit(' ', 1)
324+
if len(parts) == 2:
325+
checksums[parts[0]] = parts[1]
326+
seen = set()
327+
for cat in m.get('categories', {}).values():
328+
for s in cat.get('sounds', []):
329+
f = s['file']
330+
rel = f[len('sounds/'):] if f.startswith('sounds/') else posixpath.basename(f)
331+
seen.add(rel)
332+
def is_valid(rel):
333+
stored = checksums.get(rel)
334+
if not stored:
335+
return False
336+
fp = os.path.join(packs_dir, 'sounds', rel)
337+
if not os.path.isfile(fp):
338+
return False
339+
actual = hashlib.sha256(open(fp, 'rb').read()).hexdigest()
340+
return actual == stored
341+
print('yes' if seen and all(is_valid(r) for r in seen) else 'no')
342+
" 2>/dev/null || echo "no")
343+
if [ "$pack_complete" = "yes" ]; then
344+
TOTAL_SKIPPED_PACKS=$((TOTAL_SKIPPED_PACKS + 1))
345+
printf " [%${idx_width}d/%d] %s ✓\n" "$PACK_INDEX" "$TOTAL_PACKS" "$pack"
346+
continue
347+
fi
348+
fi
303349

304350
# Get source info from registry (or use fallback)
305351
SOURCE_REPO=""
@@ -344,9 +390,21 @@ for p in data.get('packs', []):
344390
PACK_BASE="https://raw.githubusercontent.com/$SOURCE_REPO/$SOURCE_REF"
345391
fi
346392

393+
# Verify manifest exists before creating any local directories
394+
if ! curl -fsSL --head "$PACK_BASE/openpeon.json" >/dev/null 2>&1; then
395+
TOTAL_FAILED_PACKS=$((TOTAL_FAILED_PACKS + 1))
396+
FAILED_PACK_NAMES+=("$pack")
397+
printf " [%${idx_width}d/%d] %s ❌\n" "$PACK_INDEX" "$TOTAL_PACKS" "$pack"
398+
continue
399+
fi
400+
401+
mkdir -p "$PEON_DIR/packs/$pack/sounds"
402+
347403
# Download manifest
348404
if ! curl -fsSL "$PACK_BASE/openpeon.json" -o "$PEON_DIR/packs/$pack/openpeon.json" 2>/dev/null; then
349-
echo " Warning: failed to download manifest for $pack" >&2
405+
TOTAL_FAILED_PACKS=$((TOTAL_FAILED_PACKS + 1))
406+
FAILED_PACK_NAMES+=("$pack")
407+
printf " [%${idx_width}d/%d] %s ❌\n" "$PACK_INDEX" "$TOTAL_PACKS" "$pack"
350408
continue
351409
fi
352410

@@ -393,30 +451,23 @@ print(len(seen))
393451
while read -r ifile; do
394452
ifile="${ifile%$'\r'}" # strip Windows CRLF trailing CR (Python on Windows outputs \r\n)
395453
[ -z "$ifile" ] && continue
396-
if ! is_safe_filename "$ifile"; then
397-
echo " Warning: skipped unsafe icon path in $pack: $ifile" >&2
398-
continue
399-
fi
454+
is_safe_filename "$ifile" || continue
400455
mkdir -p "$PEON_DIR/packs/$pack/$(dirname "$ifile")"
401-
if ! curl -fsSL "$PACK_BASE/$(urlencode_filename "$ifile")" \
402-
-o "$PEON_DIR/packs/$pack/$ifile" </dev/null 2>/dev/null; then
403-
echo " Warning: failed to download $pack/$ifile" >&2
404-
fi
456+
curl -fsSL "$PACK_BASE/$(urlencode_filename "$ifile")" \
457+
-o "$PEON_DIR/packs/$pack/$ifile" </dev/null 2>/dev/null || true
405458
done <<< "$ICON_LIST"
406459
fi
407460

408461
if [ "$IS_TTY" = true ] && [ "$SOUND_COUNT" != "?" ]; then
409462
local_file_count=0
410463
local_byte_count=0
411464

465+
printf '\033[?25l' # hide cursor during progress
412466
draw_progress "$PACK_INDEX" "$TOTAL_PACKS" "$pack" 0 "$SOUND_COUNT" 0
413467

414468
while read -r sfile; do
415469
sfile="${sfile%$'\r'}" # strip Windows CRLF trailing CR (Python on Windows outputs \r\n)
416-
if ! is_safe_filename "$sfile"; then
417-
echo " Warning: skipped unsafe filename in $pack: $sfile" >&2
418-
continue
419-
fi
470+
is_safe_filename "$sfile" || continue
420471
mkdir -p "$PEON_DIR/packs/$pack/sounds/$(dirname "$sfile")"
421472
if is_cached_valid "$PEON_DIR/packs/$pack/sounds/$sfile" "$CHECKSUMS_FILE" "$sfile"; then
422473
local_file_count=$((local_file_count + 1))
@@ -429,7 +480,7 @@ print(len(seen))
429480
fsize=$(wc -c < "$PEON_DIR/packs/$pack/sounds/$sfile" | tr -d ' ')
430481
local_byte_count=$((local_byte_count + fsize))
431482
else
432-
echo " Warning: failed to download $pack/sounds/$sfile" >&2
483+
break
433484
fi
434485
draw_progress "$PACK_INDEX" "$TOTAL_PACKS" "$pack" \
435486
"$local_file_count" "$SOUND_COUNT" "$local_byte_count"
@@ -449,17 +500,39 @@ for cat in m.get('categories', {}).values():
449500
print(rel)
450501
")
451502

452-
draw_progress "$PACK_INDEX" "$TOTAL_PACKS" "$pack" \
453-
"$local_file_count" "$SOUND_COUNT" "$local_byte_count"
454-
printf "\n"
503+
# Clear the progress bar line and print final status on a clean line
504+
printf '\033[?25h' # restore cursor
505+
printf "\r%80s\r" ""
506+
if [ "$local_file_count" -eq "$SOUND_COUNT" ]; then
507+
printf " [%${idx_width}d/%d] %s ✅\n" "$PACK_INDEX" "$TOTAL_PACKS" "$pack"
508+
TOTAL_DOWNLOAD_PACKS=$((TOTAL_DOWNLOAD_PACKS + 1))
509+
else
510+
printf " [%${idx_width}d/%d] %s (%d/%d) ⚠️\n" "$PACK_INDEX" "$TOTAL_PACKS" "$pack" "$local_file_count" "$SOUND_COUNT"
511+
TOTAL_PARTIAL_PACKS=$((TOTAL_PARTIAL_PACKS + 1))
512+
PARTIAL_PACK_NAMES+=("$pack")
513+
fi
455514

456515
TOTAL_DOWNLOAD_FILES=$((TOTAL_DOWNLOAD_FILES + local_file_count))
457516
TOTAL_DOWNLOAD_BYTES=$((TOTAL_DOWNLOAD_BYTES + local_byte_count))
458-
TOTAL_DOWNLOAD_PACKS=$((TOTAL_DOWNLOAD_PACKS + 1))
459517
else
460-
printf " [%d/%d] %s " "$PACK_INDEX" "$TOTAL_PACKS" "$pack"
518+
printf " [%${idx_width}d/%d] %s " "$PACK_INDEX" "$TOTAL_PACKS" "$pack"
519+
local_file_count=0
461520

462-
python3 -c "
521+
while read -r sfile; do
522+
sfile="${sfile%$'\r'}" # strip Windows CRLF trailing CR (Python on Windows outputs \r\n)
523+
is_safe_filename "$sfile" || continue
524+
mkdir -p "$PEON_DIR/packs/$pack/sounds/$(dirname "$sfile")"
525+
if is_cached_valid "$PEON_DIR/packs/$pack/sounds/$sfile" "$CHECKSUMS_FILE" "$sfile"; then
526+
printf "."
527+
local_file_count=$((local_file_count + 1))
528+
elif curl -fsSL "$PACK_BASE/sounds/$(urlencode_filename "$sfile")" -o "$PEON_DIR/packs/$pack/sounds/$sfile" </dev/null 2>/dev/null; then
529+
store_checksum "$CHECKSUMS_FILE" "$sfile" "$PEON_DIR/packs/$pack/sounds/$sfile"
530+
printf "."
531+
local_file_count=$((local_file_count + 1))
532+
else
533+
break
534+
fi
535+
done < <(python3 -c "
463536
import json, posixpath
464537
m = json.load(open('$manifest_py'))
465538
seen = set()
@@ -473,37 +546,70 @@ for cat in m.get('categories', {}).values():
473546
if rel not in seen:
474547
seen.add(rel)
475548
print(rel)
476-
" | while read -r sfile; do
477-
sfile="${sfile%$'\r'}" # strip Windows CRLF trailing CR (Python on Windows outputs \r\n)
478-
if ! is_safe_filename "$sfile"; then
479-
echo " Warning: skipped unsafe filename in $pack: $sfile" >&2
480-
continue
481-
fi
482-
mkdir -p "$PEON_DIR/packs/$pack/sounds/$(dirname "$sfile")"
483-
if is_cached_valid "$PEON_DIR/packs/$pack/sounds/$sfile" "$CHECKSUMS_FILE" "$sfile"; then
484-
printf "."
485-
elif curl -fsSL "$PACK_BASE/sounds/$(urlencode_filename "$sfile")" -o "$PEON_DIR/packs/$pack/sounds/$sfile" </dev/null 2>/dev/null; then
486-
store_checksum "$CHECKSUMS_FILE" "$sfile" "$PEON_DIR/packs/$pack/sounds/$sfile"
487-
printf "."
488-
else
489-
printf "x"
490-
echo " Warning: failed to download $pack/sounds/$sfile" >&2
491-
fi
492-
done
549+
")
493550

494-
printf " %s sounds\n" "$SOUND_COUNT"
495-
TOTAL_DOWNLOAD_PACKS=$((TOTAL_DOWNLOAD_PACKS + 1))
551+
if [ "$SOUND_COUNT" != "?" ] && [ "$local_file_count" -eq "$SOUND_COUNT" ]; then
552+
printf " ✅ %s sounds\n" "$SOUND_COUNT"
553+
TOTAL_DOWNLOAD_PACKS=$((TOTAL_DOWNLOAD_PACKS + 1))
554+
else
555+
printf " ⚠️%s/%s sounds\n" "$local_file_count" "$SOUND_COUNT"
556+
TOTAL_PARTIAL_PACKS=$((TOTAL_PARTIAL_PACKS + 1))
557+
PARTIAL_PACK_NAMES+=("$pack")
558+
fi
496559
fi
497560
done
498561

499-
if [ "$IS_TTY" = true ] && [ "$TOTAL_DOWNLOAD_PACKS" -gt 0 ]; then
500-
if [ "$TOTAL_DOWNLOAD_BYTES" -ge 1048576 ]; then
501-
SUMMARY_SIZE="$(( TOTAL_DOWNLOAD_BYTES / 1048576 )).$(( (TOTAL_DOWNLOAD_BYTES % 1048576) * 10 / 1048576 )) MB"
502-
elif [ "$TOTAL_DOWNLOAD_BYTES" -ge 1024 ]; then
503-
SUMMARY_SIZE="$(( TOTAL_DOWNLOAD_BYTES / 1024 )) KB"
562+
# --- Summary ---
563+
echo ""
564+
SUMMARY_PARTS=()
565+
if [ "$TOTAL_DOWNLOAD_PACKS" -gt 0 ]; then
566+
if [ "$IS_TTY" = true ] && [ "$TOTAL_DOWNLOAD_BYTES" -gt 0 ]; then
567+
if [ "$TOTAL_DOWNLOAD_BYTES" -ge 1048576 ]; then
568+
SUMMARY_SIZE="$(( TOTAL_DOWNLOAD_BYTES / 1048576 )).$(( (TOTAL_DOWNLOAD_BYTES % 1048576) * 10 / 1048576 )) MB"
569+
elif [ "$TOTAL_DOWNLOAD_BYTES" -ge 1024 ]; then
570+
SUMMARY_SIZE="$(( TOTAL_DOWNLOAD_BYTES / 1024 )) KB"
571+
else
572+
SUMMARY_SIZE="$TOTAL_DOWNLOAD_BYTES B"
573+
fi
574+
SUMMARY_PARTS+=("$TOTAL_DOWNLOAD_PACKS downloaded ($TOTAL_DOWNLOAD_FILES files, $SUMMARY_SIZE)")
504575
else
505-
SUMMARY_SIZE="$TOTAL_DOWNLOAD_BYTES B"
576+
SUMMARY_PARTS+=("$TOTAL_DOWNLOAD_PACKS downloaded")
506577
fi
578+
fi
579+
if [ "$TOTAL_SKIPPED_PACKS" -gt 0 ]; then
580+
SUMMARY_PARTS+=("$TOTAL_SKIPPED_PACKS already installed")
581+
fi
582+
if [ "$TOTAL_PARTIAL_PACKS" -gt 0 ]; then
583+
SUMMARY_PARTS+=("⚠️ $TOTAL_PARTIAL_PACKS partial")
584+
fi
585+
if [ "$TOTAL_FAILED_PACKS" -gt 0 ]; then
586+
SUMMARY_PARTS+=("$TOTAL_FAILED_PACKS unavailable")
587+
fi
588+
if [ "${#SUMMARY_PARTS[@]}" -gt 0 ]; then
589+
IFS=" " ; echo "${SUMMARY_PARTS[*]}" ; unset IFS
590+
fi
591+
if [ "${#PARTIAL_PACK_NAMES[@]}" -gt 0 ]; then
507592
echo ""
508-
echo "Downloaded $TOTAL_DOWNLOAD_PACKS packs ($TOTAL_DOWNLOAD_FILES files, $SUMMARY_SIZE)"
593+
echo "Partial downloads (some sounds unavailable):"
594+
for p in "${PARTIAL_PACK_NAMES[@]}"; do echo " - $p"; done
595+
fi
596+
if [ "${#FAILED_PACK_NAMES[@]}" -gt 0 ]; then
597+
echo ""
598+
echo "Failed downloads (manifest unavailable):"
599+
for p in "${FAILED_PACK_NAMES[@]}"; do echo " - $p"; done
600+
fi
601+
602+
# Report total disk usage for all packs
603+
PACKS_PATH="$PEON_DIR/packs"
604+
# Resolve symlink to show actual storage location
605+
PACKS_REAL="$(cd "$PACKS_PATH" 2>/dev/null && pwd -P)"
606+
DISK_BYTES=$(du -sk "$PACKS_REAL" 2>/dev/null | cut -f1)
607+
if [ -n "$DISK_BYTES" ] && [ "$DISK_BYTES" -gt 0 ]; then
608+
DISK_MB=$(( DISK_BYTES / 1024 ))
609+
echo ""
610+
if [ "$PACKS_REAL" != "$PACKS_PATH" ]; then
611+
echo "Disk usage: ${DISK_MB} MB ($PACKS_REAL)"
612+
else
613+
echo "Disk usage: ${DISK_MB} MB"
614+
fi
509615
fi

tests/pack-download.bats

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ JSON
109109
@test "invalid pack name is skipped" {
110110
run bash "$PACK_DL_SH" --dir="$TEST_DIR" --packs="../etc/passwd"
111111
[ "$status" -eq 0 ]
112-
[[ "$output" == *"skipping invalid"* ]] || [[ "$(cat "$TEST_DIR/stderr.log" 2>/dev/null)" == *"skipping invalid"* ]]
112+
# Invalid pack name should be silently skipped — no directory created
113+
[ ! -d "$TEST_DIR/packs/../etc/passwd" ]
113114
}
114115

115116
@test "missing --dir shows error" {

0 commit comments

Comments
 (0)