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 62 public let databaseQueue: DispatchQueue 63 private var db: OpaquePointer? 64 65 public required override init() { 66 self.databaseQueue = DispatchQueue(label: "expo.database.DatabaseQueue") 67 } 68 69 deinit { 70 closeDatabase() 71 } 72 73 public func openDatabase(inDirectory directory: URL) throws { 74 dispatchPrecondition(condition: .onQueue(databaseQueue)) 75 db = try UpdatesDatabaseInitialization.initializeDatabaseWithLatestSchema(inDirectory: directory) 76 } 77 78 public func closeDatabase() { 79 sqlite3_close(db) 80 db = nil 81 } 82 83 public func execute(sql: String, withArgs args: [Any?]?) throws -> [[String: Any?]] { 84 dispatchPrecondition(condition: .onQueue(databaseQueue)) 85 return try UpdatesDatabaseUtils.execute(sql: sql, withArgs: args, onDatabase: db.require("Missing database handle")) 86 } 87 88 public func executeForObjC(sql: String, withArgs args: [Any]?) throws -> [Any] { 89 return try execute(sql: sql, withArgs: args) 90 } 91 92 public func addUpdate(_ update: Update) throws { 93 let sql = """ 94 INSERT INTO "updates" ("id", "scope_key", "commit_time", "runtime_version", "manifest", "status" , "keep", "last_accessed", "successful_launch_count", "failed_launch_count") 95 VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1, ?7, ?8, ?9); 96 """ 97 _ = try execute( 98 sql: sql, 99 withArgs: [ 100 update.updateId, 101 update.scopeKey, 102 update.commitTime, 103 update.runtimeVersion, 104 update.manifest.rawManifestJSON(), 105 update.status.rawValue, 106 update.lastAccessed, 107 update.successfulLaunchCount, 108 update.failedLaunchCount 109 ] 110 ) 111 } 112 113 public func addNewAssets(_ assets: [UpdateAsset], toUpdateWithId updateId: UUID) throws { 114 sqlite3_exec(db, "BEGIN;", nil, nil, nil) 115 116 let assetInsertSql = """ 117 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") 118 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, 0); 119 """ 120 for asset in assets { 121 do { 122 _ = try execute( 123 sql: assetInsertSql, 124 withArgs: [ 125 asset.key, 126 asset.url, 127 asset.headers, 128 asset.extraRequestHeaders, 129 asset.type, 130 asset.metadata, 131 asset.downloadTime.require("asset downloadTime should be nonnull"), 132 asset.filename, 133 asset.contentHash, 134 UpdatesDatabaseHashType.Sha1.rawValue, 135 asset.expectedHash 136 ] 137 ) 138 } catch { 139 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 140 return 141 } 142 143 // statements must stay in precisely this order for last_insert_rowid() to work correctly 144 if asset.isLaunchAsset { 145 let updateSql = "UPDATE updates SET launch_asset_id = last_insert_rowid() WHERE id = ?1;" 146 do { 147 _ = try execute(sql: updateSql, withArgs: [updateId]) 148 } catch { 149 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 150 return 151 } 152 } 153 154 let updateInsertSql = """ 155 INSERT OR REPLACE INTO updates_assets ("update_id", "asset_id") VALUES (?1, last_insert_rowid()); 156 """ 157 do { 158 _ = try execute(sql: updateInsertSql, withArgs: [updateId]) 159 } catch { 160 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 161 return 162 } 163 } 164 165 sqlite3_exec(db, "COMMIT;", nil, nil, nil) 166 } 167 168 public func addExistingAsset(_ asset: UpdateAsset, toUpdateWithId updateId: UUID) throws -> Bool { 169 guard let key = asset.key else { 170 return false 171 } 172 173 sqlite3_exec(db, "BEGIN;", nil, nil, nil) 174 175 let assetSelectSql = """ 176 SELECT id FROM assets WHERE "key" = ?1 LIMIT 1; 177 """ 178 let rows = try execute(sql: assetSelectSql, withArgs: [key]) 179 if !rows.isEmpty { 180 let assetId: NSNumber = rows[0].requiredValue(forKey: "id") 181 let insertSql = """ 182 INSERT OR REPLACE INTO updates_assets ("update_id", "asset_id") VALUES (?1, ?2); 183 """ 184 do { 185 _ = try execute(sql: insertSql, withArgs: [updateId, assetId.intValue]) 186 } catch { 187 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 188 throw UpdatesDatabaseError.addExistingAssetInsertError 189 } 190 191 if asset.isLaunchAsset { 192 let updateSql = "UPDATE updates SET launch_asset_id = ?1 WHERE id = ?2;" 193 do { 194 _ = try execute(sql: updateSql, withArgs: [assetId.intValue, updateId]) 195 } catch { 196 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 197 throw UpdatesDatabaseError.addExistingAssetInsertError 198 } 199 } 200 } 201 202 sqlite3_exec(db, "COMMIT;", nil, nil, nil) 203 204 if rows.isEmpty { 205 return false 206 } 207 208 return true 209 } 210 211 public func updateAsset(_ asset: UpdateAsset) throws { 212 let assetUpdateSql = """ 213 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; 214 """ 215 _ = try execute( 216 sql: assetUpdateSql, 217 withArgs: [ 218 asset.key, 219 asset.headers, 220 asset.extraRequestHeaders, 221 asset.type, 222 asset.metadata, 223 asset.downloadTime.require("asset downloadTime should be nonnull"), 224 asset.filename, 225 asset.contentHash.require("asset contentHash should be nonnull"), 226 asset.expectedHash, 227 asset.url?.absoluteString 228 ] 229 ) 230 } 231 232 public func mergeAsset(_ asset: UpdateAsset, withExistingEntry existingAsset: UpdateAsset) throws { 233 var shouldUpdate = false 234 235 // if the existing entry came from an embedded manifest, it may not have a URL in the database 236 if let url = asset.url, 237 existingAsset.url == nil || url != existingAsset.url { 238 existingAsset.url = url 239 shouldUpdate = true 240 } 241 242 if let extraRequestHeaders = asset.extraRequestHeaders, 243 existingAsset.extraRequestHeaders == nil || !NSDictionary(dictionary: extraRequestHeaders).isEqual(to: existingAsset.extraRequestHeaders!) { 244 existingAsset.extraRequestHeaders = extraRequestHeaders 245 shouldUpdate = true 246 } 247 248 if shouldUpdate { 249 try updateAsset(existingAsset) 250 } 251 252 // all other properties should be overridden by database values 253 asset.filename = existingAsset.filename 254 asset.contentHash = existingAsset.contentHash 255 asset.expectedHash = existingAsset.expectedHash 256 asset.downloadTime = existingAsset.downloadTime 257 } 258 259 public func markUpdateFinished(_ update: Update) throws { 260 if update.status != UpdateStatus.StatusDevelopment { 261 update.status = UpdateStatus.StatusReady 262 } 263 264 let updateSql = "UPDATE updates SET status = ?1, keep = 1 WHERE id = ?2;" 265 _ = try execute(sql: updateSql, withArgs: [update.status.rawValue, update.updateId]) 266 } 267 268 public func markUpdateAccessed(_ update: Update) throws { 269 update.lastAccessed = Date() 270 let updateSql = "UPDATE updates SET last_accessed = ?1 WHERE id = ?2;" 271 _ = try execute(sql: updateSql, withArgs: [update.lastAccessed, update.updateId]) 272 } 273 274 public func incrementSuccessfulLaunchCountForUpdate(_ update: Update) throws { 275 update.successfulLaunchCount += 1 276 let updateSql = "UPDATE updates SET successful_launch_count = ?1 WHERE id = ?2;" 277 _ = try execute(sql: updateSql, withArgs: [update.successfulLaunchCount, update.updateId]) 278 } 279 280 public func incrementFailedLaunchCountForUpdate(_ update: Update) throws { 281 update.failedLaunchCount += 1 282 let updateSql = "UPDATE updates SET failed_launch_count = ?1 WHERE id = ?2;" 283 _ = try execute(sql: updateSql, withArgs: [update.failedLaunchCount, update.updateId]) 284 } 285 286 public func setScopeKey(_ scopeKey: String, onUpdate update: Update) throws { 287 let updateSql = "UPDATE updates SET scope_key = ?1 WHERE id = ?2;" 288 _ = try execute(sql: updateSql, withArgs: [scopeKey, update.updateId]) 289 } 290 291 public func setUpdateCommitTime(_ commitTime: Date, onUpdate update: Update) throws { 292 let updateSql = "UPDATE updates SET commit_time = ?1 WHERE id = ?2;" 293 _ = try execute(sql: updateSql, withArgs: [commitTime, update.updateId]) 294 } 295 296 public func markMissingAssets(_ assets: [UpdateAsset]) throws { 297 sqlite3_exec(db, "BEGIN;", nil, nil, nil) 298 299 let updateSql = "UPDATE updates SET status = ?1 WHERE id IN (SELECT DISTINCT update_id FROM updates_assets WHERE asset_id = ?2);" 300 for asset in assets { 301 do { 302 _ = try execute(sql: updateSql, withArgs: [UpdateStatus.StatusPending.rawValue, asset.assetId]) 303 } catch { 304 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 305 throw UpdatesDatabaseError.markMissingAssetsError 306 } 307 } 308 309 sqlite3_exec(db, "COMMIT;", nil, nil, nil) 310 } 311 312 public func deleteUpdates(_ updates: [Update]) throws { 313 sqlite3_exec(db, "BEGIN;", nil, nil, nil) 314 315 let updateSql = "DELETE FROM updates WHERE id = ?1;" 316 for update in updates { 317 do { 318 _ = try execute(sql: updateSql, withArgs: [update.updateId]) 319 } catch { 320 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 321 throw UpdatesDatabaseError.deleteUpdatesError 322 } 323 } 324 325 sqlite3_exec(db, "COMMIT;", nil, nil, nil) 326 } 327 328 public func deleteUnusedAssets() throws -> [UpdateAsset] { 329 // the simplest way to mark the assets we want to delete 330 // is to mark all assets for deletion, then go back and unmark 331 // those assets in updates we want to keep 332 // this is safe as long as we do this inside of a transaction 333 334 sqlite3_exec(db, "BEGIN;", nil, nil, nil) 335 336 let update1Sql = "UPDATE assets SET marked_for_deletion = 1;" 337 do { 338 _ = try execute(sql: update1Sql, withArgs: nil) 339 } catch { 340 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 341 throw UpdatesDatabaseError.deleteUnusedAssetsError 342 } 343 344 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);" 345 do { 346 _ = try execute(sql: update2Sql, withArgs: nil) 347 } catch { 348 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 349 throw UpdatesDatabaseError.deleteUnusedAssetsError 350 } 351 352 // check for duplicate rows representing a single file on disk 353 let update3Sql = "UPDATE assets SET marked_for_deletion = 0 WHERE relative_path IN (SELECT relative_path FROM assets WHERE marked_for_deletion = 0);" 354 do { 355 _ = try execute(sql: update3Sql, withArgs: nil) 356 } catch { 357 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 358 throw UpdatesDatabaseError.deleteUnusedAssetsError 359 } 360 361 var rows: [[String: Any?]] 362 let selectSql = "SELECT * FROM assets WHERE marked_for_deletion = 1;" 363 do { 364 rows = try execute(sql: selectSql, withArgs: nil) 365 } catch { 366 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 367 throw UpdatesDatabaseError.deleteUnusedAssetsError 368 } 369 370 let assets = rows.map { row in 371 asset(withRow: row) 372 } 373 374 let deleteSql = "DELETE FROM assets WHERE marked_for_deletion = 1;" 375 do { 376 _ = try execute(sql: deleteSql, withArgs: nil) 377 } catch { 378 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 379 throw UpdatesDatabaseError.deleteUnusedAssetsError 380 } 381 382 sqlite3_exec(db, "COMMIT;", nil, nil, nil) 383 384 return assets 385 } 386 387 public func allUpdates(withConfig config: UpdatesConfig) throws -> [Update] { 388 let sql = "SELECT * FROM updates;" 389 let rows = try execute(sql: sql, withArgs: nil) 390 return rows.map { row in 391 update(withRow: row, config: config) 392 } 393 } 394 395 public func allUpdates(withStatus status: UpdateStatus, config: UpdatesConfig) throws -> [Update] { 396 let sql = "SELECT * FROM updates WHERE status = ?1;" 397 let rows = try execute(sql: sql, withArgs: [status.rawValue]) 398 return rows.map { row in 399 update(withRow: row, config: config) 400 } 401 } 402 403 public func allUpdateIds(withStatus status: UpdateStatus) throws -> [UUID] { 404 let sql = "SELECT id FROM updates WHERE status = ?1;" 405 let rows = try execute(sql: sql, withArgs: [status.rawValue]) 406 return rows.map { row in 407 // swiftlint:disable:next force_cast 408 row["id"] as! UUID 409 } 410 } 411 412 public func launchableUpdates(withConfig config: UpdatesConfig) throws -> [Update] { 413 // if an update has successfully launched at least once, we treat it as launchable 414 // even if it has also failed to launch at least once 415 let sql = String( 416 format: "SELECT * FROM updates WHERE scope_key = ?1 AND (successful_launch_count > 0 OR failed_launch_count < 1) AND status IN (%li, %li, %li);", 417 UpdateStatus.StatusReady.rawValue, 418 UpdateStatus.StatusEmbedded.rawValue, 419 UpdateStatus.StatusDevelopment.rawValue 420 ) 421 422 let rows = try execute(sql: sql, withArgs: [config.scopeKey]) 423 return rows.map { row in 424 update(withRow: row, config: config) 425 } 426 } 427 428 public func update(withId updateId: UUID, config: UpdatesConfig) throws -> Update? { 429 let sql = "SELECT * FROM updates WHERE updates.id = ?1;" 430 let rows = try execute(sql: sql, withArgs: [updateId]) 431 if rows.isEmpty { 432 return nil 433 } 434 return update(withRow: rows.first!, config: config) 435 } 436 437 public func allAssets() throws -> [UpdateAsset] { 438 let sql = "SELECT * FROM assets;" 439 let rows = try execute(sql: sql, withArgs: nil) 440 return rows.map { row in 441 asset(withRow: row) 442 } 443 } 444 445 public func assets(withUpdateId updateId: UUID) throws -> [UpdateAsset] { 446 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;" 447 let rows = try execute(sql: sql, withArgs: [updateId]) 448 return rows.map { row in 449 asset(withRow: row) 450 } 451 } 452 453 public func asset(withKey key: String?) throws -> UpdateAsset? { 454 guard let key = key else { 455 return nil 456 } 457 458 let sql = """ 459 SELECT * FROM assets WHERE "key" = ?1 LIMIT 1; 460 """ 461 462 let rows = try execute(sql: sql, withArgs: [key]) 463 if rows.isEmpty { 464 return nil 465 } 466 return asset(withRow: rows.first!) 467 } 468 469 private func jsonData(withKey key: String, scopeKey: String) throws -> [String: Any]? { 470 let sql = """ 471 SELECT * FROM json_data WHERE "key" = ?1 AND "scope_key" = ?2 472 """ 473 let rows = try execute(sql: sql, withArgs: [key, scopeKey]) 474 guard let firstRow = rows.first, 475 let value = firstRow["value"] as? String else { 476 return nil 477 } 478 479 return try JSONSerialization.jsonObject(with: value.data(using: .utf8)!) as? [String: Any] 480 } 481 482 private func setJsonData(_ data: [String: Any], withKey key: String, scopeKey: String, isInTransaction: Bool) throws { 483 if !isInTransaction { 484 sqlite3_exec(db, "BEGIN;", nil, nil, nil) 485 } 486 487 let deleteSql = """ 488 DELETE FROM json_data WHERE "key" = ?1 AND "scope_key" = ?2; 489 """ 490 do { 491 _ = try execute(sql: deleteSql, withArgs: [key, scopeKey]) 492 } catch { 493 if !isInTransaction { 494 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 495 } 496 throw UpdatesDatabaseError.setJsonDataError 497 } 498 499 let insertSql = """ 500 INSERT INTO json_data ("key", "value", "last_updated", "scope_key") VALUES (?1, ?2, ?3, ?4); 501 """ 502 do { 503 _ = try execute(sql: insertSql, withArgs: [key, data, Date().timeIntervalSince1970 * 1000, scopeKey]) 504 } catch { 505 if !isInTransaction { 506 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 507 } 508 throw UpdatesDatabaseError.setJsonDataError 509 } 510 511 if !isInTransaction { 512 sqlite3_exec(db, "COMMIT;", nil, nil, nil) 513 } 514 } 515 516 public func serverDefinedHeaders(withScopeKey scopeKey: String) throws -> [String: Any]? { 517 return try jsonData(withKey: UpdatesDatabase.ServerDefinedHeadersKey, scopeKey: scopeKey) 518 } 519 520 public func manifestFilters(withScopeKey scopeKey: String) throws -> [String: Any]? { 521 return try jsonData(withKey: UpdatesDatabase.ManifestFiltersKey, scopeKey: scopeKey) 522 } 523 524 public func staticBuildData(withScopeKey scopeKey: String) throws -> [String: Any]? { 525 return try jsonData(withKey: UpdatesDatabase.StaticBuildDataKey, scopeKey: scopeKey) 526 } 527 528 public func setServerDefinedHeaders(_ serverDefinedHeaders: [String: Any], withScopeKey scopeKey: String) throws { 529 return try setJsonData(serverDefinedHeaders, withKey: UpdatesDatabase.ServerDefinedHeadersKey, scopeKey: scopeKey, isInTransaction: false) 530 } 531 532 public func setManifestFilters(_ manifestFilters: [String: Any], withScopeKey scopeKey: String) throws { 533 return try setJsonData(manifestFilters, withKey: UpdatesDatabase.ManifestFiltersKey, scopeKey: scopeKey, isInTransaction: false) 534 } 535 536 public func setStaticBuildData(_ staticBuildData: [String: Any], withScopeKey scopeKey: String) throws { 537 return try setJsonData(staticBuildData, withKey: UpdatesDatabase.StaticBuildDataKey, scopeKey: scopeKey, isInTransaction: false) 538 } 539 540 internal func setMetadata(withResponseHeaderData responseHeaderData: ResponseHeaderData, scopeKey: String) throws { 541 sqlite3_exec(db, "BEGIN;", nil, nil, nil) 542 543 if let serverDefinedHeaders = responseHeaderData.serverDefinedHeaders { 544 do { 545 _ = try setJsonData(serverDefinedHeaders, withKey: UpdatesDatabase.ServerDefinedHeadersKey, scopeKey: scopeKey, isInTransaction: true) 546 } catch { 547 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 548 throw UpdatesDatabaseError.setJsonDataError 549 } 550 } 551 552 if let manifestFilters = responseHeaderData.manifestFilters { 553 do { 554 _ = try setJsonData(manifestFilters, withKey: UpdatesDatabase.ManifestFiltersKey, scopeKey: scopeKey, isInTransaction: true) 555 } catch { 556 sqlite3_exec(db, "ROLLBACK;", nil, nil, nil) 557 throw UpdatesDatabaseError.setJsonDataError 558 } 559 } 560 561 sqlite3_exec(db, "COMMIT;", nil, nil, nil) 562 } 563 564 private func update(withRow row: [String: Any?], config: UpdatesConfig) -> Update { 565 let rowManifest = row["manifest"] 566 var manifest: [String: Any]? 567 if let rowManifest = rowManifest as? String { 568 manifest = (try? JSONSerialization.jsonObject(with: rowManifest.data(using: .utf8)!) as? [String: Any]).require("Update manifest should be a valid JSON object") 569 } 570 571 let keep: NSNumber = row.requiredValue(forKey: "keep") 572 let status: NSNumber = row.requiredValue(forKey: "status") 573 let successfulLaunchCount: NSNumber = row.requiredValue(forKey: "successful_launch_count") 574 let failedLaunchCount: NSNumber = row.requiredValue(forKey: "failed_launch_count") 575 576 let update = Update( 577 manifest: ManifestFactory.manifest(forManifestJSON: manifest ?? [:]), 578 config: config, 579 database: self, 580 updateId: row.requiredValue(forKey: "id"), 581 scopeKey: row.requiredValue(forKey: "scope_key"), 582 commitTime: UpdatesDatabaseUtils.date(fromUnixTimeMilliseconds: row.requiredValue(forKey: "commit_time")), 583 runtimeVersion: row.requiredValue(forKey: "runtime_version"), 584 keep: keep.intValue != 0, 585 status: UpdateStatus.init(rawValue: status.intValue)!, 586 isDevelopmentMode: false, 587 assetsFromManifest: nil 588 ) 589 update.lastAccessed = UpdatesDatabaseUtils.date(fromUnixTimeMilliseconds: row.requiredValue(forKey: "last_accessed")) 590 update.successfulLaunchCount = successfulLaunchCount.intValue 591 update.failedLaunchCount = failedLaunchCount.intValue 592 return update 593 } 594 595 private func asset(withRow row: [String: Any?]) -> UpdateAsset { 596 let rowMetadata = row["metadata"] 597 var metadata: [String: Any]? 598 if let rowMetadata = rowMetadata as? String { 599 metadata = (try? JSONSerialization.jsonObject(with: rowMetadata.data(using: .utf8)!) as? [String: Any]).require("Asset metadata should be a valid JSON object") 600 } 601 602 let rowExtraRequestHeaders = row["extra_request_headers"] 603 var extraRequestHeaders: [String: Any]? 604 if let rowExtraRequestHeaders = rowExtraRequestHeaders as? String { 605 extraRequestHeaders = (try? JSONSerialization.jsonObject(with: rowExtraRequestHeaders.data(using: .utf8)!) as? [String: Any]).require("Asset extra_request_headers should be a valid JSON object") 606 } 607 608 let launchAssetId: NSNumber? = row.optionalValue(forKey: "launch_asset_id") 609 610 var url: URL? 611 let rowUrl: Any? = row.optionalValue(forKey: "url") 612 if let rowUrl = rowUrl as? String { 613 url = URL(string: rowUrl) 614 } 615 616 var key: String? 617 let rowKey: Any? = row.optionalValue(forKey: "key") 618 if let rowKey = rowKey as? String { 619 key = rowKey 620 } 621 622 let assetId: NSNumber = row.requiredValue(forKey: "id") 623 let asset = UpdateAsset(key: key, type: row.optionalValue(forKey: "type")) 624 asset.assetId = assetId.intValue 625 asset.url = url 626 asset.extraRequestHeaders = extraRequestHeaders 627 asset.downloadTime = UpdatesDatabaseUtils.date(fromUnixTimeMilliseconds: row.requiredValue(forKey: "download_time")) 628 asset.filename = row.requiredValue(forKey: "relative_path") 629 asset.contentHash = row.requiredValue(forKey: "hash") 630 asset.expectedHash = row.optionalValue(forKey: "expected_hash") 631 asset.metadata = metadata 632 633 if let launchAssetId = launchAssetId?.intValue, 634 launchAssetId == assetId.intValue { 635 asset.isLaunchAsset = true 636 } else { 637 asset.isLaunchAsset = false 638 } 639 640 return asset 641 } 642 } 643