diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 47c1964..d94e9c5 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -4,7 +4,8 @@ "Bash(.venv\\\\Scripts\\\\python.exe 10_collect_metadata.py --source demo_fotky --limit 3 --dry-run)", "Bash(PGPASSWORD=\"\" psql -h 192.168.1.76 -p 5432 -U vladimir.buzalka -d fotky_buzalkovi -c \"\\\\dt\")", "PowerShell($env:PGPASSWORD = \"\"; psql -h 192.168.1.76 -p 5432 -U vladimir.buzalka -d fotky_buzalkovi -c \"\\\\dt\" 2>&1)", - "PowerShell(python -c \"import psycopg2; conn = psycopg2.connect\\(host='192.168.1.76', port=5432, user='vladimir.buzalka', password='', dbname='fotky_buzalkovi'\\); cur = conn.cursor\\(\\); cur.execute\\(\\\\\"SELECT table_name FROM information_schema.tables WHERE table_schema='public' ORDER BY table_name\\\\\"\\); rows = cur.fetchall\\(\\); print\\([r[0] for r in rows]\\); conn.close\\(\\)\" 2>&1)" + "PowerShell(python -c \"import psycopg2; conn = psycopg2.connect\\(host='192.168.1.76', port=5432, user='vladimir.buzalka', password='', dbname='fotky_buzalkovi'\\); cur = conn.cursor\\(\\); cur.execute\\(\\\\\"SELECT table_name FROM information_schema.tables WHERE table_schema='public' ORDER BY table_name\\\\\"\\); rows = cur.fetchall\\(\\); print\\([r[0] for r in rows]\\); conn.close\\(\\)\" 2>&1)", + "PowerShell(Rename-Item -Path \"C:\\\\Users\\\\vlado\\\\PycharmProjects\\\\fotkyBuzalkovi\\\\00 PictureCollector\\\\collect_pictures.py\" -NewName \"collect_pictures_linux.py\")" ] } } diff --git a/00 PictureCollector/collect_pictures.py b/00 PictureCollector/collect_pictures_linux.py similarity index 88% rename from 00 PictureCollector/collect_pictures.py rename to 00 PictureCollector/collect_pictures_linux.py index 0c402dc..20f6eab 100644 --- a/00 PictureCollector/collect_pictures.py +++ b/00 PictureCollector/collect_pictures_linux.py @@ -11,6 +11,21 @@ Tabulky: 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). + +Předpoklad pro hostname a ukládání cest do DB: + Skript běží na dvou Unraid serverech s různým přístupem k zálohovacímu share: + + hostname = Tower1 (druhý Unraid server — vlastník zálohy): + Zálohovací share je nativní: /mnt/user/ZalohaVsechObrazku/ + Cesta se ukládá do DB beze změny — je již v kanonickém tvaru. + + hostname = tower (první Unraid server — zálohu zapisuje přes remote mount): + Zálohovací share je mountován jako: /mnt/remotes/TOWER1.LAN_ZalohaVsechObrazku/ + Fyzické kopírování probíhá přes tento remote mount, ale do DB se ukládá + vždy nativní Tower1 cesta: /mnt/user/ZalohaVsechObrazku/... + (PATH_NORMALIZE_MAP zajistí přepis prefixu před INSERTem). + + Tím jsou cesty v DB jednotné bez ohledu na to, který server zálohu pořídil. """ import os @@ -45,6 +60,12 @@ ZALOHA_DIR_MAP = { } ZALOHA_DIR_DEFAULT = Path("/mnt/remotes/TOWER1.LAN_ZalohaVsechObrazku") +# Normalizace cesty pro DB — vždy ukládáme nativní Tower1 cestu, +# bez ohledu na to, přes jaký mount skript soubor fyzicky zapsal. +PATH_NORMALIZE_MAP = { + "/mnt/remotes/TOWER1.LAN_ZalohaVsechObrazku": "/mnt/user/ZalohaVsechObrazku", +} + EXCLUDED_DIR_NAMES_LOWER = {"zalohavsechobrazku", "zálohavsechobrázku", "zálohavsechobrazku"} BATCH_SIZE = 500 @@ -119,6 +140,15 @@ def compute_blake3(path: Path, chunk: int = 1 << 20) -> str: return h.hexdigest() +def normalize_path_for_db(path: Path) -> str: + """Převede fyzickou cestu zálohy na nativní Tower1 cestu pro uložení do DB.""" + s = str(path) + for remote, native in PATH_NORMALIZE_MAP.items(): + if s.startswith(remote): + return native + s[len(remote):] + return s + + def dest_path_for(source: Path, hostname: str) -> Path: try: relative = source.relative_to(SOURCE_BASE) @@ -237,7 +267,7 @@ def process(conn, hostname): t_copy = time.perf_counter() cur = conn.cursor() - cur.execute(SQL_INSERT_ZALOHA, (hash_val, str(dest), source.name, velikost)) + cur.execute(SQL_INSERT_ZALOHA, (hash_val, normalize_path_for_db(dest), source.name, velikost)) row = cur.fetchone() if row: zaloha_id = row[0] diff --git a/00 PictureCollector/collect_pictures_windows.py b/00 PictureCollector/collect_pictures_windows.py index 68450ed..ff4a1dc 100644 --- a/00 PictureCollector/collect_pictures_windows.py +++ b/00 PictureCollector/collect_pictures_windows.py @@ -10,6 +10,14 @@ 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). + +Předpoklad pro ukládání cest do DB: + Ať už je hostname Windows stroje jakýkoliv, Tower1 (druhý Unraid server) + je vždy dostupný jako \\Tower1\ZalohaVsechObrazku\... + Fyzické kopírování probíhá přes tuto UNC cestu, ale do DB se ukládá + vždy nativní Linux cesta Tower1, tj. /mnt/user/ZalohaVsechObrazku/... + (\\Tower1 → /mnt/user, zpětná lomítka → dopředná lomítka). + Tím jsou cesty v DB jednotné bez ohledu na to, který stroj zálohu pořídil. """ import os @@ -39,6 +47,19 @@ DB_CONFIG = { ZALOHA_DIR = Path(r"\\Tower1\ZalohaVsechObrazku") +# Normalizace cesty pro DB — Windows UNC cestu převedeme na nativní Tower1 Linux cestu. +# \\Tower1\ZalohaVsechObrazku\... → /mnt/user/ZalohaVsechObrazku/... +WINDOWS_UNC_PREFIX = r"\\Tower1" +LINUX_NATIVE_PREFIX = "/mnt/user" + + +def normalize_path_for_db(path: Path) -> str: + """Převede \\Tower1\ZalohaVsechObrazku\... na /mnt/user/ZalohaVsechObrazku/...""" + s = str(path) + if s.startswith(WINDOWS_UNC_PREFIX): + return LINUX_NATIVE_PREFIX + s[len(WINDOWS_UNC_PREFIX):].replace("\\", "/") + return s + JPEG_EXTENSIONS = {".jpg", ".jpeg"} EXCLUDED_DIR_NAMES_LOWER = { @@ -244,7 +265,7 @@ def process(conn, hostname, drives): t_copy = time.perf_counter() cur = conn.cursor() - cur.execute(SQL_INSERT_ZALOHA, (hash_val, str(dest), source.name, velikost)) + cur.execute(SQL_INSERT_ZALOHA, (hash_val, normalize_path_for_db(dest), source.name, velikost)) row = cur.fetchone() if row: zaloha_id = row[0] diff --git a/30 SběrDat/10_collect_metadata.py b/30 SběrDat/10_collect_metadata.py index 46b632d..828235c 100644 --- a/30 SběrDat/10_collect_metadata.py +++ b/30 SběrDat/10_collect_metadata.py @@ -347,7 +347,13 @@ def iter_photos(source: Path): dirs[:] = [d for d in dirs if not d.startswith(".")] for fname in files: if Path(fname).suffix.lower() in SUPPORTED_EXTENSIONS: - yield Path(root) / fname + p = Path(root) / fname + # Přeskočit symlinky které vedou mimo share (WinError 3 - \\mnt\user\...) + try: + p.stat() + except OSError: + continue + yield p def count_photos(source: Path) -> int: diff --git a/30 SběrDat/collect_and_import.py b/30 SběrDat/collect_and_import.py index 773e1f3..dff5a0a 100644 --- a/30 SběrDat/collect_and_import.py +++ b/30 SběrDat/collect_and_import.py @@ -63,8 +63,14 @@ DB_CONFIG = { } # Překlad: Linux NFS cesta (jak je uložena v DB) → Windows UNC -LINUX_PREFIX = "/mnt/remotes/TOWER1.LAN_ZalohaVsechObrazku" -WINDOWS_UNC = "//Tower1/ZalohaVsechObrazku" +# Záznamy v DB mohou mít různé Linux prefixy podle toho, odkud byl scan spuštěn. +PATH_MAPPINGS = [ + ("/mnt/remotes/TOWER1.LAN_ZalohaVsechObrazku", "//Tower1/ZalohaVsechObrazku"), + ("/mnt/user/ZalohaVsechObrazku", "//Tower1/ZalohaVsechObrazku"), +] +# Zpětná kompatibilita +LINUX_PREFIX = PATH_MAPPINGS[0][0] +WINDOWS_UNC = PATH_MAPPINGS[0][1] SUPPORTED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".heic", ".tiff", ".tif", ".webp", ".bmp"} @@ -93,10 +99,11 @@ MIME_MAP = { # --------------------------------------------------------------------------- def linux_to_windows(linux_path: str) -> Path: - """Převede /mnt/remotes/TOWER1.LAN_ZalohaVsechObrazku/... na //Tower1/ZalohaVsechObrazku/...""" - if linux_path.startswith(LINUX_PREFIX): - rel = linux_path[len(LINUX_PREFIX):] # začíná / - return Path(WINDOWS_UNC + rel) + """Převede Linux NFS cestu na Windows UNC podle tabulky PATH_MAPPINGS.""" + for linux_prefix, windows_unc in PATH_MAPPINGS: + if linux_path.startswith(linux_prefix): + rel = linux_path[len(linux_prefix):] # začíná / + return Path(windows_unc + rel) return Path(linux_path) # ---------------------------------------------------------------------------