ouais
This commit is contained in:
parent
70d983be98
commit
a7bd15b0d9
12 changed files with 271 additions and 190 deletions
|
@ -7,14 +7,23 @@ use crate::Error;
|
|||
use crate::TrashInfo;
|
||||
use crate::XDG;
|
||||
|
||||
/// A trash directory represented by a path.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TrashDir(pub PathBuf);
|
||||
|
||||
impl TrashDir {
|
||||
/// Gets your user's "home" trash directory.
|
||||
///
|
||||
/// According to Trash spec v1.0:
|
||||
///
|
||||
/// > For every user2 a “home trash” directory MUST be available.
|
||||
/// > Its name and location are $XDG_DATA_HOME/Trash;
|
||||
/// > $XDG_DATA_HOME is the base directory for user-specific data, as defined in the Desktop Base Directory Specification.
|
||||
pub fn get_home_trash() -> Self {
|
||||
TrashDir(XDG.get_data_home().join("Trash"))
|
||||
}
|
||||
|
||||
/// Returns the path to this trash directory.
|
||||
pub fn path(&self) -> &Path {
|
||||
self.0.as_ref()
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
/// All errors that could happen
|
||||
#[derive(Debug, Error)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Error {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
@ -10,7 +12,9 @@ pub enum Error {
|
|||
ParseDate(#[from] chrono::format::ParseError),
|
||||
}
|
||||
|
||||
/// Errors related to .trashinfo files
|
||||
#[derive(Debug, Error)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum TrashInfoError {
|
||||
#[error("Missing [TrashInfo] header")]
|
||||
MissingHeader,
|
||||
|
|
13
src/info.rs
13
src/info.rs
|
@ -13,27 +13,33 @@ lazy_static! {
|
|||
|
||||
const DATE_FORMAT: &str = "%Y-%m-%dT%H:%M:%S";
|
||||
|
||||
/// .trashinfo Data
|
||||
#[derive(Debug)]
|
||||
pub struct TrashInfo {
|
||||
/// The original path where this file was located before it was deleted.
|
||||
pub path: PathBuf,
|
||||
|
||||
/// The date the file was deleted.
|
||||
pub deletion_date: DateTime<Local>,
|
||||
|
||||
/// The location of the deleted file after deletion.
|
||||
pub deleted_path: PathBuf,
|
||||
|
||||
/// The location of the `info` description file.
|
||||
pub info_path: PathBuf,
|
||||
}
|
||||
|
||||
impl TrashInfo {
|
||||
/// Create a new TrashInfo based on the .trashinfo path and the deleted file path
|
||||
///
|
||||
/// This is useful for reading files from the Trash.
|
||||
pub fn from_files(
|
||||
info_path: impl AsRef<Path>,
|
||||
deleted_path: impl AsRef<Path>,
|
||||
) -> Result<Self, Error> {
|
||||
let path = info_path.as_ref();
|
||||
let info_path = path.to_path_buf();
|
||||
let info_path = info_path.as_ref().to_path_buf();
|
||||
let deleted_path = deleted_path.as_ref().to_path_buf();
|
||||
let file = File::open(path)?;
|
||||
let file = File::open(&info_path)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let mut path = None;
|
||||
|
@ -88,6 +94,7 @@ impl TrashInfo {
|
|||
})
|
||||
}
|
||||
|
||||
/// Write the current TrashInfo into a .trashinfo file.
|
||||
pub fn write(&self, mut out: impl Write) -> Result<(), io::Error> {
|
||||
writeln!(out, "[Trash Info]")?;
|
||||
writeln!(out, "Path={}", self.path.to_str().unwrap())?;
|
||||
|
|
10
src/lib.rs
10
src/lib.rs
|
@ -1,7 +1,11 @@
|
|||
#![deny(warnings)]
|
||||
//! garbage
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate structopt;
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate anyhow;
|
||||
|
@ -13,6 +17,7 @@ mod errors;
|
|||
mod info;
|
||||
mod mounts;
|
||||
pub mod ops;
|
||||
mod strategy;
|
||||
mod utils;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
@ -25,8 +30,9 @@ pub use crate::info::TrashInfo;
|
|||
use crate::mounts::Mounts;
|
||||
|
||||
lazy_static! {
|
||||
static ref XDG: BaseDirectories = BaseDirectories::new().unwrap();
|
||||
#[allow(missing_docs)]
|
||||
pub static ref MOUNTS: Mounts = Mounts::read().unwrap();
|
||||
static ref XDG: BaseDirectories = BaseDirectories::new().unwrap();
|
||||
static ref HOME_TRASH: TrashDir = TrashDir::get_home_trash();
|
||||
static ref HOME_MOUNT: PathBuf = MOUNTS.get_mount_point(HOME_TRASH.path()).unwrap();
|
||||
}
|
||||
|
|
36
src/main.rs
36
src/main.rs
|
@ -7,23 +7,16 @@ use std::io;
|
|||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use garbage::{
|
||||
ops::{self, EmptyOptions},
|
||||
TrashDir,
|
||||
};
|
||||
use structopt::StructOpt;
|
||||
|
||||
use garbage::*;
|
||||
|
||||
#[derive(StructOpt)]
|
||||
enum Command {
|
||||
#[structopt(name = "empty")]
|
||||
Empty {
|
||||
/// Only list the files that are to be deleted, without
|
||||
/// actually deleting anything.
|
||||
#[structopt(long = "dry")]
|
||||
dry: bool,
|
||||
|
||||
/// Delete all files older than (this number) of days.
|
||||
/// Removes everything if this option is not specified
|
||||
days: Option<u32>,
|
||||
},
|
||||
Empty(EmptyOptions),
|
||||
|
||||
#[structopt(name = "list")]
|
||||
List,
|
||||
|
@ -52,24 +45,9 @@ fn run() -> Result<()> {
|
|||
|
||||
let cmd = Command::from_args();
|
||||
match cmd {
|
||||
Command::Empty { dry, days } => ops::empty(dry, days),
|
||||
Command::Empty(options) => ops::empty(options),
|
||||
Command::List => {
|
||||
let home_trash = TrashDir::get_home_trash();
|
||||
let mut files = home_trash
|
||||
.iter()
|
||||
.unwrap()
|
||||
.filter_map(|entry| match entry {
|
||||
Ok(info) => Some(info),
|
||||
Err(err) => {
|
||||
eprintln!("failed to get file info: {:?}", err);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
files.sort_unstable_by_key(|info| info.deletion_date);
|
||||
for info in files {
|
||||
println!("{}\t{}", info.deletion_date, info.path.to_str().unwrap());
|
||||
}
|
||||
ops::list();
|
||||
Ok(())
|
||||
}
|
||||
Command::Put {
|
||||
|
|
|
@ -1,29 +1,52 @@
|
|||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use chrono::{Duration, Local};
|
||||
|
||||
use crate::TrashDir;
|
||||
|
||||
pub fn empty(dry: bool, days: Option<u32>) -> Result<()> {
|
||||
let home_trash = TrashDir::get_home_trash();
|
||||
let cutoff = if let Some(days) = days {
|
||||
/// Options to pass to empty
|
||||
#[derive(StructOpt)]
|
||||
pub struct EmptyOptions {
|
||||
/// Only list the files that are to be deleted, without
|
||||
/// actually deleting anything.
|
||||
pub dry: bool,
|
||||
|
||||
/// Delete all files older than (this number) of days.
|
||||
/// Removes everything if this option is not specified
|
||||
days: Option<u32>,
|
||||
|
||||
/// The path to the trash directory to empty.
|
||||
trash_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Actually delete files in the trash.
|
||||
pub fn empty(options: EmptyOptions) -> Result<()> {
|
||||
let trash_dir = options
|
||||
.trash_dir
|
||||
.map(TrashDir)
|
||||
.unwrap_or_else(|| TrashDir::get_home_trash());
|
||||
|
||||
// cutoff date
|
||||
let cutoff = if let Some(days) = options.days {
|
||||
Local::now() - Duration::days(days.into())
|
||||
} else {
|
||||
Local::now()
|
||||
};
|
||||
|
||||
for file in home_trash.iter()? {
|
||||
for file in trash_dir.iter()? {
|
||||
let file = file?;
|
||||
|
||||
// ignore files that were deleted after the cutoff (younger)
|
||||
let ignore = file.deletion_date > cutoff;
|
||||
|
||||
if !ignore {
|
||||
if dry {
|
||||
if options.dry {
|
||||
println!("{:?}", file.path);
|
||||
} else {
|
||||
fs::remove_file(file.info_path)?;
|
||||
|
||||
if file.deleted_path.exists() {
|
||||
if file.deleted_path.is_dir() {
|
||||
fs::remove_dir_all(file.deleted_path)?;
|
||||
|
|
20
src/ops/list.rs
Normal file
20
src/ops/list.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use crate::TrashDir;
|
||||
|
||||
pub fn list() {
|
||||
let home_trash = TrashDir::get_home_trash();
|
||||
let mut files = home_trash
|
||||
.iter()
|
||||
.unwrap()
|
||||
.filter_map(|entry| match entry {
|
||||
Ok(info) => Some(info),
|
||||
Err(err) => {
|
||||
eprintln!("failed to get file info: {:?}", err);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
files.sort_unstable_by_key(|info| info.deletion_date);
|
||||
for info in files {
|
||||
println!("{}\t{}", info.deletion_date, info.path.to_str().unwrap());
|
||||
}
|
||||
}
|
|
@ -1,5 +1,11 @@
|
|||
mod empty;
|
||||
mod put;
|
||||
//! Operations that garbage can do.
|
||||
|
||||
pub use self::empty::empty;
|
||||
mod empty;
|
||||
mod list;
|
||||
mod put;
|
||||
mod restore;
|
||||
|
||||
pub use self::empty::{empty, EmptyOptions};
|
||||
pub use self::list::list;
|
||||
pub use self::put::put;
|
||||
pub use self::restore::restore;
|
||||
|
|
162
src/ops/put.rs
162
src/ops/put.rs
|
@ -1,15 +1,10 @@
|
|||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use chrono::{Local};
|
||||
|
||||
use crate::utils;
|
||||
use crate::TrashDir;
|
||||
use crate::TrashInfo;
|
||||
use crate::{HOME_MOUNT, HOME_TRASH, MOUNTS};
|
||||
use crate::strategy::DeletionStrategy;
|
||||
use crate::HOME_TRASH;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
|
@ -17,151 +12,22 @@ pub enum Error {
|
|||
CannotTrashDotDirs,
|
||||
}
|
||||
|
||||
/// Throw some files into the trash.
|
||||
pub fn put(paths: Vec<PathBuf>, _recursive: bool, _force: bool) -> Result<()> {
|
||||
let strategy = DeletionStrategy::Copy;
|
||||
let strategy = DeletionStrategy::pick_strategy(&HOME_TRASH);
|
||||
for path in paths {
|
||||
// don't allow deleting '.' or '..'
|
||||
let current_dir = env::current_dir()?;
|
||||
ensure!(
|
||||
!(path == current_dir
|
||||
|| (current_dir.parent().is_some() && path == current_dir.parent().unwrap())),
|
||||
Error::CannotTrashDotDirs
|
||||
);
|
||||
|
||||
if let Err(err) = strategy.delete(path) {
|
||||
eprintln!("{:?}", err);
|
||||
eprintln!("{}", err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: implement the other ones
|
||||
#[allow(dead_code)]
|
||||
pub enum DeletionStrategy {
|
||||
Copy,
|
||||
Topdir,
|
||||
TopdirOrCopy,
|
||||
}
|
||||
|
||||
impl DeletionStrategy {
|
||||
fn get_target_trash(
|
||||
&self,
|
||||
mount: impl AsRef<Path>,
|
||||
path: impl AsRef<Path>,
|
||||
) -> Option<(TrashDir, bool)> {
|
||||
let mount = mount.as_ref();
|
||||
let _path = path.as_ref();
|
||||
|
||||
// first, are we on the home mount?
|
||||
if mount == *HOME_MOUNT {
|
||||
return Some((HOME_TRASH.clone(), false));
|
||||
}
|
||||
|
||||
// are we just copying?
|
||||
if let DeletionStrategy::Copy = self {
|
||||
return Some((HOME_TRASH.clone(), true));
|
||||
}
|
||||
|
||||
// try to use the $topdir/.Trash directory
|
||||
let topdir_trash = mount.join(".Trash");
|
||||
if self.should_use_topdir_trash(&topdir_trash) {
|
||||
return Some((
|
||||
TrashDir(topdir_trash.join(utils::get_uid().to_string())),
|
||||
false,
|
||||
));
|
||||
}
|
||||
|
||||
// try to use the $topdir/.Trash-$uid directory
|
||||
let topdir_trash_uid = mount.join(format!(".Trash-{}", utils::get_uid()));
|
||||
if self.should_use_topdir_trash_uid(&topdir_trash_uid) {
|
||||
return Some((TrashDir(topdir_trash_uid), false));
|
||||
}
|
||||
|
||||
// do we have the copy option
|
||||
if let DeletionStrategy::TopdirOrCopy = self {
|
||||
return Some((HOME_TRASH.clone(), true));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn should_use_topdir_trash(&self, path: impl AsRef<Path>) -> bool {
|
||||
let path = path.as_ref();
|
||||
if !path.exists() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let dir = match File::open(path) {
|
||||
Ok(file) => file,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let meta = match dir.metadata() {
|
||||
Ok(meta) => meta,
|
||||
Err(_) => return false,
|
||||
};
|
||||
if meta.file_type().is_symlink() {
|
||||
return false;
|
||||
}
|
||||
let perms = meta.permissions();
|
||||
|
||||
perms.mode() & 0o1000 > 0
|
||||
}
|
||||
|
||||
fn should_use_topdir_trash_uid(&self, path: impl AsRef<Path>) -> bool {
|
||||
let path = path.as_ref();
|
||||
if !path.exists() {
|
||||
match fs::create_dir(path) {
|
||||
Ok(_) => (),
|
||||
Err(_) => return false,
|
||||
};
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn delete(&self, target: impl AsRef<Path>) -> Result<()> {
|
||||
let target = target.as_ref();
|
||||
|
||||
// don't allow deleting '.' or '..'
|
||||
let current_dir = env::current_dir()?;
|
||||
ensure!(
|
||||
!(target == current_dir
|
||||
|| (current_dir.parent().is_some() && target == current_dir.parent().unwrap())),
|
||||
Error::CannotTrashDotDirs
|
||||
);
|
||||
|
||||
let target_mount = MOUNTS
|
||||
.get_mount_point(target)
|
||||
.ok_or_else(|| anyhow!("couldn't get mount point"))?;
|
||||
let (trash_dir, copy) = match self.get_target_trash(target_mount, target) {
|
||||
Some(x) => x,
|
||||
None => bail!("no trash dir could be selected, u suck"),
|
||||
};
|
||||
|
||||
// preparing metadata
|
||||
let now = Local::now();
|
||||
let elapsed = now.timestamp_millis();
|
||||
let file_name = format!(
|
||||
"{}.{}",
|
||||
elapsed,
|
||||
target.file_name().unwrap().to_str().unwrap()
|
||||
);
|
||||
|
||||
let trash_file_path = trash_dir.files_dir()?.join(&file_name);
|
||||
let trash_info_path = trash_dir.info_dir()?.join(file_name + ".trashinfo");
|
||||
|
||||
let trash_info = TrashInfo {
|
||||
path: utils::into_absolute(target)?,
|
||||
deletion_date: now,
|
||||
deleted_path: trash_file_path.clone(),
|
||||
info_path: trash_info_path.clone(),
|
||||
};
|
||||
{
|
||||
let trash_info_file = File::create(trash_info_path)?;
|
||||
trash_info.write(&trash_info_file)?;
|
||||
}
|
||||
|
||||
// copy the file over
|
||||
if copy {
|
||||
utils::recursive_copy(&target, &trash_file_path)?;
|
||||
fs::remove_dir_all(&target)?;
|
||||
} else {
|
||||
fs::rename(&target, &trash_file_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
3
src/ops/restore.rs
Normal file
3
src/ops/restore.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub fn restore() {
|
||||
// let trash = select_trash();
|
||||
}
|
158
src/strategy.rs
Normal file
158
src/strategy.rs
Normal file
|
@ -0,0 +1,158 @@
|
|||
//! Contains [DeletionStrategy][1], which determines how the target
|
||||
//! file will actually be deleted.
|
||||
//!
|
||||
//! [1]: crate::strategy::DeletionStrategy
|
||||
|
||||
use std::fs::{self, File};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use chrono::Local;
|
||||
|
||||
use crate::dir::TrashDir;
|
||||
use crate::info::TrashInfo;
|
||||
use crate::utils;
|
||||
use crate::{HOME_MOUNT, HOME_TRASH, MOUNTS};
|
||||
|
||||
// TODO: implement the other ones
|
||||
|
||||
/// DeletionStrategy describes whether the deleting a directory will:
|
||||
///
|
||||
/// * Move: move the candidate files/directories to the trash directory (this requires that both the candidate and the trash directories be on the same filesystem)
|
||||
/// * Copy: recursively copy the candidate files/directories to the trash directory
|
||||
/// * Topdir:
|
||||
#[allow(dead_code)]
|
||||
pub enum DeletionStrategy {
|
||||
Move,
|
||||
Copy,
|
||||
Topdir,
|
||||
TopdirOrCopy,
|
||||
}
|
||||
|
||||
impl DeletionStrategy {
|
||||
/// This method picks the ideal strategy
|
||||
pub fn pick_strategy(trash_dir: &TrashDir) -> DeletionStrategy {
|
||||
DeletionStrategy::Move
|
||||
}
|
||||
|
||||
fn get_target_trash(
|
||||
&self,
|
||||
mount: impl AsRef<Path>,
|
||||
path: impl AsRef<Path>,
|
||||
) -> Option<(TrashDir, bool)> {
|
||||
let mount = mount.as_ref();
|
||||
let _path = path.as_ref();
|
||||
|
||||
// first, are we on the home mount?
|
||||
if mount == *HOME_MOUNT {
|
||||
return Some((HOME_TRASH.clone(), false));
|
||||
}
|
||||
|
||||
// are we just copying?
|
||||
if let DeletionStrategy::Copy = self {
|
||||
return Some((HOME_TRASH.clone(), true));
|
||||
}
|
||||
|
||||
// try to use the $topdir/.Trash directory
|
||||
let topdir_trash = mount.join(".Trash");
|
||||
if self.should_use_topdir_trash(&topdir_trash) {
|
||||
return Some((
|
||||
TrashDir(topdir_trash.join(utils::get_uid().to_string())),
|
||||
false,
|
||||
));
|
||||
}
|
||||
|
||||
// try to use the $topdir/.Trash-$uid directory
|
||||
let topdir_trash_uid = mount.join(format!(".Trash-{}", utils::get_uid()));
|
||||
if self.should_use_topdir_trash_uid(&topdir_trash_uid) {
|
||||
return Some((TrashDir(topdir_trash_uid), false));
|
||||
}
|
||||
|
||||
// do we have the copy option
|
||||
if let DeletionStrategy::TopdirOrCopy = self {
|
||||
return Some((HOME_TRASH.clone(), true));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn should_use_topdir_trash(&self, path: impl AsRef<Path>) -> bool {
|
||||
let path = path.as_ref();
|
||||
if !path.exists() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let dir = match File::open(path) {
|
||||
Ok(file) => file,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let meta = match dir.metadata() {
|
||||
Ok(meta) => meta,
|
||||
Err(_) => return false,
|
||||
};
|
||||
if meta.file_type().is_symlink() {
|
||||
return false;
|
||||
}
|
||||
let perms = meta.permissions();
|
||||
|
||||
perms.mode() & 0o1000 > 0
|
||||
}
|
||||
|
||||
fn should_use_topdir_trash_uid(&self, path: impl AsRef<Path>) -> bool {
|
||||
let path = path.as_ref();
|
||||
if !path.exists() {
|
||||
match fs::create_dir(path) {
|
||||
Ok(_) => (),
|
||||
Err(_) => return false,
|
||||
};
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn delete(&self, target: impl AsRef<Path>) -> Result<()> {
|
||||
let target = target.as_ref();
|
||||
|
||||
let target_mount = MOUNTS
|
||||
.get_mount_point(target)
|
||||
.ok_or_else(|| anyhow!("couldn't get mount point"))?;
|
||||
let (trash_dir, copy) = match self.get_target_trash(target_mount, target) {
|
||||
Some(x) => x,
|
||||
None => bail!("no trash dir could be selected, u suck"),
|
||||
};
|
||||
|
||||
// preparing metadata
|
||||
let now = Local::now();
|
||||
let elapsed = now.timestamp_millis();
|
||||
let file_name = format!(
|
||||
"{}.{}",
|
||||
elapsed,
|
||||
target.file_name().unwrap().to_str().unwrap()
|
||||
);
|
||||
|
||||
let trash_file_path = trash_dir.files_dir()?.join(&file_name);
|
||||
let trash_info_path = trash_dir.info_dir()?.join(file_name + ".trashinfo");
|
||||
|
||||
let trash_info = TrashInfo {
|
||||
path: utils::into_absolute(target)?,
|
||||
deletion_date: now,
|
||||
deleted_path: trash_file_path.clone(),
|
||||
info_path: trash_info_path.clone(),
|
||||
};
|
||||
{
|
||||
let trash_info_file = File::create(trash_info_path)?;
|
||||
trash_info.write(&trash_info_file)?;
|
||||
}
|
||||
|
||||
// copy the file over
|
||||
if copy {
|
||||
utils::recursive_copy(&target, &trash_file_path)?;
|
||||
fs::remove_dir_all(&target)?;
|
||||
} else {
|
||||
fs::rename(&target, &trash_file_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ pub fn get_uid() -> u64 {
|
|||
unsafe { libc::getuid().into() }
|
||||
}
|
||||
|
||||
/// This function recursively copies all the contents of src into dst.
|
||||
pub fn recursive_copy(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
|
||||
let src = src.as_ref();
|
||||
let dst = dst.as_ref();
|
||||
|
|
Loading…
Reference in a new issue