yiekheng 57255e2624 Add signed R2 URLs, batched page fetching, and 3D chapter badges
- Sign all image URLs server-side with 60s expiry presigned URLs
- Add /api/pages endpoint for batched page fetching (7 per batch)
- PageReader prefetches next batch when user scrolls to 3rd page
- Move chapter count badge outside overflow-hidden for 3D effect
- Fix missing URL signing on search and genre pages
- Extract signCoverUrls helper to reduce duplication
- Clamp API limit param to prevent abuse

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:15:01 +08:00

55 lines
1.4 KiB
TypeScript

import {
S3Client,
PutObjectCommand,
GetObjectCommand,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({
region: "auto",
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY!,
secretAccessKey: process.env.R2_SECRET_KEY!,
},
});
export async function getPresignedUploadUrl(key: string) {
const command = new PutObjectCommand({
Bucket: process.env.R2_BUCKET!,
Key: key,
ContentType: "image/webp",
});
return getSignedUrl(s3, command, { expiresIn: 3600 });
}
export function getPublicUrl(key: string) {
return `${process.env.R2_PUBLIC_URL}/${key}`;
}
export async function getPresignedReadUrl(key: string) {
const command = new GetObjectCommand({
Bucket: process.env.R2_BUCKET!,
Key: key,
});
return getSignedUrl(s3, command, { expiresIn: 60 });
}
export async function signUrl(publicUrl: string) {
const prefix = process.env.R2_PUBLIC_URL!;
if (!publicUrl.startsWith(prefix)) return publicUrl;
const key = publicUrl.replace(prefix, "").replace(/^\//, "");
return getPresignedReadUrl(key);
}
export async function signCoverUrls<T extends { coverUrl: string }>(
items: T[]
): Promise<T[]> {
return Promise.all(
items.map(async (item) => ({
...item,
coverUrl: await signUrl(item.coverUrl),
}))
);
}