Coverage for src/drive_file_existence.py: 100%
49 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-06 02:49 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-06 02:49 +0000
1"""File existence checking utilities.
3This module provides file and package existence checking functionality,
4separating existence validation logic from sync operations per SRP.
5"""
7__author__ = "Mandar Patil (mandarons@pm.me)"
9import os
10from datetime import timezone
11from pathlib import Path
12from shutil import rmtree
13from typing import Any
15from src import DEFAULT_REQUEST_TIMEOUT_SEC, configure_icloudpy_logging, get_logger
17# Configure icloudpy logging immediately after import
18configure_icloudpy_logging()
20LOGGER = get_logger()
23def file_exists(item: Any, local_file: str) -> bool:
24 """Check if a file exists locally and is up-to-date.
26 Args:
27 item: iCloud file item with date_modified and size attributes
28 local_file: Path to the local file
30 Returns:
31 True if file exists and is up-to-date, False otherwise
32 """
33 if not (item and local_file and os.path.isfile(local_file)):
34 LOGGER.debug(f"File {local_file} does not exist locally.")
35 return False
37 local_file_modified_time = int(os.path.getmtime(local_file))
38 # iCloudPy produces date_modified via strptime(..., "%Y-%m-%dT%H:%M:%SZ") — always
39 # naive UTC with no tzinfo. replace(tzinfo=UTC) is the correct conversion.
40 remote_file_modified_time = int(item.date_modified.replace(tzinfo=timezone.utc).timestamp())
41 local_file_size = os.path.getsize(local_file)
42 remote_file_size = item.size
44 if local_file_modified_time == remote_file_modified_time and (
45 local_file_size == remote_file_size
46 or (local_file_size == 0 and remote_file_size is None)
47 or (local_file_size is None and remote_file_size == 0)
48 ):
49 LOGGER.debug(f"No changes detected. Skipping the file {local_file} ...")
50 return True
52 LOGGER.debug(
53 f"Changes detected: local_modified_time is {local_file_modified_time}, "
54 + f"remote_modified_time is {remote_file_modified_time}, "
55 + f"local_file_size is {local_file_size} and remote_file_size is {remote_file_size}.",
56 )
57 return False
60def package_exists(item: Any, local_package_path: str) -> bool:
61 """Check if a package exists locally and is up-to-date.
63 Args:
64 item: iCloud package item with date_modified and size attributes
65 local_package_path: Path to the local package directory
67 Returns:
68 True if package exists and is up-to-date, False otherwise
69 """
70 if not (item and local_package_path and os.path.isdir(local_package_path)):
71 LOGGER.debug(f"Package {local_package_path} does not exist locally.")
72 return False
74 local_package_modified_time = int(os.path.getmtime(local_package_path))
75 # iCloudPy produces date_modified via strptime(..., "%Y-%m-%dT%H:%M:%SZ") — always
76 # naive UTC with no tzinfo. replace(tzinfo=UTC) is the correct conversion.
77 remote_package_modified_time = int(item.date_modified.replace(tzinfo=timezone.utc).timestamp())
78 local_package_size = sum(f.stat().st_size for f in Path(local_package_path).glob("**/*") if f.is_file())
79 remote_package_size = item.size
81 if local_package_modified_time == remote_package_modified_time and local_package_size == remote_package_size:
82 LOGGER.debug(f"No changes detected. Skipping the package {local_package_path} ...")
83 return True
85 LOGGER.info(
86 f"Changes detected: local_modified_time is {local_package_modified_time}, "
87 + f"remote_modified_time is {remote_package_modified_time}, "
88 + f"local_package_size is {local_package_size} and remote_package_size is {remote_package_size}.",
89 )
90 rmtree(local_package_path)
91 return False
94def is_package(item: Any, timeout: int = DEFAULT_REQUEST_TIMEOUT_SEC) -> bool:
95 """Determine if an iCloud item is a package that needs special handling.
97 Args:
98 item: iCloud item to check
99 timeout: HTTP read timeout in seconds (default: DEFAULT_REQUEST_TIMEOUT_SEC)
101 Returns:
102 True if item is a package, False otherwise
103 """
104 file_is_a_package = False
105 try:
106 with item.open(stream=True, timeout=timeout) as response:
107 file_is_a_package = response.url and "/packageDownload?" in response.url
108 except Exception as e:
109 # Enhanced error logging with file context
110 # This catches all exceptions including iCloudPy errors like ObjectNotFoundException
111 error_msg = str(e)
112 item_name = getattr(item, "name", "Unknown file")
113 if "ObjectNotFoundException" in error_msg or "NOT_FOUND" in error_msg:
114 LOGGER.error(f"File not found in iCloud Drive while checking package type - {item_name}: {error_msg}")
115 else:
116 LOGGER.error(f"Failed to check package type for {item_name}: {error_msg}")
117 # Return False if we can't determine package type due to error
118 file_is_a_package = False
119 return file_is_a_package