Upload comic images to S3 and update database with S3 URLs

This commit is contained in:
Riccardo Giorato
2025-12-26 13:33:38 +01:00
parent 50ae57ec9b
commit a2dd20ccdb
4 changed files with 271 additions and 12 deletions
+9 -4
View File
@@ -10,6 +10,7 @@ import {
} from "@/lib/db-actions";
import { freeTierRateLimit } from "@/lib/rate-limit";
import { COMIC_STYLES } from "@/lib/constants";
import { uploadImageToS3 } from "@/lib/s3-upload";
const NEW_MODEL = false;
@@ -243,9 +244,13 @@ COMPOSITION:
const imageUrl = response.data[0].url;
// Update page in database
// Upload image to S3 for permanent storage
const s3Key = `${storyId || story!.id}/page-${page.pageNumber}-${Date.now()}.jpg`;
const s3ImageUrl = await uploadImageToS3(imageUrl, s3Key);
// Update page in database with S3 URL
try {
await updatePage(page.id, imageUrl);
await updatePage(page.id, s3ImageUrl);
} catch (dbError) {
console.error("Error updating page in database:", dbError);
return NextResponse.json(
@@ -255,9 +260,9 @@ COMPOSITION:
}
const responseData = storyId
? { imageUrl, pageId: page.id, pageNumber: page.pageNumber }
? { imageUrl: s3ImageUrl, pageId: page.id, pageNumber: page.pageNumber }
: {
imageUrl,
imageUrl: s3ImageUrl,
storyId: story!.id,
storySlug: story!.slug,
pageId: page.id,
+46
View File
@@ -0,0 +1,46 @@
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
if (!process.env.S3_UPLOAD_KEY || !process.env.S3_UPLOAD_SECRET || !process.env.S3_UPLOAD_BUCKET || !process.env.S3_UPLOAD_REGION) {
throw new Error("Missing required S3 environment variables");
}
const s3Client = new S3Client({
region: process.env.S3_UPLOAD_REGION || "us-east-1",
credentials: {
accessKeyId: process.env.S3_UPLOAD_KEY!,
secretAccessKey: process.env.S3_UPLOAD_SECRET!,
},
});
export async function uploadImageToS3(
imageUrl: string,
key: string
): Promise<string> {
try {
const response = await fetch(imageUrl);
if (!response.ok) {
throw new Error(`Failed to fetch image: ${response.statusText}`);
}
const imageBuffer = Buffer.from(await response.arrayBuffer());
const command = new PutObjectCommand({
Bucket: process.env.S3_UPLOAD_BUCKET!,
Key: `comics/${key}`,
Body: imageBuffer,
ContentType: "image/jpeg",
Metadata: {
"app-name": "make-comics",
"type": "comic-page",
},
});
await s3Client.send(command);
const publicUrl = `https://${process.env.S3_UPLOAD_BUCKET}.s3.${process.env.S3_UPLOAD_REGION}.amazonaws.com/comics/${key}`;
return publicUrl;
} catch (error) {
console.error("Error uploading to S3:", error);
throw new Error("Failed to upload image to S3");
}
}
+1
View File
@@ -9,6 +9,7 @@
"start": "next start"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.958.0",
"@clerk/nextjs": "^6.36.5",
"@hookform/resolvers": "^3.10.0",
"@neondatabase/serverless": "^1.0.2",
+215 -8
View File
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@aws-sdk/client-s3':
specifier: ^3.958.0
version: 3.958.0
'@clerk/nextjs':
specifier: ^6.36.5
version: 6.36.5(next@16.1.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -240,14 +243,18 @@ packages:
'@aws-crypto/util@5.2.0':
resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==}
'@aws-sdk/client-s3@3.957.0':
resolution: {integrity: sha512-UvKQ9CpqzJxMjSjFpBaeWFAnR2sQChQH2jJEhsCFkAvrz1KDrU7sjOqUAPUH+ytsP5xghx1AZhfK2rVE/+JS0Q==}
'@aws-sdk/client-s3@3.958.0':
resolution: {integrity: sha512-ol8Sw37AToBWb6PjRuT/Wu40SrrZSA0N4F7U3yTkjUNX0lirfO1VFLZ0hZtZplVJv8GNPITbiczxQ8VjxESXxg==}
engines: {node: '>=18.0.0'}
'@aws-sdk/client-sso@3.957.0':
resolution: {integrity: sha512-iRdRjd+IpOogqRPt8iNRcg30J53z4rRfMviGwpKgsEa/fx3inCUPOuca3Ap7ZDES0atnEg3KGSJ3V/NQiEJ4BA==}
engines: {node: '>=18.0.0'}
'@aws-sdk/client-sso@3.958.0':
resolution: {integrity: sha512-6qNCIeaMzKzfqasy2nNRuYnMuaMebCcCPP4J2CVGkA8QYMbIVKPlkn9bpB20Vxe6H/r3jtCCLQaOJjVTx/6dXg==}
engines: {node: '>=18.0.0'}
'@aws-sdk/client-sts@3.957.0':
resolution: {integrity: sha512-rqH0blCu4LrbW3wiFxPBAttqz81NrJk1fAxem869FyRwDXtNVDFgQeVNLyUKpf340O2uRiES/PkCMb240T86PQ==}
engines: {node: '>=18.0.0'}
@@ -272,14 +279,26 @@ packages:
resolution: {integrity: sha512-YuoZmIeE91YIeUfihh8SiSu546KtTvU+4rG5SaL30U9+nGq6P11GRRgqF0ANUyRseLC9ONHt+utar4gbO3++og==}
engines: {node: '>=18.0.0'}
'@aws-sdk/credential-provider-ini@3.958.0':
resolution: {integrity: sha512-u7twvZa1/6GWmPBZs6DbjlegCoNzNjBsMS/6fvh5quByYrcJr/uLd8YEr7S3UIq4kR/gSnHqcae7y2nL2bqZdg==}
engines: {node: '>=18.0.0'}
'@aws-sdk/credential-provider-login@3.957.0':
resolution: {integrity: sha512-XcD5NEQDWYk8B4gs89bkwf2d+DNF8oS2NR5RoHJEbX4l8KErVATUjpEYVn6/rAFEktungxlYTnQ5wh0cIQvP5w==}
engines: {node: '>=18.0.0'}
'@aws-sdk/credential-provider-login@3.958.0':
resolution: {integrity: sha512-sDwtDnBSszUIbzbOORGh5gmXGl9aK25+BHb4gb1aVlqB+nNL2+IUEJA62+CE55lXSH8qXF90paivjK8tOHTwPA==}
engines: {node: '>=18.0.0'}
'@aws-sdk/credential-provider-node@3.957.0':
resolution: {integrity: sha512-b9FT/7BQcJ001w+3JbTiJXfxHrWvPb7zDvvC1i1FKcNOvyCt3BGu04n4nO/b71a3iBnbfBXI89hCIZQsuLcEgw==}
engines: {node: '>=18.0.0'}
'@aws-sdk/credential-provider-node@3.958.0':
resolution: {integrity: sha512-vdoZbNG2dt66I7EpN3fKCzi6fp9xjIiwEA/vVVgqO4wXCGw8rKPIdDUus4e13VvTr330uQs2W0UNg/7AgtquEQ==}
engines: {node: '>=18.0.0'}
'@aws-sdk/credential-provider-process@3.957.0':
resolution: {integrity: sha512-/KIz9kadwbeLy6SKvT79W81Y+hb/8LMDyeloA2zhouE28hmne+hLn0wNCQXAAupFFlYOAtZR2NTBs7HBAReJlg==}
engines: {node: '>=18.0.0'}
@@ -288,10 +307,18 @@ packages:
resolution: {integrity: sha512-gTLPJFOkGtn3tVGglRhCar2oOobK1YctZRAT8nfJr17uaSRoAP46zIIHNYBZZUMqImb0qAHD9Ugm+Zd9sIqxyA==}
engines: {node: '>=18.0.0'}
'@aws-sdk/credential-provider-sso@3.958.0':
resolution: {integrity: sha512-CBYHJ5ufp8HC4q+o7IJejCUctJXWaksgpmoFpXerbjAso7/Fg7LLUu9inXVOxlHKLlvYekDXjIUBXDJS2WYdgg==}
engines: {node: '>=18.0.0'}
'@aws-sdk/credential-provider-web-identity@3.957.0':
resolution: {integrity: sha512-x17xMeD7c+rKEsWachGIMifACqkugskrETWz18QDWismFcrmUuOcZu5rUa8s9y1pnITLKUQ1xU/qDLPH52jLlA==}
engines: {node: '>=18.0.0'}
'@aws-sdk/credential-provider-web-identity@3.958.0':
resolution: {integrity: sha512-dgnvwjMq5Y66WozzUzxNkCFap+umHUtqMMKlr8z/vl9NYMLem/WUbWNpFFOVFWquXikc+ewtpBMR4KEDXfZ+KA==}
engines: {node: '>=18.0.0'}
'@aws-sdk/lib-storage@3.957.0':
resolution: {integrity: sha512-qTkYG37Zi4wcoF45tx9e/jNxNax59ZqjfLvEzVL84QzICYU4Jz2VONmm56UUYstaNHSBwTtNOjQV3gAKjJ+aCw==}
engines: {node: '>=18.0.0'}
@@ -342,6 +369,10 @@ packages:
resolution: {integrity: sha512-PZUFtaUTSZWO+mbgQGWSiwz3EqedsuKNb7Xoxjzh5rfJE352DD4/jScQEhVPxvdLw62IK9b5UDu5kZlxzBs9Ow==}
engines: {node: '>=18.0.0'}
'@aws-sdk/nested-clients@3.958.0':
resolution: {integrity: sha512-/KuCcS8b5TpQXkYOrPLYytrgxBhv81+5pChkOlhegbeHttjM69pyUpQVJqyfDM/A7wPLnDrzCAnk4zaAOkY0Nw==}
engines: {node: '>=18.0.0'}
'@aws-sdk/region-config-resolver@3.957.0':
resolution: {integrity: sha512-V8iY3blh8l2iaOqXWW88HbkY5jDoWjH56jonprG/cpyqqCnprvpMUZWPWYJoI8rHRf2bqzZeql1slxG6EnKI7A==}
engines: {node: '>=18.0.0'}
@@ -358,6 +389,10 @@ packages:
resolution: {integrity: sha512-oSwo3BZ6gcvhjTg036V0UQmtENUeNwfCU35iDckX961CdI1alQ3TKRWLzKrwvXCbrOx+bZsuA1PHsTbNhI/+Fw==}
engines: {node: '>=18.0.0'}
'@aws-sdk/token-providers@3.958.0':
resolution: {integrity: sha512-UCj7lQXODduD1myNJQkV+LYcGYJ9iiMggR8ow8Hva1g3A/Na5imNXzz6O67k7DAee0TYpy+gkNw+SizC6min8Q==}
engines: {node: '>=18.0.0'}
'@aws-sdk/types@3.957.0':
resolution: {integrity: sha512-wzWC2Nrt859ABk6UCAVY/WYEbAd7FjkdrQL6m24+tfmWYDNRByTJ9uOgU/kw9zqLCAwb//CPvrJdhqjTznWXAg==}
engines: {node: '>=18.0.0'}
@@ -2838,13 +2873,13 @@ snapshots:
'@smithy/util-utf8': 2.3.0
tslib: 2.8.1
'@aws-sdk/client-s3@3.957.0':
'@aws-sdk/client-s3@3.958.0':
dependencies:
'@aws-crypto/sha1-browser': 5.2.0
'@aws-crypto/sha256-browser': 5.2.0
'@aws-crypto/sha256-js': 5.2.0
'@aws-sdk/core': 3.957.0
'@aws-sdk/credential-provider-node': 3.957.0
'@aws-sdk/credential-provider-node': 3.958.0
'@aws-sdk/middleware-bucket-endpoint': 3.957.0
'@aws-sdk/middleware-expect-continue': 3.957.0
'@aws-sdk/middleware-flexible-checksums': 3.957.0
@@ -2941,6 +2976,49 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/client-sso@3.958.0':
dependencies:
'@aws-crypto/sha256-browser': 5.2.0
'@aws-crypto/sha256-js': 5.2.0
'@aws-sdk/core': 3.957.0
'@aws-sdk/middleware-host-header': 3.957.0
'@aws-sdk/middleware-logger': 3.957.0
'@aws-sdk/middleware-recursion-detection': 3.957.0
'@aws-sdk/middleware-user-agent': 3.957.0
'@aws-sdk/region-config-resolver': 3.957.0
'@aws-sdk/types': 3.957.0
'@aws-sdk/util-endpoints': 3.957.0
'@aws-sdk/util-user-agent-browser': 3.957.0
'@aws-sdk/util-user-agent-node': 3.957.0
'@smithy/config-resolver': 4.4.5
'@smithy/core': 3.20.0
'@smithy/fetch-http-handler': 5.3.8
'@smithy/hash-node': 4.2.7
'@smithy/invalid-dependency': 4.2.7
'@smithy/middleware-content-length': 4.2.7
'@smithy/middleware-endpoint': 4.4.1
'@smithy/middleware-retry': 4.4.17
'@smithy/middleware-serde': 4.2.8
'@smithy/middleware-stack': 4.2.7
'@smithy/node-config-provider': 4.3.7
'@smithy/node-http-handler': 4.4.7
'@smithy/protocol-http': 5.3.7
'@smithy/smithy-client': 4.10.2
'@smithy/types': 4.11.0
'@smithy/url-parser': 4.2.7
'@smithy/util-base64': 4.3.0
'@smithy/util-body-length-browser': 4.2.0
'@smithy/util-body-length-node': 4.2.1
'@smithy/util-defaults-mode-browser': 4.3.16
'@smithy/util-defaults-mode-node': 4.2.19
'@smithy/util-endpoints': 3.2.7
'@smithy/util-middleware': 4.2.7
'@smithy/util-retry': 4.2.7
'@smithy/util-utf8': 4.2.0
tslib: 2.8.1
transitivePeerDependencies:
- aws-crt
'@aws-sdk/client-sts@3.957.0':
dependencies:
'@aws-crypto/sha256-browser': 5.2.0
@@ -3046,6 +3124,25 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/credential-provider-ini@3.958.0':
dependencies:
'@aws-sdk/core': 3.957.0
'@aws-sdk/credential-provider-env': 3.957.0
'@aws-sdk/credential-provider-http': 3.957.0
'@aws-sdk/credential-provider-login': 3.958.0
'@aws-sdk/credential-provider-process': 3.957.0
'@aws-sdk/credential-provider-sso': 3.958.0
'@aws-sdk/credential-provider-web-identity': 3.958.0
'@aws-sdk/nested-clients': 3.958.0
'@aws-sdk/types': 3.957.0
'@smithy/credential-provider-imds': 4.2.7
'@smithy/property-provider': 4.2.7
'@smithy/shared-ini-file-loader': 4.4.2
'@smithy/types': 4.11.0
tslib: 2.8.1
transitivePeerDependencies:
- aws-crt
'@aws-sdk/credential-provider-login@3.957.0':
dependencies:
'@aws-sdk/core': 3.957.0
@@ -3059,6 +3156,19 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/credential-provider-login@3.958.0':
dependencies:
'@aws-sdk/core': 3.957.0
'@aws-sdk/nested-clients': 3.958.0
'@aws-sdk/types': 3.957.0
'@smithy/property-provider': 4.2.7
'@smithy/protocol-http': 5.3.7
'@smithy/shared-ini-file-loader': 4.4.2
'@smithy/types': 4.11.0
tslib: 2.8.1
transitivePeerDependencies:
- aws-crt
'@aws-sdk/credential-provider-node@3.957.0':
dependencies:
'@aws-sdk/credential-provider-env': 3.957.0
@@ -3076,6 +3186,23 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/credential-provider-node@3.958.0':
dependencies:
'@aws-sdk/credential-provider-env': 3.957.0
'@aws-sdk/credential-provider-http': 3.957.0
'@aws-sdk/credential-provider-ini': 3.958.0
'@aws-sdk/credential-provider-process': 3.957.0
'@aws-sdk/credential-provider-sso': 3.958.0
'@aws-sdk/credential-provider-web-identity': 3.958.0
'@aws-sdk/types': 3.957.0
'@smithy/credential-provider-imds': 4.2.7
'@smithy/property-provider': 4.2.7
'@smithy/shared-ini-file-loader': 4.4.2
'@smithy/types': 4.11.0
tslib: 2.8.1
transitivePeerDependencies:
- aws-crt
'@aws-sdk/credential-provider-process@3.957.0':
dependencies:
'@aws-sdk/core': 3.957.0
@@ -3098,6 +3225,19 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/credential-provider-sso@3.958.0':
dependencies:
'@aws-sdk/client-sso': 3.958.0
'@aws-sdk/core': 3.957.0
'@aws-sdk/token-providers': 3.958.0
'@aws-sdk/types': 3.957.0
'@smithy/property-provider': 4.2.7
'@smithy/shared-ini-file-loader': 4.4.2
'@smithy/types': 4.11.0
tslib: 2.8.1
transitivePeerDependencies:
- aws-crt
'@aws-sdk/credential-provider-web-identity@3.957.0':
dependencies:
'@aws-sdk/core': 3.957.0
@@ -3110,9 +3250,21 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/lib-storage@3.957.0(@aws-sdk/client-s3@3.957.0)':
'@aws-sdk/credential-provider-web-identity@3.958.0':
dependencies:
'@aws-sdk/client-s3': 3.957.0
'@aws-sdk/core': 3.957.0
'@aws-sdk/nested-clients': 3.958.0
'@aws-sdk/types': 3.957.0
'@smithy/property-provider': 4.2.7
'@smithy/shared-ini-file-loader': 4.4.2
'@smithy/types': 4.11.0
tslib: 2.8.1
transitivePeerDependencies:
- aws-crt
'@aws-sdk/lib-storage@3.957.0(@aws-sdk/client-s3@3.958.0)':
dependencies:
'@aws-sdk/client-s3': 3.958.0
'@smithy/abort-controller': 4.2.7
'@smithy/middleware-endpoint': 4.4.1
'@smithy/smithy-client': 4.10.2
@@ -3258,6 +3410,49 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/nested-clients@3.958.0':
dependencies:
'@aws-crypto/sha256-browser': 5.2.0
'@aws-crypto/sha256-js': 5.2.0
'@aws-sdk/core': 3.957.0
'@aws-sdk/middleware-host-header': 3.957.0
'@aws-sdk/middleware-logger': 3.957.0
'@aws-sdk/middleware-recursion-detection': 3.957.0
'@aws-sdk/middleware-user-agent': 3.957.0
'@aws-sdk/region-config-resolver': 3.957.0
'@aws-sdk/types': 3.957.0
'@aws-sdk/util-endpoints': 3.957.0
'@aws-sdk/util-user-agent-browser': 3.957.0
'@aws-sdk/util-user-agent-node': 3.957.0
'@smithy/config-resolver': 4.4.5
'@smithy/core': 3.20.0
'@smithy/fetch-http-handler': 5.3.8
'@smithy/hash-node': 4.2.7
'@smithy/invalid-dependency': 4.2.7
'@smithy/middleware-content-length': 4.2.7
'@smithy/middleware-endpoint': 4.4.1
'@smithy/middleware-retry': 4.4.17
'@smithy/middleware-serde': 4.2.8
'@smithy/middleware-stack': 4.2.7
'@smithy/node-config-provider': 4.3.7
'@smithy/node-http-handler': 4.4.7
'@smithy/protocol-http': 5.3.7
'@smithy/smithy-client': 4.10.2
'@smithy/types': 4.11.0
'@smithy/url-parser': 4.2.7
'@smithy/util-base64': 4.3.0
'@smithy/util-body-length-browser': 4.2.0
'@smithy/util-body-length-node': 4.2.1
'@smithy/util-defaults-mode-browser': 4.3.16
'@smithy/util-defaults-mode-node': 4.2.19
'@smithy/util-endpoints': 3.2.7
'@smithy/util-middleware': 4.2.7
'@smithy/util-retry': 4.2.7
'@smithy/util-utf8': 4.2.0
tslib: 2.8.1
transitivePeerDependencies:
- aws-crt
'@aws-sdk/region-config-resolver@3.957.0':
dependencies:
'@aws-sdk/types': 3.957.0
@@ -3298,6 +3493,18 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/token-providers@3.958.0':
dependencies:
'@aws-sdk/core': 3.957.0
'@aws-sdk/nested-clients': 3.958.0
'@aws-sdk/types': 3.957.0
'@smithy/property-provider': 4.2.7
'@smithy/shared-ini-file-loader': 4.4.2
'@smithy/types': 4.11.0
tslib: 2.8.1
transitivePeerDependencies:
- aws-crt
'@aws-sdk/types@3.957.0':
dependencies:
'@smithy/types': 4.11.0
@@ -5259,9 +5466,9 @@ snapshots:
next-s3-upload@0.3.4(next@16.1.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0):
dependencies:
'@aws-sdk/client-s3': 3.957.0
'@aws-sdk/client-s3': 3.958.0
'@aws-sdk/client-sts': 3.957.0
'@aws-sdk/lib-storage': 3.957.0(@aws-sdk/client-s3@3.957.0)
'@aws-sdk/lib-storage': 3.957.0(@aws-sdk/client-s3@3.958.0)
'@aws-sdk/s3-request-presigner': 3.957.0
'@smithy/fetch-http-handler': 2.5.0
next: 16.1.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)