use std::fs::{self, OpenOptions};
use std::io::ErrorKind;
use std::ops::Range;
use std::path::{Path, PathBuf};
use super::state::{ROOT, child_key};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum NameProblem {
Empty,
Slash,
Nul,
Dots,
Taken,
}
impl NameProblem {
#[must_use]
pub fn message(&self) -> String {
let key = match self {
Self::Empty => "quvyta.file-manager.name-empty",
Self::Slash => "quvyta.file-manager.name-slash",
Self::Nul => "quvyta.file-manager.name-nul",
Self::Dots => "quvyta.file-manager.name-dots",
Self::Taken => "quvyta.file-manager.name-taken",
};
crate::t!(key)
}
}
pub fn check_name<'a>(
name: &str,
mut siblings: impl Iterator<Item = &'a str>,
current: Option<&str>,
) -> Result<(), NameProblem> {
if name.trim().is_empty() {
return Err(NameProblem::Empty);
}
if name.contains('/') {
return Err(NameProblem::Slash);
}
if name.contains('\0') {
return Err(NameProblem::Nul);
}
if name == "." || name == ".." {
return Err(NameProblem::Dots);
}
if current != Some(name) && siblings.any(|sibling| sibling == name) {
return Err(NameProblem::Taken);
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum FileError {
Name(NameProblem),
Outside,
IntoItself,
Taken(String),
CrossDevice,
Denied,
NotReadable,
Missing,
NoTrash,
NoRoom,
Stopped,
System(String),
}
impl FileError {
#[must_use]
pub fn message(&self) -> String {
match self {
Self::Name(problem) => problem.message(),
Self::Outside => crate::t!("quvyta.file-manager.outside"),
Self::IntoItself => crate::t!("quvyta.file-manager.into-itself"),
Self::Taken(name) => crate::t!("quvyta.file-manager.taken", name = name.as_str()),
Self::CrossDevice => crate::t!("quvyta.file-manager.cross-device"),
Self::Denied => crate::t!("quvyta.file-manager.denied"),
Self::NotReadable => crate::t!("quvyta.file-manager.not-readable"),
Self::Missing => crate::t!("quvyta.file-manager.missing"),
Self::NoTrash => crate::t!("quvyta.file-manager.no-trash"),
Self::NoRoom => crate::t!("quvyta.file-manager.no-room"),
Self::Stopped => crate::t!("quvyta.file-manager.stopped"),
Self::System(said) => said.clone(),
}
}
}
impl From<std::io::Error> for FileError {
fn from(error: std::io::Error) -> Self {
match error.kind() {
ErrorKind::CrossesDevices => Self::CrossDevice,
ErrorKind::PermissionDenied => Self::Denied,
ErrorKind::NotFound => Self::Missing,
ErrorKind::StorageFull => Self::NoRoom,
_ => Self::System(error.to_string()),
}
}
}
pub(super) fn read_error(error: std::io::Error) -> FileError {
if error.kind() == ErrorKind::PermissionDenied { FileError::NotReadable } else { error.into() }
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum FileChange {
Created(String),
Moved(String, String),
Deleted(String),
Copied(String),
Trashed(String),
}
#[must_use]
pub fn parent_key(key: &str) -> &str {
key.rsplit_once('/').map_or(ROOT, |(parent, _)| parent)
}
#[must_use]
pub fn name_of(key: &str) -> &str {
key.rsplit_once('/').map_or(key, |(_, name)| name)
}
#[must_use]
pub fn stem(name: &str, folder: bool) -> Range<usize> {
let length = name.chars().count();
if folder {
return 0..length;
}
match name.chars().rev().position(|c| c == '.').map(|from_end| length - 1 - from_end) {
Some(dot) if dot > 0 => 0..dot,
_ => 0..length,
}
}
#[must_use]
pub fn is_within(key: &str, folder: &str) -> bool {
key == folder || key.strip_prefix(folder).is_some_and(|rest| rest.starts_with('/'))
}
#[must_use]
pub fn is_inside(key: &str) -> bool {
!key.is_empty() && !key.contains('\0') && key.split('/').all(|part| !part.is_empty() && part != "." && part != "..")
}
fn entry_path(root: &Path, key: &str, confined: bool) -> Result<PathBuf, FileError> {
let parts: Vec<&str> = key.split('/').collect();
let mut path = root.to_path_buf();
for (index, part) in parts.iter().enumerate() {
if part.is_empty() || *part == "." || *part == ".." || part.contains('\0') {
return Err(FileError::Outside);
}
path.push(part);
if confined && index + 1 < parts.len() {
real_folder(&path)?;
}
}
Ok(path)
}
fn folder_path(root: &Path, key: &str, confined: bool) -> Result<PathBuf, FileError> {
if key == ROOT {
return Ok(root.to_path_buf());
}
let path = entry_path(root, key, confined)?;
if confined {
real_folder(&path)?;
}
Ok(path)
}
fn real_folder(path: &Path) -> Result<(), FileError> {
let meta = fs::symlink_metadata(path)?;
if meta.file_type().is_symlink() || !meta.is_dir() {
return Err(FileError::Outside);
}
Ok(())
}
fn occupied(path: &Path) -> bool {
fs::symlink_metadata(path).is_ok()
}
fn usable(name: &str) -> Result<(), FileError> {
check_name(name, std::iter::empty(), None).map_err(FileError::Name)
}
pub fn create_file(root: &Path, folder: &str, name: &str, confined: bool) -> Result<FileChange, FileError> {
usable(name)?;
let path = folder_path(root, folder, confined)?.join(name);
OpenOptions::new().write(true).create_new(true).open(&path).map_err(|error| taken_or(error, name))?;
Ok(FileChange::Created(child_key(folder, name)))
}
pub fn create_folder(root: &Path, folder: &str, name: &str, confined: bool) -> Result<FileChange, FileError> {
usable(name)?;
let path = folder_path(root, folder, confined)?.join(name);
fs::create_dir(&path).map_err(|error| taken_or(error, name))?;
Ok(FileChange::Created(child_key(folder, name)))
}
pub fn rename(root: &Path, key: &str, name: &str, confined: bool) -> Result<FileChange, FileError> {
usable(name)?;
let from = entry_path(root, key, confined)?;
let folder = parent_key(key);
if name_of(key) == name {
return Ok(FileChange::Moved(key.to_owned(), key.to_owned()));
}
let to = folder_path(root, folder, confined)?.join(name);
if occupied(&to) {
return Err(FileError::Taken(name.to_owned()));
}
fs::rename(&from, &to)?;
Ok(FileChange::Moved(key.to_owned(), child_key(folder, name)))
}
pub fn move_into(root: &Path, key: &str, into: &str, confined: bool) -> Result<FileChange, FileError> {
if is_within(into, key) {
return Err(FileError::IntoItself);
}
let from = entry_path(root, key, confined)?;
let name = name_of(key);
if parent_key(key) == into {
return Ok(FileChange::Moved(key.to_owned(), key.to_owned()));
}
let to = folder_path(root, into, confined)?.join(name);
if occupied(&to) {
return Err(FileError::Taken(name.to_owned()));
}
fs::rename(&from, &to)?;
Ok(FileChange::Moved(key.to_owned(), child_key(into, name)))
}
pub fn delete(root: &Path, key: &str, confined: bool) -> Result<FileChange, FileError> {
let path = entry_path(root, key, confined)?;
let meta = fs::symlink_metadata(&path)?;
if meta.is_dir() {
fs::remove_dir_all(&path)?;
} else {
fs::remove_file(&path)?;
}
Ok(FileChange::Deleted(key.to_owned()))
}
pub(super) fn to_trash(trash: &Path, root: &Path, key: &str, confined: bool) -> Result<FileChange, FileError> {
let path = entry_path(root, key, confined)?;
fs::symlink_metadata(&path)?;
super::trash::move_to_trash(trash, &path)?;
Ok(FileChange::Trashed(key.to_owned()))
}
fn taken_or(error: std::io::Error, name: &str) -> FileError {
if error.kind() == ErrorKind::AlreadyExists { FileError::Taken(name.to_owned()) } else { error.into() }
}
pub(super) struct Watch<'a> {
pub(super) progress: &'a dyn Fn(u64, u64),
pub(super) stopped: &'a dyn Fn() -> bool,
}
impl Watch<'_> {
pub(super) fn none() -> Self {
Self { progress: &|_, _| {}, stopped: &|| false }
}
}
const BLOCK: usize = 256 * 1024;
fn weight(path: &Path) -> u64 {
let Ok(meta) = fs::symlink_metadata(path) else { return 0 };
if !meta.is_dir() {
return meta.len();
}
let Ok(entries) = fs::read_dir(path) else { return 0 };
entries.flatten().map(|entry| weight(&entry.path())).sum()
}
pub fn copy_into(root: &Path, key: &str, into: &str, confined: bool) -> Result<FileChange, FileError> {
copy_watched(root, key, into, confined, &Watch::none())
}
pub(super) fn copy_watched(
root: &Path,
key: &str,
into: &str,
confined: bool,
watch: &Watch<'_>,
) -> Result<FileChange, FileError> {
if is_within(into, key) {
return Err(FileError::IntoItself);
}
let from = entry_path(root, key, confined)?;
let name = name_of(key);
let to = folder_path(root, into, confined)?.join(name);
if occupied(&to) {
return Err(FileError::Taken(name.to_owned()));
}
let total = weight(&from);
let mut written = 0;
(watch.progress)(0, total);
match copy_tree(&from, &to, total, &mut written, watch) {
Ok(()) => Ok(FileChange::Copied(child_key(into, name))),
Err(problem) => {
let _ = fs::symlink_metadata(&to)
.map(|meta| if meta.is_dir() { fs::remove_dir_all(&to) } else { fs::remove_file(&to) });
Err(problem)
}
}
}
fn copy_tree(from: &Path, to: &Path, total: u64, written: &mut u64, watch: &Watch<'_>) -> Result<(), FileError> {
if (watch.stopped)() {
return Err(FileError::Stopped);
}
let meta = fs::symlink_metadata(from)?;
if meta.file_type().is_symlink() {
let target = fs::read_link(from)?;
std::os::unix::fs::symlink(target, to)?;
return Ok(());
}
if !meta.is_dir() {
copy_file(from, to, total, written, watch)?;
return Ok(());
}
fs::create_dir(to)?;
let mut entries: Vec<PathBuf> = fs::read_dir(from)?.flatten().map(|entry| entry.path()).collect();
entries.sort();
for entry in entries {
let Some(name) = entry.file_name() else { continue };
copy_tree(&entry, &to.join(name), total, written, watch)?;
}
fs::set_permissions(to, meta.permissions())?;
Ok(())
}
fn copy_file(from: &Path, to: &Path, total: u64, written: &mut u64, watch: &Watch<'_>) -> Result<(), FileError> {
use std::io::{Read, Write};
let mut source = fs::File::open(from)?;
let mut target = OpenOptions::new().write(true).create_new(true).open(to)?;
let mut block = vec![0u8; BLOCK];
loop {
if (watch.stopped)() {
return Err(FileError::Stopped);
}
let read = source.read(&mut block)?;
if read == 0 {
break;
}
target.write_all(&block[..read])?;
*written += read as u64;
(watch.progress)(*written, total);
}
target.flush()?;
fs::set_permissions(to, fs::metadata(from)?.permissions())?;
Ok(())
}