Mark
Mark

Reputation: 6977

How can I delete a file from iCloud without downloading it to the device first?

I have a basic iOS app that shows a list of documents. I'm trying to delete a document, but noticed that the code below fails with "No such file or directory" if the document has not yet been downloaded from iCloud to the device.

Documents can be quite large (40MB) and I'd like to avoid downloading the document only to delete it (this takes time and bandwidth out of the user's data plan). Is this possible at all?

[[[NSFileCoordinator alloc] initWithFilePresenter:nil]
coordinateWritingItemAtURL:documentURL
                  options:NSFileCoordinatorWritingForDeleting
         writingItemAtURL:previewURL
                  options:NSFileCoordinatorWritingForDeleting
                    error:&error
               byAccessor:^(NSURL *newDocumentURL, NSURL *newPreviewURL){

    // Fails with "No such file" error if not yet downloaded from iCloud:
    [[NSFileManager defaultManager] removeItemAtURL:newDocumentURL error:&error];
    [[NSFileManager defaultManager] removeItemAtURL:newPreviewURL  error:&error];
}];

The full error:

Error Domain=NSCocoaErrorDomain Code=4 "The operation couldn’t be completed. 
(Cocoa error 4.)" UserInfo=0x14e82930 {NSUnderlyingError=0x14e69220 
"The operation couldn’t be completed. No such file or directory",

Upvotes: 4

Views: 1863

Answers (2)

Daniel Galasko
Daniel Galasko

Reputation: 24247

note: Apple used to have some sample code to illustrate this but alas it has been removed.

As has been pointed out in the answers you would need to call this method in a loop since you need a separate NSFileCoordinator for each file you would like to delete.

Something that I missed was that you have to call fileManager.removeItemAtURL with the URL of the NSFileAccessIntent object that you create for deleting and not the normal URL that you get access to from your NSMetadataQueryItem.

func removeFile(at url: URL, completionHandler: ((Error?) -> Void)? = nil) {
    // `url` may be a security scoped resource.
    let successfulSecurityScopedResourceAccess = url.startAccessingSecurityScopedResource()

    let fileCoordinator = NSFileCoordinator()
    let writingIntent = NSFileAccessIntent.writingIntent(with: url, options: .forDeleting)
    fileCoordinator.coordinate(with: [writingIntent], queue: backgroundQueue) { (accessError) in
        if accessError != nil {
            completionHandler?(accessError)
            return
        }

        let fileManager = FileManager()
        var error: Error?
        do {
            try fileManager.removeItem(at: writingIntent.url)
        }
        catch let fileError {
            error = fileError
        }
        if successfulSecurityScopedResourceAccess {
            url.stopAccessingSecurityScopedResource()
        }

        completionHandler?(error)
    }
}

If you wanted to delete multiple items:

for url in urlsToDelete {
    removeFile(at: url)
}

Upvotes: 6

Gregzo
Gregzo

Reputation: 1194

If you just need to delete a file, use the other NSFileCoordinator coordinateWritingItemAtURL ( the one with a single newURL parameter in the accessor block ).

If you need to batch delete, then create an array of NSFileAccessIntent and use NSFileCoordinator's coordinateAccessWithIntents.

Example:

- ( void )deleteItemsAtURLs: ( NSArray * )urls queue: ( NSOperationQueue * )queue
{
    //assuming urls is an array of urls to be deleted
    NSFileCoordinator   * coordinator;
    NSMutableArray      * writingIntents;
    NSURL               * url;

    writingIntents = [ NSMutableArray arrayWithCapacity: urls.count ];

    for( url in urls )
    {
        [ writingIntents addObject: [ NSFileAccessIntent writingIntentWithURL: url options: NSFileCoordinatorWritingForDeleting ] ];
    }

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

    [ coordinator coordinateAccessWithIntents: writingIntents
                                        queue: queue
                                   byAccessor: ^( NSError * error )
     {
         if( error )
         {
             //handle
             return;
         }
         NSFileAccessIntent * intent;

         error = nil;

         for( intent in writingIntents )
         {
             [ [ NSFileManager defaultManager ] removeItemAtURL: intent.URL error: &error ];
             if( error )
             {
                 //handle
             }
         }
     }];
}

Upvotes: 2

Related Questions