Skip to content

Commit 9a2026d

Browse files
committed
chore: add mutagen session state conversions
1 parent 64c54d6 commit 9a2026d

File tree

4 files changed

+254
-26
lines changed

4 files changed

+254
-26
lines changed

Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift

+7-9
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,17 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
1111
var body: some View {
1212
Group {
1313
Table(items, selection: $selection) {
14-
TableColumn("Local Path") { row in
15-
Text(row.localPath.path())
14+
TableColumn("Local Path") {
15+
Text($0.localPath).help($0.localPath)
1616
}.width(min: 200, ideal: 240)
17-
TableColumn("Workspace", value: \.workspace)
17+
TableColumn("Workspace", value: \.agentHost)
1818
.width(min: 100, ideal: 120)
19-
TableColumn("Remote Path", value: \.remotePath)
19+
TableColumn("Remote Path") { Text($0.remotePath).help($0.remotePath) }
2020
.width(min: 100, ideal: 120)
21-
TableColumn("Status") { $0.status.body }
21+
TableColumn("Status") { $0.status.column.help($0.statusAndErrors) }
2222
.width(min: 80, ideal: 100)
23-
TableColumn("Size") { item in
24-
Text(item.size)
25-
}
26-
.width(min: 60, ideal: 80)
23+
TableColumn("Size") { Text($0.maxSize.humanSizeBytes).help($0.sizeDescription) }
24+
.width(min: 60, ideal: 80)
2725
}
2826
.frame(minWidth: 400, minHeight: 200)
2927
.padding(.bottom, 25)

Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift

+179-17
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,142 @@ import SwiftUI
22

33
public struct FileSyncSession: Identifiable {
44
public let id: String
5-
public let localPath: URL
6-
public let workspace: String
7-
// This is a string as to be host-OS agnostic
5+
public let name: String
6+
7+
public let localPath: String
8+
public let agentHost: String
89
public let remotePath: String
910
public let status: FileSyncStatus
10-
public let size: String
11+
12+
public let maxSize: FileSyncSessionEndpointSize
13+
public let localSize: FileSyncSessionEndpointSize
14+
public let remoteSize: FileSyncSessionEndpointSize
15+
16+
public let errors: [FileSyncError]
17+
18+
init(state: Synchronization_State) {
19+
id = state.session.identifier
20+
name = state.session.name
21+
22+
// If the protocol isn't what we expect for alpha or beta, show unknown
23+
localPath = if state.session.alpha.protocol == Url_Protocol.local, !state.session.alpha.path.isEmpty {
24+
state.session.alpha.path
25+
} else {
26+
"Unknown"
27+
}
28+
if state.session.beta.protocol == Url_Protocol.ssh, !state.session.beta.host.isEmpty {
29+
let host = state.session.beta.host
30+
// TOOD: We need to either:
31+
// - make this compatible with custom suffixes
32+
// - always strip the tld
33+
// - always keep the tld
34+
agentHost = host.hasSuffix(".coder") ? String(host.dropLast(6)) : host
35+
} else {
36+
agentHost = "Unknown"
37+
}
38+
remotePath = if !state.session.beta.path.isEmpty {
39+
state.session.beta.path
40+
} else {
41+
"Unknown"
42+
}
43+
44+
var status: FileSyncStatus = if state.session.paused {
45+
.paused
46+
} else {
47+
convertSessionStatus(status: state.status)
48+
}
49+
if case .error = status {} else {
50+
if state.conflicts.count > 0 {
51+
status = .needsAttention(name: "Conflicts", desc: "The session has conflicts that need to be resolved")
52+
}
53+
}
54+
self.status = status
55+
56+
localSize = .init(
57+
sizeBytes: state.alphaState.totalFileSize,
58+
fileCount: state.alphaState.files,
59+
dirCount: state.alphaState.directories,
60+
symLinkCount: state.alphaState.symbolicLinks
61+
)
62+
remoteSize = .init(
63+
sizeBytes: state.betaState.totalFileSize,
64+
fileCount: state.betaState.files,
65+
dirCount: state.betaState.directories,
66+
symLinkCount: state.betaState.symbolicLinks
67+
)
68+
maxSize = localSize.maxOf(other: remoteSize)
69+
70+
errors = accumulateErrors(from: state)
71+
}
72+
73+
public var statusAndErrors: String {
74+
var out = "\(status.type)\n\n\(status.description)"
75+
errors.forEach { out += "\n\t\($0)" }
76+
return out
77+
}
78+
79+
public var sizeDescription: String {
80+
var out = ""
81+
if localSize != remoteSize {
82+
out += "Maximum:\n\(maxSize.description(linePrefix: " "))\n\n"
83+
}
84+
out += "Local:\n\(localSize.description(linePrefix: " "))\n\n"
85+
out += "Remote:\n\(remoteSize.description(linePrefix: " "))"
86+
return out
87+
}
88+
}
89+
90+
public struct FileSyncSessionEndpointSize: Equatable {
91+
public let sizeBytes: UInt64
92+
public let fileCount: UInt64
93+
public let dirCount: UInt64
94+
public let symLinkCount: UInt64
95+
96+
public init(sizeBytes: UInt64, fileCount: UInt64, dirCount: UInt64, symLinkCount: UInt64) {
97+
self.sizeBytes = sizeBytes
98+
self.fileCount = fileCount
99+
self.dirCount = dirCount
100+
self.symLinkCount = symLinkCount
101+
}
102+
103+
func maxOf(other: FileSyncSessionEndpointSize) -> FileSyncSessionEndpointSize {
104+
FileSyncSessionEndpointSize(
105+
sizeBytes: max(sizeBytes, other.sizeBytes),
106+
fileCount: max(fileCount, other.fileCount),
107+
dirCount: max(dirCount, other.dirCount),
108+
symLinkCount: max(symLinkCount, other.symLinkCount)
109+
)
110+
}
111+
112+
public var humanSizeBytes: String {
113+
humanReadableBytes(sizeBytes)
114+
}
115+
116+
public func description(linePrefix: String = "") -> String {
117+
var result = ""
118+
result += linePrefix + humanReadableBytes(sizeBytes) + "\n"
119+
let numberFormatter = NumberFormatter()
120+
numberFormatter.numberStyle = .decimal
121+
if let formattedFileCount = numberFormatter.string(from: NSNumber(value: fileCount)) {
122+
result += "\(linePrefix)\(formattedFileCount) file\(fileCount == 1 ? "" : "s")\n"
123+
}
124+
if let formattedDirCount = numberFormatter.string(from: NSNumber(value: dirCount)) {
125+
result += "\(linePrefix)\(formattedDirCount) director\(dirCount == 1 ? "y" : "ies")"
126+
}
127+
if symLinkCount > 0, let formattedSymLinkCount = numberFormatter.string(from: NSNumber(value: symLinkCount)) {
128+
result += "\n\(linePrefix)\(formattedSymLinkCount) symlink\(symLinkCount == 1 ? "" : "s")"
129+
}
130+
return result
131+
}
11132
}
12133

13134
public enum FileSyncStatus {
14135
case unknown
15-
case error(String)
136+
case error(name: String, desc: String)
16137
case ok
17138
case paused
18-
case needsAttention(String)
19-
case working(String)
139+
case needsAttention(name: String, desc: String)
140+
case working(name: String, desc: String)
20141

21142
public var color: Color {
22143
switch self {
@@ -31,28 +152,69 @@ public enum FileSyncStatus {
31152
case .needsAttention:
32153
.orange
33154
case .working:
34-
.white
155+
.purple
35156
}
36157
}
37158

38-
public var description: String {
159+
public var type: String {
39160
switch self {
40161
case .unknown:
41162
"Unknown"
42-
case let .error(msg):
43-
msg
163+
case let .error(name, _):
164+
"\(name)"
44165
case .ok:
45166
"Watching"
46167
case .paused:
47168
"Paused"
48-
case let .needsAttention(msg):
49-
msg
50-
case let .working(msg):
51-
msg
169+
case let .needsAttention(name, _):
170+
name
171+
case let .working(name, _):
172+
name
52173
}
53174
}
54175

55-
public var body: some View {
56-
Text(description).foregroundColor(color)
176+
public var description: String {
177+
switch self {
178+
case .unknown:
179+
"Unknown status message."
180+
case let .error(_, desc):
181+
desc
182+
case .ok:
183+
"The session is watching for filesystem changes."
184+
case .paused:
185+
"The session is paused."
186+
case let .needsAttention(_, desc):
187+
desc
188+
case let .working(_, desc):
189+
desc
190+
}
191+
}
192+
193+
public var column: some View {
194+
Text(type).foregroundColor(color)
195+
}
196+
}
197+
198+
public enum FileSyncEndpoint {
199+
case local
200+
case remote
201+
}
202+
203+
public enum FileSyncProblemType {
204+
case scan
205+
case transition
206+
}
207+
208+
public enum FileSyncError {
209+
case generic(String)
210+
case problem(FileSyncEndpoint, FileSyncProblemType, path: String, error: String)
211+
212+
var description: String {
213+
switch self {
214+
case let .generic(error):
215+
error
216+
case let .problem(endpoint, type, path, error):
217+
"\(endpoint) \(type) error at \(path): \(error)"
218+
}
57219
}
58220
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// swiftlint:disable:next cyclomatic_complexity
2+
func convertSessionStatus(status: Synchronization_Status) -> FileSyncStatus {
3+
switch status {
4+
case .disconnected:
5+
.error(name: "Disconnected",
6+
desc: "The session is unpaused but not currently connected or connecting to either endpoint.")
7+
case .haltedOnRootEmptied:
8+
.error(name: "Halted on root emptied", desc: "The session is halted due to the root emptying safety check.")
9+
case .haltedOnRootDeletion:
10+
.error(name: "Halted on root deletion", desc: "The session is halted due to the root deletion safety check.")
11+
case .haltedOnRootTypeChange:
12+
.error(
13+
name: "Halted on root type change",
14+
desc: "The session is halted due to the root type change safety check."
15+
)
16+
case .waitingForRescan:
17+
.error(name: "Waiting for rescan",
18+
desc: "The session is waiting to retry scanning after an error during the previous scan.")
19+
case .connectingAlpha:
20+
// Alpha -> Local
21+
.working(name: "Connecting (local)", desc: "The session is attempting to connect to the local endpoint.")
22+
case .connectingBeta:
23+
// Beta -> Remote
24+
.working(name: "Connecting (remote)", desc: "The session is attempting to connect to the remote endpoint.")
25+
case .scanning:
26+
.working(name: "Scanning", desc: "The session is scanning the filesystem on each endpoint.")
27+
case .reconciling:
28+
.working(name: "Reconciling", desc: "The session is performing reconciliation.")
29+
case .stagingAlpha:
30+
// Alpha -> Local
31+
.working(name: "Staging (local)", desc: "The session is staging files locally")
32+
case .stagingBeta:
33+
// Beta -> Remote
34+
.working(name: "Staging (remote)", desc: "The session is staging files on the remote")
35+
case .transitioning:
36+
.working(name: "Transitioning", desc: "The session is performing transition operations on each endpoint.")
37+
case .saving:
38+
.working(name: "Saving", desc: "The session is recording synchronization history to disk.")
39+
case .watching:
40+
.ok
41+
case .UNRECOGNIZED:
42+
.unknown
43+
}
44+
}
45+
46+
func accumulateErrors(from state: Synchronization_State) -> [FileSyncError] {
47+
var errors: [FileSyncError] = []
48+
if !state.lastError.isEmpty {
49+
errors.append(.generic(state.lastError))
50+
}
51+
for problem in state.alphaState.scanProblems {
52+
errors.append(.problem(.local, .scan, path: problem.path, error: problem.error))
53+
}
54+
for problem in state.alphaState.transitionProblems {
55+
errors.append(.problem(.local, .transition, path: problem.path, error: problem.error))
56+
}
57+
for problem in state.betaState.scanProblems {
58+
errors.append(.problem(.remote, .scan, path: problem.path, error: problem.error))
59+
}
60+
for problem in state.betaState.transitionProblems {
61+
errors.append(.problem(.remote, .transition, path: problem.path, error: problem.error))
62+
}
63+
return errors
64+
}
65+
66+
func humanReadableBytes(_ bytes: UInt64) -> String {
67+
ByteCountFormatter().string(fromByteCount: Int64(bytes))
68+
}

0 commit comments

Comments
 (0)