use std::collections::{HashMap, HashSet, VecDeque};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
use serde::Serialize;
use tauri::plugin::Builder as PluginBuilder;
use tauri::Runtime;
use crate::error::{Error, Result};
use crate::paths::{outbox_dir, Paths};
use crate::Shared;
mod allow;
pub mod commands;
mod dialog;
mod image;
mod open;
pub(crate) mod protocol;
mod read;
mod transfer;
mod video;
#[cfg(test)]
mod tests;
pub use transfer::Cancel;
struct Task {
label: String,
instance: u64,
upload: bool,
cancel: Arc<Cancel>,
}
#[derive(Debug, Serialize, PartialEq)]
pub struct Persisted {
pub path: String,
pub owned: bool,
pub size: u64,
pub mtime: u64,
}
pub struct FilesState {
paths: Paths,
pub(crate) allow: allow::Allow,
writes: read::BlobWrites,
tasks: Mutex<HashMap<String, Task>>,
cancelled_early: Mutex<VecDeque<String>>,
downloading: Mutex<HashSet<PathBuf>>,
parts: transfer::Parts,
proxy: Option<String>,
client: OnceLock<std::result::Result<reqwest::Client, String>>,
}
impl FilesState {
pub fn new(paths: Paths, allow_read_dirs: Vec<PathBuf>, allow_write_dirs: Vec<PathBuf>, proxy: Option<String>) -> Self {
let downloads = download_dir();
let allow = allow::Allow::new(paths.data.clone(), paths.temp.clone(), downloads, allow_read_dirs, allow_write_dirs);
Self {
paths,
allow,
writes: read::BlobWrites::default(),
tasks: Mutex::new(HashMap::new()),
cancelled_early: Mutex::new(VecDeque::new()),
downloading: Mutex::new(HashSet::new()),
parts: transfer::Parts::default(),
proxy,
client: OnceLock::new(),
}
}
pub fn check_write(&self, path: &Path) -> Result<PathBuf> {
self.allow.check_write(path)
}
pub fn check_read(&self, path: &Path) -> Result<PathBuf> {
self.allow.check_read(path)
}
pub fn release_instance(&self, label: &str, instance: u64) {
for task in self.tasks.lock().unwrap().values() {
if task.upload && task.label == label && task.instance == instance {
task.cancel.cancel();
}
}
self.writes.release_instance(label, instance);
}
fn client(&self) -> Result<reqwest::Client> {
let built = self.client.get_or_init(|| transfer::build_client(self.proxy.as_deref(), true));
built.clone().map_err(Error::network)
}
fn start_task(&self, task: &str, label: &str, instance: u64, upload: bool) -> Result<Arc<Cancel>> {
let cancel = Arc::new(Cancel::default());
{
let mut early = self.cancelled_early.lock().unwrap();
if let Some(i) = early.iter().position(|t| t == task) {
early.remove(i);
return Err(Error::aborted());
}
}
let mut tasks = self.tasks.lock().unwrap();
if tasks.contains_key(task) {
return Err(Error::invalid_state("task_running"));
}
tasks.insert(task.to_string(), Task { label: label.to_string(), instance, upload, cancel: cancel.clone() });
Ok(cancel)
}
fn finish_task(&self, task: &str) {
self.tasks.lock().unwrap().remove(task);
}
pub fn cancel(&self, task: &str) {
if let Some(t) = self.tasks.lock().unwrap().get(task) {
t.cancel.cancel();
return;
}
let mut early = self.cancelled_early.lock().unwrap();
early.push_back(task.to_string());
while early.len() > 64 {
early.pop_front();
}
}
#[allow(clippy::too_many_arguments)]
pub async fn upload_part(&self, task: &str, label: &str, instance: u64, path: &str, part: transfer::UploadPart, progress: Arc<dyn Fn(u64) + Send + Sync>) -> Result<u16> {
let real = self.allow.check_read(Path::new(path))?;
let client = self.client()?;
let cancel = self.start_task(task, label, instance, true)?;
let result = transfer::upload_part(&client, transfer::UploadPart { path: real, ..part }, cancel, progress).await;
self.finish_task(task);
result
}
#[allow(clippy::too_many_arguments)]
pub async fn download(&self, task: &str, label: &str, instance: u64, url: &str, dest: Option<&str>, rename: bool, name: Option<&str>, progress: &(dyn Fn(u64, Option<u64>) + Send + Sync)) -> Result<(PathBuf, u64)> {
let fallback_name = || transfer::sanitize_file_name(name.map(str::to_string).or_else(|| transfer::name_from_url(url)).as_deref().unwrap_or(""), "download");
let (dir, file_name, picked) = match dest.filter(|d| !d.is_empty()) {
None => {
let dir = self.allow.downloads().ok_or_else(|| Error::unsupported("downloads_dir_unavailable"))?.to_path_buf();
fs::create_dir_all(&dir).map_err(|e| Error::from_io(&e))?;
let dir = fs::canonicalize(&dir).map_err(|e| Error::from_io(&e))?;
(dir, fallback_name(), false)
}
Some(dest) => {
let real = self.allow.check_write(Path::new(dest))?;
if real.is_dir() || dest.ends_with('/') || dest.ends_with(std::path::MAIN_SEPARATOR) {
fs::create_dir_all(&real).map_err(|e| Error::from_io(&e))?;
(real, fallback_name(), false)
} else {
let dir = real.parent().ok_or_else(|| Error::permission_denied("path_not_allowed"))?.to_path_buf();
fs::create_dir_all(&dir).map_err(|e| Error::from_io(&e))?;
let given = real.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
(dir, transfer::sanitize_file_name(&given, "download"), self.allow.is_picked_save(&real))
}
}
};
let rename = rename && !picked;
let target = {
let mut busy = self.downloading.lock().unwrap();
let target = transfer::choose_target(&dir, &file_name, rename, |p| busy.contains(p));
busy.insert(target.clone());
target
};
let client = self.client();
let result = match client {
Ok(client) => match self.start_task(task, label, instance, false) {
Ok(cancel) => {
let r = transfer::download(&client, url, &target, rename, &self.parts, &cancel, progress).await;
self.finish_task(task);
r
}
Err(err) => Err(err),
},
Err(err) => Err(err),
};
self.downloading.lock().unwrap().remove(&target);
let (path, size) = result?;
open::mark_origin(&path, Some(url));
self.allow.register_read(&path);
Ok((path, size))
}
pub fn persist(&self, path: &str, temp: bool, app_key: &str, username: &str, copy_max_bytes: u64) -> Result<Persisted> {
let real = self.allow.check_read(Path::new(path))?;
let meta = fs::metadata(&real).map_err(|e| Error::from_io_read(&e))?;
if !meta.is_file() {
return Err(Error::validation("file_unreadable", "不是普通文件"));
}
let outbox = outbox_dir(&self.paths.data, app_key, username);
crate::paths::ensure_private_dir(&outbox)?;
let outbox = fs::canonicalize(&outbox).map_err(|e| Error::from_io(&e))?;
if real.starts_with(&outbox) {
return Ok(Persisted { path: path.to_string(), owned: true, size: meta.len(), mtime: read::mtime_ms(&meta) });
}
let name = real.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
let dest = outbox.join(format!("{}-{}", read::random_hex(), strip_random_prefix(&name)));
if self.allow.in_temp(&real) {
move_file(&real, &dest)?;
} else if temp {
log::warn!("标为临时文件的附件不在临时目录中,按普通文件处理");
return self.persist(path, false, app_key, username, copy_max_bytes);
} else if meta.len() <= copy_max_bytes {
if let Err(err) = fs::copy(&real, &dest) {
let _ = fs::remove_file(&dest);
return Err(Error::from_io(&err));
}
} else {
self.allow.persist_add(&real)?;
return Ok(Persisted { path: path.to_string(), owned: false, size: meta.len(), mtime: read::mtime_ms(&meta) });
}
let saved = fs::metadata(&dest).map_err(|e| Error::from_io(&e))?;
Ok(Persisted { path: dest.to_string_lossy().into_owned(), owned: true, size: saved.len(), mtime: read::mtime_ms(&saved) })
}
pub fn release(&self, path: &str, owned: bool) -> Result<()> {
let path = Path::new(path);
if !owned {
return self.allow.persist_remove(path);
}
let Some((real, exists)) = allow::resolve(path) else { return Err(Error::permission_denied("path_not_allowed")) };
if !(self.allow.in_user_dir(&real) || self.allow.in_temp(&real)) {
return Err(Error::permission_denied("path_not_allowed"));
}
if exists {
fs::remove_file(&real).map_err(|e| Error::from_io(&e))?;
}
Ok(())
}
}
fn strip_random_prefix(name: &str) -> &str {
match name.split_once('-') {
Some((prefix, rest)) if prefix.len() == 16 && prefix.bytes().all(|b| b.is_ascii_hexdigit()) && !rest.is_empty() => rest,
_ => name,
}
}
fn move_file(from: &Path, to: &Path) -> Result<()> {
if fs::rename(from, to).is_ok() {
return Ok(());
}
if let Err(err) = fs::copy(from, to) {
let _ = fs::remove_file(to);
return Err(Error::from_io(&err));
}
let _ = fs::remove_file(from);
Ok(())
}
fn download_dir() -> Option<PathBuf> {
dirs::download_dir().or_else(|| dirs::home_dir().map(|h| h.join("Downloads")))
}
pub fn on_drag_drop(shared: &Shared, label: &str, paths: &[PathBuf]) {
let files = shared.files.register_dropped(paths);
if !files.is_empty() {
shared.hub.send(label, serde_json::json!({ "t": "files_dropped", "paths": files }));
}
}
impl FilesState {
fn register_dropped(&self, paths: &[PathBuf]) -> Vec<String> {
paths
.iter()
.filter(|p| fs::File::open(p).is_ok())
.filter_map(|p| self.allow.register_read(p).map(|_| p.to_string_lossy().into_owned()))
.collect()
}
}
pub fn register_protocol<R: Runtime>(builder: PluginBuilder<R>) -> PluginBuilder<R> {
protocol::register(builder)
}