@alepha/file
Version:
Helpers for creating and managing file-like objects seamlessly.
205 lines (202 loc) • 7 kB
JavaScript
import { createReadStream } from "node:fs";
import { readFile } from "node:fs/promises";
import { PassThrough, Readable } from "node:stream";
import { ReadableStream as ReadableStream$1 } from "node:stream/web";
import { fileURLToPath } from "node:url";
//#region src/helpers/getContentType.ts
/**
* Can be used to get the content type of file based on its extension.
*
* Feel free to add more mime types in your project!
*/
const mimeMap = {
json: "application/json",
txt: "text/plain",
html: "text/html",
htm: "text/html",
xml: "application/xml",
csv: "text/csv",
pdf: "application/pdf",
md: "text/markdown",
markdown: "text/markdown",
rtf: "application/rtf",
css: "text/css",
js: "application/javascript",
mjs: "application/javascript",
ts: "application/typescript",
jsx: "text/jsx",
tsx: "text/tsx",
zip: "application/zip",
rar: "application/vnd.rar",
"7z": "application/x-7z-compressed",
tar: "application/x-tar",
gz: "application/gzip",
tgz: "application/gzip",
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
webp: "image/webp",
svg: "image/svg+xml",
bmp: "image/bmp",
ico: "image/x-icon",
tiff: "image/tiff",
tif: "image/tiff",
mp3: "audio/mpeg",
wav: "audio/wav",
ogg: "audio/ogg",
m4a: "audio/mp4",
aac: "audio/aac",
flac: "audio/flac",
mp4: "video/mp4",
webm: "video/webm",
avi: "video/x-msvideo",
mov: "video/quicktime",
wmv: "video/x-ms-wmv",
flv: "video/x-flv",
mkv: "video/x-matroska",
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",
woff: "font/woff",
woff2: "font/woff2",
ttf: "font/ttf",
otf: "font/otf",
eot: "application/vnd.ms-fontobject"
};
/**
* Returns the content type of file based on its filename.
* @see {mimeMap}
*/
const getContentType = (filename) => {
const ext = filename.toLowerCase().split(".").pop() || "";
return mimeMap[ext] || "application/octet-stream";
};
//#endregion
//#region src/helpers/createFile.ts
const createFile = (source, options = {}) => {
if (source instanceof File) return createFileFromWebFile(source, options);
if (typeof source === "string" && (source.startsWith("file://") || source.startsWith("http://") || source.startsWith("https://"))) return createFileFromUrl(source, options);
if (source instanceof ReadableStream || source instanceof ReadableStream$1) return createFileFromStream(Readable.from(source), options);
if (isReadableStream(source) || source instanceof Readable) return createFileFromStream(source, options);
return createFileFromBuffer(Buffer.isBuffer(source) ? source : typeof source === "string" ? Buffer.from(source, "utf-8") : Buffer.from(source), options);
};
const createFileFromWebFile = (source, options = {}) => {
return {
name: source.name,
type: source.type || options.type || getContentType(source.name),
size: source.size || options.size || 0,
lastModified: source.lastModified || Date.now(),
stream: () => source.stream(),
arrayBuffer: async () => {
return await source.arrayBuffer();
},
text: async () => {
return await source.text();
}
};
};
const createFileFromBuffer = (source, options = {}) => {
const name = options.name ?? "file";
return {
name,
type: options.type ?? getContentType(options.name ?? name),
size: source.byteLength,
lastModified: Date.now(),
stream: () => Readable.from(source),
arrayBuffer: async () => {
return bufferToArrayBuffer(source);
},
text: async () => {
return source.toString("utf-8");
}
};
};
const createFileFromStream = (source, options = {}) => {
let buffer = null;
return {
name: options.name ?? "file",
type: options.type ?? getContentType(options.name ?? "file"),
size: options.size ?? 0,
lastModified: Date.now(),
stream: () => source,
_buffer: null,
async arrayBuffer() {
buffer ??= await streamToBuffer(source);
return bufferToArrayBuffer(buffer);
},
async text() {
buffer ??= await streamToBuffer(source);
return buffer.toString("utf-8");
}
};
};
const createFileFromUrl = (url, options = {}) => {
const parsedUrl = new URL(url);
const filename = options.name || parsedUrl.pathname.split("/").pop() || "file";
let buffer = null;
return {
name: filename,
type: options.type ?? getContentType(filename),
size: 0,
lastModified: Date.now(),
stream: () => createStreamFromUrl(url),
async arrayBuffer() {
buffer ??= await loadFromUrl(url);
return bufferToArrayBuffer(buffer);
},
async text() {
buffer ??= await loadFromUrl(url);
return buffer.toString("utf-8");
},
filepath: url
};
};
const getStreamingResponse = (url) => {
const stream = new PassThrough();
fetch(url).then((res) => Readable.fromWeb(res.body).pipe(stream)).catch((err) => stream.destroy(err));
return stream;
};
const loadFromUrl = async (url) => {
const parsedUrl = new URL(url);
if (parsedUrl.protocol === "file:") {
const filePath = fileURLToPath(url);
return await readFile(filePath);
} else if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") {
const response = await fetch(url);
if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
} else throw new Error(`Unsupported protocol: ${parsedUrl.protocol}`);
};
const createStreamFromUrl = (url) => {
const parsedUrl = new URL(url);
if (parsedUrl.protocol === "file:") return createReadStream(fileURLToPath(url));
else if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") return getStreamingResponse(url);
else throw new Error(`Unsupported protocol: ${parsedUrl.protocol}`);
};
/**
* Converts a stream-like object to a Buffer.
*/
const streamToBuffer = async (streamLike) => {
const stream = streamLike instanceof Readable ? streamLike : Readable.fromWeb(streamLike);
return new Promise((resolve, reject) => {
const buffer = [];
stream.on("data", (chunk) => buffer.push(Buffer.from(chunk)));
stream.on("end", () => resolve(Buffer.concat(buffer)));
stream.on("error", (err) => reject(new Error("Error converting stream", { cause: err })));
});
};
/**
* Converts a Node.js Buffer to an ArrayBuffer.
*/
const bufferToArrayBuffer = (buffer) => {
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
};
const isReadableStream = (obj) => obj instanceof Readable || obj != null && typeof obj === "object" && typeof obj.pipe === "function" && typeof obj.read === "function";
//#endregion
export { bufferToArrayBuffer, createFile, createFileFromBuffer, createFileFromStream, createFileFromUrl, createFileFromWebFile, getContentType, isReadableStream, mimeMap, streamToBuffer };
//# sourceMappingURL=index.js.map