use crate::{platform::linux::fs_ext::execute_file_operation, Dirent, FileAttribute, RecycleBinDirent, RecycleBinItem, Volume};
use gtk::gio::{self, traits::FileExt, Cancellable, File, FileCopyFlags, FileEnumerator, FileInfo, FileQueryInfoFlags, FileType};
use libc::{timespec, utimensat, AT_FDCWD};
use serde_json::Value;
use std::{collections::HashMap, ffi::CString, path::Path};
const ATTRIBUTES: &str = "filesystem::readonly,standard::is-hidden,standard::is-symlink,standard::name,standard::size,standard::type,time::*,dos::is-system,standard::symlink-target";
const ATTRIBUTES_FOR_RECYCLE: &str =
"trash::orig-path,trash::deletion-date,filesystem::readonly,standard::is-hidden,standard::is-symlink,standard::name,standard::size,standard::type,time::*,dos::is-system,standard::symlink-target";
pub fn list_volumes() -> Result<Vec<Volume>, String> {
let mut volumes = Vec::new();
let output = std::process::Command::new("lsblk").args(["-ba", "--json", "-o", "NAME,TYPE,FSTYPE,LABEL,VENDOR,MODEL,SIZE,MOUNTPOINT,FSAVAIL"]).output().map_err(|e| e.to_string())?;
let data: Value = serde_json::from_str(std::str::from_utf8(&output.stdout).unwrap()).map_err(|e| e.to_string())?;
let drives: Vec<&Value> = data["blockdevices"].as_array().unwrap().iter().filter(|dev| dev["type"].as_str().unwrap_or_default() == "disk").collect();
let exclude_mount_points = ["boot", "[SWAP]", "swap"];
for drive in drives {
let mut available_units = 0;
let mut total_units = 0;
let mut mount_point = String::new();
if drive["children"].is_null() {
let drive_mount_point = drive["mountpoint"].as_str().unwrap_or_default();
mount_point = drive_mount_point.to_string();
total_units += drive["size"].as_u64().unwrap_or_default();
available_units += drive["fsavail"].as_u64().unwrap_or_default();
} else {
for child in drive["children"].as_array().unwrap().iter() {
let child_mount_point = child["mountpoint"].as_str().unwrap_or_default();
if !exclude_mount_points.iter().any(|p| child_mount_point.contains(p)) {
mount_point = child_mount_point.to_string();
}
total_units += child["size"].as_u64().unwrap_or_default();
available_units += child["fsavail"].as_u64().unwrap_or_default();
}
}
if mount_point.is_empty() {
continue;
}
if exclude_mount_points.iter().any(|p| mount_point.contains(p)) {
continue;
}
let mut volume_label = if drive["label"].is_null() {
String::new()
} else {
drive["label"].to_string()
};
volume_label.push_str(if drive["vendor"].is_null() {
""
} else {
drive["vendor"].as_str().unwrap_or_default()
});
volume_label.push_str(if drive["model"].is_null() {
""
} else {
drive["model"].as_str().unwrap_or_default()
});
volumes.push(Volume {
mount_point,
volume_label,
available_units,
total_units,
});
}
Ok(volumes)
}
pub fn readdir<P: AsRef<Path>>(directory: P, recursive: bool, with_mime_type: bool) -> Result<Vec<Dirent>, String> {
if !directory.as_ref().is_dir() {
return Ok(Vec::new());
}
let file = File::for_path(directory.as_ref());
let mut entries = Vec::new();
try_readdir(file, &mut entries, recursive, with_mime_type)?;
Ok(entries)
}
fn try_readdir(dir: File, entries: &mut Vec<Dirent>, recursive: bool, with_mime_type: bool) -> Result<&mut Vec<Dirent>, String> {
for info in dir.enumerate_children(ATTRIBUTES, FileQueryInfoFlags::NOFOLLOW_SYMLINKS, Cancellable::NONE).unwrap().flatten() {
let name = info.name();
let mut full_path = dir.path().unwrap().to_path_buf();
full_path.push(name.clone());
let full_path_string = full_path.to_string_lossy().to_string();
let attributes = to_file_attribute(&info);
let mime_type = if with_mime_type {
get_mime_type(if attributes.is_symbolic_link {
&attributes.link_path
} else {
&full_path_string
})
} else {
String::new()
};
entries.push(Dirent {
name: name.file_name().unwrap_or_default().to_string_lossy().to_string(),
parent_path: dir.path().unwrap().to_string_lossy().to_string(),
full_path: full_path_string,
attributes,
mime_type,
});
if info.file_type() == FileType::Directory && recursive {
let next_dir = File::for_path(full_path);
try_readdir(next_dir, entries, recursive, with_mime_type)?;
}
}
Ok(entries)
}
pub fn stat<P: AsRef<Path>>(file_path: P) -> Result<FileAttribute, String> {
let file = File::for_path(file_path.as_ref());
let info = file.query_info(ATTRIBUTES, FileQueryInfoFlags::NONE, Cancellable::NONE).map_err(|e| e.message().to_string())?;
Ok(to_file_attribute(&info))
}
fn to_file_attribute(info: &FileInfo) -> FileAttribute {
FileAttribute {
is_directory: info.file_type() == FileType::Directory,
is_read_only: info.boolean("filesystem::readonly"),
is_hidden: info.is_hidden(),
is_system: info.boolean("dos::is-system"),
is_device: info.file_type() == FileType::Mountable,
is_file: info.file_type() == FileType::Regular,
is_symbolic_link: info.is_symlink(),
ctime_ms: to_msecs(info.attribute_uint64("time::changed"), info.attribute_uint32("time::changed-usec")),
mtime_ms: to_msecs(info.attribute_uint64("time::modified"), info.attribute_uint32("time::modified-usec")),
atime_ms: to_msecs(info.attribute_uint64("time::access"), info.attribute_uint32("time::access-usec")),
birthtime_ms: to_msecs(info.attribute_uint64("time::created"), info.attribute_uint32("time::created-usec")),
size: info.size() as u64,
link_path: if info.is_symlink() {
info.symlink_target().unwrap_or_default().to_string_lossy().to_string()
} else {
String::new()
},
}
}
fn to_msecs(secs: u64, microsecs: u32) -> u64 {
secs * 1000 + (microsecs as u64) / 1000
}
pub fn create_symlink<P1: AsRef<Path>, P2: AsRef<Path>>(full_path: P1, link_path: P2) -> Result<(), String> {
let file = gio::File::for_path(full_path);
file.make_symbolic_link(link_path, Cancellable::NONE).map_err(|e| e.message().to_string())
}
pub fn get_mime_type<P: AsRef<Path>>(file_path: P) -> String {
match mime_guess::from_path(file_path).first() {
Some(s) => s.essence_str().to_string(),
None => String::new(),
}
}
pub(crate) fn get_mime_type_fallback<P: AsRef<Path>>(file_path: P) -> Result<String, String> {
if !file_path.as_ref().is_file() {
return Ok(String::new());
}
let (ctype, _) = gtk::gio::content_type_guess(Some(file_path.as_ref().file_name().unwrap()), &[0]);
Ok(ctype.to_string())
}
fn handle_directory<P1: AsRef<Path>, P2: AsRef<Path>>(is_copy: bool, from: P1, to: P2) -> Result<(), String> {
let source = File::for_path(from.as_ref());
let to_dr = to.as_ref().join(from.as_ref().file_name().unwrap());
let dest = File::for_path(&to_dr);
if !dest.query_exists(Cancellable::NONE) {
dest.make_directory(Cancellable::NONE).map_err(|e| e.message().to_string())?;
let settable_attributes = dest.query_settable_attributes(Cancellable::NONE).unwrap();
let attributes_info = settable_attributes.attributes();
let attributes = attributes_info.iter().map(|a| a.name()).collect::<Vec<&str>>().join(",");
let info = source.query_info(&attributes, FileQueryInfoFlags::NONE, Cancellable::NONE).unwrap();
dest.set_attributes_from_info(&info, FileQueryInfoFlags::NONE, Cancellable::NONE).unwrap();
}
if let Ok(children) = source.enumerate_children("standard:name", FileQueryInfoFlags::NONE, Cancellable::NONE) {
children.into_iter().try_for_each(|info| {
let info = info.map_err(|e| e.message().to_string())?;
let from_file = from.as_ref().to_path_buf().join(info.name());
println!("here:{:?} vs {:?}", from_file, to_dr);
if is_copy {
copy(from_file, to_dr.clone())
} else {
mv(from_file, to_dr.clone())
}
})
} else {
Ok(())
}
}
pub fn mv<P1: AsRef<Path>, P2: AsRef<Path>>(from: P1, to: P2) -> Result<(), String> {
let source = File::for_path(from.as_ref());
let dest_path = to.as_ref().join(from.as_ref().file_name().unwrap());
let dest = File::for_path(&dest_path);
if from.as_ref().is_dir() {
handle_directory(false, from, to)
} else {
source.move_(&dest, FileCopyFlags::ALL_METADATA | FileCopyFlags::NOFOLLOW_SYMLINKS | FileCopyFlags::OVERWRITE, Cancellable::NONE, None).map_err(|e| e.message().to_string())
}
}
pub fn mv_async<P1: AsRef<Path>, P2: AsRef<Path>>(from: P1, to: P2, callback: impl AsyncFnMut(OperationStatus) -> Response + 'static) {
execute_file_operation(FileOperation::Move, &[from], Some(to), callback)
}
pub fn mv_all<P1: AsRef<Path>, P2: AsRef<Path>>(froms: &[P1], to: P2) -> Result<(), String> {
froms.iter().try_for_each(|from| mv(from, to.as_ref()))
}
pub fn mv_all_async<P1: AsRef<Path>, P2: AsRef<Path>>(froms: &[P1], to: P2, callback: impl AsyncFnMut(OperationStatus) -> Response + 'static) {
execute_file_operation(FileOperation::Move, froms, Some(to), callback)
}
pub fn copy<P1: AsRef<Path>, P2: AsRef<Path>>(from: P1, to: P2) -> Result<(), String> {
let source = File::for_path(from.as_ref());
let dest_path = to.as_ref().join(from.as_ref().file_name().unwrap());
let dest = File::for_path(&dest_path);
if from.as_ref().is_dir() {
handle_directory(true, from, to)
} else {
source.copy(&dest, FileCopyFlags::ALL_METADATA | FileCopyFlags::NOFOLLOW_SYMLINKS | FileCopyFlags::OVERWRITE, Cancellable::NONE, None).map_err(|e| e.message().to_string())
}
}
pub fn copy_async<P1: AsRef<Path>, P2: AsRef<Path>>(from: P1, to: P2, callback: impl AsyncFnMut(OperationStatus) -> Response + 'static) {
execute_file_operation(FileOperation::Copy, &[from], Some(to), callback)
}
pub fn copy_all<P1: AsRef<Path>, P2: AsRef<Path>>(froms: &[P1], to: P2) -> Result<(), String> {
froms.iter().try_for_each(|from| copy(from, to.as_ref()))
}
pub fn copy_all_async<P1: AsRef<Path>, P2: AsRef<Path>>(froms: &[P1], to: P2, callback: impl AsyncFnMut(OperationStatus) -> Response + 'static) {
execute_file_operation(FileOperation::Copy, froms, Some(to), callback)
}
pub fn delete<P: AsRef<Path>>(file: P) -> Result<(), String> {
if file.as_ref().is_dir() {
let children = crate::fs::readdir(file.as_ref(), false, false)?;
if children.is_empty() {
File::for_path(file).delete(Cancellable::NONE).map_err(|e| e.message().to_string())
} else {
children.iter().try_for_each(|child| delete(child.full_path.clone()))?;
File::for_path(file).delete(Cancellable::NONE).map_err(|e| e.message().to_string())
}
} else {
File::for_path(file).delete(Cancellable::NONE).map_err(|e| e.message().to_string())
}
}
pub fn delete_async<P: AsRef<Path>>(file: P, callback: impl AsyncFnMut(OperationStatus) -> Response + 'static) {
execute_file_operation(FileOperation::Delete, &[file], None::<String>, callback)
}
pub fn delete_all<P: AsRef<Path>>(files: &[P]) -> Result<(), String> {
files.iter().try_for_each(|file| delete(file.as_ref()))
}
pub fn delete_all_async<P: AsRef<Path>>(files: &[P], callback: impl AsyncFnMut(OperationStatus) -> Response + 'static) {
execute_file_operation(FileOperation::Delete, files, None::<String>, callback)
}
pub fn trash<P: AsRef<Path>>(file: P) -> Result<(), String> {
File::for_path(file).trash(Cancellable::NONE).map_err(|e| e.message().to_string())
}
pub fn trash_async<P: AsRef<Path>>(file: P, callback: impl AsyncFnMut(OperationStatus) -> Response + 'static) {
execute_file_operation(FileOperation::Trash, &[file], None::<String>, callback)
}
pub fn trash_all<P: AsRef<Path>>(files: &[P]) -> Result<(), String> {
files.iter().try_for_each(|file| trash(file.as_ref()))
}
pub fn trash_all_async<P: AsRef<Path>>(files: &[P], callback: impl AsyncFnMut(OperationStatus) -> Response + 'static) {
execute_file_operation(FileOperation::Trash, files, None::<String>, callback)
}
pub fn operate<P1: AsRef<Path>, P2: AsRef<Path>>(operation: FileOperation, froms: &[P1], to: Option<P2>, callback: impl AsyncFnMut(OperationStatus) -> Response + 'static) {
super::fs_ext::execute_file_operation(operation, froms, to, callback)
}
struct TrashData {
date: i64,
name: String,
}
const TRASH_PATH_STR: &str = "trash:///";
pub fn read_recycle_bin() -> Result<Vec<RecycleBinDirent>, String> {
let trash_file = File::for_uri(TRASH_PATH_STR);
let mut result = Vec::new();
if let Ok(mut children) = trash_file.enumerate_children(ATTRIBUTES_FOR_RECYCLE, FileQueryInfoFlags::NONE, Cancellable::NONE) {
while let Some(Ok(info)) = children.next() {
let original_path = if let Some(path) = info.attribute_as_string("trash::orig-path") {
path.to_string()
} else {
String::new()
};
let name = if let Some(name) = info.attribute_as_string("standard::name") {
name.to_string()
} else {
String::new()
};
let deleted_date_ms = if let Some(delete_date_string) = info.attribute_as_string("trash::deletion-date") {
gtk::glib::DateTime::from_iso8601(&delete_date_string, Some(>k::glib::TimeZone::local())).unwrap().to_unix() as u64
} else {
0
};
let attributes = to_file_attribute(&info);
let mime_type = get_mime_type(&original_path);
let bin_item = RecycleBinDirent {
name,
original_path,
deleted_date_ms,
attributes,
mime_type,
};
result.push(bin_item);
}
}
Ok(result)
}
pub fn undelete<P: AsRef<Path>>(file_paths: &[P]) -> Result<(), String> {
let trash_file = File::for_uri(TRASH_PATH_STR);
if let Ok(mut children) = trash_file.enumerate_children("trash::orig-path,trash::deletion-date,standard::name", FileQueryInfoFlags::NONE, Cancellable::NONE) {
let file_paths: Vec<String> = file_paths.iter().map(|f| f.as_ref().to_string_lossy().to_string()).collect();
let mut map: HashMap<String, TrashData> = HashMap::new();
while let Some(Ok(info)) = children.next() {
let orig_path = if let Some(path) = info.attribute_as_string("trash::orig-path") {
path.to_string()
} else {
String::new()
};
let date_string = info.attribute_as_string("trash::deletion-date").unwrap();
let date = gtk::glib::DateTime::from_iso8601(&date_string, Some(>k::glib::TimeZone::local())).unwrap().to_unix();
if file_paths.contains(&orig_path) {
if map.contains_key(&orig_path) {
let trash_data = map.get(&orig_path).unwrap();
if trash_data.date < date {
let _ = map.insert(
orig_path,
TrashData {
date,
name: info.name().to_string_lossy().to_string(),
},
);
}
} else {
let _ = map.insert(
orig_path,
TrashData {
date,
name: info.name().to_string_lossy().to_string(),
},
);
}
}
}
for (orig_path, trash_data) in map.iter() {
let mut trash_path = String::from(TRASH_PATH_STR);
trash_path.push_str(&trash_data.name);
File::for_uri(&trash_path).move_(&File::for_parse_name(orig_path), FileCopyFlags::OVERWRITE | FileCopyFlags::ALL_METADATA, Cancellable::NONE, None).map_err(|e| e.message().to_string())?;
}
}
Ok(())
}
pub fn undelete_by_time(targets: &[RecycleBinItem]) -> Result<(), String> {
let trash_file = File::for_uri(TRASH_PATH_STR);
if let Ok(children) = trash_file.enumerate_children("trash::orig-path,trash::deletion-date,standard::name", FileQueryInfoFlags::NONE, Cancellable::NONE) {
let args: HashMap<String, u64> = targets.iter().map(|target| (target.original_path.clone(), target.deleted_time_ms)).collect();
let map = find_items_in_recycle_bin(children, args)?;
for (orig_path, trash_data) in map.iter() {
let mut trash_path = String::from(TRASH_PATH_STR);
trash_path.push_str(&trash_data.name);
File::for_uri(&trash_path).move_(&File::for_parse_name(orig_path), FileCopyFlags::OVERWRITE | FileCopyFlags::ALL_METADATA, Cancellable::NONE, None).map_err(|e| e.message().to_string())?;
}
}
Ok(())
}
pub fn delete_from_recycle_bin(targets: &[RecycleBinItem]) -> Result<(), String> {
let trash_file = File::for_uri(TRASH_PATH_STR);
if let Ok(children) = trash_file.enumerate_children("trash::orig-path,trash::deletion-date,standard::name", FileQueryInfoFlags::NONE, Cancellable::NONE) {
let args: HashMap<String, u64> = targets.iter().map(|target| (target.original_path.clone(), target.deleted_time_ms)).collect();
let map = find_items_in_recycle_bin(children, args)?;
for (_, trash_data) in map.iter() {
let mut trash_path = String::from(TRASH_PATH_STR);
trash_path.push_str(&trash_data.name);
File::for_uri(&trash_path).delete(Cancellable::NONE).map_err(|e| e.message().to_string())?;
}
}
Ok(())
}
fn find_items_in_recycle_bin(mut children: FileEnumerator, map: HashMap<String, u64>) -> Result<HashMap<String, TrashData>, String> {
let mut items: HashMap<String, TrashData> = HashMap::new();
while let Some(Ok(info)) = children.next() {
let orig_path = if let Some(path) = info.attribute_as_string("trash::orig-path") {
path.to_string()
} else {
String::new()
};
let date_string = info.attribute_as_string("trash::deletion-date").unwrap();
let date = gtk::glib::DateTime::from_iso8601(&date_string, Some(>k::glib::TimeZone::local())).unwrap().to_unix();
if map.contains_key(&orig_path) && *map.get(&orig_path).unwrap() == date as u64 {
let _ = items.insert(
orig_path,
TrashData {
date,
name: info.name().to_string_lossy().to_string(),
},
);
}
}
Ok(items)
}
#[allow(unused_variables)]
pub fn empty_recycle_bin(root: Option<String>) -> Result<(), String> {
let trash_file = File::for_uri(TRASH_PATH_STR);
if let Ok(mut children) = trash_file.enumerate_children("trash::orig-path,trash::deletion-date,standard::name", FileQueryInfoFlags::NONE, Cancellable::NONE) {
while let Some(Ok(info)) = children.next() {
let mut trash_path = String::from(TRASH_PATH_STR);
trash_path.push_str(info.name().to_str().unwrap());
File::for_uri(&trash_path).delete(Cancellable::NONE).map_err(|e| e.message().to_string())?;
}
}
Ok(())
}
pub fn utimes<P: AsRef<Path>>(file: P, atime_ms: u64, mtime_ms: u64) -> Result<(), String> {
let path = CString::new(file.as_ref().to_string_lossy().to_string()).map_err(|e| e.to_string())?;
let timespecs = [to_timespec(atime_ms), to_timespec(mtime_ms)];
let result = unsafe { utimensat(AT_FDCWD, path.as_ptr(), timespecs.as_ptr(), 0) };
if result < 0 {
Err("utimensat failed".to_string())
} else {
Ok(())
}
}
fn to_timespec(msec: u64) -> timespec {
let mut timespec = timespec {
tv_sec: (msec / 1000) as _,
tv_nsec: ((msec % 1000) * 1000000) as i64,
};
if timespec.tv_nsec < 0 {
timespec.tv_nsec += 1e9 as i64;
timespec.tv_sec -= 1;
}
timespec
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FileOperation {
Copy,
Move,
Delete,
Trash,
}
#[derive(Debug)]
pub enum OperationStatus {
Ready(Total),
Start(String),
Progress(i64, i64),
End,
Error(String),
Confirm(String),
Finished,
}
#[derive(Debug, PartialEq)]
pub enum Response {
Proceed,
Cancel,
Replace,
Skip,
}
#[derive(Debug, Default)]
pub struct Total {
pub total_size: u64,
pub total_count: u64,
}