Last active
May 1, 2024 09:27
-
-
Save gitaarik/59213791bc2e10977de81d90ac5a0fdb to your computer and use it in GitHub Desktop.
SvelteKit serve file from filesystem with ETag header for efficient client caching, useful for private static / uploaded files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { isAuthenticatedForCookie } from "$lib/core/auth"; | |
import { readAsset } from "$lib/helpers/read-asset"; | |
/** | |
* @type {Object.<string, string>} | |
*/ | |
const fileImports = import.meta.glob( | |
"$lib/files/**/**.{jpg,jpeg,png,gif,webp,avif,svg,pdf}", | |
{ | |
eager: true, | |
import: "default", | |
query: "?url", | |
}, | |
); | |
/** | |
* @param {import("./$types").RequestEvent} requestEvent | |
*/ | |
export async function GET({ cookies, params, setHeaders }) { | |
if (!isAuthenticatedForCookie(cookies)) { | |
return new Response("No access", { status: 403 }); | |
} | |
const filePath = "/src/lib/files/" + params.slug; | |
if (!(filePath in fileImports)) { | |
return new Response("Resource not found", { status: 404 }); | |
} | |
const file = fileImports[filePath]; | |
const assetData = readAsset(file); | |
const headers = { | |
"Content-Length": String(assetData.length), | |
"Content-Type": String(assetData.mimeType), | |
ETag: await assetData.getEtag(), | |
}; | |
setHeaders(headers); | |
return new Response(assetData.contents); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import crypto from "crypto"; | |
import { DEV } from "esm-env"; | |
import { base } from "__sveltekit/paths"; | |
import { read_implementation, manifest } from "__sveltekit/server"; | |
/** | |
* @type {Object.<string, string>} | |
*/ | |
const fileEtags = {}; | |
/** | |
* @typedef {Object} AssetData | |
* @property {Number} length | |
* @property {String} mimeType | |
* @property {Function} getEtag | |
* @property {Blob|ArrayBuffer|DataView|FormData|ReadableStream|URLSearchParams|String} contents | |
*/ | |
/** | |
* @param {String} asset | |
* @returns {AssetData} | |
*/ | |
export function readAsset(asset) { | |
if (asset.startsWith("data:")) { | |
// Inline data URL | |
return readDataAsset(asset); | |
} | |
const file = | |
DEV && asset.startsWith("/@fs") ? asset : asset.slice(base.length + 1); | |
// `read_implementation()` is also used by `read()` from `$app/server` | |
// https://kit.svelte.dev/docs/modules#$app-server-read | |
// https://www.youtube.com/watch?v=m4G-6dyF1MU | |
// return read_implementation(file); | |
if (file in manifest._.server_assets) { | |
const length = manifest._.server_assets[file]; | |
const mimeType = manifest.mimeTypes[file.slice(file.lastIndexOf("."))]; | |
return { | |
length: length, | |
mimeType: mimeType, | |
contents: read_implementation(file), | |
getEtag: async () => getEtagForFile(file), | |
}; | |
} | |
throw new Error(`Asset does not exist: ${file}`); | |
} | |
/** | |
* @param {string} asset | |
* @returns {AssetData} | |
*/ | |
function readDataAsset(asset) { | |
const firstComma = asset.indexOf(","); | |
const header = asset.slice(0, firstComma); | |
const data = asset.slice(firstComma + 1); | |
const mimeType = | |
header.split(";")[0].slice("data:".length) || "application/octet-stream"; | |
if (header.endsWith(";base64")) { | |
const decoded = b64_decode(data); | |
return { | |
length: decoded.byteLength, | |
mimeType: mimeType, | |
contents: decoded, | |
getEtag: async () => getEtagForStr(data), | |
}; | |
} | |
const decoded = decodeURIComponent(data); | |
return { | |
length: decoded.length, | |
mimeType: mimeType, | |
contents: decoded, | |
getEtag: async () => getEtagForStr(data), | |
}; | |
} | |
/** | |
* @param {String} file | |
*/ | |
async function getEtagForFile(file) { | |
if (file in fileEtags) { | |
return fileEtags[file]; | |
} | |
const hash = crypto.createHash("sha256"); | |
for await (const chunk of read_implementation(file)) { | |
hash.update(chunk); | |
} | |
const etag = hash.digest("hex"); | |
// Cache for later usage | |
Object.assign(fileEtags, { [file]: etag }); | |
return etag; | |
} | |
/** | |
* @param {String} str | |
*/ | |
function getEtagForStr(str) { | |
const hash = crypto.createHash("sha256"); | |
hash.update(str); | |
return hash.digest("hex"); | |
} | |
/** | |
* @param {String} text | |
* @returns {ArrayBufferLike} | |
*/ | |
function b64_decode(text) { | |
const d = atob(text); | |
const u8 = new Uint8Array(d.length); | |
for (let i = 0; i < d.length; i++) { | |
u8[i] = d.charCodeAt(i); | |
} | |
return u8.buffer; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment