feat:use local proxy for media

use local proxy for media so that media doesnt need to be requested from
r2 everytime but can be cached locally. this also fixes some csp issues
ive been having.
This commit is contained in:
2025-11-17 10:48:40 +01:00
parent 08f7e77a86
commit 96a173b73b
7 changed files with 110 additions and 69 deletions

View File

@@ -4,7 +4,7 @@ import { sha256 } from '@oslojs/crypto/sha2';
import { encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding';
import { db } from '$lib/server/db';
import * as table from '$lib/server/db/schema';
import { getSignedR2Url } from '$lib/server/r2';
import { getLocalR2Url } from '$lib/server/r2';
const DAY_IN_MS = 1000 * 60 * 60 * 24;
@@ -63,16 +63,11 @@ export async function validateSessionToken(token: string) {
.where(eq(table.session.id, session.id));
}
// Generate signed URL for profile picture if it exists
// Generate local proxy URL for profile picture if it exists
let profilePictureUrl = user.profilePictureUrl;
if (profilePictureUrl && !profilePictureUrl.startsWith('http')) {
// It's a path, generate signed URL
try {
profilePictureUrl = await getSignedR2Url(profilePictureUrl, 24 * 60 * 60); // 24 hours
} catch (error) {
console.error('Failed to generate signed URL for profile picture:', error);
profilePictureUrl = null;
}
// It's a path, generate local proxy URL
profilePictureUrl = getLocalR2Url(profilePictureUrl);
}
return {

View File

@@ -72,3 +72,7 @@ export async function getSignedR2Url(path: string, expiresIn = 3600): Promise<st
return await getSignedUrl(r2Client, command, { expiresIn });
}
export function getLocalR2Url(path: string): string {
return `/api/media/${path}`;
}

View File

@@ -4,7 +4,7 @@ import { db } from '$lib/server/db';
import { find, findMedia, user, findLike, friendship } from '$lib/server/db/schema';
import { eq, and, sql, desc, or } from 'drizzle-orm';
import { encodeBase64url } from '@oslojs/encoding';
import { getSignedR2Url } from '$lib/server/r2';
import { getLocalR2Url } from '$lib/server/r2';
function generateFindId(): string {
const bytes = crypto.getRandomValues(new Uint8Array(15));
@@ -176,31 +176,24 @@ export const GET: RequestHandler = async ({ url, locals }) => {
// Generate signed URLs for all media items
const mediaWithSignedUrls = await Promise.all(
findMedia.map(async (mediaItem) => {
// URLs in database are now paths, generate signed URLs directly
const [signedUrl, signedThumbnailUrl] = await Promise.all([
getSignedR2Url(mediaItem.url, 24 * 60 * 60), // 24 hours
mediaItem.thumbnailUrl && !mediaItem.thumbnailUrl.startsWith('/')
? getSignedR2Url(mediaItem.thumbnailUrl, 24 * 60 * 60)
: Promise.resolve(mediaItem.thumbnailUrl) // Keep static placeholder paths as-is
]);
// URLs in database are now paths, generate local proxy URLs
const localUrl = getLocalR2Url(mediaItem.url);
const localThumbnailUrl = mediaItem.thumbnailUrl && !mediaItem.thumbnailUrl.startsWith('/')
? getLocalR2Url(mediaItem.thumbnailUrl)
: mediaItem.thumbnailUrl; // Keep static placeholder paths as-is
return {
...mediaItem,
url: signedUrl,
thumbnailUrl: signedThumbnailUrl
url: localUrl,
thumbnailUrl: localThumbnailUrl
};
})
);
// Generate signed URL for user profile picture if it exists
// Generate local proxy URL for user profile picture if it exists
let userProfilePictureUrl = findItem.profilePictureUrl;
if (userProfilePictureUrl && !userProfilePictureUrl.startsWith('http')) {
try {
userProfilePictureUrl = await getSignedR2Url(userProfilePictureUrl, 24 * 60 * 60);
} catch (error) {
console.error('Failed to generate signed URL for user profile picture:', error);
userProfilePictureUrl = null;
}
userProfilePictureUrl = getLocalR2Url(userProfilePictureUrl);
}
return {

View File

@@ -4,7 +4,7 @@ import { db } from '$lib/server/db';
import { friendship, user } from '$lib/server/db/schema';
import { eq, and, or } from 'drizzle-orm';
import { encodeBase64url } from '@oslojs/encoding';
import { getSignedR2Url } from '$lib/server/r2';
import { getLocalR2Url } from '$lib/server/r2';
import { notificationService } from '$lib/server/notifications';
import { pushService } from '$lib/server/push';
@@ -98,25 +98,18 @@ export const GET: RequestHandler = async ({ url, locals }) => {
.where(and(eq(friendship.friendId, locals.user.id), eq(friendship.status, status)));
}
// Generate signed URLs for profile pictures
const friendshipsWithSignedUrls = await Promise.all(
(friendships || []).map(async (friendship) => {
let profilePictureUrl = friendship.friendProfilePictureUrl;
if (profilePictureUrl && !profilePictureUrl.startsWith('http')) {
try {
profilePictureUrl = await getSignedR2Url(profilePictureUrl, 24 * 60 * 60);
} catch (error) {
console.error('Failed to generate signed URL for profile picture:', error);
profilePictureUrl = null;
}
}
// Generate local proxy URLs for profile pictures
const friendshipsWithSignedUrls = (friendships || []).map((friendship) => {
let profilePictureUrl = friendship.friendProfilePictureUrl;
if (profilePictureUrl && !profilePictureUrl.startsWith('http')) {
profilePictureUrl = getLocalR2Url(profilePictureUrl);
}
return {
...friendship,
friendProfilePictureUrl: profilePictureUrl
};
})
);
return {
...friendship,
friendProfilePictureUrl: profilePictureUrl
};
});
return json(friendshipsWithSignedUrls);
} catch (err) {

View File

@@ -0,0 +1,63 @@
import type { RequestHandler } from './$types';
import { error } from '@sveltejs/kit';
import { GetObjectCommand } from '@aws-sdk/client-s3';
import { r2Client } from '$lib/server/r2';
import { env } from '$env/dynamic/private';
const R2_BUCKET_NAME = env.R2_BUCKET_NAME;
export const GET: RequestHandler = async ({ params, setHeaders }) => {
const path = params.path;
if (!path) {
throw error(400, 'Path is required');
}
if (!R2_BUCKET_NAME) {
throw error(500, 'R2_BUCKET_NAME environment variable is required');
}
try {
const command = new GetObjectCommand({
Bucket: R2_BUCKET_NAME,
Key: path
});
const response = await r2Client.send(command);
if (!response.Body) {
throw error(404, 'File not found');
}
const chunks: Uint8Array[] = [];
const body = response.Body as AsyncIterable<Uint8Array>;
for await (const chunk of body) {
chunks.push(chunk);
}
const buffer = Buffer.concat(chunks);
setHeaders({
'Content-Type': response.ContentType || 'application/octet-stream',
'Cache-Control': response.CacheControl || 'public, max-age=31536000, immutable',
'Content-Length': buffer.length.toString(),
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
});
return new Response(buffer);
} catch (err) {
console.error('Error fetching file from R2:', err);
throw error(500, 'Failed to fetch file');
}
};
export const OPTIONS: RequestHandler = async ({ setHeaders }) => {
setHeaders({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
});
return new Response(null, { status: 204 });
};

View File

@@ -3,7 +3,7 @@ import type { RequestHandler } from './$types';
import { db } from '$lib/server/db';
import { user, friendship } from '$lib/server/db/schema';
import { eq, and, or, ilike, ne } from 'drizzle-orm';
import { getSignedR2Url } from '$lib/server/r2';
import { getLocalR2Url } from '$lib/server/r2';
export const GET: RequestHandler = async ({ url, locals }) => {
if (!locals.user) {
@@ -63,28 +63,21 @@ export const GET: RequestHandler = async ({ url, locals }) => {
friendshipStatusMap.set(otherUserId, f.status);
});
// Generate signed URLs and add friendship status
const usersWithSignedUrls = await Promise.all(
users.map(async (userItem) => {
let profilePictureUrl = userItem.profilePictureUrl;
if (profilePictureUrl && !profilePictureUrl.startsWith('http')) {
try {
profilePictureUrl = await getSignedR2Url(profilePictureUrl, 24 * 60 * 60);
} catch (error) {
console.error('Failed to generate signed URL for profile picture:', error);
profilePictureUrl = null;
}
}
// Generate local proxy URLs and add friendship status
const usersWithSignedUrls = users.map((userItem) => {
let profilePictureUrl = userItem.profilePictureUrl;
if (profilePictureUrl && !profilePictureUrl.startsWith('http')) {
profilePictureUrl = getLocalR2Url(profilePictureUrl);
}
const friendshipStatus = friendshipStatusMap.get(userItem.id) || 'none';
const friendshipStatus = friendshipStatusMap.get(userItem.id) || 'none';
return {
...userItem,
profilePictureUrl,
friendshipStatus
};
})
);
return {
...userItem,
profilePictureUrl,
friendshipStatus
};
});
return json(usersWithSignedUrls);
} catch (err) {

View File

@@ -130,8 +130,8 @@ self.addEventListener('fetch', (event) => {
}
}
// Handle R2 resources with cache-first strategy
if (url.hostname.includes('.r2.dev') || url.hostname.includes('.r2.cloudflarestorage.com')) {
// Handle R2 resources and local media proxy with cache-first strategy
if (url.hostname.includes('.r2.dev') || url.hostname.includes('.r2.cloudflarestorage.com') || url.pathname.startsWith('/api/media/')) {
const cachedResponse = await r2Cache.match(event.request);
if (cachedResponse) {
return cachedResponse;
@@ -146,7 +146,7 @@ self.addEventListener('fetch', (event) => {
return response;
} catch {
// Return cached version if available, or fall through to other cache checks
return cachedResponse || new Response('R2 resource not available', { status: 404 });
return cachedResponse || new Response('Media resource not available', { status: 404 });
}
}