mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-12-21 03:10:06 +01:00
♻️ Reworked how getting a CaskFile via URL works
This commit is contained in:
@@ -84,6 +84,11 @@
|
||||
039E1D7A2E5F0F300072D13D /* ValetUpgrader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */; };
|
||||
039E1D7B2E5F0F300072D13D /* ValetUpgrader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */; };
|
||||
039E1D7C2E5F0F300072D13D /* ValetUpgrader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */; };
|
||||
03ACC6452ECCAB190070D4CD /* RealWebApiTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ACC6442ECCAB140070D4CD /* RealWebApiTest.swift */; };
|
||||
03ACC6472ECCBA130070D4CD /* CaskFile+API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ACC6462ECCBA100070D4CD /* CaskFile+API.swift */; };
|
||||
03ACC6482ECCBA130070D4CD /* CaskFile+API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ACC6462ECCBA100070D4CD /* CaskFile+API.swift */; };
|
||||
03ACC6492ECCBA130070D4CD /* CaskFile+API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ACC6462ECCBA100070D4CD /* CaskFile+API.swift */; };
|
||||
03ACC64A2ECCBA130070D4CD /* CaskFile+API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ACC6462ECCBA100070D4CD /* CaskFile+API.swift */; };
|
||||
03B675E92EBA30D800EE04A9 /* NSImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B675E82EBA30D200EE04A9 /* NSImageExtension.swift */; };
|
||||
03B675EA2EBA30D800EE04A9 /* NSImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B675E82EBA30D200EE04A9 /* NSImageExtension.swift */; };
|
||||
03B675EB2EBA30D800EE04A9 /* NSImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B675E82EBA30D200EE04A9 /* NSImageExtension.swift */; };
|
||||
@@ -1038,6 +1043,8 @@
|
||||
039C29172E8AA311007F5FAB /* TestableWebApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableWebApi.swift; sourceTree = "<group>"; };
|
||||
039C291C2E8AA399007F5FAB /* TestableWebApiTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableWebApiTest.swift; sourceTree = "<group>"; };
|
||||
039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetUpgrader.swift; sourceTree = "<group>"; };
|
||||
03ACC6442ECCAB140070D4CD /* RealWebApiTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealWebApiTest.swift; sourceTree = "<group>"; };
|
||||
03ACC6462ECCBA100070D4CD /* CaskFile+API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaskFile+API.swift"; sourceTree = "<group>"; };
|
||||
03B675E82EBA30D200EE04A9 /* NSImageExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSImageExtension.swift; sourceTree = "<group>"; };
|
||||
03BFF5262E312C39007F96FA /* Startup+Timers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Startup+Timers.swift"; sourceTree = "<group>"; };
|
||||
03BFF52B2E313240007F96FA /* StatusMenu+Driver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusMenu+Driver.swift"; sourceTree = "<group>"; };
|
||||
@@ -1455,11 +1462,19 @@
|
||||
039C291E2E8AA39B007F5FAB /* Api */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
039C291C2E8AA399007F5FAB /* TestableWebApiTest.swift */,
|
||||
);
|
||||
path = Api;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
03ACC6432ECCAAF70070D4CD /* WebApi */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
039C291C2E8AA399007F5FAB /* TestableWebApiTest.swift */,
|
||||
03ACC6442ECCAB140070D4CD /* RealWebApiTest.swift */,
|
||||
);
|
||||
path = WebApi;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
03BFF1D12E3CF4F2004C56A9 /* Provision */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1485,6 +1500,7 @@
|
||||
C4E2E84628FC1D8C003B070C /* TestableConfigurationTest.swift */,
|
||||
C413E43328DA3E8F00AE33C7 /* Shell */,
|
||||
C471E6DA28F9AFCB0021E251 /* Filesystem */,
|
||||
03ACC6432ECCAAF70070D4CD /* WebApi */,
|
||||
);
|
||||
path = Testables;
|
||||
sourceTree = "<group>";
|
||||
@@ -2145,6 +2161,7 @@
|
||||
C4B79EB529CA387F00A483EE /* BrewPhpFormulaeHandler.swift */,
|
||||
C4F2E4362752F0870020E974 /* BrewDiagnostics.swift */,
|
||||
C40934A1298EEB2C00D25014 /* CaskFile.swift */,
|
||||
03ACC6462ECCBA100070D4CD /* CaskFile+API.swift */,
|
||||
C4E684082AF26B830023ED25 /* BrewTapFormulae.swift */,
|
||||
031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */,
|
||||
);
|
||||
@@ -2914,6 +2931,7 @@
|
||||
C45D654C29F52F74004C28F9 /* BrewPermissionFixer.swift in Sources */,
|
||||
C4D3660B29113F20006BD146 /* System.swift in Sources */,
|
||||
C4D36601291132B7006BD146 /* ValetScanners.swift in Sources */,
|
||||
03ACC6472ECCBA130070D4CD /* CaskFile+API.swift in Sources */,
|
||||
C4EED88927A48778006D7272 /* InterAppHandler.swift in Sources */,
|
||||
C40C7F1E2772136000DDDCDC /* PhpEnvironments.swift in Sources */,
|
||||
C4B79EB629CA387F00A483EE /* BrewPhpFormulaeHandler.swift in Sources */,
|
||||
@@ -3143,6 +3161,7 @@
|
||||
C441CC582AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */,
|
||||
C471E80828F9BAD40021E251 /* PhpExtension.swift in Sources */,
|
||||
C471E7F928F9BACB0021E251 /* PhpSwitcher.swift in Sources */,
|
||||
03ACC6482ECCBA130070D4CD /* CaskFile+API.swift in Sources */,
|
||||
C471E82A28F9BB330021E251 /* ValetListable.swift in Sources */,
|
||||
031E2B6B2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */,
|
||||
C471E82728F9BB310021E251 /* BrewDiagnostics.swift in Sources */,
|
||||
@@ -3251,6 +3270,7 @@
|
||||
C471E8B428F9BB8F0021E251 /* MainMenu+Switcher.swift in Sources */,
|
||||
C471E8B528F9BB8F0021E251 /* MainMenu+FixMyValet.swift in Sources */,
|
||||
C471E8B628F9BB8F0021E251 /* MainMenu+Actions.swift in Sources */,
|
||||
03ACC64A2ECCBA130070D4CD /* CaskFile+API.swift in Sources */,
|
||||
C471E8B728F9BB8F0021E251 /* StatusMenu.swift in Sources */,
|
||||
032DAC302E8BEB6B0018E01C /* WebApiProtocol.swift in Sources */,
|
||||
C471E8B828F9BB8F0021E251 /* StatusMenu+Items.swift in Sources */,
|
||||
@@ -3547,6 +3567,7 @@
|
||||
C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */,
|
||||
C4068CAB27B0890D00544CD5 /* MenuBarIcons.swift in Sources */,
|
||||
C4AD38B328ECD9D300FA8D83 /* TestableFileSystem.swift in Sources */,
|
||||
03ACC6492ECCBA130070D4CD /* CaskFile+API.swift in Sources */,
|
||||
C40C5C9D2846A40600E28255 /* Preset.swift in Sources */,
|
||||
C4CE7F9729683B43000102CF /* PhpVersionNumberCollection.swift in Sources */,
|
||||
C4F30B09278E1A0E00755FCE /* CustomPrefs.swift in Sources */,
|
||||
@@ -3557,6 +3578,7 @@
|
||||
C4E49DEE28F764A00026AC4E /* TestableCommand.swift in Sources */,
|
||||
C4611E612AEAD3110010BE24 /* ByteLimitView.swift in Sources */,
|
||||
C40175B92903108900763A68 /* ValetInteractor.swift in Sources */,
|
||||
03ACC6452ECCAB190070D4CD /* RealWebApiTest.swift in Sources */,
|
||||
039E1D792E5F0F300072D13D /* ValetUpgrader.swift in Sources */,
|
||||
C4CE3BBC27B324250086CA49 /* ComposerWindow.swift in Sources */,
|
||||
C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */,
|
||||
|
||||
@@ -11,15 +11,81 @@ import Foundation
|
||||
class RealWebApi: WebApiProtocol {
|
||||
var container: Container
|
||||
|
||||
var defaultHeaders: HttpHeaders {
|
||||
return [
|
||||
"User-Agent": "phpmon-nur/2.0",
|
||||
"X-phpmon-version": "\(App.shortVersion) (\(App.bundleVersion))",
|
||||
"X-phpmon-os-version": "\(App.macVersion)",
|
||||
"X-phpmon-bundle-id": "\(App.identifier)"
|
||||
]
|
||||
}
|
||||
|
||||
init(container: Container) {
|
||||
self.container = container
|
||||
}
|
||||
|
||||
func get(_ url: URL, withHeaders: HttpHeaders, withTimeout: TimeInterval) async throws -> WebApiResponse {
|
||||
return WebApiResponse(statusCode: 200, headers: [:], data: Data())
|
||||
private func request(
|
||||
url: URL,
|
||||
method: String,
|
||||
data: Data?,
|
||||
headers: HttpHeaders,
|
||||
timeout: TimeInterval
|
||||
) async throws -> WebApiResponse {
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method
|
||||
request.timeoutInterval = timeout
|
||||
for header in headers {
|
||||
request.setValue(header.value, forHTTPHeaderField: header.key)
|
||||
}
|
||||
|
||||
do {
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let response = response as? HTTPURLResponse else {
|
||||
throw WebApiError.networkError
|
||||
}
|
||||
|
||||
return WebApiResponse(
|
||||
statusCode: response.statusCode,
|
||||
headers: response.allHeaderFields as! HttpHeaders,
|
||||
data: data
|
||||
)
|
||||
} catch {
|
||||
if let urlError = error as? URLError {
|
||||
if urlError.code == .timedOut {
|
||||
throw WebApiError.timedOut
|
||||
}
|
||||
}
|
||||
throw WebApiError.networkError
|
||||
}
|
||||
}
|
||||
|
||||
func post(_ url: URL, withHeaders: HttpHeaders, withData: String, withTimeout: TimeInterval) async throws -> WebApiResponse {
|
||||
return WebApiResponse(statusCode: 200, headers: [:], data: Data())
|
||||
func get(
|
||||
_ url: URL,
|
||||
withHeaders headers: HttpHeaders = [:],
|
||||
withTimeout timeout: TimeInterval = URLSession.shared.configuration.timeoutIntervalForRequest
|
||||
) async throws -> WebApiResponse {
|
||||
try await self.request(
|
||||
url: url,
|
||||
method: "GET",
|
||||
data: nil,
|
||||
headers: headers,
|
||||
timeout: timeout
|
||||
)
|
||||
}
|
||||
|
||||
func post(
|
||||
_ url: URL,
|
||||
withHeaders headers: HttpHeaders,
|
||||
withData data: String,
|
||||
withTimeout timeout: TimeInterval
|
||||
) async throws -> WebApiResponse {
|
||||
try await self.request(
|
||||
url: url,
|
||||
method: "POST",
|
||||
data: data.data(using: .utf8),
|
||||
headers: headers,
|
||||
timeout: timeout
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,33 @@
|
||||
import Foundation
|
||||
|
||||
class TestableWebApi: WebApiProtocol {
|
||||
|
||||
// MARK: - Internal Fake Responses
|
||||
|
||||
private var fakeGetResponses: [URL: FakeWebApiResponse] = [:]
|
||||
private var fakePostResponses: [URL: FakeWebApiResponse] = [:]
|
||||
|
||||
// MARK: - Slow Mode
|
||||
|
||||
private var slow: Bool = false
|
||||
|
||||
public func setSlowMode(_ slow: Bool) {
|
||||
self.slow = slow
|
||||
}
|
||||
|
||||
// MARK: - Default Headers
|
||||
|
||||
var defaultHeaders: HttpHeaders {
|
||||
return [
|
||||
"User-Agent": "phpmon-nur/2.0",
|
||||
"X-phpmon-version": "\(App.shortVersion) (\(App.bundleVersion))",
|
||||
"X-phpmon-os-version": "\(App.macVersion)",
|
||||
"X-phpmon-bundle-id": "\(App.identifier)"
|
||||
]
|
||||
}
|
||||
|
||||
// MARK: - Constructor
|
||||
|
||||
init(
|
||||
getResponses: [URL: FakeWebApiResponse],
|
||||
postResponses: [URL: FakeWebApiResponse]
|
||||
@@ -25,6 +44,8 @@ class TestableWebApi: WebApiProtocol {
|
||||
self.fakePostResponses = postResponses
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
public func hasGetResponse(for url: URL) -> Bool {
|
||||
return fakeGetResponses.keys.contains(url)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ enum WebApiError: Error {
|
||||
case invalidURL
|
||||
case networkError
|
||||
case timedOut
|
||||
case other
|
||||
}
|
||||
|
||||
struct WebApiResponse {
|
||||
@@ -37,6 +38,8 @@ struct WebApiResponse {
|
||||
}
|
||||
|
||||
protocol WebApiProtocol {
|
||||
var defaultHeaders: HttpHeaders { get }
|
||||
|
||||
func get(
|
||||
_ url: URL,
|
||||
withHeaders headers: HttpHeaders,
|
||||
|
||||
@@ -28,8 +28,7 @@ class AppUpdater {
|
||||
|
||||
let caskUrl = Constants.Urls.UpdateCheckEndpoint
|
||||
|
||||
guard let caskFile = await CaskFile.from(App.shared.container, url: caskUrl) else {
|
||||
Log.err("The contents of the CaskFile at '\(caskUrl.absoluteString)' could not be retrieved.")
|
||||
guard let caskFile = try? await CaskFile.fromUrl(App.shared.container, caskUrl) else {
|
||||
presentCouldNotRetrieveUpdateIfInteractive()
|
||||
return .networkError
|
||||
}
|
||||
|
||||
@@ -101,15 +101,11 @@ class CrashReporter {
|
||||
request.setValue("text/crash", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("phpmon-crashrep/1.0", forHTTPHeaderField: "User-Agent")
|
||||
request.httpBody = text.data(using: .utf8)
|
||||
request.timeoutInterval = timeout
|
||||
|
||||
// Send the request synchronously, we want the report to be sent before anything else
|
||||
let config = URLSessionConfiguration.default
|
||||
config.timeoutIntervalForRequest = timeout
|
||||
|
||||
let session = URLSession(configuration: config)
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
let task = session.dataTask(with: request) { _, response, error in
|
||||
let task = URLSession.shared.dataTask(with: request) { _, response, error in
|
||||
defer { semaphore.signal() }
|
||||
|
||||
if let error = error {
|
||||
|
||||
52
phpmon/Domain/Integrations/Homebrew/CaskFile+API.swift
Normal file
52
phpmon/Domain/Integrations/Homebrew/CaskFile+API.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// CaskFile+API.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 18/11/2025.
|
||||
// Copyright © 2025 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum CaskFileError: Error {
|
||||
case requestFailed
|
||||
case invalidData
|
||||
case invalidFile
|
||||
}
|
||||
|
||||
extension CaskFile {
|
||||
public static func fromUrl(
|
||||
_ container: Container,
|
||||
_ url: URL
|
||||
) async throws -> CaskFile? {
|
||||
// First, determine if we're loading a local URL or not
|
||||
if url.scheme == "file" {
|
||||
if let string = try? container.filesystem.getStringFromFile(url.relativePath) {
|
||||
return CaskFile.from(string)
|
||||
} else {
|
||||
throw CaskFileError.invalidFile
|
||||
}
|
||||
}
|
||||
|
||||
// If it's a URL via the network we need to know if to use the complex API or not
|
||||
if isRunningTests || App.hasLoadedTestableConfiguration || url.absoluteString.contains("https://raw.githubusercontent.com") {
|
||||
// For testing simplicity, we will use curl, since we need no complex rules or headers
|
||||
return CaskFile.from(await container.shell.pipe("curl -s --max-time 10 '\(url.absoluteString)'").out)
|
||||
} else {
|
||||
// However, for the real deal, we will use the Web API
|
||||
guard let response = try? await container.webApi.get(
|
||||
url,
|
||||
withHeaders: container.webApi.defaultHeaders,
|
||||
withTimeout: .seconds(10)
|
||||
) else {
|
||||
throw CaskFileError.requestFailed
|
||||
}
|
||||
|
||||
guard let text = response.plainText else {
|
||||
throw CaskFileError.invalidData
|
||||
}
|
||||
|
||||
return CaskFile.from(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,35 +24,7 @@ struct CaskFile {
|
||||
return self.properties["version"]!
|
||||
}
|
||||
|
||||
private static func loadFromApi(_ container: Container, _ url: URL) async -> String {
|
||||
if isRunningTests || App.hasLoadedTestableConfiguration || url.absoluteString.contains("https://raw.githubusercontent.com") {
|
||||
return await container.shell.pipe("curl -s --max-time 10 '\(url.absoluteString)'").out
|
||||
} else {
|
||||
return await container.shell.pipe("""
|
||||
curl -s --max-time 10 \
|
||||
-H "User-Agent: phpmon-curl/1.0" \
|
||||
-H "X-phpmon-version: \(App.shortVersion) (\(App.bundleVersion))" \
|
||||
-H "X-phpmon-os-version: \(App.macVersion)" \
|
||||
-H "X-phpmon-bundle-id: \(App.identifier)" \
|
||||
'\(url.absoluteString)'
|
||||
""").out
|
||||
}
|
||||
}
|
||||
|
||||
public static func from(_ container: Container, url: URL) async -> CaskFile? {
|
||||
var string: String?
|
||||
|
||||
if url.scheme == "file" {
|
||||
string = try? String(contentsOf: url)
|
||||
} else {
|
||||
string = await CaskFile.loadFromApi(container, url)
|
||||
}
|
||||
|
||||
guard let string else {
|
||||
Log.err("The content of the URL for the CaskFile could not be retrieved")
|
||||
return nil
|
||||
}
|
||||
|
||||
public static func from(_ string: String) -> CaskFile? {
|
||||
let lines = string.split(separator: "\n")
|
||||
.map { line in
|
||||
return line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
@@ -27,7 +27,7 @@ struct CaskFileParserTest {
|
||||
}
|
||||
|
||||
@Test func can_extract_fields_from_cask_file() async throws {
|
||||
guard let caskFile = await CaskFile.from(container, url: CaskFileParserTest.exampleFilePath) else {
|
||||
guard let caskFile = try? await CaskFile.fromUrl(container, CaskFileParserTest.exampleFilePath) else {
|
||||
Issue.record("The CaskFile could not be parsed, check the log for more info")
|
||||
return
|
||||
}
|
||||
@@ -53,7 +53,7 @@ struct CaskFileParserTest {
|
||||
@Test func can_extract_fields_from_remote_cask_file() async throws {
|
||||
let url = URL(string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon.rb")!
|
||||
|
||||
guard let caskFile = await CaskFile.from(container, url: url) else {
|
||||
guard let caskFile = try? await CaskFile.fromUrl(container, url) else {
|
||||
Issue.record("The remote CaskFile could not be parsed, check the log for more info")
|
||||
return
|
||||
}
|
||||
|
||||
41
tests/unit/Testables/WebApi/RealWebApiTest.swift
Normal file
41
tests/unit/Testables/WebApi/RealWebApiTest.swift
Normal file
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// RealWebApiTest.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 18/11/2025.
|
||||
// Copyright © 2025 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
|
||||
struct RealWebApiTest {
|
||||
private var container: Container
|
||||
|
||||
init() throws {
|
||||
self.container = Container()
|
||||
container.bind()
|
||||
}
|
||||
|
||||
var WebApi: RealWebApi {
|
||||
return container.webApi as! RealWebApi
|
||||
}
|
||||
|
||||
@Test func requestSucceeds() async {
|
||||
let response = try! await WebApi.get(
|
||||
url("https://api.phpmon.test/up")
|
||||
)
|
||||
|
||||
#expect(response.statusCode == 200)
|
||||
#expect(response.plainText!.contains("Response rendered in"))
|
||||
}
|
||||
|
||||
@Test func requestTimesOut() async {
|
||||
await #expect(throws: WebApiError.timedOut) {
|
||||
try await WebApi.get(
|
||||
url("https://api.phpmon.test/up"),
|
||||
withTimeout: .seconds(0.01)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user