티스토리 뷰

반응형

README.md


hashduplist2csv.sh

레트로 롬/ROM 폴더 간 해시 기반 중복/고유 비교 및 결과 CSV, 에러 분석 자동화


1. 스크립트 개요

  • 두 폴더(A/B, zip/비압축 혼합 가능)의 ROM 파일을 “내용 해시”로 비교하여
  • 중복/고유/비교불가(실패) 파일을 완전 자동 추출,
  • 경로, 압축내부파일, 일치여부, 에러사유까지 한 번에 csv+로그로 산출하는 유틸리티.

2. 필수 환경/사전 조건

2.1 필수 프로그램

  • 7zz (7-Zip standalone console, v25.01 이상 필수)
    • 공식 다운로드:
      https://www.7-zip.org/download.html
    • 반드시 “7-Zip Extra: standalone console version”
      • 예: 7z2301-extra.7z7zz 또는 7zzs(또는 windows: 7zz.exe)
    • 설치/준비:
      • 압축 해제
        tar -xf 7z2501-linux-x64.tar.xz
      • 실행권한(/userdata/system/7zz 에 풀렸을 때):
        chmod +x /userdata/system/7zz
    • 권장 버전:
      • 7zz/7zzs v25.01 이상(=2024년 최신, 예전 구버전 unzip/7z 미지원!)
      • Batocera/기본 unzip은 한글·Deflate64 등 문제 O, 반드시 7zz로만 사용
  • 기타:
    • Bash (Linux/Batocera/WSL/busybox 등 호환)
    • (md5sum 내장 또는 별도 설치)

3. 실행 방법

chmod +x hashduplist2csv.sh
./hashduplist2csv.sh <비교할_폴더A> <비교할_폴더B>
  • 예:
  • ./hashduplist2csv.sh /userdata/roms/snes /userdata/roms/move-recalbox/snes
  • 7zz 위치는 스크립트 상단의 SEVEN_ZZ_CMD에서 직접 지정(기본: /userdata/system/7zz)

4. 동작 및 주요 로직

  • zip 내부 파일 및 비압축 파일을 모두 scan, 내부 파일명/외부 경로/해시 분리 저장
  • 압축 파일: 압축경로, 내부파일명 별도 분리
  • 인코딩: cp949, UTF-8, SJIS 등 자동 대응(내부에서 -scsWIN, -scsUTF-8 등 7zz 플래그 사용)
  • 모든 파일 대상 해시 추출, 실패/오류 시 상세 로그 따로 분리
  • 중복(양쪽 매칭), A만, B만, 실패분(log) 모두 csv와 텍스트로 기록/요약

5. 출력/결과 파일 구조

5.1 결과 CSV

  • result_<A폴더명>_<날짜>.csv
    • "HASH","A_ZIP_PATH","A_INNER_FILE","B_ZIP_PATH","B_INNER_FILE","MATCH"
    • 각 행: (매칭=TRUE / A_ONLY / B_ONLY)
    • 예시:
      "13f78712...","/userdata/roms/snes/a.zip","aaa.smc","/userdata/roms/move-recalbox/snes/z.zip","zzz.smc","TRUE"
      ...
      "dabe121c...","/userdata/roms/snes/foo.smc","","","","A_ONLY"
      "394acbb7...","","","/userdata/roms/move-recalbox/snes/bar.zip","inside.smc","B_ONLY"

5.2 실패 로그

  • fail_<A폴더명>_<날짜>.log
    • 열: 폴더구분 (A|B), 압축경로, 내부파일명, 원인, 상세에러(stderr 일부)
    • 예시:
      A    /userdata/roms/snes/badfile.zip    abcde.smc    압축해제실패    ERROR: Data error...
      B    /userdata/roms/move-recalbox/snes/bar.zip        일반파일해시불가    파일 길이 0

6. 처리 절차 및 상세 조건

  1. 하위 폴더 제외, 최상위 파일만 처리(압축+비압축 모두)
  2. 압축파일 내 한글/일본어/특수문자 파일명 대응(7zz만 지원)
  3. Deflate64/CP949/SJIS/UTF-8 등 인코딩 이슈 모두 7zz 플래그로 안전하게 해제
  4. 압축 내부 파일분리, 경로 및 내부명 분리 기록
  5. 해시 실패, 파일 손상, 압축해제실패, 권한 문제 등 모든 예외 상세 로그
  6. 결과는 TRUE(양쪽 공통)→A_ONLY→B_ONLY 순서, 이름순 출력(중복/빠짐X)
  7. 모든 특수문자, 한글 완전 escape/csv 정규화 수행
  8. 스크립트 실행 권장 경로: /userdata/system/ 등 (BATOCERA 등에서도 검증)

7. 유지보수 및 핸드오버 체크포인트

  • 7zz 반드시 최신/독립 실행 & 실행권한 부여
  • 실패 파일은 로그에 반드시 남기고 로그구조(열) 보증
  • 코드/로직 변경 시 기존 CSV/LOG 결과와 diff 테스트 필수
  • 전체 주요 환경/체크리스트 README/hand-over 문서로 최신화
  • “누락, 실패, 중복, 깨짐 없는 결과보증” 및 코드 주석 유지

8. FAQ & 기타 주의

  • 7zz(v25.01 이상 추천): 다운로드 링크 → “Extra: standalone console version” 반드시 사용
  • 권장 위치: /userdata/system/7zz
  • batocera에 기본 설치된 unzip/7za/7z는 한글/deflate64 등 문제 발생(사용 금지)
  • zip 이외 포맷(7z 등) 확장 시 list_zip_files 커스텀 및 7zz 플래그 추가
  • 입력 폴더 or 파일명에 공백/한글/특수문자 전량 지원, csv따옴표 정규화

9. 문의 및 추가 지원

  • 버그/기능 개선/지원 필요시 담당자 또는 github pull-request, issue 등록
  • 최신 문서 및 handover 체크리스트/실패 샘플 등은 프로젝트 폴더 및 담당자 공유 자료를 참조

hashduplist2csv.sh


#!/bin/bash
# hashduplist2csv.sh
# 사용법: ./hashduplist2csv.sh <폴더A> <폴더B>

# --- 설정 --- #
# 7zz 실행 파일의 위치를 지정합니다.
SEVEN_ZZ_CMD="/userdata/system/7zz"

A_DIR="$1"
B_DIR="$2"

# --- 인자 확인 --- #
if [ "$#" -ne 2 ]; then
    echo "사용법: $0 <비교할 폴더 A> <비교할 폴더 B>"
    echo "예시: $0 /userdata/roms/snes /userdata/roms/nes"
    exit 1
fi

DATE_TAG=$(date +"%Y%m%d_%H%M%S")
CSV_OUT="result_$(basename "$A_DIR")_${DATE_TAG}.csv"
FAIL_LOG="fail_$(basename "$A_DIR")_${DATE_TAG}.log"

# --- 스크립트 시작 --- #

if [ ! -x "$SEVEN_ZZ_CMD" ]; then
    echo "오류: 7zz 실행 파일을 찾을 수 없거나 실행 권한이 없습니다."
    echo "$SEVEN_ZZ_CMD 경로를 확인하세요."
    exit 1
fi

> "$CSV_OUT"
> "$FAIL_LOG"

echo "== 해시 추출 시작 =="

get_hash() {
    local file="$1"
    md5sum "$file" 2>/dev/null | awk '{print $1}'
}

list_zip_files() {
    local zipfile="$1"
    "$SEVEN_ZZ_CMD" l -slt -scsWIN "$zipfile" 2>/dev/null | grep 'Path = ' | sed 's/^Path = //' && return 0
    "$SEVEN_ZZ_CMD" l -slt -scsUTF-8 "$zipfile" 2>/dev/null | grep 'Path = ' | sed 's/^Path = /'
}

declare -A HASH_A HASH_B

A_TOTAL_FILES=$(find "$A_DIR" -maxdepth 1 -type f 2>/dev/null | wc -l)
B_TOTAL_FILES=$(find "$B_DIR" -maxdepth 1 -type f 2>/dev/null | wc -l)
A_SUCCESS_OPS=0; A_FAIL_OPS=0
B_SUCCESS_OPS=0; B_FAIL_OPS=0

echo -n "A폴더 분석 중..."
for f in "$A_DIR"/*; do
    echo -n "."
    [ -f "$f" ] || continue
    if [[ "${f,,}" == *.zip ]]; then
        while IFS= read -r inner; do
            [[ -z "$inner" || "$inner" == */ ]] && continue
            tmp=$(mktemp)
            error_output=$("$SEVEN_ZZ_CMD" e -so "$f" "$inner" > "$tmp" 2>&1)
            if [ $? -eq 0 ]; then
                h=$(get_hash "$tmp")
                HASH_A["$h"]="$f|$inner"
                A_SUCCESS_OPS=$((A_SUCCESS_OPS + 1))
            else
                echo -e "A\t$f\t$inner\t압축해제실패\tERROR: $error_output" >> "$FAIL_LOG"
                A_FAIL_OPS=$((A_FAIL_OPS + 1))
            fi
            rm -f "$tmp"
        done < <(list_zip_files "$f")
    else
        h=$(get_hash "$f")
        HASH_A["$h"]="$f|"
        A_SUCCESS_OPS=$((A_SUCCESS_OPS + 1))
    fi
done
echo " 완료"

echo -n "B폴더 분석 중..."
for f in "$B_DIR"/*; do
    echo -n "."
    [ -f "$f" ] || continue
    if [[ "${f,,}" == *.zip ]]; then
        while IFS= read -r inner; do
            [[ -z "$inner" || "$inner" == */ ]] && continue
            tmp=$(mktemp)
            error_output=$("$SEVEN_ZZ_CMD" e -so "$f" "$inner" > "$tmp" 2>&1)
            if [ $? -eq 0 ]; then
                h=$(get_hash "$tmp")
                HASH_B["$h"]="$f|$inner"
                B_SUCCESS_OPS=$((B_SUCCESS_OPS + 1))
            else
                echo -e "B\t$f\t$inner\t압축해제실패\tERROR: $error_output" >> "$FAIL_LOG"
                B_FAIL_OPS=$((B_FAIL_OPS + 1))
            fi
            rm -f "$tmp"
        done < <(list_zip_files "$f")
    else
        h=$(get_hash "$f")
        HASH_B["$h"]="$f|"
        B_SUCCESS_OPS=$((B_SUCCESS_OPS + 1))
    fi
done
echo " 완료"

# 2단계: CSV 작성
{
    echo '"HASH","A_ZIP_PATH","A_INNER_FILE","B_ZIP_PATH","B_INNER_FILE","MATCH"'
    for h in "${!HASH_A[@]}"; do
        val_a=${HASH_A[$h]}
        a_zip=${val_a%|*}
        a_inner=${val_a#*|}
        if [[ "$a_zip" == "$a_inner" ]]; then a_inner=""; fi

        h_esc=${h//\"/\"\"}
        a_zip_esc=${a_zip//\"/\"\"}
        a_inner_esc=${a_inner//\"/\"\"}

        if [[ -n "${HASH_B[$h]}" ]]; then
            val_b=${HASH_B[$h]}
            b_zip=${val_b%|*}
            b_inner=${val_b#*|}
            if [[ "$b_zip" == "$b_inner" ]]; then b_inner=""; fi
            b_zip_esc=${b_zip//\"/\"\"}
            b_inner_esc=${b_inner//\"/\"\"}
            printf '"%s","%s","%s","%s","%s","%s"\n' \
              "$h_esc" "$a_zip_esc" "$a_inner_esc" "$b_zip_esc" "$b_inner_esc" "TRUE"
        else
            printf '"%s","%s","%s","","","%s"\n' \
              "$h_esc" "$a_zip_esc" "$a_inner_esc" "A_ONLY"
        fi
    done
    for h in "${!HASH_B[@]}"; do
        if [[ -z "${HASH_A[$h]}" ]]; then
            val_b=${HASH_B[$h]}
            b_zip=${val_b%|*}
            b_inner=${val_b#*|}
            if [[ "$b_zip" == "$b_inner" ]]; then b_inner=""; fi

            h_esc=${h//\"/\"\"}
            b_zip_esc=${b_zip//\"/\"\"}
            b_inner_esc=${b_inner//\"/\"\"}
            printf '"%s","","","%s","%s","%s"\n' \
              "$h_esc" "$b_zip_esc" "$b_inner_esc" "B_ONLY"
        fi
    done
} >> "$CSV_OUT"

# 최종 요약 출력
echo "== 처리 완료 =="
echo "A 폴더: 전체 $A_TOTAL_FILES 개 파일 대상 / 해시 생성 성공 $A_SUCCESS_OPS 건 / 실패 $A_FAIL_OPS 건"
echo "B 폴더: 전체 $B_TOTAL_FILES 개 파일 대상 / 해시 생성 성공 $B_SUCCESS_OPS 건 / 실패 $B_FAIL_OPS 건"
echo "CSV: $CSV_OUT"
echo "처리 실패 로그: $FAIL_LOG"
728x90
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2025/09   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함
250x250