1use 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
12pub 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(®istro, &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(®istro, &remote, &local, cliente).await?;
60 }
61 }
62 Ok(())
63}
64
65pub 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
81pub 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 ®istro,
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 ®istro,
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 ®istro,
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 ®istro,
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}