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 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(®istro);
39
40 let cliente: Box<dyn ClienteSshTrait> =
41 <ClienteSsh as ClienteSshTrait>::conectar(cfg).await?;
42 executar_scp_upload_with_client(®istro, &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(®istro);
58
59 let cliente: Box<dyn ClienteSshTrait> =
60 <ClienteSsh as ClienteSshTrait>::conectar(cfg).await?;
61 executar_scp_download_with_client(®istro, &remote, &local, cliente).await?;
62 }
63 }
64 Ok(())
65}
66
67pub 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
83pub 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 ®istro,
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 ®istro,
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 ®istro,
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 ®istro,
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}