o Q 3i#@sbdZddlZddlZddlmZddlmZmZmZm Z ddl m Z ddl m Z GdddZ dS) z Directory Sync Engine - Unidirectional Mirror Synchronization Mirrors source directory to destination using mtime+size comparison N)Path)OptionalCallableDictAny)datetime)loggerc@seZdZdZdedefddZddeedeee ffd d Z d e dee e ffd d Z de de de fddZde de fddZdee e fdefddZd e fddZdS) DirectorySynca! Unidirectional directory synchronization engine Mirrors source -> destination: - Copies new/modified files (based on mtime + size) - Deletes files from destination that don't exist in source - Preserves directory structure - Handles network paths (e.g., Z:\) source destinationcCsnt||_t||_|jstd||js"td||jjdddtd|d|dS)z Initialize sync engine Args: source: Source directory path (can be network path) destination: Destination directory path (can be network path) zSource path does not exist: z Source path is not a directory: Tparentsexist_okzDirectorySync initialized: z -> N) rr r exists ValueErroris_dirmkdirrinfo)selfr r r/c:\PYTHON\Scheduler\modules\sync\sync_engine.py__init__s    zDirectorySync.__init__Nprogress_callbackreturnc Csddddgd}z|r|ddddd||j}t||d<tdt|d|r8|d dt|ddt|D]c\}\}}|j|}z2|||ri| |||d d 7<|d | j 7<|rz|d |d t|t |dWq>t y}zd |d|} t| |d| WYd}~q>d}~ww|r|ddddd||} | |d<td|d d|ddW|St y}zd|} t| |d| d}~ww)a Perform synchronization Args: progress_callback: Optional callback for progress updates Called with dict: {'phase': str, 'current': int, 'total': int, 'file': str} Returns: Dict with statistics: { 'files_scanned': int, 'files_copied': int, 'files_deleted': int, 'bytes_total': int, 'errors': List[str] } r) files_scanned files_copied files_deleted bytes_totalerrorsZscanning)phasecurrenttotalfilerzScanned z files in sourceZsyncingrrzError syncing : rNZcleaningrzSync completed: z copied, z deletedzCritical sync error: )_scan_directoryr lenrr enumerateitemsr _needs_sync _copy_filestatst_sizestr Exceptionerrorappend_cleanup_destination exception) rrstats source_filesidxrel_path source_file dest_filee error_msg deleted_countrrrsync/s`       "  zDirectorySync.sync directoryc Csi}z:|dD]1}|r9z ||}|||<Wqttfy8}ztd|d|WYd}~qd}~wwqW|StyT}z td|d|d}~ww)z Scan directory and build file map Args: directory: Directory to scan Returns: Dict mapping relative paths to absolute paths *zCannot access r%NzError scanning directory ) rglobis_file relative_toOSErrorPermissionErrorrwarningr/r0)rr>Zfile_mapitemr7r:rrrr&~s(     zDirectorySync._scan_directoryr8r9c Cs|sdSz|}|}|j|jkr!td|jWdSWdSttfyC}ztd|d|WYd}~dSd}~ww)ax Check if file needs to be synced using size comparison only Note: mtime comparison disabled because WebDAV/network drives don't preserve timestamps correctly with shutil.copy2() Args: source_file: Source file path dest_file: Destination file path Returns: True if file needs to be copied TzSize mismatch: FzCannot compare r%N) rr,r-rdebugnamerCrDrE)rr8r9Z source_statZ dest_statr:rrrr*s zDirectorySync._needs_syncc Cstz|jjdddt||td|jWdStttj fy9}zt d|d|d|d}~ww)z Copy file from source to destination, preserving metadata Args: source_file: Source file path dest_file: Destination file path Tr zCopied: zFailed to copy z to r%N) parentrshutilcopy2rrGrHrCrDErrorr0)rr8r9r:rrrr+s zDirectorySync._copy_filer5c Csd}zO||j}|D]=}||vrH|j|}z|td||d7}Wq ttfyG}ztd|d|WYd}~q d}~wwq ||jW|St yl}ztd|WYd}~|Sd}~ww)z Delete files from destination that don't exist in source Args: source_files: Dict of relative paths from source Returns: Number of files deleted rz Deleted: r$zFailed to delete r%NzError during cleanup: ) r&r unlinkrrGrCrDr0_cleanup_empty_dirsr/)rr5r<Z dest_filesr7r9r:rrrr2s,    " z"DirectorySync._cleanup_destinationc Csz:tjt|ddD]-\}}}t|}||krq zt|s+|td|Wq t t fy7Yq wWdSt yU}zt d|WYd}~dSd}~ww)zy Remove empty directories from destination Args: directory: Root directory to clean F)topdownzRemoved empty directory: z"Error cleaning empty directories: N) oswalkr.ranyiterdirrmdirrrGrCrDr/rE)rr>dirpathdirnames filenamesZdir_pathr:rrrrNs$ z!DirectorySync._cleanup_empty_dirs)N)__name__ __module__ __qualname____doc__r.rrrrrr=rr&boolr*r+intr2rNrrrrr s O##r )r[rPrJpathlibrtypingrrrrrlogururr rrrrs