Coverage for src/drive_sync_directory.py: 100%
48 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-08 21:05 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-08 21:05 +0000
1"""Drive sync directory orchestration.
3This module provides the main sync directory coordination functionality,
4orchestrating folder processing, file collection, and parallel downloads per SRP.
5"""
7__author__ = "Mandar Patil (mandarons@pm.me)"
9import unicodedata
10from typing import Any
12from src import configure_icloudpy_logging, get_logger, recycle_bin, sync_drive
13from src.drive_cleanup import remove_obsolete
14from src.drive_filtering import wanted_parent_folder
15from src.drive_folder_processing import process_folder
16from src.drive_parallel_download import collect_file_for_download, execute_parallel_downloads
18# Configure icloudpy logging immediately after import
19configure_icloudpy_logging()
21LOGGER = get_logger()
24def sync_directory(
25 drive: Any,
26 destination_path: str,
27 items: Any,
28 root: str,
29 top: bool = True,
30 filters: dict[str, list[str]] | None = None,
31 ignore: list[str] | None = None,
32 remove: bool = False,
33 config: Any | None = None,
34) -> set[str]:
35 """Synchronize a directory from iCloud Drive to local filesystem.
37 This function orchestrates the entire sync process by:
38 1. Processing folders and recursively syncing subdirectories
39 2. Collecting files for parallel download
40 3. Executing parallel downloads
41 4. Cleaning up obsolete files if requested
43 Args:
44 drive: iCloud drive service instance
45 destination_path: Local destination directory
46 items: iCloud items to process
47 root: Root directory for relative path calculations
48 top: Whether this is the top-level sync call
49 filters: Dictionary of filters (folders, file_extensions)
50 ignore: List of ignore patterns
51 remove: Whether to remove obsolete local files
52 config: Configuration object
54 Returns:
55 Set of all processed file paths
56 """
57 files = set()
58 download_tasks = []
60 if not (drive and destination_path and items and root):
61 return files
63 # First pass: process folders and collect download tasks
64 for i in items:
65 item = drive[i]
67 if item.type in ("folder", "app_library"):
68 _process_folder_item(
69 item,
70 destination_path,
71 filters,
72 ignore,
73 root,
74 files,
75 config,
76 )
77 elif item.type == "file":
78 _process_file_item(
79 item,
80 destination_path,
81 filters,
82 ignore,
83 root,
84 files,
85 download_tasks,
86 config,
87 )
89 # Second pass: execute downloads in parallel
90 if download_tasks:
91 _execute_downloads(download_tasks, config)
93 # Final cleanup if this is the top-level call
94 if top and remove:
95 remove_obsolete(
96 destination_path=destination_path,
97 files=files,
98 recycle_bin=recycle_bin.from_config(config, "drive"),
99 )
101 return files
104def _process_folder_item(
105 item: Any,
106 destination_path: str,
107 filters: dict[str, list[str]] | None,
108 ignore: list[str] | None,
109 root: str,
110 files: set[str],
111 config: Any | None,
112) -> None:
113 """Process a single folder item.
115 Args:
116 item: iCloud folder item
117 destination_path: Local destination directory
118 filters: Dictionary of filters
119 ignore: List of ignore patterns
120 root: Root directory
121 files: Set to update with processed files
122 config: Configuration object
123 """
124 new_folder = process_folder(
125 item=item,
126 destination_path=destination_path,
127 filters=filters["folders"] if filters and "folders" in filters else None,
128 ignore=ignore,
129 root=root,
130 )
131 if not new_folder:
132 return
134 try:
135 files.add(unicodedata.normalize("NFC", new_folder))
136 # Recursively sync subdirectory
137 subdirectory_files = sync_directory(
138 drive=item,
139 destination_path=new_folder,
140 items=item.dir(),
141 root=root,
142 top=False,
143 filters=filters,
144 ignore=ignore,
145 config=config,
146 )
147 files.update(subdirectory_files)
148 except Exception:
149 # Continue execution to next item, without crashing the app
150 pass
153def _process_file_item(
154 item: Any,
155 destination_path: str,
156 filters: dict[str, list[str]] | None,
157 ignore: list[str] | None,
158 root: str,
159 files: set[str],
160 download_tasks: list[dict[str, Any]],
161 config: Any | None = None,
162) -> None:
163 """Process a single file item.
165 Args:
166 item: iCloud file item
167 destination_path: Local destination directory
168 filters: Dictionary of filters
169 ignore: List of ignore patterns
170 root: Root directory
171 files: Set to update with processed files
172 download_tasks: List to append download tasks to
173 config: Configuration object
174 """
175 if not wanted_parent_folder(
176 filters=filters["folders"] if filters and "folders" in filters else None,
177 ignore=ignore,
178 root=root,
179 folder_path=destination_path,
180 ):
181 return
183 try:
184 download_info = collect_file_for_download(
185 item=item,
186 destination_path=destination_path,
187 filters=filters["file_extensions"] if filters and "file_extensions" in filters else None,
188 ignore=ignore,
189 files=files,
190 config=config,
191 )
192 if download_info:
193 download_tasks.append(download_info)
194 except Exception:
195 # Continue execution to next item, without crashing the app
196 pass
199def _execute_downloads(download_tasks: list[dict[str, Any]], config: Any) -> None:
200 """Execute parallel downloads.
202 Args:
203 download_tasks: List of download task dictionaries
204 config: Configuration object
205 """
206 max_threads = sync_drive.get_max_threads(config)
207 execute_parallel_downloads(download_tasks, max_threads)