mirror of
https://github.com/VirtualHotBar/NetMount.git
synced 2026-09-03 07:18:00 +08:00
feat: #30 支持Windows便携式版本
This commit is contained in:
41
.github/workflows/main.yml
vendored
41
.github/workflows/main.yml
vendored
@@ -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]
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user