From b18b6c835af523ba8b300392af0d74cd4ad9f7a6 Mon Sep 17 00:00:00 2001 From: Martijn Faassen Date: Fri, 14 Oct 2022 18:46:57 +0200 Subject: [PATCH] feat: CLI get command improvements (#331) This focuses on creating tests for the `iroh get` CLI command. The Iroh API is also improved along the way. This required more work in the underlying API to make it more testable. What's tested here is everything *except* the actual IPFS behavior -- the focus is on the behavior of the CLI and API and its interactions with the filesystem. ## trycmd tests In `iroh/tests/cmd` you can see quite a few `.trycmd` files. These describes the command-line interaction. Stuff behind `$ is a command, there's a special `? failed` indicator if the command is considered to have failed, and the output of the command is shown. New are the `.out` and `.in` directories with the same names as the `.trycmd` files. These describe the filesystem before the command runs (may be missing), and the filesystem after the command has run. This way we can describe the effects of the `iroh get` command - directories and files are supposed to be created. We can also test failure scenarios where we refuse to overwrite a directory that already exists. #269 tracks various test cases. ## `get_stream` The `get` high level CLI method has been removed from the mockable `Api` trait (read on to see where it went). Instead, a more low level but still useful API method `get_stream` has been added to the `Api` trait. This gets a stream of relative paths and `OutType`, describing the directories and files you can create. The big difference with what was there before is that it returns relative paths and doesn't calculate final destination paths -- that's up to the user of the API. ## No `async_trait` macro for `Api` trait The interactions between the `Api` trait, `mockall` macro and `async_trait` were getting so hairy I couldn't figure out how to express things anymore once I wanted to add `get_stream`. For my sanity and also to learn better how this really works underneath, I've rewritten the `Api` trait to describe itself explicitly in terms of `(Local)BoxFuture` and `(Local)_BoxStream`. It's more verbose but functionally equivalent and I could express what I wanted. ## `ApiExt` trait The `ApiExt` trait is a trait that implements the high level `get` command. It puts everything together: it handles various error conditions, accesses the stream and then writes the stream to the filesystem. It's basically `iroh get`. The `ApiExt` trait is solely intended to contain default trait methods, and is automatically available when the `Api` contract is fulfilled (if you `use` it). Factored out `save_get_stream` from `getadd.rs` to be solely concerned with turning a stream into files and directories on the files system. That makes it possible to test its behavior in isolation. ## test fixture The `get` test fixture now mocks `get_stream` and returns a fake stream made from a `Vec`. This defines the actual stuff that the CLI writes to disk. ## relative_path Now depend on the [`relative-path` crate](https://crates.io/crates/relative-path) because what the stream returns are clearly relative paths, and we want to force the user to do something with them before being able to actually write stuff. --- iroh-api/Cargo.toml | 3 + iroh-api/src/api.rs | 123 ++++++++++------- iroh-api/src/api_ext.rs | 124 ++++++++++++++++++ iroh-api/src/getadd.rs | 95 -------------- iroh-api/src/lib.rs | 7 +- iroh-resolver/src/resolver.rs | 24 ++-- iroh/Cargo.toml | 5 +- iroh/src/fixture.rs | 58 +++++++- iroh/src/run.rs | 24 ++-- iroh/tests/cli_tests.rs | 83 +++++++++++- iroh/tests/cmd/get.trycmd | 16 --- .../explicit/README.md | 1 + .../explicit/README.md | 1 + ...irectory_overwrite_explicit_failure.trycmd | 10 ++ .../README.md | 1 + .../README.md | 1 + ...get_cid_directory_overwrite_failure.trycmd | 10 ++ .../explicit/a/exists | 1 + .../explicit/b | 1 + ...et_cid_explicit_output_path_success.trycmd | 8 ++ .../a/exists | 1 + .../b | 1 + iroh/tests/cmd/get_cid_success.trycmd | 8 ++ iroh/tests/cmd/get_failure.trycmd | 31 +++++ .../a/exists | 1 + .../b | 1 + iroh/tests/cmd/get_ipfs_path_success.trycmd | 9 ++ iroh/tests/cmd/get_tail_success.out/b | 1 + iroh/tests/cmd/get_tail_success.trycmd | 8 ++ ...jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 | 1 + iroh/tests/cmd/get_unwrapped_file.trycmd | 7 + ...jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 | 1 + ...jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 | 1 + .../cmd/get_unwrapped_file_overwrite.trycmd | 8 ++ .../file.txt | 1 + iroh/tests/cmd/get_wrapped_file.trycmd | 7 + 36 files changed, 491 insertions(+), 192 deletions(-) create mode 100644 iroh-api/src/api_ext.rs delete mode 100644 iroh-api/src/getadd.rs delete mode 100644 iroh/tests/cmd/get.trycmd create mode 100644 iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.in/explicit/README.md create mode 100644 iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.out/explicit/README.md create mode 100644 iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.trycmd create mode 100644 iroh/tests/cmd/get_cid_directory_overwrite_failure.in/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/README.md create mode 100644 iroh/tests/cmd/get_cid_directory_overwrite_failure.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/README.md create mode 100644 iroh/tests/cmd/get_cid_directory_overwrite_failure.trycmd create mode 100644 iroh/tests/cmd/get_cid_explicit_output_path_success.out/explicit/a/exists create mode 100644 iroh/tests/cmd/get_cid_explicit_output_path_success.out/explicit/b create mode 100644 iroh/tests/cmd/get_cid_explicit_output_path_success.trycmd create mode 100644 iroh/tests/cmd/get_cid_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/a/exists create mode 100644 iroh/tests/cmd/get_cid_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/b create mode 100644 iroh/tests/cmd/get_cid_success.trycmd create mode 100644 iroh/tests/cmd/get_failure.trycmd create mode 100644 iroh/tests/cmd/get_ipfs_path_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/a/exists create mode 100644 iroh/tests/cmd/get_ipfs_path_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/b create mode 100644 iroh/tests/cmd/get_ipfs_path_success.trycmd create mode 100644 iroh/tests/cmd/get_tail_success.out/b create mode 100644 iroh/tests/cmd/get_tail_success.trycmd create mode 100644 iroh/tests/cmd/get_unwrapped_file.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 create mode 100644 iroh/tests/cmd/get_unwrapped_file.trycmd create mode 100644 iroh/tests/cmd/get_unwrapped_file_overwrite.in/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 create mode 100644 iroh/tests/cmd/get_unwrapped_file_overwrite.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 create mode 100644 iroh/tests/cmd/get_unwrapped_file_overwrite.trycmd create mode 100644 iroh/tests/cmd/get_wrapped_file.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/file.txt create mode 100644 iroh/tests/cmd/get_wrapped_file.trycmd diff --git a/iroh-api/Cargo.toml b/iroh-api/Cargo.toml index 361607917e..391d377989 100644 --- a/iroh-api/Cargo.toml +++ b/iroh-api/Cargo.toml @@ -27,4 +27,7 @@ futures = "0.3.21" async-stream = "0.3.3" mockall = { version = "0.11.2", optional = true } serde = { version = "1.0", features = ["derive"] } +relative-path = "1.7.2" +[dev-dependencies] +tempdir = "0.3.7" \ No newline at end of file diff --git a/iroh-api/src/api.rs b/iroh-api/src/api.rs index 64fab98df4..ceee5b68f5 100644 --- a/iroh-api/src/api.rs +++ b/iroh-api/src/api.rs @@ -5,39 +5,55 @@ use crate::config::{Config, CONFIG_FILE_NAME, ENV_PREFIX}; #[cfg(feature = "testing")] use crate::p2p::MockP2p; use crate::p2p::{ClientP2p, P2p}; +use crate::{Cid, IpfsPath}; use anyhow::Result; -use async_trait::async_trait; -use cid::Cid; +use futures::future::{BoxFuture, LocalBoxFuture}; use futures::stream::LocalBoxStream; +use futures::FutureExt; use futures::StreamExt; -use iroh_resolver::resolver::Path as IpfsPath; -use iroh_resolver::{resolver, unixfs_builder}; +use iroh_resolver::unixfs_builder; use iroh_rpc_client::Client; use iroh_rpc_client::StatusTable; use iroh_util::{iroh_config_path, make_config}; #[cfg(feature = "testing")] use mockall::automock; +use relative_path::RelativePathBuf; +use tokio::io::AsyncRead; pub struct Iroh { client: Client, } -pub enum OutType { +pub enum OutType { Dir, - Reader(resolver::OutPrettyReader), + Reader(Box), } +// Note: `#[async_trait]` is deliberately not in use for this trait, because it +// became very hard to express what we wanted once streams were involved. +// Instead we spell things out explicitly without magic. + #[cfg_attr(feature= "testing", automock(type P = MockP2p;))] -#[async_trait(?Send)] pub trait Api { type P: P2p; fn p2p(&self) -> Result; - async fn get<'a>(&self, ipfs_path: &IpfsPath, output: Option<&'a Path>) -> Result<()>; - async fn add(&self, path: &Path, recursive: bool, no_wrap: bool) -> Result; - async fn check(&self) -> StatusTable; - async fn watch<'a>(&self) -> LocalBoxStream<'a, StatusTable>; + /// Produces a asynchronous stream of file descriptions + /// Each description is a tuple of a relative path, and either a `Directory` or a `Reader` + /// with the file contents. + fn get_stream( + &self, + ipfs_path: &IpfsPath, + ) -> LocalBoxStream<'_, Result<(RelativePathBuf, OutType)>>; + fn add<'a>( + &'a self, + path: &'a Path, + recursive: bool, + no_wrap: bool, + ) -> LocalBoxFuture<'_, Result>; + fn check(&self) -> BoxFuture<'_, StatusTable>; + fn watch(&self) -> LocalBoxFuture<'static, LocalBoxStream<'static, StatusTable>>; } impl Iroh { @@ -67,13 +83,8 @@ impl Iroh { fn from_client(client: Client) -> Self { Self { client } } - - pub(crate) fn get_client(&self) -> &Client { - &self.client - } } -#[async_trait(?Send)] impl Api for Iroh { type P = ClientP2p; @@ -82,45 +93,67 @@ impl Api for Iroh { Ok(ClientP2p::new(p2p_client.clone())) } - async fn get<'b>(&self, ipfs_path: &IpfsPath, output: Option<&'b Path>) -> Result<()> { - let blocks = self.get_stream(ipfs_path, output); - tokio::pin!(blocks); - while let Some(block) = blocks.next().await { - let (path, out) = block?; - match out { - OutType::Dir => { - tokio::fs::create_dir_all(path).await?; + fn get_stream( + &self, + ipfs_path: &IpfsPath, + ) -> LocalBoxStream<'_, Result<(RelativePathBuf, OutType)>> { + tracing::debug!("get {:?}", ipfs_path); + let resolver = iroh_resolver::resolver::Resolver::new(self.client.clone()); + let results = resolver.resolve_recursive_with_paths(ipfs_path.clone()); + let sub_path = ipfs_path.to_relative_string(); + async_stream::try_stream! { + tokio::pin!(results); + while let Some(res) = results.next().await { + let (relative_ipfs_path, out) = res?; + let relative_path = RelativePathBuf::from_path(&relative_ipfs_path.to_relative_string())?; + // TODO(faassen) this focusing in on sub-paths should really be handled in the resolver: + // * it can be tested there far more easily than here (where currently it isn't) + // * it makes sense to have an API "what does this resolve to" in the resolver + // * the resolver may have opportunities for optimization we don't have + if !relative_path.starts_with(&sub_path) { + continue; } - OutType::Reader(mut reader) => { - if let Some(parent) = path.parent() { - tokio::fs::create_dir_all(parent).await?; - } - let mut f = tokio::fs::File::create(path).await?; - tokio::io::copy(&mut reader, &mut f).await?; + let relative_path = relative_path.strip_prefix(&sub_path).expect("should be a prefix").to_owned(); + if out.is_dir() { + yield (relative_path, OutType::Dir); + } else { + let reader = out.pretty(resolver.clone(), Default::default())?; + yield (relative_path, OutType::Reader(Box::new(reader))); } } } - Ok(()) + .boxed_local() } - async fn add(&self, path: &Path, recursive: bool, no_wrap: bool) -> Result { - let providing_client = iroh_resolver::unixfs_builder::StoreAndProvideClient { - client: Box::new(self.get_client()), - }; - if path.is_dir() { - unixfs_builder::add_dir(Some(&providing_client), path, !no_wrap, recursive).await - } else if path.is_file() { - unixfs_builder::add_file(Some(&providing_client), path, !no_wrap).await - } else { - anyhow::bail!("can only add files or directories"); + fn add<'a>( + &'a self, + path: &'a Path, + recursive: bool, + no_wrap: bool, + ) -> LocalBoxFuture<'_, Result> { + async move { + let providing_client = iroh_resolver::unixfs_builder::StoreAndProvideClient { + client: Box::new(&self.client), + }; + if path.is_dir() { + unixfs_builder::add_dir(Some(&providing_client), path, !no_wrap, recursive).await + } else if path.is_file() { + unixfs_builder::add_file(Some(&providing_client), path, !no_wrap).await + } else { + anyhow::bail!("can only add files or directories"); + } } + .boxed_local() } - async fn check(&self) -> StatusTable { - self.client.check().await + fn check(&self) -> BoxFuture<'_, StatusTable> { + async { self.client.check().await }.boxed() } - async fn watch<'b>(&self) -> LocalBoxStream<'b, iroh_rpc_client::StatusTable> { - self.client.clone().watch().await.boxed() + fn watch( + &self, + ) -> LocalBoxFuture<'static, LocalBoxStream<'static, iroh_rpc_client::StatusTable>> { + let client = self.client.clone(); + async { client.watch().await.boxed_local() }.boxed_local() } } diff --git a/iroh-api/src/api_ext.rs b/iroh-api/src/api_ext.rs new file mode 100644 index 0000000000..f82b9cc349 --- /dev/null +++ b/iroh-api/src/api_ext.rs @@ -0,0 +1,124 @@ +use std::path::{Path, PathBuf}; + +use crate::{Api, IpfsPath, OutType}; +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use futures::Stream; +use futures::StreamExt; +use relative_path::RelativePathBuf; + +#[async_trait(?Send)] +pub trait ApiExt: Api { + /// High level get, equivalent of CLI `iroh get` + async fn get<'a>( + &self, + ipfs_path: &IpfsPath, + output_path: Option<&'a Path>, + ) -> Result { + if ipfs_path.cid().is_none() { + return Err(anyhow!("IPFS path does not refer to a CID")); + } + let root_path = get_root_path(ipfs_path, output_path); + if root_path.exists() { + return Err(anyhow!( + "output path {} already exists", + root_path.display() + )); + } + let blocks = self.get_stream(ipfs_path); + save_get_stream(&root_path, blocks).await?; + Ok(root_path) + } +} + +impl ApiExt for T where T: Api {} + +/// take a stream of blocks as from `get_stream` and write them to the filesystem +async fn save_get_stream( + root_path: &Path, + blocks: impl Stream>, +) -> Result<()> { + tokio::pin!(blocks); + while let Some(block) = blocks.next().await { + let (path, out) = block?; + let full_path = path.to_path(root_path); + match out { + OutType::Dir => { + tokio::fs::create_dir_all(full_path).await?; + } + OutType::Reader(mut reader) => { + if let Some(parent) = path.parent() { + tokio::fs::create_dir_all(parent.to_path(root_path)).await?; + } + let mut f = tokio::fs::File::create(full_path).await?; + tokio::io::copy(&mut reader, &mut f).await?; + } + } + } + Ok(()) +} + +/// Given an cid and an optional output path, determine root path +fn get_root_path(ipfs_path: &IpfsPath, output_path: Option<&Path>) -> PathBuf { + match output_path { + Some(path) => path.to_path_buf(), + None => { + if ipfs_path.tail().is_empty() { + PathBuf::from(ipfs_path.cid().unwrap().to_string()) + } else { + PathBuf::from(ipfs_path.tail().last().unwrap()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + use tempdir::TempDir; + + #[tokio::test] + async fn test_save_get_stream() { + let stream = Box::pin(futures::stream::iter(vec![ + Ok((RelativePathBuf::from_path("a").unwrap(), OutType::Dir)), + Ok(( + RelativePathBuf::from_path("b").unwrap(), + OutType::Reader(Box::new(std::io::Cursor::new("hello"))), + )), + ])); + let tmp_dir = TempDir::new("test_save_get_stream").unwrap(); + save_get_stream(tmp_dir.path(), stream).await.unwrap(); + assert!(tmp_dir.path().join("a").is_dir()); + assert_eq!( + std::fs::read_to_string(tmp_dir.path().join("b")).unwrap(), + "hello" + ); + } + + #[test] + fn test_get_root_path() { + let ipfs_path = + IpfsPath::from_str("/ipfs/QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N").unwrap(); + assert_eq!( + get_root_path(&ipfs_path, None), + PathBuf::from("QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N") + ); + assert_eq!( + get_root_path(&ipfs_path, Some(Path::new("bar"))), + PathBuf::from("bar") + ); + } + + #[test] + fn test_get_root_path_with_tail() { + let ipfs_path = + IpfsPath::from_str("/ipfs/QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N/tail") + .unwrap(); + assert_eq!(get_root_path(&ipfs_path, None), PathBuf::from("tail")); + assert_eq!( + get_root_path(&ipfs_path, Some(Path::new("bar"))), + PathBuf::from("bar") + ); + } +} diff --git a/iroh-api/src/getadd.rs b/iroh-api/src/getadd.rs deleted file mode 100644 index 1504254bd1..0000000000 --- a/iroh-api/src/getadd.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::path::{Path, PathBuf}; - -use crate::api::{Iroh, OutType}; -use anyhow::{Context, Result}; -use futures::Stream; -use futures::StreamExt; -use iroh_resolver::resolver::Path as IpfsPath; -use iroh_rpc_client::Client; - -impl Iroh { - pub fn get_stream<'b>( - &self, - root: &'b IpfsPath, - output: Option<&'b Path>, - ) -> impl Stream)>> + 'b { - tracing::debug!("get {:?}", root); - let resolver = iroh_resolver::resolver::Resolver::new(self.get_client().clone()); - let results = resolver.resolve_recursive_with_paths(root.clone()); - async_stream::try_stream! { - tokio::pin!(results); - while let Some(res) = results.next().await { - let (path, out) = res?; - let path = Self::make_output_path(path, root.clone(), output.clone())?; - if out.is_dir() { - yield (path, OutType::Dir); - } else { - let reader = out.pretty(resolver.clone(), Default::default())?; - yield (path, OutType::Reader(reader)); - } - } - } - } - - /// Adjusts the full path to replace the root with any given output path - /// if it exists. - fn make_output_path(full: IpfsPath, root: IpfsPath, output: Option<&Path>) -> Result { - if let Some(output) = output { - let root_str = &root.to_string()[..]; - let full_as_path = PathBuf::from(full.to_string()); - let path_str = full_as_path.to_str().context("invalid root path")?; - let output_str = output.to_str().context("invalid output path")?; - Ok(PathBuf::from(path_str.replace(root_str, output_str))) - } else { - // returns path as a string - Ok(PathBuf::from(full.to_string_without_type())) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use iroh_resolver::resolver; - use std::str::FromStr; - - #[test] - fn test_make_output_path() { - // test with output dir - let root = - IpfsPath::from_str("/ipfs/QmYbcW4tXLXHWw753boCK8Y7uxLu5abXjyYizhLznq9PUR").unwrap(); - let full = - IpfsPath::from_str("/ipfs/QmYbcW4tXLXHWw753boCK8Y7uxLu5abXjyYizhLznq9PUR/bar.txt") - .unwrap(); - let output = Some(PathBuf::from("foo")); - let expect = PathBuf::from("foo/bar.txt"); - let got = Iroh::make_output_path(full, root, output.as_deref()).unwrap(); - assert_eq!(expect, got); - - // test with output filepath - let root = resolver::Path::from_str( - "/ipfs/QmYbcW4tXLXHWw753boCK8Y7uxLu5abXjyYizhLznq9PUR/bar.txt", - ) - .unwrap(); - let full = resolver::Path::from_str( - "/ipfs/QmYbcW4tXLXHWw753boCK8Y7uxLu5abXjyYizhLznq9PUR/bar.txt", - ) - .unwrap(); - let output = Some(PathBuf::from("foo/baz.txt")); - let expect = PathBuf::from("foo/baz.txt"); - let got = Iroh::make_output_path(full, root, output.as_deref()).unwrap(); - assert_eq!(expect, got); - - // test no output path - let root = resolver::Path::from_str("/ipfs/QmYbcW4tXLXHWw753boCK8Y7uxLu5abXjyYizhLznq9PUR") - .unwrap(); - let full = resolver::Path::from_str( - "/ipfs/QmYbcW4tXLXHWw753boCK8Y7uxLu5abXjyYizhLznq9PUR/bar.txt", - ) - .unwrap(); - let output = None; - let expect = PathBuf::from("QmYbcW4tXLXHWw753boCK8Y7uxLu5abXjyYizhLznq9PUR/bar.txt"); - let got = Iroh::make_output_path(full, root, output).unwrap(); - assert_eq!(expect, got); - } -} diff --git a/iroh-api/src/lib.rs b/iroh-api/src/lib.rs index 34fd5b912b..d4d2db03bc 100644 --- a/iroh-api/src/lib.rs +++ b/iroh-api/src/lib.rs @@ -1,18 +1,19 @@ mod api; +mod api_ext; mod config; -mod getadd; mod p2p; #[cfg(feature = "testing")] pub use crate::api::MockApi; -pub use crate::api::{Api, Iroh}; +pub use crate::api::{Api, Iroh, OutType}; +pub use crate::api_ext::ApiExt; #[cfg(feature = "testing")] pub use crate::p2p::MockP2p; pub use crate::p2p::P2p as P2pApi; pub use crate::p2p::{Lookup, PeerIdOrAddr}; pub use bytes::Bytes; pub use cid::Cid; -pub use iroh_resolver::resolver::{CidOrDomain, Path as IpfsPath}; +pub use iroh_resolver::resolver::Path as IpfsPath; pub use iroh_rpc_client::{ServiceStatus, StatusRow, StatusTable}; pub use libp2p::gossipsub::MessageId; pub use libp2p::{Multiaddr, PeerId}; diff --git a/iroh-resolver/src/resolver.rs b/iroh-resolver/src/resolver.rs index 5318138dd8..d150432fff 100644 --- a/iroh-resolver/src/resolver.rs +++ b/iroh-resolver/src/resolver.rs @@ -119,18 +119,20 @@ impl Path { self.tail.push(str.as_ref().to_owned()); } - pub fn to_string_without_type(&self) -> String { - let mut s = format!("{}", self.root); - for part in &self.tail { - if part.is_empty() { - continue; - } - s.push_str(&format!("/{}", part)[..]); - } - if self.has_trailing_slash() { - s.push('/'); + // Empty path segments in the *middle* shouldn't occur, + // though they can occur at the end, which `join` handles. + // TODO(faassen): it would make sense to return a `RelativePathBuf` here at some + // point in the future so we don't deal with bare strings anymore and + // we're forced to handle various cases more explicitly. + pub fn to_relative_string(&self) -> String { + self.tail.join("/") + } + + pub fn cid(&self) -> Option<&Cid> { + match &self.root { + CidOrDomain::Cid(cid) => Some(cid), + CidOrDomain::Domain(_) => None, } - s } } diff --git a/iroh/Cargo.toml b/iroh/Cargo.toml index 69dcf07d3e..a5e7da2ebc 100644 --- a/iroh/Cargo.toml +++ b/iroh/Cargo.toml @@ -8,19 +8,20 @@ repository = "https://github.com/n0-computer/iroh" description = "Command line interface for interacting with iroh." [features] -testing = [] +testing = ["dep:relative-path"] [dependencies] anyhow = "1.0" futures = "0.3.21" tokio = { version = "1", features = ["fs", "io-util"] } tracing = "0.1.34" -clap = { version = "4.0.9", features = ["derive"] } +clap = { version = "4.0.15", features = ["derive"] } crossterm = "0.25" tonic = "0.8" git-version = "0.3.5" iroh-metrics = { path = "../iroh-metrics", default-features = false, features = ["rpc-grpc"] } iroh-api = { path = "../iroh-api"} +relative-path = { version = "1.7.2", optional = true } [dev-dependencies] trycmd = "0.13.7" diff --git a/iroh/src/fixture.rs b/iroh/src/fixture.rs index 07e8c814ca..41c4a770f3 100644 --- a/iroh/src/fixture.rs +++ b/iroh/src/fixture.rs @@ -1,7 +1,9 @@ use std::collections::HashMap; use std::env; -use iroh_api::{Lookup, MockApi, MockP2p, PeerId}; +use futures::StreamExt; +use iroh_api::{Lookup, MockApi, MockP2p, OutType, PeerId}; +use relative_path::RelativePathBuf; type GetFixture = fn() -> MockApi; type FixtureRegistry = HashMap; @@ -28,7 +30,51 @@ fn fixture_lookup() -> MockApi { fn fixture_get() -> MockApi { let mut api = MockApi::default(); - api.expect_get().returning(|_, _| Ok(())); + api.expect_get_stream().returning(|_ipfs_path| { + futures::stream::iter(vec![ + Ok((RelativePathBuf::from_path("").unwrap(), OutType::Dir)), + Ok((RelativePathBuf::from_path("a").unwrap(), OutType::Dir)), + // git doesn't like empty directories, nor does trycmd trip if it's missing + // we rely on the unit test for save_get_stream elsewhere to check empty + // directories are created + Ok(( + RelativePathBuf::from_path("a/exists").unwrap(), + OutType::Reader(Box::new(std::io::Cursor::new("exists"))), + )), + Ok(( + RelativePathBuf::from_path("b").unwrap(), + OutType::Reader(Box::new(std::io::Cursor::new("hello"))), + )), + ]) + .boxed_local() + }); + api +} + +fn fixture_get_wrapped_file() -> MockApi { + let mut api = MockApi::default(); + api.expect_get_stream().returning(|_ipfs_path| { + futures::stream::iter(vec![ + Ok((RelativePathBuf::from_path("").unwrap(), OutType::Dir)), + Ok(( + RelativePathBuf::from_path("file.txt").unwrap(), + OutType::Reader(Box::new(std::io::Cursor::new("hello"))), + )), + ]) + .boxed_local() + }); + api +} + +fn fixture_get_unwrapped_file() -> MockApi { + let mut api = MockApi::default(); + api.expect_get_stream().returning(|_ipfs_path| { + futures::stream::iter(vec![Ok(( + RelativePathBuf::from_path("").unwrap(), + OutType::Reader(Box::new(std::io::Cursor::new("hello"))), + ))]) + .boxed_local() + }); api } @@ -36,6 +82,14 @@ fn register_fixtures() -> FixtureRegistry { [ ("lookup".to_string(), fixture_lookup as GetFixture), ("get".to_string(), fixture_get as GetFixture), + ( + "get_wrapped_file".to_string(), + fixture_get_wrapped_file as GetFixture, + ), + ( + "get_unwrapped_file".to_string(), + fixture_get_unwrapped_file as GetFixture, + ), ] .into_iter() .collect() diff --git a/iroh/src/run.rs b/iroh/src/run.rs index 2ba844dbbe..1f7f32cddf 100644 --- a/iroh/src/run.rs +++ b/iroh/src/run.rs @@ -6,7 +6,7 @@ use crate::fixture::get_fixture_api; use crate::p2p::{run_command as run_p2p_command, P2p}; use anyhow::Result; use clap::{Parser, Subcommand}; -use iroh_api::{Api, CidOrDomain, IpfsPath, Iroh}; +use iroh_api::{Api, ApiExt, IpfsPath, Iroh}; use iroh_metrics::config::Config as MetricsConfig; #[derive(Parser, Debug, Clone)] @@ -52,9 +52,9 @@ enum Commands { )] Get { /// CID or CID/with/path/qualifier to get - path: IpfsPath, + ipfs_path: IpfsPath, /// filesystem path to write to. Defaults to CID - output: Option, + output_path: Option, }, } @@ -109,18 +109,12 @@ impl Cli { let cid = api.add(path, *recursive, *no_wrap).await?; println!("/ipfs/{}", cid); } - Commands::Get { path, output } => { - let cid = if let CidOrDomain::Cid(cid) = path.root() { - cid - } else { - return Err(anyhow::anyhow!("ipfs path must refer to a CID")); - }; - api.get(path, output.as_deref()).await?; - let real_output = output - .as_deref() - .map(|path| path.to_path_buf()) - .unwrap_or_else(|| PathBuf::from(&cid.to_string())); - println!("Saving file(s) to {}", real_output.to_str().unwrap()); + Commands::Get { + ipfs_path, + output_path, + } => { + let root_path = api.get(ipfs_path, output_path.as_deref()).await?; + println!("Saving file(s) to {}", root_path.to_str().unwrap()); } }; diff --git a/iroh/tests/cli_tests.rs b/iroh/tests/cli_tests.rs index 9b9077d1c1..8ee84ba651 100644 --- a/iroh/tests/cli_tests.rs +++ b/iroh/tests/cli_tests.rs @@ -1,5 +1,8 @@ +// using globs for trycmd unfortunately leads to some issues +// when `.in` and `.out` directories are in use. So we avoid that + #[tokio::test] -async fn lookup_cli_test() { +async fn lookup_test() { trycmd::TestCases::new() .env("IROH_CTL_FIXTURE", "lookup") .case("tests/cmd/lookup.trycmd") @@ -7,10 +10,84 @@ async fn lookup_cli_test() { } #[tokio::test] -async fn get_cli_test() { +async fn get_success_cid_explicit_output_path_success_test() { + trycmd::TestCases::new() + .env("IROH_CTL_FIXTURE", "get") + .case("tests/cmd/get_cid_explicit_output_path_success.trycmd") + .run(); +} + +#[tokio::test] +async fn get_cid_success_test() { + trycmd::TestCases::new() + .env("IROH_CTL_FIXTURE", "get") + .case("tests/cmd/get_cid_success.trycmd") + .run(); +} + +#[tokio::test] +async fn get_ipfs_path_success_test() { + trycmd::TestCases::new() + .env("IROH_CTL_FIXTURE", "get") + .case("tests/cmd/get_ipfs_path_success.trycmd") + .run(); +} + +#[tokio::test] +async fn get_tail_success_test() { + // we use the get_unwrapped_file fixture because it delivers a file + // which is what the test simulates + trycmd::TestCases::new() + .env("IROH_CTL_FIXTURE", "get_unwrapped_file") + .case("tests/cmd/get_tail_success.trycmd") + .run(); +} + +#[tokio::test] +async fn get_cid_directory_overwrite_expicit_failure_test() { + trycmd::TestCases::new() + .env("IROH_CTL_FIXTURE", "get") + .case("tests/cmd/get_cid_directory_overwrite_explicit_failure.trycmd") + .run(); +} + +#[tokio::test] +async fn get_cid_directory_overwrite_failure_test() { + trycmd::TestCases::new() + .env("IROH_CTL_FIXTURE", "get") + .case("tests/cmd/get_cid_directory_overwrite_failure.trycmd") + .run(); +} + +#[tokio::test] +async fn get_wrapped_file_test() { + trycmd::TestCases::new() + .env("IROH_CTL_FIXTURE", "get_wrapped_file") + .case("tests/cmd/get_wrapped_file.trycmd") + .run(); +} + +#[tokio::test] +async fn get_unwrapped_file_test() { + trycmd::TestCases::new() + .env("IROH_CTL_FIXTURE", "get_unwrapped_file") + .case("tests/cmd/get_unwrapped_file.trycmd") + .run(); +} + +#[tokio::test] +async fn get_unwrapped_file_overwrite_test() { + trycmd::TestCases::new() + .env("IROH_CTL_FIXTURE", "get_unwrapped_file") + .case("tests/cmd/get_unwrapped_file_overwrite.trycmd") + .run(); +} + +#[tokio::test] +async fn get_failure_test() { trycmd::TestCases::new() .env("IROH_CTL_FIXTURE", "get") - .case("tests/cmd/get.trycmd") + .case("tests/cmd/get_failure.trycmd") .run(); } diff --git a/iroh/tests/cmd/get.trycmd b/iroh/tests/cmd/get.trycmd deleted file mode 100644 index dee08b4336..0000000000 --- a/iroh/tests/cmd/get.trycmd +++ /dev/null @@ -1,16 +0,0 @@ -We can get a CID: - -``` -$ iroh get QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 -Saving file(s) to QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 - -``` - -We can also get a ipfs path: - - -``` -$ iroh get /ipfs/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 -Saving file(s) to QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 - -``` diff --git a/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.in/explicit/README.md b/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.in/explicit/README.md new file mode 100644 index 0000000000..79677091f8 --- /dev/null +++ b/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.in/explicit/README.md @@ -0,0 +1 @@ +This is just to have something here. Overwriting this should fail diff --git a/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.out/explicit/README.md b/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.out/explicit/README.md new file mode 100644 index 0000000000..79677091f8 --- /dev/null +++ b/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.out/explicit/README.md @@ -0,0 +1 @@ +This is just to have something here. Overwriting this should fail diff --git a/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.trycmd b/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.trycmd new file mode 100644 index 0000000000..dd1367f701 --- /dev/null +++ b/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.trycmd @@ -0,0 +1,10 @@ + +We can get a CID that represents a directory, but this directory already +exists, so we refuse to write: + +``` +$ iroh get QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 explicit +? failed +Error: output path explicit already exists + +``` diff --git a/iroh/tests/cmd/get_cid_directory_overwrite_failure.in/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/README.md b/iroh/tests/cmd/get_cid_directory_overwrite_failure.in/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/README.md new file mode 100644 index 0000000000..79677091f8 --- /dev/null +++ b/iroh/tests/cmd/get_cid_directory_overwrite_failure.in/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/README.md @@ -0,0 +1 @@ +This is just to have something here. Overwriting this should fail diff --git a/iroh/tests/cmd/get_cid_directory_overwrite_failure.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/README.md b/iroh/tests/cmd/get_cid_directory_overwrite_failure.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/README.md new file mode 100644 index 0000000000..79677091f8 --- /dev/null +++ b/iroh/tests/cmd/get_cid_directory_overwrite_failure.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/README.md @@ -0,0 +1 @@ +This is just to have something here. Overwriting this should fail diff --git a/iroh/tests/cmd/get_cid_directory_overwrite_failure.trycmd b/iroh/tests/cmd/get_cid_directory_overwrite_failure.trycmd new file mode 100644 index 0000000000..0e4ce52485 --- /dev/null +++ b/iroh/tests/cmd/get_cid_directory_overwrite_failure.trycmd @@ -0,0 +1,10 @@ + +We can get a CID that represents a directory, but this directory already +exists, so we refuse to write: + +``` +$ iroh get QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 +? failed +Error: output path QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 already exists + +``` diff --git a/iroh/tests/cmd/get_cid_explicit_output_path_success.out/explicit/a/exists b/iroh/tests/cmd/get_cid_explicit_output_path_success.out/explicit/a/exists new file mode 100644 index 0000000000..01cd34b88d --- /dev/null +++ b/iroh/tests/cmd/get_cid_explicit_output_path_success.out/explicit/a/exists @@ -0,0 +1 @@ +exists \ No newline at end of file diff --git a/iroh/tests/cmd/get_cid_explicit_output_path_success.out/explicit/b b/iroh/tests/cmd/get_cid_explicit_output_path_success.out/explicit/b new file mode 100644 index 0000000000..b6fc4c620b --- /dev/null +++ b/iroh/tests/cmd/get_cid_explicit_output_path_success.out/explicit/b @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/iroh/tests/cmd/get_cid_explicit_output_path_success.trycmd b/iroh/tests/cmd/get_cid_explicit_output_path_success.trycmd new file mode 100644 index 0000000000..5a6ade533f --- /dev/null +++ b/iroh/tests/cmd/get_cid_explicit_output_path_success.trycmd @@ -0,0 +1,8 @@ + +We can get a CID and define an explicit output path: + +``` +$ iroh get QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 explicit +Saving file(s) to explicit + +``` \ No newline at end of file diff --git a/iroh/tests/cmd/get_cid_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/a/exists b/iroh/tests/cmd/get_cid_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/a/exists new file mode 100644 index 0000000000..01cd34b88d --- /dev/null +++ b/iroh/tests/cmd/get_cid_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/a/exists @@ -0,0 +1 @@ +exists \ No newline at end of file diff --git a/iroh/tests/cmd/get_cid_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/b b/iroh/tests/cmd/get_cid_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/b new file mode 100644 index 0000000000..b6fc4c620b --- /dev/null +++ b/iroh/tests/cmd/get_cid_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/b @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/iroh/tests/cmd/get_cid_success.trycmd b/iroh/tests/cmd/get_cid_success.trycmd new file mode 100644 index 0000000000..1d8f71fa80 --- /dev/null +++ b/iroh/tests/cmd/get_cid_success.trycmd @@ -0,0 +1,8 @@ + +We can get a CID: + +``` +$ iroh get QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 +Saving file(s) to QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 + +``` diff --git a/iroh/tests/cmd/get_failure.trycmd b/iroh/tests/cmd/get_failure.trycmd new file mode 100644 index 0000000000..a80d4984f9 --- /dev/null +++ b/iroh/tests/cmd/get_failure.trycmd @@ -0,0 +1,31 @@ +Getting something that's not a proper path to a CID fails with an error: + +``` +$ iroh get /something +? failed +error: Invalid value '/something' for '': invalid cid + +For more information try '--help' + +``` + + +Getting something that's not a CID fails too: + +``` +$ iroh get QmP8jTG1m9GSDJLCbeWhVSVg +? failed +error: Invalid value 'QmP8jTG1m9GSDJLCbeWhVSVg' for '': invalid cid + +For more information try '--help' + +``` + +Getting something that's a proper path but does not include a CID is also an error: + +``` +$ iroh get /ipns/foo +? failed +Error: IPFS path does not refer to a CID + +``` \ No newline at end of file diff --git a/iroh/tests/cmd/get_ipfs_path_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/a/exists b/iroh/tests/cmd/get_ipfs_path_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/a/exists new file mode 100644 index 0000000000..01cd34b88d --- /dev/null +++ b/iroh/tests/cmd/get_ipfs_path_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/a/exists @@ -0,0 +1 @@ +exists \ No newline at end of file diff --git a/iroh/tests/cmd/get_ipfs_path_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/b b/iroh/tests/cmd/get_ipfs_path_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/b new file mode 100644 index 0000000000..b6fc4c620b --- /dev/null +++ b/iroh/tests/cmd/get_ipfs_path_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/b @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/iroh/tests/cmd/get_ipfs_path_success.trycmd b/iroh/tests/cmd/get_ipfs_path_success.trycmd new file mode 100644 index 0000000000..89ff0e16d0 --- /dev/null +++ b/iroh/tests/cmd/get_ipfs_path_success.trycmd @@ -0,0 +1,9 @@ + +We can also get a ipfs path to a CID: + + +``` +$ iroh get /ipfs/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 +Saving file(s) to QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 + +``` \ No newline at end of file diff --git a/iroh/tests/cmd/get_tail_success.out/b b/iroh/tests/cmd/get_tail_success.out/b new file mode 100644 index 0000000000..b6fc4c620b --- /dev/null +++ b/iroh/tests/cmd/get_tail_success.out/b @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/iroh/tests/cmd/get_tail_success.trycmd b/iroh/tests/cmd/get_tail_success.trycmd new file mode 100644 index 0000000000..0b7aefdd40 --- /dev/null +++ b/iroh/tests/cmd/get_tail_success.trycmd @@ -0,0 +1,8 @@ + +We can get a CID indicating a specific path inside: + +``` +$ iroh get /ipfs/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/b +Saving file(s) to b + +``` diff --git a/iroh/tests/cmd/get_unwrapped_file.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 b/iroh/tests/cmd/get_unwrapped_file.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 new file mode 100644 index 0000000000..b6fc4c620b --- /dev/null +++ b/iroh/tests/cmd/get_unwrapped_file.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/iroh/tests/cmd/get_unwrapped_file.trycmd b/iroh/tests/cmd/get_unwrapped_file.trycmd new file mode 100644 index 0000000000..7dd6ed5c9b --- /dev/null +++ b/iroh/tests/cmd/get_unwrapped_file.trycmd @@ -0,0 +1,7 @@ +Get a file that's not wrapped: + +``` +$ iroh get /ipfs/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 +Saving file(s) to QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 + +``` \ No newline at end of file diff --git a/iroh/tests/cmd/get_unwrapped_file_overwrite.in/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 b/iroh/tests/cmd/get_unwrapped_file_overwrite.in/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 new file mode 100644 index 0000000000..5b0cfcb7ab --- /dev/null +++ b/iroh/tests/cmd/get_unwrapped_file_overwrite.in/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 @@ -0,0 +1 @@ +This won't be overwritten \ No newline at end of file diff --git a/iroh/tests/cmd/get_unwrapped_file_overwrite.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 b/iroh/tests/cmd/get_unwrapped_file_overwrite.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 new file mode 100644 index 0000000000..5b0cfcb7ab --- /dev/null +++ b/iroh/tests/cmd/get_unwrapped_file_overwrite.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 @@ -0,0 +1 @@ +This won't be overwritten \ No newline at end of file diff --git a/iroh/tests/cmd/get_unwrapped_file_overwrite.trycmd b/iroh/tests/cmd/get_unwrapped_file_overwrite.trycmd new file mode 100644 index 0000000000..e463d06d5e --- /dev/null +++ b/iroh/tests/cmd/get_unwrapped_file_overwrite.trycmd @@ -0,0 +1,8 @@ +Get a file that's not wrapped. Overwriting an existing file fails: + +``` +$ iroh get /ipfs/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 +? failed +Error: output path QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 already exists + +``` \ No newline at end of file diff --git a/iroh/tests/cmd/get_wrapped_file.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/file.txt b/iroh/tests/cmd/get_wrapped_file.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/file.txt new file mode 100644 index 0000000000..b6fc4c620b --- /dev/null +++ b/iroh/tests/cmd/get_wrapped_file.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/file.txt @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/iroh/tests/cmd/get_wrapped_file.trycmd b/iroh/tests/cmd/get_wrapped_file.trycmd new file mode 100644 index 0000000000..913c97d3f3 --- /dev/null +++ b/iroh/tests/cmd/get_wrapped_file.trycmd @@ -0,0 +1,7 @@ +Get a file wrapped into a directory: + +``` +$ iroh get /ipfs/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 +Saving file(s) to QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 + +``` \ No newline at end of file