Skip to content

Commit eaaea3d

Browse files
authored
Use S3 multipart uploads for large files (#85)
1 parent a0dd794 commit eaaea3d

2 files changed

Lines changed: 92 additions & 64 deletions

File tree

src/client.rs

Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use crate::{Crate, Status, Tool, Version};
2-
use aws_sdk_s3::client::fluent_builders::PutObject;
3-
use aws_sdk_s3::model::Object;
2+
use aws_sdk_s3::model::{CompletedMultipartUpload, CompletedPart, Object};
43
use backoff::Error;
54
use backoff::ExponentialBackoff;
65
use color_eyre::Result;
@@ -16,6 +15,8 @@ pub struct Client {
1615
tool: Tool,
1716
}
1817

18+
const CHUNK_SIZE: usize = 5 * 1024 * 1024;
19+
1920
impl Client {
2021
pub async fn new(tool: Tool, bucket: &str) -> Result<Self> {
2122
let config = aws_config::load_from_env().await;
@@ -31,22 +32,75 @@ impl Client {
3132
self.tool
3233
}
3334

34-
pub fn put_object(&self) -> PutObject {
35-
self.inner.put_object().bucket(&self.bucket)
35+
pub async fn upload(&self, key: &str, data: &[u8], content_type: &str) -> Result<()> {
36+
retry(|| self._upload(key, data, content_type)).await
3637
}
3738

38-
pub async fn upload_raw(&self, krate: &Crate, data: Vec<u8>) -> Result<()> {
39-
retry(|| {
40-
self.put_object()
41-
.key(self.tool.raw_crate_path(krate))
42-
.body(data.clone().into())
43-
.content_type("text/plain")
39+
async fn _upload(&self, key: &str, data: &[u8], content_type: &str) -> Result<()> {
40+
// S3 has a minimum multipart upload size of 5 MB. If we are below that, we need to use
41+
// PutObject.
42+
if data.len() < CHUNK_SIZE {
43+
self.inner
44+
.put_object()
45+
.bucket(&self.bucket)
46+
.key(key)
47+
.body(data.to_vec().into())
48+
.content_type(content_type)
4449
.send()
45-
})
46-
.await?;
50+
.await?;
51+
return Ok(());
52+
}
53+
54+
let res = self
55+
.inner
56+
.create_multipart_upload()
57+
.bucket(&self.bucket)
58+
.key(key)
59+
.content_type(content_type)
60+
.send()
61+
.await?;
62+
let upload_id = res.upload_id().unwrap();
63+
let mut parts = Vec::new();
64+
for (part_number, chunk) in data.chunks(CHUNK_SIZE).enumerate() {
65+
// part numbers must start at 1
66+
let part_number = part_number as i32 + 1;
67+
let upload_part_res = self
68+
.inner
69+
.upload_part()
70+
.key(key)
71+
.bucket(&self.bucket)
72+
.upload_id(upload_id)
73+
.body(chunk.to_vec().into())
74+
.part_number(part_number)
75+
.send()
76+
.await?;
77+
parts.push(
78+
CompletedPart::builder()
79+
.e_tag(upload_part_res.e_tag.unwrap_or_default())
80+
.part_number(part_number)
81+
.build(),
82+
)
83+
}
84+
let completed_multipart_upload = CompletedMultipartUpload::builder()
85+
.set_parts(Some(parts))
86+
.build();
87+
self.inner
88+
.complete_multipart_upload()
89+
.bucket(&self.bucket)
90+
.key(key)
91+
.multipart_upload(completed_multipart_upload)
92+
.upload_id(upload_id)
93+
.send()
94+
.await?;
95+
4796
Ok(())
4897
}
4998

99+
pub async fn upload_raw(&self, krate: &Crate, data: Vec<u8>) -> Result<()> {
100+
self.upload(&self.tool.raw_crate_path(krate), &data, "text/plain")
101+
.await
102+
}
103+
50104
pub async fn download_raw(&self, krate: &Crate) -> Result<Vec<u8>> {
51105
retry(|| self._download_raw(krate)).await
52106
}
@@ -69,17 +123,8 @@ impl Client {
69123
}
70124

71125
pub async fn upload_html(&self, krate: &Crate, data: Vec<u8>) -> Result<()> {
72-
retry(move || {
73-
self.inner
74-
.put_object()
75-
.bucket(&self.bucket)
76-
.key(self.tool.rendered_crate_path(krate))
77-
.body(data.clone().into())
78-
.content_type("text/html")
79-
.send()
80-
})
81-
.await?;
82-
Ok(())
126+
let key = self.tool.rendered_crate_path(krate);
127+
retry(|| self.upload(&key, &data, "text/html")).await
83128
}
84129

85130
pub async fn get_crate_downloads(&self) -> Result<HashMap<String, Option<u64>>> {
@@ -174,17 +219,8 @@ impl Client {
174219
}
175220

176221
pub async fn upload_landing_page(&self, data: Vec<u8>) -> Result<()> {
177-
retry(move || {
178-
self.inner
179-
.put_object()
180-
.bucket(&self.bucket)
181-
.key(self.tool.landing_page_path())
182-
.body(data.clone().into())
183-
.content_type("text/html")
184-
.send()
185-
})
186-
.await?;
187-
Ok(())
222+
self.upload(self.tool.landing_page_path(), &data, "text/html")
223+
.await
188224
}
189225

190226
pub async fn list_db(&self) -> Result<Option<Object>> {

src/sync.rs

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ pub async fn run(args: Args) -> Result<()> {
2323

2424
log::info!("Uploading the error page");
2525
client
26-
.put_object()
27-
.key(format!("{}/403", args.tool))
28-
.body(ERROR_PAGE.as_bytes().to_vec().into())
29-
.content_type("text/html")
30-
.send()
26+
.upload(
27+
&format!("{}/403", args.tool),
28+
ERROR_PAGE.as_bytes(),
29+
"text/html",
30+
)
3131
.await?;
3232

3333
let should_refresh_db = client
@@ -52,19 +52,11 @@ pub async fn run(args: Args) -> Result<()> {
5252

5353
let serialized = serde_json::to_string(&versions).unwrap();
5454
client
55-
.put_object()
56-
.key("crates.json")
57-
.body(serialized.into_bytes().into())
58-
.content_type("application/json")
59-
.send()
55+
.upload("crates.json", serialized.as_bytes(), "application/json")
6056
.await?;
6157
let serialized = serde_json::to_string(&name_to_downloads).unwrap();
6258
client
63-
.put_object()
64-
.key("downloads.json")
65-
.body(serialized.into_bytes().into())
66-
.content_type("application/json")
67-
.send()
59+
.upload("downloads.json", serialized.as_bytes(), "application/json")
6860
.await?;
6961
name_to_downloads
7062
} else {
@@ -87,11 +79,11 @@ pub async fn run(args: Args) -> Result<()> {
8779

8880
let ub_page = crate::render::render_ub(&crates)?;
8981
client
90-
.put_object()
91-
.key(format!("{}/ub", args.tool))
92-
.body(ub_page.into_bytes().into())
93-
.content_type("text/html")
94-
.send()
82+
.upload(
83+
&format!("{}/ub", args.tool),
84+
ub_page.as_bytes(),
85+
"text/html",
86+
)
9587
.await?;
9688

9789
Ok(())
@@ -167,11 +159,11 @@ async fn sync_all_html(client: Arc<Client>) -> Result<Vec<Crate>> {
167159
.finish()
168160
.unwrap();
169161
client
170-
.put_object()
171-
.key(format!("{}/raw.tar.xz", client.tool()))
172-
.body(raw.into())
173-
.content_type("application/octet-stream")
174-
.send()
162+
.upload(
163+
&format!("{}/raw.tar.xz", client.tool()),
164+
&raw,
165+
"application/octet-stream",
166+
)
175167
.await?;
176168

177169
let rendered: Vec<u8> = Arc::into_inner(all_rendered)
@@ -182,11 +174,11 @@ async fn sync_all_html(client: Arc<Client>) -> Result<Vec<Crate>> {
182174
.finish()
183175
.unwrap();
184176
client
185-
.put_object()
186-
.key(format!("{}/html.tar.xz", client.tool()))
187-
.body(rendered.into())
188-
.content_type("application/octet-stream")
189-
.send()
177+
.upload(
178+
&format!("{}/html.tar.xz", client.tool()),
179+
&rendered,
180+
"application/octet-stream",
181+
)
190182
.await?;
191183

192184
Ok(crates)

0 commit comments

Comments
 (0)