s3-wire 0.1.1

An async, streaming S3-compatible client for Rust
Documentation
use std::path::{Path, PathBuf};

use bytes::{Bytes, BytesMut};
use sha2::{Digest as _, Sha256};
use tokio::fs::File;
use tokio::io::{AsyncReadExt as _, AsyncSeekExt as _, AsyncWriteExt as _};

use crate::error::S3Error;

pub(crate) struct FileSnapshot {
    path: tempfile::TempPath,
    length: u64,
    sha256: Option<[u8; 32]>,
}

impl FileSnapshot {
    pub(crate) async fn create(path: PathBuf, hash: bool) -> Result<Self, S3Error> {
        let mut input = File::open(&path).await.map_err(S3Error::transport)?;
        let metadata = input.metadata().await.map_err(S3Error::transport)?;
        if !metadata.is_file() {
            return Err(S3Error::configuration(
                "upload path must identify a regular file",
            ));
        }

        let snapshot = tempfile::NamedTempFile::new().map_err(S3Error::transport)?;
        let (snapshot_file, snapshot_path) = snapshot.into_parts();
        let mut output = File::from_std(snapshot_file);
        let mut hasher = hash.then(Sha256::new);
        let mut length = 0_u64;
        let mut buffer = vec![0_u8; 64 * 1024];
        loop {
            let read = input.read(&mut buffer).await.map_err(S3Error::transport)?;
            if read == 0 {
                break;
            }
            length = length
                .checked_add(u64::try_from(read).map_err(|_| {
                    S3Error::integrity("upload file chunk length does not fit in u64")
                })?)
                .ok_or_else(|| S3Error::integrity("upload file length overflow"))?;
            if let Some(hasher) = &mut hasher {
                hasher.update(&buffer[..read]);
            }
            output
                .write_all(&buffer[..read])
                .await
                .map_err(S3Error::transport)?;
        }
        output.flush().await.map_err(S3Error::transport)?;
        drop(output);

        make_read_only(&snapshot_path)?;
        let actual = tokio::fs::metadata(&snapshot_path)
            .await
            .map_err(S3Error::transport)?
            .len();
        if actual != length {
            return Err(S3Error::integrity(
                "upload snapshot length changed while preparing",
            ));
        }

        Ok(Self {
            path: snapshot_path,
            length,
            sha256: hasher.map(|hasher| hasher.finalize().into()),
        })
    }

    pub(crate) const fn length(&self) -> u64 {
        self.length
    }

    pub(crate) fn sha256(&self) -> Option<[u8; 32]> {
        self.sha256
    }

    pub(crate) async fn open(&self) -> Result<File, S3Error> {
        File::open(&self.path).await.map_err(S3Error::transport)
    }

    pub(crate) async fn read_range(&self, offset: u64, length: usize) -> Result<Bytes, S3Error> {
        let mut file = self.open().await?;
        file.seek(std::io::SeekFrom::Start(offset))
            .await
            .map_err(S3Error::transport)?;
        let mut bytes = BytesMut::zeroed(length);
        file.read_exact(&mut bytes)
            .await
            .map_err(S3Error::transport)?;
        Ok(bytes.freeze())
    }
}

fn make_read_only(path: &Path) -> Result<(), S3Error> {
    let mut permissions = std::fs::metadata(path)
        .map_err(S3Error::transport)?
        .permissions();
    permissions.set_readonly(true);
    std::fs::set_permissions(path, permissions).map_err(S3Error::transport)
}