Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

feat: add /media/* handler and fix /media-collection route in lith

Ports the Netlify edge function media.js logic into the Express
adapter: resolves @handle → user ID → DO Spaces redirect for
paintings, tapes, and direct file paths. Also fixes media-collection
route to handle query params without a trailing path segment.

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

+103
+103
lith/server.mjs
··· 243 243 244 244 // --- Routes --- 245 245 246 + // --- /media/* handler (ports Netlify edge function media.js) --- 247 + app.all("/media/*rest", async (req, res) => { 248 + const parts = req.path.split("/").filter(Boolean); // ["media", ...] 249 + parts.shift(); // remove "media" 250 + const resourcePath = parts.join("/"); 251 + 252 + if (!resourcePath) return res.status(404).send("Missing media path"); 253 + 254 + // Content type from extension 255 + const ext = resourcePath.split(".").pop()?.toLowerCase(); 256 + const ctMap = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", zip: "application/zip", mp4: "video/mp4", json: "application/json", mjs: "text/javascript", svg: "image/svg+xml" }; 257 + 258 + // Helper: build a clean event for calling functions internally 259 + function mediaEvent(path, query) { 260 + return { 261 + httpMethod: "GET", 262 + headers: req.headers, 263 + body: null, 264 + queryStringParameters: query, 265 + path, 266 + rawUrl: `${req.protocol}://${req.get("host")}${path}`, 267 + isBase64Encoded: false, 268 + }; 269 + } 270 + 271 + // /media/tapes/CODE → get-tape function → redirect to DO Spaces 272 + if (parts[0] === "tapes" && parts[1]) { 273 + const code = parts[1].replace(/\.zip$/, ""); 274 + try { 275 + const result = await functions["get-tape"](mediaEvent("/api/get-tape", { code }), {}); 276 + if (result.statusCode === 200) { 277 + const tape = JSON.parse(result.body); 278 + const bucket = tape.bucket || "art-aesthetic-computer"; 279 + const key = tape.user ? `${tape.user}/${tape.slug}.zip` : `${tape.slug}.zip`; 280 + return res.redirect(302, `https://${bucket}.sfo3.digitaloceanspaces.com/${key}`); 281 + } 282 + } catch {} 283 + return res.status(404).send("Tape not found"); 284 + } 285 + 286 + // /media/paintings/CODE → get-painting function → redirect 287 + if (parts[0] === "paintings" && parts[1]) { 288 + const code = parts[1].replace(/\.(png|zip)$/, ""); 289 + try { 290 + const result = await functions["get-painting"]?.(mediaEvent("/api/get-painting", { code }), {}); 291 + if (result?.statusCode === 200) { 292 + const painting = JSON.parse(result.body); 293 + const bucket = painting.user ? "user-aesthetic-computer" : "art-aesthetic-computer"; 294 + const slug = painting.slug?.split(":")[0] || painting.slug; 295 + const key = painting.user ? `${painting.user}/${slug}.png` : `${slug}.png`; 296 + return res.redirect(302, `https://${bucket}.sfo3.digitaloceanspaces.com/${key}`); 297 + } 298 + } catch {} 299 + return res.status(404).send("Painting not found"); 300 + } 301 + 302 + // /media/@handle/type/slug → resolve user ID → redirect to DO Spaces 303 + if (parts[0]?.startsWith("@") || parts[0]?.match(/^ac[a-z0-9]+$/i)) { 304 + const userIdentifier = parts[0]; 305 + const subPath = parts.slice(1).join("/"); 306 + 307 + // Resolve user ID via user function directly 308 + try { 309 + const query = userIdentifier.match(/^ac[a-z0-9]+$/i) 310 + ? { code: userIdentifier } 311 + : { from: userIdentifier }; 312 + const event = { 313 + httpMethod: "GET", 314 + headers: req.headers, 315 + body: null, 316 + queryStringParameters: query, 317 + path: "/user", 318 + rawUrl: `${req.protocol}://${req.get("host")}/user`, 319 + isBase64Encoded: false, 320 + }; 321 + const result = await functions["user"](event, {}); 322 + if (result.statusCode === 200) { 323 + const user = JSON.parse(result.body); 324 + const userId = user.sub; 325 + if (userId) { 326 + const fullPath = `${userId}/${subPath}`; 327 + const baseUrl = ext === "mjs" 328 + ? "https://user-aesthetic-computer.sfo3.digitaloceanspaces.com" 329 + : "https://user.aesthetic.computer"; 330 + const encoded = fullPath.split("/").map(encodeURIComponent).join("/"); 331 + return res.redirect(302, `${baseUrl}/${encoded}`); 332 + } 333 + } 334 + } catch (err) { 335 + console.error("media user resolve error:", err.message); 336 + } 337 + return res.status(404).send("User media not found"); 338 + } 339 + 340 + // Direct file path → proxy to DO Spaces 341 + const baseUrl = ext === "mjs" 342 + ? "https://user-aesthetic-computer.sfo3.digitaloceanspaces.com" 343 + : "https://user.aesthetic.computer"; 344 + const encoded = resourcePath.split("/").map(encodeURIComponent).join("/"); 345 + return res.redirect(302, `${baseUrl}/${encoded}`); 346 + }); 347 + 246 348 // API functions (matches Netlify redirect rules) 247 349 app.all("/api/:fn", handleFunctionResolved); 248 350 app.all("/api/:fn/*rest", handleFunctionResolved); ··· 266 368 app.all("/docs", directFn("docs")); 267 369 app.all("/docs.json", directFn("docs")); 268 370 app.all("/docs/*rest", directFn("docs")); 371 + app.all("/media-collection", directFn("media-collection")); 269 372 app.all("/media-collection/*rest", directFn("media-collection")); 270 373 app.all("/device-login", directFn("device-login")); 271 374 app.all("/device-auth", directFn("device-auth"));