ipfs_webdav/
api.rs

1// Copyright 2022-2023 Debox Network
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7//
8
9use std::fmt::{Debug, Formatter};
10use std::path::{Path, PathBuf};
11use std::time::SystemTime;
12
13use async_trait::async_trait;
14use bytes::{Buf, Bytes};
15use futures::TryStreamExt;
16use ipfs_api_backend_hyper::request::{FilesLs, FilesRead, FilesWrite};
17use ipfs_api_backend_hyper::response::{FilesEntry, FilesStatResponse};
18use ipfs_api_backend_hyper::{Error, IpfsApi, IpfsClient, TryFromUri};
19
20/// Trait that defines the interface for interaction with IPFS RPC API.
21#[async_trait]
22pub trait PeerApi: Send + Sync + Debug {
23    /// Add references to IPFS files and directories in MFS (or copy within MFS).
24    async fn cp(&self, path: &str, dest: &str) -> Result<(), Error>;
25
26    /// Flush a given path's data to disk.
27    async fn flush(&self, path: &str) -> Result<(), Error>;
28
29    /// List directories in the local mutable namespace.
30    async fn ls(&self, path: &str) -> Result<Vec<PeerEntry>, Error>;
31
32    /// Make directories.
33    async fn mkdir(&self, path: &str) -> Result<PeerEntry, Error>;
34
35    /// Move files.
36    async fn mv(&self, path: &str, dest: &str) -> Result<(), Error>;
37
38    /// Read a file in a given MFS.
39    async fn read(&self, path: &str, offset: usize, count: usize) -> Result<Bytes, Error>;
40
41    /// Remove a file.
42    async fn rm(&self, path: &str) -> Result<(), Error>;
43
44    /// Display file status.
45    async fn stat(&self, path: &str) -> Result<PeerEntry, Error>;
46
47    /// Write to a mutable file in a given filesystem.
48    async fn write(
49        &self,
50        path: &str,
51        offset: usize,
52        truncate: bool,
53        data: Bytes,
54    ) -> Result<(), Error>;
55}
56
57/// IPFS node MFS (mutable file system) entity representation.
58#[derive(Debug, Clone)]
59pub struct PeerEntry {
60    /// Absolute path of MFS entity.
61    pub path: String,
62
63    /// Time of MFS entity creation.
64    pub crtime: SystemTime,
65
66    /// Time of MFS entity modification.
67    pub mtime: SystemTime,
68
69    /// Whether the entity is a directory.
70    pub is_dir: bool,
71
72    /// Size of MFS entity.
73    pub size: usize,
74}
75
76impl PeerEntry {
77    fn new_dir(path: &str) -> Self {
78        Self {
79            path: path.to_string(),
80            crtime: SystemTime::now(),
81            mtime: SystemTime::now(),
82            is_dir: true,
83            size: 0,
84        }
85    }
86
87    fn from_stat(path: &str, stat: &FilesStatResponse) -> Self {
88        Self {
89            path: path.to_string(),
90            crtime: SystemTime::now(),
91            mtime: SystemTime::now(),
92            is_dir: stat.typ == "directory",
93            size: stat.size as usize,
94        }
95    }
96
97    fn from_entry(path: &str, entry: &FilesEntry) -> Self {
98        Self {
99            path: path.to_string(),
100            crtime: SystemTime::now(),
101            mtime: SystemTime::now(),
102            is_dir: entry.typ == 1,
103            size: entry.size as usize,
104        }
105    }
106}
107
108/// The default implemented API for interfacing with the IPFS RPC API.
109/// This functionality is achieved by implementing the `PeerApi` trait for `BaseApi`.
110///
111/// To change or enhance any of the functionality of interfacing with the IPFS RPC API,
112/// users of `ipfs-webdav` need to implement the `PeerApi` trait for their implementation
113/// of an API that interfaces with the IPFS PRC API.
114pub struct BaseApi {
115    ipfs: IpfsClient,
116}
117
118impl BaseApi {
119    /// Creates default instance of `BaseApi`
120    pub fn new() -> Box<BaseApi> {
121        BaseApi::from_ipfs_client(IpfsClient::default())
122    }
123
124    /// Creates a new instance of `BaseApi` from a provided IPFS API Server URI
125    pub fn from_uri(uri: &str) -> Box<BaseApi> {
126        BaseApi::from_ipfs_client(IpfsClient::from_str(uri).unwrap())
127    }
128
129    /// Creates a new instance of `BaseApi` from provided `IpfsClient`
130    pub fn from_ipfs_client(ipfs: IpfsClient) -> Box<BaseApi> {
131        Box::new(BaseApi { ipfs })
132    }
133}
134
135impl Debug for BaseApi {
136    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
137        write!(f, "BasicApi")
138    }
139}
140
141#[async_trait]
142impl PeerApi for BaseApi {
143    async fn cp(&self, path: &str, dest: &str) -> Result<(), Error> {
144        let path = normalize_path(path);
145        let dest = normalize_path(dest);
146        self.ipfs.files_cp(&path, &dest).await
147    }
148
149    async fn flush(&self, path: &str) -> Result<(), Error> {
150        let path = normalize_path(path);
151        self.ipfs.files_flush(Some(&path)).await
152    }
153
154    async fn ls(&self, path: &str) -> Result<Vec<PeerEntry>, Error> {
155        let path = normalize_path(path);
156        let req = FilesLs {
157            path: Some(&path),
158            long: Some(true),
159            ..Default::default()
160        };
161        let res = self.ipfs.files_ls_with_options(req).await?;
162        Ok(res
163            .entries
164            .iter()
165            .map(|e| PeerEntry::from_entry(&concat_path(&path, &e.name), e))
166            .collect())
167    }
168
169    async fn mkdir(&self, path: &str) -> Result<PeerEntry, Error> {
170        let path = normalize_path(path);
171        self.ipfs.files_mkdir(&path, false).await?;
172        Ok(PeerEntry::new_dir(&path))
173    }
174
175    async fn mv(&self, path: &str, dest: &str) -> Result<(), Error> {
176        let path = normalize_path(path);
177        let dest = normalize_path(dest);
178        self.ipfs.files_mv(&path, &dest).await
179    }
180
181    async fn read(&self, path: &str, offset: usize, count: usize) -> Result<Bytes, Error> {
182        let path = normalize_path(path);
183        let req = FilesRead {
184            path: &path,
185            offset: Some(offset as i64),
186            count: Some(count as i64),
187        };
188        let data = self
189            .ipfs
190            .files_read_with_options(req)
191            .map_ok(|chunk| chunk.to_vec())
192            .try_concat()
193            .await?;
194        Ok(Bytes::copy_from_slice(&data))
195    }
196
197    async fn rm(&self, path: &str) -> Result<(), Error> {
198        let path = normalize_path(path);
199        self.ipfs.files_rm(&path, true).await
200    }
201
202    async fn stat(&self, path: &str) -> Result<PeerEntry, Error> {
203        let path = normalize_path(path);
204        let stat = self.ipfs.files_stat(&path).await?;
205        Ok(PeerEntry::from_stat(&path, &stat))
206    }
207
208    async fn write(
209        &self,
210        path: &str,
211        offset: usize,
212        truncate: bool,
213        data: Bytes,
214    ) -> Result<(), Error> {
215        let path = normalize_path(path);
216        let req = FilesWrite {
217            path: &path,
218            offset: Some(offset as i64),
219            create: Some(true),
220            truncate: Some(truncate),
221            flush: Some(false),
222            ..Default::default()
223        };
224        self.ipfs.files_write_with_options(req, data.reader()).await
225    }
226}
227
228#[inline]
229fn concat_path(p1: &str, p2: &str) -> String {
230    pb_to_string(Path::new(p1).join(Path::new(p2)))
231}
232
233#[inline]
234fn pb_to_string(path: PathBuf) -> String {
235    path.into_os_string().into_string().unwrap()
236}
237
238#[inline]
239fn normalize_path(path: &str) -> String {
240    let mut path = path.to_string();
241    if path.len() > 1 && path.ends_with('/') {
242        path.pop();
243    }
244    path
245}