Fast directory sync using os.scandir instead of dirsync/os.walk. On Windows, DirEntry.stat() reuses FindFirstFile data (no extra syscall per file), making this significantly faster than dirsync which uses os.walk + os.stat per entry. Args: source: Source directory path
(
source: str, dest: str, purge: bool = True, content: bool = False
)
| 12 | |
| 13 | |
| 14 | def _scandir_sync( |
| 15 | source: str, dest: str, purge: bool = True, content: bool = False |
| 16 | ) -> None: |
| 17 | """Fast directory sync using os.scandir instead of dirsync/os.walk. |
| 18 | |
| 19 | On Windows, DirEntry.stat() reuses FindFirstFile data (no extra syscall |
| 20 | per file), making this significantly faster than dirsync which uses |
| 21 | os.walk + os.stat per entry. |
| 22 | |
| 23 | Args: |
| 24 | source: Source directory path |
| 25 | dest: Destination directory path |
| 26 | purge: If True, delete files in dest that don't exist in source |
| 27 | content: If True, compare file contents (for Docker); otherwise use size+mtime |
| 28 | """ |
| 29 | # Collect source entries: {relative_path: absolute_path} |
| 30 | src_files: dict[str, str] = {} |
| 31 | src_dirs: set[str] = set() |
| 32 | src_len = len(source) |
| 33 | if not source.endswith(os.sep): |
| 34 | src_len += 1 |
| 35 | |
| 36 | stack = [source] |
| 37 | while stack: |
| 38 | current = stack.pop() |
| 39 | try: |
| 40 | with os.scandir(current) as it: |
| 41 | for entry in it: |
| 42 | try: |
| 43 | rel = entry.path[src_len:] |
| 44 | if entry.is_dir(follow_symlinks=False): |
| 45 | src_dirs.add(rel) |
| 46 | stack.append(entry.path) |
| 47 | elif entry.is_file(follow_symlinks=False): |
| 48 | src_files[rel] = entry.path |
| 49 | except OSError: |
| 50 | pass |
| 51 | except OSError: |
| 52 | pass |
| 53 | |
| 54 | # Ensure dest directory exists |
| 55 | os.makedirs(dest, exist_ok=True) |
| 56 | |
| 57 | # Ensure subdirectories exist in dest |
| 58 | for rel_dir in sorted(src_dirs): |
| 59 | dest_dir = os.path.join(dest, rel_dir) |
| 60 | os.makedirs(dest_dir, exist_ok=True) |
| 61 | |
| 62 | # Copy new/changed files |
| 63 | for rel_path, src_abs in src_files.items(): |
| 64 | dest_abs = os.path.join(dest, rel_path) |
| 65 | os.makedirs(os.path.dirname(dest_abs), exist_ok=True) |
| 66 | |
| 67 | if os.path.isfile(dest_abs): |
| 68 | if content: |
| 69 | # Content comparison (Docker mode) — use filecmp for speed |
| 70 | if filecmp.cmp(src_abs, dest_abs, shallow=False): |
| 71 | continue |
no test coverage detected