DoAn / core /hash_file /hash_file.py
hungnha's picture
change commit
b91b0a5
import hashlib
import json
import logging
import os
import tempfile
import shutil
from collections import defaultdict
from pathlib import Path
from typing import Dict, List, Optional
from datetime import datetime
# Hằng số
CHUNK_SIZE = 8192 # Đọc file theo chunk 8KB
DEFAULT_FILE_EXTENSION = '.pdf'
class HashProcessor:
"""Lớp xử lý hash cho files - dùng để phát hiện thay đổi và tránh xử lý lại."""
def __init__(self, verbose: bool = True):
"""Khởi tạo HashProcessor."""
self.verbose = verbose
self.logger = logging.getLogger(__name__)
if not verbose:
self.logger.setLevel(logging.WARNING)
def get_file_hash(self, path: str) -> Optional[str]:
"""Tính SHA256 hash của một file."""
h = hashlib.sha256()
try:
with open(path, "rb") as f:
while chunk := f.read(CHUNK_SIZE):
h.update(chunk)
return h.hexdigest()
except (IOError, OSError) as e:
self.logger.error(f"Lỗi khi đọc file {path}: {e}")
return None
except Exception as e:
self.logger.error(f"Lỗi không xác định khi xử lý file {path}: {e}")
return None
def scan_files_for_hash(
self,
source_dir: str,
file_extension: str = DEFAULT_FILE_EXTENSION,
recursive: bool = False
) -> Dict[str, List[Dict[str, str]]]:
"""Quét thư mục và tính hash cho tất cả files."""
source_path = Path(source_dir)
if not source_path.exists():
raise FileNotFoundError(f"Thư mục không tồn tại: {source_dir}")
hash_to_files = defaultdict(list)
self.logger.info(f"Đang quét file trong: {source_dir}")
pattern = f"**/*{file_extension}" if recursive else f"*{file_extension}"
try:
files = list(source_path.glob(pattern))
for file_path in files:
if not file_path.is_file():
continue
self.logger.info(f"Đang tính hash cho: {file_path.name}")
file_hash = self.get_file_hash(str(file_path))
if file_hash:
hash_to_files[file_hash].append({
'filename': file_path.name,
'path': str(file_path),
'size': file_path.stat().st_size
})
except PermissionError as e:
self.logger.error(f"Lỗi quyền truy cập: {e}")
raise
return hash_to_files
def load_processed_index(self, index_file: str) -> Dict:
"""Đọc file index đã xử lý từ JSON."""
if os.path.exists(index_file):
try:
with open(index_file, "r", encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError as e:
self.logger.error(f"Lỗi đọc file index {index_file}: {e}")
return {}
except Exception as e:
self.logger.error(f"Lỗi không xác định khi đọc index: {e}")
return {}
return {}
def save_processed_index(self, index_file: str, processed_hashes: Dict) -> None:
"""Lưu index đã xử lý vào file JSON (atomic write).
Ghi vào file tạm trước, sau đó rename để đảm bảo an toàn.
"""
temp_name = None
try:
os.makedirs(os.path.dirname(index_file), exist_ok=True)
# Ghi vào file tạm trước
dir_name = os.path.dirname(index_file)
with tempfile.NamedTemporaryFile('w', dir=dir_name, delete=False, encoding='utf-8') as tmp_file:
json.dump(processed_hashes, tmp_file, indent=2, ensure_ascii=False)
temp_name = tmp_file.name
# Rename file tạm thành file chính (atomic operation trên POSIX)
shutil.move(temp_name, index_file)
self.logger.info(f"Đã lưu index file an toàn: {index_file}")
except Exception as e:
self.logger.error(f"Lỗi khi lưu index file {index_file}: {e}")
if temp_name and os.path.exists(temp_name):
os.remove(temp_name)
def get_current_timestamp(self) -> str:
"""Lấy timestamp hiện tại theo định dạng ISO."""
return datetime.now().isoformat()
def get_string_hash(self, text: str) -> str:
"""Tính SHA256 hash của một chuỗi text."""
return hashlib.sha256(text.encode('utf-8')).hexdigest()