Skip to content

Commit 2fa9ac2

Browse files
committed
chore: add mutagen session state conversions
1 parent fceb8a5 commit 2fa9ac2

File tree

4 files changed

+332
-21
lines changed

4 files changed

+332
-21
lines changed

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

+4-6
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,12 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
2020
}.width(min: 200, ideal: 240)
2121
TableColumn("Workspace", value: \.agentHost)
2222
.width(min: 100, ideal: 120)
23-
TableColumn("Remote Path", value: \.betaPath)
23+
TableColumn("Remote Path") { Text($0.betaPath).help($0.betaPath) }
2424
.width(min: 100, ideal: 120)
25-
TableColumn("Status") { $0.status.body }
25+
TableColumn("Status") { $0.status.column.help($0.statusAndErrors) }
2626
.width(min: 80, ideal: 100)
27-
TableColumn("Size") { item in
28-
Text(item.size)
29-
}
30-
.width(min: 60, ideal: 80)
27+
TableColumn("Size") { Text($0.maxSize.humanSizeBytes).help($0.sizeDescription) }
28+
.width(min: 60, ideal: 80)
3129
}
3230
.frame(minWidth: 400, minHeight: 200)
3331
.padding(.bottom, 25)

Coder-Desktop/VPNLib/FileSync/FileSyncSession.swift

+269-15
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,141 @@ import SwiftUI
33
public struct FileSyncSession: Identifiable {
44
public let id: String
55
public let alphaPath: String
6+
public let name: String
7+
68
public let agentHost: String
79
public let betaPath: String
810
public let status: FileSyncStatus
9-
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+
alphaPath = 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+
betaPath = 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 = .conflicts
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+
}
10132
}
11133

12134
public enum FileSyncStatus {
13135
case unknown
14-
case error(String)
136+
case error(FileSyncErrorStatus)
15137
case ok
16138
case paused
17-
case needsAttention(String)
18-
case working(String)
139+
case conflicts
140+
case working(FileSyncWorkingStatus)
19141

20142
public var color: Color {
21143
switch self {
@@ -27,31 +149,163 @@ public enum FileSyncStatus {
27149
.red
28150
case .error:
29151
.red
30-
case .needsAttention:
152+
case .conflicts:
31153
.orange
32154
case .working:
33-
.white
155+
.purple
34156
}
35157
}
36158

37-
public var description: String {
159+
public var type: String {
38160
switch self {
39161
case .unknown:
40162
"Unknown"
41-
case let .error(msg):
42-
msg
163+
case let .error(status):
164+
status.name
43165
case .ok:
44166
"Watching"
45167
case .paused:
46168
"Paused"
47-
case let .needsAttention(msg):
48-
msg
49-
case let .working(msg):
50-
msg
169+
case .conflicts:
170+
"Conflicts"
171+
case let .working(status):
172+
status.name
173+
}
174+
}
175+
176+
public var description: String {
177+
switch self {
178+
case .unknown:
179+
"Unknown status message."
180+
case let .error(status):
181+
status.description
182+
case .ok:
183+
"The session is watching for filesystem changes."
184+
case .paused:
185+
"The session is paused."
186+
case .conflicts:
187+
"The session has conflicts that need to be resolved."
188+
case let .working(status):
189+
status.description
51190
}
52191
}
53192

54-
public var body: some View {
55-
Text(description).foregroundColor(color)
193+
public var column: some View {
194+
Text(type).foregroundColor(color)
195+
}
196+
}
197+
198+
public enum FileSyncWorkingStatus {
199+
case connectingAlpha
200+
case connectingBeta
201+
case scanning
202+
case reconciling
203+
case stagingAlpha
204+
case stagingBeta
205+
case transitioning
206+
case saving
207+
208+
var name: String {
209+
switch self {
210+
case .connectingAlpha:
211+
"Connecting (alpha)"
212+
case .connectingBeta:
213+
"Connecting (beta)"
214+
case .scanning:
215+
"Scanning"
216+
case .reconciling:
217+
"Reconciling"
218+
case .stagingAlpha:
219+
"Staging (alpha)"
220+
case .stagingBeta:
221+
"Staging (beta)"
222+
case .transitioning:
223+
"Transitioning"
224+
case .saving:
225+
"Saving"
226+
}
227+
}
228+
229+
var description: String {
230+
switch self {
231+
case .connectingAlpha:
232+
"The session is attempting to connect to the alpha endpoint."
233+
case .connectingBeta:
234+
"The session is attempting to connect to the beta endpoint."
235+
case .scanning:
236+
"The session is scanning the filesystem on each endpoint."
237+
case .reconciling:
238+
"The session is performing reconciliation."
239+
case .stagingAlpha:
240+
"The session is staging files on the alpha endpoint"
241+
case .stagingBeta:
242+
"The session is staging files on the beta endpoint"
243+
case .transitioning:
244+
"The session is performing transition operations on each endpoint."
245+
case .saving:
246+
"The session is recording synchronization history to disk."
247+
}
248+
}
249+
}
250+
251+
public enum FileSyncErrorStatus {
252+
case disconnected
253+
case haltedOnRootEmptied
254+
case haltedOnRootDeletion
255+
case haltedOnRootTypeChange
256+
case waitingForRescan
257+
258+
var name: String {
259+
switch self {
260+
case .disconnected:
261+
"Disconnected"
262+
case .haltedOnRootEmptied:
263+
"Halted on root emptied"
264+
case .haltedOnRootDeletion:
265+
"Halted on root deletion"
266+
case .haltedOnRootTypeChange:
267+
"Halted on root type change"
268+
case .waitingForRescan:
269+
"Waiting for rescan"
270+
}
271+
}
272+
273+
var description: String {
274+
switch self {
275+
case .disconnected:
276+
"The session is unpaused but not currently connected or connecting to either endpoint."
277+
case .haltedOnRootEmptied:
278+
"The session is halted due to the root emptying safety check."
279+
case .haltedOnRootDeletion:
280+
"The session is halted due to the root deletion safety check."
281+
case .haltedOnRootTypeChange:
282+
"The session is halted due to the root type change safety check."
283+
case .waitingForRescan:
284+
"The session is waiting to retry scanning after an error during the previous scan."
285+
}
286+
}
287+
}
288+
289+
public enum FileSyncEndpoint {
290+
case local
291+
case remote
292+
}
293+
294+
public enum FileSyncProblemType {
295+
case scan
296+
case transition
297+
}
298+
299+
public enum FileSyncError {
300+
case generic(String)
301+
case problem(FileSyncEndpoint, FileSyncProblemType, path: String, error: String)
302+
303+
var description: String {
304+
switch self {
305+
case let .generic(error):
306+
error
307+
case let .problem(endpoint, type, path, error):
308+
"\(endpoint) \(type) error at \(path): \(error)"
309+
}
56310
}
57311
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// swiftlint:disable:next cyclomatic_complexity
2+
func convertSessionStatus(status: Synchronization_Status) -> FileSyncStatus {
3+
switch status {
4+
case .disconnected:
5+
.error(.disconnected)
6+
case .haltedOnRootEmptied:
7+
.error(.haltedOnRootEmptied)
8+
case .haltedOnRootDeletion:
9+
.error(.haltedOnRootDeletion)
10+
case .haltedOnRootTypeChange:
11+
.error(.haltedOnRootTypeChange)
12+
case .waitingForRescan:
13+
.error(.waitingForRescan)
14+
case .connectingAlpha:
15+
.working(.connectingAlpha)
16+
case .connectingBeta:
17+
.working(.connectingBeta)
18+
case .scanning:
19+
.working(.scanning)
20+
case .reconciling:
21+
.working(.reconciling)
22+
case .stagingAlpha:
23+
.working(.stagingAlpha)
24+
case .stagingBeta:
25+
.working(.stagingBeta)
26+
case .transitioning:
27+
.working(.transitioning)
28+
case .saving:
29+
.working(.saving)
30+
case .watching:
31+
.ok
32+
case .UNRECOGNIZED:
33+
.unknown
34+
}
35+
}
36+
37+
func accumulateErrors(from state: Synchronization_State) -> [FileSyncError] {
38+
var errors: [FileSyncError] = []
39+
if !state.lastError.isEmpty {
40+
errors.append(.generic(state.lastError))
41+
}
42+
for problem in state.alphaState.scanProblems {
43+
errors.append(.problem(.local, .scan, path: problem.path, error: problem.error))
44+
}
45+
for problem in state.alphaState.transitionProblems {
46+
errors.append(.problem(.local, .transition, path: problem.path, error: problem.error))
47+
}
48+
for problem in state.betaState.scanProblems {
49+
errors.append(.problem(.remote, .scan, path: problem.path, error: problem.error))
50+
}
51+
for problem in state.betaState.transitionProblems {
52+
errors.append(.problem(.remote, .transition, path: problem.path, error: problem.error))
53+
}
54+
return errors
55+
}
56+
57+
func humanReadableBytes(_ bytes: UInt64) -> String {
58+
ByteCountFormatter().string(fromByteCount: Int64(bytes))
59+
}
File renamed without changes.

0 commit comments

Comments
 (0)