@jeffy-g/universal-fs
Version:
Universal file system utils for Node.js and Browser in TypeScript.
169 lines (168 loc) • 5.42 kB
JavaScript
/*!
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Copyright (C) 2025 jeffy-g <hirotom1107@gmail.com>
// Released under the MIT license
// https://opensource.org/licenses/mit-license.php
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
*/
/**
* @file universal-fs/src/browser-fs.ts
*/
import {
UniversalFsError,
guessMimeType,
sanitizeFilename,
convertToJSON,
formatFsErrorMessage,
createErrorParameters,
decideFormat,
emitReadFileFunction,
} from "./utils.js";
/**
* @import {
* IInternalFs,
* TMimeType,
* TUFSOptions,
* } from "./types.d.ts"
*/
const writeErrParams = createErrorParameters(void 0, "browser", "write");
/**
* Reads a file via HTTP(S) or from a Blob/File in a browser environment.
*
* - Supports input as `string` (URL) or `Blob`/`File`.
* - Automatically parses based on `options.format`
* - Supported formats: `"text"`, `"json"`, `"arrayBuffer"`, `"blob"`, `"binary"`.
*
* **Note:**
* When reading from a `Blob` or `File`, the returned `url` property will be a pseudo-URL (`"blob://local"`)
* instead of an actual object URL to avoid memory leaks. Consumers should not rely on this for displaying
* the content directly. If an actual object URL is required, consider using `URL.createObjectURL()` manually.
*
* @param filename - File path, URL string, or `Blob`/`File` instance.
* @param [options] - UniversalFs options.
* @returns Result object containing the data.
* @type {IInternalFs["readFile"]}
*/
// local file read:
// node env の場合、file access permision に問題なければ無制限
// browser env では不可
export const readFile = emitReadFileFunction("browser");
/**
* Writes a file in the browser by triggering a download.
*
* @param filename - The name to give the downloaded file.
* @param data - The data to be written and downloaded.
* @param [options] - Write options (e.g., mimeType).
* @returns Universal file system result.
* @throws {UniversalFsError} Throws when download triggering fails.
* @type {IInternalFs["writeFile"]}
*/
export async function writeFile(filename, data, options = {}) {
try {
const mimeType = guessMimeType(filename);
const blob = new Blob([data], { type: mimeType });
const url = URL.createObjectURL(blob);
await triggerDownload(url, sanitizeFilename(filename));
if (options.useDetails) {
return {
filename,
url,
size: blob.size,
strategy: "browser",
timestamp: Date.now(),
mimeType,
};
}
return void 0;
} catch (e) {
throw new UniversalFsError(formatFsErrorMessage(e, "browser", "write"), {
...writeErrParams,
cause: e,
});
}
}
// helper
const te = new TextDecoder();
/**
* Converts an ArrayBuffer to the specified format based on `options.format`.
*
* Supported formats:
* - `"text"` → UTF-8 decoded string
* - `"json"` → Parsed JSON object or array (with error handling)
* - `"blob"` → `Blob` with inferred MIME type
* - `"binary"` → `Uint8Array`
* - `"arrayBuffer"` → `ArrayBuffer`
*
* @template T - Target data type after conversion.
* @param {ArrayBuffer} buffer - Raw binary data to convert.
* @param {TMimeType} mimeType - MIME type for Blob creation.
* @param {TUFSOptions} options - Options specifying desired format.
* @returns {Promise<T>} Converted data in the requested format.
* @throws {Error} If the format is unknown or JSON parsing fails.
*/
async function convertFromBuffer(buffer, mimeType, options) {
const format = decideFormat(options);
switch (format) {
case "text": /* falls through */
case "json":
const text = te.decode(buffer);
if (format === "text") return text;
return convertToJSON(text);
case "blob":
return new Blob([buffer], { type: mimeType });
case "binary": /* falls through */
case "arrayBuffer":
if (format === "arrayBuffer") return buffer;
return new Uint8Array(buffer);
}
}
/**
* Triggers download of a Blob by creating and clicking an invisible anchor element.
*
* @param {string} url - Blob URL for download target.
* @param {string} filename - The name for the downloaded file.
* @returns {Promise<void>} Resolves when download initiated.
*/
async function triggerDownload(url, filename) {
return new Promise((resolve, reject) => {
try {
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.style.display = "none";
// Added timeout function
const timeout = setTimeout(() => {
cleanup();
reject(new Error("Download timeout"));
}, 30000); // 30sec
const cleanup = () => {
clearTimeout(timeout);
if (document.body.contains(link)) {
document.body.removeChild(link);
}
setTimeout(() => URL.revokeObjectURL(url), 100);
};
link.addEventListener(
"click",
() => {
cleanup();
resolve();
},
{ once: true },
);
// Also monitors error events
link.addEventListener(
"error",
(e) => {
cleanup();
reject(new Error(`Download failed: ${e.message || "Unknown error"}`));
},
{ once: true },
);
document.body.appendChild(link);
link.click();
} catch (e) {
reject(e);
}
});
}