1use std::{fmt, str::FromStr};
2
3use chrono::{DateTime, Utc};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use serde_json::{Map, Value};
7use uuid::Uuid;
8
9#[derive(
10 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
11)]
12#[serde(transparent)]
13pub struct SshConnectionId(String);
14
15impl SshConnectionId {
16 pub fn new() -> Self {
17 Self(format!("sshconn_{}", Uuid::new_v4().simple()))
18 }
19
20 pub fn as_str(&self) -> &str {
21 &self.0
22 }
23}
24
25impl Default for SshConnectionId {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl fmt::Display for SshConnectionId {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 f.write_str(&self.0)
34 }
35}
36
37impl FromStr for SshConnectionId {
38 type Err = &'static str;
39
40 fn from_str(value: &str) -> Result<Self, Self::Err> {
41 if value.trim().is_empty() {
42 return Err("ssh connection id cannot be empty");
43 }
44
45 Ok(Self(value.to_string()))
46 }
47}
48
49impl From<String> for SshConnectionId {
50 fn from(value: String) -> Self {
51 Self(value)
52 }
53}
54
55#[derive(
56 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
57)]
58#[serde(transparent)]
59pub struct SshMountId(String);
60
61impl SshMountId {
62 pub fn new() -> Self {
63 Self(format!("sshmnt_{}", Uuid::new_v4().simple()))
64 }
65
66 pub fn as_str(&self) -> &str {
67 &self.0
68 }
69}
70
71impl Default for SshMountId {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77impl fmt::Display for SshMountId {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 f.write_str(&self.0)
80 }
81}
82
83impl FromStr for SshMountId {
84 type Err = &'static str;
85
86 fn from_str(value: &str) -> Result<Self, Self::Err> {
87 if value.trim().is_empty() {
88 return Err("ssh mount id cannot be empty");
89 }
90
91 Ok(Self(value.to_string()))
92 }
93}
94
95impl From<String> for SshMountId {
96 fn from(value: String) -> Self {
97 Self(value)
98 }
99}
100
101#[derive(
102 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
103)]
104#[serde(transparent)]
105pub struct SshTunnelId(String);
106
107impl SshTunnelId {
108 pub fn new() -> Self {
109 Self(format!("sshtun_{}", Uuid::new_v4().simple()))
110 }
111
112 pub fn as_str(&self) -> &str {
113 &self.0
114 }
115}
116
117impl Default for SshTunnelId {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123impl fmt::Display for SshTunnelId {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 f.write_str(&self.0)
126 }
127}
128
129impl FromStr for SshTunnelId {
130 type Err = &'static str;
131
132 fn from_str(value: &str) -> Result<Self, Self::Err> {
133 if value.trim().is_empty() {
134 return Err("ssh tunnel id cannot be empty");
135 }
136
137 Ok(Self(value.to_string()))
138 }
139}
140
141impl From<String> for SshTunnelId {
142 fn from(value: String) -> Self {
143 Self(value)
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
148pub struct SshTarget {
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub host_alias: Option<String>,
151 pub host: String,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub user: Option<String>,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub port: Option<u16>,
156}
157
158impl SshTarget {
159 pub fn summary(&self) -> String {
160 let host = self.host_alias.as_deref().unwrap_or(&self.host);
161 let authority = match &self.user {
162 Some(user) if !user.is_empty() => format!("{user}@{host}"),
163 _ => host.to_string(),
164 };
165
166 match self.port {
167 Some(port) => format!("{authority}:{port}"),
168 None => authority,
169 }
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
174#[schemars(inline)]
175#[serde(rename_all = "snake_case")]
176pub enum SshAuthKind {
177 SshAgent,
178 IdentityFile,
179 ConfigAlias,
180}
181
182impl SshAuthKind {
183 pub const fn as_str(&self) -> &'static str {
184 match self {
185 Self::SshAgent => "ssh_agent",
186 Self::IdentityFile => "identity_file",
187 Self::ConfigAlias => "config_alias",
188 }
189 }
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
193#[serde(rename_all = "snake_case")]
194pub enum SshConnectionStatus {
195 Connecting,
196 Ready,
197 Degraded,
198 Disconnecting,
199 Disconnected,
200 Failed,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
204#[serde(rename_all = "snake_case")]
205pub enum SshMountStatus {
206 Mounting,
207 Mounted,
208 Unmounting,
209 Unmounted,
210 Failed,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
214#[serde(rename_all = "snake_case")]
215pub enum SshTunnelStatus {
216 Opening,
217 Active,
218 Closing,
219 Closed,
220 Failed,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
224#[schemars(inline)]
225#[serde(rename_all = "snake_case")]
226pub enum SshMountBackend {
227 Sshfs,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
231#[schemars(inline)]
232#[serde(rename_all = "snake_case")]
233pub enum SshTunnelKind {
234 LocalForward,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
238pub struct SshBinaryCapability {
239 pub available: bool,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub path: Option<String>,
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub version: Option<String>,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
247pub struct MacFuseCapability {
248 pub available: bool,
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub provider: Option<String>,
251 #[serde(skip_serializing_if = "Option::is_none")]
252 pub version: Option<String>,
253}
254
255#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
256pub struct SshCapabilityView {
257 pub platform: String,
258 pub ssh: SshBinaryCapability,
259 pub sshfs: SshBinaryCapability,
260 pub unmount: SshBinaryCapability,
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub diskutil: Option<SshBinaryCapability>,
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub macfuse: Option<MacFuseCapability>,
265}
266
267impl Default for SshCapabilityView {
268 fn default() -> Self {
269 Self {
270 platform: std::env::consts::OS.to_string(),
271 ssh: SshBinaryCapability::default(),
272 sshfs: SshBinaryCapability::default(),
273 unmount: SshBinaryCapability::default(),
274 diskutil: None,
275 macfuse: None,
276 }
277 }
278}
279
280#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
281pub struct SshConnectionSummary {
282 pub connection_id: SshConnectionId,
283 #[serde(skip_serializing_if = "Option::is_none")]
284 pub title: Option<String>,
285 #[serde(skip_serializing_if = "Option::is_none")]
286 pub description: Option<String>,
287 pub status: SshConnectionStatus,
288 pub target: SshTarget,
289 pub target_summary: String,
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub auth_kind: Option<SshAuthKind>,
292 pub started_at: DateTime<Utc>,
293 #[serde(skip_serializing_if = "Option::is_none")]
294 pub last_used_at: Option<DateTime<Utc>>,
295 pub active_session_count: usize,
296 pub active_mount_count: usize,
297 pub active_tunnel_count: usize,
298 #[serde(default, skip_serializing_if = "Map::is_empty")]
299 pub metadata: Map<String, Value>,
300}
301
302#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
303pub struct SshMountSummary {
304 pub mount_id: SshMountId,
305 #[serde(skip_serializing_if = "Option::is_none")]
306 pub title: Option<String>,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 pub description: Option<String>,
309 pub connection_id: SshConnectionId,
310 pub target_summary: String,
311 pub status: SshMountStatus,
312 pub backend: SshMountBackend,
313 pub local_path: String,
314 pub remote_path: String,
315 pub read_only: bool,
316 pub mounted_at: DateTime<Utc>,
317 #[serde(skip_serializing_if = "Option::is_none")]
318 pub last_error: Option<String>,
319}
320
321#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
322pub struct SshTunnelSummary {
323 pub tunnel_id: SshTunnelId,
324 #[serde(skip_serializing_if = "Option::is_none")]
325 pub title: Option<String>,
326 #[serde(skip_serializing_if = "Option::is_none")]
327 pub description: Option<String>,
328 pub connection_id: SshConnectionId,
329 pub target_summary: String,
330 pub kind: SshTunnelKind,
331 pub status: SshTunnelStatus,
332 pub bind_host: String,
333 pub local_port: u16,
334 pub remote_host: String,
335 pub remote_port: u16,
336 pub started_at: DateTime<Utc>,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 pub last_error: Option<String>,
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub pid: Option<u32>,
341}