use file_icon_provider::get_file_icon;
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use fs_extra::{
dir::{get_size, ls, CopyOptions, DirEntryAttr, DirEntryValue},
move_items,
};
use image::{DynamicImage, RgbaImage};
use serde::Serialize;
use std::{
collections::HashSet,
fs::{self, create_dir_all, read_dir, File},
io::{self},
path::PathBuf,
time::{SystemTime, UNIX_EPOCH},
};
use tar::Archive;
use tauri::{command, AppHandle, Manager, Runtime};
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IconOptions {
pub size: Option<u16>,
pub save_path: Option<PathBuf>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MetadataOptions {
pub omit_size: Option<bool>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Metadata {
pub size: u64,
pub name: String,
pub extname: String,
pub full_name: String,
pub parent_name: String,
pub is_exist: bool,
pub is_file: bool,
pub is_dir: bool,
pub is_symlink: bool,
pub is_absolute: bool,
pub is_relative: bool,
pub accessed_at: u128,
pub created_at: u128,
pub modified_at: u128,
}
#[derive(Debug, serde::Deserialize)]
pub struct CompressOptions {
pub includes: Option<Vec<String>>,
pub excludes: Option<Vec<String>>,
}
#[derive(Debug, serde::Deserialize)]
pub struct TransferOptions {
pub includes: Option<Vec<String>>,
pub excludes: Option<Vec<String>>,
}
#[command]
pub async fn is_exist(path: PathBuf) -> bool {
path.exists()
}
#[command]
pub async fn is_file(path: PathBuf) -> bool {
path.is_file()
}
#[command]
pub async fn is_dir(path: PathBuf) -> bool {
path.is_dir()
}
#[command]
pub async fn size(path: PathBuf) -> u64 {
get_size(path).unwrap_or(0)
}
#[command]
pub async fn name(path: PathBuf) -> String {
path.file_stem()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_default()
}
#[command]
pub async fn extname(path: PathBuf) -> String {
path.extension()
.map(|extname| extname.to_string_lossy().to_string())
.unwrap_or_default()
}
#[command]
pub async fn full_name(path: PathBuf) -> String {
path.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_default()
}
#[command]
pub async fn parent_name(path: PathBuf, level: Option<u8>) -> Result<String, String> {
let mut current = path;
for _ in 0..level.unwrap_or(1) {
if let Some(parent) = current.parent() {
current = parent.to_path_buf();
} else {
return Ok(String::default());
}
}
Ok(full_name(current).await)
}
async fn get_icon_name(path: PathBuf) -> Result<String, String> {
let is_dir = is_dir(path.clone()).await;
let name = name(path.clone()).await;
let extname = extname(path.clone()).await;
let full_name = full_name(path.clone()).await;
let is_mac_app = cfg!(target_os = "macos") && extname.eq(&"app");
let is_win_app = cfg!(target_os = "windows") && extname.eq(&"exe");
if is_mac_app || is_win_app {
return Ok(full_name);
}
if is_dir {
return Ok("__TAURI_PLUGIN_FS_PRO_DIRECTORY__".to_string());
}
if extname.is_empty() {
return Ok(name);
}
return Ok(extname);
}
#[command]
pub async fn get_default_save_icon_path<R: Runtime>(
app_handle: AppHandle<R>,
) -> Result<PathBuf, String> {
let save_path = app_handle
.path()
.app_data_dir()
.map_err(|err| err.to_string())?
.join("tauri-plugin-fs-pro")
.join("icons");
Ok(save_path)
}
#[command]
pub async fn icon<R: Runtime>(
app_handle: AppHandle<R>,
path: PathBuf,
options: Option<IconOptions>,
) -> Result<PathBuf, String> {
let size = options.as_ref().and_then(|opt| opt.size).unwrap_or(32);
let default_save_path = get_default_save_icon_path(app_handle).await?;
let save_path = options
.and_then(|opt| opt.save_path)
.unwrap_or(default_save_path);
let icon = get_file_icon(path.clone(), size).map_err(|err| err.to_string())?;
let image = RgbaImage::from_raw(icon.width, icon.height, icon.pixels)
.map(DynamicImage::ImageRgba8)
.ok_or_else(|| "Failed to convert Icon to Image".to_string())?;
create_dir_all(&save_path).map_err(|err| err.to_string())?;
let icon_name = get_icon_name(path).await?;
let save_path = save_path.join(format!("{}.png", icon_name));
if save_path.exists() {
return Ok(save_path);
}
image.save(&save_path).map_err(|err| err.to_string())?;
Ok(save_path)
}
fn system_time_to_unix_millis(time: io::Result<SystemTime>) -> u128 {
match time {
Ok(system_time) => system_time
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis())
.unwrap_or_default(),
Err(_) => 0,
}
}
#[command]
pub async fn metadata(path: PathBuf, options: Option<MetadataOptions>) -> Result<Metadata, String> {
let omit_size = options.and_then(|opt| opt.omit_size).unwrap_or(false);
let size = if omit_size {
0
} else {
size(path.clone()).await
};
let name = name(path.clone()).await;
let extname = extname(path.clone()).await;
let full_name = full_name(path.clone()).await;
let parent_name = parent_name(path.clone(), Some(1)).await?;
let is_dir = path.is_dir();
let is_file = path.is_file();
let is_exist = path.exists();
let is_symlink = path.is_symlink();
let is_absolute = path.is_absolute();
let is_relative = path.is_relative();
let metadata = fs::metadata(path).map_err(|err| err.to_string())?;
let accessed_at = system_time_to_unix_millis(metadata.accessed());
let created_at = system_time_to_unix_millis(metadata.created());
let modified_at = system_time_to_unix_millis(metadata.modified());
Ok(Metadata {
size,
name,
extname,
full_name,
parent_name,
is_dir,
is_file,
is_exist,
is_symlink,
is_absolute,
is_relative,
accessed_at,
created_at,
modified_at,
})
}
#[command]
pub async fn compress(
src_path: PathBuf,
dst_path: PathBuf,
options: Option<CompressOptions>,
) -> Result<(), String> {
let options = options.unwrap_or(CompressOptions {
includes: Some(vec![]),
excludes: Some(vec![]),
});
let includes = options.includes.unwrap_or(vec![]);
let excludes = options.excludes.unwrap_or(vec![]);
let dst_file = File::create(dst_path.clone()).map_err(|err| err.to_string())?;
let enc = GzEncoder::new(dst_file, Compression::default());
let mut tar = tar::Builder::new(enc);
for entry in read_dir(&src_path).map_err(|err| err.to_string())? {
let path = entry.map_err(|err| err.to_string())?.path();
let is_file = path.is_file();
let full_name = full_name(path.clone()).await;
if excludes.iter().any(|name| &full_name == name) {
continue;
}
if !includes.is_empty() && !includes.iter().any(|name| &full_name == name) {
continue;
}
if is_file {
let file = &mut File::open(path.clone()).map_err(|err| err.to_string())?;
tar.append_file(full_name, file)
.map_err(|err| err.to_string())?;
} else {
tar.append_dir_all(full_name, path.clone())
.map_err(|err| err.to_string())?;
}
}
tar.finish().map_err(|err| err.to_string())?;
Ok(())
}
#[command]
pub async fn decompress(src_path: PathBuf, dst_path: PathBuf) -> Result<(), String> {
create_dir_all(dst_path.clone()).map_err(|err| err.to_string())?;
let src_file = File::open(src_path).map_err(|err| err.to_string())?;
let decoder = GzDecoder::new(src_file);
let mut archive = Archive::new(decoder);
for entry in archive.entries().map_err(|err| err.to_string())? {
let mut entry = entry.map_err(|err| err.to_string())?;
let path = entry.path().map_err(|err| err.to_string())?.to_path_buf();
#[cfg(target_os = "windows")]
let path = std::path::Path::new(&path.to_string_lossy().replace("\\", "/")).to_path_buf();
entry
.unpack(dst_path.join(path))
.map_err(|err| err.to_string())?;
}
Ok(())
}
#[command]
pub async fn transfer(
src_path: PathBuf,
dst_path: PathBuf,
options: Option<TransferOptions>,
) -> Result<(), String> {
let options = options.unwrap_or(TransferOptions {
includes: Some(vec![]),
excludes: Some(vec![]),
});
let includes = options.includes.unwrap_or(vec![]);
let excludes = options.excludes.unwrap_or(vec![]);
create_dir_all(dst_path.clone()).map_err(|err| err.to_string())?;
let mut config = HashSet::new();
config.insert(DirEntryAttr::Path);
let ls_result = ls(&src_path, &config).map_err(|err| err.to_string())?;
let mut from_items = Vec::new();
for item in ls_result.items {
if let Some(path) = item.get(&DirEntryAttr::Path) {
if let &DirEntryValue::String(ref path) = path {
let path = PathBuf::from(path);
let full_name = full_name(path.clone()).await;
if excludes.iter().any(|name| &full_name == name) {
continue;
}
if !includes.is_empty() && !includes.iter().any(|name| &full_name == name) {
continue;
}
from_items.push(path);
}
}
}
let options = CopyOptions {
overwrite: true,
skip_exist: false,
buffer_size: 64000,
copy_inside: false,
content_only: false,
depth: 0,
};
move_items(&from_items, &dst_path, &options).map_err(|err| err.to_string())?;
Ok(())
}