Skip to content

Deferred Attachment Downloading

Zachary Gramana edited this page Jan 27, 2016 · 11 revisions

Since document attachments can be arbitrarily large, you may want to skip downloading them during a pull replication, both to speed up the replication and to save bandwidth. Then of course you need a way to download those attachment(s) later if they're needed locally.

NOTE: Aug 2015: This feature isn't yet available in a release. The API is preliminary and subject to change.

Skipping Attachments

To skip downloading attachments, set the CBLReplication property downloadAttachments to NO before starting the replication. This will cause this replicator to skip the content of all attachments.

CBLReplication* pull = [db createPullReplication: kRemoteURL];
pull.downloadAttachments = NO;
[pull start];

The metadata of the attachments is still available via CBLAttachment objects (or the _attachments property), as usual. But if an attachment hasn't been downloaded, its content and contentURL properties, and the openContentStream method, will all return nil. To quickly check whether an attachment's content is available without reading or opening it, use the new boolean contentAvailable property.

CBLAttachment *att = [doc.currentRevision attachmentNamed: @"bigImage"];
UIImage* bigImage = nil;
if (att.contentAvailable) {
    bigImage = [UIImage imageWithData: att.content];
}
self.imageView.image = bigImage ? bigImage : kPlaceholderImage;

Deferred Downloading

If you need the content of an attachment, you request it from the pull replication by calling its -downloadAttachment: method. The return value is an NSProgress object that can report progress and notify you when the attachment completes (or if it fails), and can be told to cancel the download.

NOTE: This section shows the basics, without the extra code to use Key-Value Observing to track the status of the download; that's shown below under Using Key-Value Observing.

Starting a download

self.progress = [pullReplication downloadAttachment: att];

(The CBLReplication doesn't have to be the exact same instance that was used to pull the document containing the attachment. Any pull replication with the same remote database URL will work.)

Observing progress

The simplest way to observe progress is just to set a timer, at an interval of maybe once a second, and check the state of the NSProgress object when the timer fires. If the download fails, the userInfo's kCBLProgressErrorKey will be set to an NSError object. Otherwise, the download is complete when the progress's completedUnitCount and totalUnitCount are equal and non-negative. (Negative values indicate the progress is in an indeterminate state, as it will be until the request is sent to the server.)

NSError* error = progress.userInfo[kCBLProgressErrorKey];
if (error) {
    [self attachmentFailed: error];
} else if (progress.completedUnitCount >= 0 && 
            progress.completedUnitCount == progress.totalUnitCount) {
    [self attachmentFinished];
} else {
    [self attachmentProgress: progress.fractionCompleted];
}

If you want immediate notification you can use key-value observing, as shown in the next section.

Canceling a download

[self.progress cancel];
self.progress = nil;

Removing a downloaded attachment

If you want to later undo a download, or if for whatever reason you don't want the contents of an attachment anymore, call the new method -[CBLAttachment purge].

CBLAttachment *att = [doc.currentRevision attachmentNamed: @"bigImage"];
[att purge];

Using Key-Value Observing (KVO)

Key-Value Observing is the idiomatic way to observe changes to the status of an object, and NSProgress supports it. You'll want to observe its fractionCompleted property, to watch for download progress and completion, and also the kCBLProgressErrorKey property of the NSProgress's userInfo.

Starting a download (with KVO)

self.progress = [pullReplication downloadAttachment: att];
[progress addObserver: self forKeyPath: @"fractionCompleted" options: 0 context: NULL];
[progress.userInfo addObserver: self forKeyPath: kCBLProgressErrorKey options: 0 context: NULL];

Observing progress (with KVO)

The NSProgress object will be updated on the background replicator thread, so the KVO observation calls will happen on that thread too! Make sure your observer code is thread-safe, or dispatch it to your work thread/queue, as shown in this example:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
                        change:(NSDictionary *)change context:(void *)context
{
    // (This method assumes the object doesn't handle any other KVO notifications!)
    dispatch_async(dispatch_get_main_queue(), ^{
        // Now we're safely on the main thread and can do UI stuff:
        NSProgress* progress = self.progress;
        NSError* error = progress.userInfo[kCBLProgressErrorKey];
        if (error != nil || progress.completedUnitCount == progress.totalUnitCount) {
            [progress removeObserver: self forKeyPath: @"fractionCompleted"];
            [progress.userInfo removeObserver: self forKeyPath: kCBLProgressErrorKey];
            self.progress = nil;
            if (error != nil) {
                [self downloadFailedWithError: error];  // your own method
            } else {
                [self downloadComplete];  // your own method
            }
        } else {
            [self updateDownloadProgress: progress.fractionCompleted];
        }
    });
}

Canceling a download (with KVO)

Always remember to remove the observers you added!

[self.progress removeObserver: self forKeyPath: @"fractionCompleted"];
[self.progress.userInfo removeObserver: self forKeyPath: kCBLProgressErrorKey];
[self.progress cancel];
self.progress = nil;

Issues & Limitations

Some of these may be addressed before this feature appears in a release.

  • There's not currently any way to pull some attachments automatically but not others. It's all or nothing.
  • Multiple calls to download the same attachment will issue redundant downloads, wasting network bandwidth. (Only one copy will be saved to disk, though.)
  • Attachment downloading isn't as fault-tolerant as regular replication: if there's no network connectivity or the server isn't reachable, the request will retry a few times but eventually fail after about 30 seconds.
  • If you retry an interrupted download, it starts over from the beginning instead of where it left off.
  • There's no way yet to cancel or pause a download.
  • There's no way to "un-download" an attachment, i.e. purge an attachment from local storage.
Clone this wiki locally