@gltf-transform/core
Version:
glTF 2.0 SDK for JavaScript and TypeScript, on Web and Node.js.
198 lines (177 loc) • 6.4 kB
text/typescript
import { Format } from '../constants.js';
import type { Document } from '../document.js';
import { FileUtils, HTTPUtils } from '../utils/index.js';
import { PlatformIO } from './platform-io.js';
/**
* *I/O service for Node.js.*
*
* The most common use of the I/O service is to read/write a {@link Document} with a given path.
* Methods are also available for converting in-memory representations of raw glTF files, both
* binary (*Uint8Array*) and JSON ({@link JSONDocument}).
*
* Usage:
*
* ```typescript
* import { NodeIO } from '@gltf-transform/core';
*
* const io = new NodeIO();
*
* // Read.
* let document;
* document = await io.read('model.glb'); // → Document
* document = await io.readBinary(glb); // Uint8Array → Document
*
* // Write.
* await io.write('model.glb', document); // → void
* const glb = await io.writeBinary(document); // Document → Uint8Array
* ```
*
* By default, NodeIO can only read/write paths on disk. To enable network requests, provide a Fetch
* API implementation (global [`fetch()`](https://nodejs.org/api/globals.html#fetch) is stable in
* Node.js v21+, or [`node-fetch`](https://www.npmjs.com/package/node-fetch) may be installed) and enable
* {@link NodeIO.setAllowNetwork setAllowNetwork}. Network requests may optionally be configured with
* [RequestInit](https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters) parameters.
*
* ```typescript
* const io = new NodeIO(fetch, {headers: {...}}).setAllowNetwork(true);
*
* const document = await io.read('https://example.com/path/to/model.glb');
* ```
*
* @category I/O
*/
export class NodeIO extends PlatformIO {
private declare _fs;
private declare _path;
private readonly _fetch: typeof fetch | null;
private readonly _fetchConfig: RequestInit;
private _init: Promise<void>;
private _fetchEnabled = false;
/**
* Constructs a new NodeIO service. Instances are reusable. By default, only NodeIO can only
* read/write paths on disk. To enable HTTP requests, provide a Fetch API implementation and
* enable {@link NodeIO.setAllowNetwork setAllowNetwork}.
*
* @param fetch Implementation of Fetch API.
* @param fetchConfig Configuration object for Fetch API.
*/
constructor(_fetch: unknown = null, _fetchConfig: RequestInit = HTTPUtils.DEFAULT_INIT) {
super();
this._fetch = _fetch as typeof fetch | null;
this._fetchConfig = _fetchConfig;
this._init = this.init();
}
public async init(): Promise<void> {
if (this._init) return this._init;
return Promise.all([import('fs'), import('path')]).then(([fs, path]) => {
this._fs = fs.promises;
this._path = path;
});
}
public setAllowNetwork(allow: boolean): this {
if (allow && !this._fetch) {
throw new Error('NodeIO requires a Fetch API implementation for HTTP requests.');
}
this._fetchEnabled = allow;
return this;
}
protected async readURI(uri: string, type: 'view'): Promise<Uint8Array>;
protected async readURI(uri: string, type: 'text'): Promise<string>;
protected async readURI(uri: string, type: 'view' | 'text'): Promise<Uint8Array | string> {
await this.init();
if (HTTPUtils.isAbsoluteURL(uri)) {
if (!this._fetchEnabled || !this._fetch) {
throw new Error('Network request blocked. Allow HTTP requests explicitly, if needed.');
}
const response = await this._fetch(uri, this._fetchConfig);
switch (type) {
case 'view':
return new Uint8Array(await response.arrayBuffer());
case 'text':
return response.text();
}
} else {
switch (type) {
case 'view':
return this._fs.readFile(uri);
case 'text':
return this._fs.readFile(uri, 'utf8');
}
}
}
protected resolve(base: string, path: string): string {
if (HTTPUtils.isAbsoluteURL(base) || HTTPUtils.isAbsoluteURL(path)) {
return HTTPUtils.resolve(base, path);
}
// https://github.com/KhronosGroup/glTF/issues/1449
// https://stackoverflow.com/a/27278490/1314762
return this._path.resolve(base, decodeURIComponent(path));
}
protected dirname(uri: string): string {
if (HTTPUtils.isAbsoluteURL(uri)) {
return HTTPUtils.dirname(uri);
}
return this._path.dirname(uri);
}
/**********************************************************************************************
* Public.
*/
/** Writes a {@link Document} instance to a local path. */
public async write(uri: string, doc: Document): Promise<void> {
await this.init();
const isGLB = !!uri.match(/\.glb$/);
await (isGLB ? this._writeGLB(uri, doc) : this._writeGLTF(uri, doc));
}
/**********************************************************************************************
* Private.
*/
/** @internal */
private async _writeGLTF(uri: string, doc: Document): Promise<void> {
this.lastWriteBytes = 0;
const { json, resources } = await this.writeJSON(doc, {
format: Format.GLTF,
basename: FileUtils.basename(uri),
});
const { _fs: fs, _path: path } = this;
const dir = path.dirname(uri);
// write json
const jsonContent = JSON.stringify(json, null, 2);
await fs.writeFile(uri, jsonContent);
this.lastWriteBytes += jsonContent.length;
// write resources
for (const batch of listBatches(Object.keys(resources), 10)) {
await Promise.all(
batch.map(async (resourceURI) => {
if (HTTPUtils.isAbsoluteURL(resourceURI)) {
if (HTTPUtils.extension(resourceURI) === 'bin') {
throw new Error(`Cannot write buffer to path "${resourceURI}".`);
}
return;
}
const resourcePath = path.join(dir, decodeURIComponent(resourceURI));
await fs.mkdir(path.dirname(resourcePath), { recursive: true });
await fs.writeFile(resourcePath, resources[resourceURI]);
this.lastWriteBytes += resources[resourceURI].byteLength;
}),
);
}
}
/** @internal */
private async _writeGLB(uri: string, doc: Document): Promise<void> {
const buffer = await this.writeBinary(doc);
await this._fs.writeFile(uri, buffer);
this.lastWriteBytes = buffer.byteLength;
}
}
/** Divides a flat input array into batches of size `batchSize`. */
function listBatches<T>(array: T[], batchSize: number): T[][] {
const batches: T[][] = [];
for (let i = 0, il = array.length; i < il; i += batchSize) {
const batch: T[] = [];
for (let j = 0; j < batchSize && i + j < il; j++) {
batch.push(array[i + j]);
}
batches.push(batch);
}
return batches;
}