mirror of
https://github.com/chipsenkbeil/distant.git
synced 2024-11-05 12:00:36 +00:00
Feat: set permissions support (#184)
This commit is contained in:
parent
137b4dc289
commit
ea0424e2f4
@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- New `SetPermissions` enum variant on protocol request
|
||||
- New `set_permissions` method available `DistantApi` and implemented by local
|
||||
server (ssh unavailable due to https://github.com/wez/wezterm/issues/3784)
|
||||
- Implementation of `DistantChannelExt::set_permissions`
|
||||
|
||||
## [0.20.0-alpha.6]
|
||||
|
||||
### Changed
|
||||
|
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -822,6 +822,7 @@ dependencies = [
|
||||
"distant-core",
|
||||
"distant-ssh2",
|
||||
"env_logger",
|
||||
"file-mode",
|
||||
"flexi_logger",
|
||||
"fork",
|
||||
"indoc",
|
||||
@ -1098,6 +1099,15 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "file-mode"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773ea145485772b8d354624b32adbe20e776353d3e48c7b03ef44e3455e9815c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filedescriptor"
|
||||
version = "0.8.2"
|
||||
|
@ -34,6 +34,7 @@ derive_more = { version = "0.99.17", default-features = false, features = ["disp
|
||||
dialoguer = { version = "0.10.3", default-features = false }
|
||||
distant-core = { version = "=0.20.0-alpha.7", path = "distant-core", features = ["schemars"] }
|
||||
directories = "5.0.0"
|
||||
file-mode = "0.1.2"
|
||||
flexi_logger = "0.25.3"
|
||||
indoc = "2.0.1"
|
||||
log = "0.4.17"
|
||||
|
@ -8,8 +8,8 @@ use distant_net::server::{ConnectionCtx, Reply, ServerCtx, ServerHandler};
|
||||
use log::*;
|
||||
|
||||
use crate::protocol::{
|
||||
self, Capabilities, ChangeKind, DirEntry, Environment, Error, Metadata, ProcessId, PtySize,
|
||||
SearchId, SearchQuery, SystemInfo,
|
||||
self, Capabilities, ChangeKind, DirEntry, Environment, Error, Metadata, Permissions, ProcessId,
|
||||
PtySize, SearchId, SearchQuery, SetPermissionsOptions, SystemInfo,
|
||||
};
|
||||
|
||||
mod local;
|
||||
@ -316,6 +316,24 @@ pub trait DistantApi {
|
||||
unsupported("metadata")
|
||||
}
|
||||
|
||||
/// Sets permissions for a file, directory, or symlink.
|
||||
///
|
||||
/// * `path` - the path to the file, directory, or symlink
|
||||
/// * `resolve_symlink` - if true, will resolve the path to the underlying file/directory
|
||||
/// * `permissions` - the new permissions to apply
|
||||
///
|
||||
/// *Override this, otherwise it will return "unsupported" as an error.*
|
||||
#[allow(unused_variables)]
|
||||
async fn set_permissions(
|
||||
&self,
|
||||
ctx: DistantCtx<Self::LocalData>,
|
||||
path: PathBuf,
|
||||
permissions: Permissions,
|
||||
options: SetPermissionsOptions,
|
||||
) -> io::Result<()> {
|
||||
unsupported("set_permissions")
|
||||
}
|
||||
|
||||
/// Searches files for matches based on a query.
|
||||
///
|
||||
/// * `query` - the specific query to perform
|
||||
@ -632,6 +650,16 @@ where
|
||||
.await
|
||||
.map(protocol::Response::Metadata)
|
||||
.unwrap_or_else(protocol::Response::from),
|
||||
protocol::Request::SetPermissions {
|
||||
path,
|
||||
permissions,
|
||||
options,
|
||||
} => server
|
||||
.api
|
||||
.set_permissions(ctx, path, permissions, options)
|
||||
.await
|
||||
.map(|_| protocol::Response::Ok)
|
||||
.unwrap_or_else(protocol::Response::from),
|
||||
protocol::Request::Search { query } => server
|
||||
.api
|
||||
.search(ctx, query)
|
||||
|
@ -2,13 +2,14 @@ use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use ignore::{DirEntry as WalkDirEntry, WalkBuilder};
|
||||
use log::*;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::protocol::{
|
||||
Capabilities, ChangeKind, ChangeKindSet, DirEntry, Environment, FileType, Metadata, ProcessId,
|
||||
PtySize, SearchId, SearchQuery, SystemInfo,
|
||||
Capabilities, ChangeKind, ChangeKindSet, DirEntry, Environment, FileType, Metadata,
|
||||
Permissions, ProcessId, PtySize, SearchId, SearchQuery, SetPermissionsOptions, SystemInfo,
|
||||
};
|
||||
use crate::{DistantApi, DistantCtx};
|
||||
|
||||
@ -411,6 +412,135 @@ impl DistantApi for LocalDistantApi {
|
||||
Metadata::read(path, canonicalize, resolve_file_type).await
|
||||
}
|
||||
|
||||
async fn set_permissions(
|
||||
&self,
|
||||
_ctx: DistantCtx<Self::LocalData>,
|
||||
path: PathBuf,
|
||||
permissions: Permissions,
|
||||
options: SetPermissionsOptions,
|
||||
) -> io::Result<()> {
|
||||
/// Builds permissions from the metadata of `entry`, failing if metadata was unavailable.
|
||||
fn build_permissions(
|
||||
entry: &WalkDirEntry,
|
||||
permissions: &Permissions,
|
||||
) -> io::Result<std::fs::Permissions> {
|
||||
// Load up our std permissions so we can modify them
|
||||
let mut std_permissions = entry
|
||||
.metadata()
|
||||
.map_err(|x| match x.io_error() {
|
||||
Some(x) => io::Error::new(x.kind(), format!("(Read permissions failed) {x}")),
|
||||
None => io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("(Read permissions failed) {x}"),
|
||||
),
|
||||
})?
|
||||
.permissions();
|
||||
|
||||
// Apply the readonly flag for all platforms
|
||||
if let Some(readonly) = permissions.is_readonly() {
|
||||
std_permissions.set_readonly(readonly);
|
||||
}
|
||||
|
||||
// On Unix platforms, we can apply a bitset change
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::prelude::*;
|
||||
let mut current = Permissions::from(std_permissions.clone());
|
||||
current.apply_from(permissions);
|
||||
std_permissions.set_mode(current.to_unix_mode());
|
||||
}
|
||||
|
||||
Ok(std_permissions)
|
||||
}
|
||||
|
||||
async fn set_permissions_impl(
|
||||
entry: &WalkDirEntry,
|
||||
permissions: &Permissions,
|
||||
) -> io::Result<()> {
|
||||
let permissions = match permissions.is_complete() {
|
||||
// If we are on a Unix platform and we have a full permission set, we do not need
|
||||
// to retrieve the permissions to modify them and can instead produce a new
|
||||
// permission set purely from the permissions
|
||||
#[cfg(unix)]
|
||||
true => std::fs::Permissions::from(*permissions),
|
||||
|
||||
// Otherwise, we have to load in the permissions from metadata and merge with our
|
||||
// changes
|
||||
_ => build_permissions(entry, permissions)?,
|
||||
};
|
||||
|
||||
if log_enabled!(Level::Trace) {
|
||||
let mut output = String::new();
|
||||
output.push_str("readonly = ");
|
||||
output.push_str(if permissions.readonly() {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
});
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::prelude::*;
|
||||
output.push_str(&format!(", mode = {:#o}", permissions.mode()));
|
||||
}
|
||||
|
||||
trace!("Setting {:?} permissions to ({})", entry.path(), output);
|
||||
}
|
||||
|
||||
tokio::fs::set_permissions(entry.path(), permissions)
|
||||
.await
|
||||
.map_err(|x| io::Error::new(x.kind(), format!("(Set permissions failed) {x}")))
|
||||
}
|
||||
|
||||
// NOTE: On Unix platforms, setting permissions would automatically resolve the symlink,
|
||||
// but on Windows this is not the case. So, on Windows, we need to resolve our path by
|
||||
// following the symlink prior to feeding it to the walk builder because it does not appear
|
||||
// to resolve the symlink itself.
|
||||
//
|
||||
// We do this by canonicalizing the path if following symlinks is enabled.
|
||||
let path = if options.follow_symlinks {
|
||||
tokio::fs::canonicalize(path).await?
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
let walk = WalkBuilder::new(path)
|
||||
.follow_links(options.follow_symlinks)
|
||||
.max_depth(if options.recursive { None } else { Some(0) })
|
||||
.standard_filters(false)
|
||||
.skip_stdout(true)
|
||||
.build();
|
||||
|
||||
// Process as much as possible and then fail with an error
|
||||
let mut errors = Vec::new();
|
||||
for entry in walk {
|
||||
match entry {
|
||||
Ok(entry) if entry.path_is_symlink() && options.exclude_symlinks => {}
|
||||
Ok(entry) => {
|
||||
if let Err(x) = set_permissions_impl(&entry, &permissions).await {
|
||||
errors.push(format!("{:?}: {x}", entry.path()));
|
||||
}
|
||||
}
|
||||
Err(x) => {
|
||||
errors.push(x.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
errors
|
||||
.into_iter()
|
||||
.map(|x| format!("* {x}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn search(
|
||||
&self,
|
||||
ctx: DistantCtx<Self::LocalData>,
|
||||
@ -1810,6 +1940,442 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn set_permissions_should_set_readonly_flag_if_specified() {
|
||||
let (api, ctx, _rx) = setup(1).await;
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
// Verify that not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(!permissions.readonly(), "File is already set to readonly");
|
||||
|
||||
// Change the file permissions
|
||||
api.set_permissions(
|
||||
ctx,
|
||||
file.path().to_path_buf(),
|
||||
Permissions::readonly(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Retrieve permissions to verify set
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(permissions.readonly(), "File not set to readonly");
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
#[cfg_attr(not(unix), ignore)]
|
||||
async fn set_permissions_should_set_unix_permissions_if_on_unix_platform() {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::prelude::*;
|
||||
|
||||
let (api, ctx, _rx) = setup(1).await;
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
// Verify that permissions do not match our readonly state
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
let mode = permissions.mode() & 0o777;
|
||||
assert_ne!(mode, 0o400, "File is already set to 0o400");
|
||||
|
||||
// Change the file permissions
|
||||
api.set_permissions(
|
||||
ctx,
|
||||
file.path().to_path_buf(),
|
||||
Permissions::from_unix_mode(0o400),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Retrieve file permissions to verify set
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
|
||||
// Drop the upper bits that mode can have (only care about read/write/exec)
|
||||
let mode = permissions.mode() & 0o777;
|
||||
|
||||
assert_eq!(mode, 0o400, "Wrong permissions on file: {:o}", mode);
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
#[cfg_attr(unix, ignore)]
|
||||
async fn set_permissions_should_set_readonly_flag_if_not_on_unix_platform() {
|
||||
let (api, ctx, _rx) = setup(1).await;
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
// Verify that not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(!permissions.readonly(), "File is already set to readonly");
|
||||
|
||||
// Change the file permissions to be readonly (in general)
|
||||
api.set_permissions(
|
||||
ctx,
|
||||
file.path().to_path_buf(),
|
||||
Permissions::from_unix_mode(0o400),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// Retrieve file permissions to verify set
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
|
||||
assert!(permissions.readonly(), "File not marked as readonly");
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn set_permissions_should_not_recurse_if_option_false() {
|
||||
let (api, ctx, _rx) = setup(1).await;
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
let symlink = temp.child("link");
|
||||
symlink.symlink_to_file(file.path()).unwrap();
|
||||
|
||||
// Verify that dir is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(temp.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"Temp dir is already set to readonly"
|
||||
);
|
||||
|
||||
// Verify that file is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(!permissions.readonly(), "File is already set to readonly");
|
||||
|
||||
// Verify that symlink is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(symlink.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"Symlink is already set to readonly"
|
||||
);
|
||||
|
||||
// Change the permissions of the directory and not the contents underneath
|
||||
api.set_permissions(
|
||||
ctx,
|
||||
temp.path().to_path_buf(),
|
||||
Permissions::readonly(),
|
||||
SetPermissionsOptions {
|
||||
recursive: false,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Retrieve permissions of the file, symlink, and directory to verify set
|
||||
let permissions = tokio::fs::symlink_metadata(temp.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(permissions.readonly(), "Temp directory not set to readonly");
|
||||
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(!permissions.readonly(), "File unexpectedly set to readonly");
|
||||
|
||||
let permissions = tokio::fs::symlink_metadata(symlink.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"Symlink unexpectedly set to readonly"
|
||||
);
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn set_permissions_should_traverse_symlinks_while_recursing_if_following_symlinks_enabled(
|
||||
) {
|
||||
let (api, ctx, _rx) = setup(1).await;
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
let temp2 = assert_fs::TempDir::new().unwrap();
|
||||
let file2 = temp2.child("file");
|
||||
file2.write_str("some text").unwrap();
|
||||
|
||||
let symlink = temp.child("link");
|
||||
symlink.symlink_to_dir(temp2.path()).unwrap();
|
||||
|
||||
// Verify that symlink is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(file2.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(!permissions.readonly(), "File2 is already set to readonly");
|
||||
|
||||
// Change the main directory permissions
|
||||
api.set_permissions(
|
||||
ctx,
|
||||
temp.path().to_path_buf(),
|
||||
Permissions::readonly(),
|
||||
SetPermissionsOptions {
|
||||
follow_symlinks: true,
|
||||
recursive: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Retrieve permissions referenced by another directory
|
||||
let permissions = tokio::fs::symlink_metadata(file2.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(permissions.readonly(), "File2 not set to readonly");
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn set_permissions_should_not_traverse_symlinks_while_recursing_if_following_symlinks_disabled(
|
||||
) {
|
||||
let (api, ctx, _rx) = setup(1).await;
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
let temp2 = assert_fs::TempDir::new().unwrap();
|
||||
let file2 = temp2.child("file");
|
||||
file2.write_str("some text").unwrap();
|
||||
|
||||
let symlink = temp.child("link");
|
||||
symlink.symlink_to_dir(temp2.path()).unwrap();
|
||||
|
||||
// Verify that symlink is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(file2.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(!permissions.readonly(), "File2 is already set to readonly");
|
||||
|
||||
// Change the main directory permissions
|
||||
api.set_permissions(
|
||||
ctx,
|
||||
temp.path().to_path_buf(),
|
||||
Permissions::readonly(),
|
||||
SetPermissionsOptions {
|
||||
follow_symlinks: false,
|
||||
recursive: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Retrieve permissions referenced by another directory
|
||||
let permissions = tokio::fs::symlink_metadata(file2.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"File2 unexpectedly set to readonly"
|
||||
);
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn set_permissions_should_skip_symlinks_if_exclude_symlinks_enabled() {
|
||||
let (api, ctx, _rx) = setup(1).await;
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
let symlink = temp.child("link");
|
||||
symlink.symlink_to_file(file.path()).unwrap();
|
||||
|
||||
// Verify that symlink is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(symlink.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"Symlink is already set to readonly"
|
||||
);
|
||||
|
||||
// Change the symlink permissions
|
||||
api.set_permissions(
|
||||
ctx,
|
||||
symlink.path().to_path_buf(),
|
||||
Permissions::readonly(),
|
||||
SetPermissionsOptions {
|
||||
exclude_symlinks: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Retrieve permissions to verify not set
|
||||
let permissions = tokio::fs::symlink_metadata(symlink.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"Symlink (or file underneath) set to readonly"
|
||||
);
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn set_permissions_should_support_recursive_if_option_specified() {
|
||||
let (api, ctx, _rx) = setup(1).await;
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
// Verify that dir is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(temp.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"Temp dir is already set to readonly"
|
||||
);
|
||||
|
||||
// Verify that file is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(!permissions.readonly(), "File is already set to readonly");
|
||||
|
||||
// Change the permissions of the file pointed to by the symlink
|
||||
api.set_permissions(
|
||||
ctx,
|
||||
temp.path().to_path_buf(),
|
||||
Permissions::readonly(),
|
||||
SetPermissionsOptions {
|
||||
recursive: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Retrieve permissions of the file, symlink, and directory to verify set
|
||||
let permissions = tokio::fs::symlink_metadata(temp.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(permissions.readonly(), "Temp directory not set to readonly");
|
||||
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(permissions.readonly(), "File not set to readonly");
|
||||
}
|
||||
|
||||
#[test(tokio::test)]
|
||||
async fn set_permissions_should_support_following_explicit_symlink_if_option_specified() {
|
||||
let (api, ctx, _rx) = setup(1).await;
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
let symlink = temp.child("link");
|
||||
symlink.symlink_to_file(file.path()).unwrap();
|
||||
|
||||
// Verify that file is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(!permissions.readonly(), "File is already set to readonly");
|
||||
|
||||
// Verify that symlink is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(symlink.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"Symlink is already set to readonly"
|
||||
);
|
||||
|
||||
// Change the permissions of the file pointed to by the symlink
|
||||
api.set_permissions(
|
||||
ctx,
|
||||
symlink.path().to_path_buf(),
|
||||
Permissions::readonly(),
|
||||
SetPermissionsOptions {
|
||||
follow_symlinks: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Retrieve permissions of the file and symlink to verify set
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(permissions.readonly(), "File not set to readonly");
|
||||
|
||||
let permissions = tokio::fs::symlink_metadata(symlink.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"Symlink unexpectedly set to readonly"
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: Ignoring on windows because it's using WSL which wants a Linux path
|
||||
// with / but thinks it's on windows and is providing \
|
||||
#[test(tokio::test)]
|
||||
|
@ -11,8 +11,8 @@ use crate::client::{
|
||||
Watcher,
|
||||
};
|
||||
use crate::protocol::{
|
||||
self, Capabilities, ChangeKindSet, DirEntry, Environment, Error as Failure, Metadata, PtySize,
|
||||
SearchId, SearchQuery, SystemInfo,
|
||||
self, Capabilities, ChangeKindSet, DirEntry, Environment, Error as Failure, Metadata,
|
||||
Permissions, PtySize, SearchId, SearchQuery, SetPermissionsOptions, SystemInfo,
|
||||
};
|
||||
|
||||
pub type AsyncReturn<'a, T, E = io::Error> =
|
||||
@ -57,6 +57,14 @@ pub trait DistantChannelExt {
|
||||
resolve_file_type: bool,
|
||||
) -> AsyncReturn<'_, Metadata>;
|
||||
|
||||
/// Sets permissions for a path on a remote machine
|
||||
fn set_permissions(
|
||||
&mut self,
|
||||
path: impl Into<PathBuf>,
|
||||
permissions: Permissions,
|
||||
options: SetPermissionsOptions,
|
||||
) -> AsyncReturn<'_, ()>;
|
||||
|
||||
/// Perform a search
|
||||
fn search(&mut self, query: impl Into<SearchQuery>) -> AsyncReturn<'_, Searcher>;
|
||||
|
||||
@ -257,6 +265,23 @@ impl DistantChannelExt
|
||||
)
|
||||
}
|
||||
|
||||
fn set_permissions(
|
||||
&mut self,
|
||||
path: impl Into<PathBuf>,
|
||||
permissions: Permissions,
|
||||
options: SetPermissionsOptions,
|
||||
) -> AsyncReturn<'_, ()> {
|
||||
make_body!(
|
||||
self,
|
||||
protocol::Request::SetPermissions {
|
||||
path: path.into(),
|
||||
permissions,
|
||||
options,
|
||||
},
|
||||
@ok
|
||||
)
|
||||
}
|
||||
|
||||
fn search(&mut self, query: impl Into<SearchQuery>) -> AsyncReturn<'_, Searcher> {
|
||||
let query = query.into();
|
||||
Box::pin(async move { Searcher::search(self.clone(), query).await })
|
||||
|
@ -23,6 +23,9 @@ pub use filesystem::*;
|
||||
mod metadata;
|
||||
pub use metadata::*;
|
||||
|
||||
mod permissions;
|
||||
pub use permissions::*;
|
||||
|
||||
mod pty;
|
||||
pub use pty::*;
|
||||
|
||||
@ -344,6 +347,22 @@ pub enum Request {
|
||||
resolve_file_type: bool,
|
||||
},
|
||||
|
||||
/// Sets permissions on a file, directory, or symlink on the remote machine
|
||||
#[strum_discriminants(strum(
|
||||
message = "Supports setting permissions on a file, directory, or symlink"
|
||||
))]
|
||||
SetPermissions {
|
||||
/// The path to the file, directory, or symlink on the remote machine
|
||||
path: PathBuf,
|
||||
|
||||
/// New permissions to apply to the file, directory, or symlink
|
||||
permissions: Permissions,
|
||||
|
||||
/// Additional options to supply when setting permissions
|
||||
#[serde(default)]
|
||||
options: SetPermissionsOptions,
|
||||
},
|
||||
|
||||
/// Searches filesystem using the provided query
|
||||
#[strum_discriminants(strum(message = "Supports searching filesystem using queries"))]
|
||||
Search {
|
||||
|
294
distant-core/src/protocol/permissions.rs
Normal file
294
distant-core/src/protocol/permissions.rs
Normal file
@ -0,0 +1,294 @@
|
||||
use bitflags::bitflags;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(default, deny_unknown_fields, rename_all = "snake_case")]
|
||||
pub struct SetPermissionsOptions {
|
||||
/// Whether or not to exclude symlinks from traversal entirely, meaning that permissions will
|
||||
/// not be set on symlinks (usually resolving the symlink and setting the permission of the
|
||||
/// referenced file or directory) that are explicitly provided or show up during recursion.
|
||||
pub exclude_symlinks: bool,
|
||||
|
||||
/// Whether or not to traverse symlinks when recursively setting permissions. Note that this
|
||||
/// does NOT influence setting permissions when encountering a symlink as most platforms will
|
||||
/// resolve the symlink before setting permissions.
|
||||
pub follow_symlinks: bool,
|
||||
|
||||
/// Whether or not to set the permissions of the file hierarchies rooted in the paths, instead
|
||||
/// of just the paths themselves.
|
||||
pub recursive: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl SetPermissionsOptions {
|
||||
pub fn root_schema() -> schemars::schema::RootSchema {
|
||||
schemars::schema_for!(SetPermissionsOptions)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents permissions to apply to some path on a remote machine
|
||||
///
|
||||
/// When used to set permissions on a file, directory, or symlink,
|
||||
/// only fields that are set (not `None`) will be applied.
|
||||
///
|
||||
/// On `Unix` platforms, this translates directly into the mode that
|
||||
/// you would find with `chmod`. On all other platforms, this uses the
|
||||
/// write flags to determine whether or not to set the readonly status.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Permissions {
|
||||
/// Represents whether or not owner can read from the file
|
||||
pub owner_read: Option<bool>,
|
||||
|
||||
/// Represents whether or not owner can write to the file
|
||||
pub owner_write: Option<bool>,
|
||||
|
||||
/// Represents whether or not owner can execute the file
|
||||
pub owner_exec: Option<bool>,
|
||||
|
||||
/// Represents whether or not associated group can read from the file
|
||||
pub group_read: Option<bool>,
|
||||
|
||||
/// Represents whether or not associated group can write to the file
|
||||
pub group_write: Option<bool>,
|
||||
|
||||
/// Represents whether or not associated group can execute the file
|
||||
pub group_exec: Option<bool>,
|
||||
|
||||
/// Represents whether or not other can read from the file
|
||||
pub other_read: Option<bool>,
|
||||
|
||||
/// Represents whether or not other can write to the file
|
||||
pub other_write: Option<bool>,
|
||||
|
||||
/// Represents whether or not other can execute the file
|
||||
pub other_exec: Option<bool>,
|
||||
}
|
||||
|
||||
impl Permissions {
|
||||
/// Creates a set of [`Permissions`] that indicate readonly status.
|
||||
///
|
||||
/// ```
|
||||
/// use distant_core::protocol::Permissions;
|
||||
///
|
||||
/// let permissions = Permissions::readonly();
|
||||
/// assert_eq!(permissions.is_readonly(), Some(true));
|
||||
/// assert_eq!(permissions.is_writable(), Some(false));
|
||||
/// ```
|
||||
pub fn readonly() -> Self {
|
||||
Self {
|
||||
owner_write: Some(false),
|
||||
group_write: Some(false),
|
||||
other_write: Some(false),
|
||||
|
||||
owner_read: Some(true),
|
||||
group_read: Some(true),
|
||||
other_read: Some(true),
|
||||
|
||||
owner_exec: None,
|
||||
group_exec: None,
|
||||
other_exec: None,
|
||||
}
|
||||
}
|
||||
/// Creates a set of [`Permissions`] that indicate globally writable status.
|
||||
///
|
||||
/// ```
|
||||
/// use distant_core::protocol::Permissions;
|
||||
///
|
||||
/// let permissions = Permissions::writable();
|
||||
/// assert_eq!(permissions.is_readonly(), Some(false));
|
||||
/// assert_eq!(permissions.is_writable(), Some(true));
|
||||
/// ```
|
||||
pub fn writable() -> Self {
|
||||
Self {
|
||||
owner_write: Some(true),
|
||||
group_write: Some(true),
|
||||
other_write: Some(true),
|
||||
|
||||
owner_read: Some(true),
|
||||
group_read: Some(true),
|
||||
other_read: Some(true),
|
||||
|
||||
owner_exec: None,
|
||||
group_exec: None,
|
||||
other_exec: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the permission set has a value specified for each permission (no `None`
|
||||
/// settings).
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.owner_read.is_some()
|
||||
&& self.owner_write.is_some()
|
||||
&& self.owner_exec.is_some()
|
||||
&& self.group_read.is_some()
|
||||
&& self.group_write.is_some()
|
||||
&& self.group_exec.is_some()
|
||||
&& self.other_read.is_some()
|
||||
&& self.other_write.is_some()
|
||||
&& self.other_exec.is_some()
|
||||
}
|
||||
|
||||
/// Returns `true` if permissions represent readonly, `false` if permissions represent
|
||||
/// writable, and `None` if no permissions have been set to indicate either status.
|
||||
#[inline]
|
||||
pub fn is_readonly(&self) -> Option<bool> {
|
||||
// Negate the writable status to indicate whether or not readonly
|
||||
self.is_writable().map(|x| !x)
|
||||
}
|
||||
|
||||
/// Returns `true` if permissions represent ability to write, `false` if permissions represent
|
||||
/// inability to write, and `None` if no permissions have been set to indicate either status.
|
||||
#[inline]
|
||||
pub fn is_writable(&self) -> Option<bool> {
|
||||
self.owner_write
|
||||
.zip(self.group_write)
|
||||
.zip(self.other_write)
|
||||
.map(|((owner, group), other)| owner || group || other)
|
||||
}
|
||||
|
||||
/// Applies `other` settings to `self`, overwriting any of the permissions in `self` with `other`.
|
||||
#[inline]
|
||||
pub fn apply_from(&mut self, other: &Self) {
|
||||
macro_rules! apply {
|
||||
($key:ident) => {{
|
||||
if let Some(value) = other.$key {
|
||||
self.$key = Some(value);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
apply!(owner_read);
|
||||
apply!(owner_write);
|
||||
apply!(owner_exec);
|
||||
apply!(group_read);
|
||||
apply!(group_write);
|
||||
apply!(group_exec);
|
||||
apply!(other_read);
|
||||
apply!(other_write);
|
||||
apply!(other_exec);
|
||||
}
|
||||
|
||||
/// Applies `self` settings to `other`, overwriting any of the permissions in `other` with
|
||||
/// `self`.
|
||||
#[inline]
|
||||
pub fn apply_to(&self, other: &mut Self) {
|
||||
Self::apply_from(other, self)
|
||||
}
|
||||
|
||||
/// Converts a Unix `mode` into the permission set.
|
||||
pub fn from_unix_mode(mode: u32) -> Self {
|
||||
let flags = UnixFilePermissionFlags::from_bits_truncate(mode);
|
||||
Self {
|
||||
owner_read: Some(flags.contains(UnixFilePermissionFlags::OWNER_READ)),
|
||||
owner_write: Some(flags.contains(UnixFilePermissionFlags::OWNER_WRITE)),
|
||||
owner_exec: Some(flags.contains(UnixFilePermissionFlags::OWNER_EXEC)),
|
||||
group_read: Some(flags.contains(UnixFilePermissionFlags::GROUP_READ)),
|
||||
group_write: Some(flags.contains(UnixFilePermissionFlags::GROUP_WRITE)),
|
||||
group_exec: Some(flags.contains(UnixFilePermissionFlags::GROUP_EXEC)),
|
||||
other_read: Some(flags.contains(UnixFilePermissionFlags::OTHER_READ)),
|
||||
other_write: Some(flags.contains(UnixFilePermissionFlags::OTHER_WRITE)),
|
||||
other_exec: Some(flags.contains(UnixFilePermissionFlags::OTHER_EXEC)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts to a Unix `mode` from a permission set. For any missing setting, a 0 bit is used.
|
||||
pub fn to_unix_mode(&self) -> u32 {
|
||||
let mut flags = UnixFilePermissionFlags::empty();
|
||||
|
||||
macro_rules! is_true {
|
||||
($opt:expr) => {{
|
||||
$opt.is_some() && $opt.unwrap()
|
||||
}};
|
||||
}
|
||||
|
||||
if is_true!(self.owner_read) {
|
||||
flags.insert(UnixFilePermissionFlags::OWNER_READ);
|
||||
}
|
||||
if is_true!(self.owner_write) {
|
||||
flags.insert(UnixFilePermissionFlags::OWNER_WRITE);
|
||||
}
|
||||
if is_true!(self.owner_exec) {
|
||||
flags.insert(UnixFilePermissionFlags::OWNER_EXEC);
|
||||
}
|
||||
|
||||
if is_true!(self.group_read) {
|
||||
flags.insert(UnixFilePermissionFlags::GROUP_READ);
|
||||
}
|
||||
if is_true!(self.group_write) {
|
||||
flags.insert(UnixFilePermissionFlags::GROUP_WRITE);
|
||||
}
|
||||
if is_true!(self.group_exec) {
|
||||
flags.insert(UnixFilePermissionFlags::GROUP_EXEC);
|
||||
}
|
||||
|
||||
if is_true!(self.other_read) {
|
||||
flags.insert(UnixFilePermissionFlags::OTHER_READ);
|
||||
}
|
||||
if is_true!(self.other_write) {
|
||||
flags.insert(UnixFilePermissionFlags::OTHER_WRITE);
|
||||
}
|
||||
if is_true!(self.other_exec) {
|
||||
flags.insert(UnixFilePermissionFlags::OTHER_EXEC);
|
||||
}
|
||||
|
||||
flags.bits()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl Permissions {
|
||||
pub fn root_schema() -> schemars::schema::RootSchema {
|
||||
schemars::schema_for!(Permissions)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl From<std::fs::Permissions> for Permissions {
|
||||
/// Converts [`std::fs::Permissions`] into [`Permissions`] using
|
||||
/// [`std::os::unix::fs::PermissionsExt::mode`] to supply the bitset.
|
||||
fn from(permissions: std::fs::Permissions) -> Self {
|
||||
use std::os::unix::prelude::*;
|
||||
Self::from_unix_mode(permissions.mode())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
impl From<std::fs::Permissions> for Permissions {
|
||||
/// Converts [`std::fs::Permissions`] into [`Permissions`] using the `readonly` flag.
|
||||
///
|
||||
/// This will not set executable flags, but will set all read and write flags with write flags
|
||||
/// being `false` if `readonly`, otherwise set to `true`.
|
||||
fn from(permissions: std::fs::Permissions) -> Self {
|
||||
if permissions.readonly() {
|
||||
Self::readonly()
|
||||
} else {
|
||||
Self::writable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl From<Permissions> for std::fs::Permissions {
|
||||
/// Converts [`Permissions`] into [`std::fs::Permissions`] using
|
||||
/// [`std::os::unix::fs::PermissionsExt::from_mode`].
|
||||
fn from(permissions: Permissions) -> Self {
|
||||
use std::os::unix::prelude::*;
|
||||
std::fs::Permissions::from_mode(permissions.to_unix_mode())
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
struct UnixFilePermissionFlags: u32 {
|
||||
const OWNER_READ = 0o400;
|
||||
const OWNER_WRITE = 0o200;
|
||||
const OWNER_EXEC = 0o100;
|
||||
const GROUP_READ = 0o40;
|
||||
const GROUP_WRITE = 0o20;
|
||||
const GROUP_EXEC = 0o10;
|
||||
const OTHER_READ = 0o4;
|
||||
const OTHER_WRITE = 0o2;
|
||||
const OTHER_EXEC = 0o1;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Weak};
|
||||
@ -9,13 +9,15 @@ use async_once_cell::OnceCell;
|
||||
use async_trait::async_trait;
|
||||
use distant_core::net::server::ConnectionCtx;
|
||||
use distant_core::protocol::{
|
||||
Capabilities, CapabilityKind, DirEntry, Environment, FileType, Metadata, ProcessId, PtySize,
|
||||
SystemInfo, UnixMetadata,
|
||||
Capabilities, CapabilityKind, DirEntry, Environment, FileType, Metadata, Permissions,
|
||||
ProcessId, PtySize, SetPermissionsOptions, SystemInfo, UnixMetadata,
|
||||
};
|
||||
use distant_core::{DistantApi, DistantCtx};
|
||||
use log::*;
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use wezterm_ssh::{FilePermissions, OpenFileType, OpenOptions, Session as WezSession, WriteMode};
|
||||
use wezterm_ssh::{
|
||||
FilePermissions, OpenFileType, OpenOptions, Session as WezSession, Utf8PathBuf, WriteMode,
|
||||
};
|
||||
|
||||
use crate::process::{spawn_pty, spawn_simple, SpawnResult};
|
||||
use crate::utils::{self, to_other_error};
|
||||
@ -87,6 +89,9 @@ impl DistantApi for SshDistantApi {
|
||||
capabilities.take(CapabilityKind::Search);
|
||||
capabilities.take(CapabilityKind::CancelSearch);
|
||||
|
||||
// Broken via wezterm-ssh, so not supported right now
|
||||
capabilities.take(CapabilityKind::SetPermissions);
|
||||
|
||||
Ok(capabilities)
|
||||
}
|
||||
|
||||
@ -664,7 +669,7 @@ impl DistantApi for SshDistantApi {
|
||||
// Check that owner, group, or other has write permission (if not, then readonly)
|
||||
readonly: metadata
|
||||
.permissions
|
||||
.map(FilePermissions::is_readonly)
|
||||
.map(|x| !x.owner_write && !x.group_write && !x.other_write)
|
||||
.unwrap_or(true),
|
||||
accessed: metadata.accessed.map(u128::from),
|
||||
modified: metadata.modified.map(u128::from),
|
||||
@ -684,6 +689,136 @@ impl DistantApi for SshDistantApi {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
async fn set_permissions(
|
||||
&self,
|
||||
ctx: DistantCtx<Self::LocalData>,
|
||||
path: PathBuf,
|
||||
permissions: Permissions,
|
||||
options: SetPermissionsOptions,
|
||||
) -> io::Result<()> {
|
||||
debug!(
|
||||
"[Conn {}] Setting permissions for {:?} {{permissions: {:?}, options: {:?}}}",
|
||||
ctx.connection_id, path, permissions, options
|
||||
);
|
||||
|
||||
// Unsupported until issue resolved: https://github.com/wez/wezterm/issues/3784
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"Unsupported until issue resolved: https://github.com/wez/wezterm/issues/3784",
|
||||
));
|
||||
|
||||
let sftp = self.session.sftp();
|
||||
|
||||
macro_rules! set_permissions {
|
||||
($path:ident, $metadata:ident) => {{
|
||||
let mut current = Permissions::from_unix_mode(
|
||||
$metadata
|
||||
.permissions
|
||||
.ok_or_else(|| to_other_error("Unable to read file permissions"))?
|
||||
.to_unix_mode(),
|
||||
);
|
||||
|
||||
current.apply_from(&permissions);
|
||||
|
||||
$metadata.permissions =
|
||||
Some(FilePermissions::from_unix_mode(current.to_unix_mode()));
|
||||
|
||||
println!("set_metadata for {:?}", $path.as_path());
|
||||
sftp.set_metadata($path.as_path(), $metadata)
|
||||
.compat()
|
||||
.await
|
||||
.map_err(to_other_error)?;
|
||||
|
||||
if $metadata.is_dir() {
|
||||
Some($path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}};
|
||||
($path:ident) => {{
|
||||
let mut path = Utf8PathBuf::try_from($path).map_err(to_other_error)?;
|
||||
|
||||
// Query metadata to determine if we are working with a symlink
|
||||
println!("symlink_metadata for {:?}", path);
|
||||
let mut metadata = sftp
|
||||
.symlink_metadata(&path)
|
||||
.compat()
|
||||
.await
|
||||
.map_err(to_other_error)?;
|
||||
|
||||
// If we are excluding symlinks and this is a symlink, then we're done
|
||||
if options.exclude_symlinks && metadata.is_symlink() {
|
||||
None
|
||||
} else {
|
||||
// If we are following symlinks and this is a symlink, then get the real path
|
||||
// and destination metadata
|
||||
if options.follow_symlinks && metadata.is_symlink() {
|
||||
println!("read_link for {:?}", path);
|
||||
path = sftp
|
||||
.read_link(path)
|
||||
.compat()
|
||||
.await
|
||||
.map_err(to_other_error)?;
|
||||
|
||||
println!("metadata for {:?}", path);
|
||||
metadata = sftp
|
||||
.metadata(&path)
|
||||
.compat()
|
||||
.await
|
||||
.map_err(to_other_error)?;
|
||||
}
|
||||
|
||||
set_permissions!(path, metadata)
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
let mut paths = VecDeque::new();
|
||||
|
||||
// Queue up our path if it is a directory
|
||||
if let Some(path) = set_permissions!(path) {
|
||||
paths.push_back(path);
|
||||
}
|
||||
|
||||
if options.recursive {
|
||||
while let Some(path) = paths.pop_front() {
|
||||
println!("read_dir for {:?}", path);
|
||||
let paths_and_metadata =
|
||||
sftp.read_dir(path).compat().await.map_err(to_other_error)?;
|
||||
for (mut path, mut metadata) in paths_and_metadata {
|
||||
if options.exclude_symlinks && metadata.is_symlink() {
|
||||
println!("skipping symlink for {:?}", path);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we are following symlinks, then adjust our path and metadata
|
||||
if options.follow_symlinks && metadata.is_symlink() {
|
||||
println!("read_link for {:?}", path);
|
||||
path = sftp
|
||||
.read_link(path)
|
||||
.compat()
|
||||
.await
|
||||
.map_err(to_other_error)?;
|
||||
|
||||
println!("metadata for {:?}", path);
|
||||
metadata = sftp
|
||||
.metadata(&path)
|
||||
.compat()
|
||||
.await
|
||||
.map_err(to_other_error)?;
|
||||
}
|
||||
|
||||
if let Some(path) = set_permissions!(path, metadata) {
|
||||
paths.push_back(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn proc_spawn(
|
||||
&self,
|
||||
ctx: DistantCtx<Self::LocalData>,
|
||||
|
@ -4,7 +4,9 @@ use std::time::Duration;
|
||||
|
||||
use assert_fs::prelude::*;
|
||||
use assert_fs::TempDir;
|
||||
use distant_core::protocol::{ChangeKindSet, Environment, FileType, Metadata};
|
||||
use distant_core::protocol::{
|
||||
ChangeKindSet, Environment, FileType, Metadata, Permissions, SetPermissionsOptions,
|
||||
};
|
||||
use distant_core::{DistantChannelExt, DistantClient};
|
||||
use once_cell::sync::Lazy;
|
||||
use predicates::prelude::*;
|
||||
@ -1207,6 +1209,478 @@ async fn metadata_should_resolve_file_type_of_symlink_if_flag_specified(
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test(tokio::test)]
|
||||
#[ignore]
|
||||
async fn set_permissions_should_set_readonly_flag_if_specified(
|
||||
#[future] client: Ctx<DistantClient>,
|
||||
) {
|
||||
let mut client = client.await;
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
// Verify that not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(!permissions.readonly(), "File is already set to readonly");
|
||||
|
||||
// Change the file permissions
|
||||
client
|
||||
.set_permissions(
|
||||
file.path().to_path_buf(),
|
||||
Permissions::readonly(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Retrieve permissions to verify set
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(permissions.readonly(), "File not set to readonly");
|
||||
}
|
||||
|
||||
#[allow(unused_attributes)]
|
||||
#[rstest]
|
||||
#[test(tokio::test)]
|
||||
#[cfg_attr(not(unix), ignore)]
|
||||
#[ignore]
|
||||
async fn set_permissions_should_set_unix_permissions_if_on_unix_platform(
|
||||
#[future] client: Ctx<DistantClient>,
|
||||
) {
|
||||
#[allow(unused_mut, unused_variables)]
|
||||
let mut client = client.await;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::prelude::*;
|
||||
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
// Verify that permissions do not match our readonly state
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
let mode = permissions.mode() & 0o777;
|
||||
assert_ne!(mode, 0o400, "File is already set to 0o400");
|
||||
|
||||
// Change the file permissions
|
||||
client
|
||||
.set_permissions(
|
||||
file.path().to_path_buf(),
|
||||
Permissions::from_unix_mode(0o400),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Retrieve file permissions to verify set
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
|
||||
// Drop the upper bits that mode can have (only care about read/write/exec)
|
||||
let mode = permissions.mode() & 0o777;
|
||||
|
||||
assert_eq!(mode, 0o400, "Wrong permissions on file: {:o}", mode);
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_attributes)]
|
||||
#[rstest]
|
||||
#[test(tokio::test)]
|
||||
#[cfg_attr(unix, ignore)]
|
||||
#[ignore]
|
||||
async fn set_permissions_should_set_readonly_flag_if_not_on_unix_platform(
|
||||
#[future] client: Ctx<DistantClient>,
|
||||
) {
|
||||
let mut client = client.await;
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
// Verify that not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(!permissions.readonly(), "File is already set to readonly");
|
||||
|
||||
// Change the file permissions to be readonly (in general)
|
||||
client
|
||||
.set_permissions(
|
||||
file.path().to_path_buf(),
|
||||
Permissions::from_unix_mode(0o400),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// Retrieve file permissions to verify set
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
|
||||
assert!(permissions.readonly(), "File not marked as readonly");
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test(tokio::test)]
|
||||
#[ignore]
|
||||
async fn set_permissions_should_not_recurse_if_option_false(#[future] client: Ctx<DistantClient>) {
|
||||
let mut client = client.await;
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
let symlink = temp.child("link");
|
||||
symlink.symlink_to_file(file.path()).unwrap();
|
||||
|
||||
// Verify that dir is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(temp.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"Temp dir is already set to readonly"
|
||||
);
|
||||
|
||||
// Verify that file is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(!permissions.readonly(), "File is already set to readonly");
|
||||
|
||||
// Verify that symlink is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(symlink.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"Symlink is already set to readonly"
|
||||
);
|
||||
|
||||
// Change the permissions of the directory and not the contents underneath
|
||||
client
|
||||
.set_permissions(
|
||||
temp.path().to_path_buf(),
|
||||
Permissions::readonly(),
|
||||
SetPermissionsOptions {
|
||||
recursive: false,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Retrieve permissions of the file, symlink, and directory to verify set
|
||||
let permissions = tokio::fs::symlink_metadata(temp.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(permissions.readonly(), "Temp directory not set to readonly");
|
||||
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(!permissions.readonly(), "File unexpectedly set to readonly");
|
||||
|
||||
let permissions = tokio::fs::symlink_metadata(symlink.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"Symlink unexpectedly set to readonly"
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test(tokio::test)]
|
||||
#[ignore]
|
||||
async fn set_permissions_should_traverse_symlinks_while_recursing_if_following_symlinks_enabled(
|
||||
#[future] client: Ctx<DistantClient>,
|
||||
) {
|
||||
let mut client = client.await;
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
let temp2 = assert_fs::TempDir::new().unwrap();
|
||||
let file2 = temp2.child("file");
|
||||
file2.write_str("some text").unwrap();
|
||||
|
||||
let symlink = temp.child("link");
|
||||
symlink.symlink_to_dir(temp2.path()).unwrap();
|
||||
|
||||
// Verify that symlink is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(file2.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(!permissions.readonly(), "File2 is already set to readonly");
|
||||
|
||||
// Change the main directory permissions
|
||||
client
|
||||
.set_permissions(
|
||||
temp.path().to_path_buf(),
|
||||
Permissions::readonly(),
|
||||
SetPermissionsOptions {
|
||||
follow_symlinks: true,
|
||||
recursive: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Retrieve permissions referenced by another directory
|
||||
let permissions = tokio::fs::symlink_metadata(file2.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(permissions.readonly(), "File2 not set to readonly");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test(tokio::test)]
|
||||
#[ignore]
|
||||
async fn set_permissions_should_not_traverse_symlinks_while_recursing_if_following_symlinks_disabled(
|
||||
#[future] client: Ctx<DistantClient>,
|
||||
) {
|
||||
let mut client = client.await;
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
let temp2 = assert_fs::TempDir::new().unwrap();
|
||||
let file2 = temp2.child("file");
|
||||
file2.write_str("some text").unwrap();
|
||||
|
||||
let symlink = temp.child("link");
|
||||
symlink.symlink_to_dir(temp2.path()).unwrap();
|
||||
|
||||
// Verify that symlink is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(file2.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(!permissions.readonly(), "File2 is already set to readonly");
|
||||
|
||||
// Change the main directory permissions
|
||||
client
|
||||
.set_permissions(
|
||||
temp.path().to_path_buf(),
|
||||
Permissions::readonly(),
|
||||
SetPermissionsOptions {
|
||||
follow_symlinks: false,
|
||||
recursive: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Retrieve permissions referenced by another directory
|
||||
let permissions = tokio::fs::symlink_metadata(file2.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"File2 unexpectedly set to readonly"
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test(tokio::test)]
|
||||
#[ignore]
|
||||
async fn set_permissions_should_skip_symlinks_if_exclude_symlinks_enabled(
|
||||
#[future] client: Ctx<DistantClient>,
|
||||
) {
|
||||
let mut client = client.await;
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
let symlink = temp.child("link");
|
||||
symlink.symlink_to_file(file.path()).unwrap();
|
||||
|
||||
// Verify that symlink is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(symlink.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"Symlink is already set to readonly"
|
||||
);
|
||||
|
||||
// Change the symlink permissions
|
||||
client
|
||||
.set_permissions(
|
||||
symlink.path().to_path_buf(),
|
||||
Permissions::readonly(),
|
||||
SetPermissionsOptions {
|
||||
exclude_symlinks: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Retrieve permissions to verify not set
|
||||
let permissions = tokio::fs::symlink_metadata(symlink.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"Symlink (or file underneath) set to readonly"
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test(tokio::test)]
|
||||
#[ignore]
|
||||
async fn set_permissions_should_support_recursive_if_option_specified(
|
||||
#[future] client: Ctx<DistantClient>,
|
||||
) {
|
||||
let mut client = client.await;
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
// Verify that dir is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(temp.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"Temp dir is already set to readonly"
|
||||
);
|
||||
|
||||
// Verify that file is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(!permissions.readonly(), "File is already set to readonly");
|
||||
|
||||
// Change the permissions of the file pointed to by the symlink
|
||||
client
|
||||
.set_permissions(
|
||||
temp.path().to_path_buf(),
|
||||
Permissions::readonly(),
|
||||
SetPermissionsOptions {
|
||||
recursive: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Retrieve permissions of the file, symlink, and directory to verify set
|
||||
let permissions = tokio::fs::symlink_metadata(temp.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(permissions.readonly(), "Temp directory not set to readonly");
|
||||
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(permissions.readonly(), "File not set to readonly");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test(tokio::test)]
|
||||
#[ignore]
|
||||
async fn set_permissions_should_support_following_symlinks_if_option_specified(
|
||||
#[future] client: Ctx<DistantClient>,
|
||||
) {
|
||||
let mut client = client.await;
|
||||
let temp = assert_fs::TempDir::new().unwrap();
|
||||
let file = temp.child("file");
|
||||
file.write_str("some text").unwrap();
|
||||
|
||||
let symlink = temp.child("link");
|
||||
symlink.symlink_to_file(file.path()).unwrap();
|
||||
|
||||
// Verify that file is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(!permissions.readonly(), "File is already set to readonly");
|
||||
|
||||
// Verify that symlink is not readonly by default
|
||||
let permissions = tokio::fs::symlink_metadata(symlink.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"Symlink is already set to readonly"
|
||||
);
|
||||
|
||||
// Change the permissions of the file pointed to by the symlink
|
||||
client
|
||||
.set_permissions(
|
||||
symlink.path().to_path_buf(),
|
||||
Permissions::readonly(),
|
||||
SetPermissionsOptions {
|
||||
follow_symlinks: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Retrieve permissions of the file and symlink to verify set
|
||||
let permissions = tokio::fs::symlink_metadata(file.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(permissions.readonly(), "File not set to readonly");
|
||||
|
||||
let permissions = tokio::fs::symlink_metadata(symlink.path())
|
||||
.await
|
||||
.unwrap()
|
||||
.permissions();
|
||||
assert!(
|
||||
!permissions.readonly(),
|
||||
"Symlink unexpectedly set to readonly"
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[test(tokio::test)]
|
||||
async fn proc_spawn_should_not_fail_even_if_process_not_found(
|
||||
|
@ -9,6 +9,9 @@ use crate::sshd::*;
|
||||
async fn detect_family_should_return_windows_if_sshd_on_windows(#[future] ssh: Ctx<Ssh>) {
|
||||
let ssh = ssh.await;
|
||||
let family = ssh.detect_family().await.expect("Failed to detect family");
|
||||
|
||||
// NOTE: We are testing against the local machine, so if Rust was compiled for Windows, then we
|
||||
// are also on a Windows machine remotely for this test!
|
||||
assert_eq!(
|
||||
family,
|
||||
if cfg!(windows) {
|
||||
|
@ -6,7 +6,9 @@ use std::time::Duration;
|
||||
use anyhow::Context;
|
||||
use distant_core::net::common::{ConnectionId, Host, Map, Request, Response};
|
||||
use distant_core::net::manager::ManagerClient;
|
||||
use distant_core::protocol::{self, ChangeKindSet, FileType, SearchQuery, SystemInfo};
|
||||
use distant_core::protocol::{
|
||||
self, ChangeKindSet, FileType, Permissions, SearchQuery, SetPermissionsOptions, SystemInfo,
|
||||
};
|
||||
use distant_core::{DistantChannel, DistantChannelExt, RemoteCommand, Searcher, Watcher};
|
||||
use log::*;
|
||||
use serde_json::json;
|
||||
@ -1007,6 +1009,72 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult {
|
||||
formatter.print(res).context("Failed to print match")?;
|
||||
}
|
||||
}
|
||||
ClientSubcommand::FileSystem(ClientFileSystemSubcommand::SetPermissions {
|
||||
cache,
|
||||
connection,
|
||||
network,
|
||||
follow_symlinks,
|
||||
recursive,
|
||||
mode,
|
||||
path,
|
||||
}) => {
|
||||
debug!("Parsing {mode:?} into a proper set of permissions");
|
||||
let permissions = {
|
||||
if mode.trim().eq_ignore_ascii_case("readonly") {
|
||||
Permissions::readonly()
|
||||
} else if mode.trim().eq_ignore_ascii_case("notreadonly") {
|
||||
Permissions::writable()
|
||||
} else {
|
||||
// Attempt to parse an octal number (chmod absolute), falling back to
|
||||
// parsing the mode string similar to chmod's symbolic mode
|
||||
let mode = match u32::from_str_radix(&mode, 8) {
|
||||
Ok(absolute) => file_mode::Mode::from(absolute),
|
||||
Err(_) => {
|
||||
let mut new_mode = file_mode::Mode::empty();
|
||||
new_mode
|
||||
.set_str(&mode)
|
||||
.context("Failed to parse mode string")?;
|
||||
new_mode
|
||||
}
|
||||
};
|
||||
Permissions::from_unix_mode(mode.mode())
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Connecting to manager");
|
||||
let mut client = Client::new(network)
|
||||
.using_prompt_auth_handler()
|
||||
.connect()
|
||||
.await
|
||||
.context("Failed to connect to manager")?;
|
||||
|
||||
let mut cache = read_cache(&cache).await;
|
||||
let connection_id =
|
||||
use_or_lookup_connection_id(&mut cache, connection, &mut client).await?;
|
||||
|
||||
debug!("Opening channel to connection {}", connection_id);
|
||||
let channel = client
|
||||
.open_raw_channel(connection_id)
|
||||
.await
|
||||
.with_context(|| format!("Failed to open channel to connection {connection_id}"))?;
|
||||
|
||||
let options = SetPermissionsOptions {
|
||||
recursive,
|
||||
follow_symlinks,
|
||||
exclude_symlinks: false,
|
||||
};
|
||||
debug!("Setting permissions for {path:?} as (permissions = {permissions:?}, options = {options:?})");
|
||||
channel
|
||||
.into_client()
|
||||
.into_channel()
|
||||
.set_permissions(path.as_path(), permissions, options)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to set permissions for {path:?} using connection {connection_id}"
|
||||
)
|
||||
})?;
|
||||
}
|
||||
ClientSubcommand::FileSystem(ClientFileSystemSubcommand::Watch {
|
||||
cache,
|
||||
connection,
|
||||
|
@ -121,6 +121,7 @@ impl Options {
|
||||
| ClientFileSystemSubcommand::Remove { network, .. }
|
||||
| ClientFileSystemSubcommand::Rename { network, .. }
|
||||
| ClientFileSystemSubcommand::Search { network, .. }
|
||||
| ClientFileSystemSubcommand::SetPermissions { network, .. }
|
||||
| ClientFileSystemSubcommand::Watch { network, .. }
|
||||
| ClientFileSystemSubcommand::Write { network, .. },
|
||||
) => {
|
||||
@ -740,6 +741,40 @@ pub enum ClientFileSystemSubcommand {
|
||||
paths: Vec<PathBuf>,
|
||||
},
|
||||
|
||||
/// Sets permissions for the specified path on the remote machine
|
||||
SetPermissions {
|
||||
/// Location to store cached data
|
||||
#[clap(
|
||||
long,
|
||||
value_hint = ValueHint::FilePath,
|
||||
value_parser,
|
||||
default_value = CACHE_FILE_PATH_STR.as_str()
|
||||
)]
|
||||
cache: PathBuf,
|
||||
|
||||
/// Specify a connection being managed
|
||||
#[clap(long)]
|
||||
connection: Option<ConnectionId>,
|
||||
|
||||
#[clap(flatten)]
|
||||
network: NetworkSettings,
|
||||
|
||||
/// Recursively set permissions of files/directories/symlinks
|
||||
#[clap(short = 'R', long)]
|
||||
recursive: bool,
|
||||
|
||||
/// Follow symlinks, which means that they will be unaffected
|
||||
#[clap(short = 'L', long)]
|
||||
follow_symlinks: bool,
|
||||
|
||||
/// Mode string following `chmod` format (or set readonly flag if `readonly` or
|
||||
/// `notreadonly` is specified)
|
||||
mode: String,
|
||||
|
||||
/// The path to the file, directory, or symlink on the remote machine
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
/// Watch a path for changes on the remote machine
|
||||
Watch {
|
||||
/// Location to store cached data
|
||||
@ -828,6 +863,7 @@ impl ClientFileSystemSubcommand {
|
||||
Self::Remove { cache, .. } => cache.as_path(),
|
||||
Self::Rename { cache, .. } => cache.as_path(),
|
||||
Self::Search { cache, .. } => cache.as_path(),
|
||||
Self::SetPermissions { cache, .. } => cache.as_path(),
|
||||
Self::Watch { cache, .. } => cache.as_path(),
|
||||
Self::Write { cache, .. } => cache.as_path(),
|
||||
}
|
||||
@ -843,6 +879,7 @@ impl ClientFileSystemSubcommand {
|
||||
Self::Remove { network, .. } => network,
|
||||
Self::Rename { network, .. } => network,
|
||||
Self::Search { network, .. } => network,
|
||||
Self::SetPermissions { network, .. } => network,
|
||||
Self::Watch { network, .. } => network,
|
||||
Self::Write { network, .. } => network,
|
||||
}
|
||||
|
@ -47,6 +47,8 @@ const EXPECTED_TABLE: &str = indoc! {"
|
||||
+------------------+------------------------------------------------------------------+
|
||||
| search | Supports searching filesystem using queries |
|
||||
+------------------+------------------------------------------------------------------+
|
||||
| set_permissions | Supports setting permissions on a file, directory, or symlink |
|
||||
+------------------+------------------------------------------------------------------+
|
||||
| system_info | Supports retrieving system information |
|
||||
+------------------+------------------------------------------------------------------+
|
||||
| unwatch | Supports unwatching filesystem for changes |
|
||||
|
Loading…
Reference in New Issue
Block a user