188 lines
5.5 KiB
TypeScript
188 lines
5.5 KiB
TypeScript
/**
|
|
* Cloudflare Worker for generating signed URLs for R2 objects
|
|
*
|
|
* This worker provides secure access to private R2 buckets by generating
|
|
* time-limited signed URLs.
|
|
*
|
|
* Endpoint: /signed-url?file=<path>&bucket=<bucket>&expires=<seconds>
|
|
*
|
|
* Example:
|
|
* GET /signed-url?file=manuals/Crane/manual.pdf&bucket=vending-vm-manuals&expires=3600
|
|
*
|
|
* Returns: { url: "https://signed-url..." }
|
|
*/
|
|
|
|
export interface Env {
|
|
// R2 bucket bindings (configured in wrangler.toml)
|
|
MANUALS_BUCKET?: R2Bucket
|
|
THUMBNAILS_BUCKET?: R2Bucket
|
|
// Default bucket if not specified
|
|
DEFAULT_BUCKET?: R2Bucket
|
|
}
|
|
|
|
export default {
|
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
const url = new URL(request.url)
|
|
|
|
// Only allow GET requests
|
|
if (request.method !== "GET") {
|
|
return new Response("Method not allowed", { status: 405 })
|
|
}
|
|
|
|
// Handle signed URL generation
|
|
if (url.pathname === "/signed-url") {
|
|
return handleSignedUrl(request, env, url)
|
|
}
|
|
|
|
// Health check endpoint
|
|
if (url.pathname === "/health") {
|
|
return new Response(
|
|
JSON.stringify({ status: "ok", timestamp: new Date().toISOString() }),
|
|
{
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
)
|
|
}
|
|
|
|
return new Response("Not found", { status: 404 })
|
|
},
|
|
}
|
|
|
|
/**
|
|
* Generate a signed URL for an R2 object
|
|
*/
|
|
async function handleSignedUrl(
|
|
request: Request,
|
|
env: Env,
|
|
url: URL
|
|
): Promise<Response> {
|
|
const filePath = url.searchParams.get("file")
|
|
const bucketName = url.searchParams.get("bucket") || "manuals"
|
|
const expiresIn = parseInt(url.searchParams.get("expires") || "3600", 10) // Default 1 hour
|
|
|
|
if (!filePath) {
|
|
return new Response(
|
|
JSON.stringify({ error: "file parameter is required" }),
|
|
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
)
|
|
}
|
|
|
|
// Validate expires parameter
|
|
if (isNaN(expiresIn) || expiresIn < 60 || expiresIn > 604800) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
error:
|
|
"expires must be between 60 and 604800 seconds (1 minute to 7 days)",
|
|
}),
|
|
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
)
|
|
}
|
|
|
|
// Get the appropriate bucket
|
|
let bucket: R2Bucket | undefined
|
|
if (bucketName === "manuals" && env.MANUALS_BUCKET) {
|
|
bucket = env.MANUALS_BUCKET
|
|
} else if (bucketName === "thumbnails" && env.THUMBNAILS_BUCKET) {
|
|
bucket = env.THUMBNAILS_BUCKET
|
|
} else if (env.DEFAULT_BUCKET) {
|
|
bucket = env.DEFAULT_BUCKET
|
|
} else {
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: `Bucket '${bucketName}' not found or not configured`,
|
|
}),
|
|
{ status: 404, headers: { "Content-Type": "application/json" } }
|
|
)
|
|
}
|
|
|
|
try {
|
|
// Generate signed URL
|
|
// Note: R2 signed URLs are generated using the bucket's presigned URL method
|
|
// This is a simplified example - actual implementation may vary based on R2 API
|
|
const signedUrl = await bucket.createMultipartUpload(filePath, {
|
|
httpMetadata: {
|
|
contentType: getContentType(filePath),
|
|
},
|
|
})
|
|
|
|
// For R2, we need to use the R2 API to generate presigned URLs
|
|
// This is a placeholder - actual R2 signed URL generation may require
|
|
// using the R2 API directly or a different method
|
|
const object = await bucket.get(filePath)
|
|
|
|
if (!object) {
|
|
return new Response(JSON.stringify({ error: "File not found" }), {
|
|
status: 404,
|
|
headers: { "Content-Type": "application/json" },
|
|
})
|
|
}
|
|
|
|
// Generate presigned URL (this is a simplified example)
|
|
// In production, you would use R2's actual presigned URL API
|
|
const presignedUrl = await generatePresignedUrl(bucket, filePath, expiresIn)
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
url: presignedUrl,
|
|
expiresIn,
|
|
expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(),
|
|
}),
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "GET",
|
|
},
|
|
}
|
|
)
|
|
} catch (error) {
|
|
console.error("Error generating signed URL:", error)
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: "Failed to generate signed URL",
|
|
message: error instanceof Error ? error.message : "Unknown error",
|
|
}),
|
|
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a presigned URL for R2 object
|
|
* Note: This is a placeholder - actual implementation depends on R2 API
|
|
*/
|
|
async function generatePresignedUrl(
|
|
bucket: R2Bucket,
|
|
key: string,
|
|
expiresIn: number
|
|
): Promise<string> {
|
|
// R2 doesn't have a direct presigned URL API like S3
|
|
// You would need to use R2's public URL with custom domain or
|
|
// implement a custom signing mechanism
|
|
//
|
|
// For now, this returns a placeholder URL
|
|
// In production, you would:
|
|
// 1. Use R2's public bucket URL if bucket is public
|
|
// 2. Or implement custom signing using R2's API
|
|
// 3. Or use a custom domain with R2
|
|
|
|
throw new Error(
|
|
"Presigned URL generation not yet implemented. Use public buckets or implement custom signing."
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Get content type based on file extension
|
|
*/
|
|
function getContentType(filename: string): string {
|
|
const ext = filename.split(".").pop()?.toLowerCase()
|
|
const types: Record<string, string> = {
|
|
pdf: "application/pdf",
|
|
jpg: "image/jpeg",
|
|
jpeg: "image/jpeg",
|
|
png: "image/png",
|
|
webp: "image/webp",
|
|
gif: "image/gif",
|
|
}
|
|
return types[ext || ""] || "application/octet-stream"
|
|
}
|