sharing/
utils.rs

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    // Construye el comando completo en una sola cadena
10    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/// Runtime configuration loaded from the install's `.env` file.
28///
29/// `ConfigEnv` is the single struct that every binary reads at startup.
30/// Fields are plain POD (strings, booleans, ports) so the type is trivially
31/// serializable to JSON for the Tauri configurator UI.
32///
33/// ## Lifecycle
34///
35/// 1. The installer writes a default `.env` via `init_env` on first run.
36/// 2. Each binary calls [`ConfigEnv::load`] at startup, which reads the
37///    `.env` through `dotenv::from_path` and materialises the struct.
38/// 3. The configurator calls [`ConfigEnv::save`] after the user edits a
39///    field, then pushes an IPC restart message to `pispas-modules`.
40///
41/// ## Invariants
42///
43/// * `local_ussl = true` requires the embedded TLS cert and a client that
44///   connects via `wss://local.unpispas.es:<local_port>`. Binding to
45///   `127.0.0.1` is fine because DNS resolves `local.unpispas.es` there.
46/// * `modules` is an ordered list used by
47///   `pispas_modules::load_services` to instantiate the services. Names
48///   not in the match arm are logged and ignored.
49///
50/// ## Extending
51///
52/// Adding a field requires touching four places — see `CLAUDE.md § 5`.
53/// Forgetting one of them silently resets the new field to its default
54/// every time the user saves from the configurator.
55#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
56pub struct ConfigEnv {
57    /// Stable identity of this install. Used as the Windows Service name
58    /// and echoed back in every WebSocket message envelope.
59    pub service_name: String,
60    /// Build version tag, echoed in message envelopes for log correlation.
61    pub service_vers: String,
62    /// Backend REST API hostname (e.g. `api.unpispas.es`).
63    pub pispas_host: String,
64    /// Backend WebSocket hostname that `pispas-modules` keeps an outbound
65    /// connection to (e.g. `wss.unpispas.es`).
66    pub remote_host: String,
67    /// Backend WebSocket port. Usually `443`.
68    pub remote_port: u16,
69    /// `true` → connect with `wss://`, `false` → `ws://`.
70    pub remote_ussl: bool,
71    /// Bind address for the local WebSocket server. Almost always
72    /// `127.0.0.1`. See `docs/CONFIGURATION.md` before changing.
73    pub local_host: String,
74    /// Bind port for the local WebSocket server. Default `5005`.
75    pub local_port: u16,
76    /// `true` → accept TLS on the local socket (recommended for browser
77    /// clients that require `wss://`). The listener is dual-mode and still
78    /// accepts plain `ws://` when this is on.
79    pub local_ussl: bool,
80    /// Ordered list of service modules to load at boot
81    /// (`base`, `print`, `paytef`, …).
82    pub modules: Vec<String>,
83    /// Cached printer list shown in the configurator UI. Refreshed by the
84    /// service on demand.
85    pub list_printers: Option<Vec<String>>,
86}
87
88impl Default for ConfigEnv {
89    fn default() -> Self {
90        Self {
91            service_name: "local_service".to_string(),
92            service_vers: env::var("SERVICE_VERS").unwrap_or_else(|_| "1.0.0.0".to_string()),
93            pispas_host: env::var("PISPAS_HOST").unwrap_or_else(|_| "api.unpispas.es".to_string()),
94            remote_host: env::var("REMOTE_HOST").unwrap_or_else(|_| "wss.unpispas.es".to_string()),
95            remote_port: env::var("REMOTE_PORT")
96                .unwrap_or_else(|_| "443".to_string())
97                .parse()
98                .unwrap_or(443),
99            remote_ussl: env::var("REMOTE_USSL")
100                .unwrap_or_else(|_| "true".to_string())
101                .eq_ignore_ascii_case("true"),
102            local_host: env::var("LOCAL_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()),
103            local_port: env::var("LOCAL_PORT")
104                .unwrap_or_else(|_| "5005".to_string())
105                .parse()
106                .unwrap_or(5005),
107            local_ussl: env::var("LOCAL_USSL")
108                .unwrap_or_else(|_| "true".to_string())
109                .eq_ignore_ascii_case("true"),
110            modules: env::var("MODULES")
111                .unwrap_or_else(|_| "base,print".to_string()) // Valor predeterminado
112                .split(',')
113                .map(|s| s.trim().to_string())
114                .collect(),
115            list_printers: None, // Inicialmente vacío
116        }
117    }
118}
119
120impl ConfigEnv {
121    pub fn load() -> Self {
122        let env_path = paths::env_file_path();
123        tracing::info!("Loading cfg from {}", env_path.display());
124
125        // Carga el archivo .env desde el directorio bin
126        if env_path.exists() {
127            from_path(&env_path).ok();
128        } else {
129            init_env();
130        }
131
132        Self {
133            service_name: env::var("SERVICE_NAME").unwrap_or_else(|_| "unpispas_pdfwritter".to_string()),
134            service_vers: env::var("SERVICE_VERS").unwrap_or_else(|_| "1.0.0.2".to_string()),
135            pispas_host: env::var("PISPAS_HOST").unwrap_or_else(|_| "api.unpispas.es".to_string()),
136            remote_host: env::var("REMOTE_HOST").unwrap_or_else(|_| "wss.unpispas.es".to_string()),
137            remote_port: env::var("REMOTE_PORT")
138                .unwrap_or_else(|_| "443".to_string())
139                .parse()
140                .unwrap_or(8765),
141            remote_ussl: env::var("REMOTE_USSL")
142                .unwrap_or_else(|_| "true".to_string())
143                .parse()
144                .unwrap_or(false),
145            local_host: env::var("LOCAL_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()),
146            local_port: env::var("LOCAL_PORT")
147                .unwrap_or_else(|_| "5005".to_string())
148                .parse()
149                .unwrap_or(5005),
150            local_ussl: env::var("LOCAL_USSL")
151                .unwrap_or_else(|_| "true".to_string())
152                .eq_ignore_ascii_case("true"),
153            modules: env::var("MODULES")
154                .unwrap_or_else(|_| "base,print".to_string()) // Valor predeterminado
155                .split(',')
156                .map(|s| s.trim().to_string())
157                .collect(), // Convierte a Vec<String>
158            list_printers: None, // Inicialmente vacío
159        }
160    }
161
162    pub fn change_service_name(&mut self, new_name: &str) {
163        self.service_name = new_name.to_string();
164    }
165
166    pub fn change_service_vers(&mut self, new_vers: &str) {
167        self.service_vers = new_vers.to_string();
168    }
169
170    pub fn change_pispas_host(&mut self, new_host: &str) {
171        self.pispas_host = new_host.to_string();
172    }
173
174    pub fn change_remote_host(&mut self, new_host: &str) {
175        self.remote_host = new_host.to_string();
176    }
177
178    pub fn change_remote_port(&mut self, new_port: u16) {
179        self.remote_port = new_port;
180    }
181
182    pub fn change_remote_ussl(&mut self, ussl: bool) {
183        self.remote_ussl = ussl;
184    }
185
186    pub fn change_local_host(&mut self, new_host: &str) {
187        self.local_host = new_host.to_string();
188    }
189
190    pub fn change_local_port(&mut self, new_port: u16) {
191        self.local_port = new_port;
192    }
193
194    pub fn change_local_ussl(&mut self, ussl: bool) {
195        self.local_ussl = ussl;
196    }
197
198    pub fn change_modules(&mut self, new_modules: Vec<String>) {
199        self.modules = new_modules;
200    }
201
202    pub fn save(&self) {
203        let list_printers_str = self.list_printers.as_ref().map_or("".to_string(), |printers| printers.join(","));
204        tracing::info!("Saving config: {:?}", self);
205        let env_content = format!(
206            "SERVICE_NAME={}\nSERVICE_VERS={}\nPISPAS_HOST={}\nREMOTE_HOST={}\nREMOTE_PORT={}\nREMOTE_USSL={}\nLOCAL_HOST={}\nLOCAL_PORT={}\nLOCAL_USSL={}\nMODULES={}\nLIST_PRINTERS={}\n",
207            self.service_name,
208            self.service_vers,
209            self.pispas_host,
210            self.remote_host,
211            self.remote_port,
212            self.remote_ussl,
213            self.local_host,
214            self.local_port,
215            self.local_ussl,
216            self.modules.join(","),
217            list_printers_str
218        );
219
220        let env_path = crate::paths::env_file_path();
221
222        if !paths::bin_dir().exists() {
223            fs::create_dir_all(paths::bin_dir()).unwrap();
224        }
225        
226        println!("Saving config to {}", env_path.display());
227        
228        match fs::write(&env_path, env_content) {
229            Ok(_) => tracing::info!("Config saved successfully to {}", env_path.display()),
230            Err(e) => {
231                println!("Failed to save config to {}: {}", env_path.display(), e);
232                tracing::error!("Failed to save config to {}: {}", env_path.display(), e)
233            },
234        }        
235    }
236}
237fn init_env() {
238    let bin_dir = paths::bin_dir();
239    let env_path = bin_dir.join(".env");
240
241    let service_name =     crate::natives::api::get_name_service();
242    
243
244    if !env_path.exists() {
245        let default_env_content = format!(r#"SERVICE_NAME={}
246SERVICE_VERS=1.0.0.3
247LOCAL_HOST=127.0.0.1
248LOCAL_PORT=5005
249LOCAL_USSL=true
250REMOTE_USSL=true
251REMOTE_HOST=wss.unpispas.es
252REMOTE_PORT=443
253PISPAS_HOST=api.unpispas.es
254MODULES=base,print
255LIST_PRINTERS=POS80, CommandViewer
256"#, service_name);
257        if !bin_dir.exists() {
258            match fs::create_dir_all(&bin_dir) {
259                Ok(_) => tracing::info!("Created bin directory at {}", bin_dir.display()),
260                Err(e) => {
261                    tracing::error!("Failed to create bin directory at {}: {}", bin_dir.display(), e);                    
262                }
263            }
264        }
265
266        match fs::write(&env_path, default_env_content) {
267            Ok(_) => tracing::info!("Created default .env file at {}", env_path.display()),
268            Err(e) => {
269                tracing::error!("Failed to create .env file at {}: {}", env_path.display(), e);                
270            }
271        }
272    }
273
274    from_path(env_path).ok();
275}
276
277
278
279pub fn open_folder(path: &str) {
280    let _ = std::process::Command::new("cmd")
281        .args(&["/C", "explorer", &path])
282        .spawn();
283}
284
285
286
287
288
289fn get_last_modified_time(path: &std::path::Path) -> std::io::Result<std::time::SystemTime> {
290    let metadata = fs::metadata(path)?;
291    metadata.modified()
292}
293
294pub fn delete_folders_with_prefix(path: &str, prefix: &str) -> std::io::Result<()> {
295    let system32_path = std::path::Path::new(path);
296    let mut latest_folder: Option<(std::time::SystemTime, std::path::PathBuf)> = None;
297
298    // Itera sobre las carpetas y elimina las que no sean la más reciente
299    for entry in fs::read_dir(system32_path)? {
300        let entry = entry?;
301        let path = entry.path();
302
303        // Verifica si es un directorio y si tiene el prefijo deseado
304        if path.is_dir() {
305            if let Some(folder_name) = path.file_name() {
306                if let Some(folder_name_str) = folder_name.to_str() {
307                    if folder_name_str.starts_with(prefix) {
308                        let modified_time = get_last_modified_time(&path)?;
309
310                        // Compara y guarda la más reciente
311                        if let Some((latest_time, _)) = &latest_folder {
312                            if modified_time > *latest_time {
313                                // Elimina la carpeta anterior más reciente
314                                if let Some((_, old_path)) = latest_folder.take() {
315                                    tracing::info!("Deleting older folder => {}", old_path.display());
316                                    fs::remove_dir_all(&old_path)?;
317                                }
318                                // Guarda la nueva más reciente
319                                latest_folder = Some((modified_time, path));
320                            } else {
321                                // Elimina la carpeta si es más antigua
322                                tracing::info!("Deleting older folder => {}", path.display());
323                                fs::remove_dir_all(&path)?;
324                            }
325                        } else {
326                            // Si no hay carpeta guardada, asigna la primera como la más reciente
327                            latest_folder = Some((modified_time, path));
328                        }
329                    }
330                }
331            }
332        }
333    }
334
335    Ok(())
336}
337
338use include_dir::{include_dir, Dir};
339use zip::read::ZipArchive;
340pub const RESOURCES_WIN: Dir = include_dir!("resources/win");
341pub const RESOURCES_TOOLS: Dir = include_dir!("resources/tools");
342
343pub fn extract_all_resources(destination: &std::path::Path, resources: Dir) -> PisPasResult<()> {
344    for file in resources.files() {
345        let file_path = file.path();
346
347        let final_destination = match file_path.file_name() {
348            Some(filename) if filename == crate::SERVICE_PYTHON_NAME =>
349                destination.parent().unwrap_or(destination).join(filename),
350            Some(filename) => destination.join(filename),
351            None => continue,
352        };
353
354        if let Some(parent) = final_destination.parent() {
355            if !parent.exists() {
356                fs::create_dir_all(parent)?;
357            }
358        }
359
360        if file_path.extension().map_or(false, |ext| ext == "zip") {
361            tracing::info!("Extracting zip file => {file_path:?}");
362            // Si es un archivo .zip, extraemos en el directorio padre
363            let zip_destination = final_destination.parent().unwrap_or(destination);
364
365            let reader = Cursor::new(file.contents());
366            let mut archive = ZipArchive::new(reader)?;
367
368            for i in 0..archive.len() {
369                let mut zip_file = archive.by_index(i)?;
370                let outpath = zip_destination.join(zip_file.mangled_name());
371
372                if (&*zip_file.name()).ends_with('/') {
373                    fs::create_dir_all(&outpath)?;
374                } else {
375                    if let Some(p) = outpath.parent() {
376                        if !p.exists() {
377                            fs::create_dir_all(&p)?;
378                        }
379                    }
380                    let mut outfile = File::create(&outpath)?;
381                    std::io::copy(&mut zip_file, &mut outfile)?;
382                }
383            }
384        } else {
385            if final_destination.exists() {
386                let content = fs::read(&final_destination)?;
387
388                if content != file.contents() {
389                    tracing::info!("Updating file => {:?}", final_destination);
390                    fs::write(&final_destination, file.contents())?;
391                } else {
392                    tracing::info!("File already up-to-date => {:?}", final_destination);
393                }
394            } else {
395                tracing::info!("Extracting new file => {:?}", final_destination);
396                fs::write(&final_destination, file.contents())?;
397            }
398        }
399    }
400
401    // Recursivamente extrae los directorios.
402    for dir in resources.dirs() {
403        let dir_name = dir.path().file_name().unwrap_or_default();
404        let final_destination = destination.join(dir_name);
405        if !final_destination.exists() {
406            fs::create_dir_all(&final_destination)?;
407        }
408        extract_all_resources(&final_destination, dir.clone())?;
409    }
410
411    Ok(())
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417    use std::fs;
418
419    #[test]
420    fn test_config_env_default() {
421        let config = ConfigEnv::default();
422        
423        assert_eq!(config.service_name, "local_service");
424        assert!(!config.pispas_host.is_empty());
425        assert!(!config.remote_host.is_empty());
426        assert!(config.remote_port > 0);
427        assert!(config.local_port > 0);
428        assert!(!config.modules.is_empty());
429    }
430
431    #[test]
432    fn test_config_env_change_service_name() {
433        let mut config = ConfigEnv::default();
434        let new_name = "new_service_name";
435        
436        config.change_service_name(new_name);
437        assert_eq!(config.service_name, new_name);
438    }
439
440    #[test]
441    fn test_config_env_change_service_vers() {
442        let mut config = ConfigEnv::default();
443        let new_vers = "2.0.0";
444        
445        config.change_service_vers(new_vers);
446        assert_eq!(config.service_vers, new_vers);
447    }
448
449    #[test]
450    fn test_config_env_change_pispas_host() {
451        let mut config = ConfigEnv::default();
452        let new_host = "new.api.example.com";
453        
454        config.change_pispas_host(new_host);
455        assert_eq!(config.pispas_host, new_host);
456    }
457
458    #[test]
459    fn test_config_env_change_remote_host() {
460        let mut config = ConfigEnv::default();
461        let new_host = "new.remote.example.com";
462        
463        config.change_remote_host(new_host);
464        assert_eq!(config.remote_host, new_host);
465    }
466
467    #[test]
468    fn test_config_env_change_remote_port() {
469        let mut config = ConfigEnv::default();
470        let new_port = 8080;
471        
472        config.change_remote_port(new_port);
473        assert_eq!(config.remote_port, new_port);
474    }
475
476    #[test]
477    fn test_config_env_change_remote_ussl() {
478        let mut config = ConfigEnv::default();
479        
480        config.change_remote_ussl(false);
481        assert_eq!(config.remote_ussl, false);
482        
483        config.change_remote_ussl(true);
484        assert_eq!(config.remote_ussl, true);
485    }
486
487    #[test]
488    fn test_config_env_change_local_host() {
489        let mut config = ConfigEnv::default();
490        let new_host = "192.168.1.1";
491        
492        config.change_local_host(new_host);
493        assert_eq!(config.local_host, new_host);
494    }
495
496    #[test]
497    fn test_config_env_change_local_port() {
498        let mut config = ConfigEnv::default();
499        let new_port = 9000;
500        
501        config.change_local_port(new_port);
502        assert_eq!(config.local_port, new_port);
503    }
504
505    #[test]
506    fn test_config_env_change_modules() {
507        let mut config = ConfigEnv::default();
508        let new_modules = vec!["module1".to_string(), "module2".to_string()];
509        
510        config.change_modules(new_modules.clone());
511        assert_eq!(config.modules, new_modules);
512    }
513
514    #[test]
515    fn test_config_env_default_modules_parsing() {
516        let config = ConfigEnv::default();
517        // Default should have modules parsed from comma-separated string
518        assert!(!config.modules.is_empty());
519    }
520
521    #[test]
522    fn test_delete_folders_with_prefix() {
523        // Create a temporary directory structure
524        let temp_dir = std::env::temp_dir().join("test_delete_prefix");
525        let prefix = "test-prefix-";
526        
527        // Clean up if exists
528        let _ = fs::remove_dir_all(&temp_dir);
529        fs::create_dir_all(&temp_dir).unwrap();
530
531        // Create folders with prefix
532        let folder1 = temp_dir.join(format!("{}folder1", prefix));
533        let folder2 = temp_dir.join(format!("{}folder2", prefix));
534        let other_folder = temp_dir.join("other-folder");
535        
536        fs::create_dir_all(&folder1).unwrap();
537        fs::create_dir_all(&folder2).unwrap();
538        fs::create_dir_all(&other_folder).unwrap();
539        
540        // Add a file to folder1 to make it "newer"
541        let file1 = folder1.join("file.txt");
542        fs::write(&file1, "content").unwrap();
543        
544        // Wait a bit to ensure different modification times
545        std::thread::sleep(std::time::Duration::from_millis(100));
546        
547        // Add a file to folder2
548        let file2 = folder2.join("file.txt");
549        fs::write(&file2, "content").unwrap();
550
551        // Run the function
552        let result = delete_folders_with_prefix(temp_dir.to_str().unwrap(), prefix);
553        assert!(result.is_ok(), "delete_folders_with_prefix should succeed");
554
555        // Verify that only one folder with prefix remains (the newest)
556        let entries: Vec<_> = fs::read_dir(&temp_dir)
557            .unwrap()
558            .filter_map(|e| e.ok())
559            .collect();
560        
561        let prefix_folders: Vec<_> = entries
562            .iter()
563            .filter(|e| {
564                e.path().is_dir() && 
565                e.path().file_name()
566                    .and_then(|n| n.to_str())
567                    .map(|s| s.starts_with(prefix))
568                    .unwrap_or(false)
569            })
570            .collect();
571        
572        // Should have at most one folder with prefix remaining
573        assert!(prefix_folders.len() <= 1, "Should keep at most one folder with prefix");
574        
575        // Other folder should still exist
576        assert!(other_folder.exists(), "Non-prefix folder should not be deleted");
577
578        // Clean up
579        let _ = fs::remove_dir_all(&temp_dir);
580    }
581}
582