Spaces:
Runtime error
Runtime error
| # hash_tools.py | |
| """ | |
| Gera uma planilha .xlsx (cabeçalho preto, linhas cinza) contendo: | |
| - Nome do arquivo | |
| - Extensão | |
| - SHA256 | |
| - SHA512 | |
| - MD5 | |
| Entrada: lista com 0..N caminhos; cada item pode ser: | |
| - um arquivo "solto" OU | |
| - um .zip (com subpastas) | |
| Para .zip, os arquivos são lidos de dentro do zip (sem extrair em disco). | |
| """ | |
| from __future__ import annotations | |
| from pathlib import Path | |
| from typing import Iterable, List, Tuple | |
| import hashlib | |
| import zipfile | |
| import io | |
| import time | |
| from openpyxl import Workbook | |
| from openpyxl.styles import Font, PatternFill, Alignment, Border, Side | |
| # ------------------------------------------------------------ | |
| # Hash helpers | |
| # ------------------------------------------------------------ | |
| def _hash_stream(stream: io.BufferedReader, chunk_size: int = 1024 * 1024) -> Tuple[str, str, str]: | |
| """Lê um stream binário e calcula SHA256, SHA512 e MD5 em uma única passada.""" | |
| h256 = hashlib.sha256() | |
| h512 = hashlib.sha512() | |
| hmd5 = hashlib.md5() | |
| while True: | |
| chunk = stream.read(chunk_size) | |
| if not chunk: | |
| break | |
| h256.update(chunk) | |
| h512.update(chunk) | |
| hmd5.update(chunk) | |
| return h256.hexdigest(), h512.hexdigest(), hmd5.hexdigest() | |
| def _hash_file(path: Path) -> Tuple[str, str, str]: | |
| """Calcula os hashes de um arquivo do disco.""" | |
| with path.open("rb") as f: | |
| return _hash_stream(f) | |
| def _hash_zip_entry(zf: zipfile.ZipFile, zi: zipfile.ZipInfo) -> Tuple[str, str, str]: | |
| """Calcula hashes de um item dentro do ZIP, sem extrair para disco.""" | |
| with zf.open(zi, "r") as f: | |
| return _hash_stream(io.BufferedReader(f)) | |
| # ------------------------------------------------------------ | |
| # Coleta de arquivos (múltiplas origens) | |
| # ------------------------------------------------------------ | |
| def _iter_zip_files(zip_path: Path) -> Iterable[Tuple[str, str, str, str, str]]: | |
| """ | |
| Itera pelos arquivos de um .zip gerando tuplas: | |
| (nome_exibicao, extensao, sha256, sha512, md5) | |
| - nome_exibicao: "<zip>::<caminho/interno>" | |
| - extensao: sufixo do nome (minúsculo, incluindo '.'); vazio se não existir | |
| """ | |
| with zipfile.ZipFile(zip_path, "r") as zf: | |
| for zi in zf.infolist(): | |
| if zi.is_dir(): | |
| continue | |
| nome_rel = f"{zip_path.name}::{zi.filename}" | |
| ext = Path(zi.filename).suffix.lower() | |
| sha256, sha512, md5 = _hash_zip_entry(zf, zi) | |
| yield (nome_rel, ext, sha256, sha512, md5) | |
| def _iter_single_file(file_path: Path) -> Iterable[Tuple[str, str, str, str, str]]: | |
| """Itera um arquivo único retornando a mesma tupla de dados.""" | |
| ext = file_path.suffix.lower() | |
| sha256, sha512, md5 = _hash_file(file_path) | |
| yield (file_path.name, ext, sha256, sha512, md5) | |
| def iterar_entradas(paths: List[Path]) -> Iterable[Tuple[str, str, str, str, str]]: | |
| """ | |
| Recebe uma lista de caminhos. Para cada caminho: | |
| - se for .zip, itera os itens internos; | |
| - caso contrário, calcula diretamente. | |
| """ | |
| for p in paths: | |
| if p.suffix.lower() == ".zip": | |
| yield from _iter_zip_files(p) | |
| else: | |
| yield from _iter_single_file(p) | |
| # ------------------------------------------------------------ | |
| # Excel estilizado (cabeçalho preto, linhas cinza) | |
| # ------------------------------------------------------------ | |
| def _escrever_excel_formatado(linhas: List[Tuple[str, str, str, str, str]], xlsx_path: Path) -> None: | |
| """ | |
| Gera um XLSX com: | |
| - Cabeçalho: fundo preto, fonte branca e negrito | |
| - Linhas: fundo cinza claro | |
| - Bordas finas e alinhamento à esquerda | |
| - Largura de coluna autoajustada (com limite) | |
| """ | |
| wb = Workbook() | |
| ws = wb.active | |
| ws.title = "Hashes" | |
| headers = ["Nome do arquivo", "Extensão", "SHA256", "SHA512", "MD5"] | |
| fill_header = PatternFill("solid", fgColor="111111") # preto | |
| font_header = Font(bold=True, color="FFFFFF") # branco | |
| fill_row = PatternFill("solid", fgColor="E5E5E5") # cinza claro | |
| border_thin = Border( | |
| left=Side(style="thin", color="444444"), | |
| right=Side(style="thin", color="444444"), | |
| top=Side(style="thin", color="444444"), | |
| bottom=Side(style="thin", color="444444"), | |
| ) | |
| align_left = Alignment(horizontal="left", vertical="center") | |
| # Cabeçalho | |
| ws.append(headers) | |
| for col in range(1, len(headers) + 1): | |
| c = ws.cell(row=1, column=col) | |
| c.fill = fill_header | |
| c.font = font_header | |
| c.alignment = align_left | |
| c.border = border_thin | |
| # Linhas | |
| for row_vals in linhas: | |
| ws.append(list(row_vals)) | |
| for row in ws.iter_rows(min_row=2, max_row=ws.max_row, min_col=1, max_col=len(headers)): | |
| for c in row: | |
| c.fill = fill_row | |
| c.alignment = align_left | |
| c.border = border_thin | |
| # Ajuste de largura (máx 100) | |
| for col_cells in ws.columns: | |
| max_len = 0 | |
| letter = col_cells[0].column_letter | |
| for c in col_cells: | |
| val = "" if c.value is None else str(c.value) | |
| if len(val) > max_len: | |
| max_len = len(val) | |
| ws.column_dimensions[letter].width = min(max_len + 2, 100) | |
| wb.save(xlsx_path) | |
| # ------------------------------------------------------------ | |
| # API pública: gera XLSX a partir de 1..N entradas (arquivos e/ou zips) | |
| # ------------------------------------------------------------ | |
| def gerar_planilha_hash_multi(paths: List[Path]) -> str: | |
| """ | |
| Recebe lista de caminhos (mistos: arquivos e/ou .zip). | |
| Retorna o caminho do XLSX gerado (no diretório atual). | |
| """ | |
| linhas: List[Tuple[str, str, str, str, str]] = list(iterar_entradas(paths)) | |
| # nome amigável com timestamp | |
| stamp = time.strftime("%Y%m%d-%H%M%S") | |
| out_path = Path.cwd() / f"hashes_{stamp}.xlsx" | |
| _escrever_excel_formatado(linhas, out_path) | |
| return str(out_path) | |