Aggiorna coda dopo ogni file copiato

This commit is contained in:
2026-07-02 09:42:26 +02:00
parent e5a76992d4
commit 2d837008be
3 changed files with 121 additions and 12 deletions

View File

@@ -94,6 +94,7 @@ Il backup deve essere incrementale non distruttivo:
- cancellazioni sul master non devono cancellare file sullo slave;
- `robocopy` non deve essere usato con `/MIR` o `/PURGE`;
- i file cancellati o non piu' presenti devono essere marcati come ignorati nella coda.
- ogni file deve essere rimosso dalla coda appena la copia di quel singolo file e' riuscita su tutte le share configurate, senza attendere la fine dell'intero backup.
La copia deve ricreare sotto ogni share di destinazione il percorso relativo rispetto alla cartella monitorata. Esempio: con cartella monitorata `D:\Lavori`, il file `D:\Lavori\Cliente\a.psd` deve essere copiato in `\\server\share\Lavori\Cliente\a.psd`.

View File

@@ -52,19 +52,24 @@ def run_backup(config: AppConfig, logger: logging.Logger) -> BackupResult:
skipped_count=len(skipped) + len(unstable),
)
copy_result = _run_copy_engine(config, stable, logger)
copy_result = _run_copy_engine(config, stable, logger, registry)
if not copy_result.success:
return BackupResult(
False,
copy_result.message,
copied_count=copy_result.copied_count,
skipped_count=len(skipped) + len(unstable),
error_code=copy_result.error_code,
)
registry.mark_status([event.path for event in stable], "copied")
message = f"Backup completato: {len(stable)} file copiati"
message = f"Backup completato: {copy_result.copied_count} file copiati"
logger.info(message)
return BackupResult(True, message, copied_count=len(stable), skipped_count=len(skipped) + len(unstable))
return BackupResult(
True,
message,
copied_count=copy_result.copied_count,
skipped_count=len(skipped) + len(unstable),
)
def shutdown_after_backup(config: AppConfig, logger: logging.Logger) -> bool:
@@ -144,25 +149,40 @@ def _build_rsync_command(config: AppConfig, events: list[ChangeEvent]) -> list[s
]
def _run_copy_engine(config: AppConfig, events: list[ChangeEvent], logger: logging.Logger) -> BackupResult:
def _run_copy_engine(
config: AppConfig,
events: list[ChangeEvent],
logger: logging.Logger,
registry: ChangeRegistry,
) -> BackupResult:
if config.backup.engine == "robocopy":
return _run_robocopy(config, events, logger)
return _run_robocopy(config, events, logger, registry)
if config.backup.engine == "rsync":
return _run_rsync(config, events, logger)
result = _run_rsync(config, events, logger)
if result.success:
registry.mark_status([event.path for event in events], "copied")
return BackupResult(True, result.message, copied_count=len(events))
return result
message = f"Motore backup non supportato: {config.backup.engine}"
logger.error(message)
return BackupResult(False, message, error_code="backup_engine_unsupported")
def _run_robocopy(config: AppConfig, events: list[ChangeEvent], logger: logging.Logger) -> BackupResult:
def _run_robocopy(
config: AppConfig,
events: list[ChangeEvent],
logger: logging.Logger,
registry: ChangeRegistry,
) -> BackupResult:
if not config.backup.remote_destinations:
message = "Nessuna destinazione robocopy configurata"
logger.error(message)
return BackupResult(False, message, error_code="backup_destination_missing")
copied_files = 0
copied_commands = 0
for destination_root in config.backup.remote_destinations:
for event in events:
for event in events:
for destination_root in config.backup.remote_destinations:
command = _build_robocopy_command(config, event, destination_root)
logger.info("Running robocopy command: %s", _redact_command(command))
try:
@@ -182,8 +202,19 @@ def _run_robocopy(config: AppConfig, events: list[ChangeEvent], logger: logging.
logger.error(message)
return BackupResult(False, message, error_code="robocopy_failed")
copied_commands += 1
registry.mark_status([event.path], "copied")
copied_files += 1
logger.info(
"Marked copied after successful robocopy to %s destination(s): %s",
len(config.backup.remote_destinations),
event.path,
)
return BackupResult(True, f"Robocopy completato: {copied_commands} copie")
return BackupResult(
True,
f"Robocopy completato: {copied_files} file, {copied_commands} copie",
copied_count=copied_files,
)
def _run_rsync(config: AppConfig, events: list[ChangeEvent], logger: logging.Logger) -> BackupResult:

View File

@@ -1,15 +1,17 @@
from __future__ import annotations
from pathlib import Path
import subprocess
from bakrest.backup import (
_build_robocopy_command,
_build_rsync_command,
_robocopy_destination_dir,
_run_robocopy,
_split_existing_files,
)
from bakrest.config import parse_config
from bakrest.registry import ChangeEvent
from bakrest.registry import ChangeEvent, ChangeRegistry
def test_split_existing_files_ignores_deleted(tmp_path: Path) -> None:
@@ -95,3 +97,78 @@ def test_build_robocopy_command_preserves_path_under_include_root(tmp_path: Path
assert command[3] == "image.psd"
assert "/MIR" not in command
assert "/PURGE" not in command
def test_robocopy_marks_each_file_copied_after_all_destinations(
tmp_path: Path,
monkeypatch,
) -> None:
source = tmp_path / "Lavori"
source.mkdir()
first = source / "a.docx"
second = source / "b.docx"
first.write_text("a", encoding="utf-8")
second.write_text("b", encoding="utf-8")
config = parse_config(
{
"watch": {
"include_dirs": [str(source)],
"include_extensions": [".docx"],
"exclude_dirs": [],
"exclude_patterns": [],
},
"backup": {
"engine": "robocopy",
"robocopy_path": "robocopy",
"remote_destinations": ["\\\\server\\Backup1", "\\\\server\\Backup2"],
"server_check": {"type": "tcp", "port": 445, "interval_seconds": 1800, "timeout_seconds": 1},
},
"storage": {
"change_registry": str(tmp_path / "changes.sqlite"),
"logs_dir": str(tmp_path / "logs"),
"status_file": str(tmp_path / "status.json"),
},
"stability": {
"min_age_seconds": 0,
"unchanged_check_interval_seconds": 0,
"unchanged_checks_required": 0,
},
},
tmp_path / "config.toml",
)
registry = ChangeRegistry(config.storage.change_registry)
events = [
ChangeEvent(path=str(first), event_type="modified"),
ChangeEvent(path=str(second), event_type="modified"),
]
for event in events:
registry.record(event)
pending_counts_after_commands: list[int] = []
def fake_run(command, capture_output, text, check):
pending_counts_after_commands.append(registry.pending_count())
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
monkeypatch.setattr("bakrest.backup.subprocess.run", fake_run)
result = _run_robocopy(config, events, _NullLogger(), registry)
assert result.success
assert result.copied_count == 2
assert registry.pending_count() == 0
assert pending_counts_after_commands == [2, 2, 1, 1]
class _NullLogger:
def info(self, *args, **kwargs) -> None:
pass
def warning(self, *args, **kwargs) -> None:
pass
def error(self, *args, **kwargs) -> None:
pass
def exception(self, *args, **kwargs) -> None:
pass