use crate::errors::{AwsError, RustDistError, RustDistResult};
use aws_config::AppName;
use aws_sdk_s3::middleware::DefaultMiddleware;
use aws_sdk_s3::model::Object;
use aws_sdk_s3::operation::ListObjectsV2;
use aws_sdk_s3::output::ListObjectsV2Output;
use aws_sdk_s3::{Config, Region};
use aws_sig_auth::signer::OperationSigningConfig;
use aws_sig_auth::signer::SigningRequirements;
use aws_smithy_client::erase::DynConnector;
use rust_releases_io::{base_cache_dir, is_stale, Document};
use std::convert::{TryFrom, TryInto};
use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use std::time::Duration;
const RUST_DIST_REGION: Region = Region::from_static("us-west-1");
const RUST_DIST_BUCKET: &str = "static-rust-lang-org";
const OBJECT_PREFIX: &str = "dist/rustc-";
const SOURCE_CACHE_DIR: &str = "source_dist_index";
const OUTPUT_PATH: &str = "dist_static-rust-lang-org.txt";
const REQUEST_SIZE: i32 = 1000;
const TIMEOUT: Duration = Duration::from_secs(86_400);
#[derive(Clone, Debug, Eq, PartialEq)]
enum ChunkState {
Offset(String),
Complete,
}
trait ChunkClient {
fn download_chunk(
&self,
offset: Option<impl Into<String>>,
to: &mut impl Write,
) -> RustDistResult<ChunkState>;
fn download(&self, to: &mut impl Write) -> RustDistResult<()>;
}
struct Client {
#[allow(dead_code)]
aws_config: Config,
aws_s3_client: SmithyClient,
runtime: tokio::runtime::Runtime,
}
impl Client {
pub fn try_default() -> RustDistResult<Self> {
let runtime = tokio::runtime::Runtime::new()?;
let app_name = AppName::new("rust-releases+`github|foresterre|rust-releases`")
.map_err(AwsError::InvalidAppName)?;
let aws_config = aws_sdk_s3::Config::builder()
.app_name(app_name)
.region(RUST_DIST_REGION)
.build();
let aws_s3_client = aws_smithy_client::Builder::dyn_https()
.middleware(aws_sdk_s3::middleware::DefaultMiddleware::new())
.build();
Ok(Self {
aws_config,
aws_s3_client,
runtime,
})
}
}
type SmithyClient = aws_smithy_client::Client<DynConnector, DefaultMiddleware>;
async fn list_objects(
client: &SmithyClient,
conf: &Config,
offset: Option<impl Into<String>>,
) -> Result<ListObjectsV2Output, AwsError> {
let input = ListObjectsV2::builder()
.bucket(RUST_DIST_BUCKET)
.max_keys(REQUEST_SIZE)
.set_start_after(offset.map(Into::into))
.prefix(OBJECT_PREFIX)
.build()
.map_err(|_| AwsError::ListObjectsBuildOperationInput)?;
let mut operation = input
.make_operation(conf)
.await
.map_err(|_| AwsError::ListObjectsMakeOperation)?;
{
let mut properties = operation.properties_mut();
let signing_config = properties
.get_mut::<OperationSigningConfig>()
.ok_or(AwsError::DisableSigning)?;
signing_config.signing_requirements = SigningRequirements::Disabled;
}
client
.call(operation)
.await
.map_err(|e| AwsError::ListObjectsError(Box::new(e)))
}
impl ChunkClient for Client {
fn download_chunk(
&self,
offset: Option<impl Into<String>>,
to: &mut impl Write,
) -> RustDistResult<ChunkState> {
let raw =
self.runtime
.block_on(list_objects(&self.aws_s3_client, &self.aws_config, offset))?;
let objects = raw.contents.ok_or(RustDistError::ChunkMetadataMissing)?;
let state = match write_objects(to, &objects) {
Some(key) => ChunkState::Offset(key),
None => ChunkState::Complete,
};
Ok(state)
}
fn download(&self, to: &mut impl Write) -> RustDistResult<()> {
let mut offset = None;
while let Ok(ChunkState::Offset(next_offset)) = self.download_chunk(offset.to_owned(), to) {
offset = Some(next_offset);
}
Ok(())
}
}
struct BiWriter<T1: Write, T2: Write> {
buffer1: T1,
buffer2: T2,
}
impl<T1: Write, T2: Write> Write for BiWriter<T1, T2> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let _ = self.buffer1.write(buf)?;
self.buffer2.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.buffer1.flush()?;
self.buffer2.flush()
}
}
impl BiWriter<BufWriter<Vec<u8>>, BufWriter<File>> {
fn try_from_path(path: impl AsRef<Path>) -> RustDistResult<Self> {
let memory = BufWriter::new(Vec::new());
let file = BufWriter::new(OpenOptions::new().create(true).append(true).open(path)?);
Ok(Self {
buffer1: memory,
buffer2: file,
})
}
fn into_owned_memory(mut self) -> RustDistResult<Vec<u8>> {
self.flush()?;
self.buffer1.into_inner().map_err(RustDistError::from)
}
}
struct PersistingMemCache<P: AsRef<Path>> {
cache_file: P,
buffer: BiWriter<BufWriter<Vec<u8>>, BufWriter<File>>,
}
impl<P: AsRef<Path>> PersistingMemCache<P> {
fn try_from_path(path: P) -> RustDistResult<Self> {
let buffer = BiWriter::try_from_path(path.as_ref())?;
Ok(Self {
cache_file: path,
buffer,
})
}
}
impl<P: AsRef<Path>> Write for PersistingMemCache<P> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.buffer.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.buffer.flush()
}
}
impl<P: AsRef<Path>> TryFrom<PersistingMemCache<P>> for Document {
type Error = RustDistError;
fn try_from(value: PersistingMemCache<P>) -> Result<Self, Self::Error> {
let path = value.cache_file.as_ref().to_path_buf();
let mem = value.buffer.into_owned_memory()?;
Ok(Document::RemoteCached(path, mem))
}
}
fn check_cache(output_path: &Path) -> RustDistResult<Option<Document>> {
if output_path.is_file() && !is_stale(&output_path, TIMEOUT)? {
Ok(Some(Document::LocalPath(output_path.to_path_buf())))
} else {
let parent = output_path.parent().ok_or_else(|| {
let error: std::io::Error = std::io::ErrorKind::NotFound.into();
error
})?;
std::fs::create_dir_all(parent)?;
Ok(None)
}
}
fn cache_file_path() -> RustDistResult<PathBuf> {
let mut base = base_cache_dir()?;
base.push(SOURCE_CACHE_DIR);
base.push(OUTPUT_PATH);
Ok(base)
}
pub(in crate) fn fetch() -> RustDistResult<Document> {
let output_path = cache_file_path()?;
if let Some(cached) = check_cache(&output_path)? {
return Ok(cached);
}
let client = Client::try_default()?;
let mut buffer = PersistingMemCache::try_from_path(output_path)?;
client.download(&mut buffer)?;
buffer.try_into()
}
fn write_objects(buffer: &mut impl std::io::Write, objects: &[Object]) -> Option<String> {
for object in objects {
if let Some(key) = object.key.as_deref() {
let _ = buffer.write(format!("{}\n", key).as_bytes());
}
}
let _ = buffer.flush();
objects
.last()
.and_then(|obj| obj.key.as_ref().map(|o| o.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fetch_meta_manifest() {
__internal_dl_test!({
let meta = fetch();
assert!(meta.is_ok());
})
}
}