zulip-api 0.0.8

Client library for interacting with the Zulip API
Documentation
use std::io::Read;

use crate::{upload_file, Error, Result};

use super::{Client, Endpoint};

impl Client {
    /// Gets the base64-encoded filename for [TUS resumable uploads](https://tus.io).
    pub fn encode_filename(&self, file_path: &str) -> Result<String> {
        use base64::{Base64, Encoding};

        let path = std::path::Path::new(file_path);

        path.file_name()
            .and_then(|s| s.to_str())
            .map(|s| Base64::encode_string(s.as_bytes()))
            .ok_or(Error::io_filename("missing file name"))
    }

    /// Creates a [TUS resumable upload](https://tus.io) link.
    ///
    /// Returns the resumable file URL path for further TUS requests.
    pub async fn tus_create(&self, file_name: &str, file_len: usize) -> Result<String> {
        let file_md = self
            .encode_filename(file_name)
            .map(|b64| format!("filename {b64},is_confidential"))?;

        self.http
            .post(self.endpoint(Endpoint::ResumableUploadFile))
            .header("Authorization", self.auth_header())
            .header("Content-Length", "0")
            .header("Upload-Length", format!("{file_len}"))
            .header("Tus-Resumable", "1.0.0")
            .header("Upload-Metadata", file_md)
            .send()
            .await
            .map_err(Error::from)
            .and_then(|r| {
                r.headers()
                    .get("Location")
                    .cloned()
                    .ok_or(Error::io_other("missing Location header"))
            })
            .and_then(|v| v.to_str().map(|s| s.to_string()).map_err(Error::from))
    }

    /// Uploads a file using [TUS resumable upload](https://tus.io).
    ///
    /// Returns the resumable file URL path for further TUS requests.
    pub async fn tus_upload(
        &self,
        file_url: &str,
        file_path: &str,
    ) -> Result<upload_file::Response> {
        let file = std::fs::File::open(file_path)?;
        let file_len = file
            .metadata()
            .map_err(Error::from)
            .map(|m| m.len() as usize)?;

        let mut file_buf = std::io::BufReader::new(file);

        let mut body = Vec::with_capacity(file_len);
        file_buf.read_to_end(&mut body)?;

        let res = self
            .http
            .patch(file_url)
            .header("Authorization", self.auth_header())
            .header("Content-Type", "application/offset+octet-stream")
            .header("Upload-Offset", "0")
            .header("Tus-Resumable", "1.0.0")
            .body(body)
            .send()
            .await?;

        log::debug!("File upload (TUS) response: {res:?}");

        let bytes = res.bytes().await?;
        let body = str::from_utf8(&bytes[..])?;

        log::debug!("File upload (TUS) response body: {body}");

        serde_json::from_str::<upload_file::Response>(body).map_err(Error::from)
    }

    /// Creates and uploads a file using the TUS resumable upload protocol.
    pub async fn tus_create_and_upload(&self, file_path: &str) -> Result<upload_file::Response> {
        let file = std::fs::File::open(file_path)?;
        let path = std::path::Path::new(file_path);

        let file_name = path
            .file_name()
            .and_then(|p| p.to_str())
            .ok_or(Error::io_other("invalid filename for the file path"))?;

        let file_md = self
            .encode_filename(file_name)
            .map(|b64| format!("filename {b64},is_confidential"))?;

        let file_len = file
            .metadata()
            .map_err(Error::from)
            .map(|m| m.len() as usize)?;

        let mut file_buf = std::io::BufReader::new(file);

        let mut body = Vec::with_capacity(file_len);
        file_buf.read_to_end(&mut body)?;

        let res = self
            .http
            .post(self.endpoint(Endpoint::ResumableUploadFile))
            .header("Authorization", self.auth_header())
            .header("Content-Type", "application/offset+octet-stream")
            .header("Upload-Length", format!("{file_len}"))
            .header("Upload-Offset", "0")
            .header("Tus-Resumable", "1.0.0")
            .header("Upload-Metadata", file_md)
            .body(body)
            .send()
            .await?;

        log::debug!("File upload (TUS) response: {res:?}");

        let bytes = res.bytes().await?;
        let body = str::from_utf8(&bytes[..])?;

        log::debug!("File upload (TUS) response body: {body}");

        serde_json::from_str::<upload_file::Response>(body).map_err(Error::from)
    }
}