UNPKG

@npio/filesystem

Version:

A free visual website editor, powered with your own SolidJS components.

181 lines (160 loc) 5.09 kB
import { ReadStream } from "fs"; import { mkdir, open, rm, stat } from "fs/promises"; import mime from "mime/lite"; import { dirname, isAbsolute, join } from "path"; import parseRange from "range-parser"; import { pipeline } from "stream/promises"; import { appendResponseHeader, getQuery, H3Event, sendNoContent, serveStatic, setResponseHeaders, setResponseStatus, } from "vinxi/http"; import { Driver } from "../types"; import { commonCacheAge, isFont, maxCacheAge } from "../utils"; export type LocalOptions = { publicPath: string; privatePath: string; baseUrl?: string; }; const normalizePath = (path: string) => { if (isAbsolute(path)) return path; return join(process.cwd(), path); }; export const createLocal = (options: LocalOptions) => { const publicPath = normalizePath(options.publicPath); const privatePath = normalizePath(options.privatePath); const targetPath = (name: string, private_ = false) => join(private_ ? privatePath : publicPath, name); return { publicUrl() { return options.baseUrl || "/"; }, async get(params) { const target = targetPath(params.name, params.private); try { const fileStat = await stat(target); return { lastModified: new Date(fileStat.mtime), size: fileStat.size, stream: async () => (await open(target, "r")).createReadStream(), }; } catch (err) {} }, async put(params) { const target = targetPath(params.name, params.private); // TODO: Optimize this (can be skipped on multiple uploads) await mkdir(dirname(target), { recursive: true }); const fd = await open(target, "w"); const fw = fd.createWriteStream({ autoClose: true }); try { await pipeline(params.stream, fw); } catch (error) { // Cleanup incomplete files await this.delete(params); throw new Error("Failed to save the file: " + error); } }, async delete(params) { const target = targetPath(params.name, params.private); await rm(target, { force: true, }); }, async deleteDir(params) { const target = targetPath(params.name, params.private); await rm(target, { recursive: true, force: true, }); }, } satisfies Driver; }; export const createLocalRequestHandler = (options: LocalOptions) => { const baseUrl = options.baseUrl || "/"; const publicPath = normalizePath(options.publicPath); const serveStorage = async (event: H3Event) => { if (!event.path.startsWith(baseUrl)) { return; } const filePath = (id: string) => { const file = id.slice(baseUrl.length); const filePath_ = join(publicPath, file); return filePath_; }; let stream: ReadStream; let rangeInfo: | { size: number; start: number; end: number; } | undefined; const result = await serveStatic(event, { fallthrough: true, getContents: (id) => { if (rangeInfo) { setResponseStatus(206); setResponseHeaders({ "Content-Range": `bytes ${rangeInfo.start}-${rangeInfo.end}/${rangeInfo.size}`, "Accept-Ranges": "bytes", "Content-Length": String(rangeInfo.end - rangeInfo.start + 1), }); } return stream; }, getMeta: async (id) => { const target = filePath(id); const stats = await stat(target).catch(() => {}); if (!stats || !stats.isFile()) { return; } /** * Inspired by * https://github.com/pillarjs/send/blob/7c92a68b67600992d877cf1869d171d9fb3a033f/index.js#L534 * https://medium.com/@vishal1909/how-to-handle-partial-content-in-node-js-8b0a5aea216 * https://github.com/phoenixinfotech1984/node-content-range */ const rangeHeader = event.headers.get("range"); if (rangeHeader) { const ranges = parseRange(stats.size, rangeHeader); if (ranges === -1) { setResponseStatus(416); appendResponseHeader("Content-Range", `bytes */${stats.size}`); return; } if (ranges !== -2 && ranges.length === 1) { rangeInfo = { start: ranges[0].start, end: ranges[0].end, size: stats.size, }; } } const useMaxAge = isFont(event.path) || getQuery().ts != null; appendResponseHeader( "Cache-Control", `public, max-age=${useMaxAge ? maxCacheAge : commonCacheAge}`, ); stream = (await open(target, "r")).createReadStream({ start: rangeInfo?.start, end: rangeInfo?.end, autoClose: true, }); return { type: mime.getType(id) || undefined, size: stats.size, mtime: stats.mtimeMs, }; }, }); if (result === false) { return sendNoContent(404); } return result; }; return serveStorage; };