How to properly Implement backup/restore(not sync, not core data app, not document based app) feature with iCloud?

151 views Asked by At

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.
0

There are 0 answers