1use crate::cli::AcaoScp;
7use crate::erros::ErroSshCli;
8use crate::i18n::{self, Mensagem};
9use crate::output;
10use crate::ssh::cliente::{ClienteSsh, ClienteSshTrait};
11use crate::vps;
12use std::path::PathBuf;
13
14#[derive(Debug, Default, Clone)]
16pub struct OpcoesScp {
17 pub password: Option<String>,
19 pub key: Option<String>,
21 pub key_passphrase: Option<String>,
23 pub timeout: Option<u64>,
25 pub replace_host_key: bool,
27 pub json: bool,
29}
30
31pub async fn executar_scp(
33 acao: AcaoScp,
34 config_override: Option<PathBuf>,
35 opts: OpcoesScp,
36) -> anyhow::Result<()> {
37 if crate::signals::cancelado() {
38 return Err(anyhow::anyhow!(i18n::t(Mensagem::OperacaoCancelada)));
39 }
40
41 match acao {
42 AcaoScp::Upload {
43 vps_nome,
44 local,
45 remote,
46 ..
47 } => {
48 if local.is_dir() {
50 return Err(ErroSshCli::ArgumentoInvalido(i18n::t(
51 Mensagem::ScpUploadSomenteArquivo,
52 ))
53 .into());
54 }
55 if !local.is_file() {
56 return Err(ErroSshCli::ArquivoNaoEncontrado(local.display().to_string()).into());
57 }
58
59 let mut registro = vps::buscar_por_nome(config_override.clone(), &vps_nome)?
60 .ok_or_else(|| ErroSshCli::VpsNaoEncontrada(vps_nome.clone()))?;
61
62 aplicar_opcoes_scp(&mut registro, &opts);
63
64 let caminho = crate::vps::resolver_caminho_config(config_override.clone())?;
65 let cfg = crate::vps::construir_configuracao(
66 ®istro,
67 Some(&caminho),
68 opts.replace_host_key,
69 );
70
71 let cliente: Box<dyn ClienteSshTrait> =
72 <ClienteSsh as ClienteSshTrait>::conectar(cfg).await?;
73 executar_scp_upload_with_client(&vps_nome, &local, &remote, cliente, opts.json).await?;
74 }
75 AcaoScp::Download {
76 vps_nome,
77 remote,
78 local,
79 ..
80 } => {
81 if local.is_dir() {
82 return Err(ErroSshCli::ArgumentoInvalido(i18n::t(
83 Mensagem::ScpDownloadLocalNaoDiretorio,
84 ))
85 .into());
86 }
87
88 let mut registro = vps::buscar_por_nome(config_override.clone(), &vps_nome)?
89 .ok_or_else(|| ErroSshCli::VpsNaoEncontrada(vps_nome.clone()))?;
90
91 aplicar_opcoes_scp(&mut registro, &opts);
92
93 let caminho = crate::vps::resolver_caminho_config(config_override.clone())?;
94 let cfg = crate::vps::construir_configuracao(
95 ®istro,
96 Some(&caminho),
97 opts.replace_host_key,
98 );
99
100 let cliente: Box<dyn ClienteSshTrait> =
101 <ClienteSsh as ClienteSshTrait>::conectar(cfg).await?;
102 executar_scp_download_with_client(&vps_nome, &remote, &local, cliente, opts.json)
103 .await?;
104 }
105 }
106 Ok(())
107}
108
109fn aplicar_opcoes_scp(registro: &mut crate::vps::modelo::VpsRegistro, opts: &OpcoesScp) {
110 if let Some(ref pwd) = opts.password {
111 registro.senha = secrecy::SecretString::from(pwd.clone());
112 }
113 if let Some(ref k) = opts.key {
114 registro.key_path = Some(k.clone());
115 }
116 if let Some(ref kp) = opts.key_passphrase {
117 registro.key_passphrase = Some(secrecy::SecretString::from(kp.clone()));
118 }
119 if let Some(t) = opts.timeout {
120 registro.timeout_ms = t;
121 }
122}
123
124pub async fn executar_scp_upload_with_client(
126 vps_nome: &str,
127 local: &std::path::Path,
128 remote: &std::path::Path,
129 mut cliente: Box<dyn ClienteSshTrait>,
130 json: bool,
131) -> anyhow::Result<()> {
132 let resultado = cliente.upload(local, remote).await?;
133 cliente.desconectar().await?;
134 if json {
135 output::imprimir_transferencia_json(
136 "upload",
137 vps_nome,
138 &local.display().to_string(),
139 &remote.display().to_string(),
140 resultado.bytes_transferidos,
141 resultado.duracao_ms,
142 );
143 } else {
144 output::imprimir_sucesso(&i18n::t(Mensagem::ScpUploadConcluido {
145 bytes: resultado.bytes_transferidos,
146 ms: resultado.duracao_ms,
147 }));
148 }
149 Ok(())
150}
151
152pub async fn executar_scp_download_with_client(
154 vps_nome: &str,
155 remote: &std::path::Path,
156 local: &std::path::Path,
157 mut cliente: Box<dyn ClienteSshTrait>,
158 json: bool,
159) -> anyhow::Result<()> {
160 let resultado = cliente.download(remote, local).await?;
161 cliente.desconectar().await?;
162 if json {
163 output::imprimir_transferencia_json(
164 "download",
165 vps_nome,
166 &local.display().to_string(),
167 &remote.display().to_string(),
168 resultado.bytes_transferidos,
169 resultado.duracao_ms,
170 );
171 } else {
172 output::imprimir_sucesso(&i18n::t(Mensagem::ScpDownloadConcluido {
173 bytes: resultado.bytes_transferidos,
174 ms: resultado.duracao_ms,
175 }));
176 }
177 Ok(())
178}
179
180#[cfg(test)]
181mod testes {
182 use super::*;
183 use crate::erros::ErroSshCli;
184 use crate::ssh::cliente::{
185 CanalTunel, ConfiguracaoConexao, SaidaExecucao, TransferenciaResultado,
186 };
187 use crate::vps::modelo::{VpsRegistro, SCHEMA_VERSION_ATUAL};
188 use crate::vps::{self, ArquivoConfig};
189 use async_trait::async_trait;
190 use secrecy::SecretString;
191 use serial_test::serial;
192 use std::collections::BTreeMap;
193 use std::path::Path;
194 use tempfile::TempDir;
195
196 struct ClienteFakeScp {
197 upload_ok: bool,
198 download_ok: bool,
199 bytes_upload: u64,
200 bytes_download: u64,
201 }
202
203 #[async_trait]
204 impl ClienteSshTrait for ClienteFakeScp {
205 async fn conectar(_cfg: ConfiguracaoConexao) -> Result<Box<Self>, ErroSshCli> {
206 Err(ErroSshCli::ConexaoFalhou(
207 "não implementado em teste".to_string(),
208 ))
209 }
210
211 async fn executar_comando(
212 &mut self,
213 _cmd: &str,
214 _max_chars: usize,
215 _stdin_data: Option<Vec<u8>>,
216 ) -> Result<SaidaExecucao, ErroSshCli> {
217 Err(ErroSshCli::CanalFalhou(
218 "não implementado em teste".to_string(),
219 ))
220 }
221
222 async fn upload(
223 &mut self,
224 _local: &Path,
225 _remote: &Path,
226 ) -> Result<TransferenciaResultado, ErroSshCli> {
227 if self.upload_ok {
228 Ok(TransferenciaResultado {
229 bytes_transferidos: self.bytes_upload,
230 duracao_ms: 10,
231 })
232 } else {
233 Err(ErroSshCli::CanalFalhou("upload falhou".to_string()))
234 }
235 }
236
237 async fn download(
238 &mut self,
239 _remote: &Path,
240 _local: &Path,
241 ) -> Result<TransferenciaResultado, ErroSshCli> {
242 if self.download_ok {
243 Ok(TransferenciaResultado {
244 bytes_transferidos: self.bytes_download,
245 duracao_ms: 20,
246 })
247 } else {
248 Err(ErroSshCli::CanalFalhou("download falhou".to_string()))
249 }
250 }
251
252 async fn abrir_canal_tunel(
253 &self,
254 _host_remoto: &str,
255 _porta_remota: u16,
256 _endereco_origem: &str,
257 _porta_origem: u16,
258 ) -> Result<Box<dyn CanalTunel>, ErroSshCli> {
259 Err(ErroSshCli::CanalFalhou(
260 "não implementado em teste".to_string(),
261 ))
262 }
263
264 async fn desconectar(&self) -> Result<(), ErroSshCli> {
265 Ok(())
266 }
267 }
268
269 fn registro_teste(nome: &str) -> VpsRegistro {
270 VpsRegistro::novo(
271 nome.to_string(),
272 "127.0.0.1".to_string(),
273 1,
274 "root".to_string(),
275 SecretString::from("senha-teste".to_string()),
276 None,
277 None,
278 Some(100),
279 Some(1000),
280 Some(1000),
281 None,
282 None,
283 false,
284 )
285 }
286
287 fn salvar_config_com_vps(tmp: &TempDir, nome: &str) {
288 let mut hosts = BTreeMap::new();
289 hosts.insert(nome.to_string(), registro_teste(nome));
290 let arquivo = ArquivoConfig {
291 schema_version: SCHEMA_VERSION_ATUAL,
292 hosts,
293 };
294 let caminho = tmp.path().join("config.toml");
295 vps::salvar(&caminho, &arquivo).expect("salvar config teste");
296 }
297
298 #[tokio::test]
299 async fn executar_scp_upload_with_client_retorna_ok() {
300 let cliente = Box::new(ClienteFakeScp {
301 upload_ok: true,
302 download_ok: true,
303 bytes_upload: 128,
304 bytes_download: 0,
305 });
306 let local = Path::new("/tmp/local.txt");
307 let remote = Path::new("/tmp/remote.txt");
308 let resultado = executar_scp_upload_with_client("v1", local, remote, cliente, false).await;
309 assert!(resultado.is_ok());
310 }
311
312 #[tokio::test]
313 async fn executar_scp_download_with_client_retorna_ok() {
314 let cliente = Box::new(ClienteFakeScp {
315 upload_ok: true,
316 download_ok: true,
317 bytes_upload: 0,
318 bytes_download: 256,
319 });
320 let resultado = executar_scp_download_with_client(
321 "v1",
322 Path::new("/tmp/remote.txt"),
323 Path::new("/tmp/local.txt"),
324 cliente,
325 false,
326 )
327 .await;
328 assert!(resultado.is_ok());
329 }
330
331 #[tokio::test]
332 async fn executar_scp_upload_with_client_retorna_erro() {
333 let cliente = Box::new(ClienteFakeScp {
334 upload_ok: false,
335 download_ok: true,
336 bytes_upload: 0,
337 bytes_download: 0,
338 });
339 let resultado = executar_scp_upload_with_client(
340 "v1",
341 Path::new("/tmp/local.txt"),
342 Path::new("/tmp/remote.txt"),
343 cliente,
344 false,
345 )
346 .await;
347 assert!(resultado.is_err());
348 }
349
350 #[tokio::test]
351 async fn executar_scp_download_with_client_retorna_erro() {
352 let cliente = Box::new(ClienteFakeScp {
353 upload_ok: true,
354 download_ok: false,
355 bytes_upload: 0,
356 bytes_download: 0,
357 });
358 let resultado = executar_scp_download_with_client(
359 "v1",
360 Path::new("/tmp/remote.txt"),
361 Path::new("/tmp/local.txt"),
362 cliente,
363 false,
364 )
365 .await;
366 assert!(resultado.is_err());
367 }
368
369 #[tokio::test]
370 #[serial]
371 async fn executar_scp_upload_tenta_conectar_quando_vps_existe() {
372 let tmp = TempDir::new().unwrap();
373 salvar_config_com_vps(&tmp, "vps-upload");
374 let local = tmp.path().join("local.bin");
375 std::fs::write(&local, b"abc").unwrap();
376 let acao = AcaoScp::Upload {
377 vps_nome: "vps-upload".to_string(),
378 local,
379 remote: PathBuf::from("/tmp/x"),
380 password: None,
381 password_stdin: false,
382 key: None,
383 key_passphrase: None,
384 key_passphrase_stdin: false,
385 timeout: Some(100),
386 json: false,
387 };
388 let r = executar_scp(
389 acao,
390 Some(tmp.path().to_path_buf()),
391 OpcoesScp {
392 timeout: Some(100),
393 ..Default::default()
394 },
395 )
396 .await;
397 assert!(r.is_err());
399 }
400
401 #[tokio::test]
402 #[serial]
403 async fn executar_scp_download_tenta_conectar_quando_vps_existe() {
404 let tmp = TempDir::new().unwrap();
405 salvar_config_com_vps(&tmp, "vps-download");
406 let acao = AcaoScp::Download {
407 vps_nome: "vps-download".to_string(),
408 remote: PathBuf::from("/tmp/x"),
409 local: tmp.path().join("out.bin"),
410 password: None,
411 password_stdin: false,
412 key: None,
413 key_passphrase: None,
414 key_passphrase_stdin: false,
415 timeout: Some(100),
416 json: false,
417 };
418 let r = executar_scp(
419 acao,
420 Some(tmp.path().to_path_buf()),
421 OpcoesScp {
422 timeout: Some(100),
423 ..Default::default()
424 },
425 )
426 .await;
427 assert!(r.is_err());
428 }
429
430 #[tokio::test]
431 #[serial]
432 async fn executar_scp_upload_rejeita_diretorio() {
433 let tmp = TempDir::new().unwrap();
434 salvar_config_com_vps(&tmp, "vps-dir");
435 let acao = AcaoScp::Upload {
436 vps_nome: "vps-dir".to_string(),
437 local: tmp.path().to_path_buf(),
438 remote: PathBuf::from("/tmp/x"),
439 password: None,
440 password_stdin: false,
441 key: None,
442 key_passphrase: None,
443 key_passphrase_stdin: false,
444 timeout: None,
445 json: false,
446 };
447 let r = executar_scp(acao, Some(tmp.path().to_path_buf()), OpcoesScp::default()).await;
448 assert!(r.is_err());
449 let msg = format!("{:?}", r.err().unwrap());
450 assert!(
451 msg.contains("regular files")
452 || msg.contains("arquivos regulares")
453 || msg.contains("ArgumentoInvalido"),
454 "msg={msg}"
455 );
456 }
457}