Initial, untested implementation

pull/184/head
Chip Senkbeil 1 year ago
parent 137b4dc289
commit 3a91f1ab7b
No known key found for this signature in database
GPG Key ID: 35EF1F8EC72A4131

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)

@ -7,8 +7,8 @@ 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 +411,16 @@ 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<()> {
permissions.write(path, options).await
}
async fn search(
&self,
ctx: DistantCtx<Self::LocalData>,

@ -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,299 @@
use std::cmp;
use std::io;
use std::path::Path;
use bitflags::bitflags;
use ignore::types::TypesBuilder;
use ignore::WalkBuilder;
use serde::{Deserialize, Serialize};
const MAXIMUM_THREADS: usize = 12;
#[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 set the permissions of the file hierarchies rooted in the paths, instead
/// of just the paths themselves
pub recursive: bool,
/// Whether or not to resolve the pathes to the underlying file/directory prior to setting the
/// permissions
pub resolve_symlink: 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.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Permissions {
/// Whether or not the file/directory/symlink is marked as unwriteable
pub readonly: Option<bool>,
/// Represents permissions that are specific to a unix remote machine
pub unix: Option<UnixPermissions>,
}
impl Permissions {
pub async fn read(path: impl AsRef<Path>, resolve_symlink: bool) -> io::Result<Self> {
let std_permissions = Self::read_std_permissions(path, resolve_symlink).await?;
Ok(Self {
readonly: Some(std_permissions.readonly()),
#[cfg(unix)]
unix: Some({
use std::os::unix::prelude::*;
crate::protocol::UnixPermissions::from(std_permissions.mode())
}),
#[cfg(not(unix))]
unix: None,
})
}
/// Sets the permissions for the specified `path`.
///
/// If `resolve_symlink` is true, will resolve the path to the underlying file/directory prior
/// to attempting to set the permissions.
///
/// If `recursive` is true, will apply permissions to all
///
/// When used to set permissions on a file, directory, or symlink, only fields that are set
/// (not `None`) will be applied.
pub async fn write(
&self,
path: impl AsRef<Path>,
options: SetPermissionsOptions,
) -> io::Result<()> {
macro_rules! set_permissions {
($path:expr) => {{
let mut std_permissions =
Self::read_std_permissions($path, options.resolve_symlink).await?;
// Apply the readonly flag if we are provided it
if let Some(readonly) = self.readonly {
std_permissions.set_readonly(readonly);
}
// Update our unix permissions if we were given new permissions by loading
// in the current permissions and applying any changes on top of those
#[cfg(unix)]
if let Some(permissions) = self.unix {
use std::os::unix::prelude::*;
let mut current = UnixPermissions::from(std_permissions.mode());
current.merge(&permissions);
std_permissions.set_mode(current.into());
}
tokio::fs::set_permissions($path, std_permissions).await?;
}};
}
if !options.recursive {
set_permissions!(path.as_ref());
Ok(())
} else {
let walk = WalkBuilder::new(path)
.follow_links(options.resolve_symlink)
.threads(cmp::min(MAXIMUM_THREADS, num_cpus::get()))
.types(
TypesBuilder::new()
.add_defaults()
.build()
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?,
)
.skip_stdout(true)
.build();
for result in walk {
let entry = result.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
set_permissions!(entry.path());
}
Ok(())
}
}
/// Reads [`std::fs::Permissions`] from `path`.
///
/// If `resolve_symlink` is true, will resolve the path to the underlying file/directory prior
/// to attempting to read the permissions.
async fn read_std_permissions(
path: impl AsRef<Path>,
resolve_symlink: bool,
) -> io::Result<std::fs::Permissions> {
Ok(if resolve_symlink {
tokio::fs::metadata(path.as_ref()).await?.permissions()
} else {
tokio::fs::symlink_metadata(path.as_ref())
.await?
.permissions()
})
}
}
#[cfg(feature = "schemars")]
impl Permissions {
pub fn root_schema() -> schemars::schema::RootSchema {
schemars::schema_for!(Permissions)
}
}
/// Represents unix-specific permissions about some path on a remote machine
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct UnixPermissions {
/// 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 UnixPermissions {
/// Merges `other` with `self`, overwriting any of the permissions in `self` with `other`.
pub fn merge(&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);
}
}
#[cfg(feature = "schemars")]
impl UnixPermissions {
pub fn root_schema() -> schemars::schema::RootSchema {
schemars::schema_for!(UnixPermissions)
}
}
impl From<u32> for UnixPermissions {
/// Create from a unix mode bitset
fn from(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)),
}
}
}
impl From<UnixPermissions> for u32 {
/// Convert to a unix mode bitset
fn from(metadata: UnixPermissions) -> Self {
let mut flags = UnixFilePermissionFlags::empty();
macro_rules! is_true {
($opt:expr) => {{
$opt.is_some() && $opt.unwrap()
}};
}
if is_true!(metadata.owner_read) {
flags.insert(UnixFilePermissionFlags::OWNER_READ);
}
if is_true!(metadata.owner_write) {
flags.insert(UnixFilePermissionFlags::OWNER_WRITE);
}
if is_true!(metadata.owner_exec) {
flags.insert(UnixFilePermissionFlags::OWNER_EXEC);
}
if is_true!(metadata.group_read) {
flags.insert(UnixFilePermissionFlags::GROUP_READ);
}
if is_true!(metadata.group_write) {
flags.insert(UnixFilePermissionFlags::GROUP_WRITE);
}
if is_true!(metadata.group_exec) {
flags.insert(UnixFilePermissionFlags::GROUP_EXEC);
}
if is_true!(metadata.other_read) {
flags.insert(UnixFilePermissionFlags::OTHER_READ);
}
if is_true!(metadata.other_write) {
flags.insert(UnixFilePermissionFlags::OTHER_WRITE);
}
if is_true!(metadata.other_exec) {
flags.insert(UnixFilePermissionFlags::OTHER_EXEC);
}
flags.bits()
}
}
impl UnixPermissions {
pub fn is_readonly(self) -> bool {
macro_rules! is_true {
($opt:expr) => {{
$opt.is_some() && $opt.unwrap()
}};
}
!(is_true!(self.owner_read) || is_true!(self.group_read) || is_true!(self.other_read))
}
}
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, UnixPermissions,
};
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};
@ -684,6 +686,102 @@ impl DistantApi for SshDistantApi {
})
}
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
);
let sftp = self.session.sftp();
macro_rules! set_permissions {
($path:expr) => {{
let filename = if options.resolve_symlink {
sftp.read_link($path)
.compat()
.await
.map_err(to_other_error)?
} else {
Utf8PathBuf::try_from($path).map_err(to_other_error)?
};
let mut metadata = sftp
.symlink_metadata(&filename)
.compat()
.await
.map_err(to_other_error)?;
// As is with Rust using `set_readonly`, this will make world-writable if true!
if let Some(readonly) = permissions.readonly {
let mut current = UnixPermissions::from(
metadata
.permissions
.ok_or_else(|| to_other_error("Unable to read file permissions"))?
.to_unix_mode(),
);
current.owner_write = Some(!readonly);
current.group_write = Some(!readonly);
current.other_write = Some(!readonly);
metadata.permissions = Some(FilePermissions::from_unix_mode(current.into()));
}
if let Some(new_permissions) = permissions.unix.as_ref() {
let mut current = UnixPermissions::from(
metadata
.permissions
.ok_or_else(|| to_other_error("Unable to read file permissions"))?
.to_unix_mode(),
);
current.merge(new_permissions);
metadata.permissions = Some(FilePermissions::from_unix_mode(current.into()));
}
sftp.set_metadata(filename.as_path(), metadata)
.compat()
.await
.map_err(to_other_error)?;
if metadata.is_dir() {
Some(filename)
} else {
None
}
}};
}
if options.recursive {
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);
}
while let Some(path) = paths.pop_front() {
let paths_and_metadata =
sftp.read_dir(path).compat().await.map_err(to_other_error)?;
for (path, _) in paths_and_metadata {
if let Some(path) = set_permissions!(path) {
paths.push_back(path);
}
}
}
} else {
let _ = set_permissions!(path);
}
Ok(())
}
async fn proc_spawn(
&self,
ctx: DistantCtx<Self::LocalData>,

@ -6,7 +6,10 @@ 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,
UnixPermissions,
};
use distant_core::{DistantChannel, DistantChannelExt, RemoteCommand, Searcher, Watcher};
use log::*;
use serde_json::json;
@ -1007,6 +1010,74 @@ 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 = {
let readonly = if mode.trim().eq_ignore_ascii_case("readonly") {
Some(true)
} else if mode.trim().eq_ignore_ascii_case("notreadonly") {
Some(false)
} else {
None
};
Permissions {
readonly,
unix: {
if readonly.is_none() {
let mut new_mode = file_mode::Mode::empty();
new_mode
.set_str(&mode)
.context("Failed to parse mode string")?;
Some(UnixPermissions::from(new_mode.mode()))
} else {
None
}
},
}
};
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,
resolve_symlink: follow_symlinks,
};
debug!("Setting permissions for {path:?} as (mode = {mode}, 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,
}

Loading…
Cancel
Save