Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 1.6.0 (unreleased)

* Update minimum MacOS target to v12
* [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

* Update core extension to 0.4.5 ([changelog](https://github.com/powersync-ja/powersync-sqlite-core/releases/tag/v0.4.5))
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ let package = Package(
name: packageName,
platforms: [
.iOS(.v13),
.macOS(.v10_15),
.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
Loading