|
| 1 | +## Attachments |
| 2 | + |
| 3 | +In many cases, you might want to sync large binary data (like images) along with the data synced by |
| 4 | +PowerSync. |
| 5 | +Embedding this data directly in your source databases is [inefficient and not recommended](https://docs.powersync.com/usage/use-case-examples/attachments). |
| 6 | + |
| 7 | +Instead, the PowerSync SDK for Dart and Flutter provides utilities you can use to _reference_ this binary data |
| 8 | +in your primary data model, and then download it from a secondary data store such as S3. |
| 9 | +Because binary data is not directly stored in the source database in this model, we call these files _attachments_. |
| 10 | + |
| 11 | +## Alpha release |
| 12 | + |
| 13 | +The attachment helpers described in this document are currently in an alpha state, intended for testing. |
| 14 | +Expect breaking changes and instability as development continues. |
| 15 | +The attachments API is marked as `@experimental` for this reason. |
| 16 | + |
| 17 | +Do not rely on these libraries for production use. |
| 18 | + |
| 19 | +## Usage |
| 20 | + |
| 21 | +An `AttachmentQueue` instance is used to manage and sync attachments in your app. |
| 22 | +The attachments' state is stored in a local-only attachments table. |
| 23 | + |
| 24 | +### Key assumptions |
| 25 | + |
| 26 | +- Each attachment is identified by a unique id. |
| 27 | +- Attachments are immutable once created. |
| 28 | +- Relational data should reference attachments using a foreign key column. |
| 29 | +- Relational data should reflect the holistic state of attachments at any given time. Any existing local attachment |
| 30 | + will be deleted locally if no relational data references it. |
| 31 | + |
| 32 | +### Example implementation |
| 33 | + |
| 34 | +See the [supabase todolist](https://github.com/powersync-ja/powersync.dart/tree/main/demos/supabase-todolist) demo for |
| 35 | +a basic example of attachment syncing. |
| 36 | + |
| 37 | +### Setup |
| 38 | + |
| 39 | +First, add a table storing local attachment state to your database schema. |
| 40 | + |
| 41 | +```dart |
| 42 | +final schema = Schema([ |
| 43 | + AttachmentsQueueTable(), |
| 44 | + // In this document, we assume the photo_id column of the todos table references an optional photo |
| 45 | + // stored as an attachment. |
| 46 | + Table('todos', [ |
| 47 | + Column.text('list_id'), |
| 48 | + Column.text('photo_id'), |
| 49 | + Column.text('description'), |
| 50 | + Column.integer('completed'), |
| 51 | + ]), |
| 52 | +]); |
| 53 | +``` |
| 54 | + |
| 55 | +Next, create an `AttachmentQueue` instance. This class provides default syncing utilities and implements a default |
| 56 | +sync strategy. This class can be extended for custom functionality, if needed. |
| 57 | + |
| 58 | +```dart |
| 59 | +final directory = await getApplicationDocumentsDirectory(); |
| 60 | +
|
| 61 | +final attachmentQueue = AttachmentQueue( |
| 62 | + db: db, |
| 63 | + remoteStorage: SupabaseStorageAdapter(), // instance responsible for uploads and downloads |
| 64 | + logger: logger, |
| 65 | + localStorage: IOLocalStorage(appDocDir), // IOLocalStorage requires `dart:io` and is not available on the web |
| 66 | + watchAttachments: () => db.watch(''' |
| 67 | + SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL |
| 68 | + ''').map((results) => [ |
| 69 | + for (final row in results) |
| 70 | + WatchedAttachmentItem( |
| 71 | + id: row['id'] as String, |
| 72 | + fileExtension: 'jpg', |
| 73 | + ) |
| 74 | + ], |
| 75 | + ), |
| 76 | +); |
| 77 | +``` |
| 78 | + |
| 79 | +Here, |
| 80 | + |
| 81 | + - An instance of `LocalStorageAdapter`, such as the `IOLocalStorage` provided by the SDK, is responsible for storing |
| 82 | + attachment contents locally. |
| 83 | + - An instance of `RemoteStorageAdapter` is responsible for downloading and uploading attachment contents to the secondary |
| 84 | + service, such as S3, Firebase cloud storage or Supabase storage. |
| 85 | + - `watchAttachments` is a function emitting a stream of attachment items that are considered to be referenced from |
| 86 | + the current database state. In this example, `todos.photo_id` is the only column referencing attachments. |
| 87 | + |
| 88 | +Next, start the sync process by calling `attachmentQueue.startSync()`. |
| 89 | + |
| 90 | +## Storing attachments |
| 91 | + |
| 92 | +To create a new attachment locally, call `AttachmentQueue.saveFile`. To represent the attachment, this method takes |
| 93 | +the contents to store, the media type, an optional file extension and id. |
| 94 | +The queue will store the contents in a local file and mark is as queued for uploads. It also invokes a callback |
| 95 | +responsible for referencing the id of the generated attachment in the primary data model: |
| 96 | + |
| 97 | +```dart |
| 98 | +Future<Attachment> savePhotoAttachment( |
| 99 | + Stream<List<int>> photoData, String todoId, |
| 100 | + {String mediaType = 'image/jpeg'}) async { |
| 101 | + // Save the file using the AttachmentQueue API |
| 102 | + return await attachmentQueue.saveFile( |
| 103 | + data: photoData, |
| 104 | + mediaType: mediaType, |
| 105 | + fileExtension: 'jpg', |
| 106 | + metaData: 'Photo attachment for todo: $todoId', |
| 107 | + updateHook: (context, attachment) async { |
| 108 | + // Update the todo item to reference this attachment |
| 109 | + await context.execute( |
| 110 | + 'UPDATE todos SET photo_id = ? WHERE id = ?', |
| 111 | + [attachment.id, todoId], |
| 112 | + ); |
| 113 | + }, |
| 114 | + ); |
| 115 | +} |
| 116 | +``` |
| 117 | + |
| 118 | +## Deleting attachments |
| 119 | + |
| 120 | +To delete attachments, it is sufficient to stop referencing them in the data model, e.g. via |
| 121 | +`UPDATE todos SET photo_id = NULL` in this example. The attachment sync implementation will eventually |
| 122 | +delete orphaned attachments from the local storage. |
0 commit comments