Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
* Update core extension to 0.4.6 ([changelog](https://github.com/powersync-ja/powersync-sqlite-core/releases/tag/v0.4.6))
* Add `getCrudTransactions()`, returning an async sequence of transactions.
* Compatibility with Swift 6.2 and XCode 26.
* Update minimum MacOS target to v12
* Update minimum iOS target to v15
* [Attachment Helpers] Added automatic verification or records' `local_uri` values on `AttachmentQueue` initialization.
initialization can be awaited with `AttachmentQueue.waitForInit()`. `AttachmentQueue.startSync()` also performs this verification.
`waitForInit()` is only recommended if `startSync` is not called directly after creating the queue.

## 1.5.1

Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ if let corePath = localCoreExtension {
let package = Package(
name: packageName,
platforms: [
.iOS(.v13),
.macOS(.v10_15),
.iOS(.v15),
.macOS(.v12),
.watchOS(.v9)
],
products: [
Expand Down
40 changes: 40 additions & 0 deletions Sources/PowerSync/attachments/AttachmentQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ public protocol AttachmentQueueProtocol: Sendable {
var localStorage: any LocalStorageAdapter { get }
var downloadAttachments: Bool { get }

/// Waits for automatically triggered initialization.
/// This ensures all attachment records have been verified before use.
/// The `startSync` method also performs this verification. This call is not
/// needed if `startSync` is called after creating the Attachment Queue.
func waitForInit() async throws

/// Starts the attachment sync process
func startSync() async throws

Expand Down Expand Up @@ -287,6 +293,9 @@ public actor AttachmentQueue: AttachmentQueueProtocol {

private let _getLocalUri: @Sendable (_ filename: String) async -> String

private let initializedSubject = PassthroughSubject<Result<Void, Error>, Never>()
private var initializationResult: Result<Void, Error>?

/// Initializes the attachment queue
/// - Parameters match the stored properties
public init(
Expand Down Expand Up @@ -337,13 +346,44 @@ public actor AttachmentQueue: AttachmentQueueProtocol {
syncThrottle: self.syncThrottleDuration
)

// Storing a reference to this task is non-trivial since we capture
// Self. Swift 6 Strict concurrency checking will complain about a nonisolated initializer
Task {
do {
try await attachmentsService.withContext { context in
try await self.verifyAttachments(context: context)
}
await self.setInitializedResult(.success(()))
} catch {
self.logger.error("Error verifying attachments: \(error.localizedDescription)", tag: logTag)
await self.setInitializedResult(.failure(error))
}
}
}

/// Actor isolated method to set the initialization result
private func setInitializedResult(_ result: Result<Void, Error>) {
initializationResult = result
initializedSubject.send(result)
}

public func waitForInit() async throws {
if let isInitialized = initializationResult {
switch isInitialized {
case .success:
return
case let .failure(error):
throw error
}
}

// Wait for the result asynchronously
for try await result in initializedSubject.values {
switch result {
case .success:
return
case let .failure(error):
throw error
}
}
}
Expand Down
78 changes: 78 additions & 0 deletions Tests/PowerSyncTests/AttachmentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,84 @@ final class AttachmentTests: XCTestCase {
try await queue.clearQueue()
try await queue.close()
}

func testAttachmentInitVerification() async throws {
actor MockRemoteStorage: RemoteStorageAdapter {
func uploadFile(
fileData _: Data,
attachment _: Attachment
) async throws {}

func downloadFile(attachment _: Attachment) async throws -> Data {
return Data([1, 2, 3])
}

func deleteFile(attachment _: Attachment) async throws {}
}

// Create an attachments record which has an invalid local_uri
let attachmentsDirectory = getAttachmentDirectory()

try FileManager.default.createDirectory(
at: URL(fileURLWithPath: attachmentsDirectory),
withIntermediateDirectories: true,
attributes: nil
)

let filename = "test.jpeg"

try Data("1".utf8).write(
to: URL(fileURLWithPath: attachmentsDirectory).appendingPathComponent(filename)
)
try await database.execute(
sql: """
INSERT OR REPLACE INTO
attachments (id, timestamp, filename, local_uri, media_type, size, state, has_synced, meta_data)
VALUES
(uuid(), ?, ?, ?, ?, ?, ?, ?, ?)
""",
parameters: [
Date().ISO8601Format(),
filename,
// This is a broken local_uri
URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("not_attachments/test.jpeg").path,
"application/jpeg",
1,
AttachmentState.synced.rawValue,
1,
""
]
)

let mockedRemote = MockRemoteStorage()

let queue = AttachmentQueue(
db: database,
remoteStorage: mockedRemote,
attachmentsDirectory: attachmentsDirectory,
watchAttachments: { [database = database!] in try database.watch(options: WatchOptions(
sql: "SELECT photo_id FROM users WHERE photo_id IS NOT NULL",
mapper: { cursor in try WatchedAttachmentItem(
id: cursor.getString(name: "photo_id"),
fileExtension: "jpg"
) }
)) }
)

try await queue.waitForInit()

// the attachment should have been corrected in the init
let attachments = try await queue.attachmentsService.withContext { context in
try await context.getAttachments()
}

guard let firstAttachment = attachments.first else {
XCTFail("Could not find the attachment record")
return
}

XCTAssert(firstAttachment.localUri == URL(fileURLWithPath: attachmentsDirectory).appendingPathComponent(filename).path)
}
}

public enum WaitForMatchError: Error {
Expand Down