use anyhow::{Context, Result, bail};
use aws_sdk_s3::{Client, types::Object};
use buildstructor::buildstructor;
use tokio::{
io::AsyncBufReadExt,
select,
sync::mpsc::{UnboundedReceiver, UnboundedSender},
};
use tokio_util::sync::CancellationToken;
use tracing::{error, info, info_span, instrument, trace, warn};
use url::Url;
#[derive(Debug, Clone)]
pub struct Worker {
client: Client,
objects_tx: UnboundedSender<(String, Vec<Object>)>,
bytes_tx: UnboundedSender<String>,
cancel: CancellationToken,
}
#[buildstructor]
impl Worker {
#[builder]
pub fn new(
client: Client,
objects_tx: UnboundedSender<(String, Vec<Object>)>,
bytes_tx: UnboundedSender<String>,
cancel: CancellationToken,
) -> Self {
Self {
client,
objects_tx,
bytes_tx,
cancel,
}
}
#[cfg_attr(feature = "tracing", instrument(level = "debug", err, skip_all))]
pub async fn work(
self,
mut sources_rx: UnboundedReceiver<Url>,
mut contents_rx: UnboundedReceiver<(String, Object)>,
) -> Result<()> {
loop {
select! {
biased;
() = self.cancel.cancelled() => {
#[cfg(feature = "tracing")]
info!("cancellation received; shutting down");
return Ok(());
}
Some((bucket, object)) = contents_rx.recv() => {
get_object(&self.client, bucket, object, self.bytes_tx.clone()).await?;
},
Some(url) = sources_rx.recv() => {
list_objects(&self.client, url, self.objects_tx.clone()).await?;
},
}
}
}
}
#[cfg_attr(feature = "tracing",
instrument(level = "TRACE", err, skip(client, tx), fields(url = %url))
)]
async fn list_objects(
client: &Client,
url: Url,
tx: UnboundedSender<(String, Vec<Object>)>,
) -> Result<()> {
let bucket = url.host().unwrap().to_string();
let prefix = url.path().trim_start_matches('/');
#[cfg(feature = "tracing")]
trace!(%bucket, %prefix);
let mut stream = client
.list_objects_v2()
.bucket(&bucket)
.prefix(prefix)
.into_paginator()
.send();
while let Some(page) = stream.next().await {
let page = page.context("list_objects_v2")?;
let Some(objects) = page.contents else {
#[cfg(feature = "tracing")]
warn!(%bucket, %prefix, "no objects");
return Ok(());
};
tx.send((bucket.clone(), objects))?;
}
Ok(())
}
#[cfg_attr(
feature = "tracing",
instrument(level = "TRACE", ret, err, skip_all, fields(
bucket = %bucket,
key = object.key,
))
)]
async fn get_object(
client: &Client,
bucket: String,
object: Object,
tx: UnboundedSender<String>,
) -> Result<()> {
let object = match client
.get_object()
.bucket(bucket)
.set_key(object.key().map(Into::into))
.send()
.await
{
Ok(o) => o,
Err(e) => {
#[cfg(feature = "tracing")]
error!(?e, "get_object");
bail!(e);
},
};
let reader = object.body.into_async_read();
let mut lines = reader.lines();
let span = info_span!("send.lines");
while let Some(line) = lines.next_line().await? {
let _guard = span.enter();
tx.send(line)?;
}
Ok(())
}