notebookVb

This commit is contained in:
administrator
2026-05-25 06:39:49 +02:00
parent 4d8888a598
commit db84c3e501
3 changed files with 291 additions and 107 deletions
+90 -56
View File
@@ -9,6 +9,7 @@ Cesta zálohy: \\Tower1\ZalohaVsechObrazku\{hostname}\{písmeno_disku}\{cesta}
Příklad: \\Tower1\ZalohaVsechObrazku\JMENO-PC\D\Foto\2023\img.jpg
Bezpečné pro opakované spuštění (pokračuje tam kde skončilo).
Bezpečné pro souběžný běh na více strojích (ON CONFLICT v SQL).
"""
import os
@@ -23,6 +24,7 @@ from datetime import datetime
import blake3
import psycopg2
from psycopg2.extras import execute_values
# ── Konfigurace ───────────────────────────────────────────────────────────────
@@ -34,26 +36,25 @@ DB_CONFIG = {
"database": "fotky_buzalkovi",
}
# Záloha vždy na Tower1 přes UNC
ZALOHA_DIR = Path(r"\\Tower1\ZalohaVsechObrazku")
JPEG_EXTENSIONS = {".jpg", ".jpeg"}
# Adresáře přeskočené podle JMÉNA (kdekoliv ve stromě, case-insensitive)
EXCLUDED_DIR_NAMES = {
"ZalohaVsechObrazku",
"ZalohaechObrázků",
"$Recycle.Bin",
"$RECYCLE.BIN",
"System Volume Information",
"Recovery",
EXCLUDED_DIR_NAMES_LOWER = {
"zalohavsechobrazku",
"zálohavsechobrázku",
"lohavsechobrazku",
"$recycle.bin",
"system volume information",
"recovery",
}
# Celé cesty které se přeskočí (včetně podadresářů)
EXCLUDED_FULL_PATHS = {
Path(os.environ.get("WINDIR", r"C:\Windows")), # C:\Windows
Path(os.environ.get("WINDIR", r"C:\Windows")),
}
BATCH_SIZE = 500
LOG_FILE = Path(__file__).parent / "collect_pictures_windows.log"
# ── Logging ───────────────────────────────────────────────────────────────────
@@ -69,14 +70,12 @@ logging.basicConfig(
)
log = logging.getLogger(__name__)
# ── SQL (stejné jako Linux verze) ─────────────────────────────────────────────
SQL_HASH_EXISTS = "SELECT id FROM zaloha_obrazku WHERE blake3_hash = %s"
SQL_SOURCE_EXISTS = "SELECT id FROM zdrojove_soubory WHERE hostname = %s AND cesta_zdroje = %s"
# ── SQL ───────────────────────────────────────────────────────────────────────
SQL_INSERT_ZALOHA = """
INSERT INTO zaloha_obrazku (blake3_hash, cesta_zalohy, nazev_souboru, velikost)
VALUES (%s, %s, %s, %s)
ON CONFLICT (blake3_hash) DO NOTHING
RETURNING id
"""
@@ -86,10 +85,11 @@ VALUES (%s, %s, %s, %s, %s, %s)
ON CONFLICT (hostname, cesta_zdroje) DO NOTHING
"""
SQL_GET_ZALOHA_ID = "SELECT id FROM zaloha_obrazku WHERE blake3_hash = %s"
# ── Pomocné funkce ────────────────────────────────────────────────────────────
def get_local_drives() -> list[Path]:
"""Vrátí seznam lokálních pevných disků (typ FIXED), přeskočí síťové a CD."""
DRIVE_FIXED = 3
drives = []
bitmask = ctypes.windll.kernel32.GetLogicalDrives()
@@ -103,13 +103,7 @@ def get_local_drives() -> list[Path]:
return drives
def is_excluded_dir(name: str) -> bool:
"""Vrátí True pokud jméno adresáře patří do vyloučených (case-insensitive)."""
return name.lower() in {n.lower() for n in EXCLUDED_DIR_NAMES}
def is_excluded_path(path: Path) -> bool:
"""Vrátí True pokud cesta začíná některou z EXCLUDED_FULL_PATHS."""
for excl in EXCLUDED_FULL_PATHS:
try:
path.relative_to(excl)
@@ -128,12 +122,8 @@ def compute_blake3(path: Path, chunk: int = 1 << 20) -> str:
def dest_path_for(source: Path, hostname: str) -> Path:
"""
Zkonstruuje cílovou cestu v záloze.
D:\Foto\2023\img.jpg → \\Tower1\ZalohaVsechObrazku\JMENO-PC\D\Foto\2023\img.jpg
"""
drive_letter = source.drive.rstrip(":") # "D:" → "D"
relative = source.relative_to(source.drive + "\\") # "Foto\2023\img.jpg"
drive_letter = source.drive.rstrip(":")
relative = source.relative_to(source.drive + "\\")
return ZALOHA_DIR / hostname / drive_letter / relative
@@ -145,41 +135,78 @@ def copy_to_backup(source: Path, dest: Path) -> None:
def iter_jpeg_files(drives: list[Path]):
"""Generátor: prochází všechny lokální disky, vrací JPG/JPEG soubory."""
for drive in drives:
log.info(f"Skenuji disk: {drive}")
for root, dirs, files in os.walk(drive, followlinks=False):
root_path = Path(root)
# Přeskočit celé vyloučené cesty (C:\Windows apod.)
if is_excluded_path(root_path):
dirs.clear()
continue
# Odfiltrovat vyloučené adresáře podle jména
dirs[:] = [
d for d in dirs
if not is_excluded_dir(d) and not is_excluded_path(root_path / d)
if d.lower() not in EXCLUDED_DIR_NAMES_LOWER
and not is_excluded_path(root_path / d)
]
for fname in files:
if Path(fname).suffix.lower() in JPEG_EXTENSIONS:
yield root_path / fname
def load_known_sources(conn, hostname: str) -> set[str]:
with conn.cursor("src_cursor") as cur:
cur.itersize = 10000
cur.execute("SELECT cesta_zdroje FROM zdrojove_soubory WHERE hostname = %s", (hostname,))
return {row[0] for row in cur}
def load_known_hashes(conn) -> dict[str, int]:
with conn.cursor("hash_cursor") as cur:
cur.itersize = 10000
cur.execute("SELECT blake3_hash, id FROM zaloha_obrazku")
return {row[0]: row[1] for row in cur}
# ── Hlavní logika ─────────────────────────────────────────────────────────────
def process(conn, hostname, drives):
cur = conn.cursor()
log.info(f"Hostname zdroje: {hostname}")
log.info("Načítám známé zdroje z DB...")
known_sources = load_known_sources(conn, hostname)
log.info(f"Známých zdrojů v DB: {len(known_sources)}")
log.info("Načítám známé hashe z DB...")
known_hashes = load_known_hashes(conn)
log.info(f"Známých hashů v DB: {len(known_hashes)}")
stats = {"nalezeno": 0, "kopirovano": 0, "duplicit": 0, "chyb": 0, "preskoceno": 0}
pending_zdroje = []
def flush_zdroje():
if not pending_zdroje:
return
cur = conn.cursor()
execute_values(
cur,
"""INSERT INTO zdrojove_soubory
(hostname, cesta_zdroje, nazev_souboru, velikost, blake3_hash, zaloha_id)
VALUES %s
ON CONFLICT (hostname, cesta_zdroje) DO NOTHING""",
pending_zdroje,
)
conn.commit()
cur.close()
pending_zdroje.clear()
for source in iter_jpeg_files(drives):
stats["nalezeno"] += 1
src_str = str(source)
cur.execute(SQL_SOURCE_EXISTS, (hostname, src_str))
if cur.fetchone():
if src_str in known_sources:
stats["preskoceno"] += 1
if stats["preskoceno"] % 500 == 0:
if stats["preskoceno"] % 5000 == 0:
log.info(f"Přeskočeno (již v DB): {stats['preskoceno']}")
continue
@@ -191,15 +218,10 @@ def process(conn, hostname, drives):
stats["chyb"] += 1
continue
cur.execute(SQL_HASH_EXISTS, (hash_val,))
row = cur.fetchone()
zaloha_id = known_hashes.get(hash_val)
if row:
zaloha_id = row[0]
cur.execute(SQL_INSERT_ZDROJ, (hostname, src_str, source.name, velikost, hash_val, zaloha_id))
conn.commit()
if zaloha_id is not None:
stats["duplicit"] += 1
log.debug(f"DUPLIKÁT {source.name} (zaloha_id={zaloha_id})")
else:
dest = dest_path_for(source, hostname)
try:
@@ -209,21 +231,35 @@ def process(conn, hostname, drives):
stats["chyb"] += 1
continue
cur = conn.cursor()
cur.execute(SQL_INSERT_ZALOHA, (hash_val, str(dest), source.name, velikost))
zaloha_id = cur.fetchone()[0]
cur.execute(SQL_INSERT_ZDROJ, (hostname, src_str, source.name, velikost, hash_val, zaloha_id))
row = cur.fetchone()
if row:
zaloha_id = row[0]
else:
cur.execute(SQL_GET_ZALOHA_ID, (hash_val,))
zaloha_id = cur.fetchone()[0]
cur.close()
conn.commit()
known_hashes[hash_val] = zaloha_id
stats["kopirovano"] += 1
log.info(f"ZKOPÍROVÁNO [{stats['kopirovano']:>6}] {source}")
if stats["nalezeno"] % 1000 == 0:
pending_zdroje.append((hostname, src_str, source.name, velikost, hash_val, zaloha_id))
known_sources.add(src_str)
if len(pending_zdroje) >= BATCH_SIZE:
flush_zdroje()
if stats["nalezeno"] % 5000 == 0:
log.info(
f"Průběh: nalezeno={stats['nalezeno']} "
f"nových={stats['kopirovano']} duplikátů={stats['duplicit']} "
f"chyb={stats['chyb']} přeskočeno={stats['preskoceno']}"
)
cur.close()
flush_zdroje()
return stats
@@ -245,26 +281,24 @@ def main():
log.error(f"Nelze se připojit k DB: {e}")
sys.exit(1)
stats = {"nalezeno": 0, "kopirovano": 0, "duplicit": 0, "chyb": 0, "preskoceno": 0}
try:
stats = process(conn, hostname, drives)
except KeyboardInterrupt:
log.warning("Přerušeno uživatelem — dosavadní záznamy jsou uloženy.")
conn.rollback()
stats = {}
except Exception as e:
log.error(f"Neočekávaná chyba: {e}", exc_info=True)
conn.rollback()
stats = {}
finally:
conn.close()
if stats:
log.info("-" * 60)
log.info(f"Nalezeno JPG/JPEG: {stats['nalezeno']}")
log.info(f"Zkopírováno nových: {stats['kopirovano']}")
log.info(f"Duplikátů (hash): {stats['duplicit']}")
log.info(f"Přeskočeno (v DB): {stats['preskoceno']}")
log.info(f"Chyb: {stats['chyb']}")
log.info("-" * 60)
log.info(f"Nalezeno JPG/JPEG: {stats['nalezeno']}")
log.info(f"Zkopírováno nových: {stats['kopirovano']}")
log.info(f"Duplikátů (hash): {stats['duplicit']}")
log.info(f"Přeskočeno (v DB): {stats['preskoceno']}")
log.info(f"Chyb: {stats['chyb']}")
log.info("Hotovo.")