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