blitztext-app-linux/BlitztextMac/Services/BlitztextInstallLocationService.swift
2026-05-23 07:28:46 +02:00

177 lines
6.0 KiB
Swift

import AppKit
import Foundation
enum BlitztextInstallLocationService {
enum InstallLocation: Equatable {
case applications
case userApplications
case outsideApplications(URL)
case unknown
var isInApplicationsFolder: Bool {
switch self {
case .applications, .userApplications:
return true
case .outsideApplications, .unknown:
return false
}
}
var isCanonicalInstall: Bool {
if case .applications = self {
return true
}
return false
}
}
enum MoveError: LocalizedError {
case sourceBundleMissing(URL)
case destinationUnavailable
case destinationExists(URL)
case destinationNotWritable(URL)
case copyFailed(source: URL, destination: URL, underlying: Error)
var errorDescription: String? {
switch self {
case .sourceBundleMissing:
return "Die aktuelle App-Installation wurde nicht gefunden."
case .destinationUnavailable:
return "Der Zielordner /Applications ist nicht verfügbar."
case .destinationExists:
return "Am Zielort liegt bereits eine Blitztext-Installation."
case .destinationNotWritable:
return "Der Zielordner /Applications ist auf diesem Mac nicht beschreibbar."
case .copyFailed:
return "Blitztext konnte nicht nach /Applications kopiert werden."
}
}
}
static var bundleURL: URL {
Bundle.main.bundleURL.standardizedFileURL.resolvingSymlinksInPath()
}
static var bundleName: String {
bundleURL.deletingPathExtension().lastPathComponent
}
static var currentInstallLocation: InstallLocation {
let currentDirectory = bundleURL.deletingLastPathComponent()
let standardizedCurrentDirectory = currentDirectory.standardizedFileURL.resolvingSymlinksInPath()
if standardizedCurrentDirectory == systemApplicationsDirectoryURL.standardizedFileURL.resolvingSymlinksInPath() {
return .applications
}
if standardizedCurrentDirectory == userApplicationsDirectoryURL.standardizedFileURL.resolvingSymlinksInPath() {
return .userApplications
}
if FileManager.default.fileExists(atPath: bundleURL.path) {
return .outsideApplications(bundleURL)
}
return .unknown
}
static var shouldOfferMoveToApplications: Bool {
!currentInstallLocation.isCanonicalInstall
}
static var systemApplicationsDirectoryURL: URL {
URL(fileURLWithPath: "/Applications", isDirectory: true)
}
static var userApplicationsDirectoryURL: URL {
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Applications", isDirectory: true)
}
static var preferredInstallDirectoryURL: URL? {
systemApplicationsDirectoryURL
}
static var preferredInstallBundleURL: URL? {
preferredInstallDirectoryURL?.appendingPathComponent(bundleURL.lastPathComponent)
}
static var knownInstallBundleURLs: [URL] {
let candidates = [
bundleURL,
systemApplicationsDirectoryURL.appendingPathComponent(bundleURL.lastPathComponent),
userApplicationsDirectoryURL.appendingPathComponent(bundleURL.lastPathComponent)
]
var seen = Set<String>()
return candidates.filter { candidate in
let key = candidate.standardizedFileURL.resolvingSymlinksInPath().path
guard seen.insert(key).inserted else { return false }
return FileManager.default.fileExists(atPath: candidate.path)
}
}
static var otherInstalledBundleURLs: [URL] {
knownInstallBundleURLs.filter { $0 != bundleURL }
}
static func moveCurrentAppToApplications(replacingExisting: Bool = true) throws -> URL {
let sourceURL = bundleURL
guard FileManager.default.fileExists(atPath: sourceURL.path) else {
throw MoveError.sourceBundleMissing(sourceURL)
}
guard let destinationDirectoryURL = preferredInstallDirectoryURL else {
throw MoveError.destinationUnavailable
}
let destinationURL = destinationDirectoryURL.appendingPathComponent(sourceURL.lastPathComponent)
if sourceURL.standardizedFileURL.resolvingSymlinksInPath() == destinationURL.standardizedFileURL.resolvingSymlinksInPath() {
return destinationURL
}
try ensureDirectoryExists(at: destinationDirectoryURL)
if FileManager.default.fileExists(atPath: destinationURL.path) {
guard replacingExisting else {
throw MoveError.destinationExists(destinationURL)
}
try FileManager.default.removeItem(at: destinationURL)
}
guard FileManager.default.isWritableFile(atPath: destinationDirectoryURL.path) else {
throw MoveError.destinationNotWritable(destinationDirectoryURL)
}
do {
try FileManager.default.copyItem(at: sourceURL, to: destinationURL)
} catch {
throw MoveError.copyFailed(source: sourceURL, destination: destinationURL, underlying: error)
}
return destinationURL
}
static func moveToApplicationsAndRelaunch(replacingExisting: Bool = true) throws {
let destinationURL = try moveCurrentAppToApplications(replacingExisting: replacingExisting)
let openProcess = Process()
openProcess.executableURL = URL(fileURLWithPath: "/usr/bin/open")
openProcess.arguments = [destinationURL.path]
try openProcess.run()
DispatchQueue.main.async {
NSApplication.shared.terminate(nil)
}
}
private static func ensureDirectoryExists(at url: URL) throws {
if FileManager.default.fileExists(atPath: url.path) {
return
}
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
}
}