Skip to main content

ssh_mcp/transfer/
types.rs

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