mirror of
https://github.com/nushell/nushell.git
synced 2025-04-01 11:46:20 +02:00
1235 lines
40 KiB
Rust
1235 lines
40 KiB
Rust
use crate::filesystem::utils::FileStructure;
|
|
use crate::maybe_text_codec::{MaybeTextCodec, StringOrBinary};
|
|
use crate::shell::shell_args::{CdArgs, CopyArgs, LsArgs, MkdirArgs, MvArgs, RemoveArgs};
|
|
use crate::shell::Shell;
|
|
use crate::BufCodecReader;
|
|
use crate::{
|
|
filesystem::dir_info::{DirBuilder, DirInfo},
|
|
CommandArgs,
|
|
};
|
|
use encoding_rs::Encoding;
|
|
use nu_data::config::LocalConfigDiff;
|
|
use nu_path::{canonicalize, canonicalize_with, expand_path_with};
|
|
use nu_protocol::{CommandAction, ConfigPath, TaggedDictBuilder, Value};
|
|
use nu_source::{Span, Tag};
|
|
use nu_stream::{ActionStream, Interruptible, IntoActionStream, OutputStream};
|
|
use std::collections::VecDeque;
|
|
use std::fs::OpenOptions;
|
|
use std::io::{ErrorKind, Write};
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::atomic::AtomicBool;
|
|
use std::sync::Arc;
|
|
use std::{collections::HashMap, io::BufReader};
|
|
|
|
#[cfg(unix)]
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
use nu_errors::ShellError;
|
|
use nu_protocol::{Primitive, ReturnSuccess, UntaggedValue};
|
|
use nu_source::Tagged;
|
|
|
|
const GLOB_PARAMS: glob::MatchOptions = glob::MatchOptions {
|
|
case_sensitive: true,
|
|
require_literal_separator: false,
|
|
require_literal_leading_dot: false,
|
|
};
|
|
|
|
#[derive(Eq, PartialEq, Clone, Copy)]
|
|
pub enum FilesystemShellMode {
|
|
Cli,
|
|
Script,
|
|
}
|
|
|
|
pub struct FilesystemShell {
|
|
pub(crate) path: String,
|
|
pub(crate) last_path: String,
|
|
pub(crate) mode: FilesystemShellMode,
|
|
}
|
|
|
|
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(),
|
|
mode: self.mode,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FilesystemShell {
|
|
fn is_cli(&self) -> bool {
|
|
matches!(&self.mode, FilesystemShellMode::Cli)
|
|
}
|
|
|
|
pub fn basic(mode: FilesystemShellMode) -> FilesystemShell {
|
|
let path = match std::env::current_dir() {
|
|
Ok(path) => path,
|
|
Err(_) => PathBuf::from("/"),
|
|
};
|
|
|
|
FilesystemShell {
|
|
path: path.to_string_lossy().to_string(),
|
|
last_path: path.to_string_lossy().to_string(),
|
|
mode,
|
|
}
|
|
}
|
|
|
|
pub fn with_location(
|
|
path: String,
|
|
mode: FilesystemShellMode,
|
|
) -> Result<FilesystemShell, std::io::Error> {
|
|
let path = canonicalize_with(&path, std::env::current_dir()?)?;
|
|
let path = path.display().to_string();
|
|
let last_path = path.clone();
|
|
|
|
Ok(FilesystemShell {
|
|
path,
|
|
last_path,
|
|
mode,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub fn homedir_if_possible() -> Option<PathBuf> {
|
|
dirs_next::home_dir()
|
|
}
|
|
|
|
impl Shell for FilesystemShell {
|
|
fn name(&self) -> String {
|
|
"filesystem".to_string()
|
|
}
|
|
|
|
fn homedir(&self) -> Option<PathBuf> {
|
|
homedir_if_possible()
|
|
}
|
|
|
|
fn ls(
|
|
&self,
|
|
LsArgs {
|
|
path,
|
|
all,
|
|
long,
|
|
short_names,
|
|
du,
|
|
}: LsArgs,
|
|
name_tag: Tag,
|
|
ctrl_c: Arc<AtomicBool>,
|
|
) -> Result<ActionStream, 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 permission_denied(&p) {
|
|
#[cfg(unix)]
|
|
let error_msg = format!(
|
|
"The permissions of {:o} do not allow access for this user",
|
|
p.metadata()
|
|
.expect(
|
|
"this shouldn't be called since we already know there is a dir"
|
|
)
|
|
.permissions()
|
|
.mode()
|
|
& 0o0777
|
|
);
|
|
#[cfg(not(unix))]
|
|
let error_msg = String::from("Permission denied");
|
|
return Err(ShellError::labeled_error(
|
|
"Permission denied",
|
|
error_msg,
|
|
&p_tag,
|
|
));
|
|
}
|
|
if is_empty_dir(&p) {
|
|
return Ok(ActionStream::empty());
|
|
}
|
|
p.push("*");
|
|
}
|
|
(p, p_tag)
|
|
}
|
|
None => {
|
|
if is_empty_dir(&self.path()) {
|
|
return Ok(ActionStream::empty());
|
|
} else {
|
|
(PathBuf::from("./*"), name_tag.clone())
|
|
}
|
|
}
|
|
};
|
|
|
|
let hidden_dir_specified = is_hidden_dir(&path);
|
|
|
|
let mut paths = glob::glob_with(&path.to_string_lossy(), GLOB_PARAMS)
|
|
.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,
|
|
));
|
|
}
|
|
|
|
let mut hidden_dirs = vec![];
|
|
|
|
// Generated stream: impl Stream<Item = Result<ReturnSuccess, ShellError>
|
|
|
|
Ok(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 path_contains_hidden_folder(&path, &hidden_dirs) {
|
|
return None;
|
|
}
|
|
|
|
if !all && !hidden_dir_specified && is_hidden_dir(&path) {
|
|
if path.is_dir() {
|
|
hidden_dirs.push(path);
|
|
}
|
|
return None;
|
|
}
|
|
|
|
let metadata = match std::fs::symlink_metadata(&path) {
|
|
Ok(metadata) => Some(metadata),
|
|
Err(e) => {
|
|
if e.kind() == ErrorKind::PermissionDenied || e.kind() == ErrorKind::Other {
|
|
None
|
|
} else {
|
|
return Some(Err(e.into()));
|
|
}
|
|
}
|
|
};
|
|
|
|
let entry = dir_entry_dict(
|
|
&path,
|
|
metadata.as_ref(),
|
|
name_tag.clone(),
|
|
long,
|
|
short_names,
|
|
du,
|
|
ctrl_c.clone(),
|
|
)
|
|
.map(ReturnSuccess::Value);
|
|
|
|
Some(entry)
|
|
})
|
|
.interruptible(ctrl_c_copy)
|
|
.into_action_stream())
|
|
}
|
|
|
|
fn cd(&self, args: CdArgs, name: Tag) -> Result<ActionStream, ShellError> {
|
|
let path = match args.path {
|
|
None => match homedir_if_possible() {
|
|
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 {
|
|
// Extra expand attempt allows cd from /home/user/non-existent-dir/..
|
|
// to /home/user
|
|
let path = match canonicalize_with(&target, self.path()) {
|
|
Ok(p) => p,
|
|
_ => expand_path_with(&target, self.path()),
|
|
};
|
|
|
|
if !path.exists() {
|
|
return 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(),
|
|
));
|
|
|
|
//Loading local configs in script mode, makes scripts behave different on different
|
|
//filesystems and might therefore surprise users. That's why we only load them in cli mode.
|
|
if self.is_cli() {
|
|
match canonicalize(self.path()) {
|
|
Err(e) => {
|
|
let err = ShellError::untagged_runtime_error(format!(
|
|
"Could not get absolute path from current fs shell. The error was: {:?}",
|
|
e
|
|
));
|
|
stream.push_back(ReturnSuccess::value(
|
|
UntaggedValue::Error(err).into_value(Tag::unknown()),
|
|
));
|
|
}
|
|
Ok(current_pwd) => {
|
|
let (changes, errs) = LocalConfigDiff::between(current_pwd, path);
|
|
|
|
for err in errs {
|
|
stream.push_back(ReturnSuccess::value(
|
|
UntaggedValue::Error(err).into_value(Tag::unknown()),
|
|
));
|
|
}
|
|
|
|
for unload_cfg in changes.cfgs_to_unload {
|
|
stream.push_back(ReturnSuccess::action(CommandAction::UnloadConfig(
|
|
ConfigPath::Local(unload_cfg),
|
|
)));
|
|
}
|
|
|
|
for load_cfg in changes.cfgs_to_load {
|
|
stream.push_back(ReturnSuccess::action(CommandAction::LoadConfig(
|
|
ConfigPath::Local(load_cfg),
|
|
)));
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
Ok(stream.into())
|
|
}
|
|
|
|
fn cp(
|
|
&self,
|
|
CopyArgs {
|
|
src,
|
|
dst,
|
|
recursive,
|
|
}: CopyArgs,
|
|
name: Tag,
|
|
path: &str,
|
|
) -> Result<ActionStream, 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_with(&source.to_string_lossy(), GLOB_PARAMS) {
|
|
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| matches!(f, Ok(f) if f.is_dir()));
|
|
|
|
if any_source_is_dir && !recursive {
|
|
return Err(ShellError::labeled_error(
|
|
"Directories must be copied using \"--recursive\"",
|
|
"resolves to a directory (not copied)",
|
|
src.tag,
|
|
));
|
|
}
|
|
|
|
for entry in sources.into_iter().flatten() {
|
|
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_with(&dst.item, &path)?;
|
|
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_with(&source_file, &path)?;
|
|
|
|
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(ActionStream::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 {
|
|
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(val);
|
|
}
|
|
}
|
|
|
|
Ok(stream.into())
|
|
}
|
|
|
|
fn mv(
|
|
&self,
|
|
MvArgs { src, dst }: MvArgs,
|
|
_name: Tag,
|
|
path: &str,
|
|
) -> Result<ActionStream, ShellError> {
|
|
let path = Path::new(path);
|
|
let source = path.join(&src.item);
|
|
let destination = path.join(&dst.item);
|
|
|
|
let mut sources = glob::glob_with(&source.to_string_lossy(), GLOB_PARAMS)
|
|
.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,
|
|
));
|
|
}
|
|
|
|
let some_if_source_is_destination = sources
|
|
.iter()
|
|
.find(|f| matches!(f, Ok(f) if destination.starts_with(f)));
|
|
if destination.exists() && destination.is_dir() && sources.len() == 1 {
|
|
if let Some(Ok(filename)) = some_if_source_is_destination {
|
|
return Err(ShellError::labeled_error(
|
|
format!(
|
|
"Not possible to move {:?} to itself",
|
|
filename.file_name().expect("Invalid file name")
|
|
),
|
|
"cannot move to itself",
|
|
dst.tag,
|
|
));
|
|
}
|
|
}
|
|
|
|
if let Some(Ok(_filename)) = some_if_source_is_destination {
|
|
sources = sources
|
|
.into_iter()
|
|
.filter(|f| matches!(f, Ok(f) if !destination.starts_with(f)))
|
|
.collect();
|
|
}
|
|
|
|
for entry in sources.into_iter().flatten() {
|
|
move_file(
|
|
TaggedPathBuf(&entry, &src.tag),
|
|
TaggedPathBuf(&destination, &dst.tag),
|
|
)?
|
|
}
|
|
|
|
Ok(ActionStream::empty())
|
|
}
|
|
|
|
fn rm(
|
|
&self,
|
|
RemoveArgs {
|
|
rest: targets,
|
|
recursive,
|
|
trash: _trash,
|
|
permanent: _permanent,
|
|
force: _force,
|
|
}: RemoveArgs,
|
|
name: Tag,
|
|
path: &str,
|
|
) -> Result<ActionStream, ShellError> {
|
|
let rm_always_trash = nu_data::config::config(Tag::unknown())?
|
|
.get("rm_always_trash")
|
|
.map(|val| val.is_true())
|
|
.unwrap_or(false);
|
|
|
|
#[cfg(not(feature = "trash-support"))]
|
|
{
|
|
if rm_always_trash {
|
|
return Err(ShellError::untagged_runtime_error(
|
|
"Cannot execute `rm`; the current configuration specifies \
|
|
`rm_always_trash = true`, but the current nu executable was not \
|
|
built with feature `trash_support`.",
|
|
));
|
|
} else if _trash {
|
|
return Err(ShellError::labeled_error(
|
|
"Cannot execute `rm` with option `--trash`; feature `trash-support` not enabled",
|
|
"this option is only available if nu is built with the `trash-support` feature",
|
|
name
|
|
));
|
|
}
|
|
}
|
|
|
|
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_with(
|
|
&path.to_string_lossy(),
|
|
glob::MatchOptions {
|
|
require_literal_leading_dot: true,
|
|
..GLOB_PARAMS
|
|
},
|
|
) {
|
|
Ok(files) => {
|
|
for file in files {
|
|
match file {
|
|
Ok(ref f) => {
|
|
// It is not appropriate to try and remove the
|
|
// current directory or its parent when using
|
|
// glob patterns.
|
|
let name = f.display().to_string();
|
|
if name.ends_with("/.") || name.ends_with("/..") {
|
|
continue;
|
|
}
|
|
|
|
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() && !_force {
|
|
return Err(ShellError::labeled_error(
|
|
"No valid paths",
|
|
"no valid paths",
|
|
name_tag,
|
|
));
|
|
}
|
|
|
|
Ok(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() {
|
|
#[cfg(unix)]
|
|
let is_socket = metadata.file_type().is_socket();
|
|
#[cfg(unix)]
|
|
let is_fifo = metadata.file_type().is_fifo();
|
|
|
|
#[cfg(not(unix))]
|
|
let is_socket = false;
|
|
#[cfg(not(unix))]
|
|
let is_fifo = false;
|
|
|
|
if metadata.is_file()
|
|
|| metadata.file_type().is_symlink()
|
|
|| recursive
|
|
|| is_socket
|
|
|| is_fifo
|
|
|| is_empty()
|
|
{
|
|
let result;
|
|
#[cfg(feature = "trash-support")]
|
|
{
|
|
use std::io::Error;
|
|
result = if _trash || (rm_always_trash && !_permanent) {
|
|
trash::delete(&f).map_err(|e: trash::Error| {
|
|
Error::new(ErrorKind::Other, format!("{:?}", e))
|
|
})
|
|
} else if metadata.is_file() {
|
|
std::fs::remove_file(&f)
|
|
} else {
|
|
std::fs::remove_dir_all(&f)
|
|
};
|
|
}
|
|
#[cfg(not(feature = "trash-support"))]
|
|
{
|
|
result = if metadata.is_file() || is_socket || is_fifo {
|
|
std::fs::remove_file(&f)
|
|
} else {
|
|
std::fs::remove_dir_all(&f)
|
|
};
|
|
}
|
|
|
|
if let Err(e) = result {
|
|
let msg =
|
|
format!("Could not delete because: {:}\nTry '--trash' flag", e);
|
|
Err(ShellError::labeled_error(msg, e.to_string(), 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,
|
|
))
|
|
}
|
|
})
|
|
.into_action_stream())
|
|
}
|
|
|
|
fn path(&self) -> String {
|
|
self.path.clone()
|
|
}
|
|
|
|
fn pwd(&self, args: CommandArgs) -> Result<ActionStream, ShellError> {
|
|
let path = PathBuf::from(self.path());
|
|
let p = match 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,
|
|
));
|
|
}
|
|
};
|
|
|
|
Ok(ActionStream::one(ReturnSuccess::value(
|
|
UntaggedValue::Primitive(Primitive::String(p.to_string_lossy().to_string()))
|
|
.into_value(&args.call_info.name_tag),
|
|
)))
|
|
}
|
|
|
|
fn set_path(&mut self, path: String) {
|
|
let pathbuf = PathBuf::from(&path);
|
|
let path = match canonicalize_with(pathbuf.as_path(), self.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 open(
|
|
&self,
|
|
path: &Path,
|
|
name: Span,
|
|
with_encoding: Option<&'static Encoding>,
|
|
) -> Result<
|
|
Box<dyn Iterator<Item = Result<StringOrBinary, ShellError>> + Sync + Send>,
|
|
ShellError,
|
|
> {
|
|
let metadata = std::fs::metadata(&path);
|
|
|
|
let read_full = if let Ok(metadata) = metadata {
|
|
// Arbitrarily capping the file at 32 megs, so we don't try to read large files in all at once
|
|
metadata.is_file() && metadata.len() < (1024 * 1024 * 32)
|
|
} else {
|
|
false
|
|
};
|
|
|
|
if read_full {
|
|
// We should, in theory, be able to read in the whole file as one chunk
|
|
let buffer = std::fs::read(&path).map_err(|e| {
|
|
ShellError::labeled_error(
|
|
format!("Error opening file: {:?}", e),
|
|
"Error opening file",
|
|
name,
|
|
)
|
|
})?;
|
|
|
|
let bytes_mut = bytes::BytesMut::from(&buffer[..]);
|
|
|
|
let mut codec = MaybeTextCodec::new(with_encoding);
|
|
|
|
match codec.decode(&bytes_mut).map_err(|_| {
|
|
ShellError::labeled_error("Error opening file", "error opening file", name)
|
|
})? {
|
|
Some(sb) => Ok(Box::new(vec![Ok(sb)].into_iter())),
|
|
None => Ok(Box::new(vec![].into_iter())),
|
|
}
|
|
} else {
|
|
// We don't know that this is a finite file, so treat it as a stream
|
|
let f = std::fs::File::open(&path).map_err(|e| {
|
|
ShellError::labeled_error(
|
|
format!("Error opening file: {:?}", e),
|
|
"Error opening file",
|
|
name,
|
|
)
|
|
})?;
|
|
let buf_reader = BufReader::new(f);
|
|
let buf_codec = BufCodecReader::new(buf_reader, MaybeTextCodec::new(with_encoding));
|
|
|
|
Ok(Box::new(buf_codec))
|
|
}
|
|
}
|
|
|
|
fn save(
|
|
&mut self,
|
|
full_path: &Path,
|
|
save_data: &[u8],
|
|
name: Span,
|
|
append: bool,
|
|
) -> Result<OutputStream, ShellError> {
|
|
let mut options = OpenOptions::new();
|
|
if append {
|
|
options.append(true)
|
|
} else {
|
|
options.write(true).create(true).truncate(true)
|
|
};
|
|
|
|
match options
|
|
.open(full_path)
|
|
.and_then(|ref mut file| file.write_all(save_data))
|
|
{
|
|
Ok(_) => Ok(OutputStream::empty()),
|
|
Err(e) => Err(ShellError::labeled_error(
|
|
e.to_string(),
|
|
"IO error while saving",
|
|
name,
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn is_interactive(&self) -> bool {
|
|
self.mode == FilesystemShellMode::Cli
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
move_item(from, from_tag, &to)
|
|
}
|
|
|
|
fn move_item(from: &Path, from_tag: &Tag, to: &Path) -> Result<(), ShellError> {
|
|
// 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/folder. This is necessary if we're moving across filesystems or devices.
|
|
std::fs::rename(&from, &to).or_else(|_| {
|
|
match if from.is_file() {
|
|
let mut options = fs_extra::file::CopyOptions::new();
|
|
options.overwrite = true;
|
|
fs_extra::file::move_file(from, to, &options)
|
|
} else {
|
|
let mut options = fs_extra::dir::CopyOptions::new();
|
|
options.overwrite = true;
|
|
options.copy_inside = true;
|
|
fs_extra::dir::move_dir(from, to, &options)
|
|
} {
|
|
Ok(_) => Ok(()),
|
|
Err(e) => Err(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 permission_denied(dir: impl AsRef<Path>) -> bool {
|
|
match dir.as_ref().read_dir() {
|
|
Err(e) => matches!(e.kind(), std::io::ErrorKind::PermissionDenied),
|
|
Ok(_) => false,
|
|
}
|
|
}
|
|
|
|
fn is_hidden_dir(dir: impl AsRef<Path>) -> bool {
|
|
#[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
|
|
}
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
{
|
|
dir.as_ref()
|
|
.file_name()
|
|
.map(|name| name.to_string_lossy().starts_with('.'))
|
|
.unwrap_or(false)
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
use std::os::unix::fs::FileTypeExt;
|
|
|
|
pub fn get_file_type(md: &std::fs::Metadata) -> &str {
|
|
let ft = md.file_type();
|
|
let mut file_type = "Unknown";
|
|
if ft.is_dir() {
|
|
file_type = "Dir";
|
|
} else if ft.is_file() {
|
|
file_type = "File";
|
|
} else if ft.is_symlink() {
|
|
file_type = "Symlink";
|
|
} else {
|
|
#[cfg(unix)]
|
|
{
|
|
if ft.is_block_device() {
|
|
file_type = "Block device";
|
|
} else if ft.is_char_device() {
|
|
file_type = "Char device";
|
|
} else if ft.is_fifo() {
|
|
file_type = "Pipe";
|
|
} else if ft.is_socket() {
|
|
file_type = "Socket";
|
|
}
|
|
}
|
|
}
|
|
file_type
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub(crate) fn dir_entry_dict(
|
|
filename: &std::path::Path,
|
|
metadata: Option<&std::fs::Metadata>,
|
|
tag: impl Into<Tag>,
|
|
long: bool,
|
|
short_name: bool,
|
|
du: bool,
|
|
ctrl_c: Arc<AtomicBool>,
|
|
) -> Result<Value, ShellError> {
|
|
let tag = tag.into();
|
|
let mut dict = TaggedDictBuilder::new(&tag);
|
|
// Insert all columns first to maintain proper table alignment if we can't find (or are not allowed to view) any information
|
|
if long {
|
|
#[cfg(windows)]
|
|
{
|
|
for column in [
|
|
"name", "type", "target", "readonly", "size", "created", "accessed", "modified",
|
|
] {
|
|
dict.insert_untagged(column, UntaggedValue::nothing());
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
for column in [
|
|
"name",
|
|
"type",
|
|
"target",
|
|
"num_links",
|
|
"inode",
|
|
"readonly",
|
|
"mode",
|
|
"uid",
|
|
"group",
|
|
"size",
|
|
"created",
|
|
"accessed",
|
|
"modified",
|
|
] {
|
|
dict.insert_untagged(column, UntaggedValue::nothing());
|
|
}
|
|
}
|
|
} else {
|
|
for column in ["name", "type", "target", "size", "modified"] {
|
|
if column == "target" {
|
|
continue;
|
|
}
|
|
dict.insert_untagged(column, UntaggedValue::nothing());
|
|
}
|
|
}
|
|
|
|
let name = if short_name {
|
|
filename.file_name().and_then(|s| s.to_str())
|
|
} else {
|
|
filename.to_str()
|
|
}
|
|
.ok_or_else(|| {
|
|
ShellError::labeled_error(
|
|
format!("Invalid file name: {:}", filename.to_string_lossy()),
|
|
"invalid file name",
|
|
tag,
|
|
)
|
|
})?;
|
|
|
|
dict.insert_untagged("name", UntaggedValue::filepath(name));
|
|
|
|
if let Some(md) = metadata {
|
|
dict.insert_untagged("type", get_file_type(md));
|
|
}
|
|
|
|
if long {
|
|
if let Some(md) = metadata {
|
|
if md.file_type().is_symlink() {
|
|
let symlink_target_untagged_value: UntaggedValue;
|
|
if let Ok(path_to_link) = filename.read_link() {
|
|
symlink_target_untagged_value =
|
|
UntaggedValue::string(path_to_link.to_string_lossy());
|
|
} else {
|
|
symlink_target_untagged_value =
|
|
UntaggedValue::string("Could not obtain target file's path");
|
|
}
|
|
dict.insert_untagged("target", symlink_target_untagged_value);
|
|
}
|
|
}
|
|
}
|
|
|
|
if long {
|
|
if let Some(md) = metadata {
|
|
dict.insert_untagged(
|
|
"readonly",
|
|
UntaggedValue::boolean(md.permissions().readonly()),
|
|
);
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::MetadataExt;
|
|
let mode = md.permissions().mode();
|
|
dict.insert_untagged(
|
|
"mode",
|
|
UntaggedValue::string(umask::Mode::from(mode).to_string()),
|
|
);
|
|
|
|
let nlinks = md.nlink();
|
|
dict.insert_untagged("num_links", UntaggedValue::string(nlinks.to_string()));
|
|
|
|
let inode = md.ino();
|
|
dict.insert_untagged("inode", UntaggedValue::string(inode.to_string()));
|
|
|
|
if let Some(user) = users::get_user_by_uid(md.uid()) {
|
|
dict.insert_untagged(
|
|
"uid",
|
|
UntaggedValue::string(user.name().to_string_lossy()),
|
|
);
|
|
}
|
|
|
|
if let Some(group) = users::get_group_by_gid(md.gid()) {
|
|
dict.insert_untagged(
|
|
"group",
|
|
UntaggedValue::string(group.name().to_string_lossy()),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(md) = metadata {
|
|
let mut size_untagged_value: UntaggedValue = UntaggedValue::nothing();
|
|
|
|
if md.is_dir() {
|
|
let dir_size: u64 = if du {
|
|
let params = DirBuilder::new(
|
|
Tag {
|
|
anchor: None,
|
|
span: Span::new(0, 2),
|
|
},
|
|
None,
|
|
false,
|
|
None,
|
|
false,
|
|
);
|
|
|
|
DirInfo::new(filename, ¶ms, None, ctrl_c).get_size()
|
|
} else {
|
|
md.len()
|
|
};
|
|
|
|
size_untagged_value = UntaggedValue::filesize(dir_size);
|
|
} else if md.is_file() {
|
|
size_untagged_value = UntaggedValue::filesize(md.len());
|
|
} else if md.file_type().is_symlink() {
|
|
if let Ok(symlink_md) = filename.symlink_metadata() {
|
|
size_untagged_value = UntaggedValue::filesize(symlink_md.len() as u64);
|
|
}
|
|
}
|
|
|
|
dict.insert_untagged("size", size_untagged_value);
|
|
}
|
|
|
|
if let Some(md) = metadata {
|
|
if long {
|
|
if let Ok(c) = md.created() {
|
|
dict.insert_untagged("created", UntaggedValue::system_date(c));
|
|
}
|
|
|
|
if let Ok(a) = md.accessed() {
|
|
dict.insert_untagged("accessed", UntaggedValue::system_date(a));
|
|
}
|
|
}
|
|
|
|
if let Ok(m) = md.modified() {
|
|
dict.insert_untagged("modified", UntaggedValue::system_date(m));
|
|
}
|
|
}
|
|
|
|
Ok(dict.into_value())
|
|
}
|
|
|
|
fn path_contains_hidden_folder(path: &Path, folders: &[PathBuf]) -> bool {
|
|
let path_str = path.to_str().expect("failed to read path");
|
|
if folders
|
|
.iter()
|
|
.any(|p| path_str.starts_with(&p.to_str().expect("failed to read hidden paths")))
|
|
{
|
|
return true;
|
|
}
|
|
false
|
|
}
|