This commit is contained in:
fengchuanhn@gmail.com
2026-05-22 18:33:46 +08:00
parent 0eb5dbde5b
commit 2abc2134a8
10 changed files with 891 additions and 31 deletions

View File

@@ -0,0 +1,143 @@
//! 画中画/字幕素材SQLite 元数据 + 本地文件管理
use std::path::{Component, Path, PathBuf};
use chrono::Local;
use serde_json::json;
use tauri::{AppHandle, Manager, State};
use uuid::Uuid;
use crate::material_db::MaterialDb;
use crate::material_store::MaterialStore;
const ALLOWED_EXTS: &[&str] = &[
"mp4", "mov", "avi", "mkv", "jpg", "jpeg", "png", "gif", "bmp", "webp",
];
fn normalize_path(path: &str) -> PathBuf {
Path::new(path)
.components()
.filter(|c| !matches!(c, Component::ParentDir))
.collect()
}
fn materials_dir(app: &AppHandle) -> Result<PathBuf, String> {
let dir = app
.path()
.app_data_dir()
.map_err(|e| format!("无法获取应用数据目录: {e}"))?
.join("video-material");
Ok(dir)
}
fn is_under_materials_dir(root: &Path, target: &Path) -> bool {
if let (Ok(root), Ok(canonical)) = (root.canonicalize(), target.canonicalize()) {
return canonical.starts_with(&root);
}
let root_s = root.to_string_lossy().to_lowercase();
let target_s = target.to_string_lossy().to_lowercase();
target_s.contains("video-material") && target_s.starts_with(&root_s)
}
#[tauri::command]
pub async fn list_materials(store: State<'_, MaterialStore>) -> Result<Vec<crate::material_db::MaterialRecord>, String> {
store.list().await
}
#[tauri::command]
pub async fn update_material_name(
store: State<'_, MaterialStore>,
id: i64,
name: String,
) -> Result<(), String> {
store.update_name(id, name).await
}
#[tauri::command]
pub async fn delete_material(
app: AppHandle,
store: State<'_, MaterialStore>,
id: i64,
) -> Result<(), String> {
let file_path = store.delete(id).await?;
if let Some(path) = file_path {
delete_material_file_if_allowed(&app, &path).await?;
}
Ok(())
}
/// 将用户选择的媒体文件复制到应用数据目录并写入数据库
#[tauri::command]
pub async fn import_material_files(
app: AppHandle,
store: State<'_, MaterialStore>,
source_paths: Vec<String>,
) -> Result<Vec<crate::material_db::MaterialRecord>, String> {
let mut imported = Vec::new();
for raw in source_paths {
let src = normalize_path(raw.trim());
if !src.is_file() {
return Err(format!("文件不存在: {}", src.display()));
}
let ext = src
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
if !ALLOWED_EXTS.contains(&ext.as_str()) {
return Err(format!("不支持的文件格式: .{ext}"));
}
let saved = copy_to_materials_dir(&app, &src, &ext).await?;
let now = chrono::Utc::now().timestamp_millis();
let name = MaterialDb::material_name_from_path(raw.trim());
let material_type = MaterialDb::material_type_from_path(raw.trim());
let info = json!({ "sourcePath": raw.trim() });
let record = store
.insert(
name,
saved,
material_type,
info,
now,
now,
)
.await?;
imported.push(record);
}
Ok(imported)
}
async fn copy_to_materials_dir(
app: &AppHandle,
src: &Path,
ext: &str,
) -> Result<String, String> {
let root = materials_dir(app)?;
let date = Local::now().format("%Y%m%d").to_string();
let dir = root.join(&date);
tokio::fs::create_dir_all(&dir)
.await
.map_err(|e| format!("创建素材目录失败: {e}"))?;
let file_name = format!("{}_{}.{}", Uuid::new_v4(), Local::now().format("%H%M%S"), ext);
let dest = dir.join(file_name);
tokio::fs::copy(src, &dest)
.await
.map_err(|e| format!("复制素材失败: {e}"))?;
Ok(dest.to_string_lossy().to_string())
}
async fn delete_material_file_if_allowed(app: &AppHandle, path: &str) -> Result<(), String> {
let target = normalize_path(path);
let dir = materials_dir(app)?;
if !is_under_materials_dir(&dir, &target) {
return Err("不允许删除该路径下的文件".into());
}
if target.is_file() {
tokio::fs::remove_file(&target)
.await
.map_err(|e| format!("删除文件失败: {e}"))?;
}
Ok(())
}