[READ ONLY MIRROR] Spark Social AppView Server github.com/sprksocial/server
atproto deno hono lexicon
5
fork

Configure Feed

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

Merge branch 'main' of https://github.com/sprksocial/spark-back-end

+210 -19
+63
services/cdn/bun.lock
··· 8 8 "hono": "^4.7.5", 9 9 "pino": "^9.6.0", 10 10 "pino-pretty": "^13.0.0", 11 + "sharp": "^0.34.1", 11 12 }, 12 13 "devDependencies": { 13 14 "@types/bun": "latest", ··· 25 26 26 27 "@atproto/identity": ["@atproto/identity@0.4.7", "", { "dependencies": { "@atproto/common-web": "^0.4.1", "@atproto/crypto": "^0.4.4" } }, "sha512-A61OT9yc74dEFi1elODt/tzQNSwV3ZGZCY5cRl6NYO9t/0AVdaD+fyt81yh3mRxyI8HeVOecvXl3cPX5knz9rQ=="], 27 28 29 + "@emnapi/runtime": ["@emnapi/runtime@1.4.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw=="], 30 + 31 + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.1.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A=="], 32 + 33 + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.1.0" }, "os": "darwin", "cpu": "x64" }, "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q=="], 34 + 35 + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA=="], 36 + 37 + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ=="], 38 + 39 + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.1.0", "", { "os": "linux", "cpu": "arm" }, "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA=="], 40 + 41 + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew=="], 42 + 43 + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.1.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ=="], 44 + 45 + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.1.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA=="], 46 + 47 + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q=="], 48 + 49 + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w=="], 50 + 51 + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A=="], 52 + 53 + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.1.0" }, "os": "linux", "cpu": "arm" }, "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA=="], 54 + 55 + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ=="], 56 + 57 + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.1.0" }, "os": "linux", "cpu": "s390x" }, "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA=="], 58 + 59 + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA=="], 60 + 61 + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ=="], 62 + 63 + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg=="], 64 + 65 + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.1", "", { "dependencies": { "@emnapi/runtime": "^1.4.0" }, "cpu": "none" }, "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg=="], 66 + 67 + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw=="], 68 + 69 + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.1", "", { "os": "win32", "cpu": "x64" }, "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw=="], 70 + 28 71 "@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], 29 72 30 73 "@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], ··· 47 90 48 91 "bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="], 49 92 93 + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], 94 + 95 + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], 96 + 97 + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], 98 + 99 + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], 100 + 50 101 "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], 51 102 52 103 "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], 104 + 105 + "detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="], 53 106 54 107 "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], 55 108 ··· 70 123 "hono": ["hono@4.7.5", "", {}, "sha512-fDOK5W2C1vZACsgLONigdZTRZxuBqFtcKh7bUQ5cVSbwI2RWjloJDcgFOVzbQrlI6pCmhlTsVYZ7zpLj4m4qMQ=="], 71 124 72 125 "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], 126 + 127 + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], 73 128 74 129 "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], 75 130 ··· 107 162 108 163 "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], 109 164 165 + "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], 166 + 167 + "sharp": ["sharp@0.34.1", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.7.1" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.1", "@img/sharp-darwin-x64": "0.34.1", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", "@img/sharp-libvips-linux-arm64": "1.1.0", "@img/sharp-libvips-linux-ppc64": "1.1.0", "@img/sharp-libvips-linux-s390x": "1.1.0", "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", "@img/sharp-linux-arm": "0.34.1", "@img/sharp-linux-arm64": "0.34.1", "@img/sharp-linux-s390x": "0.34.1", "@img/sharp-linux-x64": "0.34.1", "@img/sharp-linuxmusl-arm64": "0.34.1", "@img/sharp-linuxmusl-x64": "0.34.1", "@img/sharp-wasm32": "0.34.1", "@img/sharp-win32-ia32": "0.34.1", "@img/sharp-win32-x64": "0.34.1" } }, "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg=="], 168 + 169 + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], 170 + 110 171 "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], 111 172 112 173 "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], ··· 116 177 "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], 117 178 118 179 "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], 180 + 181 + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 119 182 120 183 "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], 121 184
+2 -1
services/cdn/package.json
··· 18 18 "@atproto/identity": "^0.4.7", 19 19 "hono": "^4.7.5", 20 20 "pino": "^9.6.0", 21 - "pino-pretty": "^13.0.0" 21 + "pino-pretty": "^13.0.0", 22 + "sharp": "^0.34.1" 22 23 } 23 24 }
+120 -16
services/cdn/src/imageHandler.ts
··· 1 1 import type { Context } from 'hono' 2 2 import { pino } from 'pino' 3 + import sharp from 'sharp' 3 4 import type { BidirectionalResolver } from './id-resolver' 4 5 5 6 // Get logger instance from parent ··· 10 11 }, 11 12 }) 12 13 13 - // In-memory image cache: did:cid -> {buffer, timestamp} 14 + // Size dimensions for different size options 15 + interface SizeDimension { 16 + width: number 17 + } 18 + 19 + const SIZE_DIMENSIONS: Record<string, SizeDimension | null> = { 20 + tiny: { width: 150 }, 21 + medium: { width: 600 }, 22 + full: null, // original size 23 + } 24 + 25 + // In-memory image cache: did:cid:size:format -> {buffer, timestamp} 14 26 interface CachedImage { 15 - buffer: ArrayBuffer 27 + buffer: Buffer | ArrayBuffer 16 28 timestamp: number 17 29 contentType: string 18 30 } ··· 44 56 return { buffer: imageBytes, contentType } 45 57 } 46 58 59 + // Transform image based on size and format 60 + export const transformImage = async ( 61 + imageBuffer: ArrayBuffer, 62 + originalContentType: string, 63 + size: string, 64 + format?: string, 65 + ): Promise<{ buffer: Buffer; contentType: string }> => { 66 + // Convert ArrayBuffer to Buffer for Sharp 67 + const buffer = Buffer.from(imageBuffer) 68 + let transformer = sharp(buffer) 69 + 70 + // Apply resize if not "full" 71 + if (size !== 'full' && SIZE_DIMENSIONS[size]) { 72 + transformer = transformer.resize({ 73 + ...(SIZE_DIMENSIONS[size] as SizeDimension), 74 + withoutEnlargement: true, // Don't upscale images smaller than target size 75 + fit: 'inside', // Maintain aspect ratio and ensure dimensions don't exceed specified values 76 + }) 77 + } 78 + 79 + // Apply format conversion if specified 80 + let contentType = originalContentType 81 + if (format) { 82 + switch (format.toLowerCase()) { 83 + case 'webp': 84 + transformer = transformer.webp() 85 + contentType = 'image/webp' 86 + break 87 + case 'png': 88 + transformer = transformer.png() 89 + contentType = 'image/png' 90 + break 91 + case 'jpg': 92 + case 'jpeg': 93 + transformer = transformer.jpeg() 94 + contentType = 'image/jpeg' 95 + break 96 + case 'avif': 97 + transformer = transformer.avif() 98 + contentType = 'image/avif' 99 + break 100 + default: 101 + // Keep original format if unsupported 102 + break 103 + } 104 + } 105 + 106 + const outputBuffer = await transformer.toBuffer() 107 + return { buffer: outputBuffer, contentType } 108 + } 109 + 47 110 // Cleanup old cache entries 48 111 export function cleanupCache() { 49 112 const now = Date.now() ··· 67 130 } 68 131 } 69 132 133 + // Parse format from path if present 134 + function parseFormat(path: string): string | undefined { 135 + const formatMatch = path.match(/@([a-zA-Z0-9]+)$/) 136 + return formatMatch ? formatMatch[1].toLowerCase() : undefined 137 + } 138 + 70 139 // Generic image handler for both avatar and regular images 71 140 export const imageHandler = async ( 72 141 c: Context, 73 142 bidirectionalResolver: BidirectionalResolver, 74 143 ) => { 144 + const size = c.req.param('size') || 'full' 145 + if (!['tiny', 'medium', 'full'].includes(size)) { 146 + return c.json( 147 + { error: 'Invalid size parameter. Must be tiny, medium, or full' }, 148 + 400, 149 + ) 150 + } 151 + 75 152 const did = c.req.param('did') 76 153 const cid = c.req.param('cid') 77 - const cacheKey = `${did}:${cid}` 154 + const format = parseFormat(c.req.path) 155 + 156 + // Create a unique cache key that includes size and format 157 + const cacheKey = `${did}:${cid}:${size}:${format || 'original'}` 78 158 79 159 try { 80 - let imageBuffer: ArrayBuffer 160 + let transformedBuffer: Buffer 81 161 let contentType: string 82 162 let fromCache = false 83 163 84 164 const cachedEntry = imageCache.get(cacheKey) 85 165 if (cachedEntry) { 86 - logger.info({ did, cid, cached: true }, 'Found image in cache') 87 - imageBuffer = cachedEntry.buffer 166 + logger.info( 167 + { did, cid, size, format, cached: true }, 168 + 'Found transformed image in cache', 169 + ) 170 + transformedBuffer = 171 + cachedEntry.buffer instanceof Buffer 172 + ? cachedEntry.buffer 173 + : Buffer.from(new Uint8Array(cachedEntry.buffer)) 88 174 contentType = cachedEntry.contentType 89 175 fromCache = true 90 176 } else { ··· 101 187 'Fetching image from PDS', 102 188 ) 103 189 const result = await getImage(didDoc.pds, did, cid) 104 - imageBuffer = result.buffer 105 - contentType = result.contentType 106 190 107 - // Cache the image 191 + // Transform the image according to size and format 192 + logger.info({ did, cid, size, format }, 'Transforming image') 193 + const transformed = await transformImage( 194 + result.buffer, 195 + result.contentType, 196 + size, 197 + format, 198 + ) 199 + 200 + transformedBuffer = transformed.buffer 201 + contentType = transformed.contentType 202 + 203 + // Cache the transformed image 108 204 imageCache.set(cacheKey, { 109 - buffer: imageBuffer, 205 + buffer: transformedBuffer, 110 206 timestamp: Date.now(), 111 207 contentType, 112 208 }) ··· 114 210 cleanupCache() 115 211 } 116 212 117 - const fileSize = imageBuffer.byteLength 213 + const fileSize = transformedBuffer.byteLength 118 214 119 215 // Set headers 120 216 c.header('Content-Type', contentType) 121 217 c.header('Content-Length', fileSize.toString()) 122 - c.header('ETag', cid) 218 + c.header('ETag', `${cid}-${size}-${format || 'original'}`) 123 219 c.header('Cache-Control', 'public, max-age=86400') 124 220 125 221 logger.info( 126 - { did, cid, size: fileSize, cached: fromCache, type: contentType }, 127 - 'Serving image', 222 + { 223 + did, 224 + cid, 225 + size, 226 + format, 227 + fileSize, 228 + cached: fromCache, 229 + type: contentType, 230 + }, 231 + 'Serving transformed image', 128 232 ) 129 233 130 - return c.body(imageBuffer) 234 + return c.body(transformedBuffer) 131 235 } catch (err) { 132 - logger.error({ err, did, cid }, 'Error serving image') 236 + logger.error({ err, did, cid, size, format }, 'Error serving image') 133 237 return c.json({ error: 'Error serving image' }, 500) 134 238 } 135 239 }
+25 -2
services/cdn/src/index.ts
··· 35 35 36 36 app.get('/video/:did/:cid', (c) => videoHandler(c, bidirectionalResolver)) 37 37 38 - app.get('/avatar/:did/:cid', (c) => imageHandler(c, bidirectionalResolver)) 38 + // Routes for avatar images with size and format options 39 + app.get('/avatar/:size/:did/:cid', (c) => 40 + imageHandler(c, bidirectionalResolver), 41 + ) 42 + app.get('/avatar/:size/:did/:cid/:format', (c) => 43 + imageHandler(c, bidirectionalResolver), 44 + ) 45 + 46 + // Routes for regular images with size and format options 47 + app.get('/img/:size/:did/:cid', (c) => 48 + imageHandler(c, bidirectionalResolver), 49 + ) 50 + app.get('/img/:size/:did/:cid/:format', (c) => 51 + imageHandler(c, bidirectionalResolver), 52 + ) 53 + 54 + // Backward compatibility routes (default to 'full' size) 55 + app.get('/avatar/:did/:cid', (c) => { 56 + c.req.param = Object.assign(c.req.param, { size: 'full' }) 57 + return imageHandler(c, bidirectionalResolver) 58 + }) 39 59 40 - app.get('/img/:did/:cid', (c) => imageHandler(c, bidirectionalResolver)) 60 + app.get('/img/:did/:cid', (c) => { 61 + c.req.param = Object.assign(c.req.param, { size: 'full' }) 62 + return imageHandler(c, bidirectionalResolver) 63 + }) 41 64 42 65 logger.info({ port }, 'CDN service is running') 43 66 } catch (err) {