mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 16:55:37 +02:00
⭐ feat: Create image pipeline
This commit is contained in:
@@ -50,7 +50,7 @@ export const apiService = new sst.aws.Service("Api", {
|
|||||||
transform: {
|
transform: {
|
||||||
taskDefinition: (args) => {
|
taskDefinition: (args) => {
|
||||||
const volumes = $output(args.volumes).apply(v => {
|
const volumes = $output(args.volumes).apply(v => {
|
||||||
const next = [...v, {
|
const next = [...(v || []), {
|
||||||
name: "shared-tmp",
|
name: "shared-tmp",
|
||||||
dockerVolumeConfiguration: {
|
dockerVolumeConfiguration: {
|
||||||
scope: "shared",
|
scope: "shared",
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const authService = new sst.aws.Service("Auth", {
|
|||||||
transform: {
|
transform: {
|
||||||
taskDefinition: (args) => {
|
taskDefinition: (args) => {
|
||||||
const volumes = $output(args.volumes).apply(v => {
|
const volumes = $output(args.volumes).apply(v => {
|
||||||
const next = [...v, {
|
const next = [...(v || []), {
|
||||||
name: "shared-tmp",
|
name: "shared-tmp",
|
||||||
dockerVolumeConfiguration: {
|
dockerVolumeConfiguration: {
|
||||||
scope: "shared",
|
scope: "shared",
|
||||||
|
|||||||
179
infra/images.ts
Normal file
179
infra/images.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { domain } from "./dns";
|
||||||
|
import { storage } from "./storage";
|
||||||
|
|
||||||
|
const cache = new sst.cloudflare.Kv("ImageCache");
|
||||||
|
|
||||||
|
const bucket = new sst.cloudflare.Bucket("ImageBucket");
|
||||||
|
|
||||||
|
const imageRouter = new sst.aws.Router("ImageRouter", {
|
||||||
|
routes: {
|
||||||
|
"/*": {
|
||||||
|
bucket: storage,
|
||||||
|
rewrite: { regex: "^/([a-zA-Z0-9_-]+)$", to: "/images/$1" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const imageCdn = new sst.cloudflare.Worker("ImageCDN", {
|
||||||
|
url: true,
|
||||||
|
domain: "cdn." + domain,
|
||||||
|
link: [bucket, cache, imageRouter],
|
||||||
|
handler: "packages/functions/src/images/index.ts",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const outputs = {
|
||||||
|
cdn: imageCdn.url
|
||||||
|
}
|
||||||
|
|
||||||
|
// const transformedImageBucket = new sst.aws.Bucket("TranformedStorage");
|
||||||
|
|
||||||
|
// const imageProcessorFunction = new sst.aws.Function("ImageProcessor",
|
||||||
|
// {
|
||||||
|
// handler: "src/image-processor.handler",
|
||||||
|
// nodejs: { install: ["sharp"] },
|
||||||
|
// memory: "1024 MB",
|
||||||
|
// timeout: "30 seconds",
|
||||||
|
// url: true,
|
||||||
|
// link: [storage, transformedImageBucket],
|
||||||
|
// environment: {
|
||||||
|
// transformedImageCacheTTL:
|
||||||
|
// process.env.transformedImageCacheTTL ?? "max-age=31622400",
|
||||||
|
// maxImageSize: process.env.MAX_IMAGE_SIZE ?? "4700000",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const cloudfrontOAI = new aws.cloudfront.OriginAccessIdentity("CloudfrontOAI");
|
||||||
|
|
||||||
|
// const lambdaOAC = new aws.cloudfront.OriginAccessControl("OriginAccessControl",
|
||||||
|
// {
|
||||||
|
// name: `${$app.name}-${$app.stage}-OriginAccessControl`,
|
||||||
|
// originAccessControlOriginType: "lambda",
|
||||||
|
// signingBehavior: "always",
|
||||||
|
// signingProtocol: "sigv4",
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const cloudfrontResponseHeadersPolicy =
|
||||||
|
// new aws.cloudfront.ResponseHeadersPolicy("ResponseHeadersPolicy",
|
||||||
|
// {
|
||||||
|
// name: `${$app.name}-${$app.stage}-ResponseHeadersPolicy`,
|
||||||
|
// customHeadersConfig: {
|
||||||
|
// items: [
|
||||||
|
// {
|
||||||
|
// header: "x-aws-image-optimization",
|
||||||
|
// value: "v1.0",
|
||||||
|
// override: true,
|
||||||
|
// },
|
||||||
|
// { header: "vary", value: "accept", override: true },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// corsConfig: {
|
||||||
|
// accessControlAllowCredentials: false,
|
||||||
|
// accessControlAllowHeaders: {
|
||||||
|
// items: ["*"],
|
||||||
|
// },
|
||||||
|
// accessControlAllowMethods: {
|
||||||
|
// items: ["GET"],
|
||||||
|
// },
|
||||||
|
// accessControlAllowOrigins: {
|
||||||
|
// items: ["*"],
|
||||||
|
// },
|
||||||
|
// accessControlMaxAgeSec: 600,
|
||||||
|
// originOverride: false,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const cachePolicy = new aws.cloudfront.CachePolicy("CachePolicy",
|
||||||
|
// {
|
||||||
|
// name: `${$app.name}-${$app.stage}-CachePolicy`,
|
||||||
|
// defaultTtl: 86400,
|
||||||
|
// maxTtl: 31536000,
|
||||||
|
// minTtl: 0,
|
||||||
|
// parametersInCacheKeyAndForwardedToOrigin: {
|
||||||
|
// cookiesConfig: {
|
||||||
|
// cookieBehavior: "none",
|
||||||
|
// },
|
||||||
|
// headersConfig: {
|
||||||
|
// headerBehavior: "none",
|
||||||
|
// },
|
||||||
|
// queryStringsConfig: {
|
||||||
|
// queryStringBehavior: "whitelist",
|
||||||
|
// queryStrings: {
|
||||||
|
// items: ["w", "h", "dpr", "format"]
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const groupOriginId = `${$app.name}-${$app.stage}-"GroupOrigin`;
|
||||||
|
// const primaryOriginId = `${$app.name}-${$app.stage}-PrimaryOrigin`;
|
||||||
|
// const secondaryOriginId = `${$app.name}-${$app.stage}-SecondaryOrigin`;
|
||||||
|
|
||||||
|
// const s3Distribution = new sst.aws.Cdn("ImageDistribution",
|
||||||
|
// {
|
||||||
|
// originGroups: [
|
||||||
|
// {
|
||||||
|
// originId: groupOriginId,
|
||||||
|
// failoverCriteria: {
|
||||||
|
// statusCodes: [403, 500, 503, 504],
|
||||||
|
// },
|
||||||
|
// members: [
|
||||||
|
// {
|
||||||
|
// originId: primaryOriginId,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// originId: secondaryOriginId,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// origins: [
|
||||||
|
// {
|
||||||
|
// originId: primaryOriginId,
|
||||||
|
// domainName: imageProcessorFunction.url.apply(
|
||||||
|
// (url) => new URL(url).hostname,
|
||||||
|
// ),
|
||||||
|
// originAccessControlId: lambdaOAC.id,
|
||||||
|
// customOriginConfig: {
|
||||||
|
// originProtocolPolicy: "https-only",
|
||||||
|
// httpPort: 443,
|
||||||
|
// httpsPort: 443,
|
||||||
|
// originSslProtocols: ["TLSv1.2"],
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// originId: secondaryOriginId,
|
||||||
|
// domainName:
|
||||||
|
// transformedImageBucket.nodes.bucket.bucketRegionalDomainName,
|
||||||
|
// s3OriginConfig: {
|
||||||
|
// originAccessIdentity: cloudfrontOAI.cloudfrontAccessIdentityPath,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// defaultCacheBehavior: {
|
||||||
|
// cachePolicyId: cachePolicy.id,
|
||||||
|
// allowedMethods: ["GET", "HEAD"],
|
||||||
|
// cachedMethods: ["GET", "HEAD"],
|
||||||
|
// targetOriginId: groupOriginId,
|
||||||
|
// viewerProtocolPolicy: "redirect-to-https",
|
||||||
|
// functionAssociations: [
|
||||||
|
// {
|
||||||
|
// eventType: "viewer-request",
|
||||||
|
// functionArn: cloudfrontRewriteFunction.arn,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// responseHeadersPolicyId: cloudfrontResponseHeadersPolicy.id,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// new aws.lambda.Permission("AllowCloudFrontServicePrincipal", {
|
||||||
|
// action: "lambda:InvokeFunctionUrl",
|
||||||
|
// function: imageProcessorFunction.arn,
|
||||||
|
// principal: "cloudfront.amazonaws.com",
|
||||||
|
// statementId: "AllowCloudFrontServicePrincipal",
|
||||||
|
// sourceArn: s3Distribution.nodes.distribution.arn,
|
||||||
|
// });
|
||||||
@@ -1 +1,3 @@
|
|||||||
export const storage = new sst.aws.Bucket("Storage");
|
export const storage = new sst.aws.Bucket("Storage",{
|
||||||
|
access: "cloudfront",
|
||||||
|
});
|
||||||
@@ -146,9 +146,9 @@ export const zero = new sst.aws.Service("Zero", {
|
|||||||
ZERO_NUM_SYNC_WORKERS: "1",
|
ZERO_NUM_SYNC_WORKERS: "1",
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
ZERO_CHANGE_STREAMER_URI: replicationManager.url.apply((val) =>
|
ZERO_CHANGE_STREAMER_URI: replicationManager?.url.apply((val) =>
|
||||||
val.replace("http://", "ws://"),
|
val.replace("http://", "ws://"),
|
||||||
),
|
) ?? "",
|
||||||
ZERO_UPSTREAM_MAX_CONNS: "15",
|
ZERO_UPSTREAM_MAX_CONNS: "15",
|
||||||
ZERO_CVR_MAX_CONNS: "160",
|
ZERO_CVR_MAX_CONNS: "160",
|
||||||
}),
|
}),
|
||||||
|
|||||||
50
packages/functions/src/images/image.ts
Normal file
50
packages/functions/src/images/image.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import { HTTPException } from "hono/http-exception";
|
||||||
|
import { Resource } from "sst";
|
||||||
|
|
||||||
|
export namespace ImageRoute {
|
||||||
|
export const route = new Hono()
|
||||||
|
.get(
|
||||||
|
"/:hashWithExt",
|
||||||
|
(c) => {
|
||||||
|
const { hashWithExt } = c.req.param();
|
||||||
|
|
||||||
|
// Validate format
|
||||||
|
// Split hash and extension
|
||||||
|
const match = hashWithExt.match(/^([a-zA-Z0-9_-]+)\.(avif|webp)$/);
|
||||||
|
if (!match) {
|
||||||
|
throw new HTTPException(400, { message: "Invalid image hash or format" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, hash, format] = match;
|
||||||
|
|
||||||
|
const query = c.req.query();
|
||||||
|
// Parse dimensions
|
||||||
|
const width = parseInt(query.w || query.width || "");
|
||||||
|
const height = parseInt(query.h || query.height || "");
|
||||||
|
const dpr = parseFloat(query.dpr || "1");
|
||||||
|
|
||||||
|
if (isNaN(width) || width <= 0) {
|
||||||
|
throw new HTTPException(400, { message: "Invalid width" });
|
||||||
|
}
|
||||||
|
if (!isNaN(height) && height < 0) {
|
||||||
|
throw new HTTPException(400, { message: "Invalid height" });
|
||||||
|
}
|
||||||
|
if (dpr < 1 || dpr > 4) {
|
||||||
|
throw new HTTPException(400, { message: "Invalid dpr" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = `${Resource.ImageRouter.url}/images/00641801e0f80a82bbbbf9dec5593cd7cbe2a5eec45d36199a5c14ed30d8df66`
|
||||||
|
|
||||||
|
|
||||||
|
// Normalize and build cache key
|
||||||
|
const cacheKey = `${hash}_${format}_w${width}${height ? `_h${height}` : ""}_dpr${dpr}`;
|
||||||
|
|
||||||
|
// Add aggressive caching
|
||||||
|
c.header("Cache-Control", "public, max-age=315360000, immutable");
|
||||||
|
|
||||||
|
// Placeholder image response (to be replaced by real logic)
|
||||||
|
return c.text(`Would serve image: ${cacheKey}`);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
18
packages/functions/src/images/index.ts
Normal file
18
packages/functions/src/images/index.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import { logger } from "hono/logger";
|
||||||
|
import { ImageRoute } from "./image";
|
||||||
|
|
||||||
|
const app = new Hono();
|
||||||
|
app
|
||||||
|
.use(logger(), async (c, next) => {
|
||||||
|
c.header("Cache-Control", "public, max-age=315360000, immutable");
|
||||||
|
return next();
|
||||||
|
})
|
||||||
|
|
||||||
|
const routes = app
|
||||||
|
.get("/", (c) => c.text("Hello World 👋🏾"))
|
||||||
|
.route("/image", ImageRoute.route)
|
||||||
|
|
||||||
|
|
||||||
|
export type Routes = typeof routes;
|
||||||
|
export default app;
|
||||||
13
sst-env.d.ts
vendored
13
sst-env.d.ts
vendored
@@ -57,6 +57,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ImageRouter": {
|
||||||
|
"type": "sst.aws.Router"
|
||||||
|
"url": string
|
||||||
|
}
|
||||||
"NestriFamilyMonthly": {
|
"NestriFamilyMonthly": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
@@ -117,6 +121,15 @@ declare module "sst" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// cloudflare
|
||||||
|
import * as cloudflare from "@cloudflare/workers-types";
|
||||||
|
declare module "sst" {
|
||||||
|
export interface Resource {
|
||||||
|
"ImageBucket": cloudflare.R2Bucket
|
||||||
|
"ImageCDN": cloudflare.Service
|
||||||
|
"ImageCache": cloudflare.KVNamespace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
import "sst"
|
import "sst"
|
||||||
export {}
|
export {}
|
||||||
@@ -1 +1,22 @@
|
|||||||
{}
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"ESNext"
|
||||||
|
],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
/* Linting */
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user