artifactory_web_api/
lib.rs

1//! A simple Artifactory client
2
3use std::io::Write;
4use std::path::Path;
5
6use chrono::offset::FixedOffset;
7use chrono::DateTime;
8use serde_derive::*;
9use thiserror::Error;
10use url::Url;
11
12/// An unauthenticated Artifactory client.
13pub struct Client {
14    origin: String,
15}
16
17impl Client {
18    /// Create a new client without any authentication.
19    ///
20    /// This can be used for read-only access of artifacts.
21    pub fn new<S: AsRef<str>>(origin: S) -> Self {
22        Self {
23            origin: origin.as_ref().to_string(),
24        }
25    }
26
27    /// Fetch metadata about a remote artifact.
28    pub async fn file_info(&self, path: ArtifactoryPath) -> Result<FileInfo, Error> {
29        let url = format!("{}/artifactory/api/storage/{}", self.origin, path.0);
30        let url = Url::parse(&url).unwrap();
31
32        let info: FileInfo = reqwest::get(url).await?.json().await?;
33
34        Ok(info)
35    }
36
37    /// Fetch a remote artifact.
38    ///
39    /// An optional progress closure can be provided to get updates on the
40    /// transfer.
41    pub async fn pull<F>(
42        &self,
43        path: ArtifactoryPath,
44        dest: &Path,
45        mut progress: F,
46    ) -> Result<(), Error>
47    where
48        F: FnMut(DownloadProgress),
49    {
50        let url = format!("{}/artifactory/{}", self.origin, path.0);
51        let url = Url::parse(&url).unwrap();
52
53        let mut dest = std::fs::File::create(dest)?;
54
55        let mut res = reqwest::get(url).await?;
56
57        let expected_bytes_downloaded = res.content_length().unwrap_or(0);
58        let mut bytes_downloaded = 0;
59        while let Some(chunk) = res.chunk().await? {
60            bytes_downloaded += chunk.as_ref().len() as u64;
61            dest.write_all(chunk.as_ref())?;
62            let status = DownloadProgress {
63                expected_bytes_downloaded,
64                bytes_downloaded,
65            };
66            progress(status);
67        }
68
69        Ok(())
70    }
71}
72
73/// A path on the remote Artifactory instance.
74pub struct ArtifactoryPath(String);
75
76impl<S: AsRef<str>> From<S> for ArtifactoryPath {
77    fn from(s: S) -> Self {
78        Self(s.as_ref().to_string())
79    }
80}
81
82/// Metadata on a remote artifact.
83#[derive(Debug, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct FileInfo {
86    uri: Url,
87    download_uri: Url,
88    repo: String,
89    path: String,
90    remote_url: Option<Url>,
91    created: DateTime<FixedOffset>,
92    created_by: String,
93    last_modified: DateTime<FixedOffset>,
94    modified_by: String,
95    last_updated: DateTime<FixedOffset>,
96    size: String,
97    mime_type: String,
98    pub checksums: Checksums,
99    original_checksums: OriginalChecksums,
100}
101
102/// File checksums that should always be sent.
103#[derive(Debug, Deserialize)]
104pub struct Checksums {
105    #[serde(with = "hex")]
106    pub md5: Vec<u8>,
107    #[serde(with = "hex")]
108    pub sha1: Vec<u8>,
109    #[serde(with = "hex")]
110    pub sha256: Vec<u8>,
111}
112
113/// File checksums that are only sent if they were originally uploaded.
114#[derive(Debug, Deserialize)]
115pub struct OriginalChecksums {
116    md5: Option<String>,
117    sha1: Option<String>,
118    sha256: Option<String>,
119}
120
121#[derive(Clone, Copy, Debug)]
122pub struct DownloadProgress {
123    pub expected_bytes_downloaded: u64,
124    pub bytes_downloaded: u64,
125}
126
127#[derive(Error, Debug)]
128pub enum Error {
129    #[error("An IO error occurred.")]
130    Io(#[from] std::io::Error),
131    #[error("A HTTP related error occurred.")]
132    Reqwest(#[from] reqwest::Error),
133}