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