Files
defrag/app/migration/copy.py
2025-12-13 12:00:34 +01:00

130 lines
4.7 KiB
Python

import shutil
from pathlib import Path
from typing import Optional
import os
from ..shared.logger import ProgressLogger
class CopyMigrationStrategy:
def __init__(self, logger: Optional[ProgressLogger]=None, preserve_metadata: bool=True, verify_checksums: bool=True):
self.logger = logger
self.preserve_metadata = preserve_metadata
self.verify_checksums = verify_checksums
def migrate(self, source: Path, destination: Path, verify: bool=True) -> bool:
if not source.exists():
if self.logger:
self.logger.error(f'Source file does not exist: {source}')
return False
destination.parent.mkdir(parents=True, exist_ok=True)
try:
if self.preserve_metadata:
shutil.copy2(source, destination)
else:
shutil.copy(source, destination)
if verify and self.verify_checksums:
if not self._verify_copy(source, destination):
if self.logger:
self.logger.error(f'Verification failed: {source} -> {destination}')
destination.unlink()
return False
return True
except Exception as e:
if self.logger:
self.logger.error(f'Copy failed: {source} -> {destination}: {e}')
return False
def _verify_copy(self, source: Path, destination: Path) -> bool:
source_size = source.stat().st_size
dest_size = destination.stat().st_size
if source_size != dest_size:
return False
if source_size > 1024 * 1024:
from ..deduplication.chunker import hash_file
source_hash = hash_file(source)
dest_hash = hash_file(destination)
return source_hash == dest_hash
with open(source, 'rb') as f1, open(destination, 'rb') as f2:
return f1.read() == f2.read()
def can_migrate(self, source: Path, destination: Path) -> bool:
if not source.exists():
return False
dest_dir = destination.parent
if dest_dir.exists():
return os.access(dest_dir, os.W_OK)
parent = dest_dir.parent
while not parent.exists() and parent != parent.parent:
parent = parent.parent
return parent.exists() and os.access(parent, os.W_OK)
def estimate_time(self, source: Path) -> float:
if not source.exists():
return 0.0
size = source.stat().st_size
typical_speed = 100 * 1024 * 1024
return size / typical_speed
def cleanup(self, source: Path) -> bool:
try:
if source.exists():
source.unlink()
return True
except Exception as e:
if self.logger:
self.logger.warning(f'Failed to cleanup {source}: {e}')
return False
class FastCopyStrategy(CopyMigrationStrategy):
def __init__(self, logger: Optional[ProgressLogger]=None):
super().__init__(logger=logger, preserve_metadata=True, verify_checksums=False)
class SafeCopyStrategy(CopyMigrationStrategy):
def __init__(self, logger: Optional[ProgressLogger]=None):
super().__init__(logger=logger, preserve_metadata=True, verify_checksums=True)
class ReferenceCopyStrategy:
def __init__(self, logger: Optional[ProgressLogger]=None):
self.logger = logger
def migrate(self, source: Path, destination: Path, verify: bool=True) -> bool:
if not source.exists():
if self.logger:
self.logger.error(f'Source file does not exist: {source}')
return False
destination.parent.mkdir(parents=True, exist_ok=True)
try:
import subprocess
result = subprocess.run(['cp', '--reflink=auto', str(source), str(destination)], capture_output=True, check=False)
if result.returncode != 0:
shutil.copy2(source, destination)
return True
except Exception as e:
if self.logger:
self.logger.error(f'Reflink copy failed: {source} -> {destination}: {e}')
return False
def can_migrate(self, source: Path, destination: Path) -> bool:
if not source.exists():
return False
dest_dir = destination.parent
if dest_dir.exists():
return os.access(dest_dir, os.W_OK)
return True
def estimate_time(self, source: Path) -> float:
return 0.1
def cleanup(self, source: Path) -> bool:
try:
if source.exists():
source.unlink()
return True
except Exception as e:
if self.logger:
self.logger.warning(f'Failed to cleanup {source}: {e}')
return False