I'm trying to backup(not sync) my app's user data and database using iCloud. What I'm trying to achieve is the on-demand backup and restore feature. I've created a folder structure like ubiquityContainerRoot/userId/deviceId/Documents/
and ubiquityContainerRoot/userId/deviceId/Database/
. Both of these folders will contain files of miscellaneous types.
I need a way to properly monitor the progress of these files being uploaded to iCloud i.e I need to know when all the files ubiquityContainerRoot/userId/deviceId/
have been uploaded to iCloud and progress as a whole (from the perspective of the folder ubiquityContainerRoot/userId/deviceId/
).
What I've done so far is this
- Copied the user data and database in corresponding ubiquity container URL
- Created a NSMetaDataQuery to monitor
NSMetadataQueryUbiquitousDataScope
- Tracking progress for each file manually and calculating overall progress every time the query fires
My code is as below:
private var query: NSMetadataQuery?
private let notificationCenter = NotificationCenter.default
private var fileSizeMap = [URL: Double]()
private var progressMap = [URL: Double]()
func test() {
let fileManager = FileManager.default
let documentsSourceDirectory = fileManager
.urls(for: .documentDirectory, in: .userDomainMask).first!
print("documentsSourceDirectory: \(documentsSourceDirectory.path)")
let databaseSourceDirectory = DATABASE_URL
print("databaseSourceDirectory: \(databaseSourceDirectory.path)")
let userId = "USER_ID"
let deviceId = DEVICE_ID
let iCloudContainerRoot = fileManager.url(forUbiquityContainerIdentifier: iCLOUD_IDENITIFiER)!
.appendingPathComponent(userId)
.appendingPathComponent(deviceId)
print("iCloudContainerRoot: \(iCloudContainerRoot.path)")
let documentsCloudDirectory = iCloudContainerRoot
.appendingPathComponent("Documents")
let databaseCloudDirectory = iCloudContainerRoot
.appendingPathComponent("Database")
do {
try fileManager.copyAndOverwriteItem(at: documentsSourceDirectory, to: documentsCloudDirectory)
try fileManager.copyAndOverwriteItem(at: databaseSourceDirectory, to: databaseCloudDirectory)
print("Copied data to iCloud.")
} catch {
fatalError(error.localizedDescription)
}
fileSizeMap = [:]
progressMap = [:]
populateFileSizeMap(cloudURL: documentsCloudDirectory)
populateFileSizeMap(cloudURL: databaseCloudDirectory)
createQuery()
}
private func createQuery() {
let query = NSMetadataQuery()
query.operationQueue = .main
query.searchScopes = [NSMetadataQueryUbiquitousDataScope]
query.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemFSNameKey, "*")
self.query = query
notificationCenter.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: query.operationQueue) {
[weak self] (notification) in
print("NSMetadataQueryDidFinishGathering")
self?.queryDidFire(notification: notification)
}
notificationCenter.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: query.operationQueue) {
[weak self] (notification) in
print("NSMetadataQueryDidUpdate")
self?.queryDidFire(notification: notification)
}
query.operationQueue?.addOperation {
print("starting query")
query.start()
query.enableUpdates()
}
}
private func queryDidFire(notification: Notification) {
guard let query = notification.object as? NSMetadataQuery else {
print("Can not retrieve query from notification.")
return
}
// without disabling the query when processing, app might crash randomly
query.disableUpdates()
print("Result count: \(query.results.count)")
for result in query.results {
if let item = result as? NSMetadataItem {
handeMetadataItem(item)
} else {
print("Not a meta data item")
}
}
// reenable updates on the query
query.enableUpdates()
}
private func handeMetadataItem(_ item: NSMetadataItem) {
if let error = item.value(forAttribute: NSMetadataUbiquitousItemUploadingErrorKey) as? NSError {
print("Item error: \(error.localizedDescription)")
return
}
let srcURL = item.value(forAttribute: NSMetadataItemURLKey) as! URL
print("Item URL: \(srcURL.path)")
if let progress = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double {
print("Item upload progress: \(progress)")
handleProgress(for: srcURL, progress: progress)
}
if let isUploaded = item.value(forAttribute: NSMetadataUbiquitousItemIsUploadedKey) as? Bool,
let isUploading = item.value(forAttribute: NSMetadataUbiquitousItemIsUploadingKey) as? Bool
{
print("Item isUploaded: \(isUploaded), isUploading: \(isUploading)")
}
}
private func populateFileSizeMap(cloudURL: URL) {
let fileURLs = try! FileManager.default.contentsOfDirectory(at: cloudURL, includingPropertiesForKeys: nil)
for fileURL in fileURLs {
do {
let properties = try fileURL.resourceValues(forKeys: [.fileSizeKey])
let fileSize = properties.fileSize ?? 0
fileSizeMap[fileURL] = Double(fileSize)
} catch {
fatalError("Can not retrieve file size for file at: \(fileURL.path)")
}
}
}
private func handleProgress(for fileURL: URL, progress: Double) {
guard let fileSize = fileSizeMap[fileURL] else { return }
let prevUploadedSize = progressMap[fileURL] ?? 0
let currUploadedSize = fileSize * (progress / 100.0)
guard currUploadedSize >= prevUploadedSize else {
fatalError("Metadata query reported less upload percentage than before")
}
progressMap[fileURL] = currUploadedSize
let totalSizeToUpload = fileSizeMap.values.reduce(0) { $0 + $1 }
print("totalSizeToUpload: \(totalSizeToUpload)")
let totalSizeUploaded = progressMap.values.reduce(0) { $0 + $1 }
print("totalSizeUploaded: \(totalSizeUploaded)")
let uploadPercentage = (totalSizeUploaded / totalSizeToUpload) * 100
print("uploadPercentage: \(uploadPercentage)")
}
Now I want to know
- is this the correct way to implement backup/restore (code for restore is not posted here) with iCloud or is there a better API for achieving this?
- is there a way to monitor upload progress for a whole folder with NSMetaDataQuery?
- how do I know the upload progress if the user goes background and enters foreground later, do I need to need to deploy another NSMetaDataQuery(just create a query without copying the data again) to monitor the changes or is there a way to do something that will awake my app in the background when backup completed?
I'm also facing some problems any help on which is highly appreciated
- sometimes deleting the iCloud through the settings app from a device does not clear backup data on another device with the same iCloud account logged in for a long period of time.