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