Skip to main content

ssh_cli/
scp.rs

1//! Transferência de arquivos via SCP sobre SSH.
2//!
3//! Wrapper que usa os métodos `upload` e `download` do [`ClienteSsh`].
4
5use crate::cli::AcaoScp;
6use crate::erros::ErroSshCli;
7use crate::output;
8use crate::ssh::cliente::{ClienteSsh, ClienteSshTrait};
9use crate::vps;
10use std::path::PathBuf;
11
12/// Executa o subcomando SCP (upload/download).
13pub async fn executar_scp(
14    acao: AcaoScp,
15    config_override: Option<PathBuf>,
16    password_override: Option<String>,
17) -> anyhow::Result<()> {
18    if crate::signals::cancelado() {
19        return Err(anyhow::anyhow!(crate::i18n::t(
20            crate::i18n::Mensagem::OperacaoCancelada
21        )));
22    }
23
24    match acao {
25        AcaoScp::Upload {
26            vps_nome,
27            local,
28            remote,
29            ..
30        } => {
31            // GAP-SSH-SCP-001: validar arquivo local antes do connect SSH.
32            if !local.is_file() {
33                return Err(ErroSshCli::ArquivoNaoEncontrado(local.display().to_string()).into());
34            }
35
36            let mut registro = vps::buscar_por_nome(config_override.clone(), &vps_nome)?
37                .ok_or_else(|| ErroSshCli::VpsNaoEncontrada(vps_nome.clone()))?;
38
39            if let Some(pwd) = password_override {
40                registro.senha = secrecy::SecretString::from(pwd);
41            }
42
43            let caminho = crate::vps::resolver_caminho_config(config_override.clone())?;
44            let cfg = crate::vps::construir_configuracao(&registro, Some(&caminho), false);
45
46            let cliente: Box<dyn ClienteSshTrait> =
47                <ClienteSsh as ClienteSshTrait>::conectar(cfg).await?;
48            executar_scp_upload_with_client(&registro, &local, &remote, cliente).await?;
49        }
50        AcaoScp::Download {
51            vps_nome,
52            remote,
53            local,
54            ..
55        } => {
56            let mut registro = vps::buscar_por_nome(config_override.clone(), &vps_nome)?
57                .ok_or_else(|| ErroSshCli::VpsNaoEncontrada(vps_nome.clone()))?;
58
59            if let Some(pwd) = password_override {
60                registro.senha = secrecy::SecretString::from(pwd);
61            }
62
63            let caminho = crate::vps::resolver_caminho_config(config_override.clone())?;
64            let cfg = crate::vps::construir_configuracao(&registro, Some(&caminho), false);
65
66            let cliente: Box<dyn ClienteSshTrait> =
67                <ClienteSsh as ClienteSshTrait>::conectar(cfg).await?;
68            executar_scp_download_with_client(&registro, &remote, &local, cliente).await?;
69        }
70    }
71    Ok(())
72}
73
74/// Versão testável de upload SCP que aceita o cliente como parâmetro.
75pub async fn executar_scp_upload_with_client(
76    _registro: &crate::vps::modelo::VpsRegistro,
77    local: &std::path::Path,
78    remote: &std::path::Path,
79    mut cliente: Box<dyn ClienteSshTrait>,
80) -> anyhow::Result<()> {
81    let resultado = cliente.upload(local, remote).await?;
82    cliente.desconectar().await?;
83    output::imprimir_sucesso(&format!(
84        "Upload concluído: {} bytes em {}ms",
85        resultado.bytes_transferidos, resultado.duracao_ms
86    ));
87    Ok(())
88}
89
90/// Versão testável de download SCP que aceita o cliente como parâmetro.
91pub async fn executar_scp_download_with_client(
92    _registro: &crate::vps::modelo::VpsRegistro,
93    remote: &std::path::Path,
94    local: &std::path::Path,
95    mut cliente: Box<dyn ClienteSshTrait>,
96) -> anyhow::Result<()> {
97    let resultado = cliente.download(remote, local).await?;
98    cliente.desconectar().await?;
99    output::imprimir_sucesso(&format!(
100        "Download concluído: {} bytes em {}ms",
101        resultado.bytes_transferidos, resultado.duracao_ms
102    ));
103    Ok(())
104}
105
106#[cfg(test)]
107mod testes {
108    use super::*;
109    use crate::erros::ErroSshCli;
110    use crate::ssh::cliente::{
111        CanalTunel, ConfiguracaoConexao, SaidaExecucao, TransferenciaResultado,
112    };
113    use crate::vps::modelo::{VpsRegistro, SCHEMA_VERSION_ATUAL};
114    use crate::vps::{self, ArquivoConfig};
115    use async_trait::async_trait;
116    use secrecy::SecretString;
117    use serial_test::serial;
118    use std::collections::BTreeMap;
119    use std::path::Path;
120    use tempfile::TempDir;
121
122    struct ClienteFakeScp {
123        upload_ok: bool,
124        download_ok: bool,
125        bytes_upload: u64,
126        bytes_download: u64,
127    }
128
129    #[async_trait]
130    impl ClienteSshTrait for ClienteFakeScp {
131        async fn conectar(_cfg: ConfiguracaoConexao) -> Result<Box<Self>, ErroSshCli> {
132            Err(ErroSshCli::ConexaoFalhou(
133                "não implementado em teste".to_string(),
134            ))
135        }
136
137        async fn executar_comando(
138            &mut self,
139            _cmd: &str,
140            _max_chars: usize,
141            _stdin_data: Option<Vec<u8>>,
142        ) -> Result<SaidaExecucao, ErroSshCli> {
143            Err(ErroSshCli::CanalFalhou(
144                "não implementado em teste".to_string(),
145            ))
146        }
147
148        async fn upload(
149            &mut self,
150            _local: &Path,
151            _remote: &Path,
152        ) -> Result<TransferenciaResultado, ErroSshCli> {
153            if self.upload_ok {
154                Ok(TransferenciaResultado {
155                    bytes_transferidos: self.bytes_upload,
156                    duracao_ms: 10,
157                })
158            } else {
159                Err(ErroSshCli::CanalFalhou("upload falhou".to_string()))
160            }
161        }
162
163        async fn download(
164            &mut self,
165            _remote: &Path,
166            _local: &Path,
167        ) -> Result<TransferenciaResultado, ErroSshCli> {
168            if self.download_ok {
169                Ok(TransferenciaResultado {
170                    bytes_transferidos: self.bytes_download,
171                    duracao_ms: 20,
172                })
173            } else {
174                Err(ErroSshCli::CanalFalhou("download falhou".to_string()))
175            }
176        }
177
178        async fn abrir_canal_tunel(
179            &self,
180            _host_remoto: &str,
181            _porta_remota: u16,
182            _endereco_origem: &str,
183            _porta_origem: u16,
184        ) -> Result<Box<dyn CanalTunel>, ErroSshCli> {
185            Err(ErroSshCli::CanalFalhou(
186                "não implementado em teste".to_string(),
187            ))
188        }
189
190        async fn desconectar(&self) -> Result<(), ErroSshCli> {
191            Ok(())
192        }
193    }
194
195    fn registro_teste(nome: &str) -> VpsRegistro {
196        VpsRegistro::novo(
197            nome.to_string(),
198            "127.0.0.1".to_string(),
199            1,
200            "root".to_string(),
201            SecretString::from("senha-teste".to_string()),
202            None,
203            None,
204            Some(100),
205            Some(1000),
206            Some(1000),
207            None,
208            None,
209            false,
210        )
211    }
212
213    fn salvar_config_com_vps(tmp: &TempDir, nome: &str) {
214        let mut hosts = BTreeMap::new();
215        hosts.insert(nome.to_string(), registro_teste(nome));
216        let arquivo = ArquivoConfig {
217            schema_version: SCHEMA_VERSION_ATUAL,
218            hosts,
219        };
220        let caminho = tmp.path().join("config.toml");
221        vps::salvar(&caminho, &arquivo).expect("salvar config teste");
222    }
223
224    #[tokio::test]
225    async fn executar_scp_upload_with_client_retorna_ok() {
226        let cliente = ClienteFakeScp {
227            upload_ok: true,
228            download_ok: true,
229            bytes_upload: 128,
230            bytes_download: 0,
231        };
232        let registro = registro_teste("vps-a");
233
234        let resultado = executar_scp_upload_with_client(
235            &registro,
236            Path::new("/tmp/local.txt"),
237            Path::new("/tmp/remote.txt"),
238            Box::new(cliente),
239        )
240        .await;
241
242        assert!(resultado.is_ok());
243    }
244
245    #[tokio::test]
246    async fn executar_scp_download_with_client_retorna_ok() {
247        let cliente = ClienteFakeScp {
248            upload_ok: true,
249            download_ok: true,
250            bytes_upload: 0,
251            bytes_download: 256,
252        };
253        let registro = registro_teste("vps-b");
254
255        let resultado = executar_scp_download_with_client(
256            &registro,
257            Path::new("/tmp/remote.txt"),
258            Path::new("/tmp/local.txt"),
259            Box::new(cliente),
260        )
261        .await;
262
263        assert!(resultado.is_ok());
264    }
265
266    #[tokio::test]
267    async fn executar_scp_upload_with_client_retorna_erro() {
268        let cliente = ClienteFakeScp {
269            upload_ok: false,
270            download_ok: true,
271            bytes_upload: 0,
272            bytes_download: 0,
273        };
274        let registro = registro_teste("vps-c");
275
276        let resultado = executar_scp_upload_with_client(
277            &registro,
278            Path::new("/tmp/local.txt"),
279            Path::new("/tmp/remote.txt"),
280            Box::new(cliente),
281        )
282        .await;
283
284        assert!(resultado.is_err());
285    }
286
287    #[tokio::test]
288    async fn executar_scp_download_with_client_retorna_erro() {
289        let cliente = ClienteFakeScp {
290            upload_ok: true,
291            download_ok: false,
292            bytes_upload: 0,
293            bytes_download: 0,
294        };
295        let registro = registro_teste("vps-d");
296
297        let resultado = executar_scp_download_with_client(
298            &registro,
299            Path::new("/tmp/remote.txt"),
300            Path::new("/tmp/local.txt"),
301            Box::new(cliente),
302        )
303        .await;
304
305        assert!(resultado.is_err());
306    }
307
308    #[tokio::test]
309    #[serial]
310    async fn executar_scp_upload_tenta_conectar_quando_vps_existe() {
311        let tmp = TempDir::new().expect("tempdir");
312        salvar_config_com_vps(&tmp, "vps-upload");
313
314        let resultado = executar_scp(
315            AcaoScp::Upload {
316                vps_nome: "vps-upload".to_string(),
317                local: tmp.path().join("arquivo-local.txt"),
318                remote: PathBuf::from("/tmp/arquivo-remoto.txt"),
319                password: None,
320            },
321            Some(tmp.path().to_path_buf()),
322            None,
323        )
324        .await;
325
326        assert!(resultado.is_err());
327    }
328
329    #[tokio::test]
330    #[serial]
331    async fn executar_scp_download_tenta_conectar_quando_vps_existe() {
332        let tmp = TempDir::new().expect("tempdir");
333        salvar_config_com_vps(&tmp, "vps-download");
334
335        let resultado = executar_scp(
336            AcaoScp::Download {
337                vps_nome: "vps-download".to_string(),
338                remote: PathBuf::from("/tmp/arquivo-remoto.txt"),
339                local: tmp.path().join("arquivo-local.txt"),
340                password: None,
341            },
342            Some(tmp.path().to_path_buf()),
343            None,
344        )
345        .await;
346
347        assert!(resultado.is_err());
348    }
349}