1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
use crate::io;
use crate::io::EnvironmentIo;
use crate::repository::local::LocalCachedRepository;
use crate::utils::MapResultExt;
use crate::{PackageInfo, PackageJson, VersionSelector};
use core::iter::Iterator;
use core::option::Option;
use futures::prelude::*;
use indexmap::IndexMap;
use std::convert::Infallible;
use url::Url;

pub trait PackageCollection {
    /// get all packages in the collection
    fn get_all_packages(&self) -> impl Iterator<Item = PackageInfo>;

    /// get all package versions of the specified package
    fn find_packages(&self, package: &str) -> impl Iterator<Item = PackageInfo>;

    /// get specified version of specified package
    fn find_package_by_name(
        &self,
        package: &str,
        package_selector: VersionSelector,
    ) -> Option<PackageInfo>;
}

pub trait EnvironmentIoHolder {
    type EnvironmentIo: EnvironmentIo;
    fn io(&self) -> &Self::EnvironmentIo;
}

/// The trait for downloading remote packages.
///
/// Caching packages is responsibility of this crate.
pub trait RemotePackageDownloader {
    type FileStream: AsyncRead + AsyncSeek + Unpin;

    /// Get package from remote server.
    fn get_package(
        &self,
        repository: &LocalCachedRepository,
        package: &PackageJson,
    ) -> impl Future<Output = io::Result<Self::FileStream>> + Send;
}

/// The HTTP Client.
pub trait HttpClient: Sync {
    /// Get resource from the URL with specified headers
    ///
    /// Note: If remote server returns error status code, this function should return error.
    fn get(
        &self,
        url: &Url,
        headers: &IndexMap<Box<str>, Box<str>>,
    ) -> impl Future<Output = io::Result<impl AsyncRead + Send>> + Send;

    /// Get resource from the URL with specified headers and etag
    ///
    /// Returning `Ok(None)` means cache matched.
    /// Returning `Ok(Some((stream, etag)))` means cache not matched and get from remote server.
    /// Returning `Err(_)` means error.
    ///
    /// Note: If remote server returns error status code, this function should return error.
    fn get_with_etag(
        &self,
        url: &Url,
        headers: &IndexMap<Box<str>, Box<str>>,
        current_etag: Option<&str>,
    ) -> impl Future<Output = io::Result<Option<(impl AsyncRead + Send, Option<Box<str>>)>>> + Send;
}

impl HttpClient for reqwest::Client {
    async fn get(
        &self,
        url: &Url,
        headers: &IndexMap<Box<str>, Box<str>>,
    ) -> io::Result<impl AsyncRead> {
        // file not found: err

        let mut request = self.get(url.to_owned());

        for (name, header) in headers {
            request = request.header(name.as_ref(), header.as_ref());
        }

        Ok(request
            .send()
            .await
            .and_then(reqwest::Response::error_for_status)
            .err_mapped()?
            .bytes_stream()
            .map(|x| x.err_mapped())
            .into_async_read())
    }

    async fn get_with_etag(
        &self,
        url: &Url,
        headers: &IndexMap<Box<str>, Box<str>>,
        current_etag: Option<&str>,
    ) -> io::Result<Option<(impl AsyncRead, Option<Box<str>>)>> {
        let mut request = self.get(url.to_owned());
        for (name, value) in headers {
            request = request.header(name.as_ref(), value.as_ref());
        }
        if let Some(etag) = current_etag {
            request = request.header("If-None-Match", etag.to_owned())
        }
        let response = request.send().await.err_mapped()?;
        let response = response.error_for_status().err_mapped()?;

        if current_etag.is_some() && response.status() == 304 {
            // for requests with etag, 304 means cache matched
            return Ok(None);
        }

        let etag = response
            .headers()
            .get("Etag")
            .and_then(|x| x.to_str().ok())
            .map(Into::into);

        // response.json() doesn't support BOM
        let response_stream = response
            .bytes_stream()
            .map(|x| x.err_mapped())
            .into_async_read();

        Ok(Some((response_stream, etag)))
    }
}

impl HttpClient for Infallible {
    async fn get(&self, _: &Url, _: &IndexMap<Box<str>, Box<str>>) -> io::Result<impl AsyncRead> {
        Ok(io::empty())
    }

    async fn get_with_etag(
        &self,
        _: &Url,
        _: &IndexMap<Box<str>, Box<str>>,
        _: Option<&str>,
    ) -> io::Result<Option<(impl AsyncRead, Option<Box<str>>)>> {
        Ok(Some((io::empty(), None)))
    }
}