Normalizza percorsi e volumi USN
This commit is contained in:
20
README.md
20
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.
|
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
|
## Avvio tray app per test
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|||||||
@@ -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;
|
- 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.
|
- 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
|
## Configurazione
|
||||||
|
|
||||||
Il file di configurazione dovra' definire almeno:
|
Il file di configurazione dovra' definire almeno:
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ def default_config_path() -> Path:
|
|||||||
|
|
||||||
def expand_path(value: str, target_user: str | None = None) -> Path:
|
def expand_path(value: str, target_user: str | None = None) -> Path:
|
||||||
expanded = expand_user_vars(value, target_user)
|
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:
|
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
|
profile = Path("C:/Users") / user
|
||||||
replacements = {
|
replacements = {
|
||||||
"%USERPROFILE%": str(profile),
|
"%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"),
|
"%APPDATA%": str(profile / "AppData" / "Roaming"),
|
||||||
"%LOCALAPPDATA%": str(profile / "AppData" / "Local"),
|
"%LOCALAPPDATA%": str(profile / "AppData" / "Local"),
|
||||||
"%TEMP%": str(profile / "AppData" / "Local" / "Temp"),
|
"%TEMP%": str(profile / "AppData" / "Local" / "Temp"),
|
||||||
@@ -47,7 +55,43 @@ def expand_user_vars(value: str, target_user: str | None = None) -> str:
|
|||||||
expanded = value
|
expanded = value
|
||||||
for token, replacement in replacements.items():
|
for token, replacement in replacements.items():
|
||||||
expanded = _replace_case_insensitive(expanded, token, replacement)
|
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:
|
def _replace_case_insensitive(value: str, old: str, new: str) -> str:
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ def volume_name_for_path(path: Path) -> str | None:
|
|||||||
drive = path.drive
|
drive = path.drive
|
||||||
if not drive:
|
if not drive:
|
||||||
return None
|
return None
|
||||||
return f"\\\\.\\{drive.rstrip(':')}:"
|
return f"\\\\.\\{drive.rstrip(':').upper()}:"
|
||||||
|
|
||||||
|
|
||||||
def file_reference_number(path: Path) -> int:
|
def file_reference_number(path: Path) -> int:
|
||||||
|
|||||||
@@ -21,3 +21,17 @@ def test_expand_path_uses_target_user_for_temp() -> None:
|
|||||||
assert expand_path("%TEMP%", "pettirosso") == Path(
|
assert expand_path("%TEMP%", "pettirosso") == Path(
|
||||||
"C:/Users/pettirosso/AppData/Local/Temp"
|
"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")
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ from bakrest.usn_journal import (
|
|||||||
USN_REASON_RENAME_NEW_NAME,
|
USN_REASON_RENAME_NEW_NAME,
|
||||||
is_meaningful_usn_reason,
|
is_meaningful_usn_reason,
|
||||||
parse_usn_records,
|
parse_usn_records,
|
||||||
|
volume_name_for_path,
|
||||||
)
|
)
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def test_usn_reason_filter_accepts_real_data_changes() -> None:
|
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 len(records) == 1
|
||||||
assert records[0].file_reference_number == 12345
|
assert records[0].file_reference_number == 12345
|
||||||
assert records[0].reason == USN_REASON_FILE_CREATE
|
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:"
|
||||||
|
|||||||
Reference in New Issue
Block a user