feat: #30 支持Windows便携式版本

This commit is contained in:
VirtualHotBar
2026-06-02 07:38:46 +08:00
parent 4bf383caec
commit 39b9cef253
4 changed files with 157 additions and 34 deletions

View File

@@ -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]

View File

@@ -28,11 +28,7 @@ fn resolve_tilde(app: &tauri::AppHandle<Runtime>, path: &str) -> anyhow::Result<
}
fn app_data_dir(app: &tauri::AppHandle<Runtime>) -> anyhow::Result<PathBuf> {
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<Runtime>, candidate: &Path) -> anyhow::Result<()> {

View File

@@ -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<Runtime>, path: &str) -> anyhow::Result<P
}
fn app_data_dir(app: &tauri::AppHandle<Runtime>) -> anyhow::Result<PathBuf> {
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)?;

View File

@@ -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` 标记文件。
/// 若存在,则数据存储在 `<exe_dir>/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())
}
}
/// 获取数据目录路径。
/// - 便携式模式:`<exe_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<M: tauri::Manager<Runtime>> 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<Runtime>, enabled: bool) -> Result<
})
}
/// Get the current autostart mode.
/// Returns "none", "registry", or "task_scheduler".
#[tauri::command]
fn get_autostart_mode(app: tauri::AppHandle<Runtime>) -> 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<Runtime>, mode: String) -> Result<bool, String> {
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<bool, usize> {
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-<name>.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-<name>.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));