Luigit
repositories / will

will

owned by admin

src/service/static.ts

Raw
import { createReadStream } from 'node:fs'
import { readFile, stat } from 'node:fs/promises'
import type { ServerResponse } from 'node:http'
import { extname, resolve } from 'node:path'
import { HttpError, sendError } from '../shared/http.ts'

const CONTENT_TYPES: Record<string, string> = {
  '.html': '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',
  '.svg': 'image/svg+xml',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.jpeg': 'image/jpeg',
  '.webp': 'image/webp',
  '.ico': 'image/x-icon',
  '.txt': 'text/plain; charset=utf-8',
  '.woff2': 'font/woff2',
}

/** Serve a file under root with path-traversal protection. */
export async function serveFile(
  res: ServerResponse,
  root: string,
  unsafePath: string,
  options: { immutable?: boolean } = {},
): Promise<void> {
  const rootResolved = resolve(root)
  const target = resolve(rootResolved, `./${unsafePath.replace(/^\/+/, '')}`)
  if (target !== rootResolved && !target.startsWith(`${rootResolved}/`)) {
    throw new HttpError(403, 'forbidden')
  }
  let info: import('node:fs').Stats
  try {
    info = await stat(target)
  } catch {
    throw new HttpError(404, `not found: ${unsafePath}`)
  }
  if (!info.isFile()) throw new HttpError(404, `not a file: ${unsafePath}`)
  const type = CONTENT_TYPES[extname(target)] ?? 'application/octet-stream'
  res.writeHead(200, {
    'content-type': type,
    'content-length': info.size,
    'cache-control': options.immutable ? 'public, max-age=31536000, immutable' : 'no-cache',
  })
  await new Promise<void>((done) => {
    const stream = createReadStream(target)
    stream.on('error', () => {
      if (!res.headersSent) sendError(res, 500, 'read error')
      else res.end()
      done()
    })
    stream.on('end', () => done())
    stream.pipe(res)
  })
}

export async function readJsonFile(path: string): Promise<unknown> {
  const raw = await readFile(path, 'utf8')
  return JSON.parse(raw)
}