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