3 use std
::collections
::BTreeMap
;
6 use std
::io
::prelude
::*;
7 use std
::io
::{Cursor, SeekFrom}
;
8 use std
::time
::Instant
;
10 use anyhow
::{bail, format_err, Context, Result}
;
11 use curl
::easy
::{Easy, List}
;
12 use percent_encoding
::{percent_encode, NON_ALPHANUMERIC}
;
13 use serde
::{Deserialize, Serialize}
;
17 /// The base URL for issuing API requests.
19 /// Optional authorization token.
20 /// If None, commands requiring authorization will fail.
21 token
: Option
<String
>,
22 /// Curl handle for issuing requests.
26 #[derive(PartialEq, Clone, Copy)]
32 #[derive(Deserialize)]
35 pub description
: Option
<String
>,
36 pub max_version
: String
,
43 pub deps
: Vec
<NewCrateDependency
>,
44 pub features
: BTreeMap
<String
, Vec
<String
>>,
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
>,
50 pub readme_file
: Option
<String
>,
51 pub keywords
: Vec
<String
>,
52 pub categories
: Vec
<String
>,
53 pub license
: Option
<String
>,
54 pub license_file
: Option
<String
>,
55 pub repository
: Option
<String
>,
56 pub badges
: BTreeMap
<String
, BTreeMap
<String
, String
>>,
57 pub links
: Option
<String
>,
61 pub struct NewCrateDependency
{
63 pub default_features
: bool
,
65 pub features
: Vec
<String
>,
66 pub version_req
: String
,
67 pub target
: Option
<String
>,
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
>,
75 #[derive(Deserialize)]
79 pub avatar
: Option
<String
>,
80 pub email
: Option
<String
>,
81 pub name
: Option
<String
>,
85 pub invalid_categories
: Vec
<String
>,
86 pub invalid_badges
: Vec
<String
>,
87 pub other
: Vec
<String
>,
90 #[derive(Deserialize)]
94 #[derive(Deserialize)]
95 struct OwnerResponse
{
99 #[derive(Deserialize)]
100 struct ApiErrorList
{
101 errors
: Vec
<ApiError
>,
103 #[derive(Deserialize)]
108 struct OwnersReq
<'a
> {
109 users
: &'a
[&'a
str],
111 #[derive(Deserialize)]
115 #[derive(Deserialize)]
119 #[derive(Deserialize)]
126 pub enum ResponseError
{
134 headers
: Vec
<String
>,
137 Other(anyhow
::Error
),
140 impl std
::error
::Error
for ResponseError
{
141 fn source(&self) -> Option
<&(dyn std
::error
::Error
+ '
static)> {
143 ResponseError
::Curl(..) => None
,
144 ResponseError
::Api { .. }
=> None
,
145 ResponseError
::Code { .. }
=> None
,
146 ResponseError
::Other(e
) => Some(e
.as_ref()),
151 impl fmt
::Display
for ResponseError
{
152 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
154 ResponseError
::Curl(e
) => write
!(f
, "{}", e
),
155 ResponseError
::Api { code, errors }
=> {
156 f
.write_str("the remote server responded with an error")?
;
158 write
!(f
, " (status {} {})", code
, reason(*code
))?
;
160 write
!(f
, ": {}", errors
.join(", "))
162 ResponseError
::Code
{
168 "failed to get a 200 OK response, got {}\n\
174 headers
.join("\n\t"),
177 ResponseError
::Other(..) => write
!(f
, "invalid response from server"),
182 impl From
<curl
::Error
> for ResponseError
{
183 fn from(error
: curl
::Error
) -> Self {
184 ResponseError
::Curl(error
)
189 /// Creates a new `Registry`.
194 /// use curl::easy::Easy;
195 /// use crates_io::Registry;
197 /// let mut handle = Easy::new();
198 /// // If connecting to crates.io, a user-agent is required.
199 /// handle.useragent("my_crawler (example.com/info)");
200 /// let mut reg = Registry::new_handle(String::from("https://crates.io"), None, handle);
202 pub fn new_handle(host
: String
, token
: Option
<String
>, handle
: Easy
) -> Registry
{
210 pub fn host(&self) -> &str {
214 pub fn host_is_crates_io(&self) -> bool
{
215 is_url_crates_io(&self.host
)
218 pub fn add_owners(&mut self, krate
: &str, owners
: &[&str]) -> Result
<String
> {
219 let body
= serde_json
::to_string(&OwnersReq { users: owners }
)?
;
220 let body
= self.put(&format
!("/crates/{}/owners", krate
), body
.as_bytes())?
;
221 assert
!(serde_json
::from_str
::<OwnerResponse
>(&body
)?
.ok
);
222 Ok(serde_json
::from_str
::<OwnerResponse
>(&body
)?
.msg
)
225 pub fn remove_owners(&mut self, krate
: &str, owners
: &[&str]) -> Result
<()> {
226 let body
= serde_json
::to_string(&OwnersReq { users: owners }
)?
;
227 let body
= self.delete(&format
!("/crates/{}/owners", krate
), Some(body
.as_bytes()))?
;
228 assert
!(serde_json
::from_str
::<OwnerResponse
>(&body
)?
.ok
);
232 pub fn list_owners(&mut self, krate
: &str) -> Result
<Vec
<User
>> {
233 let body
= self.get(&format
!("/crates/{}/owners", krate
))?
;
234 Ok(serde_json
::from_str
::<Users
>(&body
)?
.users
)
237 pub fn publish(&mut self, krate
: &NewCrate
, mut tarball
: &File
) -> Result
<Warnings
> {
238 let json
= serde_json
::to_string(krate
)?
;
239 // Prepare the body. The format of the upload request is:
242 // <json request> (metadata for the package)
243 // <le u32 of tarball>
246 // NOTE: This can be replaced with `stream_len` if it is ever stabilized.
248 // This checks the length using seeking instead of metadata, because
249 // on some filesystems, getting the metadata will fail because
250 // the file was renamed in ops::package.
251 let tarball_len
= tarball
252 .seek(SeekFrom
::End(0))
253 .with_context(|| "failed to seek tarball")?
;
255 .seek(SeekFrom
::Start(0))
256 .with_context(|| "failed to seek tarball")?
;
258 let mut w
= Vec
::new();
259 w
.extend(&(json
.len() as u32).to_le_bytes());
260 w
.extend(json
.as_bytes().iter().cloned());
261 w
.extend(&(tarball_len
as u32).to_le_bytes());
264 let size
= tarball_len
as usize + header
.len();
265 let mut body
= Cursor
::new(header
).chain(tarball
);
267 let url
= format
!("{}/api/v1/crates/new", self.host
);
269 let token
= match self.token
.as_ref() {
271 None
=> bail
!("no upload token found, please run `cargo login`"),
273 self.handle
.put(true)?
;
274 self.handle
.url(&url
)?
;
275 self.handle
.in_filesize(size
as u64)?
;
276 let mut headers
= List
::new();
277 headers
.append("Accept: application/json")?
;
278 headers
.append(&format
!("Authorization: {}", token
))?
;
279 self.handle
.http_headers(headers
)?
;
281 let started
= Instant
::now();
283 .handle(&mut |buf
| body
.read(buf
).unwrap_or(0))
284 .map_err(|e
| match e
{
285 ResponseError
::Code { code, .. }
287 && started
.elapsed().as_secs() >= 29
288 && self.host_is_crates_io() =>
291 "Request timed out after 30 seconds. If you're trying to \
292 upload a crate it may be too large. If the crate is under \
293 10MB in size, you can email help@crates.io for assistance.\n\
301 let response
= if body
.is_empty() {
304 body
.parse
::<serde_json
::Value
>()?
307 let invalid_categories
: Vec
<String
> = response
309 .and_then(|j
| j
.get("invalid_categories"))
310 .and_then(|j
| j
.as_array())
311 .map(|x
| x
.iter().flat_map(|j
| j
.as_str()).map(Into
::into
).collect())
312 .unwrap_or_else(Vec
::new
);
314 let invalid_badges
: Vec
<String
> = response
316 .and_then(|j
| j
.get("invalid_badges"))
317 .and_then(|j
| j
.as_array())
318 .map(|x
| x
.iter().flat_map(|j
| j
.as_str()).map(Into
::into
).collect())
319 .unwrap_or_else(Vec
::new
);
321 let other
: Vec
<String
> = response
323 .and_then(|j
| j
.get("other"))
324 .and_then(|j
| j
.as_array())
325 .map(|x
| x
.iter().flat_map(|j
| j
.as_str()).map(Into
::into
).collect())
326 .unwrap_or_else(Vec
::new
);
335 pub fn search(&mut self, query
: &str, limit
: u32) -> Result
<(Vec
<Crate
>, u32)> {
336 let formatted_query
= percent_encode(query
.as_bytes(), NON_ALPHANUMERIC
);
338 &format
!("/crates?q={}&per_page={}", formatted_query
, limit
),
343 let crates
= serde_json
::from_str
::<Crates
>(&body
)?
;
344 Ok((crates
.crates
, crates
.meta
.total
))
347 pub fn yank(&mut self, krate
: &str, version
: &str) -> Result
<()> {
348 let body
= self.delete(&format
!("/crates/{}/{}/yank", krate
, version
), None
)?
;
349 assert
!(serde_json
::from_str
::<R
>(&body
)?
.ok
);
353 pub fn unyank(&mut self, krate
: &str, version
: &str) -> Result
<()> {
354 let body
= self.put(&format
!("/crates/{}/{}/unyank", krate
, version
), &[])?
;
355 assert
!(serde_json
::from_str
::<R
>(&body
)?
.ok
);
359 fn put(&mut self, path
: &str, b
: &[u8]) -> Result
<String
> {
360 self.handle
.put(true)?
;
361 self.req(path
, Some(b
), Auth
::Authorized
)
364 fn get(&mut self, path
: &str) -> Result
<String
> {
365 self.handle
.get(true)?
;
366 self.req(path
, None
, Auth
::Authorized
)
369 fn delete(&mut self, path
: &str, b
: Option
<&[u8]>) -> Result
<String
> {
370 self.handle
.custom_request("DELETE")?
;
371 self.req(path
, b
, Auth
::Authorized
)
374 fn req(&mut self, path
: &str, body
: Option
<&[u8]>, authorized
: Auth
) -> Result
<String
> {
375 self.handle
.url(&format
!("{}/api/v1{}", self.host
, path
))?
;
376 let mut headers
= List
::new();
377 headers
.append("Accept: application/json")?
;
378 headers
.append("Content-Type: application/json")?
;
380 if authorized
== Auth
::Authorized
{
381 let token
= match self.token
.as_ref() {
383 None
=> bail
!("no upload token found, please run `cargo login`"),
385 headers
.append(&format
!("Authorization: {}", token
))?
;
387 self.handle
.http_headers(headers
)?
;
390 self.handle
.upload(true)?
;
391 self.handle
.in_filesize(body
.len() as u64)?
;
392 self.handle(&mut |buf
| body
.read(buf
).unwrap_or(0))
393 .map_err(|e
| e
.into())
395 None
=> self.handle(&mut |_
| 0).map_err(|e
| e
.into()),
401 read
: &mut dyn FnMut(&mut [u8]) -> usize,
402 ) -> std
::result
::Result
<String
, ResponseError
> {
403 let mut headers
= Vec
::new();
404 let mut body
= Vec
::new();
406 let mut handle
= self.handle
.transfer();
407 handle
.read_function(|buf
| Ok(read(buf
)))?
;
408 handle
.write_function(|data
| {
409 body
.extend_from_slice(data
);
412 handle
.header_function(|data
| {
413 // Headers contain trailing \r\n, trim them to make it easier
415 let s
= String
::from_utf8_lossy(data
).trim().to_string();
422 let body
= match String
::from_utf8(body
) {
425 return Err(ResponseError
::Other(format_err
!(
426 "response body was not valid utf-8"
430 let errors
= serde_json
::from_str
::<ApiErrorList
>(&body
)
432 .map(|s
| s
.errors
.into_iter().map(|s
| s
.detail
).collect
::<Vec
<_
>>());
434 match (self.handle
.response_code()?
, errors
) {
435 (0, None
) | (200, None
) => Ok(body
),
436 (code
, Some(errors
)) => Err(ResponseError
::Api { code, errors }
),
437 (code
, None
) => Err(ResponseError
::Code
{
446 fn reason(code
: u32) -> &'
static str {
447 // Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
450 101 => "Switching Protocol",
451 103 => "Early Hints",
455 203 => "Non-Authoritative Information",
457 205 => "Reset Content",
458 206 => "Partial Content",
459 300 => "Multiple Choice",
460 301 => "Moved Permanently",
463 304 => "Not Modified",
464 307 => "Temporary Redirect",
465 308 => "Permanent Redirect",
466 400 => "Bad Request",
467 401 => "Unauthorized",
468 402 => "Payment Required",
471 405 => "Method Not Allowed",
472 406 => "Not Acceptable",
473 407 => "Proxy Authentication Required",
474 408 => "Request Timeout",
477 411 => "Length Required",
478 412 => "Precondition Failed",
479 413 => "Payload Too Large",
480 414 => "URI Too Long",
481 415 => "Unsupported Media Type",
482 416 => "Request Range Not Satisfiable",
483 417 => "Expectation Failed",
484 429 => "Too Many Requests",
485 431 => "Request Header Fields Too Large",
486 500 => "Internal Server Error",
487 501 => "Not Implemented",
488 502 => "Bad Gateway",
489 503 => "Service Unavailable",
490 504 => "Gateway Timeout",
495 /// Returns `true` if the host of the given URL is "crates.io".
496 pub fn is_url_crates_io(url
: &str) -> bool
{
498 .map(|u
| u
.host_str() == Some("crates.io"))