use crate::error::{Result, SZipError};
use google_cloud_storage::client::Client;
use google_cloud_storage::http::objects::upload::{UploadObjectRequest, UploadType};
use std::future::Future;
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncSeek, AsyncWrite};
use tokio::sync::mpsc;
pub const DEFAULT_CHUNK_SIZE: usize = 8 * 1024 * 1024;
pub const CHUNK_ALIGNMENT: usize = 256 * 1024;
pub struct GCSZipWriter {
upload_tx: mpsc::UnboundedSender<UploadCommand>,
upload_task: Option<tokio::task::JoinHandle<Result<()>>>,
buffer: Vec<u8>,
chunk_size: usize,
position: u64,
shutdown_initiated: bool,
}
enum UploadCommand {
UploadChunk { data: Vec<u8> },
Finalize { final_data: Option<Vec<u8>> },
}
pub struct GCSZipWriterBuilder {
client: Option<Client>,
bucket: String,
object: String,
chunk_size: usize,
}
impl GCSZipWriter {
pub async fn new(
client: Client,
bucket: impl Into<String>,
object: impl Into<String>,
) -> Result<Self> {
Self::builder()
.client(client)
.bucket(bucket)
.object(object)
.build()
.await
}
pub fn builder() -> GCSZipWriterBuilder {
GCSZipWriterBuilder {
client: None,
bucket: String::new(),
object: String::new(),
chunk_size: DEFAULT_CHUNK_SIZE,
}
}
}
impl GCSZipWriterBuilder {
pub fn client(mut self, client: Client) -> Self {
self.client = Some(client);
self
}
pub fn bucket(mut self, bucket: impl Into<String>) -> Self {
self.bucket = bucket.into();
self
}
pub fn object(mut self, object: impl Into<String>) -> Self {
self.object = object.into();
self
}
pub fn chunk_size(mut self, chunk_size: usize) -> Self {
assert!(
chunk_size.is_multiple_of(CHUNK_ALIGNMENT),
"Chunk size must be a multiple of 256KB"
);
self.chunk_size = chunk_size;
self
}
pub async fn build(self) -> Result<GCSZipWriter> {
let client = self
.client
.ok_or_else(|| SZipError::InvalidFormat("GCS client must be set".to_string()))?;
let (tx, rx) = mpsc::unbounded_channel();
let upload_task = tokio::spawn(upload_worker(client, self.bucket, self.object, rx));
Ok(GCSZipWriter {
upload_tx: tx,
upload_task: Some(upload_task),
buffer: Vec::with_capacity(self.chunk_size),
chunk_size: self.chunk_size,
position: 0,
shutdown_initiated: false,
})
}
}
impl AsyncWrite for GCSZipWriter {
fn poll_write(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
self.buffer.extend_from_slice(buf);
self.position += buf.len() as u64;
if self.buffer.len() >= self.chunk_size {
let chunk_size = self.chunk_size;
let data = std::mem::replace(&mut self.buffer, Vec::with_capacity(chunk_size));
if self
.upload_tx
.send(UploadCommand::UploadChunk { data })
.is_err()
{
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"Upload task terminated unexpectedly",
)));
}
}
Poll::Ready(Ok(buf.len()))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
if !self.shutdown_initiated {
self.shutdown_initiated = true;
let final_data = if !self.buffer.is_empty() {
Some(std::mem::take(&mut self.buffer))
} else {
None
};
if self
.upload_tx
.send(UploadCommand::Finalize { final_data })
.is_err()
{
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"Upload task terminated unexpectedly",
)));
}
}
if let Some(task) = self.upload_task.as_mut() {
match Pin::new(task).poll(cx) {
Poll::Ready(Ok(Ok(()))) => Poll::Ready(Ok(())),
Poll::Ready(Ok(Err(e))) => {
Poll::Ready(Err(io::Error::other(format!("GCS upload failed: {}", e))))
}
Poll::Ready(Err(e)) => Poll::Ready(Err(io::Error::other(format!(
"Upload task panicked: {}",
e
)))),
Poll::Pending => Poll::Pending,
}
} else {
Poll::Ready(Ok(()))
}
}
}
impl AsyncSeek for GCSZipWriter {
fn start_seek(self: Pin<&mut Self>, position: io::SeekFrom) -> io::Result<()> {
match position {
io::SeekFrom::Current(0) => Ok(()), _ => Err(io::Error::new(
io::ErrorKind::Unsupported,
"GCS writer does not support seeking",
)),
}
}
fn poll_complete(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<u64>> {
Poll::Ready(Ok(self.position))
}
}
impl Unpin for GCSZipWriter {}
async fn upload_worker(
client: Client,
bucket: String,
object: String,
mut rx: mpsc::UnboundedReceiver<UploadCommand>,
) -> Result<()> {
let mut accumulated_data = Vec::new();
while let Some(cmd) = rx.recv().await {
match cmd {
UploadCommand::UploadChunk { data } => {
accumulated_data.extend_from_slice(&data);
}
UploadCommand::Finalize { final_data } => {
if let Some(data) = final_data {
accumulated_data.extend_from_slice(&data);
}
let upload_type = UploadType::Simple(
google_cloud_storage::http::objects::upload::Media::new(object.clone()),
);
client
.upload_object(
&UploadObjectRequest {
bucket: bucket.clone(),
..Default::default()
},
accumulated_data,
&upload_type,
)
.await
.map_err(|e| {
SZipError::Io(io::Error::other(format!("Failed to upload to GCS: {}", e)))
})?;
break;
}
}
}
Ok(())
}
impl Drop for GCSZipWriter {
fn drop(&mut self) {
}
}