1#![deny(missing_docs)]
2#[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
91pub struct PinataApi {
93 client: Client,
94}
95
96impl PinataApi {
97 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 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 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 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 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 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 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 for entry_result in WalkDir::new(base_path) {
190 let entry = entry_result?;
191 let path = entry.path();
192
193 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 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 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 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 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;