StikJIT/Sources/DeveloperDiskImageService.swift
2026-08-18 11:03:43 -06:00

112 lines
4.3 KiB
Swift

import Foundation
public struct DDIPaths: Sendable {
public var imagePath: String
public var trustcachePath: String
public var manifestPath: String
public init(imagePath: String, trustcachePath: String, manifestPath: String) {
self.imagePath = imagePath
self.trustcachePath = trustcachePath
self.manifestPath = manifestPath
}
public static func `default`(in directory: URL) -> DDIPaths {
DDIPaths(
imagePath: directory.appendingPathComponent("DDI/Image.dmg").path,
trustcachePath: directory.appendingPathComponent("DDI/Image.dmg.trustcache").path,
manifestPath: directory.appendingPathComponent("DDI/BuildManifest.plist").path)
}
var allFilesUsable: Bool {
let fileManager = FileManager.default
return allPaths.allSatisfy { path in
guard fileManager.isReadableFile(atPath: path),
let attributes = try? fileManager.attributesOfItem(atPath: path),
attributes[.type] as? FileAttributeType == .typeRegular,
let size = attributes[.size] as? NSNumber else {
return false
}
return size.int64Value > 0
}
}
var allPaths: [String] {
[imagePath, trustcachePath, manifestPath]
}
func removeCachedFiles() throws {
let fileManager = FileManager.default
for path in Set(allPaths) where fileManager.fileExists(atPath: path) {
try fileManager.removeItem(atPath: path)
}
}
}
struct DDIDownloadItem {
let name: String
let destinationPath: String
let url: URL
}
enum DDIDownloadCatalog {
private static let baseURL = URL(string: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized")!
static func items(for paths: DDIPaths) -> [DDIDownloadItem] {
[
DDIDownloadItem(name: "BuildManifest.plist", destinationPath: paths.manifestPath, url: baseURL.appendingPathComponent("BuildManifest.plist")),
DDIDownloadItem(name: "Image.dmg", destinationPath: paths.imagePath, url: baseURL.appendingPathComponent("Image.dmg")),
DDIDownloadItem(name: "Image.dmg.trustcache", destinationPath: paths.trustcachePath, url: baseURL.appendingPathComponent("Image.dmg.trustcache")),
]
}
}
@available(iOS 17.4, *)
public actor DeveloperDiskImageService {
private static let sharedInstance = DeveloperDiskImageService()
public static var shared: DeveloperDiskImageService { sharedInstance }
private let session: URLSession
public init(session: URLSession = .shared) {
self.session = session
}
public func downloadIfNeeded(to paths: DDIPaths, progress: @escaping (Double, String) -> Void = { _, _ in }) async throws {
guard !paths.allFilesUsable else { return }
try await download(to: paths, progress: progress)
}
public func download(to paths: DDIPaths, progress: @escaping (Double, String) -> Void = { _, _ in }) async throws {
let items = DDIDownloadCatalog.items(for: paths)
let total = Double(items.count)
for (index, item) in items.enumerated() {
progress(Double(index) / total, "Downloading \(item.name)...")
try await downloadFile(from: item.url, to: URL(fileURLWithPath: item.destinationPath))
progress(Double(index + 1) / total, "\(item.name) ready")
}
}
private func downloadFile(from url: URL, to destinationURL: URL) async throws {
let (temporaryURL, response) = try await session.download(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw StikJITError.ddiDownload("invalid response for \(url.absoluteString)")
}
guard (200..<300).contains(httpResponse.statusCode) else {
throw StikJITError.ddiDownload("HTTP \(httpResponse.statusCode) for \(url.absoluteString)")
}
let fileManager = FileManager.default
try fileManager.createDirectory(at: destinationURL.deletingLastPathComponent(), withIntermediateDirectories: true)
if fileManager.fileExists(atPath: destinationURL.path) {
try fileManager.removeItem(at: destinationURL)
}
try fileManager.moveItem(at: temporaryURL, to: destinationURL)
}
}