From af54e63e0e6a72515ef416701bf8d9866de794de Mon Sep 17 00:00:00 2001 From: Diego Trevisan Lara <34442011+diegotl@users.noreply.github.com> Date: Thu, 19 Oct 2023 17:19:00 +0200 Subject: [PATCH 1/3] Fix slow download speed and add more resources. (#2) * Fix slow download speed and add more resoources. * Check to update function --- Empusa.xcodeproj/project.pbxproj | 38 +++++++++- .../xcshareddata/swiftpm/Package.resolved | 9 +++ Empusa/EmpusaApp.swift | 15 +++- Empusa/EmpusaModel.swift | 7 +- Empusa/Resources/Info.plist | 10 +++ .../UI Components/CheckForUpdatesView.swift | 26 +++++++ .../Sources/EmpusaKit/Client/Client.swift | 32 ++++---- .../Sources/EmpusaKit/ContentManager.swift | 27 ++++--- .../FileManager+FoldersMerger.swift | 10 +-- .../EmpusaKit/Models/SwitchResource.swift | 68 ++++++++--------- .../EmpusaKit/Services/AssetService.swift | 4 +- .../EmpusaKit/Services/StorageService.swift | 76 ++++++++++++++++--- 12 files changed, 234 insertions(+), 88 deletions(-) create mode 100644 Empusa/Resources/Info.plist create mode 100644 Empusa/UI Components/CheckForUpdatesView.swift diff --git a/Empusa.xcodeproj/project.pbxproj b/Empusa.xcodeproj/project.pbxproj index 2e518d1..7d077be 100644 --- a/Empusa.xcodeproj/project.pbxproj +++ b/Empusa.xcodeproj/project.pbxproj @@ -17,6 +17,8 @@ 3F3B176E2ADDE117000283AE /* DestinationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3B176D2ADDE117000283AE /* DestinationView.swift */; }; 3F3B17702ADDE16A000283AE /* ResourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3B176F2ADDE16A000283AE /* ResourcesView.swift */; }; 3F3B17722ADDE25D000283AE /* DataProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3B17712ADDE25D000283AE /* DataProgressView.swift */; }; + 3FCE9D2D2AE0944C00A4E3F5 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 3FCE9D2C2AE0944C00A4E3F5 /* Sparkle */; }; + 3FCE9D312AE17F5F00A4E3F5 /* CheckForUpdatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FCE9D302AE17F5F00A4E3F5 /* CheckForUpdatesView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -43,6 +45,8 @@ 3F3B176D2ADDE117000283AE /* DestinationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationView.swift; sourceTree = ""; }; 3F3B176F2ADDE16A000283AE /* ResourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesView.swift; sourceTree = ""; }; 3F3B17712ADDE25D000283AE /* DataProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProgressView.swift; sourceTree = ""; }; + 3FCE9D2E2AE0FE2E00A4E3F5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3FCE9D302AE17F5F00A4E3F5 /* CheckForUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckForUpdatesView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -51,6 +55,7 @@ buildActionMask = 2147483647; files = ( 3F21D3412ADD77E700B6A5D9 /* EmpusaKit in Frameworks */, + 3FCE9D2D2AE0944C00A4E3F5 /* Sparkle in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -107,6 +112,7 @@ 3F21D3352ADADC8D00B6A5D9 /* Resources */ = { isa = PBXGroup; children = ( + 3FCE9D2E2AE0FE2E00A4E3F5 /* Info.plist */, 3F21D30E2ADADC7400B6A5D9 /* Assets.xcassets */, ); path = Resources; @@ -135,6 +141,7 @@ 3F3B176D2ADDE117000283AE /* DestinationView.swift */, 3F3B176F2ADDE16A000283AE /* ResourcesView.swift */, 3F3B17712ADDE25D000283AE /* DataProgressView.swift */, + 3FCE9D302AE17F5F00A4E3F5 /* CheckForUpdatesView.swift */, ); path = "UI Components"; sourceTree = ""; @@ -157,6 +164,7 @@ name = Empusa; packageProductDependencies = ( 3F21D3402ADD77E700B6A5D9 /* EmpusaKit */, + 3FCE9D2C2AE0944C00A4E3F5 /* Sparkle */, ); productName = SwitchSDTool; productReference = 3F21D3072ADADC7300B6A5D9 /* Empusa.app */; @@ -208,6 +216,9 @@ Base, ); mainGroup = 3F21D2FE2ADADC7300B6A5D9; + packageReferences = ( + 3FCE9D2B2AE0944C00A4E3F5 /* XCRemoteSwiftPackageReference "Sparkle" */, + ); productRefGroup = 3F21D3082ADADC7300B6A5D9 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -243,6 +254,7 @@ files = ( 3F3B17702ADDE16A000283AE /* ResourcesView.swift in Sources */, 3F21D30D2ADADC7300B6A5D9 /* MainView.swift in Sources */, + 3FCE9D312AE17F5F00A4E3F5 /* CheckForUpdatesView.swift in Sources */, 3F3B17722ADDE25D000283AE /* DataProgressView.swift in Sources */, 3F21D30B2ADADC7300B6A5D9 /* EmpusaApp.swift in Sources */, 3F3B176E2ADDE117000283AE /* DestinationView.swift in Sources */, @@ -396,11 +408,12 @@ CODE_SIGN_ENTITLEMENTS = Empusa/Empusa.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = D97MJ3844Q; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Empusa/Resources/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "εmpusa"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -409,7 +422,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.0.2; PRODUCT_BUNDLE_IDENTIFIER = nl.trevisa.diego.Empusa; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -425,11 +438,12 @@ CODE_SIGN_ENTITLEMENTS = Empusa/Empusa.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = D97MJ3844Q; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Empusa/Resources/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "εmpusa"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -438,7 +452,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.0.2; PRODUCT_BUNDLE_IDENTIFIER = nl.trevisa.diego.Empusa; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -516,11 +530,27 @@ }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + 3FCE9D2B2AE0944C00A4E3F5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sparkle-project/Sparkle"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.5.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ 3F21D3402ADD77E700B6A5D9 /* EmpusaKit */ = { isa = XCSwiftPackageProductDependency; productName = EmpusaKit; }; + 3FCE9D2C2AE0944C00A4E3F5 /* Sparkle */ = { + isa = XCSwiftPackageProductDependency; + package = 3FCE9D2B2AE0944C00A4E3F5 /* XCRemoteSwiftPackageReference "Sparkle" */; + productName = Sparkle; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 3F21D2FF2ADADC7300B6A5D9 /* Project object */; diff --git a/Empusa.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Empusa.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 66598a9..cf7e7ee 100644 --- a/Empusa.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Empusa.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "sparkle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sparkle-project/Sparkle", + "state" : { + "revision" : "1f07f4096e52f19b5e7abaa697b7fc592b7ff57c", + "version" : "2.5.1" + } + }, { "identity" : "zip", "kind" : "remoteSourceControl", diff --git a/Empusa/EmpusaApp.swift b/Empusa/EmpusaApp.swift index 06d283b..cc6b5a9 100644 --- a/Empusa/EmpusaApp.swift +++ b/Empusa/EmpusaApp.swift @@ -1,14 +1,27 @@ import SwiftUI +import Sparkle @main struct EmpusaApp: App { + private let updaterController: SPUStandardUpdaterController = .init( + startingUpdater: true, + updaterDelegate: nil, + userDriverDelegate: nil + ) + var body: some Scene { WindowGroup { MainView( model: EmpusaModel() ) - .frame(width: 680, height: 400) + .frame(width: 680, height: 420) } .windowResizability(.contentSize) + .commands { + CommandGroup(replacing: CommandGroupPlacement.newItem) {} + CommandGroup(after: .appInfo) { + CheckForUpdatesView(updater: updaterController.updater) + } + } } } diff --git a/Empusa/EmpusaModel.swift b/Empusa/EmpusaModel.swift index 3258908..6849b0e 100644 --- a/Empusa/EmpusaModel.swift +++ b/Empusa/EmpusaModel.swift @@ -113,6 +113,11 @@ final class EmpusaModel: ObservableObject { into: selectedVolume.url, progressSubject: progressSubject ) + + self.alertData = .init( + title: "Success", + message: "Selected resources have been downloaded into the selected destination." + ) } catch { alertData = .init(error: error) } @@ -170,7 +175,7 @@ final class EmpusaModel: ObservableObject { self.alertData = .init( title: "Success", - message: "Backup sucessfully restored to the selected volume" + message: "Backup sucessfully restored to the selected destination." ) } } diff --git a/Empusa/Resources/Info.plist b/Empusa/Resources/Info.plist new file mode 100644 index 0000000..c2bd41e --- /dev/null +++ b/Empusa/Resources/Info.plist @@ -0,0 +1,10 @@ + + + + + SUFeedURL + https://empusa.pacotevicio.app/sparkle.xml + SUPublicEDKey + 62os1l9hx8vk8noKLn706G/htYbpMlhOP2TTJQFa66w= + + diff --git a/Empusa/UI Components/CheckForUpdatesView.swift b/Empusa/UI Components/CheckForUpdatesView.swift new file mode 100644 index 0000000..7516d76 --- /dev/null +++ b/Empusa/UI Components/CheckForUpdatesView.swift @@ -0,0 +1,26 @@ +import SwiftUI +import Sparkle + +final class CheckForUpdatesViewModel: ObservableObject { + @Published var canCheckForUpdates = false + + init(updater: SPUUpdater) { + updater.publisher(for: \.canCheckForUpdates) + .assign(to: &$canCheckForUpdates) + } +} + +struct CheckForUpdatesView: View { + @ObservedObject private var checkForUpdatesViewModel: CheckForUpdatesViewModel + private let updater: SPUUpdater + + init(updater: SPUUpdater) { + self.updater = updater + self.checkForUpdatesViewModel = CheckForUpdatesViewModel(updater: updater) + } + + var body: some View { + Button("Check for Updates...", action: updater.checkForUpdates) + .disabled(!checkForUpdatesViewModel.canCheckForUpdates) + } +} diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Client/Client.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Client/Client.swift index 38e3a77..4bc7ce3 100644 --- a/Packages/EmpusaKit/Sources/EmpusaKit/Client/Client.swift +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Client/Client.swift @@ -2,14 +2,17 @@ import Foundation import Combine protocol ClientProtocol { - func request(url: URL) async throws -> T + func request( + url: URL + ) async throws -> T + func downloadFile( url: URL, progressSubject: CurrentValueSubject - ) async throws -> Data + ) async throws -> URL } -final class Client: ClientProtocol { +final class Client: NSObject, ClientProtocol { func request(url: URL) async throws -> T { let request = URLRequest(url: url) let (data, response) = try await URLSession @@ -26,21 +29,16 @@ final class Client: ClientProtocol { func downloadFile( url: URL, progressSubject: CurrentValueSubject - ) async throws -> Data { - let (asyncBytes, urlResponse) = try await URLSession - .shared - .bytes(from: url) - - let length = urlResponse.expectedContentLength - var data = Data() - data.reserveCapacity(Int(length)) - - for try await byte in asyncBytes { - data.append(byte) - let downloadProgress = Double(data.count) / Double(length) - progressSubject.send(downloadProgress) + ) async throws -> URL { + // TODO: implement download progress + defer { + progressSubject.send(1) } - return data + let (localUrl, response) = try await URLSession + .shared + .download(from: url) + + return localUrl } } diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/ContentManager.swift b/Packages/EmpusaKit/Sources/EmpusaKit/ContentManager.swift index 34daad8..72b1ba5 100644 --- a/Packages/EmpusaKit/Sources/EmpusaKit/ContentManager.swift +++ b/Packages/EmpusaKit/Sources/EmpusaKit/ContentManager.swift @@ -42,43 +42,48 @@ public final class ContentManager: ContentManagerProtocol { // Download asset progressTitleSubject.send("Downloading \(resource.assetFileName)...") - let asset = try await githubService.downloadAsset( + let assetFileTempUrl = try await githubService.downloadAsset( for: resource, progressSubject: downloadProgressSubject ) - progressTitleSubject.send("Saving \(resource.assetFileName)...") - let assetFilePath = try await storageService.saveFile( - data: asset, - fileName: resource.assetFileName + // Rename asset file + let assetFileUrl = assetFileTempUrl + .deletingLastPathComponent() + .appending(component: resource.assetFileName) + + try storageService.moveItem( + at: assetFileTempUrl, + to: assetFileUrl ) - let extractedPath = try { + let extractedUrl = try { if resource.isAssetZipped { // Unzip asset progressTitleSubject.send("Unzipping \(resource.assetFileName)...") return try storageService.unzipFile( - at: assetFilePath, + at: assetFileUrl, progressSubject: unzipProgressSubject ) } else { + // Do nothing unzipProgressSubject.send(1) - return assetFilePath + return assetFileUrl } }() // Copy asset to SD progressTitleSubject.send("Copying contents of \(resource.assetFileName) into destination...") try resource.handleAsset( - at: extractedPath, + at: extractedUrl, destination: destination, progressSubject: mergeProgressSubject ) // Delete downloaded files on disk progressTitleSubject.send("Removing temporary files...") - storageService.removeItem(at: assetFilePath) - storageService.removeItem(at: extractedPath) + storageService.removeItem(at: assetFileUrl) + storageService.removeItem(at: extractedUrl) cancellable.cancel() } diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Extensions/FileManager+FoldersMerger.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Extensions/FileManager+FoldersMerger.swift index 58aefa2..34eaa0c 100644 --- a/Packages/EmpusaKit/Sources/EmpusaKit/Extensions/FileManager+FoldersMerger.swift +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Extensions/FileManager+FoldersMerger.swift @@ -10,11 +10,6 @@ extension FileManager { .init(subsystem: "nl.trevisa.diego.Empusa.Services", category: "FileManager") } - enum ActionType { - case move - case copy - } - enum ConflictResolution { case keepSource case keepDestination @@ -22,7 +17,8 @@ extension FileManager { func moveFile( at location: URL, - to destination: URL + to destination: URL, + progressSubject: CurrentValueSubject ) { let fileName = location.lastPathComponent let filedestination = destination.appending(path: fileName) @@ -50,6 +46,8 @@ extension FileManager { } catch { logger.error("Move file error: \(error.localizedDescription)") } + + progressSubject.send(1) } func merge( diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift index 0e04dce..8acf8f7 100644 --- a/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift @@ -29,18 +29,25 @@ public enum SwitchResouceSource { public enum SwitchResource: String, CaseIterable { case hekate + case hekateIPL case atmosphere + case fusee case sigpatches case tinfoil case bootLogos case lockpickRCM + case hbAppStore public var displayName: String { switch self { case .hekate: "Hekate" + case .hekateIPL: + "hekate_ipl.ini" case .atmosphere: "Atmosphère" + case .fusee: + "Fusée" case .sigpatches: "Sigpatches" case .tinfoil: @@ -49,6 +56,8 @@ public enum SwitchResource: String, CaseIterable { "Boot logos" case .lockpickRCM: "Lockpick RCM" + case .hbAppStore: + "HB App Store" } } @@ -59,11 +68,21 @@ public enum SwitchResource: String, CaseIterable { .init(string: "https://api.github.com/repos/CTCaer/hekate/releases/latest")!, assetPrefix: "hekate_ctcaer_" ) + case .hekateIPL: + .link( + .init(string: "https://nh-server.github.io/switch-guide/files/emu/hekate_ipl.ini")!, + version: "(from NH Switch Guide)" + ) case .atmosphere: .github( .init(string: "https://api.github.com/repos/Atmosphere-NX/Atmosphere/releases/latest")!, assetPrefix: "atmosphere-" ) + case .fusee: + .github( + .init(string: "https://api.github.com/repos/Atmosphere-NX/Atmosphere/releases/latest")!, + assetPrefix: "fusee.bin" + ) case .sigpatches: .link( .init(string: "https://sigmapatches.coomer.party/sigpatches.zip")!, @@ -77,13 +96,18 @@ public enum SwitchResource: String, CaseIterable { case .bootLogos: .link( .init(string: "https://nh-server.github.io/switch-guide/files/bootlogos.zip")!, - version: nil + version: "(from NH Switch Guide)" ) case .lockpickRCM: .forgejo( .init(string: "https://vps.suchmeme.nl/git/api/v1/repos/mudkip/Lockpick_RCM/releases/latest")!, assetPrefix: "Lockpick_RCM.bin" ) + case .hbAppStore: + .github( + .init(string: "https://api.github.com/repos/fortheusers/hb-appstore/releases/latest")!, + assetPrefix: "appstore.nro" + ) } } @@ -91,8 +115,12 @@ public enum SwitchResource: String, CaseIterable { switch self { case .hekate: "hekate.zip" + case .hekateIPL: + "hekate_ipl.ini" case .atmosphere: "atmosphere.zip" + case .fusee: + "fusee.bin" case .sigpatches: "sigpatches.zip" case .tinfoil: @@ -101,49 +129,17 @@ public enum SwitchResource: String, CaseIterable { "bootlogos.zip" case .lockpickRCM: "Lockpick_RCM.bin" + case .hbAppStore: + "appstore.nro" } } var isAssetZipped: Bool { switch self { - case .lockpickRCM: + case .hekateIPL, .lockpickRCM, .hbAppStore, .fusee: false default: true } } } - -extension SwitchResource { - private var fileManager: FileManager { - .default - } - - func handleAsset( - at location: URL, - destination: URL, - progressSubject: CurrentValueSubject - ) throws { - switch self { - case .hekate, .bootLogos: - fileManager.merge( - atPath: location.appending(path: "bootloader").path(), - toPath: destination.appending(path: "bootloader").path(), - progressSubject: progressSubject - ) - - case .atmosphere, .sigpatches, .tinfoil: - fileManager.merge( - atPath: location.path(), - toPath: destination.path(), - progressSubject: progressSubject - ) - - case .lockpickRCM: - fileManager.moveFile( - at: location, - to: destination.appending(path: "bootloader").appending(path: "payloads") - ) - } - } -} diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Services/AssetService.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Services/AssetService.swift index 7f3587b..3360a2e 100644 --- a/Packages/EmpusaKit/Sources/EmpusaKit/Services/AssetService.swift +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Services/AssetService.swift @@ -8,7 +8,7 @@ enum AssetServiceError: Error { public protocol AssetServiceProtocol { func fetchRepositoryRelease(for resourceUrl: URL) async throws -> RepositoryRelease - func downloadAsset(for resource: SwitchResource, progressSubject: CurrentValueSubject) async throws -> Data + func downloadAsset(for resource: SwitchResource, progressSubject: CurrentValueSubject) async throws -> URL } public final class AssetService: AssetServiceProtocol { @@ -28,7 +28,7 @@ public final class AssetService: AssetServiceProtocol { public func downloadAsset( for resource: SwitchResource, progressSubject: CurrentValueSubject - ) async throws -> Data { + ) async throws -> URL { switch resource.source { case .github(let url, let assetPrefix), .forgejo(let url, let assetPrefix): let release = try await fetchRepositoryRelease(for: url) diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Services/StorageService.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Services/StorageService.swift index eac46dd..7824938 100644 --- a/Packages/EmpusaKit/Sources/EmpusaKit/Services/StorageService.swift +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Services/StorageService.swift @@ -5,10 +5,10 @@ import OSLog public protocol StorageServiceProtocol { func listExternalVolumes() throws -> [ExternalVolume] - func saveFile(data: Data, fileName: String) async throws -> URL func unzipFile(at location: URL, progressSubject: CurrentValueSubject) throws -> URL func unzipFile(at location: URL, to destination: URL, progressSubject: CurrentValueSubject) throws - func removeItem(at path: URL) + func moveItem(at location: URL, to destination: URL) throws + func removeItem(at location: URL) func zipDirectory(at location: URL, progressSubject: CurrentValueSubject) throws -> ZipFile } @@ -58,12 +58,6 @@ final public class StorageService: StorageServiceProtocol { } } - public func saveFile(data: Data, fileName: String) throws -> URL { - let destinationPath = tempDirectoryPath.appending(path: fileName) - try data.write(to: destinationPath) - return destinationPath - } - public func unzipFile( at location: URL, progressSubject: CurrentValueSubject @@ -99,9 +93,20 @@ final public class StorageService: StorageServiceProtocol { } } - public func removeItem(at path: URL) { + public func moveItem( + at location: URL, + to destination: URL + ) throws { + try? fileManager.removeItem(at: destination) + try fileManager.moveItem( + at: location, + to: destination + ) + } + + public func removeItem(at location: URL) { do { - try fileManager.removeItem(at: path) + try fileManager.removeItem(at: location) } catch { logger.error("StorageService: \(error.localizedDescription)") } @@ -135,3 +140,54 @@ final public class StorageService: StorageServiceProtocol { ) } } + +// MARK: - SwitchResource extensions + +extension SwitchResource { + private var fileManager: FileManager { + .default + } + + func handleAsset( + at location: URL, + destination: URL, + progressSubject: CurrentValueSubject + ) throws { + switch self { + case .hekate, .bootLogos: + fileManager.merge( + atPath: location.appending(path: "bootloader").path(), + toPath: destination.appending(path: "bootloader").path(), + progressSubject: progressSubject + ) + + case .atmosphere, .sigpatches, .tinfoil: + fileManager.merge( + atPath: location.path(), + toPath: destination.path(), + progressSubject: progressSubject + ) + + case .hekateIPL: + fileManager.moveFile( + at: location, + to: destination.appending(path: "bootloader"), + progressSubject: progressSubject + ) + + case .lockpickRCM, .fusee: + fileManager.moveFile( + at: location, + to: destination.appending(path: "bootloader").appending(path: "payloads"), + progressSubject: progressSubject + ) + + case .hbAppStore: + fileManager.moveFile( + at: location, + to: destination.appending(path: "switch"), + progressSubject: progressSubject + ) + } + } +} From 5393848fbaea437912f59b7254d29befde53d071 Mon Sep 17 00:00:00 2001 From: Diego Trevisan Lara <34442011+diegotl@users.noreply.github.com> Date: Fri, 20 Oct 2023 08:29:14 +0200 Subject: [PATCH 2/3] Keep log file in root of volume for controlling installed versions (#8) * Keep log file in root of volume for controlling installed versions. * Add several new resources --- Empusa.xcodeproj/project.pbxproj | 8 +- Empusa/EmpusaModel.swift | 45 +++-- Empusa/UI Components/ResourcesView.swift | 16 +- .../Sources/EmpusaKit/Client/Client.swift | 4 +- .../Sources/EmpusaKit/ContentManager.swift | 174 +++++++++++------- .../FileManager+FoldersMerger.swift | 2 +- .../Sources/EmpusaKit/Models/Asset.swift | 6 + .../Sources/EmpusaKit/Models/EmpusaLog.swift | 22 +++ .../EmpusaKit/Models/SwitchResource.swift | 141 +++++++++----- .../EmpusaKit/Services/AssetService.swift | 22 ++- .../EmpusaKit/Services/ResourceService.swift | 18 +- .../EmpusaKit/Services/StorageService.swift | 47 ++++- 12 files changed, 348 insertions(+), 157 deletions(-) create mode 100644 Packages/EmpusaKit/Sources/EmpusaKit/Models/Asset.swift create mode 100644 Packages/EmpusaKit/Sources/EmpusaKit/Models/EmpusaLog.swift diff --git a/Empusa.xcodeproj/project.pbxproj b/Empusa.xcodeproj/project.pbxproj index 7d077be..ae0e4c9 100644 --- a/Empusa.xcodeproj/project.pbxproj +++ b/Empusa.xcodeproj/project.pbxproj @@ -408,7 +408,7 @@ CODE_SIGN_ENTITLEMENTS = Empusa/Empusa.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = D97MJ3844Q; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -422,7 +422,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = nl.trevisa.diego.Empusa; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -438,7 +438,7 @@ CODE_SIGN_ENTITLEMENTS = Empusa/Empusa.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = D97MJ3844Q; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -452,7 +452,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = nl.trevisa.diego.Empusa; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Empusa/EmpusaModel.swift b/Empusa/EmpusaModel.swift index 6849b0e..fd55084 100644 --- a/Empusa/EmpusaModel.swift +++ b/Empusa/EmpusaModel.swift @@ -77,7 +77,6 @@ final class EmpusaModel: ObservableObject { // MARK: - Init init() { loadExternalVolumes() - loadResourcesVersions() } // MARK: - Public functions @@ -107,23 +106,27 @@ final class EmpusaModel: ObservableObject { .receive(on: RunLoop.main) .assign(to: &$progress) - do { - try await contentManager.download( - resources: selectedResources, - into: selectedVolume.url, - progressSubject: progressSubject - ) + let result = await contentManager.download( + resources: selectedResources, + into: selectedVolume, + progressSubject: progressSubject + ) - self.alertData = .init( + isProcessing = false + self.progress = nil + loadResourcesVersions() + + if !result.failedResources.isEmpty { + alertData = .init( + title: "Alert", + message: "Failed to install \(result.failedResourceNames):\n\n\(result.failedResources.last!.error.localizedDescription)" + ) + } else { + alertData = .init( title: "Success", message: "Selected resources have been downloaded into the selected destination." ) - } catch { - alertData = .init(error: error) } - - isProcessing = false - self.progress = nil } } @@ -181,10 +184,18 @@ final class EmpusaModel: ObservableObject { } func loadResourcesVersions() { - Task { [resourceService, weak self] in - self?.isLoadingResources = true - self?.availableResources = await resourceService.fetchResources() - self?.isLoadingResources = false + Task { [weak self] in + guard let self else { return } + self.isLoadingResources = true + + self.availableResources = await resourceService + .fetchResources(for: self.selectedVolume) + + self.selectedResources = availableResources + .filter{ $0.preChecked } + .map { $0.resource } + + self.isLoadingResources = false } } } diff --git a/Empusa/UI Components/ResourcesView.swift b/Empusa/UI Components/ResourcesView.swift index dc979ef..565817a 100644 --- a/Empusa/UI Components/ResourcesView.swift +++ b/Empusa/UI Components/ResourcesView.swift @@ -25,8 +25,14 @@ struct ResourcesView: View { model.selectedResources.remove(at: index) } })) { - Text(displayingResource.formattedName) - .frame(maxWidth: .infinity, alignment: .leading) + HStack { + Text(displayingResource.formattedName) + .frame(maxWidth: .infinity, alignment: .leading) + + if let version = displayingResource.version { + Text(version) + } + } } } } @@ -36,6 +42,12 @@ struct ResourcesView: View { } } .disabled(model.isProcessing) + .onAppear(perform: { + model.loadResourcesVersions() + }) + .onChange(of: model.selectedVolume) { _ in + model.loadResourcesVersions() + } } } diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Client/Client.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Client/Client.swift index 4bc7ce3..df0341a 100644 --- a/Packages/EmpusaKit/Sources/EmpusaKit/Client/Client.swift +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Client/Client.swift @@ -15,7 +15,7 @@ protocol ClientProtocol { final class Client: NSObject, ClientProtocol { func request(url: URL) async throws -> T { let request = URLRequest(url: url) - let (data, response) = try await URLSession + let (data, _) = try await URLSession .shared .data(for: request) @@ -35,7 +35,7 @@ final class Client: NSObject, ClientProtocol { progressSubject.send(1) } - let (localUrl, response) = try await URLSession + let (localUrl, _) = try await URLSession .shared .download(from: url) diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/ContentManager.swift b/Packages/EmpusaKit/Sources/EmpusaKit/ContentManager.swift index 72b1ba5..a96881e 100644 --- a/Packages/EmpusaKit/Sources/EmpusaKit/ContentManager.swift +++ b/Packages/EmpusaKit/Sources/EmpusaKit/ContentManager.swift @@ -1,8 +1,25 @@ import Combine import Foundation +import OSLog + +public struct FailedResource { + let resource: SwitchResource + public let error: Error +} + +public struct ProcessResult { + public var succeededResources: [SwitchResource] = [] + public var failedResources: [FailedResource] = [] + + public var failedResourceNames: String { + failedResources + .map { $0.resource.displayName } + .joined(separator: ", ") + } +} public protocol ContentManagerProtocol { - func download(resources: [SwitchResource], into destination: URL, progressSubject: CurrentValueSubject) async throws + func download(resources: [SwitchResource], into volume: ExternalVolume, progressSubject: CurrentValueSubject) async -> ProcessResult func backupVolume(at location: URL, progressSubject: CurrentValueSubject) async throws -> ZipFile func restoreBackup(at location: URL, to destination: URL, progressSubject: CurrentValueSubject) async throws } @@ -10,83 +27,110 @@ public protocol ContentManagerProtocol { public final class ContentManager: ContentManagerProtocol { private let storageService: StorageServiceProtocol = StorageService() private let githubService: AssetServiceProtocol = AssetService() + private let logger: Logger = .init(subsystem: "nl.trevisa.diego.Empusa.EmpusaKit", category: "ContentManager") public init() {} public func download( resources: [SwitchResource], - into destination: URL, + into volume: ExternalVolume, progressSubject: CurrentValueSubject - ) async throws { + ) async -> ProcessResult { let totalProgress = Double(resources.count) * 3 + let log: EmpusaLog = storageService.getLog(at: volume) ?? .init() + var result = ProcessResult() for resource in resources { - let accumulatedProgress = 3 * Double(resources.firstIndex(of: resource)!) - let progressTitleSubject = CurrentValueSubject("") - let downloadProgressSubject = CurrentValueSubject(0) - let unzipProgressSubject = CurrentValueSubject(0) - let mergeProgressSubject = CurrentValueSubject(0) - - let cancellable = Publishers.CombineLatest4( - progressTitleSubject, - downloadProgressSubject, - unzipProgressSubject, - mergeProgressSubject - ).map { (title, downloadProgress, unzipProgress, mergeProgress) in - ProgressData( - title: title, - progress: accumulatedProgress + downloadProgress + unzipProgress + mergeProgress, - total: totalProgress + do { + let accumulatedProgress = 3 * Double(resources.firstIndex(of: resource)!) + let progressTitleSubject = CurrentValueSubject("") + let downloadProgressSubject = CurrentValueSubject(0) + let unzipProgressSubject = CurrentValueSubject(0) + let mergeProgressSubject = CurrentValueSubject(0) + + let cancellable = Publishers.CombineLatest4( + progressTitleSubject, + downloadProgressSubject, + unzipProgressSubject, + mergeProgressSubject + ).map { (title, downloadProgress, unzipProgress, mergeProgress) in + ProgressData( + title: title, + progress: accumulatedProgress + downloadProgress + unzipProgress + mergeProgress, + total: totalProgress + ) + }.assign(to: \.value, on: progressSubject) + + // Download asset + progressTitleSubject.send("Downloading \(resource.assetFileName)...") + let asset = try await githubService.downloadAsset( + for: resource, + progressSubject: downloadProgressSubject + ) + + // Rename asset file + let assetFileUrl = asset + .url + .deletingLastPathComponent() + .appending(component: resource.assetFileName) + + try storageService.moveItem( + at: asset.url, + to: assetFileUrl + ) + + let extractedUrl = try { + if resource.isAssetZipped { + // Unzip asset + progressTitleSubject.send("Unzipping \(resource.assetFileName)...") + return try storageService.unzipFile( + at: assetFileUrl, + progressSubject: unzipProgressSubject + ) + } else { + // Do nothing + unzipProgressSubject.send(1) + return assetFileUrl + } + }() + + // Copy asset to SD + progressTitleSubject.send("Copying contents of \(resource.assetFileName) into destination...") + try resource.handleAsset( + at: extractedUrl, + destination: volume.url, + progressSubject: mergeProgressSubject + ) + + // Delete downloaded files on disk + progressTitleSubject.send("Removing temporary files...") + storageService.removeItem(at: assetFileUrl) + storageService.removeItem(at: extractedUrl) + + // Update log + log.add( + resource: resource, + version: asset.version ) - }.assign(to: \.value, on: progressSubject) - - // Download asset - progressTitleSubject.send("Downloading \(resource.assetFileName)...") - let assetFileTempUrl = try await githubService.downloadAsset( - for: resource, - progressSubject: downloadProgressSubject - ) - - // Rename asset file - let assetFileUrl = assetFileTempUrl - .deletingLastPathComponent() - .appending(component: resource.assetFileName) - - try storageService.moveItem( - at: assetFileTempUrl, - to: assetFileUrl - ) - let extractedUrl = try { - if resource.isAssetZipped { - // Unzip asset - progressTitleSubject.send("Unzipping \(resource.assetFileName)...") - return try storageService.unzipFile( - at: assetFileUrl, - progressSubject: unzipProgressSubject - ) - } else { - // Do nothing - unzipProgressSubject.send(1) - return assetFileUrl - } - }() - - // Copy asset to SD - progressTitleSubject.send("Copying contents of \(resource.assetFileName) into destination...") - try resource.handleAsset( - at: extractedUrl, - destination: destination, - progressSubject: mergeProgressSubject - ) + result.succeededResources.append(resource) + cancellable.cancel() + } catch { + result.failedResources.append(.init( + resource: resource, + error: error + )) + logger.error("Fail to execute for resource \(resource.displayName): \(error.localizedDescription)") + } + } - // Delete downloaded files on disk - progressTitleSubject.send("Removing temporary files...") - storageService.removeItem(at: assetFileUrl) - storageService.removeItem(at: extractedUrl) + // Save log file + storageService.saveLog( + log, + at: volume + ) - cancellable.cancel() - } + return result } public func backupVolume( diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Extensions/FileManager+FoldersMerger.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Extensions/FileManager+FoldersMerger.swift index 34eaa0c..3c08d1b 100644 --- a/Packages/EmpusaKit/Sources/EmpusaKit/Extensions/FileManager+FoldersMerger.swift +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Extensions/FileManager+FoldersMerger.swift @@ -7,7 +7,7 @@ import Combine extension FileManager { private var logger: Logger { - .init(subsystem: "nl.trevisa.diego.Empusa.Services", category: "FileManager") + .init(subsystem: "nl.trevisa.diego.Empusa.EmpusaKit", category: "FileManager") } enum ConflictResolution { diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Models/Asset.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Models/Asset.swift new file mode 100644 index 0000000..76faee1 --- /dev/null +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Models/Asset.swift @@ -0,0 +1,6 @@ +import Foundation + +public struct DownloadedAsset { + let version: String? + let url: URL +} diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Models/EmpusaLog.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Models/EmpusaLog.swift new file mode 100644 index 0000000..ef61788 --- /dev/null +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Models/EmpusaLog.swift @@ -0,0 +1,22 @@ +import Foundation + +public struct InstalledResource: Codable { + let resource: SwitchResource + let version: String? + var updatedAt: Date = Date() +} + +public final class EmpusaLog: Codable { + var resources: [InstalledResource] = [] + + func add( + resource: SwitchResource, + version: String? + ) { + resources.removeAll(where: { $0.resource == resource }) + resources.append(.init( + resource: resource, + version: version + )) + } +} diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift index 8acf8f7..735a139 100644 --- a/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift @@ -6,17 +6,16 @@ import Combine public struct DisplayingSwitchResource: Hashable { public let resource: SwitchResource public let version: String? + public let preChecked: Bool public var formattedName: String { - [resource.displayName, version] + [ + resource.displayName, + resource.additionalDescription + ] .compactMap { $0 } .joined(separator: " ") } - - public init(resource: SwitchResource, version: String? = nil) { - self.resource = resource - self.version = version - } } // MARK: - SwitchResource @@ -27,7 +26,7 @@ public enum SwitchResouceSource { case link(URL, version: String?) } -public enum SwitchResource: String, CaseIterable { +public enum SwitchResource: String, Codable, CaseIterable { case hekate case hekateIPL case atmosphere @@ -35,29 +34,32 @@ public enum SwitchResource: String, CaseIterable { case sigpatches case tinfoil case bootLogos + case emummc case lockpickRCM case hbAppStore + case jksv + case ftpd + case nxThemesInstaller + case nxShell + case goldleaf public var displayName: String { switch self { - case .hekate: - "Hekate" - case .hekateIPL: - "hekate_ipl.ini" - case .atmosphere: - "Atmosphère" - case .fusee: - "Fusée" - case .sigpatches: - "Sigpatches" - case .tinfoil: - "Tinfoil" - case .bootLogos: - "Boot logos" - case .lockpickRCM: - "Lockpick RCM" - case .hbAppStore: - "HB App Store" + case .hekate: "Hekate" + case .hekateIPL: "hekate_ipl.ini" + case .atmosphere: "Atmosphère" + case .fusee: "Fusée" + case .sigpatches: "Sigpatches" + case .tinfoil: "Tinfoil" + case .bootLogos: "Boot logos" + case .emummc: "emummc.txt" + case .lockpickRCM: "Lockpick RCM" + case .hbAppStore: "HB App Store" + case .jksv: "JKSV" + case .ftpd: "ftpd" + case .nxThemesInstaller: "NXThemesInstaller" + case .nxShell: "NX-Shell" + case .goldleaf: "Goldleaf" } } @@ -71,7 +73,7 @@ public enum SwitchResource: String, CaseIterable { case .hekateIPL: .link( .init(string: "https://nh-server.github.io/switch-guide/files/emu/hekate_ipl.ini")!, - version: "(from NH Switch Guide)" + version: nil ) case .atmosphere: .github( @@ -96,7 +98,12 @@ public enum SwitchResource: String, CaseIterable { case .bootLogos: .link( .init(string: "https://nh-server.github.io/switch-guide/files/bootlogos.zip")!, - version: "(from NH Switch Guide)" + version: nil + ) + case .emummc: + .link( + .init(string: "https://nh-server.github.io/switch-guide/files/emummc.txt")!, + version: nil ) case .lockpickRCM: .forgejo( @@ -108,38 +115,80 @@ public enum SwitchResource: String, CaseIterable { .init(string: "https://api.github.com/repos/fortheusers/hb-appstore/releases/latest")!, assetPrefix: "appstore.nro" ) + case .jksv: + .github( + .init(string: "https://api.github.com/repos/J-D-K/JKSV/releases/latest")!, + assetPrefix: "JKSV.nro" + ) + case .ftpd: + .github( + .init(string: "https://api.github.com/repos/mtheall/ftpd/releases/latest")!, + assetPrefix: "ftpd.nro" + ) + case .nxThemesInstaller: + .github( + .init(string: "https://api.github.com/repos/exelix11/SwitchThemeInjector/releases/latest")!, + assetPrefix: "NXThemesInstaller.nro" + ) + case .nxShell: + .github( + .init(string: "https://api.github.com/repos/joel16/NX-Shell/releases/latest")!, + assetPrefix: "NX-Shell.nro" + ) + case .goldleaf: + .github( + .init(string: "https://api.github.com/repos/XorTroll/Goldleaf/releases/latest")!, + assetPrefix: "Goldleaf.nro" + ) } } var assetFileName: String { switch self { - case .hekate: - "hekate.zip" - case .hekateIPL: - "hekate_ipl.ini" - case .atmosphere: - "atmosphere.zip" - case .fusee: - "fusee.bin" - case .sigpatches: - "sigpatches.zip" - case .tinfoil: - "tinfoil.zip" - case .bootLogos: - "bootlogos.zip" - case .lockpickRCM: - "Lockpick_RCM.bin" - case .hbAppStore: - "appstore.nro" + case .hekate: "hekate.zip" + case .hekateIPL: "hekate_ipl.ini" + case .atmosphere: "atmosphere.zip" + case .fusee: "fusee.bin" + case .sigpatches: "sigpatches.zip" + case .tinfoil: "tinfoil.zip" + case .bootLogos: "bootlogos.zip" + case .emummc: "emummc.txt" + case .lockpickRCM: "Lockpick_RCM.bin" + case .hbAppStore: "appstore.nro" + case .jksv: "JKSV.nro" + case .ftpd: "ftpd.nro" + case .nxThemesInstaller: "NXThemesInstaller.nro" + case .nxShell: "NX-Shell.nro" + case .goldleaf: "Goldleaf.nro" + } + } + + var additionalDescription: String? { + switch self { + case .hekateIPL, .bootLogos, .emummc: + "(from NH Switch Guide)" + default: + nil } } var isAssetZipped: Bool { switch self { - case .hekateIPL, .lockpickRCM, .hbAppStore, .fusee: + case .hekateIPL, .emummc, .lockpickRCM, .hbAppStore, + .fusee, .jksv, .ftpd, .nxThemesInstaller, + .nxShell, .goldleaf: false default: true } } + + var uncheckedIfInstalled: Bool { + switch self { + case .hekateIPL, .bootLogos: + true + default: + false + } + } } diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Services/AssetService.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Services/AssetService.swift index 3360a2e..3b0b543 100644 --- a/Packages/EmpusaKit/Sources/EmpusaKit/Services/AssetService.swift +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Services/AssetService.swift @@ -8,12 +8,12 @@ enum AssetServiceError: Error { public protocol AssetServiceProtocol { func fetchRepositoryRelease(for resourceUrl: URL) async throws -> RepositoryRelease - func downloadAsset(for resource: SwitchResource, progressSubject: CurrentValueSubject) async throws -> URL + func downloadAsset(for resource: SwitchResource, progressSubject: CurrentValueSubject) async throws -> DownloadedAsset } public final class AssetService: AssetServiceProtocol { private let client: ClientProtocol = Client() - private let logger: Logger = .init(subsystem: "nl.trevisa.diego.Empusa.Services", category: "AssetService") + private let logger: Logger = .init(subsystem: "nl.trevisa.diego.Empusa.EmpusaKit", category: "AssetService") public init() {} @@ -28,7 +28,7 @@ public final class AssetService: AssetServiceProtocol { public func downloadAsset( for resource: SwitchResource, progressSubject: CurrentValueSubject - ) async throws -> URL { + ) async throws -> DownloadedAsset { switch resource.source { case .github(let url, let assetPrefix), .forgejo(let url, let assetPrefix): let release = try await fetchRepositoryRelease(for: url) @@ -41,16 +41,26 @@ public final class AssetService: AssetServiceProtocol { logger.info("Will download asset \(asset.name) for \(release.name) (\(release.tagName))") - return try await client.downloadFile( + let assetUrl = try await client.downloadFile( url: asset.browserDownloadUrl, progressSubject: progressSubject ) - case .link(let url, _): - return try await client.downloadFile( + return .init( + version: release.tagName, + url: assetUrl + ) + + case .link(let url, let version): + let assetUrl = try await client.downloadFile( url: url, progressSubject: progressSubject ) + + return .init( + version: version, + url: assetUrl + ) } } } diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Services/ResourceService.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Services/ResourceService.swift index 247a170..0e4157a 100644 --- a/Packages/EmpusaKit/Sources/EmpusaKit/Services/ResourceService.swift +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Services/ResourceService.swift @@ -2,27 +2,34 @@ import Foundation import OSLog public protocol ResourceServiceProtocol { - func fetchResources() async -> [DisplayingSwitchResource] + func fetchResources(for volume: ExternalVolume) async -> [DisplayingSwitchResource] } public final class ResourceService: ResourceServiceProtocol { // MARK: - Dependencies private let assetService: AssetServiceProtocol = AssetService() - private let logger: Logger = .init(subsystem: "nl.trevisa.diego.Empusa.Services", category: "ResourceService") + private let storageService: StorageServiceProtocol = StorageService() + private let logger: Logger = .init(subsystem: "nl.trevisa.diego.Empusa.EmpusaKit", category: "ResourceService") // MARK: - Public functions public init() {} - public func fetchResources() async -> [DisplayingSwitchResource] { + public func fetchResources( + for volume: ExternalVolume + ) async -> [DisplayingSwitchResource] { var displayingResources = [DisplayingSwitchResource]() + let log = storageService.getLog(at: volume) for resource in SwitchResource.allCases { + let isInstalled = log?.resources.contains(where: { $0.resource == resource }) ?? false + switch resource.source { case .github(let url, _), .forgejo(let url, _): displayingResources.append( .init( resource: resource, - version: try? await assetService.fetchRepositoryRelease(for: url).tagName + version: try? await assetService.fetchRepositoryRelease(for: url).tagName, + preChecked: !(resource.uncheckedIfInstalled && isInstalled) ) ) @@ -30,7 +37,8 @@ public final class ResourceService: ResourceServiceProtocol { displayingResources.append( .init( resource: resource, - version: version + version: version, + preChecked: !(resource.uncheckedIfInstalled && isInstalled) ) ) } diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Services/StorageService.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Services/StorageService.swift index 7824938..fa4d48a 100644 --- a/Packages/EmpusaKit/Sources/EmpusaKit/Services/StorageService.swift +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Services/StorageService.swift @@ -10,12 +10,15 @@ public protocol StorageServiceProtocol { func moveItem(at location: URL, to destination: URL) throws func removeItem(at location: URL) func zipDirectory(at location: URL, progressSubject: CurrentValueSubject) throws -> ZipFile + + func getLog(at volume: ExternalVolume) -> EmpusaLog? + func saveLog(_ log: EmpusaLog, at volume: ExternalVolume) } final public class StorageService: StorageServiceProtocol { private let fileManager = FileManager.default private let tempDirectoryPath = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) - private let logger: Logger = .init(subsystem: "nl.trevisa.diego.Empusa.Services", category: "StorageService") + private let logger: Logger = .init(subsystem: "nl.trevisa.diego.Empusa.EmpusaKit", category: "StorageService") public init() {} @@ -67,13 +70,11 @@ final public class StorageService: StorageServiceProtocol { let directoryName = fileName.replacingOccurrences(of: ".\(fileExtension)", with: "") let destination = tempDirectoryPath.appending(path: directoryName) - try Zip.unzipFile( - location, - destination: destination, - overwrite: true, - password: nil) { unzipProgress in - progressSubject.send(unzipProgress) - } + try unzipFile( + at: location, + to: destination, + progressSubject: progressSubject + ) return destination } @@ -139,6 +140,27 @@ final public class StorageService: StorageServiceProtocol { url: destinationPath ) } + + public func getLog(at volume: ExternalVolume) -> EmpusaLog? { + do { + let logUrl = volume.url.appending(component: "empusa.log") + let logData = try Data(contentsOf: logUrl) + return try JSONDecoder().decode(EmpusaLog.self, from: logData) + } catch { + logger.error("Could not load log file in volume: \(error.localizedDescription)") + return nil + } + } + + public func saveLog(_ log: EmpusaLog, at volume: ExternalVolume) { + do { + let logUrl = volume.url.appending(component: "empusa.log") + let logData = try JSONEncoder().encode(log) + try logData.write(to: logUrl) + } catch { + logger.error("Could not save log file in volume: \(error.localizedDescription)") + } + } } // MARK: - SwitchResource extensions @@ -160,6 +182,13 @@ extension SwitchResource { toPath: destination.appending(path: "bootloader").path(), progressSubject: progressSubject ) + + case .emummc: + fileManager.moveFile( + at: location, + to: destination.appending(component: "atmosphere").appending(component: "hosts"), + progressSubject: progressSubject + ) case .atmosphere, .sigpatches, .tinfoil: fileManager.merge( @@ -182,7 +211,7 @@ extension SwitchResource { progressSubject: progressSubject ) - case .hbAppStore: + case .hbAppStore, .jksv, .ftpd, .nxThemesInstaller, .nxShell, .goldleaf: fileManager.moveFile( at: location, to: destination.appending(path: "switch"), From 680ddfdb5042073205f07552f0559c17dfab1dd9 Mon Sep 17 00:00:00 2001 From: Diego Trevisan Lara <34442011+diegotl@users.noreply.github.com> Date: Fri, 20 Oct 2023 08:31:30 +0200 Subject: [PATCH 3/3] Swift Macros (#9) * Keep log file in root of volume for controlling installed versions. * Add several new resources * Integrate macros. Fix test plan. --- Empusa.xcodeproj/project.pbxproj | 4 ++ .../xcshareddata/swiftpm/Package.resolved | 9 ++++ .../xcshareddata/xcschemes/Empusa.xcscheme | 2 +- Packages/EmpusaKit/Package.swift | 6 ++- .../EmpusaKit/Models/SwitchResource.swift | 31 +++++++------ Packages/EmpusaMacros/.gitignore | 8 ++++ Packages/EmpusaMacros/Package.swift | 45 ++++++++++++++++++ .../Sources/EmpusaMacros/EmpusaMacros.swift | 4 ++ .../EmpusaMacrosImpl/EmpusaMacrosPlugin.swift | 11 +++++ .../Sources/EmpusaMacrosImpl/URLMacro.swift | 41 +++++++++++++++++ .../EmpusaMacrosTests/EmpusaMacrosTests.swift | 46 +++++++++++++++++++ .../Tests => Tests}/Empusa.xctestplan | 0 12 files changed, 189 insertions(+), 18 deletions(-) create mode 100644 Packages/EmpusaMacros/.gitignore create mode 100644 Packages/EmpusaMacros/Package.swift create mode 100644 Packages/EmpusaMacros/Sources/EmpusaMacros/EmpusaMacros.swift create mode 100644 Packages/EmpusaMacros/Sources/EmpusaMacrosImpl/EmpusaMacrosPlugin.swift create mode 100644 Packages/EmpusaMacros/Sources/EmpusaMacrosImpl/URLMacro.swift create mode 100644 Packages/EmpusaMacros/Tests/EmpusaMacrosTests/EmpusaMacrosTests.swift rename {Packages/EmpusaKit/Tests => Tests}/Empusa.xctestplan (100%) diff --git a/Empusa.xcodeproj/project.pbxproj b/Empusa.xcodeproj/project.pbxproj index ae0e4c9..c0fdda5 100644 --- a/Empusa.xcodeproj/project.pbxproj +++ b/Empusa.xcodeproj/project.pbxproj @@ -45,6 +45,8 @@ 3F3B176D2ADDE117000283AE /* DestinationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationView.swift; sourceTree = ""; }; 3F3B176F2ADDE16A000283AE /* ResourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesView.swift; sourceTree = ""; }; 3F3B17712ADDE25D000283AE /* DataProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProgressView.swift; sourceTree = ""; }; + 3F54EB962AE2553D008A45B0 /* Empusa.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Empusa.xctestplan; sourceTree = ""; }; + 3FC172272AE1D63A00DCBE28 /* EmpusaMacros */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = EmpusaMacros; sourceTree = ""; }; 3FCE9D2E2AE0FE2E00A4E3F5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3FCE9D302AE17F5F00A4E3F5 /* CheckForUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckForUpdatesView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -104,6 +106,7 @@ 3F21D31B2ADADC7400B6A5D9 /* Tests */ = { isa = PBXGroup; children = ( + 3F54EB962AE2553D008A45B0 /* Empusa.xctestplan */, 3F21D31C2ADADC7400B6A5D9 /* EmpusaTests.swift */, ); path = Tests; @@ -121,6 +124,7 @@ 3F21D3362ADADE8B00B6A5D9 /* Packages */ = { isa = PBXGroup; children = ( + 3FC172272AE1D63A00DCBE28 /* EmpusaMacros */, 3F21D3372ADADEBE00B6A5D9 /* EmpusaKit */, ); path = Packages; diff --git a/Empusa.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Empusa.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cf7e7ee..deff244 100644 --- a/Empusa.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Empusa.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -9,6 +9,15 @@ "version" : "2.5.1" } }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b", + "version" : "509.0.0" + } + }, { "identity" : "zip", "kind" : "remoteSourceControl", diff --git a/Empusa.xcodeproj/xcshareddata/xcschemes/Empusa.xcscheme b/Empusa.xcodeproj/xcshareddata/xcschemes/Empusa.xcscheme index 08f959f..730b674 100644 --- a/Empusa.xcodeproj/xcshareddata/xcschemes/Empusa.xcscheme +++ b/Empusa.xcodeproj/xcshareddata/xcschemes/Empusa.xcscheme @@ -29,7 +29,7 @@ shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/Packages/EmpusaKit/Package.swift b/Packages/EmpusaKit/Package.swift index e4bbabe..7147369 100644 --- a/Packages/EmpusaKit/Package.swift +++ b/Packages/EmpusaKit/Package.swift @@ -13,13 +13,15 @@ let package = Package( targets: ["EmpusaKit"]), ], dependencies: [ - .package(url: "https://github.com/marmelroy/Zip.git", from: "2.1.2") + .package(url: "https://github.com/marmelroy/Zip.git", from: "2.1.2"), + .package(path: "../EmpusaMacros") ], targets: [ .target( name: "EmpusaKit", dependencies: [ - .product(name: "Zip", package: "Zip") + .product(name: "Zip", package: "Zip"), + .product(name: "EmpusaMacros", package: "EmpusaMacros") ] ), .testTarget( diff --git a/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift b/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift index 735a139..f7f80d7 100644 --- a/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift +++ b/Packages/EmpusaKit/Sources/EmpusaKit/Models/SwitchResource.swift @@ -1,5 +1,6 @@ import Foundation import Combine +import EmpusaMacros // MARK: - DisplayingSwitchResource @@ -67,77 +68,77 @@ public enum SwitchResource: String, Codable, CaseIterable { switch self { case .hekate: .github( - .init(string: "https://api.github.com/repos/CTCaer/hekate/releases/latest")!, + #URL("https://api.github.com/repos/CTCaer/hekate/releases/latest"), assetPrefix: "hekate_ctcaer_" ) case .hekateIPL: .link( - .init(string: "https://nh-server.github.io/switch-guide/files/emu/hekate_ipl.ini")!, + #URL("https://nh-server.github.io/switch-guide/files/emu/hekate_ipl.ini"), version: nil ) case .atmosphere: .github( - .init(string: "https://api.github.com/repos/Atmosphere-NX/Atmosphere/releases/latest")!, + #URL("https://api.github.com/repos/Atmosphere-NX/Atmosphere/releases/latest"), assetPrefix: "atmosphere-" ) case .fusee: .github( - .init(string: "https://api.github.com/repos/Atmosphere-NX/Atmosphere/releases/latest")!, + #URL("https://api.github.com/repos/Atmosphere-NX/Atmosphere/releases/latest"), assetPrefix: "fusee.bin" ) case .sigpatches: .link( - .init(string: "https://sigmapatches.coomer.party/sigpatches.zip")!, + #URL("https://sigmapatches.coomer.party/sigpatches.zip"), version: "16.1.0" ) case .tinfoil: .github( - .init(string: "https://api.github.com/repos/kkkkyue/Tinfoil/releases/latest")!, + #URL("https://api.github.com/repos/kkkkyue/Tinfoil/releases/latest"), assetPrefix: "Tinfoil.Self.Installer" ) case .bootLogos: .link( - .init(string: "https://nh-server.github.io/switch-guide/files/bootlogos.zip")!, + #URL("https://nh-server.github.io/switch-guide/files/bootlogos.zip"), version: nil ) case .emummc: .link( - .init(string: "https://nh-server.github.io/switch-guide/files/emummc.txt")!, + #URL("https://nh-server.github.io/switch-guide/files/emummc.txt"), version: nil ) case .lockpickRCM: .forgejo( - .init(string: "https://vps.suchmeme.nl/git/api/v1/repos/mudkip/Lockpick_RCM/releases/latest")!, + #URL("https://vps.suchmeme.nl/git/api/v1/repos/mudkip/Lockpick_RCM/releases/latest"), assetPrefix: "Lockpick_RCM.bin" ) case .hbAppStore: .github( - .init(string: "https://api.github.com/repos/fortheusers/hb-appstore/releases/latest")!, + #URL("https://api.github.com/repos/fortheusers/hb-appstore/releases/latest"), assetPrefix: "appstore.nro" ) case .jksv: .github( - .init(string: "https://api.github.com/repos/J-D-K/JKSV/releases/latest")!, + #URL("https://api.github.com/repos/J-D-K/JKSV/releases/latest"), assetPrefix: "JKSV.nro" ) case .ftpd: .github( - .init(string: "https://api.github.com/repos/mtheall/ftpd/releases/latest")!, + #URL("https://api.github.com/repos/mtheall/ftpd/releases/latest"), assetPrefix: "ftpd.nro" ) case .nxThemesInstaller: .github( - .init(string: "https://api.github.com/repos/exelix11/SwitchThemeInjector/releases/latest")!, + #URL("https://api.github.com/repos/exelix11/SwitchThemeInjector/releases/latest"), assetPrefix: "NXThemesInstaller.nro" ) case .nxShell: .github( - .init(string: "https://api.github.com/repos/joel16/NX-Shell/releases/latest")!, + #URL("https://api.github.com/repos/joel16/NX-Shell/releases/latest"), assetPrefix: "NX-Shell.nro" ) case .goldleaf: .github( - .init(string: "https://api.github.com/repos/XorTroll/Goldleaf/releases/latest")!, + #URL("https://api.github.com/repos/XorTroll/Goldleaf/releases/latest"), assetPrefix: "Goldleaf.nro" ) } diff --git a/Packages/EmpusaMacros/.gitignore b/Packages/EmpusaMacros/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/Packages/EmpusaMacros/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Packages/EmpusaMacros/Package.swift b/Packages/EmpusaMacros/Package.swift new file mode 100644 index 0000000..106515b --- /dev/null +++ b/Packages/EmpusaMacros/Package.swift @@ -0,0 +1,45 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription +import CompilerPluginSupport + +let package = Package( + name: "EmpusaMacros", + platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "EmpusaMacros", + targets: ["EmpusaMacros"] + ), + ], + dependencies: [ + // Depend on the Swift 5.9 release of SwiftSyntax + .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + // Macro implementation that performs the source transformation of a macro. + .macro( + name: "EmpusaMacrosImpl", + dependencies: [ + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax") + ] + ), + + // Library that exposes a macro as part of its API, which is used in client programs. + .target(name: "EmpusaMacros", dependencies: ["EmpusaMacrosImpl"]), + + // A test target used to develop the macro implementation. + .testTarget( + name: "EmpusaMacrosTests", + dependencies: [ + "EmpusaMacrosImpl", + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + ] + ), + ] +) diff --git a/Packages/EmpusaMacros/Sources/EmpusaMacros/EmpusaMacros.swift b/Packages/EmpusaMacros/Sources/EmpusaMacros/EmpusaMacros.swift new file mode 100644 index 0000000..9c54b90 --- /dev/null +++ b/Packages/EmpusaMacros/Sources/EmpusaMacros/EmpusaMacros.swift @@ -0,0 +1,4 @@ +import Foundation + +@freestanding(expression) +public macro URL(_ stringLiteral: String) -> URL = #externalMacro(module: "EmpusaMacrosImpl", type: "URLMacro") diff --git a/Packages/EmpusaMacros/Sources/EmpusaMacrosImpl/EmpusaMacrosPlugin.swift b/Packages/EmpusaMacros/Sources/EmpusaMacrosImpl/EmpusaMacrosPlugin.swift new file mode 100644 index 0000000..9ccd485 --- /dev/null +++ b/Packages/EmpusaMacros/Sources/EmpusaMacrosImpl/EmpusaMacrosPlugin.swift @@ -0,0 +1,11 @@ +import SwiftCompilerPlugin +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +@main +struct EmpusaMacrosPlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + URLMacro.self + ] +} diff --git a/Packages/EmpusaMacros/Sources/EmpusaMacrosImpl/URLMacro.swift b/Packages/EmpusaMacros/Sources/EmpusaMacrosImpl/URLMacro.swift new file mode 100644 index 0000000..6542ef5 --- /dev/null +++ b/Packages/EmpusaMacros/Sources/EmpusaMacrosImpl/URLMacro.swift @@ -0,0 +1,41 @@ +import Foundation +import SwiftCompilerPlugin +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +enum URLMacroError: Error, CustomStringConvertible { + case requiresStaticStringLiteral + case malformedURL(urlString: String) + + var description: String { + switch self { + case .requiresStaticStringLiteral: + return "#URL requires a static string literal" + case .malformedURL(let urlString): + return "The input URL is malformed: \(urlString)" + } + } +} + +public struct URLMacro: ExpressionMacro { + public static func expansion( + of node: some FreestandingMacroExpansionSyntax, + in context: some MacroExpansionContext + ) throws -> ExprSyntax { + guard + let argument = node.argumentList.first?.expression, + let segments = argument.as(StringLiteralExprSyntax.self)?.segments, + segments.count == 1, + case .stringSegment(let literalSegment)? = segments.first + else { + throw URLMacroError.requiresStaticStringLiteral + } + + guard let _ = URL(string: literalSegment.content.text) else { + throw URLMacroError.malformedURL(urlString: "\(argument)") + } + + return "URL(string: \(argument))!" + } +} diff --git a/Packages/EmpusaMacros/Tests/EmpusaMacrosTests/EmpusaMacrosTests.swift b/Packages/EmpusaMacros/Tests/EmpusaMacrosTests/EmpusaMacrosTests.swift new file mode 100644 index 0000000..eb72fce --- /dev/null +++ b/Packages/EmpusaMacros/Tests/EmpusaMacrosTests/EmpusaMacrosTests.swift @@ -0,0 +1,46 @@ +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +// Macro implementations build for the host, so the corresponding module is not available when cross-compiling. Cross-compiled tests may still make use of the macro itself in end-to-end tests. +#if canImport(EmpusaMacrosMacros) +import EmpusaMacrosMacros + +let testMacros: [String: Macro.Type] = [ + "stringify": StringifyMacro.self, +] +#endif + +final class EmpusaMacrosTests: XCTestCase { + func testMacro() throws { + #if canImport(EmpusaMacrosMacros) + assertMacroExpansion( + """ + #stringify(a + b) + """, + expandedSource: """ + (a + b, "a + b") + """, + macros: testMacros + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testMacroWithStringLiteral() throws { + #if canImport(EmpusaMacrosMacros) + assertMacroExpansion( + #""" + #stringify("Hello, \(name)") + """#, + expandedSource: #""" + ("Hello, \(name)", #""Hello, \(name)""#) + """#, + macros: testMacros + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } +} diff --git a/Packages/EmpusaKit/Tests/Empusa.xctestplan b/Tests/Empusa.xctestplan similarity index 100% rename from Packages/EmpusaKit/Tests/Empusa.xctestplan rename to Tests/Empusa.xctestplan