Search code examples
macoscocoaicloudnsfilemanager

Lost data while moving file out of iCloud that was not yet completely downloaded (no error reported)


I have a Mac app with iCloud integration. It's not based on NSDocument and I handle moving files in and out of iCloud myself via [NSFileManager setUbiquitous:…]. Here's what I ran into:

  1. I added a large (15 MB) document to my app while connected to iCloud
  2. Waited for document to be completely uploaded to iCloud
  3. Now I signed out of my iCloud account on my Mac → the file was removed from the Mac
  4. I opened my app, signed back in to iCloud, the file appeared on disk
  5. Quickly, through my app I moved all documents out of iCloud to the local disk (internally using [NSFileManager setUbiquitous:NO…]

The large document was not copied to the local disk (I suspect because it was not yet downloaded 100% from iCloud), but it also disappeared from iCloud. No way to recover the data. There was no error reported by NSFileManager.

Here is the relevant code:

NSArray *files = [fileManager contentsOfDirectoryAtURL:iCloudDataFolderURL
                            includingPropertiesForKeys:nil options:0 error:&error];

for (NSURL *fileURL in files) {
    // figure out URLs […]
    if (![fileManager setUbiquitous:NO 
                          itemAtURL:iCloudFileURL 
                     destinationURL:localDocumentURL 
                              error:&error]) {
        hadError = YES;
        NSLog(@"Error moving file from iCloud: %@ to local storage: %@ Error: %@",
               iCloudFileURL, localDocumentURL, error);
    }
}

I would have expected the call to [NSFileManager setUbiquitous:NO…] to either block or fail if the file is not completely on the local disk. Instead I end up with a file wrapper that shows a file size of 15 MB in Finder, but is actually empty.

What is a safe way to move documents out of iCloud to the local disk?


Solution

  • You can just use NSFileCoordinator and NSFileManager methods to move the files in and out. I don't know if it is safer, but it should give you more control over error conditions and what should happen when something goes wrong.

    For an example, take a look at the iCloud backend of the Ensembles framework here (Disclosure: it is my project). Search for uploadLocalFile:... and downloadFromPath:.... They are particularly advanced methods, with timeouts, but the idea is the same.

    First, create a file coordinator.

    NSFileCoordinator *coordinator = 
         [[NSFileCoordinator alloc] initWithFilePresenter:nil];
    

    Then copy or move the file.

        [coordinator coordinateReadingItemAtURL:fromURL options:0   
            writingItemAtURL:toURL 
            options:NSFileCoordinatorWritingForReplacing 
            error:&fileCoordinatorError 
            byAccessor:^(NSURL *newReadingURL, NSURL *newWritingURL) {
            [fileManager removeItemAtPath:newWritingURL.path error:NULL];
            [fileManager copyItemAtPath:newReadingURL.path 
                toPath:newWritingURL.path error:&fileManagerError];
        }];
    

    If you don't want to program this all yourself, I split off a class that may help (iCloudAccess).