Skip to main content

max / makenotwork

Fix DMG upload MIME type, configure S3 CORS on startup - Add application/x-diskcopy to allowed download MIME types (macOS sends this for .dmg files instead of application/x-apple-diskimage) - Configure S3 bucket CORS on startup (PutBucketCors) so browser PUT uploads to presigned URLs work without manual bucket configuration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-03-29 00:34 UTC
Commit: 092d3c2220e430382f2d95d3968fe272d787b0c9
Parent: 6e3ff43
2 files changed, +57 insertions, -6 deletions
@@ -89,7 +89,7 @@ async fn main() {
89 89 // Initialize S3 client if storage is configured
90 90 let s3: Option<std::sync::Arc<dyn makenotwork::storage::StorageBackend>> =
91 91 if let Some(ref storage_config) = config.storage {
92 - match S3Client::new(storage_config).await {
92 + match S3Client::new(storage_config, &config.host_url).await {
93 93 Ok(client) => {
94 94 tracing::info!(bucket = %storage_config.bucket, "S3 storage initialized");
95 95 Some(std::sync::Arc::new(client))
@@ -107,7 +107,7 @@ async fn main() {
107 107 // Initialize SyncKit blob S3 client if configured (separate bucket)
108 108 let synckit_s3: Option<std::sync::Arc<dyn makenotwork::storage::StorageBackend>> =
109 109 if let Some(ref synckit_storage_config) = config.synckit_storage {
110 - match S3Client::new(synckit_storage_config).await {
110 + match S3Client::new(synckit_storage_config, &config.host_url).await {
111 111 Ok(client) => {
112 112 tracing::info!(bucket = %synckit_storage_config.bucket, "SyncKit blob storage initialized");
113 113 Some(std::sync::Arc::new(client))
@@ -5,6 +5,7 @@
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,6 +51,7 @@ const ALLOWED_DOWNLOAD_MIMES: &[&str] = &[
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,8 +157,11 @@ pub struct S3Client {
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,10 +180,56 @@ impl S3Client {
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