Normalizza percorsi e volumi USN

This commit is contained in:
2026-07-02 10:25:55 +02:00
parent deaabb46c0
commit 169ede14dc
6 changed files with 90 additions and 3 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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")

View File

@@ -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:"