Files
Termind/TFI/Application/Transfers/MobileSFTPChannelModel.swift
T

242 lines
8.8 KiB
Swift

import Foundation
import Observation
import TermindApplication
import TermindDomain
import TermindPresentation
import struct TermindDomain.Host
@MainActor
@Observable
final class MobileSFTPChannelModel {
private(set) var path = "/"
var pathText = "/"
private(set) var files: [RemoteFile] = []
var query = ""
private(set) var backHistory: [String] = []
private(set) var forwardHistory: [String] = []
var operationError: String?
private(set) var isRefreshing = false
var sortField: RemoteFileSortField = .name
var sortDirection: RemoteFileSortDirection = .ascending
var showsHiddenFiles = false
var canGoBack: Bool { !backHistory.isEmpty }
var canGoForward: Bool { !forwardHistory.isEmpty }
var visibleFiles: [RemoteFile] {
let displayable = showsHiddenFiles ? files : files.filter { !$0.isHidden }
let matches =
query.isEmpty
? displayable
: displayable.filter { $0.name.localizedCaseInsensitiveContains(query) }
let descriptor = RemoteFileSortDescriptor(field: sortField, direction: sortDirection)
return matches.sorted { descriptor.precedes($0, $1) }
}
func sort(by field: RemoteFileSortField) {
if sortField == field {
sortDirection.toggle()
} else {
sortField = field
sortDirection = field.defaultDirection
}
}
@ObservationIgnored private var fileSystem: (any RemoteFileSystem)?
@ObservationIgnored private let hostID: Host.ID?
@ObservationIgnored private let history: SFTPHistoryModel?
@ObservationIgnored private var connectionGeneration: UUID?
@ObservationIgnored private var directoryRequest = UUID()
@ObservationIgnored private var handlesConnectionError: ((any Error) -> Bool)?
@ObservationIgnored private var didChangePath: (() -> Void)?
init(hostID: Host.ID? = nil, history: SFTPHistoryModel? = nil) {
self.hostID = hostID
self.history = history
}
func attach(
fileSystem: any RemoteFileSystem,
path: String,
connectionGeneration: UUID,
handlesConnectionError: @escaping (any Error) -> Bool,
didChangePath: @escaping () -> Void
) async throws {
self.fileSystem = fileSystem
self.connectionGeneration = connectionGeneration
self.handlesConnectionError = handlesConnectionError
self.didChangePath = didChangePath
operationError = nil
let request = UUID()
directoryRequest = request
let path = RemotePath.normalized(path)
let entries = try await fileSystem.directoryContents(at: path)
guard self.connectionGeneration == connectionGeneration, directoryRequest == request else { return }
self.path = path
pathText = path
files = entries
recordDirectory(path)
}
func detach() {
fileSystem = nil
connectionGeneration = nil
directoryRequest = UUID()
handlesConnectionError = nil
didChangePath = nil
files = []
operationError = nil
}
func openDirectory(_ directory: String) async {
guard let fileSystem, let connectionGeneration else { return }
let destination = RemotePath.normalized(directory)
guard destination != path else {
await reload()
return
}
let source = path
let request = UUID()
directoryRequest = request
do {
let entries = try await fileSystem.directoryContents(at: destination)
guard self.connectionGeneration == connectionGeneration, directoryRequest == request else { return }
backHistory.append(source)
forwardHistory.removeAll()
path = destination
pathText = destination
files = entries
recordDirectory(destination)
didChangePath?()
} catch {
guard self.connectionGeneration == connectionGeneration, directoryRequest == request else { return }
record(error)
}
}
func goBack() async {
guard let fileSystem, let connectionGeneration, let destination = backHistory.last else { return }
let source = path
let request = UUID()
directoryRequest = request
do {
let entries = try await fileSystem.directoryContents(at: destination)
guard self.connectionGeneration == connectionGeneration, directoryRequest == request,
path == source, backHistory.last == destination
else { return }
backHistory.removeLast()
forwardHistory.append(source)
path = destination
pathText = destination
files = entries
recordDirectory(destination)
didChangePath?()
} catch {
guard self.connectionGeneration == connectionGeneration, directoryRequest == request else { return }
record(error)
}
}
func goForward() async {
guard let fileSystem, let connectionGeneration, let destination = forwardHistory.last else { return }
let source = path
let request = UUID()
directoryRequest = request
do {
let entries = try await fileSystem.directoryContents(at: destination)
guard self.connectionGeneration == connectionGeneration, directoryRequest == request,
path == source, forwardHistory.last == destination
else { return }
forwardHistory.removeLast()
backHistory.append(source)
path = destination
pathText = destination
files = entries
recordDirectory(destination)
didChangePath?()
} catch {
guard self.connectionGeneration == connectionGeneration, directoryRequest == request else { return }
record(error)
}
}
func reload() async {
guard let fileSystem, let connectionGeneration, !isRefreshing else { return }
isRefreshing = true
defer { isRefreshing = false }
let requestedPath = path
let request = UUID()
directoryRequest = request
do {
let entries = try await fileSystem.directoryContents(at: requestedPath)
guard self.connectionGeneration == connectionGeneration, directoryRequest == request,
path == requestedPath
else { return }
files = entries
recordDirectory(requestedPath)
} catch {
guard self.connectionGeneration == connectionGeneration, directoryRequest == request else { return }
record(error)
}
}
func rename(_ file: RemoteFile, to name: String) async throws {
guard let fileSystem, let connectionGeneration else { return }
do {
try await fileSystem.rename(file, to: name)
guard self.connectionGeneration == connectionGeneration else { return }
await reload()
} catch {
guard self.connectionGeneration == connectionGeneration else { return }
if handlesConnectionError?(error) != true { throw error }
}
}
func setPermissions(_ file: RemoteFile, mode: UInt32) async throws {
guard let fileSystem, let connectionGeneration else { return }
do {
try await fileSystem.setPermissions(of: file, to: mode)
guard self.connectionGeneration == connectionGeneration else { return }
await reload()
} catch {
guard self.connectionGeneration == connectionGeneration else { return }
if handlesConnectionError?(error) != true { throw error }
}
}
func createDirectory(_ name: String) async {
guard let fileSystem, let connectionGeneration else { return }
do {
try await fileSystem.createDirectory(named: name, in: path)
guard self.connectionGeneration == connectionGeneration else { return }
await reload()
} catch {
guard self.connectionGeneration == connectionGeneration else { return }
record(error)
}
}
func delete(_ file: RemoteFile) async {
guard let fileSystem, let connectionGeneration else { return }
do {
try await fileSystem.remove(file)
guard self.connectionGeneration == connectionGeneration else { return }
await reload()
} catch {
guard self.connectionGeneration == connectionGeneration else { return }
record(error)
}
}
private func record(_ error: any Error) {
if handlesConnectionError?(error) != true {
operationError = error.localizedDescription
}
}
private func recordDirectory(_ path: String) {
guard let hostID else { return }
history?.record(kind: .directory, path: path, for: hostID)
}
}