notebookVb
This commit is contained in:
@@ -10,6 +10,7 @@ Tabulky:
|
||||
zdrojove_soubory – všechny nalezené zdrojové soubory (i duplikáty)
|
||||
|
||||
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
|
||||
@@ -37,20 +38,15 @@ DB_CONFIG = {
|
||||
SOURCE_BASE = Path("/mnt/user")
|
||||
JPEG_EXTENSIONS = {".jpg", ".jpeg"}
|
||||
|
||||
# Cílová zálohovací složka podle hostname — vždy fyzicky na Tower1
|
||||
# Klíč = socket.gethostname(), hodnota = lokální cesta k záloze
|
||||
ZALOHA_DIR_MAP = {
|
||||
"Tower1": Path("/mnt/user/ZalohaVsechObrazku"), # Tower1: lokální share
|
||||
"tower": Path("/mnt/remotes/TOWER1.LAN_ZalohaVsechObrazku"), # tower: NFS mount na Tower1
|
||||
"Tower1": Path("/mnt/user/ZalohaVsechObrazku"),
|
||||
"tower": Path("/mnt/remotes/TOWER1.LAN_ZalohaVsechObrazku"),
|
||||
}
|
||||
ZALOHA_DIR_DEFAULT = Path("/mnt/remotes/TOWER1.LAN_ZalohaVsechObrazku")
|
||||
|
||||
# Adresáře které se NIKDY nepoužijí jako zdroj (porovnává se jméno složky, kdekoliv ve stromě)
|
||||
EXCLUDED_DIR_NAMES = {
|
||||
"ZalohaVsechObrazku",
|
||||
"ZalohaVšechObrázků",
|
||||
"zalohavsechobrazku", # lowercase varianta pro jistotu
|
||||
}
|
||||
EXCLUDED_DIR_NAMES_LOWER = {"zalohavsechobrazku", "zálohavsechobrázku", "zálohavsechobrazku"}
|
||||
|
||||
BATCH_SIZE = 500
|
||||
|
||||
LOG_FILE = Path(__file__).parent / "collect_pictures.log"
|
||||
|
||||
@@ -97,12 +93,10 @@ CREATE INDEX IF NOT EXISTS idx_zdroj_zaloha ON zdrojove_soubory (zaloha_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_zdroj_host ON zdrojove_soubory (hostname);
|
||||
"""
|
||||
|
||||
SQL_HASH_EXISTS = "SELECT id, cesta_zalohy FROM zaloha_obrazku WHERE blake3_hash = %s"
|
||||
SQL_SOURCE_EXISTS = "SELECT id FROM zdrojove_soubory WHERE hostname = %s AND cesta_zdroje = %s"
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
@@ -112,6 +106,8 @@ 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 compute_blake3(path: Path, chunk: int = 1 << 20) -> str:
|
||||
@@ -123,12 +119,6 @@ def compute_blake3(path: Path, chunk: int = 1 << 20) -> str:
|
||||
|
||||
|
||||
def dest_path_for(source: Path, hostname: str) -> Path:
|
||||
"""
|
||||
Záloha vždy na TOWER1 pod /mnt/user/ZalohaVsechObrazku/{hostname}/...
|
||||
Příklad:
|
||||
tower /mnt/user/Foto/2023/img.jpg → /mnt/user/ZalohaVsechObrazku/tower/Foto/2023/img.jpg
|
||||
tower1 /mnt/user/Foto/2023/img.jpg → /mnt/user/ZalohaVsechObrazku/tower1/Foto/2023/img.jpg
|
||||
"""
|
||||
try:
|
||||
relative = source.relative_to(SOURCE_BASE)
|
||||
except ValueError:
|
||||
@@ -139,52 +129,77 @@ def dest_path_for(source: Path, hostname: str) -> Path:
|
||||
def copy_to_backup(source: Path, dest: Path) -> None:
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
if dest.exists():
|
||||
# Soubor na cílovém místě už je — nepřepisujeme, záloha platí
|
||||
return
|
||||
shutil.copy2(source, dest)
|
||||
|
||||
|
||||
def is_excluded_dir(name: str) -> bool:
|
||||
"""Vrátí True pokud jméno adresáře patří do seznamu vyloučených (case-insensitive)."""
|
||||
return name.lower() in {n.lower() for n in EXCLUDED_DIR_NAMES}
|
||||
|
||||
|
||||
def iter_jpeg_files(base: Path):
|
||||
"""Generátor: vrací Path na každý JPG/JPEG v base (rekurzivně).
|
||||
Přeskočí ZALOHA_DIR a jakýkoliv adresář jehož jméno je v EXCLUDED_DIR_NAMES.
|
||||
"""
|
||||
for root, dirs, files in os.walk(base, followlinks=False):
|
||||
root_path = Path(root)
|
||||
# Neprocházet zálohovací složku (podle plné cesty)
|
||||
if ZALOHA_DIR in (root_path, *root_path.parents) or root_path == ZALOHA_DIR:
|
||||
dirs.clear()
|
||||
continue
|
||||
# Odfiltrovat vyloučené adresáře ze subadresářů (podle jména, kdekoliv ve stromě)
|
||||
dirs[:] = [
|
||||
d for d in dirs
|
||||
if root_path / d != ZALOHA_DIR and not is_excluded_dir(d)
|
||||
if root_path / d != ZALOHA_DIR and d.lower() not in EXCLUDED_DIR_NAMES_LOWER
|
||||
]
|
||||
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):
|
||||
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(SOURCE_BASE):
|
||||
stats["nalezeno"] += 1
|
||||
src_str = str(source)
|
||||
|
||||
# Přeskočit zdroje, které už jsou v DB zpracovány (pro tento hostname)
|
||||
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
|
||||
|
||||
@@ -196,19 +211,13 @@ def process(conn, hostname):
|
||||
stats["chyb"] += 1
|
||||
continue
|
||||
|
||||
# Existuje už záloha s tímto hashem?
|
||||
cur.execute(SQL_HASH_EXISTS, (hash_val,))
|
||||
row = cur.fetchone()
|
||||
zaloha_id = known_hashes.get(hash_val)
|
||||
|
||||
if row:
|
||||
# Duplikát — jen zapíšeme zdroj, nekopírujeme
|
||||
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:
|
||||
# Hash známý z prefetch — duplikát, jen zapíšeme zdroj
|
||||
stats["duplicit"] += 1
|
||||
log.debug(f"DUPLIKÁT {source.name} (zaloha_id={zaloha_id})")
|
||||
else:
|
||||
# Nový unikátní soubor — zkopírovat a zapsat
|
||||
# Nový hash — zkopírovat a zapsat do zaloha_obrazku
|
||||
dest = dest_path_for(source, hostname)
|
||||
try:
|
||||
copy_to_backup(source, dest)
|
||||
@@ -217,21 +226,36 @@ def process(conn, hostname):
|
||||
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:
|
||||
# Jiný stroj vložil mezitím stejný hash — ON CONFLICT, získáme existující ID
|
||||
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
|
||||
|
||||
|
||||
@@ -254,12 +278,12 @@ def main():
|
||||
log.error(f"Nelze se připojit k DB: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Vytvoř tabulky pokud neexistují
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(SQL_CREATE_TABLES)
|
||||
conn.commit()
|
||||
log.info("Tabulky: OK")
|
||||
|
||||
stats = {"nalezeno": 0, "kopirovano": 0, "duplicit": 0, "chyb": 0, "preskoceno": 0}
|
||||
try:
|
||||
stats = process(conn, hostname)
|
||||
except KeyboardInterrupt:
|
||||
|
||||
Reference in New Issue
Block a user