diff --git a/README.md b/README.md index e637947..2f64cf8 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,26 @@ python -m bakrest.watchdog_runner Il watchdog usa il journal USN NTFS per distinguere modifiche reali da eventi rumorosi generati da navigazione, anteprime o metadati. La lettura del journal puo' richiedere privilegi elevati: il servizio eseguito come `LocalSystem` e' il percorso consigliato. Se USN non e' disponibile o non e' leggibile, il servizio registra un warning e usa un fallback conservativo. +Se nel log compare: + +```text +USN journal unavailable for \\.\D:: [WinError 1179] Il journal delle modifiche al volume non e' attivo. +``` + +abilita il journal USN sul volume da PowerShell/CMD amministratore: + +```powershell +fsutil usn createjournal m=134217728 a=33554432 D: +``` + +Poi riavvia il servizio. Per verificare: + +```powershell +fsutil usn queryjournal D: +``` + +Nella configurazione, per cartelle utente puoi usare token portabili come `%DOCUMENTS%`, `%DOWNLOADS%`, `%PICTURES%`, `%MUSIC%`, `%VIDEOS%`, `%FAVORITES%`. Con `target_user = "utente"` vengono espansi sotto `C:\Users\utente`. + ## Avvio tray app per test ```powershell diff --git a/SPECIFICHE.md b/SPECIFICHE.md index 5e345e9..31083bc 100644 --- a/SPECIFICHE.md +++ b/SPECIFICHE.md @@ -326,6 +326,8 @@ Quando si monitora una directory molto ampia, ad esempio un'intera unita' `D:\`, - eventi di soli metadati, chiusura handle, indicizzazione, security o basic info devono essere ignorati; - se USN non e' disponibile su un volume, il servizio deve fare fallback conservativo e registrare un warning nei log. +I nomi volume devono essere normalizzati, ad esempio `c:` e `C:` devono riferirsi allo stesso reader USN `\\.\C:`. I percorsi drive-only come `d:` devono essere normalizzati a `D:\`. + ## Configurazione Il file di configurazione dovra' definire almeno: diff --git a/src/bakrest/paths.py b/src/bakrest/paths.py index b234111..aafaaf9 100644 --- a/src/bakrest/paths.py +++ b/src/bakrest/paths.py @@ -24,7 +24,8 @@ def default_config_path() -> Path: def expand_path(value: str, target_user: str | None = None) -> Path: expanded = expand_user_vars(value, target_user) - return Path(os.path.expandvars(expanded)).expanduser() + expanded_path = Path(os.path.expandvars(expanded)).expanduser() + return normalize_windows_path(expanded_path) def expand_user_vars(value: str, target_user: str | None = None) -> str: @@ -38,6 +39,13 @@ def expand_user_vars(value: str, target_user: str | None = None) -> str: profile = Path("C:/Users") / user replacements = { "%USERPROFILE%": str(profile), + "%DESKTOP%": str(profile / "Desktop"), + "%DOCUMENTS%": str(profile / "Documents"), + "%DOWNLOADS%": str(profile / "Downloads"), + "%PICTURES%": str(profile / "Pictures"), + "%MUSIC%": str(profile / "Music"), + "%VIDEOS%": str(profile / "Videos"), + "%FAVORITES%": str(profile / "Favorites"), "%APPDATA%": str(profile / "AppData" / "Roaming"), "%LOCALAPPDATA%": str(profile / "AppData" / "Local"), "%TEMP%": str(profile / "AppData" / "Local" / "Temp"), @@ -47,7 +55,43 @@ def expand_user_vars(value: str, target_user: str | None = None) -> str: expanded = value for token, replacement in replacements.items(): expanded = _replace_case_insensitive(expanded, token, replacement) - return expanded + return _normalize_user_known_folder_aliases(expanded, profile) + + +def normalize_windows_path(path: Path) -> Path: + path_text = str(path) + if len(path_text) == 2 and path_text[1] == ":": + return Path(f"{path_text.upper()}\\") + if len(path_text) >= 2 and path_text[1] == ":": + return Path(path_text[0].upper() + path_text[1:]) + return path + + +def _normalize_user_known_folder_aliases(value: str, profile: Path) -> str: + aliases = { + "download": "Downloads", + "downloads": "Downloads", + "documenti": "Documents", + "immagini": "Pictures", + "musica": "Music", + "video": "Videos", + "preferiti": "Favorites", + } + path = Path(value) + try: + relative = path.relative_to(profile) + except ValueError: + return value + + if not relative.parts: + return value + + first = relative.parts[0] + replacement = aliases.get(first.lower()) + if not replacement: + return value + + return str(profile.joinpath(replacement, *relative.parts[1:])) def _replace_case_insensitive(value: str, old: str, new: str) -> str: diff --git a/src/bakrest/usn_journal.py b/src/bakrest/usn_journal.py index f38dd5f..65e8e01 100644 --- a/src/bakrest/usn_journal.py +++ b/src/bakrest/usn_journal.py @@ -213,7 +213,7 @@ def volume_name_for_path(path: Path) -> str | None: drive = path.drive if not drive: return None - return f"\\\\.\\{drive.rstrip(':')}:" + return f"\\\\.\\{drive.rstrip(':').upper()}:" def file_reference_number(path: Path) -> int: diff --git a/tests/test_paths.py b/tests/test_paths.py index 279d34e..4f36294 100644 --- a/tests/test_paths.py +++ b/tests/test_paths.py @@ -21,3 +21,17 @@ def test_expand_path_uses_target_user_for_temp() -> None: assert expand_path("%TEMP%", "pettirosso") == Path( "C:/Users/pettirosso/AppData/Local/Temp" ) + + +def test_expand_path_normalizes_drive_root() -> None: + assert expand_path("d:") == Path("D:/") + + +def test_expand_path_maps_downloads_token_for_target_user() -> None: + assert expand_path("%DOWNLOADS%", "utente") == Path("C:/Users/utente/Downloads") + + +def test_expand_path_maps_italian_known_folder_aliases() -> None: + assert expand_path("%USERPROFILE%\\Immagini", "utente") == Path("C:/Users/utente/Pictures") + assert expand_path("%USERPROFILE%\\Musica", "utente") == Path("C:/Users/utente/Music") + assert expand_path("%USERPROFILE%\\video", "utente") == Path("C:/Users/utente/Videos") diff --git a/tests/test_usn_journal.py b/tests/test_usn_journal.py index 54e542e..9c20dd6 100644 --- a/tests/test_usn_journal.py +++ b/tests/test_usn_journal.py @@ -11,7 +11,9 @@ from bakrest.usn_journal import ( USN_REASON_RENAME_NEW_NAME, is_meaningful_usn_reason, parse_usn_records, + volume_name_for_path, ) +from pathlib import Path def test_usn_reason_filter_accepts_real_data_changes() -> None: @@ -50,3 +52,8 @@ def test_parse_usn_record_v3_reason_and_file_reference() -> None: assert len(records) == 1 assert records[0].file_reference_number == 12345 assert records[0].reason == USN_REASON_FILE_CREATE + + +def test_volume_name_is_normalized_to_uppercase_drive() -> None: + assert volume_name_for_path(Path("c:/tmp")) == "\\\\.\\C:" + assert volume_name_for_path(Path("C:/tmp")) == "\\\\.\\C:"