Skip to content

Commit a26b8fe

Browse files
committed
chore: add mutagen session state conversions
1 parent 230bb09 commit a26b8fe

File tree

4 files changed

+254
-26
lines changed

4 files changed

+254
-26
lines changed

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

Lines changed: 7 additions & 9 deletions
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

Lines changed: 179 additions & 17 deletions
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
}
Lines changed: 68 additions & 0 deletions
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)
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