Implement file I/O commands

pull/38/head
Chip Senkbeil 3 years ago
parent f2cce4aa34
commit a707523fb5
No known key found for this signature in database
GPG Key ID: 35EF1F8EC72A4131

37
Cargo.lock generated

@ -89,6 +89,12 @@ dependencies = [
"vec_map",
]
[[package]]
name = "convert_case"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "ct-codecs"
version = "1.1.1"
@ -101,6 +107,7 @@ version = "0.99.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40eebddd2156ce1bb37b20bbe5151340a31828b1f2d22ba4141f3531710e38df"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"syn",
@ -149,6 +156,7 @@ dependencies = [
"tokio",
"tokio-stream",
"tokio-util",
"walkdir",
"whoami",
]
@ -635,6 +643,15 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
@ -897,6 +914,17 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "walkdir"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [
"same-file",
"winapi",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
@ -993,6 +1021,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"

@ -12,7 +12,7 @@ codegen-units = 1
[dependencies]
bytes = "1.0.1"
derive_more = { version = "0.99.16", default-features = false, features = ["display", "from", "error"] }
derive_more = { version = "0.99.16", default-features = false, features = ["display", "from", "error", "is_variant"] }
directories = "3.0.2"
futures = "0.3.16"
hex = "0.4.3"
@ -26,6 +26,7 @@ strum = { version = "0.21.0", features = ["derive"] }
tokio = { version = "1.9.0", features = ["full"] }
tokio-stream = { version = "0.1.7", features = ["sync"] }
tokio-util = { version = "0.6.7", features = ["codec"] }
walkdir = "2.3.2"
# Binary-specific dependencies
flexi_logger = "0.18.0"

@ -1,3 +1,4 @@
use derive_more::IsVariant;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use structopt::StructOpt;
@ -23,7 +24,7 @@ impl From<RequestPayload> for Request {
}
/// Represents the payload of a request to be performed on the remote machine
#[derive(Clone, Debug, PartialEq, Eq, AsRefStr, StructOpt, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Eq, AsRefStr, IsVariant, StructOpt, Serialize, Deserialize)]
#[serde(
rename_all = "snake_case",
deny_unknown_fields,
@ -39,6 +40,13 @@ pub enum RequestPayload {
path: PathBuf,
},
/// Reads a file from the specified path on the remote machine
/// and treats the contents as text
FileReadText {
/// The path to the file on the remote machine
path: PathBuf,
},
/// Writes a file, creating it if it does not exist, and overwriting any existing content
/// on the remote machine
FileWrite {
@ -89,32 +97,32 @@ pub enum RequestPayload {
all: bool,
},
/// Removes a directory on the remote machine
DirRemove {
/// The path to the directory on the remote machine
/// Removes a file or directory on the remote machine
Remove {
/// The path to the file or directory on the remote machine
path: PathBuf,
/// Whether or not to remove all contents within directory; if false
/// and there are still contents, then the directory is not removed
/// Whether or not to remove all contents within directory if is a directory.
/// Does nothing different for files
#[structopt(short, long)]
all: bool,
force: bool,
},
/// Copies a file/directory on the remote machine
/// Copies a file or directory on the remote machine
Copy {
/// The path to the file/directory on the remote machine
/// The path to the file or directory on the remote machine
src: PathBuf,
/// New location on the remote machine for copy of file/directory
/// New location on the remote machine for copy of file or directory
dst: PathBuf,
},
/// Moves/renames a file or directory on the remote machine
Rename {
/// The path to the file/directory on the remote machine
/// The path to the file or directory on the remote machine
src: PathBuf,
/// New location on the remote machine for the file/directory
/// New location on the remote machine for the file or directory
dst: PathBuf,
},
@ -197,7 +205,7 @@ impl From<ResponsePayload> for Response {
}
/// Represents the payload of a successful response
#[derive(Clone, Debug, PartialEq, Eq, AsRefStr, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Eq, AsRefStr, IsVariant, Serialize, Deserialize)]
#[serde(
rename_all = "snake_case",
deny_unknown_fields,
@ -222,11 +230,10 @@ pub enum ResponsePayload {
data: Vec<u8>,
},
/// Response when some data was written on the remote machine
/// such as a file write or append
Written {
/// Total bytes written
bytes_written: usize,
/// Response containing some arbitrary, text data
Text {
/// Text data associated with the response
data: String,
},
/// Response to reading a directory
@ -294,7 +301,7 @@ pub struct DirEntry {
}
/// Represents the type associated with a dir entry
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, IsVariant, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields, untagged)]
pub enum FileType {
Dir,

@ -1,5 +1,5 @@
use crate::{
data::{Request, Response},
data::{Request, Response, ResponsePayload},
net::{Client, TransportError},
opt::{CommonOpt, ExecuteFormat, ExecuteSubcommand},
utils::{Session, SessionError},
@ -30,7 +30,7 @@ async fn run_async(cmd: ExecuteSubcommand, _opt: CommonOpt) -> Result<(), Error>
let res_string = match cmd.format {
ExecuteFormat::Json => serde_json::to_string(&res)
.map_err(|x| io::Error::new(io::ErrorKind::InvalidData, x))?,
ExecuteFormat::Shell => format!("{:?}", res),
ExecuteFormat::Shell => format_human(res),
};
println!("{}", res_string);
@ -39,3 +39,48 @@ async fn run_async(cmd: ExecuteSubcommand, _opt: CommonOpt) -> Result<(), Error>
Ok(())
}
fn format_human(res: Response) -> String {
match res.payload {
ResponsePayload::Ok => "Done.".to_string(),
ResponsePayload::Error { description } => format!("Failed: '{}'.", description),
ResponsePayload::Blob { data } => String::from_utf8_lossy(&data).to_string(),
ResponsePayload::Text { data } => data,
ResponsePayload::DirEntries { entries } => entries
.into_iter()
.map(|entry| {
format!(
"{}{}",
entry.path.as_os_str().to_string_lossy(),
if entry.file_type.is_dir() {
std::path::MAIN_SEPARATOR.to_string()
} else {
String::new()
},
)
})
.collect::<Vec<String>>()
.join("\n"),
ResponsePayload::ProcList { entries } => entries
.into_iter()
.map(|entry| format!("{}: {} {}", entry.id, entry.cmd, entry.args.join(" ")))
.collect::<Vec<String>>()
.join("\n"),
ResponsePayload::ProcStart { id } => format!("Proc({}): Started.", id),
ResponsePayload::ProcStdout { id, data } => {
format!("Stdout({}): '{}'.", id, String::from_utf8_lossy(&data))
}
ResponsePayload::ProcStderr { id, data } => {
format!("Stderr({}): '{}'.", id, String::from_utf8_lossy(&data))
}
ResponsePayload::ProcDone { id, success, code } => {
if success {
format!("Proc({}): Done.", id)
} else if let Some(code) = code {
format!("Proc({}): Failed with code {}.", id, code)
} else {
format!("Proc({}): Failed.", id)
}
}
}
}

@ -1,5 +1,5 @@
use crate::{
data::{Request, Response, ResponsePayload},
data::{DirEntry, FileType, Request, RequestPayload, Response, ResponsePayload},
net::Transport,
opt::{CommonOpt, ConvertToIpAddrError, ListenSubcommand},
};
@ -8,9 +8,11 @@ use fork::{daemon, Fork};
use log::*;
use orion::aead::SecretKey;
use std::{string::FromUtf8Error, sync::Arc};
use tokio::{io, net::TcpListener};
pub type Result = std::result::Result<(), Error>;
use tokio::{
io::{self, AsyncWriteExt},
net::TcpListener,
};
use walkdir::WalkDir;
#[derive(Debug, Display, Error, From)]
pub enum Error {
@ -20,7 +22,7 @@ pub enum Error {
Utf8Error(FromUtf8Error),
}
pub fn run(cmd: ListenSubcommand, opt: CommonOpt) -> Result {
pub fn run(cmd: ListenSubcommand, opt: CommonOpt) -> Result<(), Error> {
if cmd.daemon {
// NOTE: We keep the stdin, stdout, stderr open so we can print out the pid with the parent
match daemon(false, true) {
@ -44,7 +46,7 @@ pub fn run(cmd: ListenSubcommand, opt: CommonOpt) -> Result {
Ok(())
}
async fn run_async(cmd: ListenSubcommand, _opt: CommonOpt, is_forked: bool) -> Result {
async fn run_async(cmd: ListenSubcommand, _opt: CommonOpt, is_forked: bool) -> Result<(), Error> {
let addr = cmd.host.to_ip_addr(cmd.use_ipv6)?;
let socket_addrs = cmd.port.make_socket_addrs(addr);
@ -98,9 +100,13 @@ async fn run_async(cmd: ListenSubcommand, _opt: CommonOpt, is_forked: bool) -> R
request.payload.as_ref()
);
// Process the request, converting any error into an error response
let response = Response::from_payload_with_origin(
ResponsePayload::Error {
description: String::from("Unimplemented"),
match process_request_payload(request.payload).await {
Ok(payload) => payload,
Err(x) => ResponsePayload::Error {
description: x.to_string(),
},
},
request.id,
);
@ -136,3 +142,149 @@ fn publish_data(port: u16, key: &SecretKey) {
hex::encode(key.unprotected_as_bytes())
);
}
async fn process_request_payload(
payload: RequestPayload,
) -> Result<ResponsePayload, Box<dyn std::error::Error>> {
match payload {
RequestPayload::FileRead { path } => Ok(ResponsePayload::Blob {
data: tokio::fs::read(path).await?,
}),
RequestPayload::FileReadText { path } => Ok(ResponsePayload::Text {
data: tokio::fs::read_to_string(path).await?,
}),
RequestPayload::FileWrite {
path,
input: _,
data,
} => {
tokio::fs::write(path, data).await?;
Ok(ResponsePayload::Ok)
}
RequestPayload::FileAppend {
path,
input: _,
data,
} => {
let mut file = tokio::fs::OpenOptions::new()
.append(true)
.open(path)
.await?;
file.write_all(&data).await?;
Ok(ResponsePayload::Ok)
}
RequestPayload::DirRead { path, all } => {
// Traverse, but don't include root directory in entries (hence min depth 1)
let dir = WalkDir::new(path.as_path()).min_depth(1);
// If all, will recursively traverse, otherwise just return directly from dir
let dir = if all { dir } else { dir.max_depth(1) };
// TODO: Support both returning errors and successfully-traversed entries
// TODO: Support returning full paths instead of always relative?
Ok(ResponsePayload::DirEntries {
entries: dir
.into_iter()
.map(|e| {
e.map(|e| DirEntry {
path: e.path().strip_prefix(path.as_path()).unwrap().to_path_buf(),
file_type: if e.file_type().is_dir() {
FileType::Dir
} else if e.file_type().is_file() {
FileType::File
} else {
FileType::SymLink
},
depth: e.depth(),
})
})
.collect::<Result<Vec<DirEntry>, walkdir::Error>>()?,
})
}
RequestPayload::DirCreate { path, all } => {
if all {
tokio::fs::create_dir_all(path).await?;
} else {
tokio::fs::create_dir(path).await?;
}
Ok(ResponsePayload::Ok)
}
RequestPayload::Remove { path, force } => {
let path_metadata = tokio::fs::metadata(path.as_path()).await?;
if path_metadata.is_dir() {
if force {
tokio::fs::remove_dir_all(path).await?;
} else {
tokio::fs::remove_dir(path).await?;
}
} else {
tokio::fs::remove_file(path).await?;
}
Ok(ResponsePayload::Ok)
}
RequestPayload::Copy { src, dst } => {
let src_metadata = tokio::fs::metadata(src.as_path()).await?;
if src_metadata.is_dir() {
for entry in WalkDir::new(src.as_path())
.min_depth(1)
.follow_links(false)
.into_iter()
.filter_entry(|e| e.file_type().is_file() || e.path_is_symlink())
{
let entry = entry?;
// Get unique portion of path relative to src
// NOTE: Because we are traversing files that are all within src, this
// should always succeed
let local_src = entry.path().strip_prefix(src.as_path()).unwrap();
// Get the file without any directories
let local_src_file_name = local_src.file_name().unwrap();
// Get the directory housing the file
// NOTE: Because we enforce files/symlinks, there will always be a parent
let local_src_dir = local_src.parent().unwrap();
// Map out the path to the destination
let dst_parent_dir = dst.join(local_src_dir);
// Create the destination directory for the file when copying
tokio::fs::create_dir_all(dst_parent_dir.as_path()).await?;
// Perform copying from entry to destination
let dst_file = dst_parent_dir.join(local_src_file_name);
tokio::fs::copy(entry.path(), dst_file).await?;
}
} else {
tokio::fs::copy(src, dst).await?;
}
Ok(ResponsePayload::Ok)
}
RequestPayload::Rename { src, dst } => {
tokio::fs::rename(src, dst).await?;
Ok(ResponsePayload::Ok)
}
RequestPayload::ProcRun { cmd, args, detach } => todo!(),
RequestPayload::ProcConnect { id } => todo!(),
RequestPayload::ProcKill { id } => todo!(),
RequestPayload::ProcStdin { id, data } => todo!(),
RequestPayload::ProcList {} => todo!(),
}
}

Loading…
Cancel
Save