Files
Termind/Frameworks/TermindRemoteInfrastructure/SSH/AccessControlledSFTPChannel.swift
T
Kaede Akino d2cce7d2ce feat: make core features free and add host-based Pro limits
Make AI, MCP, Metrics, and Wake-on-LAN free while retaining iCloud sync as a Pro feature. Limit free accounts to three saved hosts and preserve excess hosts with remote-operation locks.

Gate host creation, import, and runtime access; preserve stable host admission across persistence and sync. Add upgrade states on macOS and iOS, open the paywall from locked Wake actions, and use wrench icons for MCP.

Include regression coverage, opt-in UI review fixtures, localized copy, and the updated pricing website revision.
2026-10-17 06:13:38 +08:00

164 lines
6.5 KiB
Swift

import AgentKit
import Foundation
import TermindApplication
import TermindDomain
/// Admit new operations without cancelling a transfer already delegated to the channel.
nonisolated final class AccessControlledSFTPChannel: SFTPChannelProtocol, Sendable {
private let channel: any SFTPChannelProtocol
private let accessCheck: HostOperationAccessCheck?
init(channel: any SFTPChannelProtocol, accessCheck: HostOperationAccessCheck?) {
self.channel = channel
self.accessCheck = accessCheck
}
func disconnect() async { await channel.disconnect() }
public func openSiblingChannel() async throws -> any SFTPChannelProtocol {
AccessControlledSFTPChannel(
channel: try await connectedChannel().openSiblingChannel(), accessCheck: accessCheck)
}
public func homeDirectory() async throws -> String {
try await connectedChannel().homeDirectory()
}
public func listDirectory(at path: String) async throws -> [RemoteFile] {
try await connectedChannel().listDirectory(at: path)
}
public func attributes(at path: String) async throws -> RemoteFile? {
try await connectedChannel().attributes(at: path)
}
public func readFile(at path: String, maximumBytes: Int) async throws -> Data {
try await connectedChannel().readFile(at: path, maximumBytes: maximumBytes)
}
public func readTextPage(at path: String, offset: Int, limit: Int) async throws -> AgentTextPage {
try await connectedChannel().readTextPage(at: path, offset: offset, limit: limit)
}
public func writeFile(_ data: Data, at path: String, overwrite: Bool) async throws {
try await connectedChannel().writeFile(data, at: path, overwrite: overwrite)
}
public func setPermissions(at path: String, to permissions: UInt32) async throws {
try await connectedChannel().setPermissions(at: path, to: permissions)
}
public func createDirectory(at path: String) async throws {
try await connectedChannel().createDirectory(at: path)
}
public func rename(from source: String, to destination: String, overwrite: Bool) async throws {
try await connectedChannel().rename(
from: source, to: destination, overwrite: overwrite)
}
public func removeFile(at path: String) async throws {
try await connectedChannel().removeFile(at: path)
}
public func removeDirectory(at path: String) async throws {
try await connectedChannel().removeDirectory(at: path)
}
public func uploadFile(
from localURL: URL, to remotePath: String, overwrite: Bool,
cancellation: TransferCancellationToken, resume: SFTPResumeState,
progress: @escaping @Sendable (SFTPProgress) -> Void
) async throws {
try await connectedChannel().uploadFile(
from: localURL, to: remotePath, overwrite: overwrite,
cancellation: cancellation, resume: resume, progress: progress)
}
public func uploadFile(
from localURL: URL, to remotePath: String, overwrite: Bool,
cancellation: TransferCancellationToken,
progress: @escaping @Sendable (SFTPProgress) -> Void
) async throws {
try await connectedChannel().uploadFile(
from: localURL, to: remotePath, overwrite: overwrite,
cancellation: cancellation, progress: progress)
}
public func uploadStream(
fromSocket socket: Int32, to remotePath: String, byteCount: Int64,
overwrite: Bool, cancellation: TransferCancellationToken,
resume: SFTPResumeState,
progress: @escaping @Sendable (SFTPProgress) -> Void,
sourceSucceeded: @escaping @Sendable () -> Bool
) async throws {
try await connectedChannel().uploadStream(
fromSocket: socket, to: remotePath, byteCount: byteCount,
overwrite: overwrite, cancellation: cancellation, resume: resume,
progress: progress, sourceSucceeded: sourceSucceeded)
}
public func uploadStream(
fromSocket socket: Int32, to remotePath: String, byteCount: Int64,
overwrite: Bool, cancellation: TransferCancellationToken,
progress: @escaping @Sendable (SFTPProgress) -> Void,
sourceSucceeded: @escaping @Sendable () -> Bool
) async throws {
try await connectedChannel().uploadStream(
fromSocket: socket, to: remotePath, byteCount: byteCount,
overwrite: overwrite, cancellation: cancellation,
progress: progress, sourceSucceeded: sourceSucceeded)
}
public func downloadFile(
from remotePath: String, to localURL: URL, overwrite: Bool,
cancellation: TransferCancellationToken, resume: SFTPResumeState,
progress: @escaping @Sendable (SFTPProgress) -> Void
) async throws {
try await connectedChannel().downloadFile(
from: remotePath, to: localURL, overwrite: overwrite,
cancellation: cancellation, resume: resume, progress: progress)
}
public func downloadFile(
from remotePath: String, to localURL: URL, overwrite: Bool,
cancellation: TransferCancellationToken,
progress: @escaping @Sendable (SFTPProgress) -> Void
) async throws {
try await connectedChannel().downloadFile(
from: remotePath, to: localURL, overwrite: overwrite,
cancellation: cancellation, progress: progress)
}
public func openDownloadStream(
at path: String, maximumBytes: Int64,
cancellation: TransferCancellationToken
) async throws -> any SFTPDownloadStream {
try await connectedChannel().openDownloadStream(
at: path, maximumBytes: maximumBytes, cancellation: cancellation)
}
public func copyFile(
from sourcePath: String, to destinationPath: String,
on destination: any SFTPChannelProtocol, byteCount: Int64,
overwrite: Bool, cancellation: TransferCancellationToken,
progress: @escaping @Sendable (SFTPProgress) -> Void
) async throws {
let destinationChannel =
if let destination = destination as? AccessControlledSFTPChannel {
try destination.connectedChannel()
} else {
destination
}
try await connectedChannel().copyFile(
from: sourcePath, to: destinationPath, on: destinationChannel,
byteCount: byteCount, overwrite: overwrite,
cancellation: cancellation, progress: progress)
}
private func connectedChannel() throws -> any SFTPChannelProtocol {
try accessCheck?()
return channel
}
}