292 lines
8.7 KiB
Rust
292 lines
8.7 KiB
Rust
// filepath: src-tauri/src/binary_manager.rs
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
use tauri::AppHandle;
|
|
use anyhow::{Result, anyhow};
|
|
#[cfg(target_family = "unix")]
|
|
use std::os::unix::fs::PermissionsExt;
|
|
#[cfg(target_os = "windows")]
|
|
use std::os::windows::process::CommandExt;
|
|
use zip::ZipArchive;
|
|
use std::io::Cursor;
|
|
|
|
use crate::storage::{self};
|
|
|
|
const YT_DLP_REPO_URL: &str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download";
|
|
// Bellard's QuickJS binary releases URL
|
|
const QJS_REPO_URL: &str = "https://bellard.org/quickjs/binary_releases";
|
|
|
|
pub fn get_ytdlp_binary_name() -> &'static str {
|
|
if cfg!(target_os = "windows") {
|
|
"yt-dlp.exe"
|
|
} else if cfg!(target_os = "macos") {
|
|
"yt-dlp_macos"
|
|
} else {
|
|
"yt-dlp"
|
|
}
|
|
}
|
|
|
|
// Target name on disk (for yt-dlp usage)
|
|
pub fn get_qjs_binary_name() -> &'static str {
|
|
if cfg!(target_os = "windows") {
|
|
"quickjs.exe"
|
|
} else {
|
|
"quickjs"
|
|
}
|
|
}
|
|
|
|
// Source name inside the zip archive (standard QuickJS naming)
|
|
fn get_qjs_source_name_in_zip() -> &'static str {
|
|
if cfg!(target_os = "windows") {
|
|
"qjs.exe"
|
|
} else {
|
|
"qjs"
|
|
}
|
|
}
|
|
|
|
// Get base directory for all binaries
|
|
pub fn get_bin_dir(app: &AppHandle) -> Result<PathBuf> {
|
|
let app_data = storage::get_app_data_dir(app)?;
|
|
let bin_dir = app_data.join("bin");
|
|
if !bin_dir.exists() {
|
|
fs::create_dir_all(&bin_dir)?;
|
|
}
|
|
Ok(bin_dir)
|
|
}
|
|
|
|
pub fn get_ytdlp_path(app: &AppHandle) -> Result<PathBuf> {
|
|
Ok(get_bin_dir(app)?.join(get_ytdlp_binary_name()))
|
|
}
|
|
|
|
pub fn get_qjs_path(app: &AppHandle) -> Result<PathBuf> {
|
|
Ok(get_bin_dir(app)?.join(get_qjs_binary_name()))
|
|
}
|
|
|
|
pub fn check_binaries(app: &AppHandle) -> bool {
|
|
let ytdlp = get_ytdlp_path(app).map(|p| p.exists()).unwrap_or(false);
|
|
let qjs = get_qjs_path(app).map(|p| p.exists()).unwrap_or(false);
|
|
ytdlp && qjs
|
|
}
|
|
|
|
// --- yt-dlp Logic ---
|
|
|
|
pub async fn download_ytdlp(app: &AppHandle) -> Result<PathBuf> {
|
|
let binary_name = get_ytdlp_binary_name();
|
|
let url = format!("{}/{}", YT_DLP_REPO_URL, binary_name);
|
|
|
|
let response = reqwest::get(&url).await?;
|
|
if !response.status().is_success() {
|
|
return Err(anyhow!("下载 yt-dlp 失败:状态 {}", response.status()));
|
|
}
|
|
|
|
let bytes = response.bytes().await?;
|
|
let path = get_ytdlp_path(app)?;
|
|
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
|
|
fs::write(&path, bytes)?;
|
|
|
|
#[cfg(target_family = "unix")]
|
|
{
|
|
let mut perms = fs::metadata(&path)?.permissions();
|
|
perms.set_mode(0o755);
|
|
fs::set_permissions(&path, perms)?;
|
|
}
|
|
|
|
Ok(path)
|
|
}
|
|
|
|
pub async fn update_ytdlp(app: &AppHandle) -> Result<String> {
|
|
let path = get_ytdlp_path(app)?;
|
|
if !path.exists() {
|
|
download_ytdlp(app).await?;
|
|
return Ok("yt-dlp 已全新下载".to_string());
|
|
}
|
|
|
|
// Use built-in update for yt-dlp
|
|
let mut cmd = std::process::Command::new(&path);
|
|
cmd.arg("-U");
|
|
|
|
#[cfg(target_os = "windows")]
|
|
cmd.creation_flags(0x08000000);
|
|
|
|
let output = cmd.output()?;
|
|
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
return Err(anyhow!("yt-dlp 更新失败:{}", stderr));
|
|
}
|
|
|
|
// Update settings timestamp
|
|
let mut settings = storage::load_settings(app)?;
|
|
settings.last_updated = Some(chrono::Utc::now());
|
|
storage::save_settings(app, &settings)?;
|
|
|
|
Ok("yt-dlp 已更新".to_string())
|
|
}
|
|
|
|
pub fn get_ytdlp_version(app: &AppHandle) -> Result<String> {
|
|
let path = get_ytdlp_path(app)?;
|
|
if !path.exists() {
|
|
return Ok("未安装".to_string());
|
|
}
|
|
|
|
let mut cmd = std::process::Command::new(&path);
|
|
cmd.arg("--version");
|
|
|
|
#[cfg(target_os = "windows")]
|
|
cmd.creation_flags(0x08000000);
|
|
|
|
let output = cmd.output()?;
|
|
|
|
if output.status.success() {
|
|
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
|
} else {
|
|
Ok("未知".to_string())
|
|
}
|
|
}
|
|
|
|
// --- QuickJS Logic ---
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct LatestInfo {
|
|
version: String,
|
|
}
|
|
|
|
pub async fn download_qjs(app: &AppHandle) -> Result<PathBuf> {
|
|
// 1. Fetch LATEST.json to get version info (though filenames seem predictable-ish, version helps)
|
|
// Actually, looking at the file list, Bellard uses date-based versions.
|
|
// Format: quickjs-win-x86_64-YYYY-MM-DD.zip
|
|
// We need to find the correct filename dynamically or parse LATEST.json if it gave filenames.
|
|
// The LATEST.json content was {"version":"2024-01-13"} (example from prompt context).
|
|
// So we can construct the filename: quickjs-{platform}-{version}.zip
|
|
|
|
let latest_url = format!("{}/LATEST.json", QJS_REPO_URL);
|
|
let latest_resp = reqwest::get(&latest_url).await?;
|
|
if !latest_resp.status().is_success() {
|
|
return Err(anyhow!("获取 QuickJS 版本信息失败"));
|
|
}
|
|
let latest_info: LatestInfo = latest_resp.json().await?;
|
|
let version = latest_info.version;
|
|
|
|
// Construct filename based on OS/Arch
|
|
let filename = if cfg!(target_os = "windows") {
|
|
format!("quickjs-win-x86_64-{}.zip", version)
|
|
} else if cfg!(target_os = "macos") {
|
|
// NOTE: Cosmo builds are universal/portable for Linux/Mac usually?
|
|
// Based on prompt instruction: "macos download quickjs-cosmo marked file"
|
|
// Bellard lists: quickjs-cosmo-YYYY-MM-DD.zip
|
|
format!("quickjs-cosmo-{}.zip", version)
|
|
} else {
|
|
return Err(anyhow!("不支持当前操作系统的 QuickJS 自动下载"));
|
|
};
|
|
|
|
let download_url = format!("{}/{}", QJS_REPO_URL, filename);
|
|
let response = reqwest::get(&download_url).await?;
|
|
|
|
if !response.status().is_success() {
|
|
return Err(anyhow!("下载 QuickJS 失败:状态 {}", response.status()));
|
|
}
|
|
|
|
let bytes = response.bytes().await?;
|
|
let cursor = Cursor::new(bytes);
|
|
let mut archive = ZipArchive::new(cursor)?;
|
|
|
|
let bin_dir = get_bin_dir(app)?;
|
|
|
|
// Extract logic: The zip from Bellard usually contains a folder or just binaries.
|
|
// We need to find the `qjs` binary.
|
|
// Windows zip usually has `qjs.exe`
|
|
// Cosmo zip usually has `qjs`? Let's search for it.
|
|
|
|
let source_name = get_qjs_source_name_in_zip();
|
|
let target_name = get_qjs_binary_name(); // quickjs.exe or quickjs
|
|
|
|
let mut found_exe = false;
|
|
for i in 0..archive.len() {
|
|
let mut file = archive.by_index(i)?;
|
|
// Filenames in zip might be like "quickjs-win-x86_64-2024-01-13/qjs.exe"
|
|
let name = file.name().to_string();
|
|
|
|
// Skip directories
|
|
if file.is_dir() {
|
|
continue;
|
|
}
|
|
|
|
let filename_only = name.split('/').last().unwrap_or("");
|
|
|
|
if filename_only == source_name {
|
|
let mut out_file = fs::File::create(bin_dir.join(target_name))?;
|
|
std::io::copy(&mut file, &mut out_file)?;
|
|
found_exe = true;
|
|
} else if filename_only.ends_with(".dll") {
|
|
// Extract DLLs (needed for Windows MinGW builds, e.g. libwinpthread-1.dll)
|
|
let mut out_file = fs::File::create(bin_dir.join(filename_only))?;
|
|
std::io::copy(&mut file, &mut out_file)?;
|
|
}
|
|
}
|
|
|
|
if !found_exe {
|
|
return Err(anyhow!("在下载的压缩包中找不到 {}", source_name));
|
|
}
|
|
|
|
let final_path = get_qjs_path(app)?;
|
|
|
|
#[cfg(target_family = "unix")]
|
|
{
|
|
let mut perms = fs::metadata(&final_path)?.permissions();
|
|
perms.set_mode(0o755);
|
|
fs::set_permissions(&final_path, perms)?;
|
|
}
|
|
|
|
Ok(final_path)
|
|
}
|
|
|
|
pub async fn update_qjs(app: &AppHandle) -> Result<String> {
|
|
// QuickJS doesn't have self-update, so we just re-download
|
|
download_qjs(app).await?;
|
|
Ok("QuickJS 已更新/安装".to_string())
|
|
}
|
|
|
|
pub fn get_qjs_version(app: &AppHandle) -> Result<String> {
|
|
let path = get_qjs_path(app)?;
|
|
if !path.exists() {
|
|
return Ok("未安装".to_string());
|
|
}
|
|
Ok("已安装".to_string())
|
|
}
|
|
|
|
pub async fn ensure_binaries(app: &AppHandle) -> Result<()> {
|
|
let ytdlp = get_ytdlp_path(app)?;
|
|
if !ytdlp.exists() {
|
|
download_ytdlp(app).await?;
|
|
}
|
|
|
|
let qjs = get_qjs_path(app)?;
|
|
if !qjs.exists() {
|
|
download_qjs(app).await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_binary_names() {
|
|
if cfg!(target_os = "windows") {
|
|
assert_eq!(get_ytdlp_binary_name(), "yt-dlp.exe");
|
|
assert_eq!(get_qjs_binary_name(), "quickjs.exe");
|
|
assert_eq!(get_qjs_source_name_in_zip(), "qjs.exe");
|
|
} else if cfg!(target_os = "macos") {
|
|
assert_eq!(get_ytdlp_binary_name(), "yt-dlp_macos");
|
|
assert_eq!(get_qjs_binary_name(), "quickjs");
|
|
assert_eq!(get_qjs_source_name_in_zip(), "qjs");
|
|
}
|
|
}
|
|
}
|