Feat: set permissions support (#184)

pull/191/head
Chip Senkbeil 12 months ago committed by GitHub
parent 137b4dc289
commit ea0424e2f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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 {

@ -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…
Cancel
Save