tauri-plugin-deeprespond-im 1.0.0-alpha

深应智能 IM 的 Tauri 插件:本地存储、安全存储、窗口间的锁与消息、系统事件、通知、角标、文件
//! 文件的命令(《4.实现与测试》第 3.6 节;参数见插件协议的“文件”一节)。
//!
//! 全部是异步命令:同步命令在主线程上执行,读写文件、等待对话框都不能占住主线程。
//! 引擎专用:files_persist、files_release、files_upload_part(附件的上传由引擎中的核心执行)。

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use serde_json::{json, Value};
use tauri::ipc::{InvokeBody, Request, Response};
use tauri::{AppHandle, Runtime, State, Webview};

use super::dialog::{self, Filter};
use super::image::{prepare_image, ImageOpts};
use super::transfer::{Expect, UploadPart};
use super::{open, read, video};
use crate::commands::{caller, require_engine};
use crate::error::{Error, Result};
use crate::Shared;

/// 在阻塞线程池中执行文件操作。
async fn blocking<T: Send + 'static>(f: impl FnOnce() -> Result<T> + Send + 'static) -> Result<T> {
    tauri::async_runtime::spawn_blocking(f).await.map_err(|e| Error::internal(e.to_string()))?
}

#[tauri::command(rename_all = "snake_case")]
pub async fn files_pick<R: Runtime>(app: AppHandle<R>, webview: Webview<R>, state: State<'_, Arc<Shared>>, multiple: Option<bool>, filters: Option<Vec<Filter>>) -> Result<Vec<String>> {
    caller(&state, &webview)?;
    let picked = dialog::pick(&app, Some(webview.window()), multiple.unwrap_or(false), filters.unwrap_or_default()).await?;
    let shared = state.inner().clone();
    blocking(move || Ok(picked.into_iter().filter(|p| shared.files.allow.register_read(p).is_some()).map(|p| p.to_string_lossy().into_owned()).collect())).await
}

#[tauri::command(rename_all = "snake_case")]
pub async fn files_pick_save<R: Runtime>(app: AppHandle<R>, webview: Webview<R>, state: State<'_, Arc<Shared>>, default_name: Option<String>) -> Result<Option<String>> {
    caller(&state, &webview)?;
    let directory = state.files.allow.downloads().map(Path::to_path_buf);
    let picked = dialog::pick_save(&app, Some(webview.window()), default_name, directory).await?;
    Ok(picked.and_then(|p| state.files.allow.register_write(&p).map(|_| p.to_string_lossy().into_owned())))
}

#[tauri::command(rename_all = "snake_case")]
pub async fn files_stat<R: Runtime>(webview: Webview<R>, state: State<'_, Arc<Shared>>, path: String) -> Result<read::FileInfo> {
    caller(&state, &webview)?;
    let shared = state.inner().clone();
    blocking(move || {
        let real = shared.files.check_read(Path::new(&path))?;
        read::stat(&path, &real)
    })
    .await
}

#[tauri::command(rename_all = "snake_case")]
pub async fn files_read<R: Runtime>(webview: Webview<R>, state: State<'_, Arc<Shared>>, path: String, offset: u64, length: u64) -> Result<Response> {
    caller(&state, &webview)?;
    let shared = state.inner().clone();
    let bytes = blocking(move || {
        let real = shared.files.check_read(Path::new(&path))?;
        read::read_range(&real, offset, length)
    })
    .await?;
    Ok(Response::new(bytes))
}

/// Blob 分块写入临时目录:原始请求体,请求头 x-drim-handle(空表示新建)、x-drim-name(encodeURIComponent 过的文件名)、
/// x-drim-final("1" 表示最后一块)。
#[tauri::command(rename_all = "snake_case")]
pub async fn files_write<R: Runtime>(webview: Webview<R>, state: State<'_, Arc<Shared>>, request: Request<'_>) -> Result<read::WriteResult> {
    let instance = caller(&state, &webview)?;
    let header = |name: &str| request.headers().get(name).and_then(|v| v.to_str().ok()).unwrap_or("").to_string();
    let handle = header("x-drim-handle");
    let name = percent_encoding::percent_decode_str(&header("x-drim-name")).decode_utf8_lossy().into_owned();
    let last = header("x-drim-final") == "1";
    let bytes = match request.body() {
        InvokeBody::Raw(bytes) => bytes.clone(),
        InvokeBody::Json(Value::Null) => Vec::new(),
        InvokeBody::Json(_) => return Err(Error::validation("invalid_body", "files_write 的请求体必须是二进制")),
    };
    let shared = state.inner().clone();
    blocking(move || shared.files.writes.write(&shared.paths.temp, &instance.label, instance.id, &handle, &name, last, &bytes)).await
}

#[tauri::command(rename_all = "snake_case")]
pub async fn files_persist<R: Runtime>(webview: Webview<R>, state: State<'_, Arc<Shared>>, path: String, temp: Option<bool>, app_key: String, username: String, copy_max_bytes: u64) -> Result<super::Persisted> {
    require_engine(&webview)?;
    caller(&state, &webview)?;
    let shared = state.inner().clone();
    blocking(move || shared.files.persist(&path, temp.unwrap_or(false), &app_key, &username, copy_max_bytes)).await
}

#[tauri::command(rename_all = "snake_case")]
pub async fn files_release<R: Runtime>(webview: Webview<R>, state: State<'_, Arc<Shared>>, path: String, owned: bool) -> Result<()> {
    require_engine(&webview)?;
    caller(&state, &webview)?;
    let shared = state.inner().clone();
    blocking(move || shared.files.release(&path, owned)).await
}

#[tauri::command(rename_all = "snake_case")]
#[allow(clippy::too_many_arguments)]
pub async fn files_upload_part<R: Runtime>(
    webview: Webview<R>,
    state: State<'_, Arc<Shared>>,
    task: String,
    path: String,
    offset: u64,
    length: u64,
    url: String,
    method: Option<String>,
    headers: Option<HashMap<String, String>>,
    expect: Option<Expect>,
    patches: Option<Vec<u64>>,
) -> Result<Value> {
    require_engine(&webview)?;
    let instance = caller(&state, &webview)?;
    let shared = state.inner().clone();
    let progress: Arc<dyn Fn(u64) + Send + Sync> = {
        let (shared, instance, task) = (shared.clone(), instance.clone(), task.clone());
        Arc::new(move |loaded| {
            if shared.hub.is_current(&instance.label, instance.id) {
                instance.send(json!({ "t": "upload_progress", "task": task, "loaded": loaded }));
            }
        })
    };
    let part = UploadPart {
        path: PathBuf::new(),
        offset,
        length,
        url,
        method: method.filter(|m| !m.is_empty()).unwrap_or_else(|| "PUT".into()),
        headers: headers.unwrap_or_default(),
        expect,
        patches: patches.unwrap_or_default(),
    };
    let status = shared.files.upload_part(&task, &instance.label, instance.id, &path, part, progress).await?;
    Ok(json!({ "status": status }))
}

/// 下载:进度发给发起的页面;页面重新加载、窗口关闭后下载照常完成(文件留在目标位置)。
#[tauri::command(rename_all = "snake_case")]
pub async fn files_download<R: Runtime>(
    webview: Webview<R>,
    state: State<'_, Arc<Shared>>,
    task: String,
    url: String,
    dest: Option<String>,
    conflict: Option<String>,
    name: Option<String>,
) -> Result<Value> {
    let instance = caller(&state, &webview)?;
    let shared = state.inner().clone();
    let rename = conflict.as_deref() != Some("overwrite");
    let progress = {
        let (shared, instance, task) = (shared.clone(), instance.clone(), task.clone());
        move |loaded: u64, total: Option<u64>| {
            if shared.hub.is_current(&instance.label, instance.id) {
                instance.send(json!({ "t": "download_progress", "task": task, "loaded": loaded, "total": total }));
            }
        }
    };
    let (path, size) = shared.files.download(&task, &instance.label, instance.id, &url, dest.as_deref(), rename, name.as_deref(), &progress).await?;
    Ok(json!({ "path": path.to_string_lossy(), "size": size }))
}

#[tauri::command(rename_all = "snake_case")]
pub async fn files_cancel<R: Runtime>(webview: Webview<R>, state: State<'_, Arc<Shared>>, task: String) -> Result<()> {
    caller(&state, &webview)?;
    state.files.cancel(&task);
    Ok(())
}

/// 用系统默认程序打开:只限允许读取的路径和 SDK 下载的文件,可执行和脚本类文件拒绝。
#[tauri::command(rename_all = "snake_case")]
pub async fn files_open<R: Runtime>(webview: Webview<R>, state: State<'_, Arc<Shared>>, path: String) -> Result<()> {
    caller(&state, &webview)?;
    let shared = state.inner().clone();
    blocking(move || {
        let real = shared.files.check_read(Path::new(&path))?;
        open::open_path(&real)
    })
    .await
}

#[tauri::command(rename_all = "snake_case")]
pub async fn files_reveal<R: Runtime>(webview: Webview<R>, state: State<'_, Arc<Shared>>, path: String) -> Result<()> {
    caller(&state, &webview)?;
    let real = state.files.check_read(Path::new(&path))?;
    open::reveal(&real).await
}

#[tauri::command(rename_all = "snake_case")]
#[allow(clippy::too_many_arguments)]
pub async fn files_prepare_image<R: Runtime>(
    webview: Webview<R>,
    state: State<'_, Arc<Shared>>,
    path: String,
    original: bool,
    max_side: u32,
    quality: f64,
    max_bytes: u64,
    max_pixels: u64,
) -> Result<super::image::ImageOutcome> {
    caller(&state, &webview)?;
    let shared = state.inner().clone();
    let opts = ImageOpts { original, max_side, quality, max_bytes, max_pixels };
    blocking(move || {
        let real = shared.files.check_read(Path::new(&path))?;
        prepare_image(&path, &real, &shared.paths.temp, &opts)
    })
    .await
}

#[tauri::command(rename_all = "snake_case")]
pub async fn files_prepare_video<R: Runtime>(webview: Webview<R>, state: State<'_, Arc<Shared>>, path: String) -> Result<video::VideoMeta> {
    caller(&state, &webview)?;
    let shared = state.inner().clone();
    blocking(move || {
        let real = shared.files.check_read(Path::new(&path))?;
        video::prepare_video(&real)
    })
    .await
}