update
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# Local copy keeps the registry manifest shape and narrows log features so
|
||||
# GPUI and Servo can share the same workspace dependency graph.
|
||||
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "gpui_http_client"
|
||||
version = "0.2.2"
|
||||
build = false
|
||||
publish = true
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "A HTTP client library for Zed and GPUI"
|
||||
readme = false
|
||||
license = "Apache-2.0"
|
||||
resolver = "2"
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[lib]
|
||||
name = "gpui_http_client"
|
||||
path = "src/http_client.rs"
|
||||
doctest = true
|
||||
|
||||
[dependencies.anyhow]
|
||||
version = "1.0.86"
|
||||
|
||||
[dependencies.async-compression]
|
||||
version = "0.4"
|
||||
features = [
|
||||
"gzip",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[dependencies.async-fs]
|
||||
version = "2.1"
|
||||
|
||||
[dependencies.async-tar]
|
||||
version = "0.5.0-zed"
|
||||
package = "zed-async-tar"
|
||||
|
||||
[dependencies.bytes]
|
||||
version = "1.0"
|
||||
|
||||
[dependencies.derive_more]
|
||||
version = "0.99.17"
|
||||
|
||||
[dependencies.futures]
|
||||
version = "0.3"
|
||||
|
||||
[dependencies.http]
|
||||
version = "1.1"
|
||||
|
||||
[dependencies.http-body]
|
||||
version = "1.0"
|
||||
|
||||
[dependencies.log]
|
||||
version = "0.4.16"
|
||||
|
||||
[dependencies.parking_lot]
|
||||
version = "0.12.1"
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.12.15-zed"
|
||||
features = [
|
||||
"charset",
|
||||
"http2",
|
||||
"macos-system-configuration",
|
||||
"multipart",
|
||||
"rustls-tls-native-roots",
|
||||
"socks",
|
||||
"stream",
|
||||
]
|
||||
default-features = false
|
||||
package = "zed-reqwest"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.221"
|
||||
features = [
|
||||
"derive",
|
||||
"rc",
|
||||
]
|
||||
|
||||
[dependencies.serde_json]
|
||||
version = "1.0.144"
|
||||
features = [
|
||||
"preserve_order",
|
||||
"raw_value",
|
||||
]
|
||||
|
||||
[dependencies.sha2]
|
||||
version = "0.10"
|
||||
|
||||
[dependencies.tempfile]
|
||||
version = "3.20.0"
|
||||
|
||||
[dependencies.url]
|
||||
version = "2.2"
|
||||
|
||||
[dependencies.util]
|
||||
version = "0.2.2"
|
||||
package = "gpui_util"
|
||||
|
||||
[lints.clippy]
|
||||
dbg_macro = "deny"
|
||||
declare_interior_mutable_const = "deny"
|
||||
disallowed_methods = "deny"
|
||||
large_enum_variant = "allow"
|
||||
let_underscore_future = "allow"
|
||||
nonminimal_bool = "allow"
|
||||
redundant_clone = "deny"
|
||||
single_range_in_vec_init = "allow"
|
||||
todo = "deny"
|
||||
too_many_arguments = "allow"
|
||||
type_complexity = "allow"
|
||||
|
||||
[lints.clippy.style]
|
||||
level = "allow"
|
||||
priority = -1
|
||||
|
||||
[lints.rust.unexpected_cfgs]
|
||||
level = "allow"
|
||||
priority = 0
|
||||
@@ -0,0 +1,222 @@
|
||||
Copyright 2022 - 2025 Zed Industries, Inc.
|
||||
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
|
||||
1. Definitions.
|
||||
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -0,0 +1,149 @@
|
||||
use std::{
|
||||
io::{Cursor, Read},
|
||||
pin::Pin,
|
||||
task::Poll,
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures::AsyncRead;
|
||||
use http_body::{Body, Frame};
|
||||
|
||||
/// Based on the implementation of AsyncBody in
|
||||
/// <https://github.com/sagebind/isahc/blob/5c533f1ef4d6bdf1fd291b5103c22110f41d0bf0/src/body/mod.rs>.
|
||||
pub struct AsyncBody(pub Inner);
|
||||
|
||||
pub enum Inner {
|
||||
/// An empty body.
|
||||
Empty,
|
||||
|
||||
/// A body stored in memory.
|
||||
Bytes(std::io::Cursor<Bytes>),
|
||||
|
||||
/// An asynchronous reader.
|
||||
AsyncReader(Pin<Box<dyn futures::AsyncRead + Send + Sync>>),
|
||||
}
|
||||
|
||||
impl AsyncBody {
|
||||
/// Create a new empty body.
|
||||
///
|
||||
/// An empty body represents the *absence* of a body, which is semantically
|
||||
/// different than the presence of a body of zero length.
|
||||
pub fn empty() -> Self {
|
||||
Self(Inner::Empty)
|
||||
}
|
||||
/// Create a streaming body that reads from the given reader.
|
||||
pub fn from_reader<R>(read: R) -> Self
|
||||
where
|
||||
R: AsyncRead + Send + Sync + 'static,
|
||||
{
|
||||
Self(Inner::AsyncReader(Box::pin(read)))
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: Bytes) -> Self {
|
||||
Self(Inner::Bytes(Cursor::new(bytes)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AsyncBody {
|
||||
fn default() -> Self {
|
||||
Self(Inner::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<()> for AsyncBody {
|
||||
fn from(_: ()) -> Self {
|
||||
Self(Inner::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bytes> for AsyncBody {
|
||||
fn from(bytes: Bytes) -> Self {
|
||||
Self::from_bytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for AsyncBody {
|
||||
fn from(body: Vec<u8>) -> Self {
|
||||
Self::from_bytes(body.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for AsyncBody {
|
||||
fn from(body: String) -> Self {
|
||||
Self::from_bytes(body.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static [u8]> for AsyncBody {
|
||||
#[inline]
|
||||
fn from(s: &'static [u8]) -> Self {
|
||||
Self::from_bytes(Bytes::from_static(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for AsyncBody {
|
||||
#[inline]
|
||||
fn from(s: &'static str) -> Self {
|
||||
Self::from_bytes(Bytes::from_static(s.as_bytes()))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<reqwest::Body> for AsyncBody {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: reqwest::Body) -> Result<Self, Self::Error> {
|
||||
value
|
||||
.as_bytes()
|
||||
.ok_or_else(|| anyhow::anyhow!("Underlying data is a stream"))
|
||||
.map(|bytes| Self::from_bytes(Bytes::copy_from_slice(bytes)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<Self>> From<Option<T>> for AsyncBody {
|
||||
fn from(body: Option<T>) -> Self {
|
||||
match body {
|
||||
Some(body) => body.into(),
|
||||
None => Self::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl futures::AsyncRead for AsyncBody {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> std::task::Poll<std::io::Result<usize>> {
|
||||
// SAFETY: Standard Enum pin projection
|
||||
let inner = unsafe { &mut self.get_unchecked_mut().0 };
|
||||
match inner {
|
||||
Inner::Empty => Poll::Ready(Ok(0)),
|
||||
// Blocking call is over an in-memory buffer
|
||||
Inner::Bytes(cursor) => Poll::Ready(cursor.read(buf)),
|
||||
Inner::AsyncReader(async_reader) => {
|
||||
AsyncRead::poll_read(async_reader.as_mut(), cx, buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Body for AsyncBody {
|
||||
type Data = Bytes;
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn poll_frame(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
|
||||
let mut buffer = vec![0; 8192];
|
||||
match AsyncRead::poll_read(self.as_mut(), cx, &mut buffer) {
|
||||
Poll::Ready(Ok(0)) => Poll::Ready(None),
|
||||
Poll::Ready(Ok(n)) => {
|
||||
let data = Bytes::copy_from_slice(&buffer[..n]);
|
||||
Poll::Ready(Some(Ok(Frame::data(data))))
|
||||
}
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Some(Err(e))),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
use crate::HttpClient;
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use futures::AsyncReadExt;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
|
||||
pub struct GitHubLspBinaryVersion {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub digest: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct GithubRelease {
|
||||
pub tag_name: String,
|
||||
#[serde(rename = "prerelease")]
|
||||
pub pre_release: bool,
|
||||
pub assets: Vec<GithubReleaseAsset>,
|
||||
pub tarball_url: String,
|
||||
pub zipball_url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct GithubReleaseAsset {
|
||||
pub name: String,
|
||||
pub browser_download_url: String,
|
||||
pub digest: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn latest_github_release(
|
||||
repo_name_with_owner: &str,
|
||||
require_assets: bool,
|
||||
pre_release: bool,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> anyhow::Result<GithubRelease> {
|
||||
let mut response = http
|
||||
.get(
|
||||
format!("https://api.github.com/repos/{repo_name_with_owner}/releases").as_str(),
|
||||
Default::default(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.context("error fetching latest release")?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading latest release")?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
let releases = match serde_json::from_slice::<Vec<GithubRelease>>(body.as_slice()) {
|
||||
Ok(releases) => releases,
|
||||
|
||||
Err(err) => {
|
||||
log::error!("Error deserializing: {err:?}");
|
||||
log::error!(
|
||||
"GitHub API response text: {:?}",
|
||||
String::from_utf8_lossy(body.as_slice())
|
||||
);
|
||||
anyhow::bail!("error deserializing latest release: {err:?}");
|
||||
}
|
||||
};
|
||||
|
||||
let mut release = releases
|
||||
.into_iter()
|
||||
.filter(|release| !require_assets || !release.assets.is_empty())
|
||||
.find(|release| release.pre_release == pre_release)
|
||||
.context("finding a prerelease")?;
|
||||
release.assets.iter_mut().for_each(|asset| {
|
||||
if let Some(digest) = &mut asset.digest
|
||||
&& let Some(stripped) = digest.strip_prefix("sha256:")
|
||||
{
|
||||
*digest = stripped.to_owned();
|
||||
}
|
||||
});
|
||||
Ok(release)
|
||||
}
|
||||
|
||||
pub async fn get_release_by_tag_name(
|
||||
repo_name_with_owner: &str,
|
||||
tag: &str,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> anyhow::Result<GithubRelease> {
|
||||
let mut response = http
|
||||
.get(
|
||||
&format!("https://api.github.com/repos/{repo_name_with_owner}/releases/tags/{tag}"),
|
||||
Default::default(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.context("error fetching latest release")?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
let status = response.status();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading latest release")?;
|
||||
|
||||
if status.is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
let release = serde_json::from_slice::<GithubRelease>(body.as_slice()).map_err(|err| {
|
||||
log::error!("Error deserializing: {err:?}");
|
||||
log::error!(
|
||||
"GitHub API response text: {:?}",
|
||||
String::from_utf8_lossy(body.as_slice())
|
||||
);
|
||||
anyhow!("error deserializing GitHub release: {err:?}")
|
||||
})?;
|
||||
|
||||
Ok(release)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum AssetKind {
|
||||
TarGz,
|
||||
Gz,
|
||||
Zip,
|
||||
}
|
||||
|
||||
pub fn build_asset_url(repo_name_with_owner: &str, tag: &str, kind: AssetKind) -> Result<String> {
|
||||
let mut url = Url::parse(&format!(
|
||||
"https://github.com/{repo_name_with_owner}/archive/refs/tags",
|
||||
))?;
|
||||
// We're pushing this here, because tags may contain `/` and other characters
|
||||
// that need to be escaped.
|
||||
let asset_filename = format!(
|
||||
"{tag}.{extension}",
|
||||
extension = match kind {
|
||||
AssetKind::TarGz => "tar.gz",
|
||||
AssetKind::Gz => "gz",
|
||||
AssetKind::Zip => "zip",
|
||||
}
|
||||
);
|
||||
url.path_segments_mut()
|
||||
.map_err(|()| anyhow!("cannot modify url path segments"))?
|
||||
.push(&asset_filename);
|
||||
Ok(url.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::github::{AssetKind, build_asset_url};
|
||||
|
||||
#[test]
|
||||
fn test_build_asset_url() {
|
||||
let tag = "release/2.3.5";
|
||||
let repo_name_with_owner = "microsoft/vscode-eslint";
|
||||
|
||||
let tarball = build_asset_url(repo_name_with_owner, tag, AssetKind::TarGz).unwrap();
|
||||
assert_eq!(
|
||||
tarball,
|
||||
"https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.tar.gz"
|
||||
);
|
||||
|
||||
let zip = build_asset_url(repo_name_with_owner, tag, AssetKind::Zip).unwrap();
|
||||
assert_eq!(
|
||||
zip,
|
||||
"https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.zip"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
use std::{path::Path, pin::Pin, task::Poll};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use futures::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, io::BufReader};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::{HttpClient, github::AssetKind};
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize, Debug)]
|
||||
pub struct GithubBinaryMetadata {
|
||||
pub metadata_version: u64,
|
||||
pub digest: Option<String>,
|
||||
}
|
||||
|
||||
impl GithubBinaryMetadata {
|
||||
pub async fn read_from_file(metadata_path: &Path) -> Result<GithubBinaryMetadata> {
|
||||
let metadata_content = async_fs::read_to_string(metadata_path)
|
||||
.await
|
||||
.with_context(|| format!("reading metadata file at {metadata_path:?}"))?;
|
||||
serde_json::from_str(&metadata_content)
|
||||
.with_context(|| format!("parsing metadata file at {metadata_path:?}"))
|
||||
}
|
||||
|
||||
pub async fn write_to_file(&self, metadata_path: &Path) -> Result<()> {
|
||||
let metadata_content = serde_json::to_string(self)
|
||||
.with_context(|| format!("serializing metadata for {metadata_path:?}"))?;
|
||||
async_fs::write(metadata_path, metadata_content.as_bytes())
|
||||
.await
|
||||
.with_context(|| format!("writing metadata file at {metadata_path:?}"))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn download_server_binary(
|
||||
http_client: &dyn HttpClient,
|
||||
url: &str,
|
||||
digest: Option<&str>,
|
||||
destination_path: &Path,
|
||||
asset_kind: AssetKind,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
log::info!("downloading github artifact from {url}");
|
||||
let mut response = http_client
|
||||
.get(url, Default::default(), true)
|
||||
.await
|
||||
.with_context(|| format!("downloading release from {url}"))?;
|
||||
let body = response.body_mut();
|
||||
match digest {
|
||||
Some(expected_sha_256) => {
|
||||
let temp_asset_file = tempfile::NamedTempFile::new()
|
||||
.with_context(|| format!("creating a temporary file for {url}"))?;
|
||||
let (temp_asset_file, _temp_guard) = temp_asset_file.into_parts();
|
||||
let mut writer = HashingWriter {
|
||||
writer: async_fs::File::from(temp_asset_file),
|
||||
hasher: Sha256::new(),
|
||||
};
|
||||
futures::io::copy(&mut BufReader::new(body), &mut writer)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("saving archive contents into the temporary file for {url}",)
|
||||
})?;
|
||||
let asset_sha_256 = format!("{:x}", writer.hasher.finalize());
|
||||
|
||||
anyhow::ensure!(
|
||||
asset_sha_256 == expected_sha_256,
|
||||
"{url} asset got SHA-256 mismatch. Expected: {expected_sha_256}, Got: {asset_sha_256}",
|
||||
);
|
||||
writer
|
||||
.writer
|
||||
.seek(std::io::SeekFrom::Start(0))
|
||||
.await
|
||||
.with_context(|| format!("seeking temporary file {destination_path:?}",))?;
|
||||
stream_file_archive(&mut writer.writer, url, destination_path, asset_kind)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("extracting downloaded asset for {url} into {destination_path:?}",)
|
||||
})?;
|
||||
}
|
||||
None => stream_response_archive(body, url, destination_path, asset_kind)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("extracting response for asset {url} into {destination_path:?}",)
|
||||
})?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stream_response_archive(
|
||||
response: impl AsyncRead + Unpin,
|
||||
url: &str,
|
||||
destination_path: &Path,
|
||||
asset_kind: AssetKind,
|
||||
) -> Result<()> {
|
||||
match asset_kind {
|
||||
AssetKind::TarGz => extract_tar_gz(destination_path, url, response).await?,
|
||||
AssetKind::Gz => extract_gz(destination_path, url, response).await?,
|
||||
AssetKind::Zip => {
|
||||
util::archive::extract_zip(destination_path, response).await?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stream_file_archive(
|
||||
file_archive: impl AsyncRead + AsyncSeek + Unpin,
|
||||
url: &str,
|
||||
destination_path: &Path,
|
||||
asset_kind: AssetKind,
|
||||
) -> Result<()> {
|
||||
match asset_kind {
|
||||
AssetKind::TarGz => extract_tar_gz(destination_path, url, file_archive).await?,
|
||||
AssetKind::Gz => extract_gz(destination_path, url, file_archive).await?,
|
||||
#[cfg(not(windows))]
|
||||
AssetKind::Zip => {
|
||||
util::archive::extract_seekable_zip(destination_path, file_archive).await?;
|
||||
}
|
||||
#[cfg(windows)]
|
||||
AssetKind::Zip => {
|
||||
util::archive::extract_zip(destination_path, file_archive).await?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn extract_tar_gz(
|
||||
destination_path: &Path,
|
||||
url: &str,
|
||||
from: impl AsyncRead + Unpin,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(from));
|
||||
let archive = async_tar::Archive::new(decompressed_bytes);
|
||||
archive
|
||||
.unpack(&destination_path)
|
||||
.await
|
||||
.with_context(|| format!("extracting {url} to {destination_path:?}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn extract_gz(
|
||||
destination_path: &Path,
|
||||
url: &str,
|
||||
from: impl AsyncRead + Unpin,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut decompressed_bytes = GzipDecoder::new(BufReader::new(from));
|
||||
let mut file = async_fs::File::create(&destination_path)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("creating a file {destination_path:?} for a download from {url}")
|
||||
})?;
|
||||
futures::io::copy(&mut decompressed_bytes, &mut file)
|
||||
.await
|
||||
.with_context(|| format!("extracting {url} to {destination_path:?}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct HashingWriter<W: AsyncWrite + Unpin> {
|
||||
writer: W,
|
||||
hasher: Sha256,
|
||||
}
|
||||
|
||||
impl<W: AsyncWrite + Unpin> AsyncWrite for HashingWriter<W> {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<std::result::Result<usize, std::io::Error>> {
|
||||
match Pin::new(&mut self.writer).poll_write(cx, buf) {
|
||||
Poll::Ready(Ok(n)) => {
|
||||
self.hasher.update(&buf[..n]);
|
||||
Poll::Ready(Ok(n))
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
Pin::new(&mut self.writer).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_close(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<std::result::Result<(), std::io::Error>> {
|
||||
Pin::new(&mut self.writer).poll_close(cx)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
mod async_body;
|
||||
pub mod github;
|
||||
pub mod github_download;
|
||||
|
||||
pub use anyhow::{Result, anyhow};
|
||||
pub use async_body::{AsyncBody, Inner};
|
||||
use derive_more::Deref;
|
||||
use http::HeaderValue;
|
||||
pub use http::{self, Method, Request, Response, StatusCode, Uri, request::Builder};
|
||||
|
||||
use futures::{
|
||||
FutureExt as _,
|
||||
future::{self, BoxFuture},
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
#[cfg(feature = "test-support")]
|
||||
use std::fmt;
|
||||
use std::{any::type_name, sync::Arc};
|
||||
pub use url::Url;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum RedirectPolicy {
|
||||
#[default]
|
||||
NoFollow,
|
||||
FollowLimit(u32),
|
||||
FollowAll,
|
||||
}
|
||||
pub struct FollowRedirects(pub bool);
|
||||
|
||||
pub trait HttpRequestExt {
|
||||
/// Conditionally modify self with the given closure.
|
||||
fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if condition { then(self) } else { self }
|
||||
}
|
||||
|
||||
/// Conditionally unwrap and modify self with the given closure, if the given option is Some.
|
||||
fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match option {
|
||||
Some(value) => then(self, value),
|
||||
None => self,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not to follow redirects
|
||||
fn follow_redirects(self, follow: RedirectPolicy) -> Self;
|
||||
}
|
||||
|
||||
impl HttpRequestExt for http::request::Builder {
|
||||
fn follow_redirects(self, follow: RedirectPolicy) -> Self {
|
||||
self.extension(follow)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HttpClient: 'static + Send + Sync {
|
||||
fn type_name(&self) -> &'static str;
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue>;
|
||||
|
||||
fn send(
|
||||
&self,
|
||||
req: http::Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>;
|
||||
|
||||
fn get(
|
||||
&self,
|
||||
uri: &str,
|
||||
body: AsyncBody,
|
||||
follow_redirects: bool,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
let request = Builder::new()
|
||||
.uri(uri)
|
||||
.follow_redirects(if follow_redirects {
|
||||
RedirectPolicy::FollowAll
|
||||
} else {
|
||||
RedirectPolicy::NoFollow
|
||||
})
|
||||
.body(body);
|
||||
|
||||
match request {
|
||||
Ok(request) => self.send(request),
|
||||
Err(e) => Box::pin(async move { Err(e.into()) }),
|
||||
}
|
||||
}
|
||||
|
||||
fn post_json(
|
||||
&self,
|
||||
uri: &str,
|
||||
body: AsyncBody,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
let request = Builder::new()
|
||||
.uri(uri)
|
||||
.method(Method::POST)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body);
|
||||
|
||||
match request {
|
||||
Ok(request) => self.send(request),
|
||||
Err(e) => Box::pin(async move { Err(e.into()) }),
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url>;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
panic!("called as_fake on {}", type_name::<Self>())
|
||||
}
|
||||
|
||||
fn send_multipart_form<'a>(
|
||||
&'a self,
|
||||
_url: &str,
|
||||
_request: reqwest::multipart::Form,
|
||||
) -> BoxFuture<'a, anyhow::Result<Response<AsyncBody>>> {
|
||||
future::ready(Err(anyhow!("not implemented"))).boxed()
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`HttpClient`] that may have a proxy.
|
||||
#[derive(Deref)]
|
||||
pub struct HttpClientWithProxy {
|
||||
#[deref]
|
||||
client: Arc<dyn HttpClient>,
|
||||
proxy: Option<Url>,
|
||||
}
|
||||
|
||||
impl HttpClientWithProxy {
|
||||
/// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
|
||||
pub fn new(client: Arc<dyn HttpClient>, proxy_url: Option<String>) -> Self {
|
||||
let proxy_url = proxy_url
|
||||
.and_then(|proxy| proxy.parse().ok())
|
||||
.or_else(read_proxy_from_env);
|
||||
|
||||
Self::new_url(client, proxy_url)
|
||||
}
|
||||
pub fn new_url(client: Arc<dyn HttpClient>, proxy_url: Option<Url>) -> Self {
|
||||
Self {
|
||||
client,
|
||||
proxy: proxy_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for HttpClientWithProxy {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
self.client.user_agent()
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
self.client.type_name()
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
self.client.as_fake()
|
||||
}
|
||||
|
||||
fn send_multipart_form<'a>(
|
||||
&'a self,
|
||||
url: &str,
|
||||
form: reqwest::multipart::Form,
|
||||
) -> BoxFuture<'a, anyhow::Result<Response<AsyncBody>>> {
|
||||
self.client.send_multipart_form(url, form)
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`HttpClient`] that has a base URL.
|
||||
pub struct HttpClientWithUrl {
|
||||
base_url: Mutex<String>,
|
||||
client: HttpClientWithProxy,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for HttpClientWithUrl {
|
||||
type Target = HttpClientWithProxy;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.client
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClientWithUrl {
|
||||
/// Returns a new [`HttpClientWithUrl`] with the given base URL.
|
||||
pub fn new(
|
||||
client: Arc<dyn HttpClient>,
|
||||
base_url: impl Into<String>,
|
||||
proxy_url: Option<String>,
|
||||
) -> Self {
|
||||
let client = HttpClientWithProxy::new(client, proxy_url);
|
||||
|
||||
Self {
|
||||
base_url: Mutex::new(base_url.into()),
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_url(
|
||||
client: Arc<dyn HttpClient>,
|
||||
base_url: impl Into<String>,
|
||||
proxy_url: Option<Url>,
|
||||
) -> Self {
|
||||
let client = HttpClientWithProxy::new_url(client, proxy_url);
|
||||
|
||||
Self {
|
||||
base_url: Mutex::new(base_url.into()),
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the base URL.
|
||||
pub fn base_url(&self) -> String {
|
||||
self.base_url.lock().clone()
|
||||
}
|
||||
|
||||
/// Sets the base URL.
|
||||
pub fn set_base_url(&self, base_url: impl Into<String>) {
|
||||
let base_url = base_url.into();
|
||||
*self.base_url.lock() = base_url;
|
||||
}
|
||||
|
||||
/// Builds a URL using the given path.
|
||||
pub fn build_url(&self, path: &str) -> String {
|
||||
format!("{}{}", self.base_url(), path)
|
||||
}
|
||||
|
||||
/// Builds a Zed API URL using the given path.
|
||||
pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||
let base_url = self.base_url();
|
||||
let base_api_url = match base_url.as_ref() {
|
||||
"https://zed.dev" => "https://api.zed.dev",
|
||||
"https://staging.zed.dev" => "https://api-staging.zed.dev",
|
||||
"http://localhost:3000" => "http://localhost:8080",
|
||||
other => other,
|
||||
};
|
||||
|
||||
Ok(Url::parse_with_params(
|
||||
&format!("{}{}", base_api_url, path),
|
||||
query,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Builds a Zed Cloud URL using the given path.
|
||||
pub fn build_zed_cloud_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||
let base_url = self.base_url();
|
||||
let base_api_url = match base_url.as_ref() {
|
||||
"https://zed.dev" => "https://cloud.zed.dev",
|
||||
"https://staging.zed.dev" => "https://cloud.zed.dev",
|
||||
"http://localhost:3000" => "http://localhost:8787",
|
||||
other => other,
|
||||
};
|
||||
|
||||
Ok(Url::parse_with_params(
|
||||
&format!("{}{}", base_api_url, path),
|
||||
query,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Builds a Zed LLM URL using the given path.
|
||||
pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||
let base_url = self.base_url();
|
||||
let base_api_url = match base_url.as_ref() {
|
||||
"https://zed.dev" => "https://cloud.zed.dev",
|
||||
"https://staging.zed.dev" => "https://llm-staging.zed.dev",
|
||||
"http://localhost:3000" => "http://localhost:8787",
|
||||
other => other,
|
||||
};
|
||||
|
||||
Ok(Url::parse_with_params(
|
||||
&format!("{}{}", base_api_url, path),
|
||||
query,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for HttpClientWithUrl {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
self.client.user_agent()
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
self.client.proxy.as_ref()
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
self.client.type_name()
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
self.client.as_fake()
|
||||
}
|
||||
|
||||
fn send_multipart_form<'a>(
|
||||
&'a self,
|
||||
url: &str,
|
||||
request: reqwest::multipart::Form,
|
||||
) -> BoxFuture<'a, anyhow::Result<Response<AsyncBody>>> {
|
||||
self.client.send_multipart_form(url, request)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_proxy_from_env() -> Option<Url> {
|
||||
const ENV_VARS: &[&str] = &[
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"HTTP_PROXY",
|
||||
"http_proxy",
|
||||
];
|
||||
|
||||
ENV_VARS
|
||||
.iter()
|
||||
.find_map(|var| std::env::var(var).ok())
|
||||
.and_then(|env| env.parse().ok())
|
||||
}
|
||||
|
||||
pub fn read_no_proxy_from_env() -> Option<String> {
|
||||
const ENV_VARS: &[&str] = &["NO_PROXY", "no_proxy"];
|
||||
|
||||
ENV_VARS.iter().find_map(|var| std::env::var(var).ok())
|
||||
}
|
||||
|
||||
pub struct BlockedHttpClient;
|
||||
|
||||
impl BlockedHttpClient {
|
||||
pub fn new() -> Self {
|
||||
BlockedHttpClient
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for BlockedHttpClient {
|
||||
fn send(
|
||||
&self,
|
||||
_req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
Box::pin(async {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
"BlockedHttpClient disallowed request",
|
||||
)
|
||||
.into())
|
||||
})
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
None
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
None
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
panic!("called as_fake on {}", type_name::<Self>())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
type FakeHttpHandler = Arc<
|
||||
dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
>;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub struct FakeHttpClient {
|
||||
handler: Mutex<Option<FakeHttpHandler>>,
|
||||
user_agent: HeaderValue,
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
impl FakeHttpClient {
|
||||
pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
|
||||
where
|
||||
Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
|
||||
F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
|
||||
{
|
||||
Arc::new(HttpClientWithUrl {
|
||||
base_url: Mutex::new("http://test.example".into()),
|
||||
client: HttpClientWithProxy {
|
||||
client: Arc::new(Self {
|
||||
handler: Mutex::new(Some(Arc::new(move |req| Box::pin(handler(req))))),
|
||||
user_agent: HeaderValue::from_static(type_name::<Self>()),
|
||||
}),
|
||||
proxy: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_404_response() -> Arc<HttpClientWithUrl> {
|
||||
Self::create(|_| async move {
|
||||
Ok(Response::builder()
|
||||
.status(404)
|
||||
.body(Default::default())
|
||||
.unwrap())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_200_response() -> Arc<HttpClientWithUrl> {
|
||||
Self::create(|_| async move {
|
||||
Ok(Response::builder()
|
||||
.status(200)
|
||||
.body(Default::default())
|
||||
.unwrap())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn replace_handler<Fut, F>(&self, new_handler: F)
|
||||
where
|
||||
Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
|
||||
F: Fn(FakeHttpHandler, Request<AsyncBody>) -> Fut + Send + Sync + 'static,
|
||||
{
|
||||
let mut handler = self.handler.lock();
|
||||
let old_handler = handler.take().unwrap();
|
||||
*handler = Some(Arc::new(move |req| {
|
||||
Box::pin(new_handler(old_handler.clone(), req))
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
impl fmt::Debug for FakeHttpClient {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("FakeHttpClient").finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
impl HttpClient for FakeHttpClient {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
((self.handler.lock().as_ref().unwrap())(req)) as _
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
Some(&self.user_agent)
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
None
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
self
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user