1use std::{env, fs};
2use std::fs::File;
3use std::io::Cursor;
4use dotenv::from_path;
5use serde::{Deserialize, Serialize};
6use crate::{paths, PisPasResult};
7
8pub async fn install_sharing(file_path: &str, args: &[&str]) -> std::process::ExitStatus {
9 let full_command = format!(
11 "Start-Process '{}' -ArgumentList '{}' -Wait",
12 file_path,
13 args.join(" ")
14 );
15
16 println!("Executing: {}", full_command);
17 let status = std::process::Command::new("powershell")
18 .arg("-Command")
19 .arg(&full_command)
20 .status()
21 .expect("No se pudo ejecutar el instalador");
22
23 println!("Install Script ended with status: {}", status);
24 status
25}
26
27#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
56pub struct ConfigEnv {
57 pub service_name: String,
60 pub service_vers: String,
62 pub pispas_host: String,
64 pub remote_host: String,
67 pub remote_port: u16,
69 pub remote_ussl: bool,
71 pub local_host: String,
74 pub local_port: u16,
76 pub local_ussl: bool,
80 pub modules: Vec<String>,
83 pub list_printers: Option<Vec<String>>,
86 pub place_id: Option<i64>,
92 pub objid: Option<i64>,
97}
98
99impl Default for ConfigEnv {
100 fn default() -> Self {
101 Self {
102 service_name: "local_service".to_string(),
103 service_vers: env::var("SERVICE_VERS").unwrap_or_else(|_| "1.0.0.0".to_string()),
104 pispas_host: env::var("PISPAS_HOST").unwrap_or_else(|_| "api.unpispas.es".to_string()),
105 remote_host: env::var("REMOTE_HOST").unwrap_or_else(|_| "wss.unpispas.es".to_string()),
106 remote_port: env::var("REMOTE_PORT")
107 .unwrap_or_else(|_| "443".to_string())
108 .parse()
109 .unwrap_or(443),
110 remote_ussl: env::var("REMOTE_USSL")
111 .unwrap_or_else(|_| "true".to_string())
112 .eq_ignore_ascii_case("true"),
113 local_host: env::var("LOCAL_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()),
114 local_port: env::var("LOCAL_PORT")
115 .unwrap_or_else(|_| "5005".to_string())
116 .parse()
117 .unwrap_or(5005),
118 local_ussl: env::var("LOCAL_USSL")
119 .unwrap_or_else(|_| "true".to_string())
120 .eq_ignore_ascii_case("true"),
121 modules: env::var("MODULES")
122 .unwrap_or_else(|_| "base,print".to_string()) .split(',')
124 .map(|s| s.trim().to_string())
125 .collect(),
126 list_printers: None, place_id: None,
128 objid: None,
129 }
130 }
131}
132
133impl ConfigEnv {
134 pub fn load() -> Self {
135 let env_path = paths::env_file_path();
136 tracing::info!("Loading cfg from {}", env_path.display());
137
138 if env_path.exists() {
140 from_path(&env_path).ok();
141 } else {
142 init_env();
143 }
144
145 Self {
146 service_name: env::var("SERVICE_NAME").unwrap_or_else(|_| "unpispas_pdfwritter".to_string()),
147 service_vers: env::var("SERVICE_VERS").unwrap_or_else(|_| "1.0.0.2".to_string()),
148 pispas_host: env::var("PISPAS_HOST").unwrap_or_else(|_| "api.unpispas.es".to_string()),
149 remote_host: env::var("REMOTE_HOST").unwrap_or_else(|_| "wss.unpispas.es".to_string()),
150 remote_port: env::var("REMOTE_PORT")
151 .unwrap_or_else(|_| "443".to_string())
152 .parse()
153 .unwrap_or(8765),
154 remote_ussl: env::var("REMOTE_USSL")
155 .unwrap_or_else(|_| "true".to_string())
156 .parse()
157 .unwrap_or(false),
158 local_host: env::var("LOCAL_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()),
159 local_port: env::var("LOCAL_PORT")
160 .unwrap_or_else(|_| "5005".to_string())
161 .parse()
162 .unwrap_or(5005),
163 local_ussl: env::var("LOCAL_USSL")
164 .unwrap_or_else(|_| "true".to_string())
165 .eq_ignore_ascii_case("true"),
166 modules: env::var("MODULES")
167 .unwrap_or_else(|_| "base,print".to_string()) .split(',')
169 .map(|s| s.trim().to_string())
170 .collect(), list_printers: None, place_id: env::var("PLACE_ID").ok().and_then(|s| s.parse::<i64>().ok()),
173 objid: env::var("OBJID").ok().and_then(|s| s.parse::<i64>().ok()),
174 }
175 }
176
177 pub fn set_place_id(&mut self, new_place_id: i64) -> std::result::Result<(), String> {
182 match self.place_id {
183 Some(existing) if existing == new_place_id => Ok(()),
184 Some(existing) => Err(format!(
185 "place_id already set to {existing}; refusing overwrite to {new_place_id}"
186 )),
187 None => {
188 self.place_id = Some(new_place_id);
189 self.save();
190 Ok(())
191 }
192 }
193 }
194
195 pub fn set_objid(&mut self, new_objid: i64) {
200 self.objid = Some(new_objid);
201 self.save();
202 }
203
204 pub fn forget_identity(&mut self) {
209 self.place_id = None;
210 self.objid = None;
211 self.save();
212 }
213
214 pub fn change_service_name(&mut self, new_name: &str) {
215 self.service_name = new_name.to_string();
216 }
217
218 pub fn change_service_vers(&mut self, new_vers: &str) {
219 self.service_vers = new_vers.to_string();
220 }
221
222 pub fn change_pispas_host(&mut self, new_host: &str) {
223 self.pispas_host = new_host.to_string();
224 }
225
226 pub fn change_remote_host(&mut self, new_host: &str) {
227 self.remote_host = new_host.to_string();
228 }
229
230 pub fn change_remote_port(&mut self, new_port: u16) {
231 self.remote_port = new_port;
232 }
233
234 pub fn change_remote_ussl(&mut self, ussl: bool) {
235 self.remote_ussl = ussl;
236 }
237
238 pub fn change_local_host(&mut self, new_host: &str) {
239 self.local_host = new_host.to_string();
240 }
241
242 pub fn change_local_port(&mut self, new_port: u16) {
243 self.local_port = new_port;
244 }
245
246 pub fn change_local_ussl(&mut self, ussl: bool) {
247 self.local_ussl = ussl;
248 }
249
250 pub fn change_modules(&mut self, new_modules: Vec<String>) {
251 self.modules = new_modules;
252 }
253
254 pub fn save(&self) {
255 let list_printers_str = self.list_printers.as_ref().map_or("".to_string(), |printers| printers.join(","));
256 let place_id_str = self.place_id.map(|v| v.to_string()).unwrap_or_default();
259 let objid_str = self.objid.map(|v| v.to_string()).unwrap_or_default();
260 tracing::info!("Saving config: {:?}", self);
261 let env_content = format!(
262 "SERVICE_NAME={}\nSERVICE_VERS={}\nPISPAS_HOST={}\nREMOTE_HOST={}\nREMOTE_PORT={}\nREMOTE_USSL={}\nLOCAL_HOST={}\nLOCAL_PORT={}\nLOCAL_USSL={}\nMODULES={}\nLIST_PRINTERS={}\nPLACE_ID={}\nOBJID={}\n",
263 self.service_name,
264 self.service_vers,
265 self.pispas_host,
266 self.remote_host,
267 self.remote_port,
268 self.remote_ussl,
269 self.local_host,
270 self.local_port,
271 self.local_ussl,
272 self.modules.join(","),
273 list_printers_str,
274 place_id_str,
275 objid_str,
276 );
277
278 let env_path = crate::paths::env_file_path();
279
280 if !paths::bin_dir().exists() {
281 fs::create_dir_all(paths::bin_dir()).unwrap();
282 }
283
284 println!("Saving config to {}", env_path.display());
285
286 match fs::write(&env_path, env_content) {
287 Ok(_) => tracing::info!("Config saved successfully to {}", env_path.display()),
288 Err(e) => {
289 println!("Failed to save config to {}: {}", env_path.display(), e);
290 tracing::error!("Failed to save config to {}: {}", env_path.display(), e)
291 },
292 }
293 }
294}
295fn init_env() {
296 let bin_dir = paths::bin_dir();
297 let env_path = bin_dir.join(".env");
298
299 let service_name = crate::natives::api::get_name_service();
300
301
302 if !env_path.exists() {
303 let default_env_content = format!(r#"SERVICE_NAME={}
304SERVICE_VERS=1.0.0.3
305LOCAL_HOST=127.0.0.1
306LOCAL_PORT=5005
307LOCAL_USSL=true
308REMOTE_USSL=true
309REMOTE_HOST=wss.unpispas.es
310REMOTE_PORT=443
311PISPAS_HOST=api.unpispas.es
312MODULES=base,print
313LIST_PRINTERS=POS80, CommandViewer
314"#, service_name);
315 if !bin_dir.exists() {
316 match fs::create_dir_all(&bin_dir) {
317 Ok(_) => tracing::info!("Created bin directory at {}", bin_dir.display()),
318 Err(e) => {
319 tracing::error!("Failed to create bin directory at {}: {}", bin_dir.display(), e);
320 }
321 }
322 }
323
324 match fs::write(&env_path, default_env_content) {
325 Ok(_) => tracing::info!("Created default .env file at {}", env_path.display()),
326 Err(e) => {
327 tracing::error!("Failed to create .env file at {}: {}", env_path.display(), e);
328 }
329 }
330 }
331
332 from_path(env_path).ok();
333}
334
335
336
337pub fn open_folder(path: &str) {
338 let _ = std::process::Command::new("cmd")
339 .args(&["/C", "explorer", &path])
340 .spawn();
341}
342
343
344
345
346
347fn get_last_modified_time(path: &std::path::Path) -> std::io::Result<std::time::SystemTime> {
348 let metadata = fs::metadata(path)?;
349 metadata.modified()
350}
351
352pub fn delete_folders_with_prefix(path: &str, prefix: &str) -> std::io::Result<()> {
353 let system32_path = std::path::Path::new(path);
354 let mut latest_folder: Option<(std::time::SystemTime, std::path::PathBuf)> = None;
355
356 for entry in fs::read_dir(system32_path)? {
358 let entry = entry?;
359 let path = entry.path();
360
361 if path.is_dir() {
363 if let Some(folder_name) = path.file_name() {
364 if let Some(folder_name_str) = folder_name.to_str() {
365 if folder_name_str.starts_with(prefix) {
366 let modified_time = get_last_modified_time(&path)?;
367
368 if let Some((latest_time, _)) = &latest_folder {
370 if modified_time > *latest_time {
371 if let Some((_, old_path)) = latest_folder.take() {
373 tracing::info!("Deleting older folder => {}", old_path.display());
374 fs::remove_dir_all(&old_path)?;
375 }
376 latest_folder = Some((modified_time, path));
378 } else {
379 tracing::info!("Deleting older folder => {}", path.display());
381 fs::remove_dir_all(&path)?;
382 }
383 } else {
384 latest_folder = Some((modified_time, path));
386 }
387 }
388 }
389 }
390 }
391 }
392
393 Ok(())
394}
395
396use include_dir::{include_dir, Dir};
397use zip::read::ZipArchive;
398pub const RESOURCES_WIN: Dir = include_dir!("resources/win");
399pub const RESOURCES_TOOLS: Dir = include_dir!("resources/tools");
400
401pub fn extract_all_resources(destination: &std::path::Path, resources: Dir) -> PisPasResult<()> {
402 for file in resources.files() {
403 let file_path = file.path();
404
405 let final_destination = match file_path.file_name() {
406 Some(filename) if filename == crate::SERVICE_PYTHON_NAME =>
407 destination.parent().unwrap_or(destination).join(filename),
408 Some(filename) => destination.join(filename),
409 None => continue,
410 };
411
412 if let Some(parent) = final_destination.parent() {
413 if !parent.exists() {
414 fs::create_dir_all(parent)?;
415 }
416 }
417
418 if file_path.extension().map_or(false, |ext| ext == "zip") {
419 tracing::info!("Extracting zip file => {file_path:?}");
420 let zip_destination = final_destination.parent().unwrap_or(destination);
422
423 let reader = Cursor::new(file.contents());
424 let mut archive = ZipArchive::new(reader)?;
425
426 for i in 0..archive.len() {
427 let mut zip_file = archive.by_index(i)?;
428 let outpath = zip_destination.join(zip_file.mangled_name());
429
430 if (&*zip_file.name()).ends_with('/') {
431 fs::create_dir_all(&outpath)?;
432 } else {
433 if let Some(p) = outpath.parent() {
434 if !p.exists() {
435 fs::create_dir_all(&p)?;
436 }
437 }
438 let mut outfile = File::create(&outpath)?;
439 std::io::copy(&mut zip_file, &mut outfile)?;
440 }
441 }
442 } else {
443 if final_destination.exists() {
444 let content = fs::read(&final_destination)?;
445
446 if content != file.contents() {
447 tracing::info!("Updating file => {:?}", final_destination);
448 fs::write(&final_destination, file.contents())?;
449 } else {
450 tracing::info!("File already up-to-date => {:?}", final_destination);
451 }
452 } else {
453 tracing::info!("Extracting new file => {:?}", final_destination);
454 fs::write(&final_destination, file.contents())?;
455 }
456 }
457 }
458
459 for dir in resources.dirs() {
461 let dir_name = dir.path().file_name().unwrap_or_default();
462 let final_destination = destination.join(dir_name);
463 if !final_destination.exists() {
464 fs::create_dir_all(&final_destination)?;
465 }
466 extract_all_resources(&final_destination, dir.clone())?;
467 }
468
469 Ok(())
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475 use std::fs;
476
477 #[test]
478 fn test_config_env_default() {
479 let config = ConfigEnv::default();
480
481 assert_eq!(config.service_name, "local_service");
482 assert!(!config.pispas_host.is_empty());
483 assert!(!config.remote_host.is_empty());
484 assert!(config.remote_port > 0);
485 assert!(config.local_port > 0);
486 assert!(!config.modules.is_empty());
487 assert!(config.place_id.is_none(), "place_id starts unset (PRE-BOOTSTRAP)");
488 assert!(config.objid.is_none(), "objid starts unset (PRE-BOOTSTRAP)");
489 }
490
491 #[test]
492 fn test_set_place_id_first_write_wins() {
493 let mut config = ConfigEnv::default();
494 assert!(config.set_place_id(5).is_ok());
496 assert_eq!(config.place_id, Some(5));
497 assert!(config.set_place_id(5).is_ok());
499 assert!(config.set_place_id(99).is_err());
501 assert_eq!(config.place_id, Some(5), "rejected write must not clobber");
502 }
503
504 #[test]
505 fn test_forget_identity_clears_bootstrap_fields() {
506 let mut config = ConfigEnv::default();
507 let _ = config.set_place_id(5);
508 config.set_objid(42);
509 assert!(config.place_id.is_some() && config.objid.is_some());
510 config.forget_identity();
511 assert!(config.place_id.is_none());
512 assert!(config.objid.is_none());
513 }
514
515 #[test]
516 fn test_config_env_change_service_name() {
517 let mut config = ConfigEnv::default();
518 let new_name = "new_service_name";
519
520 config.change_service_name(new_name);
521 assert_eq!(config.service_name, new_name);
522 }
523
524 #[test]
525 fn test_config_env_change_service_vers() {
526 let mut config = ConfigEnv::default();
527 let new_vers = "2.0.0";
528
529 config.change_service_vers(new_vers);
530 assert_eq!(config.service_vers, new_vers);
531 }
532
533 #[test]
534 fn test_config_env_change_pispas_host() {
535 let mut config = ConfigEnv::default();
536 let new_host = "new.api.example.com";
537
538 config.change_pispas_host(new_host);
539 assert_eq!(config.pispas_host, new_host);
540 }
541
542 #[test]
543 fn test_config_env_change_remote_host() {
544 let mut config = ConfigEnv::default();
545 let new_host = "new.remote.example.com";
546
547 config.change_remote_host(new_host);
548 assert_eq!(config.remote_host, new_host);
549 }
550
551 #[test]
552 fn test_config_env_change_remote_port() {
553 let mut config = ConfigEnv::default();
554 let new_port = 8080;
555
556 config.change_remote_port(new_port);
557 assert_eq!(config.remote_port, new_port);
558 }
559
560 #[test]
561 fn test_config_env_change_remote_ussl() {
562 let mut config = ConfigEnv::default();
563
564 config.change_remote_ussl(false);
565 assert_eq!(config.remote_ussl, false);
566
567 config.change_remote_ussl(true);
568 assert_eq!(config.remote_ussl, true);
569 }
570
571 #[test]
572 fn test_config_env_change_local_host() {
573 let mut config = ConfigEnv::default();
574 let new_host = "192.168.1.1";
575
576 config.change_local_host(new_host);
577 assert_eq!(config.local_host, new_host);
578 }
579
580 #[test]
581 fn test_config_env_change_local_port() {
582 let mut config = ConfigEnv::default();
583 let new_port = 9000;
584
585 config.change_local_port(new_port);
586 assert_eq!(config.local_port, new_port);
587 }
588
589 #[test]
590 fn test_config_env_change_modules() {
591 let mut config = ConfigEnv::default();
592 let new_modules = vec!["module1".to_string(), "module2".to_string()];
593
594 config.change_modules(new_modules.clone());
595 assert_eq!(config.modules, new_modules);
596 }
597
598 #[test]
599 fn test_config_env_default_modules_parsing() {
600 let config = ConfigEnv::default();
601 assert!(!config.modules.is_empty());
603 }
604
605 #[test]
606 fn test_delete_folders_with_prefix() {
607 let temp_dir = std::env::temp_dir().join("test_delete_prefix");
609 let prefix = "test-prefix-";
610
611 let _ = fs::remove_dir_all(&temp_dir);
613 fs::create_dir_all(&temp_dir).unwrap();
614
615 let folder1 = temp_dir.join(format!("{}folder1", prefix));
617 let folder2 = temp_dir.join(format!("{}folder2", prefix));
618 let other_folder = temp_dir.join("other-folder");
619
620 fs::create_dir_all(&folder1).unwrap();
621 fs::create_dir_all(&folder2).unwrap();
622 fs::create_dir_all(&other_folder).unwrap();
623
624 let file1 = folder1.join("file.txt");
626 fs::write(&file1, "content").unwrap();
627
628 std::thread::sleep(std::time::Duration::from_millis(100));
630
631 let file2 = folder2.join("file.txt");
633 fs::write(&file2, "content").unwrap();
634
635 let result = delete_folders_with_prefix(temp_dir.to_str().unwrap(), prefix);
637 assert!(result.is_ok(), "delete_folders_with_prefix should succeed");
638
639 let entries: Vec<_> = fs::read_dir(&temp_dir)
641 .unwrap()
642 .filter_map(|e| e.ok())
643 .collect();
644
645 let prefix_folders: Vec<_> = entries
646 .iter()
647 .filter(|e| {
648 e.path().is_dir() &&
649 e.path().file_name()
650 .and_then(|n| n.to_str())
651 .map(|s| s.starts_with(prefix))
652 .unwrap_or(false)
653 })
654 .collect();
655
656 assert!(prefix_folders.len() <= 1, "Should keep at most one folder with prefix");
658
659 assert!(other_folder.exists(), "Non-prefix folder should not be deleted");
661
662 let _ = fs::remove_dir_all(&temp_dir);
664 }
665}
666