티스토리 뷰
반응형
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.7z
내7zz
또는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. 처리 절차 및 상세 조건
- 하위 폴더 제외, 최상위 파일만 처리(압축+비압축 모두)
- 압축파일 내 한글/일본어/특수문자 파일명 대응(7zz만 지원)
Deflate64
/CP949/SJIS/UTF-8 등 인코딩 이슈 모두 7zz 플래그로 안전하게 해제- 압축 내부 파일분리, 경로 및 내부명 분리 기록
- 해시 실패, 파일 손상, 압축해제실패, 권한 문제 등 모든 예외 상세 로그
- 결과는 TRUE(양쪽 공통)→A_ONLY→B_ONLY 순서, 이름순 출력(중복/빠짐X)
- 모든 특수문자, 한글 완전 escape/csv 정규화 수행
- 스크립트 실행 권장 경로:
/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
'정보와 기술 > Batocera' 카테고리의 다른 글
바토세라 공유폴더 윈도우에서 접속할 때 빼면 안되는 것 (0) | 2025.09.14 |
---|---|
RetroAchievements 롬 해시 비교 프로그램 (1) | 2025.09.01 |
Windows 11에서 Batocera SMB 공유 연결 방법 (0) | 2025.08.30 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
TAG
- Kodi
- 라즈베리파이5
- 파이널택틱스1
- RAHashCompare
- Recalbox
- Error
- 치트코드
- 코디
- XU4
- retroachievements
- 에뮬
- fbneo
- mednafen
- 설정
- ROM관리
- 바토세라
- PS1
- 리콜박스
- 레트로파이
- 레트로아크
- 오드로이드
- retroarch
- 에뮬레이터
- Deflate64
- 플스1
- RetroPie
- 새턴
- 치트
- batocera
- 레트로게임
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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