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    /// Place this TPV belongs to. `None` until the first authenticated
87    /// client opens the local WS and sends a `CHECK` with `place_id`
88    /// (first-write-wins). Once set, mismatching `place_id` on a future
89    /// CHECK is rejected — the operator must explicitly reset identity
90    /// from the configurator to re-pair the TPV to a different place.
91    pub place_id: Option<i64>,
92    /// Backend-assigned technical id (PK of the `RASPPI` table). `None`
93    /// until the WSR `CONNECT` response delivers it. Used for routing
94    /// (the WSR indexes by `objid`, not by `service_name`) and as the
95    /// FQDN prefix of the per-TPV LE cert (`<objid>.local.unpispas.es`).
96    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()) // Valor predeterminado
123                .split(',')
124                .map(|s| s.trim().to_string())
125                .collect(),
126            list_printers: None, // Inicialmente vacío
127            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        // Carga el archivo .env desde el directorio bin
139        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()) // Valor predeterminado
168                .split(',')
169                .map(|s| s.trim().to_string())
170                .collect(), // Convierte a Vec<String>
171            list_printers: None, // Inicialmente vacío
172            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    /// Set `place_id` and persist to `.env`. First-write-wins: returns
178    /// `Err` if `place_id` is already set to a different value (the
179    /// operator must explicitly reset identity to re-pair the TPV).
180    /// Setting the same value is a no-op (idempotent).
181    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    /// Set `objid` and persist. `objid` is assigned by the backend in
196    /// the WSR `CONNECT` response. Once we have it, it should never
197    /// change for the lifetime of the install — but we don't enforce
198    /// that here; the backend is the source of truth.
199    pub fn set_objid(&mut self, new_objid: i64) {
200        self.objid = Some(new_objid);
201        self.save();
202    }
203
204    /// Drop the bootstrap fields and let the TPV re-pair from scratch.
205    /// Triggered by the configurator UI when an operator wants to move
206    /// the TPV to a different place. The backend should also be told
207    /// to delete the old `RASPPI` row separately.
208    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        // `place_id` and `objid` are written only when known; empty
257        // means "still in PRE-BOOTSTRAP / waiting for backend".
258        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    // Itera sobre las carpetas y elimina las que no sean la más reciente
357    for entry in fs::read_dir(system32_path)? {
358        let entry = entry?;
359        let path = entry.path();
360
361        // Verifica si es un directorio y si tiene el prefijo deseado
362        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                        // Compara y guarda la más reciente
369                        if let Some((latest_time, _)) = &latest_folder {
370                            if modified_time > *latest_time {
371                                // Elimina la carpeta anterior más reciente
372                                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                                // Guarda la nueva más reciente
377                                latest_folder = Some((modified_time, path));
378                            } else {
379                                // Elimina la carpeta si es más antigua
380                                tracing::info!("Deleting older folder => {}", path.display());
381                                fs::remove_dir_all(&path)?;
382                            }
383                        } else {
384                            // Si no hay carpeta guardada, asigna la primera como la más reciente
385                            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            // Si es un archivo .zip, extraemos en el directorio padre
421            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    // Recursivamente extrae los directorios.
460    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        // First write succeeds.
495        assert!(config.set_place_id(5).is_ok());
496        assert_eq!(config.place_id, Some(5));
497        // Same value is idempotent.
498        assert!(config.set_place_id(5).is_ok());
499        // Different value is rejected.
500        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        // Default should have modules parsed from comma-separated string
602        assert!(!config.modules.is_empty());
603    }
604
605    #[test]
606    fn test_delete_folders_with_prefix() {
607        // Create a temporary directory structure
608        let temp_dir = std::env::temp_dir().join("test_delete_prefix");
609        let prefix = "test-prefix-";
610        
611        // Clean up if exists
612        let _ = fs::remove_dir_all(&temp_dir);
613        fs::create_dir_all(&temp_dir).unwrap();
614
615        // Create folders with prefix
616        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        // Add a file to folder1 to make it "newer"
625        let file1 = folder1.join("file.txt");
626        fs::write(&file1, "content").unwrap();
627        
628        // Wait a bit to ensure different modification times
629        std::thread::sleep(std::time::Duration::from_millis(100));
630        
631        // Add a file to folder2
632        let file2 = folder2.join("file.txt");
633        fs::write(&file2, "content").unwrap();
634
635        // Run the function
636        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        // Verify that only one folder with prefix remains (the newest)
640        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        // Should have at most one folder with prefix remaining
657        assert!(prefix_folders.len() <= 1, "Should keep at most one folder with prefix");
658        
659        // Other folder should still exist
660        assert!(other_folder.exists(), "Non-prefix folder should not be deleted");
661
662        // Clean up
663        let _ = fs::remove_dir_all(&temp_dir);
664    }
665}
666