From e1191bae93c2fe614eedaa3b0c0133fb1c104112 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 20 Mar 2025 18:29:08 +1100 Subject: [PATCH 1/3] chore: add mutagen session state conversions --- .../Views/FileSync/FileSyncConfig.swift | 10 +- .../VPNLib/FileSync/FileSyncSession.swift | 284 +++++++++++++++++- .../VPNLib/FileSync/MutagenConvert.swift | 59 ++++ .../{Convert.swift => VPNConvert.swift} | 0 4 files changed, 332 insertions(+), 21 deletions(-) create mode 100644 Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift rename Coder-Desktop/VPNLib/{Convert.swift => VPNConvert.swift} (100%) diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift index eb3065b8..dc400b5d 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift @@ -20,14 +20,12 @@ struct FileSyncConfig: View { }.width(min: 200, ideal: 240) TableColumn("Workspace", value: \.agentHost) .width(min: 100, ideal: 120) - TableColumn("Remote Path", value: \.betaPath) + TableColumn("Remote Path") { Text($0.betaPath).help($0.betaPath) } .width(min: 100, ideal: 120) - TableColumn("Status") { $0.status.body } + TableColumn("Status") { $0.status.column.help($0.statusAndErrors) } .width(min: 80, ideal: 100) - TableColumn("Size") { item in - Text(item.size) - } - .width(min: 60, ideal: 80) + TableColumn("Size") { Text($0.maxSize.humanSizeBytes).help($0.sizeDescription) } + .width(min: 60, ideal: 80) } .contextMenu(forSelectionType: FileSyncSession.ID.self, menu: { _ in }, primaryAction: { selectedSessions in diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift index e251b1a5..af49d18d 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift @@ -3,19 +3,141 @@ import SwiftUI public struct FileSyncSession: Identifiable { public let id: String public let alphaPath: String + public let name: String + public let agentHost: String public let betaPath: String public let status: FileSyncStatus - public let size: String + + public let maxSize: FileSyncSessionEndpointSize + public let localSize: FileSyncSessionEndpointSize + public let remoteSize: FileSyncSessionEndpointSize + + public let errors: [FileSyncError] + + init(state: Synchronization_State) { + id = state.session.identifier + name = state.session.name + + // If the protocol isn't what we expect for alpha or beta, show unknown + alphaPath = if state.session.alpha.protocol == Url_Protocol.local, !state.session.alpha.path.isEmpty { + state.session.alpha.path + } else { + "Unknown" + } + if state.session.beta.protocol == Url_Protocol.ssh, !state.session.beta.host.isEmpty { + let host = state.session.beta.host + // TOOD: We need to either: + // - make this compatible with custom suffixes + // - always strip the tld + // - always keep the tld + agentHost = host.hasSuffix(".coder") ? String(host.dropLast(6)) : host + } else { + agentHost = "Unknown" + } + betaPath = if !state.session.beta.path.isEmpty { + state.session.beta.path + } else { + "Unknown" + } + + var status: FileSyncStatus = if state.session.paused { + .paused + } else { + convertSessionStatus(status: state.status) + } + if case .error = status {} else { + if state.conflicts.count > 0 { + status = .conflicts + } + } + self.status = status + + localSize = .init( + sizeBytes: state.alphaState.totalFileSize, + fileCount: state.alphaState.files, + dirCount: state.alphaState.directories, + symLinkCount: state.alphaState.symbolicLinks + ) + remoteSize = .init( + sizeBytes: state.betaState.totalFileSize, + fileCount: state.betaState.files, + dirCount: state.betaState.directories, + symLinkCount: state.betaState.symbolicLinks + ) + maxSize = localSize.maxOf(other: remoteSize) + + errors = accumulateErrors(from: state) + } + + public var statusAndErrors: String { + var out = "\(status.type)\n\n\(status.description)" + errors.forEach { out += "\n\t\($0)" } + return out + } + + public var sizeDescription: String { + var out = "" + if localSize != remoteSize { + out += "Maximum:\n\(maxSize.description(linePrefix: " "))\n\n" + } + out += "Local:\n\(localSize.description(linePrefix: " "))\n\n" + out += "Remote:\n\(remoteSize.description(linePrefix: " "))" + return out + } +} + +public struct FileSyncSessionEndpointSize: Equatable { + public let sizeBytes: UInt64 + public let fileCount: UInt64 + public let dirCount: UInt64 + public let symLinkCount: UInt64 + + public init(sizeBytes: UInt64, fileCount: UInt64, dirCount: UInt64, symLinkCount: UInt64) { + self.sizeBytes = sizeBytes + self.fileCount = fileCount + self.dirCount = dirCount + self.symLinkCount = symLinkCount + } + + func maxOf(other: FileSyncSessionEndpointSize) -> FileSyncSessionEndpointSize { + FileSyncSessionEndpointSize( + sizeBytes: max(sizeBytes, other.sizeBytes), + fileCount: max(fileCount, other.fileCount), + dirCount: max(dirCount, other.dirCount), + symLinkCount: max(symLinkCount, other.symLinkCount) + ) + } + + public var humanSizeBytes: String { + humanReadableBytes(sizeBytes) + } + + public func description(linePrefix: String = "") -> String { + var result = "" + result += linePrefix + humanReadableBytes(sizeBytes) + "\n" + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + if let formattedFileCount = numberFormatter.string(from: NSNumber(value: fileCount)) { + result += "\(linePrefix)\(formattedFileCount) file\(fileCount == 1 ? "" : "s")\n" + } + if let formattedDirCount = numberFormatter.string(from: NSNumber(value: dirCount)) { + result += "\(linePrefix)\(formattedDirCount) director\(dirCount == 1 ? "y" : "ies")" + } + if symLinkCount > 0, let formattedSymLinkCount = numberFormatter.string(from: NSNumber(value: symLinkCount)) { + result += "\n\(linePrefix)\(formattedSymLinkCount) symlink\(symLinkCount == 1 ? "" : "s")" + } + return result + } } public enum FileSyncStatus { case unknown - case error(String) + case error(FileSyncErrorStatus) case ok case paused - case needsAttention(String) - case working(String) + case conflicts + case working(FileSyncWorkingStatus) public var color: Color { switch self { @@ -27,32 +149,164 @@ public enum FileSyncStatus { .red case .error: .red - case .needsAttention: + case .conflicts: .orange case .working: - .white + .purple } } - public var description: String { + public var type: String { switch self { case .unknown: "Unknown" - case let .error(msg): - msg + case let .error(status): + status.name case .ok: "Watching" case .paused: "Paused" - case let .needsAttention(msg): - msg - case let .working(msg): - msg + case .conflicts: + "Conflicts" + case let .working(status): + status.name + } + } + + public var description: String { + switch self { + case .unknown: + "Unknown status message." + case let .error(status): + status.description + case .ok: + "The session is watching for filesystem changes." + case .paused: + "The session is paused." + case .conflicts: + "The session has conflicts that need to be resolved." + case let .working(status): + status.description } } - public var body: some View { - Text(description).foregroundColor(color) + public var column: some View { + Text(type).foregroundColor(color) + } +} + +public enum FileSyncWorkingStatus { + case connectingAlpha + case connectingBeta + case scanning + case reconciling + case stagingAlpha + case stagingBeta + case transitioning + case saving + + var name: String { + switch self { + case .connectingAlpha: + "Connecting (alpha)" + case .connectingBeta: + "Connecting (beta)" + case .scanning: + "Scanning" + case .reconciling: + "Reconciling" + case .stagingAlpha: + "Staging (alpha)" + case .stagingBeta: + "Staging (beta)" + case .transitioning: + "Transitioning" + case .saving: + "Saving" + } + } + + var description: String { + switch self { + case .connectingAlpha: + "The session is attempting to connect to the alpha endpoint." + case .connectingBeta: + "The session is attempting to connect to the beta endpoint." + case .scanning: + "The session is scanning the filesystem on each endpoint." + case .reconciling: + "The session is performing reconciliation." + case .stagingAlpha: + "The session is staging files on the alpha endpoint" + case .stagingBeta: + "The session is staging files on the beta endpoint" + case .transitioning: + "The session is performing transition operations on each endpoint." + case .saving: + "The session is recording synchronization history to disk." + } + } +} + +public enum FileSyncErrorStatus { + case disconnected + case haltedOnRootEmptied + case haltedOnRootDeletion + case haltedOnRootTypeChange + case waitingForRescan + + var name: String { + switch self { + case .disconnected: + "Disconnected" + case .haltedOnRootEmptied: + "Halted on root emptied" + case .haltedOnRootDeletion: + "Halted on root deletion" + case .haltedOnRootTypeChange: + "Halted on root type change" + case .waitingForRescan: + "Waiting for rescan" + } + } + + var description: String { + switch self { + case .disconnected: + "The session is unpaused but not currently connected or connecting to either endpoint." + case .haltedOnRootEmptied: + "The session is halted due to the root emptying safety check." + case .haltedOnRootDeletion: + "The session is halted due to the root deletion safety check." + case .haltedOnRootTypeChange: + "The session is halted due to the root type change safety check." + case .waitingForRescan: + "The session is waiting to retry scanning after an error during the previous scan." + } + } +} + +public enum FileSyncEndpoint { + case local + case remote +} + +public enum FileSyncProblemType { + case scan + case transition +} + +public enum FileSyncError { + case generic(String) + case problem(FileSyncEndpoint, FileSyncProblemType, path: String, error: String) + + var description: String { + switch self { + case let .generic(error): + error + case let .problem(endpoint, type, path, error): + "\(endpoint) \(type) error at \(path): \(error)" + } } } diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift b/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift new file mode 100644 index 00000000..7afefee1 --- /dev/null +++ b/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift @@ -0,0 +1,59 @@ +// swiftlint:disable:next cyclomatic_complexity +func convertSessionStatus(status: Synchronization_Status) -> FileSyncStatus { + switch status { + case .disconnected: + .error(.disconnected) + case .haltedOnRootEmptied: + .error(.haltedOnRootEmptied) + case .haltedOnRootDeletion: + .error(.haltedOnRootDeletion) + case .haltedOnRootTypeChange: + .error(.haltedOnRootTypeChange) + case .waitingForRescan: + .error(.waitingForRescan) + case .connectingAlpha: + .working(.connectingAlpha) + case .connectingBeta: + .working(.connectingBeta) + case .scanning: + .working(.scanning) + case .reconciling: + .working(.reconciling) + case .stagingAlpha: + .working(.stagingAlpha) + case .stagingBeta: + .working(.stagingBeta) + case .transitioning: + .working(.transitioning) + case .saving: + .working(.saving) + case .watching: + .ok + case .UNRECOGNIZED: + .unknown + } +} + +func accumulateErrors(from state: Synchronization_State) -> [FileSyncError] { + var errors: [FileSyncError] = [] + if !state.lastError.isEmpty { + errors.append(.generic(state.lastError)) + } + for problem in state.alphaState.scanProblems { + errors.append(.problem(.local, .scan, path: problem.path, error: problem.error)) + } + for problem in state.alphaState.transitionProblems { + errors.append(.problem(.local, .transition, path: problem.path, error: problem.error)) + } + for problem in state.betaState.scanProblems { + errors.append(.problem(.remote, .scan, path: problem.path, error: problem.error)) + } + for problem in state.betaState.transitionProblems { + errors.append(.problem(.remote, .transition, path: problem.path, error: problem.error)) + } + return errors +} + +func humanReadableBytes(_ bytes: UInt64) -> String { + ByteCountFormatter().string(fromByteCount: Int64(bytes)) +} diff --git a/Coder-Desktop/VPNLib/Convert.swift b/Coder-Desktop/VPNLib/VPNConvert.swift similarity index 100% rename from Coder-Desktop/VPNLib/Convert.swift rename to Coder-Desktop/VPNLib/VPNConvert.swift From d42088ed3f0c58432cb4e6c024c864cc99704a94 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Tue, 25 Mar 2025 12:34:28 +1100 Subject: [PATCH 2/3] dont strip workspace tld in table --- Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift index af49d18d..32b7aa5c 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift @@ -26,12 +26,11 @@ public struct FileSyncSession: Identifiable { "Unknown" } if state.session.beta.protocol == Url_Protocol.ssh, !state.session.beta.host.isEmpty { - let host = state.session.beta.host // TOOD: We need to either: // - make this compatible with custom suffixes // - always strip the tld // - always keep the tld - agentHost = host.hasSuffix(".coder") ? String(host.dropLast(6)) : host + agentHost = state.session.beta.host } else { agentHost = "Unknown" } From 7bfeb9bc3daa559b7542d8f1cb5eb30dd9ba499c Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Tue, 25 Mar 2025 12:43:32 +1100 Subject: [PATCH 3/3] remove max size --- .../Views/FileSync/FileSyncConfig.swift | 2 +- .../VPNLib/FileSync/FileSyncSession.swift | 20 +++---------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift index dc400b5d..dc83c17a 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift @@ -24,7 +24,7 @@ struct FileSyncConfig: View { .width(min: 100, ideal: 120) TableColumn("Status") { $0.status.column.help($0.statusAndErrors) } .width(min: 80, ideal: 100) - TableColumn("Size") { Text($0.maxSize.humanSizeBytes).help($0.sizeDescription) } + TableColumn("Size") { Text($0.localSize.humanSizeBytes).help($0.sizeDescription) } .width(min: 60, ideal: 80) } .contextMenu(forSelectionType: FileSyncSession.ID.self, menu: { _ in }, diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift index 32b7aa5c..d586908d 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift @@ -9,7 +9,6 @@ public struct FileSyncSession: Identifiable { public let betaPath: String public let status: FileSyncStatus - public let maxSize: FileSyncSessionEndpointSize public let localSize: FileSyncSessionEndpointSize public let remoteSize: FileSyncSessionEndpointSize @@ -25,14 +24,14 @@ public struct FileSyncSession: Identifiable { } else { "Unknown" } - if state.session.beta.protocol == Url_Protocol.ssh, !state.session.beta.host.isEmpty { + agentHost = if state.session.beta.protocol == Url_Protocol.ssh, !state.session.beta.host.isEmpty { // TOOD: We need to either: // - make this compatible with custom suffixes // - always strip the tld // - always keep the tld - agentHost = state.session.beta.host + state.session.beta.host } else { - agentHost = "Unknown" + "Unknown" } betaPath = if !state.session.beta.path.isEmpty { state.session.beta.path @@ -64,7 +63,6 @@ public struct FileSyncSession: Identifiable { dirCount: state.betaState.directories, symLinkCount: state.betaState.symbolicLinks ) - maxSize = localSize.maxOf(other: remoteSize) errors = accumulateErrors(from: state) } @@ -77,9 +75,6 @@ public struct FileSyncSession: Identifiable { public var sizeDescription: String { var out = "" - if localSize != remoteSize { - out += "Maximum:\n\(maxSize.description(linePrefix: " "))\n\n" - } out += "Local:\n\(localSize.description(linePrefix: " "))\n\n" out += "Remote:\n\(remoteSize.description(linePrefix: " "))" return out @@ -99,15 +94,6 @@ public struct FileSyncSessionEndpointSize: Equatable { self.symLinkCount = symLinkCount } - func maxOf(other: FileSyncSessionEndpointSize) -> FileSyncSessionEndpointSize { - FileSyncSessionEndpointSize( - sizeBytes: max(sizeBytes, other.sizeBytes), - fileCount: max(fileCount, other.fileCount), - dirCount: max(dirCount, other.dirCount), - symLinkCount: max(symLinkCount, other.symLinkCount) - ) - } - public var humanSizeBytes: String { humanReadableBytes(sizeBytes) } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy