diff --git a/lib/common/rust/api/unp4k_api.dart b/lib/common/rust/api/unp4k_api.dart index a362e5d..9fd05a6 100644 --- a/lib/common/rust/api/unp4k_api.dart +++ b/lib/common/rust/api/unp4k_api.dart @@ -8,7 +8,7 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'package:freezed_annotation/freezed_annotation.dart' hide protected; part 'unp4k_api.freezed.dart'; -// These functions are ignored because they are not marked as `pub`: `dos_datetime_to_millis`, `ensure_files_loaded` +// These functions are ignored because they are not marked as `pub`: `dos_datetime_to_millis`, `ensure_files_loaded`, `p4k_get_entry` /// 打开 P4K 文件(仅打开,不读取文件列表) Future p4KOpen({required String p4KPath}) => diff --git a/lib/provider/unp4kc.dart b/lib/provider/unp4kc.dart index 215da21..1907a80 100644 --- a/lib/provider/unp4kc.dart +++ b/lib/provider/unp4kc.dart @@ -399,10 +399,10 @@ class Unp4kCModel extends _$Unp4kCModel { dPrint("extractFile .... $filePath -> $fullOutputPath"); // 使用 Rust API 提取到磁盘 - await unp4k_api.p4KExtractToDisk(filePath: filePath, outputPath: fullOutputPath); + await unp4k_api.p4KExtractToDisk(filePath: filePath, outputPath: outputPath); if (mode == "extract_open") { - const textExt = [".txt", ".xml", ".json", ".lua", ".cfg", ".ini"]; + const textExt = [".txt", ".xml", ".json", ".lua", ".cfg", ".ini", ".mtl"]; const imgExt = [".png"]; String openType = "unknown"; for (var element in textExt) { @@ -475,9 +475,7 @@ class Unp4kCModel extends _$Unp4kCModel { current++; onProgress?.call(current, total, entryPath); - - final fullOutputPath = "$outputDir\\$entryPath"; - await unp4k_api.p4KExtractToDisk(filePath: entryPath, outputPath: fullOutputPath); + await unp4k_api.p4KExtractToDisk(filePath: entryPath, outputPath: outputDir); } state = state.copyWith(endMessage: S.current.tools_unp4k_extract_completed(current)); @@ -493,8 +491,7 @@ class Unp4kCModel extends _$Unp4kCModel { return (false, 0, S.current.tools_unp4k_extract_cancelled); } - final fullOutputPath = "$outputDir\\$filePath"; - await unp4k_api.p4KExtractToDisk(filePath: filePath, outputPath: fullOutputPath); + await unp4k_api.p4KExtractToDisk(filePath: filePath, outputPath: outputDir); state = state.copyWith(endMessage: S.current.tools_unp4k_extract_completed(1)); return (true, 1, null); @@ -609,9 +606,7 @@ class Unp4kCModel extends _$Unp4kCModel { current++; onProgress?.call(current, total, extractPath); - - final fullOutputPath = "$outputDir\\$extractPath"; - await unp4k_api.p4KExtractToDisk(filePath: extractPath, outputPath: fullOutputPath); + await unp4k_api.p4KExtractToDisk(filePath: extractPath, outputPath: outputDir); } state = state.copyWith(endMessage: S.current.tools_unp4k_extract_completed(current)); diff --git a/lib/ui/tools/log_analyze_ui/log_analyze_provider.g.dart b/lib/ui/tools/log_analyze_ui/log_analyze_provider.g.dart index bcb5d08..4982d7e 100644 --- a/lib/ui/tools/log_analyze_ui/log_analyze_provider.g.dart +++ b/lib/ui/tools/log_analyze_ui/log_analyze_provider.g.dart @@ -50,7 +50,7 @@ final class ToolsLogAnalyzeProvider } } -String _$toolsLogAnalyzeHash() => r'f5079c7d35daf25b07f83bacb224484171e9c93f'; +String _$toolsLogAnalyzeHash() => r'4c1aea03394e5c5641b2eb40a31d37892bb978bf'; final class ToolsLogAnalyzeFamily extends $Family with diff --git a/rust/Cargo.lock b/rust/Cargo.lock index cd530b7..11440ea 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -3404,15 +3404,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - [[package]] name = "matches" version = "0.1.10" @@ -3716,15 +3707,6 @@ dependencies = [ "zbus", ] -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "num" version = "0.2.1" @@ -4941,8 +4923,6 @@ dependencies = [ "tao", "tokenizers", "tokio", - "tracing", - "tracing-subscriber", "unp4k_rs", "url", "uuid", @@ -6074,33 +6054,15 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - [[package]] name = "tracing-subscriber" version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", "sharded-slab", - "smallvec 1.15.1", "thread_local", - "tracing", "tracing-core", - "tracing-log", ] [[package]] @@ -6186,7 +6148,7 @@ checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" [[package]] name = "unp4k_rs" version = "0.1.0" -source = "git+https://github.com/StarCitizenToolBox/unp4k_rs?tag=V0.0.2#02867472dda1c18e81b0f635b8653fa86bd145cb" +source = "git+https://github.com/StarCitizenToolBox/unp4k_rs?rev=b55d64934bde37bb1079c2c3e2996c8286532914#b55d64934bde37bb1079c2c3e2996c8286532914" dependencies = [ "aes", "anyhow", @@ -6199,7 +6161,7 @@ dependencies = [ "globset", "indicatif", "quick-xml 0.38.4", - "rayon", + "sha2", "thiserror 2.0.17", "zip", "zstd", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 947b174..d3fe56d 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -31,13 +31,11 @@ tokenizers = { version = "0.22.2", default-features = false, features = ["onig"] ndarray = "0.17.1" serde_json = "1.0.145" serde = { version = "1.0.228", features = ["derive"] } -unp4k_rs = { git = "https://github.com/StarCitizenToolBox/unp4k_rs", tag = "V0.0.2" } +unp4k_rs = { git = "https://github.com/StarCitizenToolBox/unp4k_rs", rev = "b55d64934bde37bb1079c2c3e2996c8286532914" } uuid = { version = "1.19.0", features = ["v4"] } parking_lot = "0.12.5" crossbeam-channel = "0.5.15" librqbit = { git = "https://github.com/StarCitizenToolBox/rqbit", rev = "f8c0b0927904e1d8b0e28e708bd69fd8069d413a" } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } bytes = "1.10" # WebView diff --git a/rust/src/api/downloader_api.rs b/rust/src/api/downloader_api.rs index 3ff619f..18e5fc5 100644 --- a/rust/src/api/downloader_api.rs +++ b/rust/src/api/downloader_api.rs @@ -7,7 +7,9 @@ use anyhow::{bail, Context, Result}; use bytes::Bytes; use flutter_rust_bridge::frb; use librqbit::{ - AddTorrent, AddTorrentOptions, AddTorrentResponse, ManagedTorrent, Session, SessionOptions, SessionPersistenceConfig, TorrentStats, TorrentStatsState, WebSeedConfig, api::TorrentIdOrHash, dht::PersistentDhtConfig, limits::LimitsConfig + api::TorrentIdOrHash, dht::PersistentDhtConfig, limits::LimitsConfig, AddTorrent, + AddTorrentOptions, AddTorrentResponse, ManagedTorrent, Session, SessionOptions, + SessionPersistenceConfig, TorrentStats, TorrentStatsState, WebSeedConfig, }; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; @@ -72,7 +74,7 @@ pub struct DownloadGlobalStat { } /// Initialize the download manager session with persistence enabled -/// +/// /// Parameters: /// - working_dir: The directory to store session data (persistence, DHT, etc.) /// - default_download_dir: The default directory to store downloads @@ -90,7 +92,7 @@ pub async fn downloader_init( } let _lock = SESSION_INIT_LOCK.lock().await; - + // Double check after acquiring lock if SESSION.read().is_some() { return Ok(()); @@ -99,15 +101,15 @@ pub async fn downloader_init( // Working directory for persistence and session data let working_folder = PathBuf::from(&working_dir); std::fs::create_dir_all(&working_folder)?; - + // Default download folder let output_folder = PathBuf::from(&default_download_dir); std::fs::create_dir_all(&output_folder)?; - + // Create persistence folder for session state in working directory let persistence_folder = working_folder.join("rqbit-session"); std::fs::create_dir_all(&persistence_folder)?; - + // DHT persistence file in working directory let dht_persistence_file = working_folder.join("dht.json"); @@ -131,7 +133,7 @@ pub async fn downloader_init( upload_bps: upload_limit_bps.and_then(NonZeroU32::new), download_bps: download_limit_bps.and_then(NonZeroU32::new), }, - webseed_config: Some(WebSeedConfig{ + webseed_config: Some(WebSeedConfig { max_concurrent_per_source: 32, max_total_concurrent: 64, request_timeout_secs: 30, @@ -162,21 +164,21 @@ pub fn downloader_is_initialized() -> bool { /// Check if there are pending tasks to restore from session file (without starting the downloader) /// This reads the session.json file directly to check if there are any torrents saved. -/// +/// /// Parameters: /// - working_dir: The directory where session data is stored (same as passed to downloader_init) -/// +/// /// Returns: true if there are tasks to restore, false otherwise #[frb(sync)] pub fn downloader_has_pending_session_tasks(working_dir: String) -> bool { let session_file = PathBuf::from(&working_dir) .join("rqbit-session") .join("session.json"); - + if !session_file.exists() { return false; } - + // Try to read and parse the session file match std::fs::read_to_string(&session_file) { Ok(content) => { @@ -200,7 +202,8 @@ pub fn downloader_has_pending_session_tasks(working_dir: String) -> bool { /// Helper function to get session fn get_session() -> Result> { - SESSION.read() + SESSION + .read() .clone() .context("Downloader not initialized. Call downloader_init first.") } @@ -321,7 +324,10 @@ pub async fn downloader_add_url( .context("Failed to download torrent file")?; if !response.status().is_success() { - bail!("Failed to download torrent file: HTTP {}", response.status()); + bail!( + "Failed to download torrent file: HTTP {}", + response.status() + ); } let bytes = response @@ -345,7 +351,10 @@ pub async fn downloader_pause(task_id: usize) -> Result<()> { }; if let Some(handle) = handle { - session.pause(&handle).await.context("Failed to pause torrent")?; + session + .pause(&handle) + .await + .context("Failed to pause torrent")?; Ok(()) } else { bail!("Task not found: {}", task_id) @@ -362,7 +371,10 @@ pub async fn downloader_resume(task_id: usize) -> Result<()> { }; if let Some(handle) = handle { - session.unpause(&handle).await.context("Failed to resume torrent")?; + session + .unpause(&handle) + .await + .context("Failed to resume torrent")?; Ok(()) } else { bail!("Task not found: {}", task_id) @@ -427,7 +439,9 @@ pub async fn downloader_get_task_info(task_id: usize) -> Result DownloadTaskStatus { if stats.error.is_some() { return DownloadTaskStatus::Error; } - + if stats.finished { return DownloadTaskStatus::Finished; } @@ -508,7 +522,9 @@ pub async fn downloader_get_all_tasks() -> Result> { let (download_speed, upload_speed, num_peers) = if let Some(live) = &stats.live { let down = (live.download_speed.mbps * 1024.0 * 1024.0) as u64; let up = (live.upload_speed.mbps * 1024.0 * 1024.0) as u64; - let peers = (live.snapshot.peer_stats.queued + live.snapshot.peer_stats.connecting + live.snapshot.peer_stats.live) as usize; + let peers = (live.snapshot.peer_stats.queued + + live.snapshot.peer_stats.connecting + + live.snapshot.peer_stats.live) as usize; (down, up, peers) } else { (0, 0, 0) @@ -536,7 +552,7 @@ pub async fn downloader_get_all_tasks() -> Result> { // Merge cached completed tasks with IDs based on cache index (10000 + index) let mut result = tasks.into_inner(); let completed_tasks_cache = COMPLETED_TASKS_CACHE.read(); - + for (cache_index, task) in completed_tasks_cache.iter().enumerate() { let mut task_with_id = task.clone(); // Assign ID based on cache index: 10000, 10001, 10002, etc. @@ -552,11 +568,11 @@ pub async fn downloader_get_global_stats() -> Result { let tasks = downloader_get_all_tasks().await?; let mut stat = DownloadGlobalStat::default(); - + for task in &tasks { stat.download_speed += task.download_speed; stat.upload_speed += task.upload_speed; - + match task.status { DownloadTaskStatus::Live => stat.num_active += 1, DownloadTaskStatus::Paused | DownloadTaskStatus::Checking => stat.num_waiting += 1, @@ -568,20 +584,23 @@ pub async fn downloader_get_global_stats() -> Result { } /// Check if a task with given name exists -/// +/// /// Parameters: /// - name: Task name to search for /// - downloading_only: If true, only search in active/waiting tasks. If false, include completed tasks (default: true) pub async fn downloader_is_name_in_task(name: String, downloading_only: Option) -> bool { let downloading_only = downloading_only.unwrap_or(true); - + if let Ok(tasks) = downloader_get_all_tasks().await { for task in tasks { // If downloading_only is true, skip finished and error tasks - if downloading_only && (task.status == DownloadTaskStatus::Finished || task.status == DownloadTaskStatus::Error) { + if downloading_only + && (task.status == DownloadTaskStatus::Finished + || task.status == DownloadTaskStatus::Error) + { continue; } - + if task.name.contains(&name) { return true; } @@ -595,11 +614,11 @@ pub async fn downloader_pause_all() -> Result<()> { let session = get_session()?; let handles: Vec<_> = TORRENT_HANDLES.read().values().cloned().collect(); - + for handle in handles { let _ = session.pause(&handle).await; } - + Ok(()) } @@ -608,11 +627,11 @@ pub async fn downloader_resume_all() -> Result<()> { let session = get_session()?; let handles: Vec<_> = TORRENT_HANDLES.read().values().cloned().collect(); - + for handle in handles { let _ = session.unpause(&handle).await; } - + Ok(()) } @@ -632,11 +651,11 @@ pub async fn downloader_shutdown() -> Result<()> { let mut guard = SESSION.write(); guard.take() }; - + if let Some(session) = session_opt { session.stop().await; } - + TORRENT_HANDLES.write().clear(); // Clear completed tasks cache on shutdown COMPLETED_TASKS_CACHE.write().clear(); @@ -657,7 +676,7 @@ pub fn downloader_clear_completed_tasks_cache() { } /// Update global speed limits -/// Note: rqbit Session doesn't support runtime limit changes, +/// Note: rqbit Session doesn't support runtime limit changes, /// this function is a placeholder that returns an error. /// Speed limits should be set during downloader_init. pub async fn downloader_update_speed_limits( @@ -676,7 +695,7 @@ pub async fn downloader_remove_completed_tasks() -> Result { let tasks = downloader_get_all_tasks().await?; let mut removed_count = 0u32; - + for task in tasks { if task.status == DownloadTaskStatus::Finished { // Only process active tasks (id < 10000) @@ -684,7 +703,11 @@ pub async fn downloader_remove_completed_tasks() -> Result { let has_handle = TORRENT_HANDLES.read().contains_key(&task.id); if has_handle { // Use TorrentIdOrHash::Id for deletion - if session.delete(TorrentIdOrHash::Id(task.id), false).await.is_ok() { + if session + .delete(TorrentIdOrHash::Id(task.id), false) + .await + .is_ok() + { // Cache the task - it will get ID based on cache length COMPLETED_TASKS_CACHE.write().push(task.clone()); TORRENT_HANDLES.write().remove(&task.id); @@ -694,7 +717,7 @@ pub async fn downloader_remove_completed_tasks() -> Result { } } } - + Ok(removed_count) } @@ -702,8 +725,9 @@ pub async fn downloader_remove_completed_tasks() -> Result { pub async fn downloader_has_active_tasks() -> bool { if let Ok(tasks) = downloader_get_all_tasks().await { for task in tasks { - if task.status != DownloadTaskStatus::Finished - && task.status != DownloadTaskStatus::Error { + if task.status != DownloadTaskStatus::Finished + && task.status != DownloadTaskStatus::Error + { return true; } } diff --git a/rust/src/api/unp4k_api.rs b/rust/src/api/unp4k_api.rs index 43f6877..3335117 100644 --- a/rust/src/api/unp4k_api.rs +++ b/rust/src/api/unp4k_api.rs @@ -3,7 +3,7 @@ use flutter_rust_bridge::frb; use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, Mutex}; -use unp4k::{P4kEntry, P4kFile}; +use unp4k::{CryXmlReader, P4kEntry, P4kFile}; /// P4K 文件项信息 #[frb(dart_metadata=("freezed"))] @@ -32,7 +32,11 @@ fn dos_datetime_to_millis(date: u16, time: u16) -> i64 { let days_since_epoch = { let mut days = 0i64; for y in 1970..year { - days += if (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) { 366 } else { 365 }; + days += if (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) { + 366 + } else { + 365 + }; } let days_in_months = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; if month >= 1 && month <= 12 { @@ -45,7 +49,8 @@ fn dos_datetime_to_millis(date: u16, time: u16) -> i64 { days }; - (days_since_epoch * 86400 + (hour as i64) * 3600 + (minute as i64) * 60 + (second as i64)) * 1000 + (days_since_epoch * 86400 + (hour as i64) * 3600 + (minute as i64) * 60 + (second as i64)) + * 1000 } // 全局 P4K 读取器实例(用于保持状态) @@ -132,6 +137,32 @@ pub async fn p4k_get_all_files() -> Result> { pub async fn p4k_extract_to_memory(file_path: String) -> Result> { // 确保文件列表已加载 tokio::task::spawn_blocking(|| ensure_files_loaded()).await??; + // 获取文件 entry 的克隆 + let entry = p4k_get_entry(file_path).await?; + + // 在后台线程执行阻塞的提取操作 + let data = tokio::task::spawn_blocking(move || { + let mut reader = GLOBAL_P4K_READER.lock().unwrap(); + if reader.is_none() { + return Err(anyhow!("P4K reader not initialized")); + } + let data = reader.as_mut().unwrap().extract_entry(&entry)?; + if (entry.name.ends_with(".xml") || entry.name.ends_with(".mtl")) + && CryXmlReader::is_cryxml(&data) + { + let cry_xml_string = CryXmlReader::parse(&data)?; + return Ok(cry_xml_string.into_bytes()); + } + Ok::<_, anyhow::Error>(data) + }) + .await??; + + Ok(data) +} + +async fn p4k_get_entry(file_path: String) -> Result { + // 确保文件列表已加载 + tokio::task::spawn_blocking(|| ensure_files_loaded()).await??; // 规范化路径 let normalized_path = if file_path.starts_with("\\") { @@ -149,34 +180,27 @@ pub async fn p4k_extract_to_memory(file_path: String) -> Result> { .clone() }; - // 在后台线程执行阻塞的提取操作 - let data = tokio::task::spawn_blocking(move || { - let mut reader = GLOBAL_P4K_READER.lock().unwrap(); - if reader.is_none() { - return Err(anyhow!("P4K reader not initialized")); - } - let data = reader.as_mut().unwrap().extract_entry(&entry)?; - Ok::<_, anyhow::Error>(data) - }) - .await??; - - Ok(data) + Ok(entry) } /// 提取文件到磁盘 pub async fn p4k_extract_to_disk(file_path: String, output_path: String) -> Result<()> { - let data = p4k_extract_to_memory(file_path).await?; - + let entry = p4k_get_entry(file_path).await?; + // 在后台线程执行阻塞的文件写入操作 tokio::task::spawn_blocking(move || { let output = PathBuf::from(&output_path); - // 创建父目录 if let Some(parent) = output.parent() { std::fs::create_dir_all(parent)?; } - std::fs::write(output, data)?; + let mut reader_guard = GLOBAL_P4K_READER.lock().unwrap(); + let reader = reader_guard + .as_mut() + .ok_or_else(|| anyhow!("P4K reader not initialized"))?; + + unp4k::p4k_utils::extract_single_file(reader, &entry, &output, true)?; Ok::<_, anyhow::Error>(()) }) .await??;