Compare commits

...

2 Commits

Author SHA1 Message Date
b8a921022b Fix bug 2026-04-14 18:04:18 +08:00
f213fc6d8b Add cache and bug fix 2026-04-14 17:39:56 +08:00
2 changed files with 214 additions and 106 deletions

View File

@@ -1,15 +1,18 @@
use anyhow::Result;
use msg_tool::ext::io::MutexWrapper;
use msg_tool::ext::mutex::*;
use msg_tool::scripts::base::ReadSeek;
use msg_tool::scripts::{BUILDER, ScriptBuilder};
use msg_tool::types::{ExtraConfig, ImageOutputType, ScriptType};
use msg_tool::scripts::{BUILDER, Script, ScriptBuilder};
use msg_tool::types::{ExtraConfig, ImageOutputType, PngCompressionLevel, ScriptType};
use msg_tool::utils::img::*;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs::File;
use std::io::Read;
use std::io::{BufReader, Read};
use std::path::Path;
use std::sync::Mutex;
use std::sync::{Arc, Mutex};
use tauri::{AppHandle, Manager};
use tauri::ipc::Response;
const PNG_SIGNATURE: &[u8] = b"\x89PNG\r\n\x1a\n";
@@ -17,6 +20,175 @@ lazy_static::lazy_static! {
static ref ENTRY_TYPE_CACHE: Mutex<BTreeMap<ScriptType, EntryType>> = Mutex::new(BTreeMap::new());
}
struct CacheEntry<'a> {
archive: Box<dyn Script + Send + Sync + 'a>,
path: String,
option: FileOptions,
}
impl<'a> CacheEntry<'a> {
fn new(
mut reader: Box<dyn ReadSeek + Send + Sync + 'a>,
filename: &str,
option: FileOptions,
script_type: Option<ScriptType>,
) -> Result<Self> {
let (entry_type, msg_tool_type) = if let Some(typ) = script_type {
let entry_type = if typ.is_audio() {
EntryType::Audio
} else {
query_entry_type(&typ)
};
(entry_type, Some(typ.clone()))
} else {
let mut buffer = [0; 1024];
let n = reader.read(&mut buffer)?;
reader.rewind()?;
detect_file_type(filename, &buffer[..n])
};
if entry_type != EntryType::Archive {
return Err(anyhow::anyhow!("不是归档文件"));
}
let script_type = msg_tool_type.ok_or_else(|| anyhow::anyhow!("无法识别的归档格式"))?;
let builder = BUILDER
.iter()
.find(|b| b.script_type() == &script_type)
.ok_or_else(|| anyhow::anyhow!("不支持的归档格式"))?;
let extra_config = option.to_extra_config();
let encoding = builder.default_encoding();
let archive_encoding = builder.default_archive_encoding().unwrap_or(encoding);
let archive = builder.build_script_from_reader(
reader,
filename,
encoding,
archive_encoding,
&extra_config,
None,
)?;
Ok(CacheEntry {
archive,
path: filename.to_string(),
option,
})
}
}
struct CacheManager {
file: Option<Arc<Mutex<BufReader<File>>>>,
cache: Option<CacheEntry<'static>>,
}
impl CacheManager {
fn list_archive(
&mut self,
path: &str,
option: Option<&Vec<FileOptions>>,
) -> Result<Vec<Entry>> {
let paths = path.split("|").collect::<Vec<_>>();
let cache = self.get_entry(path, option)?;
if paths.len() == 1 {
let mut result = Vec::new();
let mut index = 0;
for entry in cache.archive.iter_archive_filename()? {
let name = entry?;
let mut entry = cache.archive.open_file(index)?;
index += 1;
let (entry_type, msg_tool_type) = if let Some(typ) = entry.script_type() {
let entry_type = if typ.is_audio() {
EntryType::Audio
} else {
query_entry_type(&typ)
};
(entry_type, Some(typ.clone()))
} else {
let mut buffer = [0; 1024];
let n = entry.read(&mut buffer)?;
detect_file_type(&name, &buffer[..n])
};
// 扁平结构,不区分文件夹,前端根据路径解析出文件夹结构
result.push(Entry {
name,
is_dir: false,
entry_type,
msg_tool_type,
size: None,
});
}
Ok(result)
} else {
let filename = path.split("|").nth(1).unwrap();
let mut entry = cache.archive.open_file_by_name(filename, false)?;
let typ = entry.script_type().map(|t| t.clone());
let entry = entry.to_data()?;
let path = path.splitn(2, "|").nth(1).unwrap();
list_archive_directory_in_archive(path, entry, filename, option, typ, 1)
}
}
fn preview_image(&mut self, path: &str, option: Option<&Vec<FileOptions>>) -> Result<Vec<u8>> {
let cache = self.get_entry(path, option)?;
let paths = path.split("|").collect::<Vec<_>>();
if paths.len() == 2 {
let filename = paths[1];
let mut entry = cache.archive.open_file_by_name(filename, false)?;
let typ = entry.script_type().map(|t| t.clone());
let entry = entry.to_data()?;
preview_image_in_directory(entry, filename, option, typ, 1)
} else {
let filename = paths[1];
let mut entry = cache.archive.open_file_by_name(filename, false)?;
let typ = entry.script_type().map(|t| t.clone());
let entry = entry.to_data()?;
let path = path.splitn(2, "|").nth(1).unwrap();
preview_image_in_archive(path, entry, filename, option, typ, 1)
}
}
// 目前只能缓存根归档文件
fn get_entry(
&mut self,
path: &str,
option: Option<&Vec<FileOptions>>,
) -> Result<&CacheEntry<'static>> {
let paths = path.split("|").collect::<Vec<_>>();
let is_hit = self.cache.as_ref().map_or(false, |cache| {
if paths[0] == cache.path {
let opt = option
.and_then(|opts| opts.get(0).cloned())
.unwrap_or_default();
if opt == cache.option {
return true;
}
}
false
});
let entry = if is_hit {
self.cache.as_ref().unwrap()
} else {
// 关闭当前缓存的文件和归档
self.cache.take();
self.file.take();
let file = Arc::new(Mutex::new(BufReader::new(File::open(paths[0])?)));
let option = option
.and_then(|opts| opts.get(0).cloned())
.unwrap_or_default();
let mfile = MutexWrapper::new(file.clone(), 0);
let cache_entry = CacheEntry::new(Box::new(mfile), paths[0], option, None)?;
self.file = Some(file.clone());
self.cache = Some(cache_entry);
self.cache.as_ref().unwrap()
};
Ok(entry)
}
}
lazy_static::lazy_static!(
static ref CACHE_MANAGER: Mutex<CacheManager> = Mutex::new(CacheManager {
file: None,
cache: None,
});
);
fn query_entry_type(script_type: &ScriptType) -> EntryType {
let mut cache = ENTRY_TYPE_CACHE.lock().unwrap();
if let Some(entry_type) = cache.get(script_type) {
@@ -90,7 +262,7 @@ pub struct GameTitle {
alias: Option<Vec<String>>,
}
#[derive(Debug, Default, Deserialize, Clone)]
#[derive(Debug, Default, Deserialize, Clone, PartialEq, Eq)]
pub struct Xp3Option {
/// 设置游戏标题,用于解密xp3文件
game_title: Option<String>,
@@ -98,7 +270,7 @@ pub struct Xp3Option {
force_decrypt: bool,
}
#[derive(Debug, Default, Deserialize, Clone)]
#[derive(Debug, Default, Deserialize, Clone, PartialEq, Eq)]
pub struct FileOptions {
xp3: Option<Xp3Option>,
}
@@ -199,11 +371,16 @@ fn detect_file_type(filename: &str, data: &[u8]) -> (EntryType, Option<ScriptTyp
(EntryType::Unknown, None)
}
fn try_detect_file_type<P: AsRef<std::path::Path> + ?Sized>(path: &P) -> Result<(EntryType, Option<ScriptType>)> {
fn try_detect_file_type<P: AsRef<std::path::Path> + ?Sized>(
path: &P,
) -> Result<(EntryType, Option<ScriptType>)> {
let mut file = File::open(path)?;
let mut buffer = [0; 1024];
let n = file.read(&mut buffer)?;
Ok(detect_file_type(&path.as_ref().to_string_lossy(), &buffer[..n]))
Ok(detect_file_type(
&path.as_ref().to_string_lossy(),
&buffer[..n],
))
}
fn list_fs_directory(path: &Path) -> Result<Vec<Entry>> {
@@ -242,64 +419,6 @@ fn list_fs_directory(path: &Path) -> Result<Vec<Entry>> {
Ok(result)
}
fn list_archive_directory(path: &Path, option: Option<&Vec<FileOptions>>) -> Result<Vec<Entry>> {
let option = option
.and_then(|opts| opts.get(0).cloned())
.unwrap_or_default();
let mut header = [0; 1024];
let n = {
let mut file = File::open(path)?;
file.read(&mut header)?
};
let (entry_type, msg_tool_type) = detect_file_type(&path.to_string_lossy(), &header[..n]);
if entry_type != EntryType::Archive {
return Err(anyhow::anyhow!("不是归档文件"));
}
let script_type = msg_tool_type.ok_or_else(|| anyhow::anyhow!("无法识别的归档格式"))?;
let builder = BUILDER
.iter()
.find(|b| b.script_type() == &script_type)
.ok_or_else(|| anyhow::anyhow!("不支持的归档格式"))?;
let extra_config = option.to_extra_config();
let encoding = builder.default_encoding();
let archive_encoding = builder.default_archive_encoding().unwrap_or(encoding);
let archive = builder.build_script_from_file(
&path.to_string_lossy(),
encoding,
archive_encoding,
&extra_config,
None,
)?;
let mut result = Vec::new();
let mut index = 0;
for entry in archive.iter_archive_filename()? {
let name = entry?;
let mut entry = archive.open_file(index)?;
index += 1;
let (entry_type, msg_tool_type) = if let Some(typ) = entry.script_type() {
let entry_type = if typ.is_audio() {
EntryType::Audio
} else {
query_entry_type(&typ)
};
(entry_type, Some(typ.clone()))
} else {
let mut buffer = [0; 1024];
let n = entry.read(&mut buffer)?;
detect_file_type(&name, &buffer[..n])
};
// 扁平结构,不区分文件夹,前端根据路径解析出文件夹结构
result.push(Entry {
name,
is_dir: false,
entry_type,
msg_tool_type,
size: None,
});
}
Ok(result)
}
fn list_archive_directory_in_archive<'a>(
path: &str,
mut reader: Box<dyn ReadSeek + Send + Sync + 'a>,
@@ -415,25 +534,13 @@ pub fn list_directory(
options: Option<Vec<FileOptions>>,
) -> Result<Vec<Entry>, ErrorMsg> {
if path.contains("|") {
let filename = path.split("|").nth(0).unwrap();
let reader = Box::new(std::io::BufReader::new(File::open(filename).map_err(
|e| ErrorMsg {
typ: ErrorType::NotFound,
msg: format!("无法打开文件: {}", e),
},
)?));
return list_archive_directory_in_archive(
path,
reader,
filename,
options.as_ref(),
None,
0,
)
.map_err(|e| ErrorMsg {
typ: ErrorType::Other,
msg: e.to_string(),
});
return CACHE_MANAGER
.lock_blocking()
.list_archive(path, options.as_ref())
.map_err(|e| ErrorMsg {
typ: ErrorType::Other,
msg: e.to_string(),
});
}
let path = std::path::Path::new(path);
if !path.exists() {
@@ -446,10 +553,13 @@ pub fn list_directory(
if let Some(parent) = path.parent() {
let _ = set_last_directory(&app, parent.to_string_lossy().as_ref());
}
return list_archive_directory(path, options.as_ref()).map_err(|e| ErrorMsg {
typ: ErrorType::Other,
msg: e.to_string(),
});
return CACHE_MANAGER
.lock_blocking()
.list_archive(&path.to_string_lossy(), options.as_ref())
.map_err(|e| ErrorMsg {
typ: ErrorType::Other,
msg: e.to_string(),
});
}
let _ = set_last_directory(&app, path.to_string_lossy().as_ref());
list_fs_directory(path).map_err(|e| ErrorMsg {
@@ -503,7 +613,8 @@ fn preview_image_in_directory<'a>(
.as_ref()
.and_then(|opts| opts.get(index).cloned())
.unwrap_or_default();
let extra_config = option.to_extra_config();
let mut extra_config = option.to_extra_config();
extra_config.png_compression_level = PngCompressionLevel::Fast;
let encoding = builder.default_encoding();
let archive_encoding = builder.default_archive_encoding().unwrap_or(encoding);
let image = builder.build_script_from_reader(
@@ -550,9 +661,6 @@ fn preview_image_in_archive<'a>(
reader.rewind()?;
detect_file_type("", &buffer[..n])
};
if entry_type != EntryType::Archive {
return Err(anyhow::anyhow!("不是归档文件"));
}
let msg_tool_type = msg_tool_type.ok_or_else(|| anyhow::anyhow!("无法识别的归档格式"))?;
let builder = BUILDER
.iter()
@@ -570,6 +678,9 @@ fn preview_image_in_archive<'a>(
None,
)?;
if path.contains("|") {
if !archive.is_archive() {
return Err(anyhow::anyhow!("不是归档文件"));
}
let filename = path.split("|").nth(0).unwrap();
let mut entry = archive.open_file_by_name(filename, false)?;
let typ = entry.script_type().map(|t| t.clone());
@@ -592,20 +703,16 @@ fn preview_image_in_archive<'a>(
/// path: /path/to/archive.zip|image.png 预览archive.zip内的image.png,options[0] 会用于打开archive.zip, options[1] 会用于打开image.png(如果需要的话)
/// path: /path/to/archive.zip|inner/archive2.zip|image.png 预览archive2.zip内的image.png,options[0] 会用于打开archive.zip, options[1] 会用于打开archive2.zip, options[2] 会用于打开image.png(如果需要的话)
#[tauri::command]
pub fn preview_image(path: &str, options: Option<Vec<FileOptions>>) -> Result<Vec<u8>, ErrorMsg> {
pub fn preview_image(path: &str, options: Option<Vec<FileOptions>>) -> Result<Response, ErrorMsg> {
if path.contains("|") {
let filename = path.split("|").nth(0).unwrap();
let reader = Box::new(std::io::BufReader::new(File::open(filename).map_err(
|e| ErrorMsg {
typ: ErrorType::NotFound,
msg: format!("无法打开文件: {}", e),
},
)?));
let path = path.splitn(2, "|").nth(1).unwrap();
return preview_image_in_archive(path, reader, filename, options.as_ref(), None, 0)
return CACHE_MANAGER
.lock_blocking()
.preview_image(path, options.as_ref())
.map_err(|e| ErrorMsg {
typ: ErrorType::Other,
msg: format!("预览图片失败: {}", e),
msg: e.to_string(),
}).map(|data| {
Response::new(data)
});
}
let path = std::path::Path::new(path);
@@ -636,5 +743,7 @@ pub fn preview_image(path: &str, options: Option<Vec<FileOptions>>) -> Result<Ve
.map_err(|e| ErrorMsg {
typ: ErrorType::Other,
msg: format!("预览图片失败: {}", e),
}).map(|data| {
Response::new(data)
})
}

View File

@@ -14,6 +14,5 @@ export async function listDirectory(path: string, options?: FileOptions[]): Prom
}
export async function previewImage(path: string, options?: FileOptions[]): Promise<Uint8Array> {
const bytes: number[] = await invoke("preview_image", { path, options: options ?? null });
return new Uint8Array(bytes);
return await invoke("preview_image", { path, options: options ?? null });
}