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] 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"),