2

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.
Nazmul Islam
  • 142
  • 9

0 Answers0