use std::{
io::Write,
path::{Path, PathBuf},
};
use actyx_sdk::{app_id, service::DirectoryChild, AppManifest, HttpClient};
use asynchronous_codec::{BytesCodec, Framed};
use futures::{
future::{try_join_all, BoxFuture},
FutureExt,
};
use reqwest::{multipart::Part, Body};
use structopt::StructOpt;
use tokio::{fs::File, io::AsyncWriteExt};
use tokio_util::compat::*;
use url::Url;
async fn mk_http_client() -> anyhow::Result<HttpClient> {
let app_manifest = AppManifest::new(
app_id!("com.example.actyx-offsets"),
"Offsets Example".into(),
"0.1.0".into(),
None,
);
let url = Url::parse("http://localhost:4454").unwrap();
HttpClient::new(url, app_manifest).await
}
#[derive(StructOpt)]
struct Opt {
#[structopt(flatten)]
command: Command,
}
#[derive(StructOpt)]
enum Command {
Add {
file: PathBuf,
},
Ls {
name_or_cid: String,
},
Get {
name_or_cid: String,
#[structopt(short, long)]
output: PathBuf,
},
Cat {
name_or_cid: String,
},
}
#[tokio::main]
pub async fn main() -> anyhow::Result<()> {
let opt = Opt::from_args();
let service = mk_http_client().await?;
match opt.command {
Command::Add { file } => {
let files: Box<dyn Iterator<Item = Part>> = if file.is_file() {
Box::new(std::iter::once(mk_part(file).await?))
} else {
Box::new(add_dir(file, "".into()).await?.into_iter())
};
let cid = service.files_post(files).await?;
println!("{}", cid);
}
Command::Ls { name_or_cid } => {
list_file_or_dir(&service, name_or_cid, 0).await?;
}
Command::Get { name_or_cid, output } => {
get_file_or_dir(service, name_or_cid, output.clone()).await?;
println!("Please find your output in {}", output.display());
}
Command::Cat { name_or_cid } => match service.files_get(&name_or_cid).await? {
actyx_sdk::service::FilesGetResponse::File { bytes, .. } => {
std::io::stdout().lock().write_all(&bytes[..])?;
}
actyx_sdk::service::FilesGetResponse::Directory { .. } => {
anyhow::bail!("{} is a directory", name_or_cid);
}
},
}
Ok(())
}
fn list_file_or_dir(client: &HttpClient, name_or_cid: String, level: usize) -> BoxFuture<'_, anyhow::Result<()>> {
async move {
let response = client.files_get(&*name_or_cid).await?;
match response {
actyx_sdk::service::FilesGetResponse::File { name, bytes, mime } if level == 0 => {
println!("{} ({}): {}", name, mime, bytes.len());
}
actyx_sdk::service::FilesGetResponse::Directory { name, cid, children } => {
let indent = level * 4;
if indent == 0 {
println!("{:<34}{:<10}{:<10}", name, 0, cid);
}
for DirectoryChild { cid, name, size } in children {
println!("{:indent$}├── {:<30}{:<10}{:<10}", "", name, size, cid, indent = indent);
list_file_or_dir(client, cid.to_string(), level + 1).await?;
}
}
_ => {}
}
Ok(())
}
.boxed()
}
fn get_file_or_dir(
client: HttpClient,
name_or_cid: String,
write_to: PathBuf,
) -> BoxFuture<'static, anyhow::Result<()>> {
async move {
match client.files_get(&*name_or_cid).await? {
actyx_sdk::service::FilesGetResponse::File { bytes, .. } => {
let mut file = File::create(write_to).await?;
file.write_all(&bytes[..]).await?;
}
actyx_sdk::service::FilesGetResponse::Directory { children, .. } => {
std::fs::create_dir_all(&write_to)?;
let futs = children.into_iter().map(|DirectoryChild { cid, name, .. }| {
get_file_or_dir(client.clone(), cid.to_string(), write_to.join(name))
});
try_join_all(futs).await?;
}
}
Ok(())
}
.boxed()
}
fn add_dir(dir: PathBuf, rel_path: String) -> BoxFuture<'static, anyhow::Result<impl IntoIterator<Item = Part>>> {
async move {
let mut buf = vec![];
for entry in dir.read_dir()? {
let entry = entry?;
if entry.metadata()?.is_dir() {
for c in add_dir(
entry.path(),
format!("{}{}/", rel_path, entry.file_name().to_string_lossy()),
)
.await?
.into_iter()
{
buf.push(c);
}
} else {
buf.push(mk_part(entry.path()).await?.file_name(format!(
"{}{}",
rel_path,
entry.file_name().to_string_lossy()
)));
}
}
Ok(buf)
}
.boxed()
}
async fn mk_part(file: impl AsRef<Path>) -> anyhow::Result<Part> {
let f = file.as_ref();
anyhow::ensure!(f.is_file(), "{} is not a file!", f.display());
let stream = Framed::new(File::open(&f).await?.compat(), BytesCodec);
Ok(
Part::stream_with_length(Body::wrap_stream(stream), f.metadata()?.len()).file_name(
f.file_name()
.expect("File must have a name")
.to_string_lossy()
.to_string(),
),
)
}