|
| 1 | +import Foundation |
| 2 | +import TableProModels |
| 3 | + |
| 4 | +public final class ConnectionManager: @unchecked Sendable { |
| 5 | + private let driverFactory: DriverFactory |
| 6 | + private let secureStore: SecureStore |
| 7 | + private let sshProvider: SSHProvider? |
| 8 | + |
| 9 | + private let lock = NSLock() |
| 10 | + private var sessions: [UUID: ConnectionSession] = [:] |
| 11 | + |
| 12 | + public init( |
| 13 | + driverFactory: DriverFactory, |
| 14 | + secureStore: SecureStore, |
| 15 | + sshProvider: SSHProvider? = nil |
| 16 | + ) { |
| 17 | + self.driverFactory = driverFactory |
| 18 | + self.secureStore = secureStore |
| 19 | + self.sshProvider = sshProvider |
| 20 | + } |
| 21 | + |
| 22 | + public func connect(_ connection: DatabaseConnection) async throws -> ConnectionSession { |
| 23 | + let password = try secureStore.retrieve(forKey: Self.passwordKey(for: connection.id)) |
| 24 | + |
| 25 | + var effectiveHost = connection.host |
| 26 | + var effectivePort = connection.port |
| 27 | + if connection.sshEnabled, let ssh = connection.sshConfiguration { |
| 28 | + guard let provider = sshProvider else { |
| 29 | + throw ConnectionError.sshNotSupported |
| 30 | + } |
| 31 | + let tunnel = try await provider.createTunnel( |
| 32 | + config: ssh, |
| 33 | + remoteHost: connection.host, |
| 34 | + remotePort: connection.port |
| 35 | + ) |
| 36 | + effectiveHost = tunnel.localHost |
| 37 | + effectivePort = tunnel.localPort |
| 38 | + } |
| 39 | + |
| 40 | + do { |
| 41 | + var effectiveConnection = connection |
| 42 | + effectiveConnection.host = effectiveHost |
| 43 | + effectiveConnection.port = effectivePort |
| 44 | + |
| 45 | + let driver = try driverFactory.createDriver(for: effectiveConnection, password: password) |
| 46 | + try await driver.connect() |
| 47 | + |
| 48 | + let session = ConnectionSession( |
| 49 | + connectionId: connection.id, |
| 50 | + driver: driver, |
| 51 | + activeDatabase: connection.database, |
| 52 | + status: .connected |
| 53 | + ) |
| 54 | + storeSession(session, for: connection.id) |
| 55 | + return session |
| 56 | + } catch { |
| 57 | + if connection.sshEnabled, let provider = sshProvider { |
| 58 | + try? await provider.closeTunnel(for: connection.id) |
| 59 | + } |
| 60 | + throw error |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + public func storePassword(_ password: String, for connectionId: UUID) throws { |
| 65 | + try secureStore.store(password, forKey: Self.passwordKey(for: connectionId)) |
| 66 | + } |
| 67 | + |
| 68 | + public func deletePassword(for connectionId: UUID) throws { |
| 69 | + try secureStore.delete(forKey: Self.passwordKey(for: connectionId)) |
| 70 | + } |
| 71 | + |
| 72 | + private static func passwordKey(for connectionId: UUID) -> String { |
| 73 | + "com.TablePro.password.\(connectionId.uuidString)" |
| 74 | + } |
| 75 | + |
| 76 | + public func disconnect(_ connectionId: UUID) async { |
| 77 | + let session = removeSession(for: connectionId) |
| 78 | + guard let session else { return } |
| 79 | + try? await session.driver.disconnect() |
| 80 | + if let sshProvider { |
| 81 | + try? await sshProvider.closeTunnel(for: connectionId) |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + public func disconnectAll() async { |
| 86 | + let ids = allSessionIds() |
| 87 | + for id in ids { |
| 88 | + await disconnect(id) |
| 89 | + } |
| 90 | + } |
| 91 | + |
| 92 | + private func allSessionIds() -> [UUID] { |
| 93 | + lock.lock() |
| 94 | + defer { lock.unlock() } |
| 95 | + return Array(sessions.keys) |
| 96 | + } |
| 97 | + |
| 98 | + public func updateSession(_ connectionId: UUID, _ mutation: (inout ConnectionSession) -> Void) { |
| 99 | + lock.lock() |
| 100 | + defer { lock.unlock() } |
| 101 | + guard var session = sessions[connectionId] else { return } |
| 102 | + mutation(&session) |
| 103 | + sessions[connectionId] = session |
| 104 | + } |
| 105 | + |
| 106 | + public func switchDatabase(_ connectionId: UUID, to database: String) async throws { |
| 107 | + guard let session = session(for: connectionId) else { |
| 108 | + throw ConnectionError.notConnected |
| 109 | + } |
| 110 | + try await session.driver.switchDatabase(to: database) |
| 111 | + updateSession(connectionId) { $0.activeDatabase = database } |
| 112 | + } |
| 113 | + |
| 114 | + public func session(for connectionId: UUID) -> ConnectionSession? { |
| 115 | + lock.lock() |
| 116 | + defer { lock.unlock() } |
| 117 | + return sessions[connectionId] |
| 118 | + } |
| 119 | + |
| 120 | + private func storeSession(_ session: ConnectionSession, for id: UUID) { |
| 121 | + lock.lock() |
| 122 | + sessions[id] = session |
| 123 | + lock.unlock() |
| 124 | + } |
| 125 | + |
| 126 | + private func removeSession(for id: UUID) -> ConnectionSession? { |
| 127 | + lock.lock() |
| 128 | + let session = sessions.removeValue(forKey: id) |
| 129 | + lock.unlock() |
| 130 | + return session |
| 131 | + } |
| 132 | +} |
0 commit comments