Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

ac-electron: publish-release multipart upload for large binaries

DigitalOcean Spaces returns InvalidArgument 400 on single-part
PutObject once files exceed ~8MB reliably. Switch to @aws-sdk/lib-storage
multipart upload for anything over 8 MB (our universal DMG is ~200MB,
ZIP ~192MB). ACL is applied via a follow-up PutObjectAcl call because
DO Spaces rejects the ACL header on CreateMultipartUpload.

Also pin checksum calculation/validation to "when_required" — SDK v3's
new default sends checksum headers that Spaces rejects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+50 -4
+50 -4
ac-electron/scripts/publish-release.mjs
··· 13 13 import path from "path"; 14 14 import { fileURLToPath } from "url"; 15 15 import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; 16 + import { Upload } from "@aws-sdk/lib-storage"; 16 17 17 18 const __dirname = path.dirname(fileURLToPath(import.meta.url)); 18 19 const distDir = path.join(__dirname, "..", "dist"); ··· 51 52 secretAccessKey: process.env.SPACES_SECRET || "", 52 53 }, 53 54 forcePathStyle: false, 55 + // DO Spaces' S3 impl still expects the legacy XML Checksum behavior; 56 + // AWS SDK v3 otherwise sends checksum headers that Spaces rejects with 57 + // InvalidArgument 400 during multipart. 58 + requestChecksumCalculation: "WHEN_REQUIRED", 59 + responseChecksumValidation: "WHEN_REQUIRED", 54 60 }); 55 61 56 62 // Files to upload: manifests + binaries matching current version ··· 106 112 107 113 async function uploadFile(file) { 108 114 const key = PREFIX + file.name; 109 - const body = fs.readFileSync(file.path); 115 + const size = fs.statSync(file.path).size; 110 116 const contentType = file.isManifest 111 117 ? "text/yaml" 112 118 : "application/octet-stream"; ··· 114 120 ? "no-cache, no-store, must-revalidate" 115 121 : "public, max-age=31536000"; 116 122 117 - console.log(` Uploading ${file.name} (${(body.length / 1024 / 1024).toFixed(1)} MB) → s3://${BUCKET}/${key}`); 123 + console.log(` Uploading ${file.name} (${(size / 1024 / 1024).toFixed(1)} MB) → s3://${BUCKET}/${key}`); 118 124 119 125 if (dryRun) return; 120 126 127 + // Use multipart for anything over 8 MB — DO Spaces frequently times out 128 + // single-part PutObject on large files (e.g. the 200 MB DMG). DO Spaces 129 + // rejects the ACL header on CreateMultipartUpload — we set ACL after. 130 + if (size > 8 * 1024 * 1024) { 131 + const upload = new Upload({ 132 + client: s3, 133 + params: { 134 + Bucket: BUCKET, 135 + Key: key, 136 + Body: fs.createReadStream(file.path), 137 + ContentType: contentType, 138 + CacheControl: cacheControl, 139 + }, 140 + queueSize: 4, 141 + partSize: 16 * 1024 * 1024, 142 + leavePartsOnError: false, 143 + }); 144 + upload.on("httpUploadProgress", (p) => { 145 + if (p.total) { 146 + const pct = ((p.loaded / p.total) * 100).toFixed(0); 147 + process.stdout.write(`\r ${pct}% (${(p.loaded / 1024 / 1024).toFixed(1)}/${(p.total / 1024 / 1024).toFixed(1)} MB)`); 148 + } 149 + }); 150 + await upload.done(); 151 + process.stdout.write("\n"); 152 + // Apply public-read ACL as a follow-up (DO Spaces requires this path 153 + // when the initial multipart request can't carry ACL). 154 + const { PutObjectAclCommand } = await import("@aws-sdk/client-s3"); 155 + await s3.send(new PutObjectAclCommand({ 156 + Bucket: BUCKET, 157 + Key: key, 158 + ACL: "public-read", 159 + })); 160 + return; 161 + } 162 + 121 163 await s3.send( 122 164 new PutObjectCommand({ 123 165 Bucket: BUCKET, 124 166 Key: key, 125 - Body: body, 167 + Body: fs.readFileSync(file.path), 126 168 ContentType: contentType, 127 169 CacheControl: cacheControl, 128 170 ACL: "public-read", ··· 214 256 } 215 257 216 258 main().catch((err) => { 217 - console.error("\nPublish failed:", err.message); 259 + console.error("\nPublish failed:", err.name, "-", err.message); 260 + if (err.$metadata) console.error(" http:", err.$metadata.httpStatusCode); 261 + if (err.Code) console.error(" code:", err.Code); 262 + if (err.cause) console.error(" cause:", err.cause?.message || err.cause); 263 + if (err.stack) console.error(err.stack); 218 264 process.exit(1); 219 265 });