Skip to content

Commit d405ddd

Browse files
Cleanup demo. Verify local storage.
1 parent 495ce97 commit d405ddd

File tree

4 files changed

+105
-58
lines changed

4 files changed

+105
-58
lines changed

Demo/PowerSyncExample/PowerSync/SystemManager.swift

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,45 @@ func getAttachmentsDirectoryPath() throws -> String {
1111
return documentsURL.appendingPathComponent("attachments").path
1212
}
1313

14+
let logTag = "SystemManager"
15+
1416
@Observable
1517
class SystemManager {
1618
let connector = SupabaseConnector()
1719
let schema = AppSchema
1820
let db: PowerSyncDatabaseProtocol
19-
21+
2022
var attachments: AttachmentQueue?
21-
23+
2224
init() {
2325
db = PowerSyncDatabase(
2426
schema: schema,
2527
dbFilename: "powersync-swift.sqlite"
2628
)
27-
attachments = Self.createAttachments(
29+
attachments = Self.createAttachmentQueue(
2830
db: db,
2931
connector: connector
3032
)
3133
}
32-
33-
private static func createAttachments(
34-
db: PowerSyncDatabaseProtocol,
35-
connector: SupabaseConnector
36-
) -> AttachmentQueue? {
37-
guard let bucket = connector.getStorageBucket() else {
38-
return nil
39-
}
4034

41-
do {
42-
let attachmentsDir = try getAttachmentsDirectoryPath()
43-
let watchedAttachments = try db.watch(
35+
/// Creates an AttachmentQueue if a Supabase Storage bucket has been specified in the config
36+
private static func createAttachmentQueue(
37+
db: PowerSyncDatabaseProtocol,
38+
connector: SupabaseConnector
39+
) -> AttachmentQueue? {
40+
guard let bucket = connector.getStorageBucket() else {
41+
db.logger.info("No Supabase Storage bucket specified. Skipping attachment queue setup.", tag: logTag)
42+
return nil
43+
}
44+
45+
do {
46+
let attachmentsDir = try getAttachmentsDirectoryPath()
47+
48+
return AttachmentQueue(
49+
db: db,
50+
remoteStorage: SupabaseRemoteStorage(storage: bucket),
51+
attachmentsDirectory: attachmentsDir,
52+
watchAttachments: { try db.watch(
4453
options: WatchOptions(
4554
sql: "SELECT photo_id FROM \(TODOS_TABLE) WHERE photo_id IS NOT NULL",
4655
parameters: [],
@@ -51,24 +60,16 @@ class SystemManager {
5160
)
5261
}
5362
)
54-
)
55-
56-
return AttachmentQueue(
57-
db: db,
58-
remoteStorage: SupabaseRemoteStorage(storage: bucket),
59-
attachmentsDirectory: attachmentsDir,
60-
watchedAttachments: watchedAttachments
61-
)
62-
} catch {
63-
print("Failed to initialize attachments queue: \(error)")
64-
return nil
65-
}
63+
) }
64+
)
65+
} catch {
66+
db.logger.error("Failed to initialize attachments queue: \(error)", tag: logTag)
67+
return nil
6668
}
69+
}
6770

6871
func connect() async {
6972
do {
70-
// Only for testing purposes
71-
try await attachments?.clearQueue()
7273
try await db.connect(connector: connector)
7374
try await attachments?.startSync()
7475
} catch {
@@ -87,8 +88,8 @@ class SystemManager {
8788
func signOut() async throws {
8889
try await db.disconnectAndClear()
8990
try await connector.client.auth.signOut()
91+
try await attachments?.stopSyncing()
9092
try await attachments?.clearQueue()
91-
try await attachments?.close()
9293
}
9394

9495
func watchLists(_ callback: @escaping (_ lists: [ListContent]) -> Void) async {
@@ -121,19 +122,34 @@ class SystemManager {
121122
}
122123

123124
func deleteList(id: String) async throws {
124-
_ = try await db.writeTransaction(callback: { transaction in
125+
let attachmentIds = try await db.writeTransaction(callback: { transaction in
126+
let attachmentIDs = try transaction.getAll(
127+
sql: "SELECT photo_id FROM \(TODOS_TABLE) WHERE list_id = ? AND photo_id IS NOT NULL",
128+
parameters: [id]
129+
) { cursor in
130+
cursor.getString(index: 0)! // :(
131+
} as? [String] // :(
132+
125133
_ = try transaction.execute(
126134
sql: "DELETE FROM \(LISTS_TABLE) WHERE id = ?",
127135
parameters: [id]
128136
)
129137

130-
// Attachments linked to these will be archived and deleted eventually
131-
// Attachments should be deleted explicitly if required
132138
_ = try transaction.execute(
133139
sql: "DELETE FROM \(TODOS_TABLE) WHERE list_id = ?",
134140
parameters: [id]
135141
)
142+
143+
return attachmentIDs ?? [] // :(
136144
})
145+
146+
if let attachments {
147+
for id in attachmentIds {
148+
try await attachments.deleteFile(
149+
attachmentId: id
150+
) { _, _ in }
151+
}
152+
}
137153
}
138154

139155
func watchTodos(_ listId: String, _ callback: @escaping (_ todos: [Todo]) -> Void) async {
@@ -214,7 +230,7 @@ class SystemManager {
214230
}
215231
}
216232

217-
func deleteTodoInTX(id: String, tx: ConnectionContext) throws {
233+
private func deleteTodoInTX(id: String, tx: ConnectionContext) throws {
218234
_ = try tx.execute(
219235
sql: "DELETE FROM \(TODOS_TABLE) WHERE id = ?",
220236
parameters: [id]

Sources/PowerSync/attachments/Attachment.swift

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@
22
public enum AttachmentState: Int {
33
/// The attachment has been queued for download from the cloud storage
44
case queuedDownload
5-
/// The attachment has been queued for upload to the cloud storage
5+
/// The attachment has been queued for upload to the cloud storage
66
case queuedUpload
7-
/// The attachment has been queued for delete in the cloud storage (and locally)
7+
/// The attachment has been queued for delete in the cloud storage (and locally)
88
case queuedDelete
9-
/// The attachment has been synced
9+
/// The attachment has been synced
1010
case synced
1111
/// The attachment has been orphaned, i.e., the associated record has been deleted
1212
case archived
13-
13+
1414
enum AttachmentStateError: Error {
1515
case invalidState(Int)
1616
}
17-
17+
1818
static func from(_ rawValue: Int) throws -> AttachmentState {
1919
guard let state = AttachmentState(rawValue: rawValue) else {
2020
throw AttachmentStateError.invalidState(rawValue)
@@ -92,21 +92,22 @@ public struct Attachment {
9292
filename _: String? = nil,
9393
state: AttachmentState? = nil,
9494
timestamp _: Int = 0,
95-
hasSynced: Int? = 0,
96-
localUri: String? = nil,
97-
mediaType: String? = nil,
98-
size: Int64? = nil,
99-
metaData: String? = nil
95+
hasSynced: Int?? = 0,
96+
localUri: String?? = .none,
97+
mediaType: String?? = .none,
98+
size: Int64?? = .none,
99+
metaData: String?? = .none
100100
) -> Attachment {
101101
return Attachment(
102102
id: id,
103-
filename: filename,
104-
state: state ?? self.state,
105-
hasSynced: hasSynced ?? self.hasSynced,
106-
localUri: localUri ?? self.localUri,
107-
mediaType: mediaType ?? self.mediaType,
108-
size: size ?? self.size,
109-
metaData: metaData ?? self.metaData
103+
filename: filename ?? filename,
104+
state: state.map { $0 } ?? self.state,
105+
timestamp: timestamp > 0 ? timestamp : timestamp,
106+
hasSynced: hasSynced.map { $0 } ?? self.hasSynced,
107+
localUri: localUri.map { $0 } ?? self.localUri,
108+
mediaType: mediaType.map { $0 } ?? self.mediaType,
109+
size: size.map { $0 } ?? self.size,
110+
metaData: metaData.map { $0 } ?? self.metaData
110111
)
111112
}
112113

Sources/PowerSync/attachments/AttachmentQueue.swift

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ public actor AttachmentQueue {
1818
/// Directory name for attachments
1919
private let attachmentsDirectory: String
2020

21-
/// Stream of watched attachments
22-
private let watchedAttachments: AsyncThrowingStream<[WatchedAttachmentItem], Error>
21+
/// Closure which creates a Stream of ``WatchedAttachmentItem``
22+
private let watchAttachments: () throws -> AsyncThrowingStream<[WatchedAttachmentItem], Error>
2323

2424
/// Local file system adapter
2525
public let localStorage: LocalStorageAdapter
@@ -79,7 +79,7 @@ public actor AttachmentQueue {
7979
db: PowerSyncDatabaseProtocol,
8080
remoteStorage: RemoteStorageAdapter,
8181
attachmentsDirectory: String,
82-
watchedAttachments: AsyncThrowingStream<[WatchedAttachmentItem], Error>,
82+
watchAttachments: @escaping () throws -> AsyncThrowingStream<[WatchedAttachmentItem], Error>,
8383
localStorage: LocalStorageAdapter = FileManagerStorageAdapter(),
8484
attachmentsQueueTableName: String = defaultTableName,
8585
errorHandler: SyncErrorHandler? = nil,
@@ -93,7 +93,7 @@ public actor AttachmentQueue {
9393
self.db = db
9494
self.remoteStorage = remoteStorage
9595
self.attachmentsDirectory = attachmentsDirectory
96-
self.watchedAttachments = watchedAttachments
96+
self.watchAttachments = watchAttachments
9797
self.localStorage = localStorage
9898
self.attachmentsQueueTableName = attachmentsQueueTableName
9999
self.errorHandler = errorHandler
@@ -128,6 +128,11 @@ public actor AttachmentQueue {
128128
try await localStorage.makeDir(path: path)
129129
}
130130
}
131+
132+
// Verify initial state
133+
try await attachmentsService.withLock {context in
134+
try await self.verifyAttachments(context: context)
135+
}
131136

132137
try await syncingService.startSync(period: syncInterval)
133138

@@ -147,7 +152,7 @@ public actor AttachmentQueue {
147152

148153
// Add attachment watching task
149154
group.addTask {
150-
for try await items in self.watchedAttachments {
155+
for try await items in try self.watchAttachments() {
151156
try await self.processWatchedAttachments(items: items)
152157
}
153158
}
@@ -380,6 +385,31 @@ public actor AttachmentQueue {
380385
try await self.localStorage.rmDir(path: self.attachmentsDirectory)
381386
}
382387
}
388+
389+
/// Verifies attachment records are present in the filesystem
390+
private func verifyAttachments(context: AttachmentContext) async throws {
391+
let attachments = try await context.getAttachments()
392+
var updates: [Attachment] = []
393+
394+
for attachment in attachments {
395+
guard let localUri = attachment.localUri else {
396+
continue
397+
}
398+
399+
let exists = try await localStorage.fileExists(filePath: localUri)
400+
if attachment.state == AttachmentState.synced ||
401+
attachment.state == AttachmentState.queuedUpload &&
402+
!exists {
403+
// The file must have been removed from the local storage
404+
updates.append(attachment.with(
405+
state: .archived,
406+
localUri: .some(nil) // Clears the value
407+
))
408+
}
409+
}
410+
411+
try await context.saveAttachments(attachments: updates)
412+
}
383413

384414
private func guardClosed() throws {
385415
if closed {

Sources/PowerSync/attachments/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ let queue = AttachmentQueue(
8484
db: db,
8585
attachmentsDirectory: try getAttachmentsDirectoryPath(),
8686
remoteStorage: RemoteStorage(),
87-
watchedAttachments: try db.watch(
87+
watchAttachments: { try db.watch(
8888
options: WatchOptions(
8989
sql: "SELECT photo_id FROM checklists WHERE photo_id IS NOT NULL",
9090
parameters: [],
@@ -95,13 +95,13 @@ let queue = AttachmentQueue(
9595
)
9696
}
9797
)
98-
)
98+
) }
9999
)
100100
```
101101

102102
- The `attachmentsDirectory` specifies where local attachment files should be stored. `FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("attachments")` is a good choice.
103103
- The `remoteStorage` is responsible for connecting to the attachments backend. See the `RemoteStorageAdapter` protocol definition.
104-
- `watchedAttachments` is a publisher of `WatchedAttachmentItem`. These items represent the attachments that should be present in the application.
104+
- `watchAttachmens` is closure which generates a publisher of `WatchedAttachmentItem`. These items represent the attachments that should be present in the application.
105105

106106
3. Call `startSync()` to start syncing attachments.
107107

0 commit comments

Comments
 (0)