1 // Copyright © 2019 650 Industries. All rights reserved. 2 3 // A lot of stuff in this class was originally written in objective-c, and the swift 4 // equivalents don't seem to work quite the same, which is important to have backwards 5 // data compatibility. 6 // swiftlint:disable legacy_objc_type 7 // swiftlint:disable line_length 8 // swiftlint:disable type_body_length 9 // swiftlint:disable force_unwrapping 10 // swiftlint:disable file_length 11 12 import Foundation 13 import sqlite3 14 import EXManifests 15 16 internal enum UpdatesDatabaseError: Error { 17 case addExistingAssetMissingAssetKey 18 case addExistingAssetInsertError 19 case addExistingAssetAssetNotFoundError 20 case markMissingAssetsError 21 case deleteUpdatesError 22 case deleteUnusedAssetsError 23 case getUpdatesError 24 case setJsonDataError 25 } 26 27 enum UpdatesDatabaseHashType: Int { 28 case Sha1 = 0 29 } 30 31 /** 32 * SQLite database that keeps track of updates currently loaded/loading to disk, including the 33 * update manifest and metadata, status, and the individual assets (including bundles/bytecode) that 34 * comprise the update. (Assets themselves are stored on the device's file system, and a relative 35 * path is kept in SQLite.) 36 * 37 * SQLite allows a many-to-many relationship between updates and assets, which means we can keep 38 * only one copy of each asset on disk at a time while also being able to clear unused assets with 39 * relative ease (see UpdatesReaper). 40 * 41 * Occasionally it's necessary to add migrations when the data structures for updates or assets must 42 * change. Extra care must be taken here, since these migrations will happen on users' devices for 43 * apps we do not control. See 44 * https://github.com/expo/expo/blob/main/packages/expo-updates/guides/migrations.md for step by 45 * step instructions. 46 * 47 * UpdatesDatabase provides a serial queue on which all database operations must be run (methods 48 * in this class will assert). This is primarily for control over what high-level operations 49 * involving the database can occur simultaneously - e.g. we don't want to be trying to download a 50 * new update at the same time UpdatesReaper is running. 51 * 52 * The `scopeKey` field in various methods here is only relevant in environments such as Expo Go in 53 * which updates from multiple scopes can be launched. 54 */ 55 @objc(EXUpdatesDatabase) 56 @objcMembers 57 public final class UpdatesDatabase: NSObject { 58 private static let ManifestFiltersKey = "manifestFilters" 59 private static let ServerDefinedHeadersKey = "serverDefinedHeaders" 60 private static let StaticBuildDataKey = "staticBuildData" 61 private static let ExtraParmasKey = "extraParams" 62 63 public let databaseQueue: DispatchQueue 64 private var db: OpaquePointer? 65 66 public required override init() { 67 self.databaseQueue = DispatchQueue(label: "expo.database.DatabaseQueue") 68 } 69 70 deinit { 71 closeDatabase() 72 } 73 openDatabasenull74 public func openDatabase(inDirectory directory: URL) throws { 75 dispatchPrecondition(condition: .onQueue(databaseQueue)) 76 db = try UpdatesDatabaseInitialization.initializeDatabaseWithLatestSchema(inDirectory: directory) 77 } 78 closeDatabasenull79 public func closeDatabase() { 80 sqlite3_close(db) 81 db = nil 82 } 83 executenull84 public func execute(sql: String, withArgs args: [Any?]?) throws -> [[String: Any?]] { 85 dispatchPrecondition(condition: .onQueue(databaseQueue)) 86 return try UpdatesDatabaseUtils.execute(sql: sql, withArgs: args, onDatabase: db.require("Missing database handle")) 87 } 88 executeForObjCnull89 public func executeForObjC(sql: String, withArgs args: [Any]?) throws -> [Any] { 90 return try execute(sql: sql, withArgs: args) 91 } 92 addUpdatenull93 public func addUpdate(_ update: Update) throws { 94 let sql = """ 95 INSERT INTO "updates" ("id", "scope_key", "commit_time", "runtime_version", "manifest", "status" , "keep", "last_accessed", "successful_launch_count", "failed_launch_count") 96 VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1, ?7, ?8, ?9); 97 """ 98 _ = try execute( 99 sql: sql, 100 withArgs: [ 101 update.updateId, 102 update.scopeKey.require("Update must have scopeKey to be stored in database"), 103 update.commitTime, 104 update.runtimeVersion, 105 update.manifest.rawManifestJSON(), 106 update.status.rawValue, 107 update.lastAccessed, 108 update.successfulLaunchCount, 109 update.failedLaunchCount 110 ] 111 ) 112 } 113 addNewAssetsnull114 public func addNewAssets(_ assets: [UpdateAsset], toUpdateWithId updateId: UUID) throws { 115 sqlite3_exec(db, "BEGIN;", nil, nil, nil) 116 117 let assetInsertSql = """ 118 INSERT OR REPLACE INTO "assets" ("key", "url", "headers", "extra_request_headers", "type", "metadata", "download_time", "relative_path", "hash", "hash_type", "expected_hash", "marked_for_deletion") 119 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, 0); 120 """ 121 for asset in assets { 122 do { 123 _ = try execute( 124 sql: assetInsertSql, 125 withArgs: [ 126 asset.key, 127 asset.url, 128 asset.headers, 129 asset.extraRequestHeaders, 130 asset.type, 131 asset.metadata, 132 asset.downloadTime.require("asset downloadTime should be nonnull"), 133 asset.filename, 134 asset.contentHash, 135 UpdatesDatabaseHashType.Sha1.rawValue, 136 asset.expectedHash 137 ] 138 ) 139 } catch { 140 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 141 return 142 } 143 144 // statements must stay in precisely this order for last_insert_rowid() to work correctly 145 if asset.isLaunchAsset { 146 let updateSql = "UPDATE updates SET launch_asset_id = last_insert_rowid() WHERE id = ?1;" 147 do { 148 _ = try execute(sql: updateSql, withArgs: [updateId]) 149 } catch { 150 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 151 return 152 } 153 } 154 155 let updateInsertSql = """ 156 INSERT OR REPLACE INTO updates_assets ("update_id", "asset_id") VALUES (?1, last_insert_rowid()); 157 """ 158 do { 159 _ = try execute(sql: updateInsertSql, withArgs: [updateId]) 160 } catch { 161 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 162 return 163 } 164 } 165 166 sqlite3_exec(db, "COMMIT;", nil, nil, nil) 167 } 168 addExistingAssetnull169 public func addExistingAsset(_ asset: UpdateAsset, toUpdateWithId updateId: UUID) throws -> Bool { 170 guard let key = asset.key else { 171 return false 172 } 173 174 sqlite3_exec(db, "BEGIN;", nil, nil, nil) 175 176 let assetSelectSql = """ 177 SELECT id FROM assets WHERE "key" = ?1 LIMIT 1; 178 """ 179 let rows = try execute(sql: assetSelectSql, withArgs: [key]) 180 if !rows.isEmpty { 181 let assetId: NSNumber = rows[0].requiredValue(forKey: "id") 182 let insertSql = """ 183 INSERT OR REPLACE INTO updates_assets ("update_id", "asset_id") VALUES (?1, ?2); 184 """ 185 do { 186 _ = try execute(sql: insertSql, withArgs: [updateId, assetId.intValue]) 187 } catch { 188 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 189 throw UpdatesDatabaseError.addExistingAssetInsertError 190 } 191 192 if asset.isLaunchAsset { 193 let updateSql = "UPDATE updates SET launch_asset_id = ?1 WHERE id = ?2;" 194 do { 195 _ = try execute(sql: updateSql, withArgs: [assetId.intValue, updateId]) 196 } catch { 197 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 198 throw UpdatesDatabaseError.addExistingAssetInsertError 199 } 200 } 201 } 202 203 sqlite3_exec(db, "COMMIT;", nil, nil, nil) 204 205 if rows.isEmpty { 206 return false 207 } 208 209 return true 210 } 211 updateAssetnull212 public func updateAsset(_ asset: UpdateAsset) throws { 213 let assetUpdateSql = """ 214 UPDATE "assets" SET "headers" = ?2, "extra_request_headers" = ?3, "type" = ?4, "metadata" = ?5, "download_time" = ?6, "relative_path" = ?7, "hash" = ?8, "expected_hash" = ?9, "url" = ?10 WHERE "key" = ?1; 215 """ 216 _ = try execute( 217 sql: assetUpdateSql, 218 withArgs: [ 219 asset.key, 220 asset.headers, 221 asset.extraRequestHeaders, 222 asset.type, 223 asset.metadata, 224 asset.downloadTime.require("asset downloadTime should be nonnull"), 225 asset.filename, 226 asset.contentHash.require("asset contentHash should be nonnull"), 227 asset.expectedHash, 228 asset.url?.absoluteString 229 ] 230 ) 231 } 232 mergeAssetnull233 public func mergeAsset(_ asset: UpdateAsset, withExistingEntry existingAsset: UpdateAsset) throws { 234 var shouldUpdate = false 235 236 // if the existing entry came from an embedded manifest, it may not have a URL in the database 237 if let url = asset.url, 238 existingAsset.url == nil || url != existingAsset.url { 239 existingAsset.url = url 240 shouldUpdate = true 241 } 242 243 if let extraRequestHeaders = asset.extraRequestHeaders, 244 existingAsset.extraRequestHeaders == nil || !NSDictionary(dictionary: extraRequestHeaders).isEqual(to: existingAsset.extraRequestHeaders!) { 245 existingAsset.extraRequestHeaders = extraRequestHeaders 246 shouldUpdate = true 247 } 248 249 if shouldUpdate { 250 try updateAsset(existingAsset) 251 } 252 253 // all other properties should be overridden by database values 254 asset.filename = existingAsset.filename 255 asset.contentHash = existingAsset.contentHash 256 asset.expectedHash = existingAsset.expectedHash 257 asset.downloadTime = existingAsset.downloadTime 258 } 259 markUpdateFinishednull260 public func markUpdateFinished(_ update: Update) throws { 261 if update.status != UpdateStatus.StatusDevelopment { 262 update.status = UpdateStatus.StatusReady 263 } 264 265 let updateSql = "UPDATE updates SET status = ?1, keep = 1 WHERE id = ?2;" 266 _ = try execute(sql: updateSql, withArgs: [update.status.rawValue, update.updateId]) 267 } 268 markUpdateAccessednull269 public func markUpdateAccessed(_ update: Update) throws { 270 update.lastAccessed = Date() 271 let updateSql = "UPDATE updates SET last_accessed = ?1 WHERE id = ?2;" 272 _ = try execute(sql: updateSql, withArgs: [update.lastAccessed, update.updateId]) 273 } 274 incrementSuccessfulLaunchCountForUpdatenull275 public func incrementSuccessfulLaunchCountForUpdate(_ update: Update) throws { 276 update.successfulLaunchCount += 1 277 let updateSql = "UPDATE updates SET successful_launch_count = ?1 WHERE id = ?2;" 278 _ = try execute(sql: updateSql, withArgs: [update.successfulLaunchCount, update.updateId]) 279 } 280 incrementFailedLaunchCountForUpdatenull281 public func incrementFailedLaunchCountForUpdate(_ update: Update) throws { 282 update.failedLaunchCount += 1 283 let updateSql = "UPDATE updates SET failed_launch_count = ?1 WHERE id = ?2;" 284 _ = try execute(sql: updateSql, withArgs: [update.failedLaunchCount, update.updateId]) 285 } 286 setScopeKeynull287 public func setScopeKey(_ scopeKey: String, onUpdate update: Update) throws { 288 let updateSql = "UPDATE updates SET scope_key = ?1 WHERE id = ?2;" 289 _ = try execute(sql: updateSql, withArgs: [scopeKey, update.updateId]) 290 } 291 setUpdateCommitTimenull292 public func setUpdateCommitTime(_ commitTime: Date, onUpdate update: Update) throws { 293 let updateSql = "UPDATE updates SET commit_time = ?1 WHERE id = ?2;" 294 _ = try execute(sql: updateSql, withArgs: [commitTime, update.updateId]) 295 } 296 markMissingAssetsnull297 public func markMissingAssets(_ assets: [UpdateAsset]) throws { 298 sqlite3_exec(db, "BEGIN;", nil, nil, nil) 299 300 let updateSql = "UPDATE updates SET status = ?1 WHERE id IN (SELECT DISTINCT update_id FROM updates_assets WHERE asset_id = ?2);" 301 for asset in assets { 302 do { 303 _ = try execute(sql: updateSql, withArgs: [UpdateStatus.StatusPending.rawValue, asset.assetId]) 304 } catch { 305 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 306 throw UpdatesDatabaseError.markMissingAssetsError 307 } 308 } 309 310 sqlite3_exec(db, "COMMIT;", nil, nil, nil) 311 } 312 deleteUpdatesnull313 public func deleteUpdates(_ updates: [Update]) throws { 314 sqlite3_exec(db, "BEGIN;", nil, nil, nil) 315 316 let updateSql = "DELETE FROM updates WHERE id = ?1;" 317 for update in updates { 318 do { 319 _ = try execute(sql: updateSql, withArgs: [update.updateId]) 320 } catch { 321 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 322 throw UpdatesDatabaseError.deleteUpdatesError 323 } 324 } 325 326 sqlite3_exec(db, "COMMIT;", nil, nil, nil) 327 } 328 deleteUnusedAssetsnull329 public func deleteUnusedAssets() throws -> [UpdateAsset] { 330 // the simplest way to mark the assets we want to delete 331 // is to mark all assets for deletion, then go back and unmark 332 // those assets in updates we want to keep 333 // this is safe as long as we do this inside of a transaction 334 335 sqlite3_exec(db, "BEGIN;", nil, nil, nil) 336 337 let update1Sql = "UPDATE assets SET marked_for_deletion = 1;" 338 do { 339 _ = try execute(sql: update1Sql, withArgs: nil) 340 } catch { 341 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 342 throw UpdatesDatabaseError.deleteUnusedAssetsError 343 } 344 345 let update2Sql = "UPDATE assets SET marked_for_deletion = 0 WHERE id IN (SELECT asset_id FROM updates_assets INNER JOIN updates ON updates_assets.update_id = updates.id WHERE updates.keep = 1);" 346 do { 347 _ = try execute(sql: update2Sql, withArgs: nil) 348 } catch { 349 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 350 throw UpdatesDatabaseError.deleteUnusedAssetsError 351 } 352 353 // check for duplicate rows representing a single file on disk 354 let update3Sql = "UPDATE assets SET marked_for_deletion = 0 WHERE relative_path IN (SELECT relative_path FROM assets WHERE marked_for_deletion = 0);" 355 do { 356 _ = try execute(sql: update3Sql, withArgs: nil) 357 } catch { 358 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 359 throw UpdatesDatabaseError.deleteUnusedAssetsError 360 } 361 362 var rows: [[String: Any?]] 363 let selectSql = "SELECT * FROM assets WHERE marked_for_deletion = 1;" 364 do { 365 rows = try execute(sql: selectSql, withArgs: nil) 366 } catch { 367 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 368 throw UpdatesDatabaseError.deleteUnusedAssetsError 369 } 370 371 let assets = rows.map { row in 372 asset(withRow: row) 373 } 374 375 let deleteSql = "DELETE FROM assets WHERE marked_for_deletion = 1;" 376 do { 377 _ = try execute(sql: deleteSql, withArgs: nil) 378 } catch { 379 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 380 throw UpdatesDatabaseError.deleteUnusedAssetsError 381 } 382 383 sqlite3_exec(db, "COMMIT;", nil, nil, nil) 384 385 return assets 386 } 387 allUpdatesnull388 public func allUpdates(withConfig config: UpdatesConfig) throws -> [Update] { 389 let sql = "SELECT * FROM updates;" 390 let rows = try execute(sql: sql, withArgs: nil) 391 return rows.map { row in 392 update(withRow: row, config: config) 393 } 394 } 395 allUpdatesnull396 public func allUpdates(withStatus status: UpdateStatus, config: UpdatesConfig) throws -> [Update] { 397 let sql = "SELECT * FROM updates WHERE status = ?1;" 398 let rows = try execute(sql: sql, withArgs: [status.rawValue]) 399 return rows.map { row in 400 update(withRow: row, config: config) 401 } 402 } 403 allUpdateIdsnull404 public func allUpdateIds(withStatus status: UpdateStatus) throws -> [UUID] { 405 let sql = "SELECT id FROM updates WHERE status = ?1;" 406 let rows = try execute(sql: sql, withArgs: [status.rawValue]) 407 return rows.map { row in 408 // swiftlint:disable:next force_cast 409 row["id"] as! UUID 410 } 411 } 412 launchableUpdatesnull413 public func launchableUpdates(withConfig config: UpdatesConfig) throws -> [Update] { 414 // if an update has successfully launched at least once, we treat it as launchable 415 // even if it has also failed to launch at least once 416 let sql = String( 417 format: "SELECT * FROM updates WHERE scope_key = ?1 AND (successful_launch_count > 0 OR failed_launch_count < 1) AND status IN (%li, %li, %li);", 418 UpdateStatus.StatusReady.rawValue, 419 UpdateStatus.StatusEmbedded.rawValue, 420 UpdateStatus.StatusDevelopment.rawValue 421 ) 422 423 let rows = try execute(sql: sql, withArgs: [config.scopeKey]) 424 return rows.map { row in 425 update(withRow: row, config: config) 426 } 427 } 428 updatenull429 public func update(withId updateId: UUID, config: UpdatesConfig) throws -> Update? { 430 let sql = "SELECT * FROM updates WHERE updates.id = ?1;" 431 let rows = try execute(sql: sql, withArgs: [updateId]) 432 if rows.isEmpty { 433 return nil 434 } 435 return update(withRow: rows.first!, config: config) 436 } 437 allAssetsnull438 public func allAssets() throws -> [UpdateAsset] { 439 let sql = "SELECT * FROM assets;" 440 let rows = try execute(sql: sql, withArgs: nil) 441 return rows.map { row in 442 asset(withRow: row) 443 } 444 } 445 assetsnull446 public func assets(withUpdateId updateId: UUID) throws -> [UpdateAsset] { 447 let sql = "SELECT assets.*, launch_asset_id FROM assets INNER JOIN updates_assets ON updates_assets.asset_id = assets.id INNER JOIN updates ON updates_assets.update_id = updates.id WHERE updates.id = ?1;" 448 let rows = try execute(sql: sql, withArgs: [updateId]) 449 return rows.map { row in 450 asset(withRow: row) 451 } 452 } 453 assetnull454 public func asset(withKey key: String?) throws -> UpdateAsset? { 455 guard let key = key else { 456 return nil 457 } 458 459 let sql = """ 460 SELECT * FROM assets WHERE "key" = ?1 LIMIT 1; 461 """ 462 463 let rows = try execute(sql: sql, withArgs: [key]) 464 if rows.isEmpty { 465 return nil 466 } 467 return asset(withRow: rows.first!) 468 } 469 jsonDatanull470 private func jsonData(withKey key: String, scopeKey: String) throws -> [String: Any]? { 471 let sql = """ 472 SELECT * FROM json_data WHERE "key" = ?1 AND "scope_key" = ?2 473 """ 474 let rows = try execute(sql: sql, withArgs: [key, scopeKey]) 475 guard let firstRow = rows.first, 476 let value = firstRow["value"] as? String else { 477 return nil 478 } 479 480 return try JSONSerialization.jsonObject(with: value.data(using: .utf8)!) as? [String: Any] 481 } 482 setJsonDatanull483 private func setJsonData(_ data: [String: Any], withKey key: String, scopeKey: String, isInTransaction: Bool) throws { 484 if !isInTransaction { 485 sqlite3_exec(db, "BEGIN;", nil, nil, nil) 486 } 487 488 let deleteSql = """ 489 DELETE FROM json_data WHERE "key" = ?1 AND "scope_key" = ?2; 490 """ 491 do { 492 _ = try execute(sql: deleteSql, withArgs: [key, scopeKey]) 493 } catch { 494 if !isInTransaction { 495 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 496 } 497 throw UpdatesDatabaseError.setJsonDataError 498 } 499 500 let insertSql = """ 501 INSERT INTO json_data ("key", "value", "last_updated", "scope_key") VALUES (?1, ?2, ?3, ?4); 502 """ 503 do { 504 _ = try execute(sql: insertSql, withArgs: [key, data, Date().timeIntervalSince1970 * 1000, scopeKey]) 505 } catch { 506 if !isInTransaction { 507 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 508 } 509 throw UpdatesDatabaseError.setJsonDataError 510 } 511 512 if !isInTransaction { 513 sqlite3_exec(db, "COMMIT;", nil, nil, nil) 514 } 515 } 516 serverDefinedHeadersnull517 public func serverDefinedHeaders(withScopeKey scopeKey: String) throws -> [String: Any]? { 518 return try jsonData(withKey: UpdatesDatabase.ServerDefinedHeadersKey, scopeKey: scopeKey) 519 } 520 manifestFiltersnull521 public func manifestFilters(withScopeKey scopeKey: String) throws -> [String: Any]? { 522 return try jsonData(withKey: UpdatesDatabase.ManifestFiltersKey, scopeKey: scopeKey) 523 } 524 staticBuildDatanull525 public func staticBuildData(withScopeKey scopeKey: String) throws -> [String: Any]? { 526 return try jsonData(withKey: UpdatesDatabase.StaticBuildDataKey, scopeKey: scopeKey) 527 } 528 extraParamsnull529 public func extraParams(withScopeKey scopeKey: String) throws -> [String: String]? { 530 return try jsonData(withKey: UpdatesDatabase.ExtraParmasKey, scopeKey: scopeKey) as? [String: String] 531 } 532 setServerDefinedHeadersnull533 public func setServerDefinedHeaders(_ serverDefinedHeaders: [String: Any], withScopeKey scopeKey: String) throws { 534 return try setJsonData(serverDefinedHeaders, withKey: UpdatesDatabase.ServerDefinedHeadersKey, scopeKey: scopeKey, isInTransaction: false) 535 } 536 setManifestFiltersnull537 public func setManifestFilters(_ manifestFilters: [String: Any], withScopeKey scopeKey: String) throws { 538 return try setJsonData(manifestFilters, withKey: UpdatesDatabase.ManifestFiltersKey, scopeKey: scopeKey, isInTransaction: false) 539 } 540 setStaticBuildDatanull541 public func setStaticBuildData(_ staticBuildData: [String: Any], withScopeKey scopeKey: String) throws { 542 return try setJsonData(staticBuildData, withKey: UpdatesDatabase.StaticBuildDataKey, scopeKey: scopeKey, isInTransaction: false) 543 } 544 setExtraParamnull545 public func setExtraParam(key: String, value: String?, withScopeKey scopeKey: String) throws { 546 sqlite3_exec(db, "BEGIN;", nil, nil, nil) 547 548 do { 549 var extraParamsToWrite = try extraParams(withScopeKey: scopeKey) ?? [:] 550 if let value = value { 551 extraParamsToWrite[key] = value 552 } else { 553 extraParamsToWrite.removeValue(forKey: key) 554 } 555 556 // ensure that this can be serialized to a structured-header dictionary 557 // this will throw for invalid values 558 _ = try StringStringDictionarySerializer.serialize(dictionary: extraParamsToWrite) 559 560 _ = try setJsonData(extraParamsToWrite, withKey: UpdatesDatabase.ExtraParmasKey, scopeKey: scopeKey, isInTransaction: true) 561 } catch { 562 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 563 throw error 564 } 565 566 sqlite3_exec(db, "COMMIT;", nil, nil, nil) 567 } 568 setMetadatanull569 internal func setMetadata(withResponseHeaderData responseHeaderData: ResponseHeaderData, scopeKey: String) throws { 570 sqlite3_exec(db, "BEGIN;", nil, nil, nil) 571 572 if let serverDefinedHeaders = responseHeaderData.serverDefinedHeaders { 573 do { 574 _ = try setJsonData(serverDefinedHeaders, withKey: UpdatesDatabase.ServerDefinedHeadersKey, scopeKey: scopeKey, isInTransaction: true) 575 } catch { 576 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 577 throw UpdatesDatabaseError.setJsonDataError 578 } 579 } 580 581 if let manifestFilters = responseHeaderData.manifestFilters { 582 do { 583 _ = try setJsonData(manifestFilters, withKey: UpdatesDatabase.ManifestFiltersKey, scopeKey: scopeKey, isInTransaction: true) 584 } catch { 585 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 586 throw UpdatesDatabaseError.setJsonDataError 587 } 588 } 589 590 sqlite3_exec(db, "COMMIT;", nil, nil, nil) 591 } 592 updatenull593 private func update(withRow row: [String: Any?], config: UpdatesConfig) -> Update { 594 let rowManifest: String = row.requiredValue(forKey: "manifest") 595 let manifest = (try? JSONSerialization.jsonObject(with: rowManifest.data(using: .utf8)!) as? [String: Any]).require("Update manifest should be a valid JSON object") 596 let keep: NSNumber = row.requiredValue(forKey: "keep") 597 let status: NSNumber = row.requiredValue(forKey: "status") 598 let successfulLaunchCount: NSNumber = row.requiredValue(forKey: "successful_launch_count") 599 let failedLaunchCount: NSNumber = row.requiredValue(forKey: "failed_launch_count") 600 601 let update = Update( 602 manifest: ManifestFactory.manifest(forManifestJSON: manifest), 603 config: config, 604 database: self, 605 updateId: row.requiredValue(forKey: "id"), 606 scopeKey: row.requiredValue(forKey: "scope_key"), 607 commitTime: UpdatesDatabaseUtils.date(fromUnixTimeMilliseconds: row.requiredValue(forKey: "commit_time")), 608 runtimeVersion: row.requiredValue(forKey: "runtime_version"), 609 keep: keep.intValue != 0, 610 status: UpdateStatus.init(rawValue: status.intValue)!, 611 isDevelopmentMode: false, 612 assetsFromManifest: nil 613 ) 614 update.lastAccessed = UpdatesDatabaseUtils.date(fromUnixTimeMilliseconds: row.requiredValue(forKey: "last_accessed")) 615 update.successfulLaunchCount = successfulLaunchCount.intValue 616 update.failedLaunchCount = failedLaunchCount.intValue 617 return update 618 } 619 assetnull620 private func asset(withRow row: [String: Any?]) -> UpdateAsset { 621 let rowMetadata = row["metadata"] 622 var metadata: [String: Any]? 623 if let rowMetadata = rowMetadata as? String { 624 metadata = (try? JSONSerialization.jsonObject(with: rowMetadata.data(using: .utf8)!) as? [String: Any]).require("Asset metadata should be a valid JSON object") 625 } 626 627 let rowExtraRequestHeaders = row["extra_request_headers"] 628 var extraRequestHeaders: [String: Any]? 629 if let rowExtraRequestHeaders = rowExtraRequestHeaders as? String { 630 extraRequestHeaders = (try? JSONSerialization.jsonObject(with: rowExtraRequestHeaders.data(using: .utf8)!) as? [String: Any]).require("Asset extra_request_headers should be a valid JSON object") 631 } 632 633 let launchAssetId: NSNumber? = row.optionalValue(forKey: "launch_asset_id") 634 635 var url: URL? 636 let rowUrl: Any? = row.optionalValue(forKey: "url") 637 if let rowUrl = rowUrl as? String { 638 url = URL(string: rowUrl) 639 } 640 641 var key: String? 642 let rowKey: Any? = row.optionalValue(forKey: "key") 643 if let rowKey = rowKey as? String { 644 key = rowKey 645 } 646 647 let assetId: NSNumber = row.requiredValue(forKey: "id") 648 let asset = UpdateAsset(key: key, type: row.optionalValue(forKey: "type")) 649 asset.assetId = assetId.intValue 650 asset.url = url 651 asset.extraRequestHeaders = extraRequestHeaders 652 asset.downloadTime = UpdatesDatabaseUtils.date(fromUnixTimeMilliseconds: row.requiredValue(forKey: "download_time")) 653 asset.filename = row.requiredValue(forKey: "relative_path") 654 asset.contentHash = row.requiredValue(forKey: "hash") 655 asset.expectedHash = row.optionalValue(forKey: "expected_hash") 656 asset.metadata = metadata 657 658 if let launchAssetId = launchAssetId?.intValue, 659 launchAssetId == assetId.intValue { 660 asset.isLaunchAsset = true 661 } else { 662 asset.isLaunchAsset = false 663 } 664 665 return asset 666 } 667 } 668