1 #![allow(unknown_lints)]
2 #![allow(clippy::identity_op)] // used for vertical alignment
4 use std
::collections
::BTreeMap
;
6 use std
::io
::prelude
::*;
7 use std
::io
::{Cursor, SeekFrom}
;
8 use std
::time
::Instant
;
10 use anyhow
::{bail, 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)]
125 /// Creates a new `Registry`.
130 /// use curl::easy::Easy;
131 /// use crates_io::Registry;
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);
138 pub fn new_handle(host
: String
, token
: Option
<String
>, handle
: Easy
) -> Registry
{
146 pub fn host(&self) -> &str {
150 pub fn host_is_crates_io(&self) -> bool
{
151 is_url_crates_io(&self.host
)
154 pub fn add_owners(&mut self, krate
: &str, owners
: &[&str]) -> Result
<String
> {
155 let body
= serde_json
::to_string(&OwnersReq { users: owners }
)?
;
156 let body
= self.put(&format
!("/crates/{}/owners", krate
), body
.as_bytes())?
;
157 assert
!(serde_json
::from_str
::<OwnerResponse
>(&body
)?
.ok
);
158 Ok(serde_json
::from_str
::<OwnerResponse
>(&body
)?
.msg
)
161 pub fn remove_owners(&mut self, krate
: &str, owners
: &[&str]) -> Result
<()> {
162 let body
= serde_json
::to_string(&OwnersReq { users: owners }
)?
;
163 let body
= self.delete(&format
!("/crates/{}/owners", krate
), Some(body
.as_bytes()))?
;
164 assert
!(serde_json
::from_str
::<OwnerResponse
>(&body
)?
.ok
);
168 pub fn list_owners(&mut self, krate
: &str) -> Result
<Vec
<User
>> {
169 let body
= self.get(&format
!("/crates/{}/owners", krate
))?
;
170 Ok(serde_json
::from_str
::<Users
>(&body
)?
.users
)
173 pub fn publish(&mut self, krate
: &NewCrate
, mut tarball
: &File
) -> Result
<Warnings
> {
174 let json
= serde_json
::to_string(krate
)?
;
175 // Prepare the body. The format of the upload request is:
178 // <json request> (metadata for the package)
179 // <le u32 of tarball>
182 // NOTE: This can be replaced with `stream_len` if it is ever stabilized.
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")?
;
191 .seek(SeekFrom
::Start(0))
192 .with_context(|| "failed to seek tarball")?
;
194 let mut w
= Vec
::new();
195 w
.extend(&(json
.len() as u32).to_le_bytes());
196 w
.extend(json
.as_bytes().iter().cloned());
197 w
.extend(&(tarball_len
as u32).to_le_bytes());
200 let size
= tarball_len
as usize + header
.len();
201 let mut body
= Cursor
::new(header
).chain(tarball
);
203 let url
= format
!("{}/api/v1/crates/new", self.host
);
205 let token
= match self.token
.as_ref() {
207 None
=> bail
!("no upload token found, please run `cargo login`"),
209 self.handle
.put(true)?
;
210 self.handle
.url(&url
)?
;
211 self.handle
.in_filesize(size
as u64)?
;
212 let mut headers
= List
::new();
213 headers
.append("Accept: application/json")?
;
214 headers
.append(&format
!("Authorization: {}", token
))?
;
215 self.handle
.http_headers(headers
)?
;
217 let body
= self.handle(&mut |buf
| body
.read(buf
).unwrap_or(0))?
;
219 let response
= if body
.is_empty() {
222 body
.parse
::<serde_json
::Value
>()?
225 let invalid_categories
: Vec
<String
> = response
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
);
232 let invalid_badges
: Vec
<String
> = response
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
);
239 let other
: Vec
<String
> = response
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
);
253 pub fn search(&mut self, query
: &str, limit
: u32) -> Result
<(Vec
<Crate
>, u32)> {
254 let formatted_query
= percent_encode(query
.as_bytes(), NON_ALPHANUMERIC
);
256 &format
!("/crates?q={}&per_page={}", formatted_query
, limit
),
261 let crates
= serde_json
::from_str
::<Crates
>(&body
)?
;
262 Ok((crates
.crates
, crates
.meta
.total
))
265 pub fn yank(&mut self, krate
: &str, version
: &str) -> Result
<()> {
266 let body
= self.delete(&format
!("/crates/{}/{}/yank", krate
, version
), None
)?
;
267 assert
!(serde_json
::from_str
::<R
>(&body
)?
.ok
);
271 pub fn unyank(&mut self, krate
: &str, version
: &str) -> Result
<()> {
272 let body
= self.put(&format
!("/crates/{}/{}/unyank", krate
, version
), &[])?
;
273 assert
!(serde_json
::from_str
::<R
>(&body
)?
.ok
);
277 fn put(&mut self, path
: &str, b
: &[u8]) -> Result
<String
> {
278 self.handle
.put(true)?
;
279 self.req(path
, Some(b
), Auth
::Authorized
)
282 fn get(&mut self, path
: &str) -> Result
<String
> {
283 self.handle
.get(true)?
;
284 self.req(path
, None
, Auth
::Authorized
)
287 fn delete(&mut self, path
: &str, b
: Option
<&[u8]>) -> Result
<String
> {
288 self.handle
.custom_request("DELETE")?
;
289 self.req(path
, b
, Auth
::Authorized
)
292 fn req(&mut self, path
: &str, body
: Option
<&[u8]>, authorized
: Auth
) -> Result
<String
> {
293 self.handle
.url(&format
!("{}/api/v1{}", self.host
, path
))?
;
294 let mut headers
= List
::new();
295 headers
.append("Accept: application/json")?
;
296 headers
.append("Content-Type: application/json")?
;
298 if authorized
== Auth
::Authorized
{
299 let token
= match self.token
.as_ref() {
301 None
=> bail
!("no upload token found, please run `cargo login`"),
303 headers
.append(&format
!("Authorization: {}", token
))?
;
305 self.handle
.http_headers(headers
)?
;
308 self.handle
.upload(true)?
;
309 self.handle
.in_filesize(body
.len() as u64)?
;
310 self.handle(&mut |buf
| body
.read(buf
).unwrap_or(0))
312 None
=> self.handle(&mut |_
| 0),
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();
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
);
327 handle
.header_function(|data
| {
328 headers
.push(String
::from_utf8_lossy(data
).into_owned());
331 started
= Instant
::now();
335 let body
= match String
::from_utf8(body
) {
337 Err(..) => bail
!("response body was not valid utf-8"),
339 let errors
= serde_json
::from_str
::<ApiErrorList
>(&body
)
341 .map(|s
| s
.errors
.into_iter().map(|s
| s
.detail
).collect
::<Vec
<_
>>());
343 match (self.handle
.response_code()?
, errors
) {
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."
350 (code
, Some(errors
)) => {
351 let reason
= reason(code
);
353 "api errors (status {} {}): {}",
359 (code
, None
) => bail
!(
360 "failed to get a 200 OK response, got {}\n\
366 headers
.join("\n\t"),
375 fn reason(code
: u32) -> &'
static str {
376 // Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
379 101 => "Switching Protocol",
380 103 => "Early Hints",
384 203 => "Non-Authoritative Information",
386 205 => "Reset Content",
387 206 => "Partial Content",
388 300 => "Multiple Choice",
389 301 => "Moved Permanently",
392 304 => "Not Modified",
393 307 => "Temporary Redirect",
394 308 => "Permanent Redirect",
395 400 => "Bad Request",
396 401 => "Unauthorized",
397 402 => "Payment Required",
400 405 => "Method Not Allowed",
401 406 => "Not Acceptable",
402 407 => "Proxy Authentication Required",
403 408 => "Request Timeout",
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",
424 /// Returns `true` if the host of the given URL is "crates.io".
425 pub fn is_url_crates_io(url
: &str) -> bool
{
427 .map(|u
| u
.host_str() == Some("crates.io"))