fix:use signed R2 URLs for uploaded media
- uploadToR2 now returns storage path instead of a: full URL. - Generate signed R2 URLs (24h expiration) for images, thumbnails, and videos in media processor and when loading finds. - Update CSP to allow *.r2.cloudflarestorage.com for img-src
This commit is contained in:
@@ -50,7 +50,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
"worker-src 'self' blob:; " +
|
"worker-src 'self' blob:; " +
|
||||||
"style-src 'self' 'unsafe-inline' fonts.googleapis.com; " +
|
"style-src 'self' 'unsafe-inline' fonts.googleapis.com; " +
|
||||||
"font-src 'self' fonts.gstatic.com; " +
|
"font-src 'self' fonts.gstatic.com; " +
|
||||||
"img-src 'self' data: blob: *.openstreetmap.org *.tile.openstreetmap.org pub-6495d15c9d09b19ddc65f0a01892a183.r2.dev; " +
|
"img-src 'self' data: blob: *.openstreetmap.org *.tile.openstreetmap.org *.r2.cloudflarestorage.com; " +
|
||||||
"connect-src 'self' *.openstreetmap.org; " +
|
"connect-src 'self' *.openstreetmap.org; " +
|
||||||
"frame-ancestors 'none'; " +
|
"frame-ancestors 'none'; " +
|
||||||
"base-uri 'self'; " +
|
"base-uri 'self'; " +
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { uploadToR2 } from './r2';
|
import { uploadToR2, getSignedR2Url } from './r2';
|
||||||
|
|
||||||
const THUMBNAIL_SIZE = 400;
|
const THUMBNAIL_SIZE = 400;
|
||||||
const MAX_IMAGE_SIZE = 1920;
|
const MAX_IMAGE_SIZE = 1920;
|
||||||
@@ -41,11 +41,17 @@ export async function processAndUploadImage(
|
|||||||
type: 'image/jpeg'
|
type: 'image/jpeg'
|
||||||
});
|
});
|
||||||
|
|
||||||
const [url, thumbnailUrl] = await Promise.all([
|
const [imagePath, thumbPath] = await Promise.all([
|
||||||
uploadToR2(imageFile, `${filename}.jpg`, 'image/jpeg'),
|
uploadToR2(imageFile, `${filename}.jpg`, 'image/jpeg'),
|
||||||
uploadToR2(thumbFile, `${filename}-thumb.jpg`, 'image/jpeg')
|
uploadToR2(thumbFile, `${filename}-thumb.jpg`, 'image/jpeg')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Generate signed URLs (24 hour expiration for images)
|
||||||
|
const [url, thumbnailUrl] = await Promise.all([
|
||||||
|
getSignedR2Url(imagePath, 24 * 60 * 60), // 24 hours
|
||||||
|
getSignedR2Url(thumbPath, 24 * 60 * 60) // 24 hours
|
||||||
|
]);
|
||||||
|
|
||||||
return { url, thumbnailUrl };
|
return { url, thumbnailUrl };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +64,10 @@ export async function processAndUploadVideo(
|
|||||||
const filename = `finds/${findId}/video-${index}-${timestamp}.mp4`;
|
const filename = `finds/${findId}/video-${index}-${timestamp}.mp4`;
|
||||||
|
|
||||||
// Upload video directly (no processing on server to save resources)
|
// Upload video directly (no processing on server to save resources)
|
||||||
const url = await uploadToR2(file, filename, 'video/mp4');
|
const videoPath = await uploadToR2(file, filename, 'video/mp4');
|
||||||
|
|
||||||
|
// Generate signed URL for video (24 hour expiration)
|
||||||
|
const url = await getSignedR2Url(videoPath, 24 * 60 * 60);
|
||||||
|
|
||||||
// For video thumbnail, generate on client-side or use placeholder
|
// For video thumbnail, generate on client-side or use placeholder
|
||||||
// This keeps server-side processing minimal
|
// This keeps server-side processing minimal
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export async function uploadToR2(file: File, path: string, contentType: string):
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return `${R2_PUBLIC_URL}/${path}`;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteFromR2(path: string): Promise<void> {
|
export async function deleteFromR2(path: string): Promise<void> {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { PageServerLoad } from './$types';
|
|||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { find, findMedia, user } from '$lib/server/db/schema';
|
import { find, findMedia, user } from '$lib/server/db/schema';
|
||||||
import { eq, and, sql, desc } from 'drizzle-orm';
|
import { eq, and, sql, desc } from 'drizzle-orm';
|
||||||
|
import { getSignedR2Url } from '$lib/server/r2';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ locals, url }) => {
|
export const load: PageServerLoad = async ({ locals, url }) => {
|
||||||
if (!locals.user) {
|
if (!locals.user) {
|
||||||
@@ -105,11 +106,42 @@ export const load: PageServerLoad = async ({ locals, url }) => {
|
|||||||
{} as Record<string, typeof media>
|
{} as Record<string, typeof media>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Combine finds with their media
|
// Combine finds with their media and generate signed URLs
|
||||||
const findsWithMedia = finds.map((findItem) => ({
|
const findsWithMedia = await Promise.all(
|
||||||
...findItem,
|
finds.map(async (findItem) => {
|
||||||
media: mediaByFind[findItem.id] || []
|
const findMedia = mediaByFind[findItem.id] || [];
|
||||||
}));
|
|
||||||
|
// Generate signed URLs for all media items
|
||||||
|
const mediaWithSignedUrls = await Promise.all(
|
||||||
|
findMedia.map(async (mediaItem) => {
|
||||||
|
// Extract path from URL if it's still a full URL, otherwise use as-is
|
||||||
|
const path = mediaItem.url.startsWith('https://')
|
||||||
|
? mediaItem.url.split('/').slice(3).join('/')
|
||||||
|
: mediaItem.url;
|
||||||
|
|
||||||
|
const thumbnailPath = mediaItem.thumbnailUrl?.startsWith('https://')
|
||||||
|
? mediaItem.thumbnailUrl.split('/').slice(3).join('/')
|
||||||
|
: mediaItem.thumbnailUrl;
|
||||||
|
|
||||||
|
const [signedUrl, signedThumbnailUrl] = await Promise.all([
|
||||||
|
getSignedR2Url(path, 24 * 60 * 60), // 24 hours
|
||||||
|
thumbnailPath ? getSignedR2Url(thumbnailPath, 24 * 60 * 60) : Promise.resolve(null)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mediaItem,
|
||||||
|
url: signedUrl,
|
||||||
|
thumbnailUrl: signedThumbnailUrl
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...findItem,
|
||||||
|
media: mediaWithSignedUrls
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
finds: findsWithMedia
|
finds: findsWithMedia
|
||||||
|
|||||||
Reference in New Issue
Block a user