forked from extern/nushell
796 lines
26 KiB
Rust
796 lines
26 KiB
Rust
use crate::commands::cd::CdArgs;
|
|
use crate::commands::command::EvaluatedWholeStreamCommandArgs;
|
|
use crate::commands::cp::CopyArgs;
|
|
use crate::commands::ls::LsArgs;
|
|
use crate::commands::mkdir::MkdirArgs;
|
|
use crate::commands::mv::MoveArgs;
|
|
use crate::commands::rm::RemoveArgs;
|
|
use crate::data::dir_entry_dict;
|
|
use crate::path::canonicalize;
|
|
use crate::prelude::*;
|
|
use crate::shell::completer::NuCompleter;
|
|
use crate::shell::shell::Shell;
|
|
use crate::utils::FileStructure;
|
|
|
|
use rustyline::completion::FilenameCompleter;
|
|
use rustyline::hint::{Hinter, HistoryHinter};
|
|
use std::collections::HashMap;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
#[cfg(unix)]
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
use nu_errors::ShellError;
|
|
use nu_parser::expand_ndots;
|
|
use nu_protocol::{Primitive, ReturnSuccess, UntaggedValue};
|
|
use nu_source::Tagged;
|
|
|
|
pub struct FilesystemShell {
|
|
pub(crate) path: String,
|
|
pub(crate) last_path: String,
|
|
completer: NuCompleter,
|
|
hinter: HistoryHinter,
|
|
}
|
|
|
|
impl std::fmt::Debug for FilesystemShell {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "FilesystemShell @ {}", self.path)
|
|
}
|
|
}
|
|
|
|
impl Clone for FilesystemShell {
|
|
fn clone(&self) -> Self {
|
|
FilesystemShell {
|
|
path: self.path.clone(),
|
|
last_path: self.path.clone(),
|
|
completer: NuCompleter {
|
|
file_completer: FilenameCompleter::new(),
|
|
commands: self.completer.commands.clone(),
|
|
homedir: self.homedir(),
|
|
},
|
|
hinter: HistoryHinter {},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FilesystemShell {
|
|
pub fn basic(commands: CommandRegistry) -> Result<FilesystemShell, std::io::Error> {
|
|
let path = std::env::current_dir()?;
|
|
|
|
Ok(FilesystemShell {
|
|
path: path.to_string_lossy().to_string(),
|
|
last_path: path.to_string_lossy().to_string(),
|
|
completer: NuCompleter {
|
|
file_completer: FilenameCompleter::new(),
|
|
commands,
|
|
homedir: dirs::home_dir(),
|
|
},
|
|
hinter: HistoryHinter {},
|
|
})
|
|
}
|
|
|
|
pub fn with_location(
|
|
path: String,
|
|
commands: CommandRegistry,
|
|
) -> Result<FilesystemShell, std::io::Error> {
|
|
let path = canonicalize(std::env::current_dir()?, &path)?;
|
|
let path = path.display().to_string();
|
|
let last_path = path.clone();
|
|
|
|
Ok(FilesystemShell {
|
|
path,
|
|
last_path,
|
|
completer: NuCompleter {
|
|
file_completer: FilenameCompleter::new(),
|
|
commands,
|
|
homedir: dirs::home_dir(),
|
|
},
|
|
hinter: HistoryHinter {},
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Shell for FilesystemShell {
|
|
fn name(&self) -> String {
|
|
"filesystem".to_string()
|
|
}
|
|
|
|
fn homedir(&self) -> Option<PathBuf> {
|
|
dirs::home_dir()
|
|
}
|
|
|
|
fn ls(
|
|
&self,
|
|
LsArgs {
|
|
path,
|
|
all,
|
|
full,
|
|
short_names,
|
|
with_symlink_targets,
|
|
du,
|
|
}: LsArgs,
|
|
name_tag: Tag,
|
|
ctrl_c: Arc<AtomicBool>,
|
|
) -> Result<OutputStream, ShellError> {
|
|
let ctrl_c_copy = ctrl_c.clone();
|
|
let (path, p_tag) = match path {
|
|
Some(p) => {
|
|
let p_tag = p.tag;
|
|
let mut p = p.item;
|
|
if p.is_dir() {
|
|
if is_empty_dir(&p) {
|
|
return Ok(OutputStream::empty());
|
|
}
|
|
p.push("*");
|
|
}
|
|
(p, p_tag)
|
|
}
|
|
None => {
|
|
if is_empty_dir(&self.path()) {
|
|
return Ok(OutputStream::empty());
|
|
} else {
|
|
(PathBuf::from("./*"), name_tag.clone())
|
|
}
|
|
}
|
|
};
|
|
|
|
let mut paths = glob::glob(&path.to_string_lossy())
|
|
.map_err(|e| ShellError::labeled_error(e.to_string(), "invalid pattern", &p_tag))?
|
|
.peekable();
|
|
|
|
if paths.peek().is_none() {
|
|
return Err(ShellError::labeled_error(
|
|
"No matches found",
|
|
"no matches found",
|
|
&p_tag,
|
|
));
|
|
}
|
|
|
|
// Generated stream: impl Stream<Item = Result<ReturnSuccess, ShellError>
|
|
|
|
Ok(futures::stream::iter(paths.filter_map(move |path| {
|
|
let path = match path.map_err(|e| ShellError::from(e.into_error())) {
|
|
Ok(path) => path,
|
|
Err(err) => return Some(Err(err)),
|
|
};
|
|
|
|
if !all && is_hidden_dir(&path) {
|
|
return None;
|
|
}
|
|
|
|
let metadata = match std::fs::symlink_metadata(&path) {
|
|
Ok(metadata) => Some(metadata),
|
|
Err(e) => {
|
|
if e.kind() == std::io::ErrorKind::PermissionDenied {
|
|
None
|
|
} else {
|
|
return Some(Err(e.into()));
|
|
}
|
|
}
|
|
};
|
|
|
|
let entry = dir_entry_dict(
|
|
&path,
|
|
metadata.as_ref(),
|
|
name_tag.clone(),
|
|
full,
|
|
short_names,
|
|
with_symlink_targets,
|
|
du,
|
|
ctrl_c.clone(),
|
|
)
|
|
.map(ReturnSuccess::Value);
|
|
|
|
Some(entry)
|
|
}))
|
|
.interruptible(ctrl_c_copy)
|
|
.to_output_stream())
|
|
}
|
|
|
|
fn cd(&self, args: CdArgs, name: Tag) -> Result<OutputStream, ShellError> {
|
|
let path = match args.path {
|
|
None => match dirs::home_dir() {
|
|
Some(o) => o,
|
|
_ => {
|
|
return Err(ShellError::labeled_error(
|
|
"Cannot change to home directory",
|
|
"cannot go to home",
|
|
&name,
|
|
))
|
|
}
|
|
},
|
|
Some(v) => {
|
|
let Tagged { item: target, tag } = v;
|
|
if target == Path::new("-") {
|
|
PathBuf::from(&self.last_path)
|
|
} else {
|
|
let path = canonicalize(self.path(), target).map_err(|_| {
|
|
ShellError::labeled_error(
|
|
"Cannot change to directory",
|
|
"directory not found",
|
|
&tag,
|
|
)
|
|
})?;
|
|
|
|
if !path.is_dir() {
|
|
return Err(ShellError::labeled_error(
|
|
"Cannot change to directory",
|
|
"is not a directory",
|
|
&tag,
|
|
));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
let has_exec = path
|
|
.metadata()
|
|
.map(|m| {
|
|
umask::Mode::from(m.permissions().mode()).has(umask::USER_READ)
|
|
})
|
|
.map_err(|e| {
|
|
ShellError::labeled_error(
|
|
"Cannot change to directory",
|
|
format!("cannot stat ({})", e),
|
|
&tag,
|
|
)
|
|
})?;
|
|
|
|
if !has_exec {
|
|
return Err(ShellError::labeled_error(
|
|
"Cannot change to directory",
|
|
"permission denied",
|
|
&tag,
|
|
));
|
|
}
|
|
}
|
|
|
|
path
|
|
}
|
|
}
|
|
};
|
|
|
|
let mut stream = VecDeque::new();
|
|
|
|
stream.push_back(ReturnSuccess::change_cwd(
|
|
path.to_string_lossy().to_string(),
|
|
));
|
|
|
|
Ok(stream.into())
|
|
}
|
|
|
|
fn cp(
|
|
&self,
|
|
CopyArgs {
|
|
src,
|
|
dst,
|
|
recursive,
|
|
}: CopyArgs,
|
|
name: Tag,
|
|
path: &str,
|
|
) -> Result<OutputStream, ShellError> {
|
|
let name_tag = name;
|
|
|
|
let path = Path::new(path);
|
|
let source = path.join(&src.item);
|
|
let destination = path.join(&dst.item);
|
|
|
|
let sources: Vec<_> = match glob::glob(&source.to_string_lossy()) {
|
|
Ok(files) => files.collect(),
|
|
Err(e) => {
|
|
return Err(ShellError::labeled_error(
|
|
e.to_string(),
|
|
"invalid pattern",
|
|
src.tag,
|
|
))
|
|
}
|
|
};
|
|
|
|
if sources.is_empty() {
|
|
return Err(ShellError::labeled_error(
|
|
"No matches found",
|
|
"no matches found",
|
|
src.tag,
|
|
));
|
|
}
|
|
|
|
if sources.len() > 1 && !destination.is_dir() {
|
|
return Err(ShellError::labeled_error(
|
|
"Destination must be a directory when copying multiple files",
|
|
"is not a directory",
|
|
dst.tag,
|
|
));
|
|
}
|
|
|
|
let any_source_is_dir = sources.iter().any(|f| match f {
|
|
Ok(f) => f.is_dir(),
|
|
Err(_) => false,
|
|
});
|
|
|
|
if any_source_is_dir && !recursive.item {
|
|
return Err(ShellError::labeled_error(
|
|
"Directories must be copied using \"--recursive\"",
|
|
"resolves to a directory (not copied)",
|
|
src.tag,
|
|
));
|
|
}
|
|
|
|
for entry in sources {
|
|
if let Ok(entry) = entry {
|
|
let mut sources = FileStructure::new();
|
|
sources.walk_decorate(&entry)?;
|
|
|
|
if entry.is_file() {
|
|
let sources = sources.paths_applying_with(|(source_file, _depth_level)| {
|
|
if destination.is_dir() {
|
|
let mut dest = canonicalize(&path, &dst.item)?;
|
|
if let Some(name) = entry.file_name() {
|
|
dest.push(name);
|
|
}
|
|
Ok((source_file, dest))
|
|
} else {
|
|
Ok((source_file, destination.clone()))
|
|
}
|
|
})?;
|
|
|
|
for (src, dst) in sources {
|
|
if src.is_file() {
|
|
std::fs::copy(src, dst).map_err(|e| {
|
|
ShellError::labeled_error(e.to_string(), e.to_string(), &name_tag)
|
|
})?;
|
|
}
|
|
}
|
|
} else if entry.is_dir() {
|
|
let destination = if !destination.exists() {
|
|
destination.clone()
|
|
} else {
|
|
match entry.file_name() {
|
|
Some(name) => destination.join(name),
|
|
None => {
|
|
return Err(ShellError::labeled_error(
|
|
"Copy aborted. Not a valid path",
|
|
"not a valid path",
|
|
dst.tag,
|
|
))
|
|
}
|
|
}
|
|
};
|
|
|
|
std::fs::create_dir_all(&destination).map_err(|e| {
|
|
ShellError::labeled_error(e.to_string(), e.to_string(), &dst.tag)
|
|
})?;
|
|
|
|
let sources = sources.paths_applying_with(|(source_file, depth_level)| {
|
|
let mut dest = destination.clone();
|
|
let path = canonicalize(&path, &source_file)?;
|
|
|
|
let comps: Vec<_> = path
|
|
.components()
|
|
.map(|fragment| fragment.as_os_str())
|
|
.rev()
|
|
.take(1 + depth_level)
|
|
.collect();
|
|
|
|
for fragment in comps.into_iter().rev() {
|
|
dest.push(fragment);
|
|
}
|
|
|
|
Ok((PathBuf::from(&source_file), dest))
|
|
})?;
|
|
|
|
let dst_tag = &dst.tag;
|
|
for (src, dst) in sources {
|
|
if src.is_dir() && !dst.exists() {
|
|
std::fs::create_dir_all(&dst).map_err(|e| {
|
|
ShellError::labeled_error(e.to_string(), e.to_string(), dst_tag)
|
|
})?;
|
|
}
|
|
|
|
if src.is_file() {
|
|
std::fs::copy(&src, &dst).map_err(|e| {
|
|
ShellError::labeled_error(e.to_string(), e.to_string(), &name_tag)
|
|
})?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(OutputStream::empty())
|
|
}
|
|
|
|
fn mkdir(
|
|
&self,
|
|
MkdirArgs {
|
|
rest: directories,
|
|
show_created_paths,
|
|
}: MkdirArgs,
|
|
name: Tag,
|
|
path: &str,
|
|
) -> Result<OutputStream, ShellError> {
|
|
let path = Path::new(path);
|
|
let mut stream = VecDeque::new();
|
|
|
|
if directories.is_empty() {
|
|
return Err(ShellError::labeled_error(
|
|
"mkdir requires directory paths",
|
|
"needs parameter",
|
|
name,
|
|
));
|
|
}
|
|
|
|
for dir in directories.iter() {
|
|
let create_at = path.join(&dir.item);
|
|
|
|
let dir_res = std::fs::create_dir_all(&create_at);
|
|
if let Err(reason) = dir_res {
|
|
return Err(ShellError::labeled_error(
|
|
reason.to_string(),
|
|
reason.to_string(),
|
|
dir.tag(),
|
|
));
|
|
}
|
|
if show_created_paths {
|
|
let val = format!("{:}", create_at.to_string_lossy()).into();
|
|
stream.push_back(Ok(ReturnSuccess::Value(val)));
|
|
}
|
|
}
|
|
|
|
Ok(stream.into())
|
|
}
|
|
|
|
fn mv(
|
|
&self,
|
|
MoveArgs { src, dst }: MoveArgs,
|
|
_name: Tag,
|
|
path: &str,
|
|
) -> Result<OutputStream, ShellError> {
|
|
let path = Path::new(path);
|
|
let source = path.join(&src.item);
|
|
let destination = path.join(&dst.item);
|
|
|
|
let sources =
|
|
glob::glob(&source.to_string_lossy()).map_or_else(|_| Vec::new(), Iterator::collect);
|
|
|
|
if sources.is_empty() {
|
|
return Err(ShellError::labeled_error(
|
|
"Invalid file or pattern",
|
|
"invalid file or pattern",
|
|
src.tag,
|
|
));
|
|
}
|
|
|
|
// We have two possibilities.
|
|
//
|
|
// First, the destination exists.
|
|
// - If a directory, move everything into that directory, otherwise
|
|
// - if only a single source, overwrite the file, otherwise
|
|
// - error.
|
|
//
|
|
// Second, the destination doesn't exist, so we can only rename a single source. Otherwise
|
|
// it's an error.
|
|
|
|
if (destination.exists() && !destination.is_dir() && sources.len() > 1)
|
|
|| (!destination.exists() && sources.len() > 1)
|
|
{
|
|
return Err(ShellError::labeled_error(
|
|
"Can only move multiple sources if destination is a directory",
|
|
"destination must be a directory when multiple sources",
|
|
dst.tag,
|
|
));
|
|
}
|
|
|
|
for entry in sources {
|
|
if let Ok(entry) = entry {
|
|
move_file(
|
|
TaggedPathBuf(&entry, &src.tag),
|
|
TaggedPathBuf(&destination, &dst.tag),
|
|
)?
|
|
}
|
|
}
|
|
|
|
Ok(OutputStream::empty())
|
|
}
|
|
|
|
fn rm(
|
|
&self,
|
|
RemoveArgs {
|
|
rest: targets,
|
|
recursive,
|
|
trash: _trash,
|
|
permanent: _permanent,
|
|
}: RemoveArgs,
|
|
name: Tag,
|
|
path: &str,
|
|
) -> Result<OutputStream, ShellError> {
|
|
let name_tag = name;
|
|
|
|
if targets.is_empty() {
|
|
return Err(ShellError::labeled_error(
|
|
"rm requires target paths",
|
|
"needs parameter",
|
|
name_tag,
|
|
));
|
|
}
|
|
|
|
let path = Path::new(path);
|
|
let mut all_targets: HashMap<PathBuf, Tag> = HashMap::new();
|
|
for target in targets {
|
|
let all_dots = target
|
|
.item
|
|
.to_str()
|
|
.map_or(false, |v| v.chars().all(|c| c == '.'));
|
|
|
|
if all_dots {
|
|
return Err(ShellError::labeled_error(
|
|
"Cannot remove any parent directory",
|
|
"cannot remove any parent directory",
|
|
target.tag,
|
|
));
|
|
}
|
|
|
|
let path = path.join(&target.item);
|
|
match glob::glob(&path.to_string_lossy()) {
|
|
Ok(files) => {
|
|
for file in files {
|
|
match file {
|
|
Ok(ref f) => {
|
|
all_targets
|
|
.entry(f.clone())
|
|
.or_insert_with(|| target.tag.clone());
|
|
}
|
|
Err(e) => {
|
|
return Err(ShellError::labeled_error(
|
|
format!("Could not remove {:}", path.to_string_lossy()),
|
|
e.to_string(),
|
|
&target.tag,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
return Err(ShellError::labeled_error(
|
|
e.to_string(),
|
|
e.to_string(),
|
|
&name_tag,
|
|
))
|
|
}
|
|
};
|
|
}
|
|
|
|
if all_targets.is_empty() {
|
|
return Err(ShellError::labeled_error(
|
|
"No valid paths",
|
|
"no valid paths",
|
|
name_tag,
|
|
));
|
|
}
|
|
|
|
Ok(
|
|
futures::stream::iter(all_targets.into_iter().map(move |(f, tag)| {
|
|
let is_empty = || match f.read_dir() {
|
|
Ok(mut p) => p.next().is_none(),
|
|
Err(_) => false,
|
|
};
|
|
|
|
if let Ok(metadata) = f.symlink_metadata() {
|
|
if metadata.is_file()
|
|
|| metadata.file_type().is_symlink()
|
|
|| recursive.item
|
|
|| is_empty()
|
|
{
|
|
let result;
|
|
#[cfg(feature = "trash-support")]
|
|
{
|
|
let rm_always_trash = config::config(Tag::unknown())?
|
|
.get("rm_always_trash")
|
|
.map(|val| val.is_true())
|
|
.unwrap_or(false);
|
|
result = if _trash.item || (rm_always_trash && !_permanent.item) {
|
|
trash::remove(&f).map_err(|_| f.to_string_lossy())
|
|
} else if metadata.is_file() {
|
|
std::fs::remove_file(&f).map_err(|_| f.to_string_lossy())
|
|
} else {
|
|
std::fs::remove_dir_all(&f).map_err(|_| f.to_string_lossy())
|
|
};
|
|
}
|
|
#[cfg(not(feature = "trash-support"))]
|
|
{
|
|
result = if metadata.is_file() {
|
|
std::fs::remove_file(&f).map_err(|_| f.to_string_lossy())
|
|
} else {
|
|
std::fs::remove_dir_all(&f).map_err(|_| f.to_string_lossy())
|
|
};
|
|
}
|
|
|
|
if let Err(e) = result {
|
|
let msg = format!("Could not delete {:}", e);
|
|
Err(ShellError::labeled_error(msg, e, tag))
|
|
} else {
|
|
let val = format!("deleted {:}", f.to_string_lossy()).into();
|
|
Ok(ReturnSuccess::Value(val))
|
|
}
|
|
} else {
|
|
let msg =
|
|
format!("Cannot remove {:}. try --recursive", f.to_string_lossy());
|
|
Err(ShellError::labeled_error(
|
|
msg,
|
|
"cannot remove non-empty directory",
|
|
tag,
|
|
))
|
|
}
|
|
} else {
|
|
let msg = format!("no such file or directory: {:}", f.to_string_lossy());
|
|
Err(ShellError::labeled_error(
|
|
msg,
|
|
"no such file or directory",
|
|
tag,
|
|
))
|
|
}
|
|
}))
|
|
.to_output_stream(),
|
|
)
|
|
}
|
|
|
|
fn path(&self) -> String {
|
|
self.path.clone()
|
|
}
|
|
|
|
fn pwd(&self, args: EvaluatedWholeStreamCommandArgs) -> Result<OutputStream, ShellError> {
|
|
let path = PathBuf::from(self.path());
|
|
let p = match dunce::canonicalize(path.as_path()) {
|
|
Ok(p) => p,
|
|
Err(_) => {
|
|
return Err(ShellError::labeled_error(
|
|
"unable to show current directory",
|
|
"pwd command failed",
|
|
&args.call_info.name_tag,
|
|
));
|
|
}
|
|
};
|
|
|
|
let mut stream = VecDeque::new();
|
|
stream.push_back(ReturnSuccess::value(
|
|
UntaggedValue::Primitive(Primitive::String(p.to_string_lossy().to_string()))
|
|
.into_value(&args.call_info.name_tag),
|
|
));
|
|
|
|
Ok(stream.into())
|
|
}
|
|
|
|
fn set_path(&mut self, path: String) {
|
|
let pathbuf = PathBuf::from(&path);
|
|
let path = match canonicalize(self.path(), pathbuf.as_path()) {
|
|
Ok(path) => {
|
|
let _ = std::env::set_current_dir(&path);
|
|
std::env::set_var("PWD", &path);
|
|
path
|
|
}
|
|
_ => {
|
|
// TODO: handle the case where the path cannot be canonicalized
|
|
pathbuf
|
|
}
|
|
};
|
|
self.last_path = self.path.clone();
|
|
self.path = path.to_string_lossy().to_string();
|
|
}
|
|
|
|
fn complete(
|
|
&self,
|
|
line: &str,
|
|
pos: usize,
|
|
ctx: &rustyline::Context<'_>,
|
|
) -> Result<(usize, Vec<rustyline::completion::Pair>), rustyline::error::ReadlineError> {
|
|
let expanded = expand_ndots(&line);
|
|
|
|
// Find the first not-matching char position, if there is one
|
|
let differ_pos = line
|
|
.chars()
|
|
.zip(expanded.chars())
|
|
.enumerate()
|
|
.find(|(_index, (a, b))| a != b)
|
|
.map(|(differ_pos, _)| differ_pos);
|
|
|
|
let pos = if let Some(differ_pos) = differ_pos {
|
|
if differ_pos < pos {
|
|
pos + (expanded.len() - line.len())
|
|
} else {
|
|
pos
|
|
}
|
|
} else {
|
|
pos
|
|
};
|
|
|
|
self.completer.complete(&expanded, pos, ctx)
|
|
}
|
|
|
|
fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option<String> {
|
|
self.hinter.hint(line, pos, ctx)
|
|
}
|
|
}
|
|
|
|
struct TaggedPathBuf<'a>(&'a PathBuf, &'a Tag);
|
|
|
|
fn move_file(from: TaggedPathBuf, to: TaggedPathBuf) -> Result<(), ShellError> {
|
|
let TaggedPathBuf(from, from_tag) = from;
|
|
let TaggedPathBuf(to, to_tag) = to;
|
|
|
|
if to.exists() && from.is_dir() && to.is_file() {
|
|
return Err(ShellError::labeled_error(
|
|
"Cannot rename a directory to a file",
|
|
"invalid destination",
|
|
to_tag,
|
|
));
|
|
}
|
|
|
|
let destination_dir_exists = if to.is_dir() {
|
|
true
|
|
} else {
|
|
to.parent().map(Path::exists).unwrap_or(true)
|
|
};
|
|
|
|
if !destination_dir_exists {
|
|
return Err(ShellError::labeled_error(
|
|
"Destination directory does not exist",
|
|
"destination does not exist",
|
|
to_tag,
|
|
));
|
|
}
|
|
|
|
let mut to = to.clone();
|
|
if to.is_dir() {
|
|
let from_file_name = match from.file_name() {
|
|
Some(name) => name,
|
|
None => {
|
|
return Err(ShellError::labeled_error(
|
|
"Not a valid entry name",
|
|
"not a valid entry name",
|
|
from_tag,
|
|
))
|
|
}
|
|
};
|
|
|
|
to.push(from_file_name);
|
|
}
|
|
|
|
// We first try a rename, which is a quick operation. If that doesn't work, we'll try a copy
|
|
// and remove the old file. This is necessary if we're moving across filesystems.
|
|
std::fs::rename(&from, &to)
|
|
.or_else(|_| std::fs::copy(&from, &to).and_then(|_| std::fs::remove_file(&from)))
|
|
.map_err(|e| {
|
|
ShellError::labeled_error(
|
|
format!("Could not move {:?} to {:?}. {:}", from, to, e.to_string()),
|
|
"could not move",
|
|
from_tag,
|
|
)
|
|
})
|
|
}
|
|
|
|
fn is_empty_dir(dir: impl AsRef<Path>) -> bool {
|
|
match dir.as_ref().read_dir() {
|
|
Err(_) => true,
|
|
Ok(mut s) => s.next().is_none(),
|
|
}
|
|
}
|
|
|
|
fn is_hidden_dir(dir: impl AsRef<Path>) -> bool {
|
|
cfg_if::cfg_if! {
|
|
if #[cfg(windows)] {
|
|
use std::os::windows::fs::MetadataExt;
|
|
|
|
if let Ok(metadata) = dir.as_ref().metadata() {
|
|
let attributes = metadata.file_attributes();
|
|
// https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
|
|
(attributes & 0x2) != 0
|
|
} else {
|
|
false
|
|
}
|
|
} else {
|
|
dir.as_ref()
|
|
.file_name()
|
|
.map(|name| name.to_string_lossy().starts_with('.'))
|
|
.unwrap_or(false)
|
|
}
|
|
}
|
|
}
|