Attachment
Use the Attachment feature to exchange large amounts — 250 KB or more — of binary data and files between devices and improve peer-to-peer sync performance. Unlike documents, attachments only sync between devices when explicitly fetched. As a result, the large amounts of data that attachments contain do not unnecessarily consume the local disk space attached to the Small Peer and mesh bandwidth.
The Attachment feature uses asynchronous fetch operations so that AttachmentFetcher
executes only
when the attachment data is available to fetch in the Small Peer or the mesh. To ensure that the
attachment fetcher remains a globally available instance and the fetch operation does not silently
abort, maintain a strong reference to the attachment fetcher for the entire duration of the
asynchronous fetch operation.
Attachment Feature Use Case
As an example, let’s evaluate how the Ditto Chat demo app for iOS uses the Attachment feature to optionally fetch an avatar image attachment from a collection named 'User'.
For the full code example, see the chat demo app for iOS.
struct User { var id: String var firstName: String var lastName: String var avatarToken: DittoAttachmentToken?}
let collection = ditto.store["users"]let userID = "1234567"
// add the Alice user to the "users" collectiondo { try collection.upsert( [ "_id": userID, "firstName": "Alice", "lastName": "Bluebonnet" ] as [String: Any?] )} catch { // handle error print(error.localizedDescription) return}
Creating a new Attachment
From the createImageMessage
function following the Large Image comment in the chat demo app
for
iOS
code, we first pass the User
instance and the URL.path
to the avatar image location, and then we
create a new DittoAttachment
object by calling newAttachment()
. Since the Chat demo app does not
require an avatar image to be uploaded for each user, we've declared the new attachment as an
optional object.
Next, we call the set()
function and pass the DittoAttachment
object using the
DittoMutableDocumentPath
instance, which links the User
document object ID to the attachment
object and stores the attachment’s data bytes and metadata properties to the Small Peer. And then we
initialize the Ditto attachment object using DittoAttachmentToken
, which we will call later to
fetch the attachment data asynchronously from either the Small Peer, if already fetched and
available from the local disk, or the peer-to-peer mesh network.
func addAvatar(to user: User, imagePath: String) { // optionally, add metadata as [String: String] let metadata = [ "filename": "\(userID)_avatar.png", "createdOn": ISO8601DateFormatter().string(from: Date()) ] let attachment = collection.newAttachment( path: imagePath, metadata: metadata )! // add the attachment to the Alice user document ditto.store["users"].findByID(user.id).update { mutableDoc in mutableDoc?["avatarToken"].set(attachment) }}
Note that on the User
model, the avatarToken
variable is of type DittoAttachmentToken
, yet we
call the set()
function with a DittoAttachment
object. This can be confusing. The set()
function, called with the attachment on a DittoMutableDocumentPath
instance, causes the data bytes
of the attachment at the given file location to be stored in the Ditto database, along with the
metadata; the document property is initialized with a DittoAttachmentToken
with which we will
later fetch the attachment data asnynchronously from the peer-to-peer mesh, or from local storage if it has
already been fetched.
Synchronizing the Attachment
Peers can now find the document, fetch the attachment, and use the attachment image. If you want to
update a progress view, use the progress
event value.
In the following example, we've wrapped
DittoCollection.fetchAttachment(token:deliverOn:onFetchEvent:)
in an
ImageAttachmentFetcher
struct for convenience.
struct ImageAttachmentFetcher { var fetcher: DittoAttachmentFetcher? init( ditto: Ditto, collection: String, token: DittoAttachmentToken, onProgress: @escaping (Double) -> Void, onComplete: @escaping (Result<UIImage, Error>) -> Void ) { self.fetcher = ditto.store[collection].fetchAttachment(token: token) { event in switch event { case .progress(let downloadedBytes, let totalBytes): let percent = Double(downloadedBytes) / Double(totalBytes) onProgress(percent) case .completed(let attachment): do { let data = try attachment.getData() if let uiImage = UIImage(data: data) { onComplete(.success(uiImage)) } } catch { onComplete(.failure(error)) } default: print("Error: event case \(event) not handled") } } }}
Notice that we update a progress view by calling the ImageAttachmentFetcher
struct with a progress
handling closure and a completion handler for handling the fetched image. Since the attachment
fetcher must remain a globally available instance for the entire duration of the asynchronous fetch
operation, we maintain a strong reference to the attachment fetcher as indicated with a property.
If, at any time, the attachment fetcher goes out of scope, asynchronous fetch operations silently aborts and the Attachment API fails.
// propertiesvar attachmentImage: UIImage?var imageFetcher: ImageAttachmentFetcher?
let doc = collection.findByID(user.id).exec()!let imageToken = doc["avatarToken"].attachmentToken!
imageFetcher = ImageAttachmentFetcher(ditto: ditto, collection: "users", token: imageToken, onProgress: { percentComplete in print("Percent complete: \(percentComplete)") }, onComplete: { result in switch result { case .success(let uiImage): attachmentImage = uiImage case .failure(let error): //handle error print(error.localizedDescription) } })
Using with Combine
If you also want to monitor attachment download progress in addition to using fetch operations,
instead of AttachedFetcher
, use the combined instance FetchAttachmentPublisher
.
Note that a FetchAttachmentPublisher
is also available. See the API
reference
for more information.