@@ -4,8 +4,13 @@ import Foundation
44final class ICloudSyncService {
55 static let shared = ICloudSyncService ( )
66
7+ private static let temporaryFilePrefix = " voyager-icloud- "
78 private let container = CKContainer ( identifier: " iCloud.com.yourCompany.Gemini-Voyager " )
89
10+ private init ( ) {
11+ removeOrphanedTemporaryFiles ( )
12+ }
13+
914 private var database : CKDatabase {
1015 container. privateCloudDatabase
1116 }
@@ -24,7 +29,7 @@ final class ICloudSyncService {
2429
2530 func write( fileName: String , json: String , completion: @escaping ( Result < Void , Error > ) -> Void ) {
2631 let temporaryURL = FileManager . default. temporaryDirectory
27- . appendingPathComponent ( " voyager- \( UUID ( ) . uuidString) .json " )
32+ . appendingPathComponent ( " \( Self . temporaryFilePrefix ) \( UUID ( ) . uuidString) .json " )
2833
2934 do {
3035 try Data ( json. utf8) . write ( to: temporaryURL, options: . atomic)
@@ -97,6 +102,68 @@ final class ICloudSyncService {
97102 }
98103 }
99104
105+ func deleteBackup( completion: @escaping ( Result < Int , Error > ) -> Void ) {
106+ collectRecordIDs ( cursor: nil , collected: [ ] , completion: completion)
107+ }
108+
109+ private func collectRecordIDs(
110+ cursor: CKQueryOperation . Cursor ? ,
111+ collected: [ CKRecord . ID ] ,
112+ completion: @escaping ( Result < Int , Error > ) -> Void
113+ ) {
114+ let operation : CKQueryOperation
115+ if let cursor {
116+ operation = CKQueryOperation ( cursor: cursor)
117+ } else {
118+ operation = CKQueryOperation (
119+ query: CKQuery ( recordType: " VoyagerSyncFile " , predicate: NSPredicate ( value: true ) )
120+ )
121+ }
122+
123+ var recordIDs = collected
124+ operation. desiredKeys = [ ]
125+ operation. recordFetchedBlock = { record in
126+ recordIDs. append ( record. recordID)
127+ }
128+ operation. queryCompletionBlock = { nextCursor, error in
129+ if let error {
130+ completion ( . failure( VoyagerICloudFailureMapper . map ( error: error) ) )
131+ return
132+ }
133+ if let nextCursor {
134+ self . collectRecordIDs (
135+ cursor: nextCursor,
136+ collected: recordIDs,
137+ completion: completion
138+ )
139+ return
140+ }
141+ self . delete ( recordIDs: recordIDs, completion: completion)
142+ }
143+ database. add ( operation)
144+ }
145+
146+ private func delete(
147+ recordIDs: [ CKRecord . ID ] ,
148+ completion: @escaping ( Result < Int , Error > ) -> Void
149+ ) {
150+ guard !recordIDs. isEmpty else {
151+ completion ( . success( 0 ) )
152+ return
153+ }
154+
155+ let operation = CKModifyRecordsOperation ( recordIDsToDelete: recordIDs)
156+ operation. isAtomic = true
157+ operation. modifyRecordsCompletionBlock = { _, deletedRecordIDs, error in
158+ if let error {
159+ completion ( . failure( VoyagerICloudFailureMapper . map ( error: error) ) )
160+ } else {
161+ completion ( . success( deletedRecordIDs? . count ?? recordIDs. count) )
162+ }
163+ }
164+ database. add ( operation)
165+ }
166+
100167 private func save(
101168 record: CKRecord ,
102169 fileName: String ,
@@ -125,4 +192,18 @@ final class ICloudSyncService {
125192 CKRecord . ID ( recordName: VoyagerICloudRecordIdentity . recordName ( for: fileName) )
126193 }
127194
195+ private func removeOrphanedTemporaryFiles( ) {
196+ let fileManager = FileManager . default
197+ let directory = fileManager. temporaryDirectory
198+ guard
199+ let files = try ? fileManager. contentsOfDirectory (
200+ at: directory,
201+ includingPropertiesForKeys: nil
202+ )
203+ else { return }
204+
205+ for file in files where file. lastPathComponent. hasPrefix ( Self . temporaryFilePrefix) {
206+ try ? fileManager. removeItem ( at: file)
207+ }
208+ }
128209}
0 commit comments