Skip to main content

rust_ipfs/unixfs/
mod.rs

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