pinata_sdk/
lib.rs

1#![deny(missing_docs)]
2//! ## Initializing the API
3//! ```
4//! use pinata_sdk::PinataApi;
5//! # use pinata_sdk::ApiError;
6//! 
7//! # async fn run() -> Result<(), ApiError> {
8//! let api = PinataApi::new("api_key", "secret_api_key").unwrap();
9//! 
10//! // test that you can connect to the API:
11//! let result = api.test_authentication().await;
12//! if let Ok(_) = result {
13//!   // credentials are correct and other api calls can be made
14//! }
15//! # Ok(())
16//! # }
17//! ```
18//! 
19//! ## Usage
20//! ### 1. Pinning a file
21//! Send a file to pinata for direct pinning to IPFS.
22//! 
23//! ```
24//! use pinata_sdk::{ApiError, PinataApi, PinByFile};
25//! 
26//! # async fn run() -> Result<(), ApiError> {
27//! let api = PinataApi::new("api_key", "secret_api_key").unwrap();
28//! 
29//! let result = api.pin_file(PinByFile::new("file_or_dir_path")).await;
30//! 
31//! if let Ok(pinned_object) = result {
32//!   let hash = pinned_object.ipfs_hash;
33//! }
34//! # Ok(())
35//! # }
36//! ```
37//! 
38//! If a directory path is used to construct `PinByFile`, then `pin_file()` will upload all the contents
39//! of the file to be pinned on pinata.
40//! 
41//! ### 2. Pinning a JSON object
42//! You can send a JSON serializable to pinata for direct pinning to IPFS.
43//! 
44//! ```
45//! use pinata_sdk::{ApiError, PinataApi, PinByJson};
46//! use std::collections::HashMap;
47//! 
48//! # async fn run() -> Result<(), ApiError> {
49//! let api = PinataApi::new("api_key", "secret_api_key").unwrap();
50//! 
51//! // HashMap derives serde::Serialize
52//! let mut json_data = HashMap::new();
53//! json_data.insert("name", "user");
54//! 
55//! let result = api.pin_json(PinByJson::new(json_data)).await;
56//! 
57//! if let Ok(pinned_object) = result {
58//!   let hash = pinned_object.ipfs_hash;
59//! }
60//! # Ok(())
61//! # }
62//! ```
63//! 
64//! ### 3. Unpinning
65//! You can unpin using the `PinataApi::unpin()` function by passing in the CID hash of the already
66//! pinned content.
67//! 
68
69#[cfg_attr(test, macro_use)]
70extern crate log;
71extern crate derive_builder;
72
73use std::fs;
74use std::path::Path;
75use reqwest::{Client, ClientBuilder, header::HeaderMap, multipart::{Form, Part}, Response};
76use walkdir::WalkDir;
77use serde::{Serialize};
78use serde::de::DeserializeOwned;
79use errors::Error;
80use utils::api_url;
81use api::internal::*;
82
83pub use api::data::*;
84pub use api::metadata::*;
85pub use errors::ApiError;
86
87mod api;
88mod utils;
89mod errors;
90
91/// API struct. Exposes functions to interact with the Pinata API
92pub struct PinataApi {
93  client: Client,
94}
95
96impl PinataApi {
97  /// Creates a new instance of PinataApi using the provided keys.
98  /// This function panics if api_key or secret_api_key's are empty/blank
99  pub fn new<S: Into<String>>(api_key: S, secret_api_key: S) -> Result<PinataApi, Error> {
100    let owned_key = api_key.into();
101    let owned_secret = secret_api_key.into();
102
103    utils::validate_keys(&owned_key, &owned_secret)?;
104
105    let mut default_headers = HeaderMap::new();
106    default_headers.insert("pinata_api_key", (&owned_key).parse().unwrap());
107    default_headers.insert("pinata_secret_api_key", (&owned_secret).parse().unwrap());
108
109    let client = ClientBuilder::new()
110      .default_headers(default_headers)
111      .build()?;
112
113    Ok(PinataApi {
114      client,
115    })
116  }
117
118  /// Test if your credentials are corrects. It returns an error if credentials are not correct
119  pub async fn test_authentication(&self) -> Result<(), ApiError> {
120    let response = self.client.get(&api_url("/data/testAuthentication"))
121      .send()
122      .await?;
123
124    self.parse_ok_result(response).await
125  }
126
127  /// Change the pin policy for an individual piece of content.
128  ///
129  /// Changes made via this function only affect the content for the hash passed in. They do not affect a user's account level pin policy.
130  ///
131  /// To read more about pin policies, please check out the [Regions and Replications](https://pinata.cloud/documentation#RegionsAndReplications) documentation
132  pub async fn set_hash_pin_policy(&self, policy: HashPinPolicy) -> Result<(), ApiError> {
133    let response = self.client.put(&api_url("/pinning/hashPinPolicy"))
134      .json(&policy)
135      .send()
136      .await?;
137
138    self.parse_ok_result(response).await
139  }
140
141  /// Add a hash to Pinata for asynchronous pinning.
142  /// 
143  /// Content added through this function is pinned in the background. Fpr this operation to succeed, the 
144  /// content for the hash provided must already be pinned by another node on the IPFS network.
145  pub async fn pin_by_hash(&self, hash: PinByHash) -> Result<PinByHashResult, ApiError> {
146    let response = self.client.post(&api_url("/pinning/pinByHash"))
147      .json(&hash)
148      .send()
149      .await?;
150
151    self.parse_result(response).await
152  }
153
154  /// Retrieve a list of all the pins that are currently in the pin queue for your user
155  pub async fn get_pin_jobs(&self, filters: PinJobsFilter) -> Result<PinJobs, ApiError> {
156    let response = self.client.get(&api_url("/pinning/pinJobs"))
157      .query(&filters)
158      .send()
159      .await?;
160
161    self.parse_result(response).await
162  }
163
164  /// Pin any JSON serializable object to Pinata IPFS nodes.
165  pub async fn pin_json<S>(&self, pin_data: PinByJson<S>) -> Result<PinnedObject, ApiError> 
166    where S: Serialize
167  {
168    let response = self.client.post(&api_url("/pinning/pinJSONToIPFS"))
169      .json(&pin_data)
170      .send()
171      .await?;
172
173    self.parse_result(response).await
174  }
175
176  /// Pin any file or folder to Pinata's IPFS nodes.
177  /// 
178  /// To upload a file use `PinByFile::new("file_path")`. If file_path is a directory, all the content
179  /// of the directory will be uploaded to IPFS and the hash of the parent directory is returned.
180  ///
181  /// If the file cannot be read or directory cannot be read an error will be returned.
182  pub async fn pin_file(&self, pin_data: PinByFile) -> Result<PinnedObject, ApiError> {
183    let mut form = Form::new();
184
185    for file_data in pin_data.files {
186      let base_path = Path::new(&file_data.file_path);
187      if base_path.is_dir() {
188        // recursively read the directory
189        for entry_result in WalkDir::new(base_path) {
190          let entry = entry_result?;
191          let path = entry.path();
192
193          // not interested in reading directory
194          if path.is_dir() { continue }
195
196          let path_name = path.strip_prefix(base_path)?;
197          let part_file_name = format!(
198            "{}/{}", 
199            base_path.file_name().unwrap().to_str().unwrap(),
200            path_name.to_str().unwrap()
201          );
202          
203          let part = Part::bytes(fs::read(path)?)
204            .file_name(part_file_name);
205          form = form.part("file", part);
206        }
207      } else {
208        let file_name = base_path.file_name().unwrap().to_str().unwrap();
209        let part = Part::bytes(fs::read(base_path)?);
210        form = form.part("file", part.file_name(String::from(file_name)));
211      }
212    }
213    
214    if let Some(metadata) = pin_data.pinata_metadata {
215      form = form.text("pinataMetadata", serde_json::to_string(&metadata).unwrap());
216    }
217    
218    if let Some(option) = pin_data.pinata_option {
219      form = form.text("pinataOptions", serde_json::to_string(&option).unwrap());
220    }
221    
222    let response = self.client.post(&api_url("/pinning/pinFileToIPFS"))
223      .multipart(form)
224      .send()
225      .await?;
226
227    self.parse_result(response).await
228  }
229
230  /// Unpin content previously uploaded to the Pinata's IPFS nodes.
231  pub async fn unpin(&self, hash: &str) -> Result<(), ApiError> {
232    let response = self.client.delete(&api_url(&format!("/pinning/unpin/{}", hash)))
233      .send()
234      .await?;
235
236    self.parse_ok_result(response).await
237  }
238
239  /// Change name and custom key values associated for a piece of content stored on Pinata.
240  pub async fn change_hash_metadata(&self, change: ChangePinMetadata) -> Result<(), ApiError> {
241    let response = self.client.put(&api_url("/pinning/hashMetadata"))
242      .json(&change)
243      .send()
244      .await?;
245
246    self.parse_ok_result(response).await
247  }
248
249  /// This endpoint returns the total combined size for all content that you've pinned through Pinata
250  pub async fn get_total_user_pinned_data(&self) ->  Result<TotalPinnedData, ApiError> {
251    let response = self.client.get(&api_url("/data/userPinnedDataTotal"))
252      .send()
253      .await?;
254
255    self.parse_result(response).await
256  }
257
258  /// This returns data on what content the sender has pinned to IPFS from pinata
259  /// 
260  /// The purpose of this endpoint is to provide insight into what is being pinned, and how
261  /// long it has been pinned. The results of this call can be filtered using [PinListFilter](struct.PinListFilter.html).
262  pub async fn get_pin_list(&self, filters: PinListFilter) -> Result<PinList, ApiError> {
263    let response = self.client.get(&api_url("/data/pinList"))
264      .query(&filters)
265      .send()
266      .await?;
267
268    self.parse_result(response).await
269  }
270
271  async fn parse_result<R>(&self, response: Response) -> Result<R, ApiError> 
272    where R: DeserializeOwned
273  {
274    if response.status().is_success() {
275      let result = response.json::<R>().await?;
276      Ok(result)
277    } else {
278      let error = response.json::<PinataApiError>().await?;
279      Err(ApiError::GenericError(error.message()))
280    }
281  }
282
283  async fn parse_ok_result(&self, response: Response) -> Result<(), ApiError> {
284    if response.status().is_success() {
285      Ok(())
286    } else {
287      let error = response.json::<PinataApiError>().await?;
288      Err(ApiError::GenericError(error.message()))
289    }
290  }
291}
292
293#[cfg(test)]
294mod tests;