Created
December 31, 2025 20:18
-
-
Save ParkWardRR/9f904b17214ec0e428a86321e9547840 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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