Skip to content

Instantly share code, notes, and snippets.

@gitaarik
Last active May 1, 2024 09:27
Show Gist options
  • Save gitaarik/59213791bc2e10977de81d90ac5a0fdb to your computer and use it in GitHub Desktop.
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
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);
}
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