1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-08-08 04:20:07 +02:00
Files
app/phpmon/Domain/Integrations/Valet/Valet.swift

242 lines
8.4 KiB
Swift

//
// Valet.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved.
//
import Foundation
class Valet {
static let shared = Valet()
/// The version of Valet that was detected.
var version: String
/// The Valet configuration file.
var config: Valet.Configuration
/// A cached list of sites that were detected after analyzing the paths set up for Valet.
var sites: [Site] = []
init() {
version = VersionExtractor.from(valet("--version"))
?? "UNKNOWN"
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/valet/config.json")
config = try! JSONDecoder().decode(
Valet.Configuration.self,
from: try! String(contentsOf: file, encoding: .utf8).data(using: .utf8)!
)
self.sites = []
}
public func startPreloadingSites() {
let maximumPreload = 10
let foundSites = self.countPaths()
if foundSites <= maximumPreload {
// Preload the sites and their drivers
Log.info("Fewer than or \(maximumPreload) sites found, preloading list of sites...")
self.reloadSites()
} else {
Log.info("\(foundSites) sites found, exceeds \(maximumPreload) for preload at launch!")
}
}
public func reloadSites() {
resolvePaths(tld: config.tld)
}
public func validateVersion() -> Void {
if version == "UNKNOWN" {
return Log.warn("The Valet version could not be extracted... that does not bode well.")
}
if version.versionCompare(Constants.MinimumRecommendedValetVersion) == .orderedAscending {
let version = version
Log.warn("Valet version \(version) is too old! (recommended: \(Constants.MinimumRecommendedValetVersion))")
DispatchQueue.main.async {
Alert.notify(message: "alert.min_valet_version.title".localized, info: "alert.min_valet_version.info".localized(version, Constants.MinimumRecommendedValetVersion))
}
} else {
Log.info("Valet version \(version) is recent enough, OK (recommended: \(Constants.MinimumRecommendedValetVersion))")
}
}
/**
Returns a count of how many sites are linked and parked.
*/
private func countPaths() -> Int {
var count = 0
for path in config.paths {
let entries = try! FileManager.default.contentsOfDirectory(atPath: path)
for entry in entries {
if resolveSite(entry, forPath: path) {
count += 1
}
}
}
return count
}
/**
Resolves all paths and creates linked or parked site instances that can be referenced later.
*/
private func resolvePaths(tld: String) {
sites = []
for path in config.paths {
let entries = try! FileManager.default.contentsOfDirectory(atPath: path)
for entry in entries {
resolvePath(entry, forPath: path, tld: tld)
}
}
}
/**
Determines whether the site can be resolved as a symbolic link or as a directory.
Regular files are ignored. Returns true if the path can be parsed.
*/
private func resolveSite(_ entry: String, forPath path: String) -> Bool {
let siteDir = path + "/" + entry
let attrs = try! FileManager.default.attributesOfItem(atPath: siteDir)
let type = attrs[FileAttributeKey.type] as! FileAttributeType
if type == FileAttributeType.typeSymbolicLink || type == FileAttributeType.typeDirectory {
return true
}
return false
}
/**
Determines whether the site can be resolved as a symbolic link or as a directory.
Regular files are ignored, and the site is added to Valet's list of sites.
*/
private func resolvePath(_ entry: String, forPath path: String, tld: String) {
let siteDir = path + "/" + entry
// See if the file is a symlink, if so, resolve it
let attrs = try! FileManager.default.attributesOfItem(atPath: siteDir)
// We can also determine whether the thing at the path is a directory, too
let type = attrs[FileAttributeKey.type] as! FileAttributeType
// We should also check that we can interpret the path correctly
if URL(fileURLWithPath: siteDir).lastPathComponent == "" {
print("Warning: could not parse the site: \(siteDir), skipping!")
return
}
if type == FileAttributeType.typeSymbolicLink {
sites.append(Site(aliasPath: siteDir, tld: tld))
} else if type == FileAttributeType.typeDirectory {
sites.append(Site(absolutePath: siteDir, tld: tld))
}
}
// MARK: - Structs
class Site {
/// Name of the site. Does not include the TLD.
var name: String!
/// The absolute path to the directory that is served.
var absolutePath: String!
/// Location of the alias. If set, this is a linked domain.
var aliasPath: String?
/// Whether the site has been secured.
var secured: Bool!
/// What driver is currently in use. If not detected, defaults to nil.
var driver: String? = nil
/// The PHP version as discovered in composer.json
var composerPhp: String = "???"
/// How the PHP version was determined
var composerPhpSource: String = "unknown"
init() {}
convenience init(absolutePath: String, tld: String) {
self.init()
self.absolutePath = absolutePath
self.name = URL(fileURLWithPath: absolutePath).lastPathComponent
self.aliasPath = nil
determineSecured(tld)
determineDriver()
determineComposerPhpVersion()
}
convenience init(aliasPath: String, tld: String) {
self.init()
self.absolutePath = try! FileManager.default.destinationOfSymbolicLink(atPath: aliasPath)
self.name = URL(fileURLWithPath: aliasPath).lastPathComponent
self.aliasPath = aliasPath
determineSecured(tld)
determineDriver()
determineComposerPhpVersion()
}
public func determineSecured(_ tld: String) {
secured = Shell.fileExists("~/.config/valet/Certificates/\(self.name!).\(tld).key")
}
public func determineDriver() {
let driver = Shell.pipe("cd '\(absolutePath!)' && valet which", requiresPath: true)
if driver.contains("This site is served by") {
self.driver = driver
// TODO: Use a regular expression to retrieve the driver instead?
.replacingOccurrences(of: "This site is served by [", with: "")
.replacingOccurrences(of: "ValetDriver].\n", with: "")
} else {
self.driver = nil
}
}
public func determineComposerPhpVersion() {
let path = "\(absolutePath!)/composer.json"
do {
if Filesystem.fileExists(path) {
(self.composerPhp, self.composerPhpSource) = try JSONDecoder().decode(
ComposerJson.self,
from: String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8).data(using: .utf8)!
).getPhpVersion()
}
} catch {
Log.err("Something went wrong reading the composer JSON file.")
}
}
}
struct Configuration: Decodable {
/// Top level domain suffix. Usually "test" but can be set to something else.
/// - Important: Does not include the actual dot. ("test", not ".test"!)
let tld: String
/// The paths that need to be checked.
let paths: [String]
/// The loopback address.
let loopback: String
/// The default site that is served if the domain is not found. Optional.
let defaultSite: String?
private enum CodingKeys: String, CodingKey {
case tld, paths, loopback, defaultSite = "default"
}
}
}