Refactor project structure and API
This commit is contained in:
271
Model/API/ServerAPI.swift
Normal file
271
Model/API/ServerAPI.swift
Normal file
@@ -0,0 +1,271 @@
|
||||
//
|
||||
// ServerAPI.swift
|
||||
// iKeyMon
|
||||
//
|
||||
// Created by tracer on 06.04.25.
|
||||
//
|
||||
import Foundation
|
||||
|
||||
final class ServerAPI {
|
||||
private let hostname: String
|
||||
private let apiKey: String
|
||||
|
||||
init(hostname: String, apiKey: String) {
|
||||
self.hostname = hostname
|
||||
self.apiKey = apiKey
|
||||
}
|
||||
|
||||
init?(server: Server) {
|
||||
guard let apiKey = KeychainHelper.loadApiKey(for: server.hostname)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
|
||||
return nil
|
||||
}
|
||||
self.hostname = server.hostname
|
||||
self.apiKey = apiKey
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func ping() async -> Bool {
|
||||
guard let url = URL(string: "https://\(hostname)/api/v2/ping") else {
|
||||
print("❌ [ServerAPI] Invalid ping URL for hostname: \(hostname)")
|
||||
return false
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue(apiKey, forHTTPHeaderField: "X-API-KEY")
|
||||
request.timeoutInterval = 10
|
||||
|
||||
do {
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
// Add debug info for comparison with server info request
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
if httpResponse.statusCode != 200 {
|
||||
print("❌ [ServerAPI] Ping HTTP Status: \(httpResponse.statusCode) for \(hostname)")
|
||||
if let responseString = String(data: data, encoding: .utf8) {
|
||||
print("❌ [ServerAPI] Ping error response: \(responseString)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let result = try? JSONDecoder().decode([String: String].self, from: data), result["response"] == "pong" {
|
||||
return true
|
||||
} else {
|
||||
print("❌ [ServerAPI] Ping failed - invalid response for \(hostname)")
|
||||
if let responseString = String(data: data, encoding: .utf8) {
|
||||
print("❌ [ServerAPI] Ping response: \(responseString)")
|
||||
}
|
||||
return false
|
||||
}
|
||||
} catch {
|
||||
print("❌ [ServerAPI] Ping error for \(hostname): \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func fetchServerInfo() async throws -> ServerInfo {
|
||||
print("🔍 [ServerAPI] Starting fetchServerInfo for hostname: \(hostname)")
|
||||
|
||||
guard let url = URL(string: "https://\(hostname)/api/v2/server") else {
|
||||
print("❌ [ServerAPI] Invalid URL for hostname: \(hostname)")
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
|
||||
print("🌐 [ServerAPI] URL created: \(url.absoluteString)")
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue(apiKey, forHTTPHeaderField: "X-API-KEY")
|
||||
// Temporarily remove Content-Type to match ping request exactly
|
||||
// request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.timeoutInterval = 30
|
||||
|
||||
print("📤 [ServerAPI] Making request with timeout: \(request.timeoutInterval)s")
|
||||
print("📤 [ServerAPI] API Key (first 10 chars): \(String(apiKey.prefix(10)))...")
|
||||
print("📤 [ServerAPI] Request headers: \(request.allHTTPHeaderFields ?? [:])")
|
||||
|
||||
do {
|
||||
let startTime = Date()
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
let endTime = Date()
|
||||
let duration = endTime.timeIntervalSince(startTime)
|
||||
|
||||
print("📥 [ServerAPI] Request completed in \(String(format: "%.2f", duration))s")
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
print("📥 [ServerAPI] HTTP Status: \(httpResponse.statusCode)")
|
||||
|
||||
// Handle 401 specifically
|
||||
if httpResponse.statusCode == 401 {
|
||||
print("❌ [ServerAPI] 401 Unauthorized - API key issue!")
|
||||
if let responseString = String(data: data, encoding: .utf8) {
|
||||
print("❌ [ServerAPI] 401 Response body: \(responseString)")
|
||||
}
|
||||
throw URLError(.userAuthenticationRequired)
|
||||
}
|
||||
|
||||
// Handle other non-200 status codes
|
||||
if httpResponse.statusCode != 200 {
|
||||
print("❌ [ServerAPI] Non-200 status code: \(httpResponse.statusCode)")
|
||||
if let responseString = String(data: data, encoding: .utf8) {
|
||||
print("❌ [ServerAPI] Error response body: \(responseString)")
|
||||
}
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
}
|
||||
|
||||
print("📥 [ServerAPI] Data size: \(data.count) bytes")
|
||||
|
||||
print("🔄 [ServerAPI] Decoding response payload...")
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
let decodedResponse = try decoder.decode(ServerResponseEnvelope.self, from: data)
|
||||
let decoded = decodedResponse.toDomain()
|
||||
print("✅ [ServerAPI] JSON decoding successful")
|
||||
|
||||
return decoded
|
||||
} catch let urlError as URLError {
|
||||
print("❌ [ServerAPI] URLError: \(urlError.localizedDescription)")
|
||||
print("❌ [ServerAPI] URLError code: \(urlError.code.rawValue)")
|
||||
throw urlError
|
||||
} catch let decodingError as DecodingError {
|
||||
print("❌ [ServerAPI] Decoding error: \(decodingError)")
|
||||
throw decodingError
|
||||
} catch {
|
||||
print("❌ [ServerAPI] Unexpected error: \(error)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Response Helpers
|
||||
|
||||
private extension ServerAPI {
|
||||
struct ServerResponseEnvelope: Decodable {
|
||||
let data: ServerData
|
||||
let meta: Meta
|
||||
|
||||
struct Meta: Decodable {
|
||||
let apiVersion: String
|
||||
}
|
||||
}
|
||||
|
||||
struct ServerData: Decodable {
|
||||
struct Port: Decodable {
|
||||
let service: String
|
||||
let status: String
|
||||
let port: Int
|
||||
let proto: String
|
||||
}
|
||||
|
||||
struct Load: Decodable {
|
||||
let minute1: Double
|
||||
let minute5: Double
|
||||
let minute15: Double
|
||||
let percent: Double
|
||||
let cpuCount: Int
|
||||
let level: String
|
||||
}
|
||||
|
||||
struct Memory: Decodable {
|
||||
let free: Int
|
||||
let used: Int
|
||||
let total: Int
|
||||
let percent: Double
|
||||
}
|
||||
|
||||
struct Disk: Decodable {
|
||||
let free: Int
|
||||
let used: Int
|
||||
let total: Int
|
||||
let percent: Double
|
||||
}
|
||||
|
||||
struct PHPInterpreter: Decodable {
|
||||
let version: String
|
||||
let path: String?
|
||||
let configFile: String?
|
||||
let extensions: [String]
|
||||
let memoryLimit: String?
|
||||
let maxExecutionTime: String?
|
||||
}
|
||||
|
||||
let hostname: String
|
||||
let ipAddresses: [String]
|
||||
let cpuCores: Int
|
||||
let serverTime: String
|
||||
let uptime: String
|
||||
let processCount: Int
|
||||
let apacheVersion: String
|
||||
let phpVersion: String
|
||||
let mysqlVersion: String?
|
||||
let mariadbVersion: String?
|
||||
let ports: [Port]?
|
||||
let load: Load
|
||||
let memory: Memory
|
||||
let swap: Memory
|
||||
let diskSpace: Disk
|
||||
let panelVersion: String
|
||||
let panelBuild: String
|
||||
let additionalPhpInterpreters: [PHPInterpreter]?
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension ServerAPI.ServerResponseEnvelope {
|
||||
func toDomain() -> ServerInfo {
|
||||
ServerInfo(
|
||||
hostname: data.hostname,
|
||||
ipAddresses: data.ipAddresses,
|
||||
cpuCores: data.cpuCores,
|
||||
serverTime: data.serverTime,
|
||||
uptime: data.uptime,
|
||||
processCount: data.processCount,
|
||||
apacheVersion: data.apacheVersion,
|
||||
phpVersion: data.phpVersion,
|
||||
mysqlVersion: data.mysqlVersion,
|
||||
mariadbVersion: data.mariadbVersion,
|
||||
ports: data.ports?.map { ServerInfo.ServicePort(service: $0.service, status: $0.status, port: $0.port, proto: $0.proto) },
|
||||
load: ServerInfo.Load(
|
||||
minute1: data.load.minute1,
|
||||
minute5: data.load.minute5,
|
||||
minute15: data.load.minute15,
|
||||
percent: data.load.percent,
|
||||
cpuCount: data.load.cpuCount,
|
||||
level: data.load.level
|
||||
),
|
||||
memory: ServerInfo.Memory(
|
||||
free: data.memory.free,
|
||||
used: data.memory.used,
|
||||
total: data.memory.total,
|
||||
percent: data.memory.percent
|
||||
),
|
||||
swap: ServerInfo.Memory(
|
||||
free: data.swap.free,
|
||||
used: data.swap.used,
|
||||
total: data.swap.total,
|
||||
percent: data.swap.percent
|
||||
),
|
||||
diskSpace: ServerInfo.DiskSpace(
|
||||
free: data.diskSpace.free,
|
||||
used: data.diskSpace.used,
|
||||
total: data.diskSpace.total,
|
||||
percent: data.diskSpace.percent
|
||||
),
|
||||
panelVersion: data.panelVersion,
|
||||
panelBuild: data.panelBuild,
|
||||
apiVersion: meta.apiVersion,
|
||||
additionalPHPInterpreters: data.additionalPhpInterpreters?.map {
|
||||
ServerInfo.PHPInterpreter(
|
||||
version: $0.version,
|
||||
path: $0.path,
|
||||
configFile: $0.configFile,
|
||||
extensions: $0.extensions,
|
||||
memoryLimit: $0.memoryLimit,
|
||||
maxExecutionTime: $0.maxExecutionTime
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user