| 5 |
5 |
|
use aws_config::BehaviorVersion;
|
| 6 |
6 |
|
use aws_sdk_s3::config::{Credentials, Region};
|
| 7 |
7 |
|
use aws_sdk_s3::presigning::PresigningConfig;
|
|
8 |
+ |
use aws_sdk_s3::types::{CorsConfiguration, CorsRule};
|
| 8 |
9 |
|
use aws_sdk_s3::Client;
|
| 9 |
10 |
|
use std::str::FromStr;
|
| 10 |
11 |
|
use std::time::Duration;
|
| 50 |
51 |
|
"application/zip",
|
| 51 |
52 |
|
"application/x-zip-compressed",
|
| 52 |
53 |
|
"application/x-apple-diskimage",
|
|
54 |
+ |
"application/x-diskcopy",
|
| 53 |
55 |
|
"application/x-msi",
|
| 54 |
56 |
|
"application/x-ole-storage",
|
| 55 |
57 |
|
"application/gzip",
|
| 155 |
157 |
|
}
|
| 156 |
158 |
|
|
| 157 |
159 |
|
impl S3Client {
|
| 158 |
|
- |
/// Create a new S3 client from storage configuration
|
| 159 |
|
- |
pub async fn new(config: &StorageConfig) -> Result<Self> {
|
|
160 |
+ |
/// Create a new S3 client from storage configuration.
|
|
161 |
+ |
///
|
|
162 |
+ |
/// Configures CORS on the bucket at startup so browser PUT uploads to
|
|
163 |
+ |
/// presigned URLs work without manual bucket configuration.
|
|
164 |
+ |
pub async fn new(config: &StorageConfig, host_url: &str) -> Result<Self> {
|
| 160 |
165 |
|
let credentials = Credentials::new(
|
| 161 |
166 |
|
&config.access_key,
|
| 162 |
167 |
|
&config.secret_key,
|
| 175 |
180 |
|
|
| 176 |
181 |
|
let client = Client::from_conf(s3_config);
|
| 177 |
182 |
|
|
| 178 |
|
- |
Ok(S3Client {
|
|
183 |
+ |
let s3 = S3Client {
|
| 179 |
184 |
|
client,
|
| 180 |
185 |
|
bucket: config.bucket.clone(),
|
| 181 |
|
- |
})
|
|
186 |
+ |
};
|
|
187 |
+ |
|
|
188 |
+ |
s3.configure_cors(host_url).await;
|
|
189 |
+ |
|
|
190 |
+ |
Ok(s3)
|
|
191 |
+ |
}
|
|
192 |
+ |
|
|
193 |
+ |
/// Ensure the S3 bucket has CORS configured for browser uploads.
|
|
194 |
+ |
async fn configure_cors(&self, host_url: &str) {
|
|
195 |
+ |
let origin = host_url.trim_end_matches('/').to_string();
|
|
196 |
+ |
let rule = match CorsRule::builder()
|
|
197 |
+ |
.allowed_origins(&origin)
|
|
198 |
+ |
.allowed_methods("PUT")
|
|
199 |
+ |
.allowed_methods("GET")
|
|
200 |
+ |
.allowed_methods("HEAD")
|
|
201 |
+ |
.allowed_headers("Content-Type")
|
|
202 |
+ |
.allowed_headers("Content-Disposition")
|
|
203 |
+ |
.expose_headers("ETag")
|
|
204 |
+ |
.max_age_seconds(3600)
|
|
205 |
+ |
.build()
|
|
206 |
+ |
{
|
|
207 |
+ |
Ok(r) => r,
|
|
208 |
+ |
Err(e) => {
|
|
209 |
+ |
tracing::warn!("Failed to build CORS rule: {}", e);
|
|
210 |
+ |
return;
|
|
211 |
+ |
}
|
|
212 |
+ |
};
|
|
213 |
+ |
|
|
214 |
+ |
let cors_config = match CorsConfiguration::builder().cors_rules(rule).build() {
|
|
215 |
+ |
Ok(c) => c,
|
|
216 |
+ |
Err(e) => {
|
|
217 |
+ |
tracing::warn!("Failed to build CORS config: {}", e);
|
|
218 |
+ |
return;
|
|
219 |
+ |
}
|
|
220 |
+ |
};
|
|
221 |
+ |
|
|
222 |
+ |
match self
|
|
223 |
+ |
.client
|
|
224 |
+ |
.put_bucket_cors()
|
|
225 |
+ |
.bucket(&self.bucket)
|
|
226 |
+ |
.cors_configuration(cors_config)
|
|
227 |
+ |
.send()
|
|
228 |
+ |
.await
|
|
229 |
+ |
{
|
|
230 |
+ |
Ok(_) => tracing::info!("S3 bucket CORS configured for {}", origin),
|
|
231 |
+ |
Err(e) => tracing::warn!("Failed to configure S3 CORS (uploads may fail): {}", e),
|
|
232 |
+ |
}
|
| 182 |
233 |
|
}
|
| 183 |
234 |
|
|
| 184 |
235 |
|
/// Generate a consistent S3 key for an object
|