diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6531446..6906290 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -357,6 +357,47 @@ jobs: tauriScript: pnpm tauri args: ${{ matrix.args }} + # Windows 便携式版本:将构建产物打包为 ZIP + - name: Create Windows portable ZIP + if: contains(matrix.platform, 'windows') + shell: pwsh + env: + PACKAGE_VERSION: ${{ needs.create-release.outputs.package_version }} + TARGET: ${{ matrix.target }} + ARCH: ${{ matrix.arch }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + $exeExt = ".exe" + $exeName = "NetMount" + + # 构建输出目录 + $releaseDir = "src-tauri/target/$env:TARGET/release" + + # 创建临时打包目录 + $portableDir = "portable-pack" + if (Test-Path $portableDir) { Remove-Item -Recurse -Force $portableDir } + New-Item -ItemType Directory -Path $portableDir | Out-Null + + # 复制可执行文件 + Copy-Item "$releaseDir/$exeName$exeExt" "$portableDir/" + + # 创建 .portable 标记文件 + New-Item -ItemType File -Path "$portableDir/.portable" -Force | Out-Null + + # 生成 ZIP 文件名 + $zipName = "NetMount_${env:PACKAGE_VERSION}_windows_${env:ARCH}_portable.zip" + + # 打包 ZIP + Compress-Archive -Path "$portableDir/*" -DestinationPath $zipName -Force + + # 清理临时目录 + Remove-Item -Recurse -Force $portableDir + + Write-Host "Created portable ZIP: $zipName" + + # 上传到 Release(使用 gh CLI) + gh release upload "v$env:PACKAGE_VERSION" $zipName --clobber + # ========== 生成 Changelog ========== generate-changelog: needs: [create-release] diff --git a/src-tauri/src/diagnostics.rs b/src-tauri/src/diagnostics.rs index 67c91e4..33d1528 100644 --- a/src-tauri/src/diagnostics.rs +++ b/src-tauri/src/diagnostics.rs @@ -28,11 +28,7 @@ fn resolve_tilde(app: &tauri::AppHandle, path: &str) -> anyhow::Result< } fn app_data_dir(app: &tauri::AppHandle) -> anyhow::Result { - Ok(app - .path() - .home_dir() - .map_err(|e| anyhow::anyhow!("Failed to get home dir: {}", e))? - .join(".netmount")) + Ok(crate::resolve_data_dir()) } fn ensure_under_app_data_dir(app: &tauri::AppHandle, candidate: &Path) -> anyhow::Result<()> { diff --git a/src-tauri/src/fs.rs b/src-tauri/src/fs.rs index d8ae3a8..46756ef 100644 --- a/src-tauri/src/fs.rs +++ b/src-tauri/src/fs.rs @@ -37,6 +37,10 @@ fn fix_openlist_config_paths(config_path: &Path) -> anyhow::Result<()> { ("", "temp_dir"), ]; + // 获取数据目录前缀(用于路径匹配) + let data_dir = crate::resolve_data_dir(); + let data_dir_str = data_dir.to_string_lossy().replace('\\', "/"); + for (parent, field) in path_fields { if let Some(value) = if parent.is_empty() { config.get(field).cloned() @@ -47,15 +51,15 @@ fn fix_openlist_config_paths(config_path: &Path) -> anyhow::Result<()> { // 检查是否为绝对路径 if is_absolute_path(path_str) { // 尝试提取相对路径部分 - // 通常路径格式为: C:/Users/username/.netmount/openlist/xxx - // 我们需要提取出相对路径: data/xxx 或 log/xxx 等 let normalized = path_str.replace('\\', "/"); // 尝试从路径中提取相对部分 + // 支持两种格式: + // - 普通模式: C:/Users/username/.netmount/openlist/xxx + // - 便携式模式: E:/NetMount/data/openlist/xxx let relative_path = if normalized.contains("/.netmount/openlist/") { normalized.split("/.netmount/openlist/").nth(1).map(|s| s.to_string()) } else if normalized.contains("/.netmount/") { - // 处理其他可能的路径格式 normalized.split("/.netmount/").nth(1).map(|s| { if s.starts_with("openlist/") { s.strip_prefix("openlist/").map(|p| p.to_string()).unwrap_or(s.to_string()) @@ -63,6 +67,18 @@ fn fix_openlist_config_paths(config_path: &Path) -> anyhow::Result<()> { s.to_string() } }) + } else if normalized.contains("/data/openlist/") { + // 便携式模式路径: E:/NetMount/data/openlist/xxx + normalized.split("/data/openlist/").nth(1).map(|s| s.to_string()) + } else if normalized.contains("/data/") && data_dir_str.contains("/data/") { + // 便携式模式其他路径: E:/NetMount/data/xxx + normalized.split("/data/").nth(1).map(|s| { + if s.starts_with("openlist/") { + s.strip_prefix("openlist/").map(|p| p.to_string()).unwrap_or(s.to_string()) + } else { + s.to_string() + } + }) } else { None }; @@ -108,11 +124,7 @@ fn resolve_path(app: &tauri::AppHandle, path: &str) -> anyhow::Result

) -> anyhow::Result { - Ok(app - .path() - .home_dir() - .map_err(|e| anyhow::anyhow!("Failed to get home dir: {}", e))? - .join(".netmount")) + Ok(crate::resolve_data_dir()) } #[tauri::command] @@ -339,11 +351,7 @@ pub fn export_config( let options: zip::write::FileOptions<'_, ()> = zip::write::FileOptions::default() .compression_method(zip::CompressionMethod::Deflated); - let data_dir = app - .path() - .home_dir() - .map_err(|e| anyhow::anyhow!("Failed to get home dir: {}", e))? - .join(".netmount"); + let data_dir = crate::resolve_data_dir(); // 递归添加目录内容(排除 log 目录) fn add_dir_to_zip( @@ -418,11 +426,7 @@ pub fn import_config( .map_err(|e| anyhow::anyhow!("无效的备份文件:{}", e))?; // 获取数据目录 - let data_dir = app - .path() - .home_dir() - .map_err(|e| anyhow::anyhow!("Failed to get home dir: {}", e))? - .join(".netmount"); + let data_dir = crate::resolve_data_dir(); // 确保数据目录存在 fs::create_dir_all(&data_dir)?; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a60f552..390b651 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -11,6 +11,7 @@ use fs::{fs_exist_dir, fs_make_dir, read_json_file, write_json_file, copy_file, use locale::Locale; use tray::Tray; +mod autostart; mod config; mod diagnostics; mod fs; @@ -19,6 +20,39 @@ mod sidecar; mod tray; mod utils; +/// 便携式模式:检测可执行文件同目录下是否存在 `.portable` 标记文件。 +/// 若存在,则数据存储在 `/data/` 而非 `~/.netmount/`。 +pub(crate) fn is_portable() -> bool { + env::current_exe() + .ok() + .and_then(|p| p.parent().map(|d| d.join(".portable").exists())) + .unwrap_or(false) +} + +/// 获取用户主目录。 +fn home_dir() -> PathBuf { + if cfg!(target_os = "windows") { + env::var("USERPROFILE").map(PathBuf::from).unwrap_or_else(|_| env::temp_dir()) + } else { + env::var("HOME").map(PathBuf::from).unwrap_or_else(|_| env::temp_dir()) + } +} + +/// 获取数据目录路径。 +/// - 便携式模式:`/data/` +/// - 普通模式:`~/.netmount/` +pub(crate) fn resolve_data_dir() -> PathBuf { + if is_portable() { + env::current_exe() + .expect("无法获取当前可执行文件路径") + .parent() + .expect("无法获取父目录") + .join("data") + } else { + home_dir().join(".netmount") + } +} + use crate::utils::download_with_progress; use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::ManagerExt; @@ -91,7 +125,7 @@ impl> AppExt for M { } fn app_data_dir(&self) -> PathBuf { - self.path().home_dir().unwrap().join(".netmount") + resolve_data_dir() } fn app_config_file(&self) -> PathBuf { @@ -378,6 +412,58 @@ fn set_autostart_state(app: tauri::AppHandle, enabled: bool) -> Result< }) } +/// Get the current autostart mode. +/// Returns "none", "registry", or "task_scheduler". +#[tauri::command] +fn get_autostart_mode(app: tauri::AppHandle) -> String { + let task_enabled = autostart::is_task_enabled(); + let registry_enabled = app.autolaunch().is_enabled().unwrap_or(false); + + if task_enabled { + "task_scheduler".to_string() + } else if registry_enabled { + "registry".to_string() + } else { + "none".to_string() + } +} + +/// Set the autostart mode. +/// Mode can be "none", "registry", or "task_scheduler". +#[tauri::command] +fn set_autostart_mode(app: tauri::AppHandle, mode: String) -> Result { + let autostart_manager = app.autolaunch(); + + // Disable all existing modes first + let _ = autostart_manager.disable(); + let _ = autostart::delete_task(); + + match mode.as_str() { + "registry" => Ok(autostart_manager.enable().is_ok()), + "task_scheduler" => { + let exe_path = std::env::current_exe() + .map_err(|e| format!("Failed to get executable path: {}", e))? + .to_string_lossy() + .to_string(); + autostart::create_task(&exe_path, false).map(|()| true) + } + _ => Ok(true), // "none" - already disabled above + } +} + +/// Check if the app is running in service (headless) mode. +/// Service mode is activated via the `--service` CLI flag. +#[tauri::command] +fn is_service_mode() -> bool { + std::env::args().any(|arg| arg == "--service") +} + +/// Check if Task Scheduler is available on this platform (Windows only). +#[tauri::command] +fn is_task_scheduler_available() -> bool { + cfg!(target_os = "windows") +} + #[tauri::command] fn download_file(url: String, out_path: String) -> Result { download_with_progress(&url, &out_path, |total_size, downloaded| { @@ -462,13 +548,11 @@ async fn spawn_sidecar( ), tauri::path::BaseDirectory::Resource) .map_err(|e| format!("Failed to resolve sidecar path: {}", e))?; - // 获取工作目录:优先使用传入的 cwd,否则使用默认目录 + // 获取工作目录:优先使用传入的 cwd,否则使用默认目录(支持便携式模式) let work_dir = if let Some(cwd_path) = cwd { std::path::PathBuf::from(cwd_path) } else { - app.path().home_dir() - .map_err(|e| format!("Failed to get home dir: {}", e))? - .join(".netmount") + resolve_data_dir() }; // 确保工作目录存在 @@ -476,7 +560,7 @@ async fn spawn_sidecar( let _ = std::fs::create_dir_all(&work_dir); } - // sidecar 统一诊断日志:~/.netmount/log/sidecar-.log + // sidecar 统一诊断日志 let log_dir = work_dir.join("log"); let _ = std::fs::create_dir_all(&log_dir); let sidecar_log_path = log_dir.join(format!("sidecar-{}.log", sidecar_name)); @@ -710,20 +794,18 @@ async fn run_sidecar_once( ) .map_err(|e| format!("Failed to resolve sidecar path: {}", e))?; - // 获取工作目录:优先使用传入的 cwd,否则使用默认目录 + // 获取工作目录:优先使用传入的 cwd,否则使用默认目录(支持便携式模式) let work_dir = if let Some(cwd_path) = cwd { std::path::PathBuf::from(cwd_path) } else { - app.path().home_dir() - .map_err(|e| format!("Failed to get home dir: {}", e))? - .join(".netmount") + resolve_data_dir() }; if !work_dir.exists() { let _ = std::fs::create_dir_all(&work_dir); } - // sidecar 统一诊断日志:~/.netmount/log/sidecar-.log + // sidecar 统一诊断日志 let log_dir = work_dir.join("log"); let _ = std::fs::create_dir_all(&log_dir); let sidecar_log_path = log_dir.join(format!("sidecar-{}.log", sidecar_name));