rust_ipfs/unixfs/
mod.rs

1//! Adaptation for `ipfs-unixfs` crate functionality on top of [`crate::Ipfs`].
2//!
3//! Adding files and directory structures is supported but not exposed via an API. See examples and
4//! `ipfs-http`.
5
6#[cfg(not(target_arch = "wasm32"))]
7use std::path::PathBuf;
8
9use anyhow::Error;
10use bytes::Bytes;
11use futures::{
12    stream::{self, BoxStream},
13    StreamExt,
14};
15use ipld_core::cid::Cid;
16use ll::file::FileReadFailed;
17pub use rust_unixfs as ll;
18
19mod add;
20mod cat;
21mod get;
22mod ls;
23pub use add::UnixfsAdd;
24pub use cat::{StartingPoint, UnixfsCat};
25pub use get::UnixfsGet;
26pub use ls::{Entry, UnixfsLs};
27
28use crate::{
29    dag::{ResolveError, UnexpectedResolved},
30    Ipfs, IpfsPath,
31};
32
33pub struct IpfsUnixfs {
34    ipfs: Ipfs,
35}
36
37pub enum AddOpt {
38    #[cfg(not(target_arch = "wasm32"))]
39    Path(PathBuf),
40    Stream(BoxStream<'static, std::io::Result<Bytes>>),
41    StreamWithName(String, BoxStream<'static, std::io::Result<Bytes>>),
42}
43
44#[cfg(not(target_arch = "wasm32"))]
45impl From<&str> for AddOpt {
46    fn from(value: &str) -> Self {
47        AddOpt::Path(PathBuf::from(value))
48    }
49}
50
51#[cfg(not(target_arch = "wasm32"))]
52impl From<String> for AddOpt {
53    fn from(value: String) -> Self {
54        AddOpt::Path(PathBuf::from(value))
55    }
56}
57
58#[cfg(not(target_arch = "wasm32"))]
59impl From<&std::path::Path> for AddOpt {
60    fn from(path: &std::path::Path) -> Self {
61        AddOpt::Path(path.to_path_buf())
62    }
63}
64
65#[cfg(not(target_arch = "wasm32"))]
66impl From<PathBuf> for AddOpt {
67    fn from(path: PathBuf) -> Self {
68        AddOpt::Path(path)
69    }
70}
71
72impl From<Vec<u8>> for AddOpt {
73    fn from(bytes: Vec<u8>) -> Self {
74        let bytes: Bytes = bytes.into();
75        Self::from(bytes)
76    }
77}
78
79impl From<&'static [u8]> for AddOpt {
80    fn from(bytes: &'static [u8]) -> Self {
81        let bytes: Bytes = bytes.into();
82        Self::from(bytes)
83    }
84}
85
86impl From<(String, Vec<u8>)> for AddOpt {
87    fn from((name, bytes): (String, Vec<u8>)) -> Self {
88        let bytes: Bytes = bytes.into();
89        Self::from((name, bytes))
90    }
91}
92
93impl From<(String, &'static [u8])> for AddOpt {
94    fn from((name, bytes): (String, &'static [u8])) -> Self {
95        let bytes: Bytes = bytes.into();
96        Self::from((name, bytes))
97    }
98}
99
100impl From<Bytes> for AddOpt {
101    fn from(bytes: Bytes) -> Self {
102        let stream = stream::once(async { Ok::<_, std::io::Error>(bytes) }).boxed();
103        AddOpt::Stream(stream)
104    }
105}
106
107impl From<(String, Bytes)> for AddOpt {
108    fn from((name, bytes): (String, Bytes)) -> Self {
109        let stream = stream::once(async { Ok::<_, std::io::Error>(bytes) }).boxed();
110        Self::from((name, stream))
111    }
112}
113
114impl From<BoxStream<'static, std::io::Result<Bytes>>> for AddOpt {
115    fn from(stream: BoxStream<'static, std::io::Result<Bytes>>) -> Self {
116        AddOpt::Stream(stream)
117    }
118}
119
120impl From<(String, BoxStream<'static, std::io::Result<Bytes>>)> for AddOpt {
121    fn from((name, stream): (String, BoxStream<'static, std::io::Result<Bytes>>)) -> Self {
122        AddOpt::StreamWithName(name, stream)
123    }
124}
125
126impl From<BoxStream<'static, std::io::Result<Vec<u8>>>> for AddOpt {
127    fn from(stream: BoxStream<'static, std::io::Result<Vec<u8>>>) -> Self {
128        AddOpt::Stream(stream.map(|result| result.map(|data| data.into())).boxed())
129    }
130}
131
132impl From<(String, BoxStream<'static, std::io::Result<Vec<u8>>>)> for AddOpt {
133    fn from((name, stream): (String, BoxStream<'static, std::io::Result<Vec<u8>>>)) -> Self {
134        let stream = stream.map(|result| result.map(|data| data.into())).boxed();
135        AddOpt::StreamWithName(name, stream)
136    }
137}
138
139impl IpfsUnixfs {
140    pub fn new(ipfs: Ipfs) -> Self {
141        Self { ipfs }
142    }
143
144    /// Creates a stream which will yield the bytes of an UnixFS file from the root Cid, with the
145    /// optional file byte range. If the range is specified and is outside of the file, the stream
146    /// will end without producing any bytes.
147    pub fn cat(&self, starting_point: impl Into<StartingPoint>) -> UnixfsCat {
148        UnixfsCat::with_ipfs(&self.ipfs, starting_point)
149    }
150
151    /// Add a file from either a file or stream
152    pub fn add<I: Into<AddOpt>>(&self, item: I) -> UnixfsAdd {
153        let item = item.into();
154        match item {
155            #[cfg(not(target_arch = "wasm32"))]
156            AddOpt::Path(path) => UnixfsAdd::with_ipfs(&self.ipfs, path),
157            AddOpt::Stream(stream) => UnixfsAdd::with_ipfs(
158                &self.ipfs,
159                add::AddOpt::Stream {
160                    name: None,
161                    total: None,
162                    stream,
163                },
164            ),
165            AddOpt::StreamWithName(name, stream) => UnixfsAdd::with_ipfs(
166                &self.ipfs,
167                add::AddOpt::Stream {
168                    name: Some(name),
169                    total: None,
170                    stream,
171                },
172            ),
173        }
174    }
175
176    /// Retreive a file and saving it to a local path.
177    ///
178    /// To create an owned version of the stream, please use `ipfs::unixfs::get` directly.
179    pub fn get<I: Into<IpfsPath>, P: AsRef<std::path::Path>>(&self, path: I, dest: P) -> UnixfsGet {
180        UnixfsGet::with_ipfs(&self.ipfs, path, dest)
181    }
182
183    /// List directory contents
184    pub fn ls<I: Into<IpfsPath>>(&self, path: I) -> UnixfsLs {
185        UnixfsLs::with_ipfs(&self.ipfs, path)
186    }
187}
188
189#[derive(Debug)]
190pub enum UnixfsStatus {
191    ProgressStatus {
192        written: usize,
193        total_size: Option<usize>,
194    },
195    CompletedStatus {
196        path: IpfsPath,
197        written: usize,
198        total_size: Option<usize>,
199    },
200    FailedStatus {
201        written: usize,
202        total_size: Option<usize>,
203        error: Error,
204    },
205}
206
207/// Types of failures which can occur while walking the UnixFS graph.
208#[derive(Debug, thiserror::Error)]
209pub enum TraversalFailed {
210    /// Failure to resolve the given path; does not happen when given a block.
211    #[error("path resolving failed")]
212    Resolving(#[source] ResolveError),
213
214    /// The given path was resolved to non dag-pb block, does not happen when starting the walk
215    /// from a block.
216    #[error("path resolved to unexpected")]
217    Path(#[source] UnexpectedResolved),
218
219    /// Loading of a block during walk failed
220    #[error("loading of {} failed", .0)]
221    Loading(Cid, #[source] Error),
222
223    #[error("data exceeded max length")]
224    MaxLengthExceeded { size: usize, length: usize },
225    #[error("Timeout while resolving {path}")]
226    Timeout { path: IpfsPath },
227
228    /// Processing of the block failed
229    #[error("walk failed on {}", .0)]
230    Walking(Cid, #[source] FileReadFailed),
231
232    #[error(transparent)]
233    Io(std::io::Error),
234}
235
236#[cfg(test)]
237mod tests {
238    #[test]
239    fn test_file_cid() {
240        // note: old versions of `ipfs::unixfs::File` was an interface where user would provide the
241        // unixfs encoded data. this test case has been migrated to put the "content" as the the
242        // file data instead of the unixfs encoding. the previous way used to produce
243        // QmSy5pnHk1EnvE5dmJSyFKG5unXLGjPpBuJJCBQkBTvBaW.
244        let content = "\u{8}\u{2}\u{12}\u{12}Here is some data\n\u{18}\u{12}";
245
246        let mut adder = rust_unixfs::file::adder::FileAdder::default();
247        let (mut blocks, consumed) = adder.push(content.as_bytes());
248        assert_eq!(consumed, content.len(), "should had consumed all content");
249        assert_eq!(
250            blocks.next(),
251            None,
252            "should not had produced any blocks yet"
253        );
254
255        let mut blocks = adder.finish();
256
257        let (cid, _block) = blocks.next().unwrap();
258        assert_eq!(blocks.next(), None, "should had been the last");
259
260        assert_eq!(
261            "QmQZE72h2Vdm3F5gWr9RLuzSw3rUJEkKedWEa8t8XVygT5",
262            cid.to_string(),
263            "matches cid from go-ipfs 0.6.0"
264        );
265    }
266}