initial
This commit is contained in:
2
ts/utils/index.ts
Normal file
2
ts/utils/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { getMimeType, isTextMimeType } from './utils.mime.js';
|
||||
export { generateETag, generateStrongETag, matchesETag } from './utils.etag.js';
|
||||
45
ts/utils/utils.etag.ts
Normal file
45
ts/utils/utils.etag.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* ETag generation utilities
|
||||
*/
|
||||
|
||||
import type * as plugins from '../plugins.js';
|
||||
|
||||
/**
|
||||
* Generate ETag from file stats
|
||||
* Uses weak ETag format: W/"size-mtime"
|
||||
*/
|
||||
export function generateETag(stat: { size: number; mtime: Date }): string {
|
||||
const mtime = stat.mtime.getTime().toString(16);
|
||||
const size = stat.size.toString(16);
|
||||
return `W/"${size}-${mtime}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate strong ETag from content
|
||||
* Uses hash of content
|
||||
*/
|
||||
export async function generateStrongETag(content: Uint8Array): Promise<string> {
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', content as unknown as ArrayBuffer);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
return `"${hashHex.slice(0, 32)}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ETag matches
|
||||
*/
|
||||
export function matchesETag(etag: string, ifNoneMatch: string | null): boolean {
|
||||
if (!ifNoneMatch) return false;
|
||||
|
||||
// Handle multiple ETags
|
||||
const etags = ifNoneMatch.split(',').map(e => e.trim());
|
||||
|
||||
// Wildcard match
|
||||
if (etags.includes('*')) return true;
|
||||
|
||||
// Weak comparison (ignore W/ prefix)
|
||||
const normalizeETag = (e: string) => e.replace(/^W\//, '');
|
||||
const normalizedETag = normalizeETag(etag);
|
||||
|
||||
return etags.some(e => normalizeETag(e) === normalizedETag);
|
||||
}
|
||||
101
ts/utils/utils.mime.ts
Normal file
101
ts/utils/utils.mime.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* MIME type detection based on file extension
|
||||
*/
|
||||
|
||||
const MIME_TYPES: Record<string, string> = {
|
||||
// Text
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.htm': 'text/html; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.js': 'text/javascript; charset=utf-8',
|
||||
'.mjs': 'text/javascript; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
'.xml': 'application/xml; charset=utf-8',
|
||||
'.txt': 'text/plain; charset=utf-8',
|
||||
'.md': 'text/markdown; charset=utf-8',
|
||||
'.csv': 'text/csv; charset=utf-8',
|
||||
'.yaml': 'text/yaml; charset=utf-8',
|
||||
'.yml': 'text/yaml; charset=utf-8',
|
||||
|
||||
// Images
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.webp': 'image/webp',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
'.bmp': 'image/bmp',
|
||||
'.avif': 'image/avif',
|
||||
|
||||
// Fonts
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.ttf': 'font/ttf',
|
||||
'.otf': 'font/otf',
|
||||
'.eot': 'application/vnd.ms-fontobject',
|
||||
|
||||
// Audio
|
||||
'.mp3': 'audio/mpeg',
|
||||
'.wav': 'audio/wav',
|
||||
'.ogg': 'audio/ogg',
|
||||
'.m4a': 'audio/mp4',
|
||||
'.flac': 'audio/flac',
|
||||
'.aac': 'audio/aac',
|
||||
|
||||
// Video
|
||||
'.mp4': 'video/mp4',
|
||||
'.webm': 'video/webm',
|
||||
'.ogv': 'video/ogg',
|
||||
'.avi': 'video/x-msvideo',
|
||||
'.mov': 'video/quicktime',
|
||||
'.mkv': 'video/x-matroska',
|
||||
|
||||
// Documents
|
||||
'.pdf': 'application/pdf',
|
||||
'.doc': 'application/msword',
|
||||
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'.xls': 'application/vnd.ms-excel',
|
||||
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'.ppt': 'application/vnd.ms-powerpoint',
|
||||
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
|
||||
// Archives
|
||||
'.zip': 'application/zip',
|
||||
'.tar': 'application/x-tar',
|
||||
'.gz': 'application/gzip',
|
||||
'.rar': 'application/vnd.rar',
|
||||
'.7z': 'application/x-7z-compressed',
|
||||
|
||||
// Source maps
|
||||
'.map': 'application/json',
|
||||
|
||||
// TypeScript
|
||||
'.ts': 'text/typescript; charset=utf-8',
|
||||
'.tsx': 'text/typescript; charset=utf-8',
|
||||
'.d.ts': 'text/typescript; charset=utf-8',
|
||||
|
||||
// WebAssembly
|
||||
'.wasm': 'application/wasm',
|
||||
|
||||
// Manifest
|
||||
'.webmanifest': 'application/manifest+json',
|
||||
};
|
||||
|
||||
/**
|
||||
* Get MIME type for a file path
|
||||
*/
|
||||
export function getMimeType(filePath: string): string {
|
||||
const ext = filePath.toLowerCase().match(/\.[^.]+$/)?.[0] ?? '';
|
||||
return MIME_TYPES[ext] ?? 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if MIME type is text-based
|
||||
*/
|
||||
export function isTextMimeType(mimeType: string): boolean {
|
||||
return mimeType.startsWith('text/') ||
|
||||
mimeType.includes('json') ||
|
||||
mimeType.includes('xml') ||
|
||||
mimeType.includes('javascript');
|
||||
}
|
||||
Reference in New Issue
Block a user