| 33 | |
| 34 | // when a user downloads an asset, we retrieve it from the bucket. we also cache the response for performance. |
| 35 | export async function handleAssetDownload(request: IRequest, env: Env, ctx: ExecutionContext) { |
| 36 | const objectName = getAssetObjectName(request.params.uploadId) |
| 37 | |
| 38 | // if we have a cached response for this request (automatically handling ranges etc.), return it |
| 39 | const cacheKey = new Request(request.url, { headers: request.headers }) |
| 40 | const cachedResponse = await caches.default.match(cacheKey) |
| 41 | if (cachedResponse) { |
| 42 | return cachedResponse |
| 43 | } |
| 44 | |
| 45 | // if not, we try to fetch the asset from the bucket |
| 46 | const object = await env.TLDRAW_BUCKET.get(objectName, { |
| 47 | range: request.headers, |
| 48 | onlyIf: request.headers, |
| 49 | }) |
| 50 | |
| 51 | if (!object) { |
| 52 | return error(404) |
| 53 | } |
| 54 | |
| 55 | // write the relevant metadata to the response headers |
| 56 | const headers = new Headers() |
| 57 | object.writeHttpMetadata(headers) |
| 58 | |
| 59 | // assets are immutable, so we can cache them basically forever: |
| 60 | headers.set('cache-control', 'public, max-age=31536000, immutable') |
| 61 | headers.set('etag', object.httpEtag) |
| 62 | |
| 63 | // we set CORS headers so all clients can access assets. we do this here so our `cors` helper in |
| 64 | // worker.ts doesn't try to set extra cors headers on responses that have been read from the |
| 65 | // cache, which isn't allowed by cloudflare. |
| 66 | headers.set('access-control-allow-origin', '*') |
| 67 | |
| 68 | // Prevent XSS from user-uploaded SVGs (or any file served with an executable content-type). |
| 69 | headers.set('content-security-policy', "default-src 'none'") |
| 70 | headers.set('x-content-type-options', 'nosniff') |
| 71 | |
| 72 | // cloudflare doesn't set the content-range header automatically in writeHttpMetadata, so we |
| 73 | // need to do it ourselves. |
| 74 | let contentRange |
| 75 | if (object.range) { |
| 76 | if ('suffix' in object.range) { |
| 77 | const start = object.size - object.range.suffix |
| 78 | const end = object.size - 1 |
| 79 | contentRange = `bytes ${start}-${end}/${object.size}` |
| 80 | } else { |
| 81 | const start = object.range.offset ?? 0 |
| 82 | const end = object.range.length ? start + object.range.length - 1 : object.size - 1 |
| 83 | if (start !== 0 || end !== object.size - 1) { |
| 84 | contentRange = `bytes ${start}-${end}/${object.size}` |
| 85 | } |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | if (contentRange) { |
| 90 | headers.set('content-range', contentRange) |
| 91 | } |
| 92 | |