Skip to content

Commit 49ce568

Browse files
authored
Fix updating assets (#230)
* Add test that shows what happens when an existing asset is updated. * fix * wip * wip * Got a failing test. * failing test * wip * wip * wip * wip
1 parent 77bbb8b commit 49ce568

File tree

7 files changed

+278
-124
lines changed

7 files changed

+278
-124
lines changed

Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -134,22 +134,17 @@
134134

135135
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
136136
extension CKRecordKeyValueSetting {
137-
subscript(at key: String) -> Int64 {
137+
fileprivate subscript(at key: String) -> Int64 {
138138
get {
139139
self["\(CKRecord.userModificationTimeKey)_\(key)"] as? Int64 ?? -1
140140
}
141141
set {
142142
self["\(CKRecord.userModificationTimeKey)_\(key)"] = max(self[at: key], newValue)
143143
}
144144
}
145-
}
146-
147-
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
148-
extension URL {
149-
init(hash data: some DataProtocol) {
150-
@Dependency(\.dataManager) var dataManager
151-
let hash = SHA256.hash(data: data).compactMap { String(format: "%02hhx", $0) }.joined()
152-
self = dataManager.temporaryDirectory.appendingPathComponent(hash)
145+
fileprivate subscript(hash key: String) -> Data? {
146+
get { self["\(key)_hash"] as? Data }
147+
set { self["\(key)_hash"] = newValue }
153148
}
154149
}
155150

@@ -172,28 +167,55 @@
172167
}
173168

174169
@discardableResult
175-
package func setValue(
176-
_ newValue: [UInt8],
170+
package func setAsset(
171+
_ newValue: CKAsset,
177172
forKey key: CKRecord.FieldKey,
178173
at userModificationTime: Int64
179174
) -> Bool {
180175
@Dependency(\.dataManager) var dataManager
176+
guard
177+
let fileURL = newValue.fileURL,
178+
let hash = dataManager.sha256(of: fileURL)
179+
else { return false }
180+
guard
181+
encryptedValues[at: key] <= userModificationTime,
182+
encryptedValues[hash: key] != hash
183+
else { return false }
181184

182-
guard encryptedValues[at: key] <= userModificationTime
183-
else {
184-
return false
185-
}
185+
self[key] = newValue
186+
encryptedValues[hash: key] = hash
187+
encryptedValues[at: key] = userModificationTime
188+
self.userModificationTime = userModificationTime
189+
return true
190+
}
186191

187-
let asset = CKAsset(fileURL: URL(hash: newValue))
188-
guard let fileURL = asset.fileURL, (self[key] as? CKAsset)?.fileURL != fileURL
192+
@discardableResult
193+
package func setValue(
194+
_ newValue: [UInt8],
195+
forKey key: CKRecord.FieldKey,
196+
at userModificationTime: Int64
197+
) -> Bool {
198+
guard encryptedValues[at: key] <= userModificationTime
189199
else { return false }
190-
withErrorReporting(.sqliteDataCloudKitFailure) {
200+
201+
@Dependency(\.dataManager) var dataManager
202+
let hash = newValue.sha256
203+
let fileURL = dataManager.temporaryDirectory.appending(
204+
component:
205+
hash
206+
.compactMap { String(format: "%02hhx", $0) }
207+
.joined()
208+
)
209+
let asset = CKAsset(fileURL: fileURL)
210+
return withErrorReporting(.sqliteDataCloudKitFailure) {
191211
try dataManager.save(Data(newValue), to: fileURL)
212+
self[key] = asset
213+
encryptedValues[at: key] = userModificationTime
214+
encryptedValues[hash: key] = hash
215+
self.userModificationTime = userModificationTime
216+
return true
192217
}
193-
self[key] = asset
194-
encryptedValues[at: key] = userModificationTime
195-
self.userModificationTime = userModificationTime
196-
return true
218+
?? false
197219
}
198220

199221
@discardableResult
@@ -271,7 +293,7 @@
271293
let keyPath = column.keyPath as! KeyPath<T, Value.QueryOutput>
272294
let didSet: Bool
273295
if let value = other[key] as? CKAsset {
274-
didSet = setValue(value, forKey: key, at: other[at: key])
296+
didSet = setAsset(value, forKey: key, at: other.encryptedValues[at: key])
275297
} else if let value = other.encryptedValues[key] as? any EquatableCKRecordValueProtocol {
276298
didSet = setValue(value, forKey: key, at: other.encryptedValues[at: key])
277299
} else if other.encryptedValues[key] == nil {
@@ -283,7 +305,7 @@
283305
var isRowValueModified: Bool {
284306
switch Value(queryOutput: row[keyPath: keyPath]).queryBinding {
285307
case .blob(let value):
286-
return (other[key] as? CKAsset)?.fileURL != URL(hash: value)
308+
return other.encryptedValues[hash: key] != value.sha256
287309
case .bool(let value):
288310
return other.encryptedValues[key] != value
289311
case .double(let value):
@@ -353,4 +375,10 @@
353375
set { self[#function] = newValue }
354376
}
355377
}
378+
379+
extension DataProtocol {
380+
fileprivate var sha256: Data {
381+
Data(SHA256.hash(data: self))
382+
}
383+
}
356384
#endif
Lines changed: 87 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,101 @@
1-
import Dependencies
2-
import Foundation
3-
4-
package protocol DataManager: Sendable {
5-
func load(_ url: URL) throws -> Data
6-
func save(_ data: Data, to url: URL) throws
7-
var temporaryDirectory: URL { get }
8-
}
9-
10-
struct LiveDataManager: DataManager {
11-
func load(_ url: URL) throws -> Data {
12-
try Data(contentsOf: url)
13-
}
14-
func save(_ data: Data, to url: URL) throws {
15-
try data.write(to: url)
1+
#if canImport(CloudKit) && canImport(CryptoKit)
2+
import CryptoKit
3+
import Dependencies
4+
import Foundation
5+
6+
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
7+
package protocol DataManager: Sendable {
8+
func load(_ url: URL) throws -> Data
9+
func save(_ data: Data, to url: URL) throws
10+
func sha256(of fileURL: URL) -> Data?
11+
var temporaryDirectory: URL { get }
1612
}
17-
var temporaryDirectory: URL {
18-
URL(fileURLWithPath: NSTemporaryDirectory())
13+
14+
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
15+
struct LiveDataManager: DataManager {
16+
func load(_ url: URL) throws -> Data {
17+
try Data(contentsOf: url)
18+
}
19+
func save(_ data: Data, to url: URL) throws {
20+
try data.write(to: url)
21+
}
22+
func sha256(of fileURL: URL) -> Data? {
23+
do {
24+
let fileHandle = try FileHandle(forReadingFrom: fileURL)
25+
defer { try? fileHandle.close() }
26+
var hasher = SHA256()
27+
while true {
28+
let shouldBreak = try autoreleasepool {
29+
guard
30+
let data = try fileHandle.read(upToCount: 1024 * 1024),
31+
!data.isEmpty
32+
else { return false }
33+
hasher.update(data: data)
34+
return true
35+
}
36+
guard !shouldBreak
37+
else { break }
38+
}
39+
let digest = hasher.finalize()
40+
return Data(digest)
41+
} catch {
42+
return nil
43+
}
44+
}
45+
var temporaryDirectory: URL {
46+
URL(fileURLWithPath: NSTemporaryDirectory())
47+
}
1948
}
20-
}
2149

22-
package struct InMemoryDataManager: DataManager {
23-
package let storage = LockIsolated<[URL: Data]>([:])
50+
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
51+
package struct InMemoryDataManager: DataManager {
52+
package let storage = LockIsolated<[URL: Data]>([:])
2453

25-
package init() {}
54+
package init() {}
2655

27-
package func load(_ url: URL) throws -> Data {
28-
try storage.withValue { storage in
29-
guard let data = storage[url]
30-
else {
31-
struct FileNotFound: Error {}
32-
throw FileNotFound()
56+
package func load(_ url: URL) throws -> Data {
57+
try storage.withValue { storage in
58+
guard let data = storage[url]
59+
else {
60+
struct FileNotFound: Error {}
61+
throw FileNotFound()
62+
}
63+
return data
3364
}
34-
return data
3565
}
36-
}
3766

38-
package func save(_ data: Data, to url: URL) throws {
39-
storage.withValue { $0[url] = data }
40-
}
67+
package func save(_ data: Data, to url: URL) throws {
68+
storage.withValue { $0[url] = data }
69+
}
4170

42-
package var temporaryDirectory: URL {
43-
URL(fileURLWithPath: "/")
44-
}
45-
}
71+
package func sha256(of fileURL: URL) -> Data? {
72+
storage.withValue {
73+
$0[fileURL].map {
74+
Data(SHA256.hash(data: $0))
75+
}
76+
}
77+
}
4678

47-
private enum DataManagerKey: DependencyKey {
48-
static var liveValue: any DataManager {
49-
LiveDataManager()
79+
package var temporaryDirectory: URL {
80+
URL(fileURLWithPath: "/")
81+
}
5082
}
51-
static var testValue: any DataManager {
52-
InMemoryDataManager()
83+
84+
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
85+
private enum DataManagerKey: DependencyKey {
86+
static var liveValue: any DataManager {
87+
LiveDataManager()
88+
}
89+
static var testValue: any DataManager {
90+
InMemoryDataManager()
91+
}
5392
}
54-
}
5593

56-
extension DependencyValues {
57-
package var dataManager: DataManager {
58-
get { self[DataManagerKey.self] }
59-
set { self[DataManagerKey.self] = newValue }
94+
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
95+
extension DependencyValues {
96+
package var dataManager: DataManager {
97+
get { self[DataManagerKey.self] }
98+
set { self[DataManagerKey.self] = newValue }
99+
}
60100
}
61-
}
101+
#endif

0 commit comments

Comments
 (0)