fix asset upload rendering errors
This commit is contained in:
parent
b11aecffa4
commit
2a3b79df15
|
|
@ -10,15 +10,17 @@ function getAssetObjectName(uploadId: string) {
|
||||||
|
|
||||||
// when a user uploads an asset, we store it in the bucket. we only allow image and video assets.
|
// when a user uploads an asset, we store it in the bucket. we only allow image and video assets.
|
||||||
export async function handleAssetUpload(request: IRequest, env: Environment) {
|
export async function handleAssetUpload(request: IRequest, env: Environment) {
|
||||||
// If this is a preflight request, return appropriate CORS headers
|
// Add CORS headers that will be used for both success and error responses
|
||||||
|
const corsHeaders = {
|
||||||
|
'access-control-allow-origin': '*',
|
||||||
|
'access-control-allow-methods': 'GET, POST, HEAD, OPTIONS',
|
||||||
|
'access-control-allow-headers': '*',
|
||||||
|
'access-control-max-age': '86400',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle preflight
|
||||||
if (request.method === 'OPTIONS') {
|
if (request.method === 'OPTIONS') {
|
||||||
const headers = new Headers({
|
return new Response(null, { headers: corsHeaders })
|
||||||
'access-control-allow-origin': '*',
|
|
||||||
'access-control-allow-methods': 'POST, OPTIONS',
|
|
||||||
'access-control-allow-headers': 'content-type',
|
|
||||||
'access-control-max-age': '86400',
|
|
||||||
})
|
|
||||||
return new Response(null, { headers })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -26,21 +28,38 @@ export async function handleAssetUpload(request: IRequest, env: Environment) {
|
||||||
|
|
||||||
const contentType = request.headers.get('content-type') ?? ''
|
const contentType = request.headers.get('content-type') ?? ''
|
||||||
if (!contentType.startsWith('image/') && !contentType.startsWith('video/')) {
|
if (!contentType.startsWith('image/') && !contentType.startsWith('video/')) {
|
||||||
return error(400, 'Invalid content type')
|
return new Response('Invalid content type', {
|
||||||
|
status: 400,
|
||||||
|
headers: corsHeaders
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await env.TLDRAW_BUCKET.head(objectName)) {
|
if (await env.TLDRAW_BUCKET.head(objectName)) {
|
||||||
return error(409, 'Upload already exists')
|
return new Response('Upload already exists', {
|
||||||
|
status: 409,
|
||||||
|
headers: corsHeaders
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await env.TLDRAW_BUCKET.put(objectName, request.body, {
|
await env.TLDRAW_BUCKET.put(objectName, request.body, {
|
||||||
httpMetadata: request.headers,
|
httpMetadata: request.headers,
|
||||||
})
|
})
|
||||||
|
|
||||||
return { ok: true }
|
return new Response(JSON.stringify({ ok: true }), {
|
||||||
|
headers: {
|
||||||
|
...corsHeaders,
|
||||||
|
'content-type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Asset upload failed:', error);
|
console.error('Asset upload failed:', error)
|
||||||
return new Response(`Upload failed: ${(error as Error).message}`, { status: 500 });
|
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
...corsHeaders,
|
||||||
|
'content-type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,75 +69,103 @@ export async function handleAssetDownload(
|
||||||
env: Environment,
|
env: Environment,
|
||||||
ctx: ExecutionContext
|
ctx: ExecutionContext
|
||||||
) {
|
) {
|
||||||
const objectName = getAssetObjectName(request.params.uploadId)
|
// Define CORS headers to be used consistently
|
||||||
|
const corsHeaders = {
|
||||||
// if we have a cached response for this request (automatically handling ranges etc.), return it
|
'access-control-allow-origin': '*',
|
||||||
const cacheKey = new Request(request.url, { headers: request.headers })
|
'access-control-allow-methods': 'GET, HEAD, OPTIONS',
|
||||||
// @ts-ignore
|
'access-control-allow-headers': '*',
|
||||||
const cachedResponse = await caches.default.match(cacheKey)
|
'access-control-expose-headers': 'content-length, content-range',
|
||||||
if (cachedResponse) {
|
'access-control-max-age': '86400',
|
||||||
return cachedResponse
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if not, we try to fetch the asset from the bucket
|
// Handle preflight
|
||||||
const object = await env.TLDRAW_BUCKET.get(objectName, {
|
if (request.method === 'OPTIONS') {
|
||||||
range: request.headers,
|
return new Response(null, { headers: corsHeaders })
|
||||||
onlyIf: request.headers,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!object) {
|
|
||||||
return error(404)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// write the relevant metadata to the response headers
|
try {
|
||||||
const headers = new Headers()
|
const objectName = getAssetObjectName(request.params.uploadId)
|
||||||
object.writeHttpMetadata(headers)
|
|
||||||
|
|
||||||
// assets are immutable, so we can cache them basically forever:
|
// Handle cached response
|
||||||
headers.set('cache-control', 'public, max-age=31536000, immutable')
|
const cacheKey = new Request(request.url, { headers: request.headers })
|
||||||
headers.set('etag', object.httpEtag)
|
// @ts-ignore
|
||||||
|
const cachedResponse = await caches.default.match(cacheKey)
|
||||||
|
if (cachedResponse) {
|
||||||
|
const headers = new Headers(cachedResponse.headers)
|
||||||
|
Object.entries(corsHeaders).forEach(([key, value]) => headers.set(key, value))
|
||||||
|
return new Response(cachedResponse.body, {
|
||||||
|
status: cachedResponse.status,
|
||||||
|
headers
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Set comprehensive CORS headers for asset access
|
// Get from bucket
|
||||||
headers.set('access-control-allow-origin', '*')
|
const object = await env.TLDRAW_BUCKET.get(objectName, {
|
||||||
headers.set('access-control-allow-methods', 'GET, HEAD, OPTIONS')
|
range: request.headers,
|
||||||
headers.set('access-control-allow-headers', '*')
|
onlyIf: request.headers,
|
||||||
headers.set('access-control-expose-headers', 'content-length, content-range')
|
})
|
||||||
headers.set('cross-origin-resource-policy', 'cross-origin')
|
|
||||||
headers.set('cross-origin-opener-policy', 'same-origin')
|
|
||||||
headers.set('cross-origin-embedder-policy', 'require-corp')
|
|
||||||
|
|
||||||
// cloudflare doesn't set the content-range header automatically in writeHttpMetadata, so we
|
if (!object) {
|
||||||
// need to do it ourselves.
|
return new Response('Not Found', {
|
||||||
let contentRange
|
status: 404,
|
||||||
if (object.range) {
|
headers: corsHeaders
|
||||||
if ('suffix' in object.range) {
|
})
|
||||||
const start = object.size - object.range.suffix
|
}
|
||||||
const end = object.size - 1
|
|
||||||
contentRange = `bytes ${start}-${end}/${object.size}`
|
// Set up response headers
|
||||||
} else {
|
const headers = new Headers()
|
||||||
const start = object.range.offset ?? 0
|
object.writeHttpMetadata(headers)
|
||||||
const end = object.range.length ? start + object.range.length - 1 : object.size - 1
|
Object.entries(corsHeaders).forEach(([key, value]) => headers.set(key, value))
|
||||||
if (start !== 0 || end !== object.size - 1) {
|
|
||||||
|
headers.set('cache-control', 'public, max-age=31536000, immutable')
|
||||||
|
headers.set('etag', object.httpEtag)
|
||||||
|
headers.set('cross-origin-resource-policy', 'cross-origin')
|
||||||
|
headers.set('cross-origin-opener-policy', 'same-origin')
|
||||||
|
headers.set('cross-origin-embedder-policy', 'require-corp')
|
||||||
|
|
||||||
|
// Handle content range
|
||||||
|
let contentRange
|
||||||
|
if (object.range) {
|
||||||
|
if ('suffix' in object.range) {
|
||||||
|
const start = object.size - object.range.suffix
|
||||||
|
const end = object.size - 1
|
||||||
contentRange = `bytes ${start}-${end}/${object.size}`
|
contentRange = `bytes ${start}-${end}/${object.size}`
|
||||||
|
} else {
|
||||||
|
const start = object.range.offset ?? 0
|
||||||
|
const end = object.range.length ? start + object.range.length - 1 : object.size - 1
|
||||||
|
if (start !== 0 || end !== object.size - 1) {
|
||||||
|
contentRange = `bytes ${start}-${end}/${object.size}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (contentRange) {
|
||||||
|
headers.set('content-range', contentRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = 'body' in object && object.body ? object.body : null
|
||||||
|
const status = body ? (contentRange ? 206 : 200) : 304
|
||||||
|
|
||||||
|
// Cache successful responses
|
||||||
|
if (status === 200) {
|
||||||
|
const [cacheBody, responseBody] = body!.tee()
|
||||||
|
// @ts-ignore
|
||||||
|
ctx.waitUntil(caches.default.put(cacheKey, new Response(cacheBody, { headers, status })))
|
||||||
|
return new Response(responseBody, { headers, status })
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(body, { headers, status })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Asset download failed:', error)
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: (error as Error).message }),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
...corsHeaders,
|
||||||
|
'content-type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentRange) {
|
|
||||||
headers.set('content-range', contentRange)
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure we get the correct body/status for the response
|
|
||||||
const body = 'body' in object && object.body ? object.body : null
|
|
||||||
const status = body ? (contentRange ? 206 : 200) : 304
|
|
||||||
|
|
||||||
// we only cache complete (200) responses
|
|
||||||
if (status === 200) {
|
|
||||||
const [cacheBody, responseBody] = body!.tee()
|
|
||||||
// @ts-ignore
|
|
||||||
ctx.waitUntil(caches.default.put(cacheKey, new Response(cacheBody, { headers, status })))
|
|
||||||
return new Response(responseBody, { headers, status })
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(body, { headers, status })
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,18 +37,19 @@ const { preflight, corsify } = cors({
|
||||||
return origin
|
return origin
|
||||||
}
|
}
|
||||||
|
|
||||||
// For development - check if it's a localhost or local IP
|
// For development - check if it's a localhost or local IP (both http and https)
|
||||||
if (
|
if (
|
||||||
origin.match(
|
origin.match(
|
||||||
/^http:\/\/(localhost|127\.0\.0\.1|192\.168\.|169\.254\.|10\.)/,
|
/^https?:\/\/(localhost|127\.0\.0\.1|192\.168\.|169\.254\.|10\.)/,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return origin
|
return origin
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined
|
// If no match found, return * to allow all origins
|
||||||
|
return "*"
|
||||||
},
|
},
|
||||||
allowMethods: ["GET", "POST", "OPTIONS", "UPGRADE"],
|
allowMethods: ["GET", "POST", "HEAD", "OPTIONS", "UPGRADE"],
|
||||||
allowHeaders: [
|
allowHeaders: [
|
||||||
"Content-Type",
|
"Content-Type",
|
||||||
"Authorization",
|
"Authorization",
|
||||||
|
|
@ -62,7 +63,8 @@ const { preflight, corsify } = cors({
|
||||||
"Content-Range",
|
"Content-Range",
|
||||||
"Range",
|
"Range",
|
||||||
"If-None-Match",
|
"If-None-Match",
|
||||||
"If-Modified-Since"
|
"If-Modified-Since",
|
||||||
|
"*"
|
||||||
],
|
],
|
||||||
maxAge: 86400,
|
maxAge: 86400,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue