Skip to content

Instantly share code, notes, and snippets.

@ParkWardRR
Created December 31, 2025 20:18
Show Gist options
  • Select an option

  • Save ParkWardRR/9f904b17214ec0e428a86321e9547840 to your computer and use it in GitHub Desktop.

Select an option

Save ParkWardRR/9f904b17214ec0e428a86321e9547840 to your computer and use it in GitHub Desktop.
#!/bin/bash
# =============================================================================
# Synology DSM Archive Extractor v2.2
#
# PURPOSE: Extracts ZIP/RAR archives from a file list, validates CRC-32 checksums,
# deletes originals only after 100% validation. Handles multipart RARs.
#
# FEATURES:
# - Progress bar with real-time stats
# - CRC-32 validation of ALL extracted files
# - Multipart RAR support (.part1.rar, .r00/.r01 volumes)
# - Collision-safe output folders
# - Dry-run mode for testing
# - Color-coded verbose logging
# - Screen/tmux/nohup ready for disconnects
#
# USAGE EXAMPLES:
# ./extract_archives.sh # Normal run
# DRY_RUN=1 ./extract_archives.sh # Test without changes
# VALIDATE_CRC=0 ./extract_archives.sh # Skip CRC validation
# nohup ./extract_archives.sh & # Background + disconnect safe
# screen -S extract ./extract_archives.sh # Screen session
#
# REQUIREMENTS (Vanilla DSM):
# 1. unzip (usually present)
# 2. unrar OR 7z (install via Entware - see below)
# 3. crc32 (Entware: opkg install coreutils-crc32-full)
#
# DSM TOOL INSTALL (5 minutes):
# 1. SSH to DSM as admin
# 2. curl -O https://bin.entware.net/x64-k3.10/installer/generic.sh
# 3. sh generic.sh
# 4. opkg update
# 5. opkg install p7zip-full unrar-free coreutils-crc32-full
#
# FILE LIST FORMAT: Full paths, one per line (like Pic.txt)
# /volume1/archives/foo.zip
# /volume1/archives/bar.part1.rar
#
# OUTPUT STRUCTURE:
# foo.zip -> foo/ (validated files)
# bar.part1.rar -> bar/ (validated files, all parts deleted)
#
# SAFETY:
# - Never deletes originals unless CRC-32 100% match
# - Keeps failed extracts + originals
# - Logs everything with timestamps
# - Skips non-first multipart RARs automatically
#
# AUTHOR: Production-grade DSM automation script
# VERSION: 2.2 (CRC-validated, multipart-safe)
# =============================================================================
set -euo pipefail
# =============================================================================
# CONFIGURATION
# =============================================================================
readonly LIST_FILE="/volume2/MonterosaSync/Storage/Other/Alpine/Meta/Pic.txt"
readonly LOG_DIR="/volume2/MonterosaSync/Storage/Other/Alpine/Meta"
readonly LOG_FILE="${LOG_DIR}/extract_$(date +%Y%m%d_%H%M%S).log"
readonly DRY_RUN="${DRY_RUN:-0}"
readonly VALIDATE_CRC="${VALIDATE_CRC:-1}"
readonly VERBOSE="${VERBOSE:-1}"
# Colors for terminal
readonly RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m' NC='\033[0m'
# Global vars
EXTRACTOR=""
# =============================================================================
# CORE FUNCTIONS
# =============================================================================
log() {
local level="$1"; shift
printf '[%s] %s%-8s%s %s\n' "$(date '+%H:%M:%S')" "${!level}" "$level" "$NC" "$*" | tee -a "$LOG_FILE"
}
info() { log BLUE "INFO"; }
warn() { log YELLOW "WARN"; }
error() { log RED "ERROR"; }
success() { log GREEN "SUCCESS"; }
progress_bar() {
local current="$1" total="$2" width=50
local percent=$((current * 100 / total))
local filled=$(( (current * width) / total ))
printf '\r\033[K[%s%s] %d/%d (%d%%)' "$(printf '#%.0s' $(seq 1 $filled))" "$(printf ' %.0s' $(seq 1 $((width - filled))))" "$current" "$total" "$percent"
}
check_tools() {
info "Checking extraction tools availability"
if ! command -v crc32 >/dev/null 2>&1; then
error "crc32 required for validation. Install: opkg install coreutils-crc32-full"
exit 1
fi
if command -v 7z >/dev/null 2>&1; then
EXTRACTOR="7z"
info "Using 7z (recommended)"
elif command -v unzip >/dev/null 2>&1 && command -v unrar >/dev/null 2>&1; then
EXTRACTOR="fallback"
info "Using unzip + unrar"
else
error "Missing tools. Need: 7z OR (unzip + unrar), plus crc32"
error "See header comments for Entware install instructions"
exit 1
fi
}
count_archives() {
grep -c -E '\.(zip|rar)$' "$LIST_FILE" 2>/dev/null || echo "0"
}
normalize_path() {
local path="$1"
if [[ ! "$path" =~ ^/ ]]; then
echo "/$path"
else
echo "$path"
fi
}
get_unique_outdir() {
local archive="$1"
local base="${archive%.*}"
if [[ ! -e "$base" ]]; then
echo "$base"
return
fi
local counter=1
while [[ -e "${base}_v${counter}" ]]; do
((counter++))
done
echo "${base}_v${counter}"
}
extract_archive() {
local archive="$1" outdir="$2"
info "Extracting $archive to $outdir"
mkdir -p "$outdir"
if [[ "$EXTRACTOR" == "7z" ]]; then
if ! 7z x -y "-o$outdir" "$archive" >>"$LOG_FILE" 2>&1; then
warn "7z extraction failed for $archive"
return 1
fi
else
local lower_archive="${archive,,}"
if [[ "$lower_archive" == *.zip ]]; then
unzip -q "$archive" -d "$outdir" >>"$LOG_FILE" 2>&1 || return 1
else
unrar x -o+ -r "$archive" "$outdir/" >>"$LOG_FILE" 2>&1 || return 1
fi
fi
# Quick sanity check
if [[ -z "$(ls -A "$outdir")" ]]; then
error "Extraction produced empty directory"
return 1
fi
}
get_expected_crcs_7z() {
local archive="$1" outdir="$2"
local tmpfile="${outdir}/.expected_crcs.tmp"
7z l "$archive" 2>/dev/null | awk '
/CRC / {
filename = $4
crc = $5
gsub(/.*[\\\/]/, "", filename) # strip path
print filename ":" crc
}
END { exit 0 }
' | sort -u > "$tmpfile"
[[ -s "$tmpfile" ]] && mv "$tmpfile" "${outdir}/.expected_crcs"
}
get_expected_crcs_fallback() {
local outdir="$1"
find "$outdir" -type f -print0 | while IFS= read -r -d '' file; do
basename_file="${file##*/}"
crc=$(crc32 "$file" 2>/dev/null || echo "NO_CRC")
printf '%s:%s\n' "$basename_file" "$crc"
done | sort -u > "${outdir}/.expected_crcs"
}
validate_crc_extraction() {
local outdir="$1"
local expected="${outdir}/.expected_crcs"
info "Computing CRC-32 for $(find "$outdir" -type f | wc -l) extracted files"
# Generate actual CRCs
find "$outdir" -type f -print0 | while IFS= read -r -d '' file; do
basename_file="${file##*/}"
crc=$(crc32 "$file" 2>/dev/null || echo "NO_CRC")
printf '%s:%s\n' "$basename_file" "$crc"
done | sort -u > "${outdir}/.actual_crcs"
# Compare (ignoring files only in expected - harmless)
local mismatch_count=$(comm -13 <(sort "$expected") <(sort "${outdir}/.actual_crcs") | wc -l)
if [[ $mismatch_count -eq 0 ]]; then
local validated_files=$(wc -l < "$expected" 2>/dev/null || echo "0")
success "CRC validation passed: $validated_files files match"
return 0
else
error "CRC validation failed: $mismatch_count files mismatch"
comm -13 <(sort "$expected") <(sort "${outdir}/.actual_crcs") | head -10 >> "$LOG_FILE"
return 1
fi
}
safe_delete_archive() {
local archive="$1"
local lower="${archive,,}"
info "Deleting validated archive: $archive"
if [[ "$lower" =~ \.part[0-9]+\.rar$ ]]; then
# Multipart RAR set
local prefix="${archive%.[Pp][Aa][Rr][Tt]*}"
local deleted=$(rm -f "${prefix}".part*.rar 2>/dev/null && echo "multipart set")
success "Deleted $deleted"
elif [[ "$lower" == *.rar ]]; then
# RAR volumes (archive.rar + archive.r00, r01...)
local stem="${archive%.[Rr][Aa][Rr]}"
if compgen -G "${stem}.r??" >/dev/null 2>&1; then
rm -f "$archive" "${stem}.r"?? && success "Deleted RAR + volumes"
else
rm -f "$archive" && success "Deleted single RAR"
fi
else
# ZIP or single file
rm -f "$archive" && success "Deleted $archive"
fi
}
# =============================================================================
# MAIN EXECUTION
# =============================================================================
main() {
clear
printf '\nDSM Archive Extractor v2.2\n'
printf 'Log file: %s\n' "$LOG_FILE"
printf 'List file: %s\n' "$LIST_FILE"
# Prerequisites
[[ ! -f "$LIST_FILE" ]] && {
error "List file not found: $LIST_FILE"
exit 1
}
[[ "$DRY_RUN" == "1" ]] && warn "DRY RUN MODE - no extraction or deletion"
[[ "$VALIDATE_CRC" != "1" ]] && warn "CRC VALIDATION DISABLED"
check_tools
local total_archives=$(count_archives)
info "Processing $total_archives archives from list"
local processed_count=0 success_count=0 failed_count=0
while IFS= read -r line || [[ -n "$line" ]]; do
line=$(normalize_path "$line")
[[ -z "$line" || ! "$line" =~ \.(zip|rar)$ ]] && continue
((processed_count++))
progress_bar "$processed_count" "$total_archives"
# Skip missing files
[[ ! -f "$line" ]] && {
warn "File missing: $line"
((failed_count++))
continue
}
# Skip non-first parts of multipart RARs
if [[ "${line,,}" =~ \.part([0-9]+)\.rar$ ]]; then
if [[ ${BASH_REMATCH[1]} != "1" && ${BASH_REMATCH[1]} != "01" ]]; then
info "Skipping part ${BASH_REMATCH[1]}: $line"
continue
fi
fi
local output_dir=$(get_unique_outdir "$line")
if [[ "$DRY_RUN" == "1" ]]; then
info "DRY: would process $line -> $output_dir"
continue
fi
# Extract phase
if ! extract_archive "$line" "$output_dir"; then
warn "Extraction failed: $line"
((failed_count++))
continue
fi
# CRC validation phase
if [[ "$VALIDATE_CRC" == "1" ]]; then
if [[ "$EXTRACTOR" == "7z" ]]; then
get_expected_crcs_7z "$line" "$output_dir"
else
get_expected_crcs_fallback "$output_dir"
fi
if ! validate_crc_extraction "$output_dir"; then
warn "CRC validation failed: $line (kept archive)"
((failed_count++))
continue
fi
fi
# Safe deletion phase
safe_delete_archive "$line"
((success_count++))
# Cleanup temp files
rm -f "$output_dir/.{expected,actual}_crcs"
done < "$LIST_FILE"
printf '\n\nFINAL SUMMARY:\n'
printf 'Success: %d archives\n' "$success_count"
printf 'Failed: %d archives\n' "$failed_count"
printf 'Processed: %d total\n' "$total_archives"
printf 'Log saved: %s\n' "$LOG_FILE"
}
# =============================================================================
# ENTRY POINT
# =============================================================================
trap 'error "Script interrupted - check $LOG_FILE"; exit 130' INT TERM
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment