diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1cc42cd76..8de56a967 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2325,6 +2325,7 @@ dependencies = [ "tokio", "tokio-util", "url", + "windows-sys 0.60.2", ] [[package]] @@ -5188,7 +5189,6 @@ dependencies = [ "tauri-plugin", "thiserror 2.0.12", "tokio", - "windows-sys 0.60.2", ] [[package]] diff --git a/src-tauri/plugins/tauri-plugin-llamacpp/Cargo.toml b/src-tauri/plugins/tauri-plugin-llamacpp/Cargo.toml index c0370c3ad..fd58f6225 100644 --- a/src-tauri/plugins/tauri-plugin-llamacpp/Cargo.toml +++ b/src-tauri/plugins/tauri-plugin-llamacpp/Cargo.toml @@ -25,10 +25,6 @@ thiserror = "2.0.12" tokio = { version = "1", features = ["full"] } reqwest = { version = "0.11", features = ["json", "blocking", "stream"] } -# Windows-specific dependencies -[target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.60.2", features = ["Win32_Storage_FileSystem"] } - # Unix-specific dependencies [target.'cfg(unix)'.dependencies] nix = { version = "=0.30.1", features = ["signal", "process"] } diff --git a/src-tauri/plugins/tauri-plugin-llamacpp/src/path.rs b/src-tauri/plugins/tauri-plugin-llamacpp/src/path.rs index bd335037b..9b489d510 100644 --- a/src-tauri/plugins/tauri-plugin-llamacpp/src/path.rs +++ b/src-tauri/plugins/tauri-plugin-llamacpp/src/path.rs @@ -3,31 +3,7 @@ use std::path::PathBuf; use crate::error::{ErrorCode, LlamacppError, ServerResult}; #[cfg(windows)] -use std::os::windows::ffi::OsStrExt; - -#[cfg(windows)] -use std::ffi::OsStr; - -#[cfg(windows)] -use windows_sys::Win32::Storage::FileSystem::GetShortPathNameW; - -/// Get Windows short path to avoid issues with spaces and special characters -#[cfg(windows)] -pub fn get_short_path>(path: P) -> Option { - let wide: Vec = OsStr::new(path.as_ref()) - .encode_wide() - .chain(Some(0)) - .collect(); - - let mut buffer = vec![0u16; 260]; - let len = unsafe { GetShortPathNameW(wide.as_ptr(), buffer.as_mut_ptr(), buffer.len() as u32) }; - - if len > 0 { - Some(String::from_utf16_lossy(&buffer[..len as usize])) - } else { - None - } -} +use jan_utils::path::get_short_path; /// Validate that a binary path exists and is accessible pub fn validate_binary_path(backend_path: &str) -> ServerResult { @@ -259,18 +235,6 @@ mod tests { assert!(result.is_err()); } - #[cfg(windows)] - #[test] - fn test_get_short_path() { - // Test with a real path that should exist on Windows - use std::env; - if let Ok(temp_dir) = env::var("TEMP") { - let result = get_short_path(&temp_dir); - // Should return some short path or None (both are valid) - // We can't assert the exact value as it depends on the system - println!("Short path result: {:?}", result); - } - } #[test] fn test_validate_model_path_multiple_m_flags() { diff --git a/src-tauri/src/core/filesystem/commands.rs b/src-tauri/src/core/filesystem/commands.rs index 1601f87aa..bfc29b415 100644 --- a/src-tauri/src/core/filesystem/commands.rs +++ b/src-tauri/src/core/filesystem/commands.rs @@ -184,6 +184,17 @@ pub fn decompress(app: tauri::AppHandle, path: &str, output_dir: &str) -> Result ) })?; + // Use short path on Windows to handle paths with spaces + #[cfg(windows)] + let file = { + if let Some(short_path) = jan_utils::path::get_short_path(&path_buf) { + fs::File::open(&short_path).map_err(|e| e.to_string())? + } else { + fs::File::open(&path_buf).map_err(|e| e.to_string())? + } + }; + + #[cfg(not(windows))] let file = fs::File::open(&path_buf).map_err(|e| e.to_string())?; if path.ends_with(".tar.gz") { let tar = flate2::read::GzDecoder::new(file); diff --git a/src-tauri/utils/Cargo.toml b/src-tauri/utils/Cargo.toml index 071d39eeb..7d313a42b 100644 --- a/src-tauri/utils/Cargo.toml +++ b/src-tauri/utils/Cargo.toml @@ -16,6 +16,9 @@ tokio = { version = "1", features = ["process", "fs", "macros", "rt"] } tokio-util = "0.7.14" url = "2.5" +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.60.2", features = ["Win32_Storage_FileSystem"] } + [dev-dependencies] tempfile = "3.0" diff --git a/src-tauri/utils/src/path.rs b/src-tauri/utils/src/path.rs index 41918edf0..6134ce373 100644 --- a/src-tauri/utils/src/path.rs +++ b/src-tauri/utils/src/path.rs @@ -1,76 +1,121 @@ -#[cfg(windows)] -use std::path::Prefix; -use std::path::{Component, Path, PathBuf}; - -/// Normalizes file paths by handling path components, prefixes, and resolving relative paths -/// Based on: https://github.com/rust-lang/cargo/blob/rust-1.67.0/crates/cargo-util/src/paths.rs#L82-L107 -pub fn normalize_path(path: &Path) -> PathBuf { - let mut components = path.components().peekable(); - let mut ret = if let Some(c @ Component::Prefix(_prefix_component)) = components.peek().cloned() - { - #[cfg(windows)] - // Remove only the Verbatim prefix, but keep the drive letter (e.g., C:\) - match _prefix_component.kind() { - Prefix::VerbatimDisk(disk) => { - components.next(); // skip this prefix - // Re-add the disk prefix (e.g., C:) - let mut pb = PathBuf::new(); - pb.push(format!("{}:", disk as char)); - pb - } - Prefix::Verbatim(_) | Prefix::VerbatimUNC(_, _) => { - components.next(); // skip this prefix - PathBuf::new() - } - _ => { - components.next(); - PathBuf::from(c.as_os_str()) - } - } - #[cfg(not(windows))] - { - components.next(); // skip this prefix - PathBuf::from(c.as_os_str()) - } - } else { - PathBuf::new() - }; - - for component in components { - match component { - Component::Prefix(..) => unreachable!(), - Component::RootDir => { - ret.push(component.as_os_str()); - } - Component::CurDir => {} - Component::ParentDir => { - ret.pop(); - } - Component::Normal(c) => { - ret.push(c); - } - } - } - ret -} - -/// Removes file:/ and file:\ prefixes from file paths -pub fn normalize_file_path(path: &str) -> String { - path.replace("file:/", "").replace("file:\\", "") -} - -/// Removes prefix from path string with proper formatting -pub fn remove_prefix(path: &str, prefix: &str) -> String { - if !prefix.is_empty() && path.starts_with(prefix) { - let result = path[prefix.len()..].to_string(); - if result.is_empty() { - "/".to_string() - } else if result.starts_with('/') { - result - } else { - format!("/{}", result) - } - } else { - path.to_string() - } -} +#[cfg(windows)] +use std::path::Prefix; +use std::path::{Component, Path, PathBuf}; + +#[cfg(windows)] +use std::os::windows::ffi::OsStrExt; + +#[cfg(windows)] +use std::ffi::OsStr; + +#[cfg(windows)] +use windows_sys::Win32::Storage::FileSystem::GetShortPathNameW; + +/// Normalizes file paths by handling path components, prefixes, and resolving relative paths +/// Based on: https://github.com/rust-lang/cargo/blob/rust-1.67.0/crates/cargo-util/src/paths.rs#L82-L107 +pub fn normalize_path(path: &Path) -> PathBuf { + let mut components = path.components().peekable(); + let mut ret = if let Some(c @ Component::Prefix(_prefix_component)) = components.peek().cloned() + { + #[cfg(windows)] + // Remove only the Verbatim prefix, but keep the drive letter (e.g., C:\) + match _prefix_component.kind() { + Prefix::VerbatimDisk(disk) => { + components.next(); // skip this prefix + // Re-add the disk prefix (e.g., C:) + let mut pb = PathBuf::new(); + pb.push(format!("{}:", disk as char)); + pb + } + Prefix::Verbatim(_) | Prefix::VerbatimUNC(_, _) => { + components.next(); // skip this prefix + PathBuf::new() + } + _ => { + components.next(); + PathBuf::from(c.as_os_str()) + } + } + #[cfg(not(windows))] + { + components.next(); // skip this prefix + PathBuf::from(c.as_os_str()) + } + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + ret.push(c); + } + } + } + ret +} + +/// Removes file:/ and file:\ prefixes from file paths +pub fn normalize_file_path(path: &str) -> String { + path.replace("file:/", "").replace("file:\\", "") +} + +/// Removes prefix from path string with proper formatting +pub fn remove_prefix(path: &str, prefix: &str) -> String { + if !prefix.is_empty() && path.starts_with(prefix) { + let result = path[prefix.len()..].to_string(); + if result.is_empty() { + "/".to_string() + } else if result.starts_with('/') { + result + } else { + format!("/{}", result) + } + } else { + path.to_string() + } +} + +/// Get Windows short path to avoid issues with spaces and special characters +#[cfg(windows)] +pub fn get_short_path>(path: P) -> Option { + let wide: Vec = OsStr::new(path.as_ref()) + .encode_wide() + .chain(Some(0)) + .collect(); + + let mut buffer = vec![0u16; 260]; + let len = unsafe { GetShortPathNameW(wide.as_ptr(), buffer.as_mut_ptr(), buffer.len() as u32) }; + + if len > 0 { + Some(String::from_utf16_lossy(&buffer[..len as usize])) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(windows)] + #[test] + fn test_get_short_path() { + // Test with a real path that should exist on Windows + use std::env; + if let Ok(temp_dir) = env::var("TEMP") { + let result = get_short_path(&temp_dir); + // Should return some short path or None (both are valid) + // We can't assert the exact value as it depends on the system + println!("Short path result: {:?}", result); + } + } +}