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
//! A simple Artifactory client

use std::io::Write;
use std::path::Path;

use chrono::offset::FixedOffset;
use chrono::DateTime;
use serde_derive::*;
use thiserror::Error;
use url::Url;

/// An unauthenticated Artifactory client.
pub struct Client {
    origin: String,
}

impl Client {
    /// Create a new client without any authentication.
    ///
    /// This can be used for read-only access of artifacts.
    pub fn new<S: AsRef<str>>(origin: S) -> Self {
        Self {
            origin: origin.as_ref().to_string(),
        }
    }

    /// Fetch metadata about a remote artifact.
    pub async fn file_info(&self, path: ArtifactoryPath) -> Result<FileInfo, Error> {
        let url = format!("{}/artifactory/api/storage/{}", self.origin, path.0);
        let url = Url::parse(&url).unwrap();

        let info: FileInfo = reqwest::get(url).await?.json().await?;

        Ok(info)
    }

    /// Fetch a remote artifact.
    ///
    /// An optional progress closure can be provided to get updates on the
    /// transfer.
    pub async fn pull<F>(
        &self,
        path: ArtifactoryPath,
        dest: &Path,
        mut progress: F,
    ) -> Result<(), Error>
    where
        F: FnMut(DownloadProgress),
    {
        let url = format!("{}/artifactory/{}", self.origin, path.0);
        let url = Url::parse(&url).unwrap();

        let mut dest = std::fs::File::create(dest)?;

        let mut res = reqwest::get(url).await?;

        let expected_bytes_downloaded = res.content_length().unwrap_or(0);
        let mut bytes_downloaded = 0;
        while let Some(chunk) = res.chunk().await? {
            bytes_downloaded += chunk.as_ref().len() as u64;
            dest.write_all(chunk.as_ref())?;
            let status = DownloadProgress {
                expected_bytes_downloaded,
                bytes_downloaded,
            };
            progress(status);
        }

        Ok(())
    }
}

/// A path on the remote Artifactory instance.
pub struct ArtifactoryPath(String);

impl<S: AsRef<str>> From<S> for ArtifactoryPath {
    fn from(s: S) -> Self {
        Self(s.as_ref().to_string())
    }
}

/// Metadata on a remote artifact.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FileInfo {
    uri: Url,
    download_uri: Url,
    repo: String,
    path: String,
    remote_url: Option<Url>,
    created: DateTime<FixedOffset>,
    created_by: String,
    last_modified: DateTime<FixedOffset>,
    modified_by: String,
    last_updated: DateTime<FixedOffset>,
    size: String,
    mime_type: String,
    pub checksums: Checksums,
    original_checksums: OriginalChecksums,
}

/// File checksums that should always be sent.
#[derive(Debug, Deserialize)]
pub struct Checksums {
    #[serde(with = "hex")]
    pub md5: Vec<u8>,
    #[serde(with = "hex")]
    pub sha1: Vec<u8>,
    #[serde(with = "hex")]
    pub sha256: Vec<u8>,
}

/// File checksums that are only sent if they were originally uploaded.
#[derive(Debug, Deserialize)]
pub struct OriginalChecksums {
    md5: Option<String>,
    sha1: Option<String>,
    sha256: Option<String>,
}

#[derive(Clone, Copy, Debug)]
pub struct DownloadProgress {
    pub expected_bytes_downloaded: u64,
    pub bytes_downloaded: u64,
}

#[derive(Error, Debug)]
pub enum Error {
    #[error("An IO error occurred.")]
    Io(#[from] std::io::Error),
    #[error("A HTTP related error occurred.")]
    Reqwest(#[from] reqwest::Error),
}