]> git.proxmox.com Git - cargo.git/blame - crates/crates-io/lib.rs
Merge branch 'master' into doc_versioning
[cargo.git] / crates / crates-io / lib.rs
CommitLineData
638ac111 1#![allow(unknown_lints)]
ec197891 2#![allow(clippy::identity_op)] // used for vertical alignment
6d1d3a68 3
f38c53f5 4use std::collections::BTreeMap;
a9fd1c2c 5use std::fs::File;
a6dad622 6use std::io::prelude::*;
eb081ed3 7use std::io::{Cursor, SeekFrom};
35cb0794 8use std::time::Instant;
9fba127e 9
eb081ed3 10use anyhow::{bail, Context, Result};
d0430dd2 11use curl::easy::{Easy, List};
3c67dc84 12use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
54c42142 13use serde::{Deserialize, Serialize};
6af67890 14use url::Url;
134edb20 15
9fba127e 16pub struct Registry {
70f84bf3 17 /// The base URL for issuing API requests.
9fba127e 18 host: String,
70f84bf3
EH
19 /// Optional authorization token.
20 /// If None, commands requiring authorization will fail.
0c25226b 21 token: Option<String>,
70f84bf3 22 /// Curl handle for issuing requests.
f7d213e7 23 handle: Easy,
9fba127e
AC
24}
25
abe56727 26#[derive(PartialEq, Clone, Copy)]
0c25226b
JB
27pub enum Auth {
28 Authorized,
9fba127e 29 Unauthorized,
f7d213e7
AC
30}
31
a5a298f1 32#[derive(Deserialize)]
0c25226b
JB
33pub struct Crate {
34 pub name: String,
35 pub description: Option<String>,
e95044e3 36 pub max_version: String,
0c25226b
JB
37}
38
a5a298f1 39#[derive(Serialize)]
9fba127e
AC
40pub struct NewCrate {
41 pub name: String,
42 pub vers: String,
43 pub deps: Vec<NewCrateDependency>,
f38c53f5 44 pub features: BTreeMap<String, Vec<String>>,
9fba127e
AC
45 pub authors: Vec<String>,
46 pub description: Option<String>,
47 pub documentation: Option<String>,
48 pub homepage: Option<String>,
49 pub readme: Option<String>,
76a69b4c 50 pub readme_file: Option<String>,
9fba127e 51 pub keywords: Vec<String>,
0f01d9bd 52 pub categories: Vec<String>,
9fba127e 53 pub license: Option<String>,
5acb5f56 54 pub license_file: Option<String>,
9fba127e 55 pub repository: Option<String>,
f38c53f5 56 pub badges: BTreeMap<String, BTreeMap<String, String>>,
70f84bf3 57 pub links: Option<String>,
9fba127e
AC
58}
59
a5a298f1 60#[derive(Serialize)]
9fba127e
AC
61pub struct NewCrateDependency {
62 pub optional: bool,
63 pub default_features: bool,
64 pub name: String,
65 pub features: Vec<String>,
66 pub version_req: String,
67 pub target: Option<String>,
3b5994e7 68 pub kind: String,
e82443df
AC
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub registry: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub explicit_name_in_toml: Option<String>,
9fba127e
AC
73}
74
a5a298f1 75#[derive(Deserialize)]
a3538e25 76pub struct User {
55321111 77 pub id: u32,
a3538e25 78 pub login: String,
fc68cf52 79 pub avatar: Option<String>,
a3538e25
AC
80 pub email: Option<String>,
81 pub name: Option<String>,
82}
83
f697b8c6
CNG
84pub struct Warnings {
85 pub invalid_categories: Vec<String>,
f5f4c417 86 pub invalid_badges: Vec<String>,
e66c413b 87 pub other: Vec<String>,
f697b8c6
CNG
88}
89
1e682848
AC
90#[derive(Deserialize)]
91struct R {
92 ok: bool,
93}
94#[derive(Deserialize)]
95struct OwnerResponse {
96 ok: bool,
97 msg: String,
98}
99#[derive(Deserialize)]
100struct ApiErrorList {
101 errors: Vec<ApiError>,
102}
103#[derive(Deserialize)]
104struct ApiError {
105 detail: String,
106}
107#[derive(Serialize)]
108struct OwnersReq<'a> {
109 users: &'a [&'a str],
110}
111#[derive(Deserialize)]
112struct Users {
113 users: Vec<User>,
114}
115#[derive(Deserialize)]
116struct TotalCrates {
117 total: u32,
118}
119#[derive(Deserialize)]
120struct Crates {
121 crates: Vec<Crate>,
122 meta: TotalCrates,
123}
9fba127e 124impl Registry {
eaa58964
EH
125 /// Creates a new `Registry`.
126 ///
127 /// ## Example
128 ///
129 /// ```rust
130 /// use curl::easy::Easy;
131 /// use crates_io::Registry;
132 ///
133 /// let mut handle = Easy::new();
134 /// // If connecting to crates.io, a user-agent is required.
135 /// handle.useragent("my_crawler (example.com/info)");
136 /// let mut reg = Registry::new_handle(String::from("https://crates.io"), None, handle);
137 /// ```
1e682848 138 pub fn new_handle(host: String, token: Option<String>, handle: Easy) -> Registry {
9fba127e 139 Registry {
0247dc42
E
140 host,
141 token,
142 handle,
9fba127e
AC
143 }
144 }
145
70f84bf3
EH
146 pub fn host(&self) -> &str {
147 &self.host
148 }
149
6af67890 150 pub fn host_is_crates_io(&self) -> bool {
65274ea7 151 is_url_crates_io(&self.host)
6af67890
SG
152 }
153
e53d5965 154 pub fn add_owners(&mut self, krate: &str, owners: &[&str]) -> Result<String> {
a5a298f1 155 let body = serde_json::to_string(&OwnersReq { users: owners })?;
385b54b3 156 let body = self.put(&format!("/crates/{}/owners", krate), body.as_bytes())?;
8150b602
NB
157 assert!(serde_json::from_str::<OwnerResponse>(&body)?.ok);
158 Ok(serde_json::from_str::<OwnerResponse>(&body)?.msg)
9fba127e
AC
159 }
160
161 pub fn remove_owners(&mut self, krate: &str, owners: &[&str]) -> Result<()> {
a5a298f1 162 let body = serde_json::to_string(&OwnersReq { users: owners })?;
385b54b3 163 let body = self.delete(&format!("/crates/{}/owners", krate), Some(body.as_bytes()))?;
8150b602 164 assert!(serde_json::from_str::<OwnerResponse>(&body)?.ok);
9fba127e
AC
165 Ok(())
166 }
167
a3538e25 168 pub fn list_owners(&mut self, krate: &str) -> Result<Vec<User>> {
385b54b3 169 let body = self.get(&format!("/crates/{}/owners", krate))?;
a5a298f1 170 Ok(serde_json::from_str::<Users>(&body)?.users)
a3538e25
AC
171 }
172
eb081ed3 173 pub fn publish(&mut self, krate: &NewCrate, mut tarball: &File) -> Result<Warnings> {
a5a298f1 174 let json = serde_json::to_string(krate)?;
9fba127e
AC
175 // Prepare the body. The format of the upload request is:
176 //
177 // <le u32 of json>
178 // <json request> (metadata for the package)
179 // <le u32 of tarball>
180 // <source tarball>
eb081ed3
EH
181
182 // NOTE: This can be replaced with `stream_len` if it is ever stabilized.
183 //
184 // This checks the length using seeking instead of metadata, because
185 // on some filesystems, getting the metadata will fail because
186 // the file was renamed in ops::package.
187 let tarball_len = tarball
188 .seek(SeekFrom::End(0))
189 .with_context(|| "failed to seek tarball")?;
190 tarball
191 .seek(SeekFrom::Start(0))
192 .with_context(|| "failed to seek tarball")?;
9fba127e 193 let header = {
a6dad622 194 let mut w = Vec::new();
aab416f6 195 w.extend(&(json.len() as u32).to_le_bytes());
51d19d01 196 w.extend(json.as_bytes().iter().cloned());
eb081ed3 197 w.extend(&(tarball_len as u32).to_le_bytes());
a6dad622 198 w
9fba127e 199 };
eb081ed3 200 let size = tarball_len as usize + header.len();
a6dad622 201 let mut body = Cursor::new(header).chain(tarball);
9fba127e
AC
202
203 let url = format!("{}/api/v1/crates/new", self.host);
0c25226b 204
97a2f271
AC
205 let token = match self.token.as_ref() {
206 Some(s) => s,
37cffbe0 207 None => bail!("no upload token found, please run `cargo login`"),
97a2f271 208 };
82655b46
SG
209 self.handle.put(true)?;
210 self.handle.url(&url)?;
211 self.handle.in_filesize(size as u64)?;
f7d213e7 212 let mut headers = List::new();
82655b46
SG
213 headers.append("Accept: application/json")?;
214 headers.append(&format!("Authorization: {}", token))?;
215 self.handle.http_headers(headers)?;
f7d213e7 216
6af67890 217 let body = self.handle(&mut |buf| body.read(buf).unwrap_or(0))?;
f5f4c417 218
51d19d01 219 let response = if body.is_empty() {
a5a298f1 220 "{}".parse()?
51d19d01
KP
221 } else {
222 body.parse::<serde_json::Value>()?
7dd0f932 223 };
f5f4c417 224
e95044e3 225 let invalid_categories: Vec<String> = response
226 .get("warnings")
227 .and_then(|j| j.get("invalid_categories"))
228 .and_then(|j| j.as_array())
229 .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
230 .unwrap_or_else(Vec::new);
231
232 let invalid_badges: Vec<String> = response
233 .get("warnings")
234 .and_then(|j| j.get("invalid_badges"))
235 .and_then(|j| j.as_array())
236 .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
237 .unwrap_or_else(Vec::new);
f5f4c417 238
e66c413b
CNG
239 let other: Vec<String> = response
240 .get("warnings")
241 .and_then(|j| j.get("other"))
242 .and_then(|j| j.as_array())
243 .map(|x| x.iter().flat_map(|j| j.as_str()).map(Into::into).collect())
244 .unwrap_or_else(Vec::new);
245
f5f4c417 246 Ok(Warnings {
0247dc42
E
247 invalid_categories,
248 invalid_badges,
e66c413b 249 other,
f5f4c417 250 })
9fba127e
AC
251 }
252
823f1ff3 253 pub fn search(&mut self, query: &str, limit: u32) -> Result<(Vec<Crate>, u32)> {
3c67dc84 254 let formatted_query = percent_encode(query.as_bytes(), NON_ALPHANUMERIC);
82655b46 255 let body = self.req(
385b54b3 256 &format!("/crates?q={}&per_page={}", formatted_query, limit),
1e682848
AC
257 None,
258 Auth::Unauthorized,
82655b46 259 )?;
0c25226b 260
a5a298f1 261 let crates = serde_json::from_str::<Crates>(&body)?;
134edb20 262 Ok((crates.crates, crates.meta.total))
0c25226b
JB
263 }
264
9fba127e 265 pub fn yank(&mut self, krate: &str, version: &str) -> Result<()> {
385b54b3 266 let body = self.delete(&format!("/crates/{}/{}/yank", krate, version), None)?;
a5a298f1 267 assert!(serde_json::from_str::<R>(&body)?.ok);
9fba127e
AC
268 Ok(())
269 }
270
271 pub fn unyank(&mut self, krate: &str, version: &str) -> Result<()> {
385b54b3 272 let body = self.put(&format!("/crates/{}/{}/unyank", krate, version), &[])?;
a5a298f1 273 assert!(serde_json::from_str::<R>(&body)?.ok);
9fba127e
AC
274 Ok(())
275 }
276
385b54b3 277 fn put(&mut self, path: &str, b: &[u8]) -> Result<String> {
82655b46 278 self.handle.put(true)?;
f7d213e7 279 self.req(path, Some(b), Auth::Authorized)
a3538e25
AC
280 }
281
385b54b3 282 fn get(&mut self, path: &str) -> Result<String> {
82655b46 283 self.handle.get(true)?;
f7d213e7 284 self.req(path, None, Auth::Authorized)
9fba127e
AC
285 }
286
385b54b3 287 fn delete(&mut self, path: &str, b: Option<&[u8]>) -> Result<String> {
82655b46 288 self.handle.custom_request("DELETE")?;
f7d213e7 289 self.req(path, b, Auth::Authorized)
a3538e25
AC
290 }
291
385b54b3 292 fn req(&mut self, path: &str, body: Option<&[u8]>, authorized: Auth) -> Result<String> {
82655b46 293 self.handle.url(&format!("{}/api/v1{}", self.host, path))?;
f7d213e7 294 let mut headers = List::new();
82655b46
SG
295 headers.append("Accept: application/json")?;
296 headers.append("Content-Type: application/json")?;
0c25226b 297
0c25226b 298 if authorized == Auth::Authorized {
97a2f271
AC
299 let token = match self.token.as_ref() {
300 Some(s) => s,
37cffbe0 301 None => bail!("no upload token found, please run `cargo login`"),
97a2f271 302 };
82655b46 303 headers.append(&format!("Authorization: {}", token))?;
0c25226b 304 }
82655b46 305 self.handle.http_headers(headers)?;
a3538e25 306 match body {
f7d213e7 307 Some(mut body) => {
82655b46
SG
308 self.handle.upload(true)?;
309 self.handle.in_filesize(body.len() as u64)?;
6af67890 310 self.handle(&mut |buf| body.read(buf).unwrap_or(0))
f7d213e7 311 }
6af67890 312 None => self.handle(&mut |_| 0),
9fba127e 313 }
9fba127e 314 }
f7d213e7 315
6af67890
SG
316 fn handle(&mut self, read: &mut dyn FnMut(&mut [u8]) -> usize) -> Result<String> {
317 let mut headers = Vec::new();
318 let mut body = Vec::new();
319 let started;
320 {
321 let mut handle = self.handle.transfer();
322 handle.read_function(|buf| Ok(read(buf)))?;
323 handle.write_function(|data| {
324 body.extend_from_slice(data);
325 Ok(data.len())
326 })?;
327 handle.header_function(|data| {
328 headers.push(String::from_utf8_lossy(data).into_owned());
329 true
330 })?;
331 started = Instant::now();
332 handle.perform()?;
35cb0794 333 }
6af67890
SG
334
335 let body = match String::from_utf8(body) {
336 Ok(body) => body,
337 Err(..) => bail!("response body was not valid utf-8"),
338 };
51a56a45
MK
339 let errors = serde_json::from_str::<ApiErrorList>(&body)
340 .ok()
341 .map(|s| s.errors.into_iter().map(|s| s.detail).collect::<Vec<_>>());
6af67890
SG
342
343 match (self.handle.response_code()?, errors) {
51a56a45
MK
344 (0, None) | (200, None) => {}
345 (503, None) if started.elapsed().as_secs() >= 29 && self.host_is_crates_io() => bail!(
346 "Request timed out after 30 seconds. If you're trying to \
347 upload a crate it may be too large. If the crate is under \
348 10MB in size, you can email help@crates.io for assistance."
349 ),
6af67890 350 (code, Some(errors)) => {
0f751def
AC
351 let reason = reason(code);
352 bail!(
353 "api errors (status {} {}): {}",
354 code,
355 reason,
356 errors.join(", ")
357 )
6af67890
SG
358 }
359 (code, None) => bail!(
360 "failed to get a 200 OK response, got {}\n\
361 headers:\n\
362 \t{}\n\
363 body:\n\
364 {}",
51a56a45
MK
365 code,
366 headers.join("\n\t"),
367 body,
6af67890 368 ),
97593400 369 }
9fba127e 370
6af67890
SG
371 Ok(body)
372 }
9fba127e 373}
0f751def
AC
374
375fn reason(code: u32) -> &'static str {
376 // Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
377 match code {
378 100 => "Continue",
379 101 => "Switching Protocol",
380 103 => "Early Hints",
381 200 => "OK",
382 201 => "Created",
383 202 => "Accepted",
384 203 => "Non-Authoritative Information",
385 204 => "No Content",
386 205 => "Reset Content",
387 206 => "Partial Content",
388 300 => "Multiple Choice",
389 301 => "Moved Permanently",
390 302 => "Found",
391 303 => "See Other",
392 304 => "Not Modified",
393 307 => "Temporary Redirect",
394 308 => "Permanent Redirect",
395 400 => "Bad Request",
396 401 => "Unauthorized",
397 402 => "Payment Required",
398 403 => "Forbidden",
399 404 => "Not Found",
400 405 => "Method Not Allowed",
401 406 => "Not Acceptable",
402 407 => "Proxy Authentication Required",
403 408 => "Request Timeout",
404 409 => "Conflict",
405 410 => "Gone",
406 411 => "Length Required",
407 412 => "Precondition Failed",
408 413 => "Payload Too Large",
409 414 => "URI Too Long",
410 415 => "Unsupported Media Type",
411 416 => "Request Range Not Satisfiable",
412 417 => "Expectation Failed",
413 429 => "Too Many Requests",
414 431 => "Request Header Fields Too Large",
415 500 => "Internal Server Error",
416 501 => "Not Implemented",
417 502 => "Bad Gateway",
418 503 => "Service Unavailable",
419 504 => "Gateway Timeout",
420 _ => "<unknown>",
421 }
422}
65274ea7
EH
423
424/// Returns `true` if the host of the given URL is "crates.io".
425pub fn is_url_crates_io(url: &str) -> bool {
426 Url::parse(url)
427 .map(|u| u.host_str() == Some("crates.io"))
428 .unwrap_or(false)
429}