Skip to main content

ssh_mcp/transfer/
types.rs

1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "kebab-case")]
7pub enum TransferOperation {
8    Put,
9    Get,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "kebab-case")]
14pub enum TransferTransport {
15    Auto,
16    ExecRaw,
17    Sftp,
18    Scp,
19    Rsync,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "kebab-case")]
24pub enum TransferKind {
25    File,
26    Directory,
27}
28
29/// Rsync-specific options for fine-tuning transfer behavior.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct RsyncOptions {
32    /// Verify file integrity using checksums after transfer (deterministic, safe).
33    #[serde(default = "default_true")]
34    pub checksum: bool,
35    /// Compress data during transfer (optional optimization).
36    #[serde(default = "default_false")]
37    pub compress: bool,
38    /// Delete files on destination not present on source (potentially dangerous).
39    #[serde(default = "default_false")]
40    pub delete: bool,
41    /// Update destination files in-place (safe and efficient).
42    #[serde(default = "default_true")]
43    pub inplace: bool,
44    /// Keep partially transferred files for resume (safe, enables resume).
45    #[serde(default = "default_true")]
46    pub partial: bool,
47    /// Bandwidth limit in KB/s (optional, None means unlimited).
48    pub bwlimit: Option<u32>,
49}
50
51impl Default for RsyncOptions {
52    fn default() -> Self {
53        Self {
54            checksum: true,
55            compress: false,
56            delete: false,
57            inplace: true,
58            partial: true,
59            bwlimit: None,
60        }
61    }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct TransferParams {
66    pub operation: TransferOperation,
67
68    /// Local path.
69    ///
70    /// - For `put`: local source path (file or directory; must be within local_root, relative-only)
71    /// - For `get`: local destination path (must be within local_root, relative-only)
72    pub local_path: String,
73
74    /// Remote path.
75    ///
76    /// - For `put`: remote destination path
77    /// - For `get`: remote source path
78    pub remote_path: String,
79
80    #[serde(default = "default_transport")]
81    pub transport: TransferTransport,
82
83    /// Optional explicit kind. If omitted, the server auto-detects.
84    pub kind: Option<TransferKind>,
85
86    /// Whether overwriting an existing destination is allowed.
87    ///
88    /// Note: For file transfers, `overwrite=false` relies on creating a hard-link to install the
89    /// final file without replacement. This requires hard-link support on the destination
90    /// filesystem:
91    /// - `put` (local -> remote): requires hard-link support on the remote filesystem
92    /// - `get` (remote -> local): requires hard-link support on the local filesystem
93    #[serde(default = "default_overwrite")]
94    pub overwrite: bool,
95
96    /// Optional timeout override for this transfer.
97    pub timeout_ms: Option<u64>,
98
99    /// When true, return full diagnostic response including staging details.
100    /// When false or omitted, return compact response with only essential fields.
101    #[serde(default)]
102    pub verbose: bool,
103
104    /// Rsync-specific options. Only used when transport is `Rsync`.
105    #[serde(default)]
106    pub rsync_options: RsyncOptions,
107}
108
109fn default_true() -> bool {
110    true
111}
112
113fn default_false() -> bool {
114    false
115}
116
117fn default_transport() -> TransferTransport {
118    TransferTransport::Auto
119}
120
121fn default_overwrite() -> bool {
122    false
123}
124
125impl Default for TransferParams {
126    fn default() -> Self {
127        Self {
128            operation: TransferOperation::Put,
129            local_path: String::new(),
130            remote_path: String::new(),
131            transport: default_transport(),
132            kind: None,
133            overwrite: default_overwrite(),
134            timeout_ms: None,
135            verbose: false,
136            rsync_options: RsyncOptions::default(),
137        }
138    }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct ResolvedPaths {
143    pub local_path: PathBuf,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
147pub struct TransferCounts {
148    pub bytes: u64,
149    pub files: u64,
150    pub directories: u64,
151}
152
153impl TransferCounts {
154    pub fn zero() -> Self {
155        Self {
156            bytes: 0,
157            files: 0,
158            directories: 0,
159        }
160    }
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct TransferStaging {
165    pub local: Option<StagingLocal>,
166    pub remote: Option<StagingRemote>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct StagingLocal {
171    pub staging_path: String,
172    pub backup_path: Option<String>,
173    pub final_path: String,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct StagingRemote {
178    pub staging_path: String,
179    pub backup_path: Option<String>,
180    pub final_path: String,
181    pub staging_base_home: String,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct TransferResponse {
186    pub ok: bool,
187    pub error: Option<String>,
188
189    pub params: TransferParams,
190    pub kind: Option<TransferKind>,
191
192    pub transport_used: TransferTransport,
193    #[serde(skip_serializing_if = "Vec::is_empty", default)]
194    pub fallback_chain: Vec<TransferTransport>,
195    pub remote_home: Option<String>,
196    pub local_root: String,
197
198    pub resolved_paths: Option<ResolvedPaths>,
199    pub staging: Option<TransferStaging>,
200    pub counts: Option<TransferCounts>,
201
202    pub elapsed_ms: Option<u64>,
203    pub semantics: Option<String>,
204}
205
206impl TransferResponse {
207    pub fn ok_stub(
208        params: TransferParams,
209        transport_used: TransferTransport,
210        remote_home: &str,
211        local_root: &Path,
212    ) -> Self {
213        Self {
214            ok: false,
215            error: None,
216            params,
217            kind: None,
218            transport_used,
219            fallback_chain: Vec::new(),
220            remote_home: Some(remote_home.to_string()),
221            local_root: local_root.display().to_string(),
222            resolved_paths: None,
223            staging: None,
224            counts: None,
225            elapsed_ms: None,
226            semantics: None,
227        }
228    }
229
230    pub fn error(params: TransferParams, local_root: &Path, msg: &str) -> Self {
231        Self {
232            ok: false,
233            error: Some(msg.to_string()),
234            transport_used: params.transport,
235            fallback_chain: Vec::new(),
236            remote_home: None,
237            local_root: local_root.display().to_string(),
238            params,
239            kind: None,
240            resolved_paths: None,
241            staging: None,
242            counts: None,
243            elapsed_ms: None,
244            semantics: None,
245        }
246    }
247
248    pub fn set_error(&mut self, msg: &str) {
249        self.ok = false;
250        self.error = Some(msg.to_string());
251    }
252}
253
254/// Compact transfer response for non-verbose mode.
255/// Contains only essential fields that agents need.
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct CompactTransferResponse {
258    pub ok: bool,
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub error: Option<String>,
261    pub kind: Option<TransferKind>,
262    pub transport_used: TransferTransport,
263    #[serde(skip_serializing_if = "Vec::is_empty", default)]
264    pub fallback_chain: Vec<TransferTransport>,
265    // Paths are always included for DevOps context
266    pub local_path: String,
267    pub remote_path: String,
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub counts: Option<TransferCounts>,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub elapsed_ms: Option<u64>,
272}
273
274impl TransferResponse {
275    /// Convert to compact representation for non-verbose responses.
276    pub fn to_compact(&self) -> CompactTransferResponse {
277        CompactTransferResponse {
278            ok: self.ok,
279            error: self.error.clone(),
280            kind: self.kind,
281            transport_used: self.transport_used,
282            fallback_chain: self.fallback_chain.clone(),
283            local_path: self.params.local_path.clone(),
284            remote_path: self.params.remote_path.clone(),
285            counts: self.counts.clone(),
286            elapsed_ms: self.elapsed_ms,
287        }
288    }
289
290    /// Serialize to JSON, using compact format unless verbose is true.
291    pub fn to_json(&self, verbose: bool) -> Result<String, serde_json::Error> {
292        if verbose {
293            serde_json::to_string(self)
294        } else {
295            serde_json::to_string(&self.to_compact())
296        }
297    }
298}