bunny_api/edge/
upload.rs

1
2use super::StorageZone;
3use crate::client::Client;
4use crate::error::{APIResult, error_from_response, Error as APIError};
5use crate::files::{file_content, checksum, Error as FileError, urlencode_path};
6use std::path::Path;
7use reqwest::Method;
8
9/// Errors that may occur when uploading files
10#[derive(Debug, thiserror::Error)]
11pub enum Error {
12    /// Error while processing a file.
13    #[error(transparent)]
14    File(#[from] FileError),
15    #[error("expected SHA256 checksum {expected}, got {received}")]
16    /// Expected the data to send to have a certain SHA256 checksum,
17    /// but the actual checksum did not match.
18    MismatchedSha256 {
19        /// The expected SHA256 checksum.
20        expected: String,
21        /// The calculated SHA256 checksum.
22        received: String,
23    }
24}
25
26fn get_url(storage_zone: StorageZone, zone_name: &str, dest: &str) -> String {
27    let dest = dest.strip_prefix('/').unwrap_or(dest);
28    let dest = urlencode_path(dest);
29    format!("https://{}/{}/{}", storage_zone.url(), zone_name, dest)
30}
31
32async fn upload_data(client: &Client, url: String, content: Vec<u8>, expected_sha256: Option<String>) -> APIResult<(), Error> {
33    #[cfg(feature = "tracing")]
34    tracing::debug!("hashing file contents");
35    let hash = checksum(&content);
36    if let Some(expected) = expected_sha256 {
37        let expected = expected.to_uppercase();
38
39        #[cfg(feature = "tracing")]
40        tracing::debug!(%expected, %hash, "checking that hash matches expected value");
41        
42        if expected != hash {
43            return Err(APIError::new(
44                Method::PUT,
45                url,
46                Error::MismatchedSha256 { expected, received: hash }
47            ));
48        }
49    }
50
51    let request = client
52        .put(&url)
53        .header("Checksum", hash)
54        .header("content-type", "application/octet-stream")
55        .body(content)
56        .build()
57        .map_err(crate::Error::map_request_err(
58            Method::PUT,
59            url.clone(),
60        ))?;
61
62    let resp = client
63        .send_logged(request)
64        .await
65        .map_err(crate::Error::map_request_err(
66            Method::PUT,
67            url.clone(),
68        ))?;
69
70    error_from_response(resp)
71        .await
72        .map(|_| ())
73        .map_err(crate::Error::map_response_err(Method::PUT, url))
74}
75
76/// Upload a file to the given Storage Zone by local path.
77///
78/// # Errors
79///
80/// - I/O errors while reading the file
81/// - See also: [`upload_file_data`]
82#[allow(clippy::module_name_repetitions)]
83pub async fn upload_file_by_path(client: &Client, storage_zone: StorageZone, zone_name: &str, dest: &str, file: &Path, expected_sha256: Option<String>) -> APIResult<(), Error> {
84    let url = get_url(storage_zone, zone_name, dest);
85    let content = file_content(file).await
86        .map_err(Error::from)
87        .map_err(crate::Error::map_err(
88            Method::PUT,
89            url.clone(),
90        ))?;
91
92    upload_data(client, url, content, expected_sha256).await
93}
94
95/// Upload bytes to the given Storage Zone.
96///
97/// # Errors
98///
99/// - Failure to send the request
100/// - Error response from the server
101#[allow(clippy::module_name_repetitions)]
102pub async fn upload_file_data(client: &Client, storage_zone: StorageZone, zone_name: &str, dest: &str, content: Vec<u8>, expected_sha256: Option<String>) -> APIResult<(), Error> {
103    let url = get_url(storage_zone, zone_name, dest);
104    upload_data(client, url, content, expected_sha256).await
105}