@npio/filesystem
Version:
A free visual website editor, powered with your own SolidJS components.
181 lines (160 loc) • 5.09 kB
text/typescript
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;
};