From 1d5ddd544e91c9b6c25b5fed20680dfffe4b8160 Mon Sep 17 00:00:00 2001 From: ivy Date: Wed, 23 Aug 2023 12:13:14 -0700 Subject: [PATCH 1/3] refactor: files extention feat: files ext now look for index.html if no file is specified feat: files ext now look for 404.html if file could not be found --- ext/files.ts | 211 ++++++++++++++++++++++++++++----------------------- 1 file changed, 114 insertions(+), 97 deletions(-) diff --git a/ext/files.ts b/ext/files.ts index e23b357..a399e5f 100644 --- a/ext/files.ts +++ b/ext/files.ts @@ -2,119 +2,136 @@ import { R2Bucket } from 'https://cdn.jsdelivr.net/npm/@cloudflare/workers-types@4.20230821.0/index.ts' import { join } from 'https://deno.land/std@0.199.0/path/mod.ts' import { createExtension } from '../extensions.ts' +import { AppContext } from '../mod.ts' // An extension to serve static files from Cloudflare R2, an S3 bucket, or the local file system. +type FilesCommonType = { + cacheControl?: string + etag?: boolean +} + +type FsType = { + type?: 'fs' + directory: string +} + +type R2Type = { + type: 'r2' + name: string +} + +type S3Type = { + type: 's3' + endpoint: string + bucketName: string + accessKeyId: string + secretAccessKey: string +} + /** * An extension to serve static files from Cloudflare R2 or the local file system. * * @since v1.2 */ export const files = createExtension<{ - serve: - & { - cacheControl?: string - etag?: boolean - } - & ( - | { - directory: string - type?: 'fs' - } - | { - name: string - type: 'r2' - } - ) - // | { - // endpoint: string - // bucketName: string - // accessKeyId: string - // secretAccessKey: string - // type: 's3' - // } + serve: FilesCommonType & (FsType | R2Type | S3Type) }>({ - async onRequest({ + onRequest({ app, prefix, _: { serve, }, }) { - if (serve.type === 'r2') { - if (!app.env) { - throw new Error( - 'You need to use the Cloudflare Workers runtime to serve static files from an R2 bucket!', - ) - } - - const bucket = app.env[serve.name] as R2Bucket - - const object = await bucket.get( - prefix !== '*' - ? app.request.pathname.substring(prefix.length + 1) - : app.request.pathname, - ) - - if (object === null) { - return - } - - return new Response(object.body as ReadableStream, { - headers: { - ...(serve.etag !== false && { etag: object.httpEtag }), - 'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m - }, - }) - } else { - const path = join( - serve.directory, - prefix !== '*' - ? app.request.pathname.substring(prefix.length + 1) - : app.request.pathname, - ) - - let exists, - stat: Deno.FileInfo | undefined - - try { - stat = await Deno.lstat( - path, - ) - - exists = stat.isFile - } catch (_) { - exists = false - } - - if (!exists || !stat) { - return - } - - const file = await Deno.open( - path, - { read: true }, - ) - - return new Response(file.readable, { - headers: { - ...(serve.etag !== false && - { - etag: Array.prototype.map.call( - new Uint8Array( - await crypto.subtle.digest( - { name: 'SHA-1' }, - new TextEncoder().encode( - `${stat.birthtime?.getTime()}:${stat.mtime?.getTime()}:${stat.size}`, - ), - ), - ), - (x) => ('00' + x.toString(16)).slice(-2), - ).join(''), - }), - 'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m - }, - }) + switch (serve.type) { + case 'r2': + return handleR2Files(app, serve, prefix) + case 's3': + throw new Error('S3 is not yet supported!') + case 'fs': + default: + return handleFsFiles(app, serve, prefix) } }, }) + +async function handleR2Files( + app: AppContext, + serve: FilesCommonType & R2Type, + prefix: string, +) { + if (app.runtime !== 'cloudflare' || !app.env) { + throw new Error( + 'You need to use the Cloudflare Workers runtime to serve static files from an R2 bucket!', + ) + } + + const bucket = app.env[serve.name] as R2Bucket + + const object = await bucket.get( + prefix !== '*' + ? app.request.pathname.substring(prefix.length + 1) + : app.request.pathname, + ) + + if (!object) return new Response(null, { status: 404 }) + + return new Response(object.body as ReadableStream, { + headers: { + ...(serve.etag !== false && { etag: object.httpEtag }), + 'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m + }, + }) +} + +async function handleFsFiles( + app: AppContext, + serve: FilesCommonType & FsType, + prefix: string, +) { + const path = join( + serve.directory, + prefix !== '*' + ? app.request.pathname.substring(prefix.length + 1) + : app.request.pathname, + ) + + let stat: Deno.FileInfo + let file: Deno.FsFile + + try { + stat = await Deno.lstat(path) + if (stat.isDirectory) { + stat = await Deno.lstat(join(path, 'index.html')) + file = await Deno.open(join(path, 'index.html'), { read: true }) + } else { + file = await Deno.open(path, { read: true }) + } + } catch { + try { + stat = await Deno.lstat(join(serve.directory, '404.html')) + file = await Deno.open(join(serve.directory, '404.html'), { read: true }) + } catch { + return + } + } + + return new Response(file.readable, { + headers: { + ...(serve.etag !== false && { etag: await etag(stat) }), + 'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m + }, + }) +} + +async function etag(stat: Deno.FileInfo) { + const encoder = new TextEncoder() + const data = encoder.encode( + `${stat.birthtime?.getTime()}:${stat.mtime?.getTime()}:${stat.size}`, + ) + const hash = await crypto.subtle.digest({ name: 'SHA-1' }, data) + const hashArray = Array.from(new Uint8Array(hash)) + const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') + return hashHex +} From 6fe382ab1f503a510696aeae3611c50f54faeb7d Mon Sep 17 00:00:00 2001 From: ivy Date: Wed, 23 Aug 2023 12:53:34 -0700 Subject: [PATCH 2/3] fix: requested changes --- ext/files.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ext/files.ts b/ext/files.ts index a399e5f..eb2e792 100644 --- a/ext/files.ts +++ b/ext/files.ts @@ -6,22 +6,22 @@ import { AppContext } from '../mod.ts' // An extension to serve static files from Cloudflare R2, an S3 bucket, or the local file system. -type FilesCommonType = { +type GeneralOptions = { cacheControl?: string etag?: boolean } -type FsType = { +type FsOptions = { type?: 'fs' directory: string } -type R2Type = { +type R2Options = { type: 'r2' name: string } -type S3Type = { +type S3Options = { type: 's3' endpoint: string bucketName: string @@ -35,7 +35,7 @@ type S3Type = { * @since v1.2 */ export const files = createExtension<{ - serve: FilesCommonType & (FsType | R2Type | S3Type) + serve: GeneralOptions & (FsOptions | R2Options | S3Options) }>({ onRequest({ app, @@ -58,7 +58,7 @@ export const files = createExtension<{ async function handleR2Files( app: AppContext, - serve: FilesCommonType & R2Type, + serve: GeneralOptions & R2Options, prefix: string, ) { if (app.runtime !== 'cloudflare' || !app.env) { @@ -87,7 +87,7 @@ async function handleR2Files( async function handleFsFiles( app: AppContext, - serve: FilesCommonType & FsType, + serve: GeneralOptions & FsOptions, prefix: string, ) { const path = join( From 0aa76f363cbe5d67cc111f33388c74f7a951443d Mon Sep 17 00:00:00 2001 From: ivy Date: Wed, 23 Aug 2023 14:39:17 -0700 Subject: [PATCH 3/3] feat: add index and 404 page looking for r2 handling --- ext/files.ts | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/ext/files.ts b/ext/files.ts index eb2e792..40d18ae 100644 --- a/ext/files.ts +++ b/ext/files.ts @@ -74,15 +74,35 @@ async function handleR2Files( ? app.request.pathname.substring(prefix.length + 1) : app.request.pathname, ) - - if (!object) return new Response(null, { status: 404 }) - - return new Response(object.body as ReadableStream, { - headers: { - ...(serve.etag !== false && { etag: object.httpEtag }), - 'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m - }, - }) + if (object) { + return new Response(object.body as ReadableStream, { + headers: { + ...(serve.etag !== false && { etag: object.httpEtag }), + 'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m + }, + }) + } else { + const indexPath = join(app.request.pathname, 'index.html') + const indexObject = await bucket.get(indexPath) + if (indexObject) { + return new Response(indexObject.body as ReadableStream, { + headers: { + ...(serve.etag !== false && { etag: indexObject.httpEtag }), + 'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m + }, + }) + } else { + const errorPath = join(prefix, '404.html') + const errorObject = await bucket.get(errorPath) + if (errorObject) { + return new Response(errorObject.body as ReadableStream, { + headers: { + 'cache-control': serve.cacheControl ?? 's-maxage=300', // 5m + }, + }) + } + } + } } async function handleFsFiles(