1313import logging
1414import os
1515import sqlite3
16+ import time
1617from dataclasses import dataclass
1718from datetime import datetime , timezone
1819
@@ -133,6 +134,9 @@ class ManifestRow:
133134class ManifestDB :
134135 """SQLite-backed asset manifest for tracking downloaded files."""
135136
137+ _MAX_WRITE_RETRIES = 3
138+ _RETRY_BASE_DELAY = 0.1
139+
136140 def __init__ (self , download_dir : str ) -> None :
137141 self ._db_path = os .path .join (download_dir , ".icloudpd.db" )
138142 self ._conn : sqlite3 .Connection | None = None
@@ -229,7 +233,13 @@ def close(self) -> None:
229233 """Close the manifest DB, committing any pending writes."""
230234 if self ._conn :
231235 if self ._dirty :
232- self ._conn .commit ()
236+ try :
237+ self ._conn .commit ()
238+ except sqlite3 .OperationalError as e :
239+ if "locked" in str (e ):
240+ logger .warning ("Manifest commit on close failed: %s" , e )
241+ else :
242+ raise
233243 self ._dirty = False
234244 self ._pending_count = 0
235245 self ._conn .close ()
@@ -238,9 +248,15 @@ def close(self) -> None:
238248 def flush (self ) -> None :
239249 """Commit pending writes without closing."""
240250 if self ._conn and self ._dirty :
241- self ._conn .commit ()
242- self ._dirty = False
243- self ._pending_count = 0
251+ try :
252+ self ._conn .commit ()
253+ self ._dirty = False
254+ self ._pending_count = 0
255+ except sqlite3 .OperationalError as e :
256+ if "locked" in str (e ):
257+ logger .warning ("Manifest flush failed: %s" , e )
258+ else :
259+ raise
244260
245261 def __enter__ (self ) -> "ManifestDB" :
246262 self .open ()
@@ -308,69 +324,90 @@ def upsert(
308324 raw_fields : str | None = None ,
309325 ) -> None :
310326 """Insert or update a manifest entry. Auto-flushes every 500 writes."""
327+ now = datetime .now (tz = timezone .utc ).isoformat ()
328+ params = (
329+ asset_id , zone_id , local_path , version_size , version_checksum ,
330+ change_tag , now , now , item_type , filename ,
331+ asset_date , added_date , is_favorite , is_hidden , is_deleted ,
332+ original_width , original_height , duration , orientation ,
333+ title , description , keywords , gps_latitude , gps_longitude , gps_altitude ,
334+ gps_speed , gps_timestamp , timezone_offset , asset_subtype , hdr_type ,
335+ burst_flags , burst_flags_ext , burst_id , original_orientation , raw_fields ,
336+ )
337+ sql = (
338+ f"INSERT INTO manifest ({ _ALL_COLUMNS } ) "
339+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) "
340+ "ON CONFLICT(asset_id, zone_id, local_path) DO UPDATE SET "
341+ "version_size=excluded.version_size, "
342+ "version_checksum=excluded.version_checksum, "
343+ "change_tag=excluded.change_tag, "
344+ "last_updated_at=excluded.last_updated_at, "
345+ "item_type=excluded.item_type, "
346+ "filename=excluded.filename, "
347+ "asset_date=excluded.asset_date, "
348+ "added_date=excluded.added_date, "
349+ "is_favorite=excluded.is_favorite, "
350+ "is_hidden=excluded.is_hidden, "
351+ "is_deleted=excluded.is_deleted, "
352+ "original_width=excluded.original_width, "
353+ "original_height=excluded.original_height, "
354+ "duration=excluded.duration, "
355+ "orientation=excluded.orientation, "
356+ "title=excluded.title, "
357+ "description=excluded.description, "
358+ "keywords=excluded.keywords, "
359+ "gps_latitude=excluded.gps_latitude, "
360+ "gps_longitude=excluded.gps_longitude, "
361+ "gps_altitude=excluded.gps_altitude, "
362+ "gps_speed=excluded.gps_speed, "
363+ "gps_timestamp=excluded.gps_timestamp, "
364+ "timezone_offset=excluded.timezone_offset, "
365+ "asset_subtype=excluded.asset_subtype, "
366+ "hdr_type=excluded.hdr_type, "
367+ "burst_flags=excluded.burst_flags, "
368+ "burst_flags_ext=excluded.burst_flags_ext, "
369+ "burst_id=excluded.burst_id, "
370+ "original_orientation=excluded.original_orientation, "
371+ "raw_fields=excluded.raw_fields"
372+ )
373+ for attempt in range (self ._MAX_WRITE_RETRIES ):
374+ try :
375+ self ._db .execute (sql , params )
376+ self ._dirty = True
377+ self ._pending_count += 1
378+ if self ._pending_count >= self ._flush_interval :
379+ self .flush ()
380+ return
381+ except sqlite3 .OperationalError as e :
382+ if "locked" in str (e ) and attempt < self ._MAX_WRITE_RETRIES - 1 :
383+ logger .debug (
384+ "Manifest write retry %d for %s: %s" ,
385+ attempt + 1 , local_path , e ,
386+ )
387+ time .sleep (self ._RETRY_BASE_DELAY * (attempt + 1 ))
388+ else :
389+ logger .warning ("Manifest write failed for %s: %s" , local_path , e )
390+ return
391+ except sqlite3 .Error as e :
392+ logger .warning ("Manifest write failed for %s: %s" , local_path , e )
393+ return
394+
395+ def update_path (self , asset_id : str , zone_id : str , old_path : str , new_path : str ) -> None :
396+ """Update local_path for an existing manifest entry."""
311397 try :
312- now = datetime .now (tz = timezone .utc ).isoformat ()
313398 self ._db .execute (
314- f"INSERT INTO manifest ({ _ALL_COLUMNS } ) "
315- "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) "
316- "ON CONFLICT(asset_id, zone_id, local_path) DO UPDATE SET "
317- "version_size=excluded.version_size, "
318- "version_checksum=excluded.version_checksum, "
319- "change_tag=excluded.change_tag, "
320- "last_updated_at=excluded.last_updated_at, "
321- "item_type=excluded.item_type, "
322- "filename=excluded.filename, "
323- "asset_date=excluded.asset_date, "
324- "added_date=excluded.added_date, "
325- "is_favorite=excluded.is_favorite, "
326- "is_hidden=excluded.is_hidden, "
327- "is_deleted=excluded.is_deleted, "
328- "original_width=excluded.original_width, "
329- "original_height=excluded.original_height, "
330- "duration=excluded.duration, "
331- "orientation=excluded.orientation, "
332- "title=excluded.title, "
333- "description=excluded.description, "
334- "keywords=excluded.keywords, "
335- "gps_latitude=excluded.gps_latitude, "
336- "gps_longitude=excluded.gps_longitude, "
337- "gps_altitude=excluded.gps_altitude, "
338- "gps_speed=excluded.gps_speed, "
339- "gps_timestamp=excluded.gps_timestamp, "
340- "timezone_offset=excluded.timezone_offset, "
341- "asset_subtype=excluded.asset_subtype, "
342- "hdr_type=excluded.hdr_type, "
343- "burst_flags=excluded.burst_flags, "
344- "burst_flags_ext=excluded.burst_flags_ext, "
345- "burst_id=excluded.burst_id, "
346- "original_orientation=excluded.original_orientation, "
347- "raw_fields=excluded.raw_fields" ,
348- (
349- asset_id , zone_id , local_path , version_size , version_checksum ,
350- change_tag , now , now , item_type , filename ,
351- asset_date , added_date , is_favorite , is_hidden , is_deleted ,
352- original_width , original_height , duration , orientation ,
353- title , description , keywords , gps_latitude , gps_longitude , gps_altitude ,
354- gps_speed , gps_timestamp , timezone_offset , asset_subtype , hdr_type ,
355- burst_flags , burst_flags_ext , burst_id , original_orientation , raw_fields ,
356- ),
399+ "UPDATE manifest SET local_path = ?, last_updated_at = ? "
400+ "WHERE asset_id = ? AND zone_id = ? AND local_path = ?" ,
401+ (new_path , datetime .now (tz = timezone .utc ).isoformat (),
402+ asset_id , zone_id , old_path ),
357403 )
358404 self ._dirty = True
359405 self ._pending_count += 1
360- if self ._pending_count >= self ._flush_interval :
361- self .flush ()
362406 except sqlite3 .Error as e :
363- logger .warning ("Manifest write failed for %s: %s" , local_path , e )
364-
365- def update_path (self , asset_id : str , zone_id : str , old_path : str , new_path : str ) -> None :
366- """Update local_path for an existing manifest entry."""
367- self ._db .execute (
368- "UPDATE manifest SET local_path = ?, last_updated_at = ? "
369- "WHERE asset_id = ? AND zone_id = ? AND local_path = ?" ,
370- (new_path , datetime .now (tz = timezone .utc ).isoformat (),
371- asset_id , zone_id , old_path ),
372- )
373- self ._dirty = True
407+ logger .warning (
408+ "Manifest path update failed for %s -> %s: %s" ,
409+ old_path , new_path , e ,
410+ )
374411
375412 def count_by_path (self , local_path : str ) -> int :
376413 """Count how many manifest entries reference a given local path."""
0 commit comments