const stream = require("stream");
const clone = require("clone");
const path = require("path");
const fnTrue = () => true;
const fnFalse = () => false;
/**
* Resource
*
* @public
* @memberof module:@ui5/fs
*/
class Resource {
/**
* Function for dynamic creation of content streams
*
* @public
* @callback module:@ui5/fs.Resource~createStream
* @returns {stream.Readable} A readable stream of a resources content
*/
/**
* The constructor.
*
* @public
* @param {object} parameters Parameters
* @param {string} parameters.path Virtual path
* @param {fs.Stats|object} [parameters.statInfo] File information. Instance of
* [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} or similar object
* @param {Buffer} [parameters.buffer] Content of this resources as a Buffer instance
* (cannot be used in conjunction with parameters string, stream or createStream)
* @param {string} [parameters.string] Content of this resources as a string
* (cannot be used in conjunction with parameters buffer, stream or createStream)
* @param {Stream} [parameters.stream] Readable stream of the content of this resource
* (cannot be used in conjunction with parameters buffer, string or createStream)
* @param {module:@ui5/fs.Resource~createStream} [parameters.createStream] Function callback that returns a readable
* stream of the content of this resource (cannot be used in conjunction with parameters buffer,
* string or stream).
* In some cases this is the most memory-efficient way to supply resource content
* @param {object} [parameters.project] Experimental, internal parameter. Do not use
*/
constructor({path, statInfo, buffer, string, createStream, stream, project}) {
if (!path) {
throw new Error("Cannot create Resource: path parameter missing");
}
if (buffer && createStream || buffer && string || string && createStream || buffer && stream ||
string && stream || createStream && stream) {
throw new Error("Cannot create Resource: Please set only one content parameter. " +
"Buffer, string, stream or createStream");
}
this._path = path;
this._name = this._getNameFromPath(path);
this._project = project; // Experimental, internal parameter
this._statInfo = statInfo || { // TODO
isFile: fnTrue,
isDirectory: fnFalse,
isBlockDevice: fnFalse,
isCharacterDevice: fnFalse,
isSymbolicLink: fnFalse,
isFIFO: fnFalse,
isSocket: fnFalse,
atimeMs: new Date().getTime(),
mtimeMs: new Date().getTime(),
ctimeMs: new Date().getTime(),
birthtimeMs: new Date().getTime(),
atime: new Date(),
mtime: new Date(),
ctime: new Date(),
birthtime: new Date()
};
if (createStream) {
this._createStream = createStream;
} else if (stream) {
this._stream = stream;
} else if (buffer) {
this.setBuffer(buffer);
} else if (typeof string === "string" || string instanceof String) {
this.setString(string);
}
// Tracing:
this._collections = [];
}
/**
* Gets a buffer with the resource content.
*
* @public
* @returns {Promise<Buffer>} Promise resolving with a buffer of the resource content.
*/
async getBuffer() {
if (this._contentDrained) {
throw new Error(`Content of Resource ${this._path} has been drained. ` +
"This might be caused by requesting resource content after a content stream has been " +
"requested and no new content (e.g. a new stream) has been set.");
}
if (this._buffer) {
return this._buffer;
} else if (this._createStream || this._stream) {
return this._getBufferFromStream();
} else {
throw new Error(`Resource ${this._path} has no content`);
}
}
/**
* Sets a Buffer as content.
*
* @public
* @param {Buffer} buffer Buffer instance
*/
setBuffer(buffer) {
this._createStream = null;
// if (this._stream) { // TODO this may cause strange issues
// this._stream.destroy();
// }
this._stream = null;
this._buffer = buffer;
this._contentDrained = false;
this._streamDrained = false;
}
/**
* Gets a string with the resource content.
*
* @public
* @returns {Promise<string>} Promise resolving with the resource content.
*/
getString() {
if (this._contentDrained) {
return Promise.reject(new Error(`Content of Resource ${this._path} has been drained. ` +
"This might be caused by requesting resource content after a content stream has been " +
"requested and no new content (e.g. a new stream) has been set."));
}
return this.getBuffer().then((buffer) => buffer.toString());
}
/**
* Sets a String as content
*
* @public
* @param {string} string Resource content
*/
setString(string) {
this.setBuffer(Buffer.from(string, "utf8"));
}
/**
* Gets a readable stream for the resource content.
*
* Repetitive calls of this function are only possible if new content has been set in the meantime (through
* [setStream]{@link module:@ui5/fs.Resource#setStream}, [setBuffer]{@link module:@ui5/fs.Resource#setBuffer}
* or [setString]{@link module:@ui5/fs.Resource#setString}). This
* is to prevent consumers from accessing drained streams.
*
* @public
* @returns {stream.Readable} Readable stream for the resource content.
*/
getStream() {
if (this._contentDrained) {
throw new Error(`Content of Resource ${this._path} has been drained. ` +
"This might be caused by requesting resource content after a content stream has been " +
"requested and no new content (e.g. a new stream) has been set.");
}
let contentStream;
if (this._buffer) {
const bufferStream = new stream.PassThrough();
bufferStream.end(this._buffer);
contentStream = bufferStream;
} else if (this._createStream || this._stream) {
contentStream = this._getStream();
}
if (!contentStream) {
throw new Error(`Resource ${this._path} has no content`);
}
// If a stream instance is being returned, it will typically get drained be the consumer.
// In that case, further content access will result in a "Content stream has been drained" error.
// However, depending on the execution environment, a resources content stream might have been
// transformed into a buffer. In that case further content access is possible as a buffer can't be
// drained.
// To prevent unexpected "Content stream has been drained" errors caused by changing environments, we flag
// the resource content as "drained" every time a stream is requested. Even if actually a buffer or
// createStream callback is being used.
this._contentDrained = true;
return contentStream;
}
/**
* Sets a readable stream as content.
*
* @public
* @param {stream.Readable|module:@ui5/fs.Resource~createStream} stream Readable stream of the resource content or
callback for dynamic creation of a readable stream
*/
setStream(stream) {
this._buffer = null;
// if (this._stream) { // TODO this may cause strange issues
// this._stream.destroy();
// }
if (typeof stream === "function") {
this._createStream = stream;
this._stream = null;
} else {
this._stream = stream;
this._createStream = null;
}
this._contentDrained = false;
this._streamDrained = false;
}
/**
* Gets the resources path
*
* @public
* @returns {string} (Virtual) path of the resource
*/
getPath() {
return this._path;
}
/**
* Sets the resources path
*
* @public
* @param {string} path (Virtual) path of the resource
*/
setPath(path) {
this._path = path;
this._name = this._getNameFromPath(path);
}
/**
* Gets the resources stat info.
* Note that a resources stat information is not updated when the resource is being modified.
* Also, depending on the used adapter, some fields might be missing which would be present for a
* [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} instance.
*
* @public
* @returns {fs.Stats|object} Instance of [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats}
* or similar object
*/
getStatInfo() {
return this._statInfo;
}
/**
* Size in bytes allocated by the underlying buffer.
*
* @see {TypedArray#byteLength}
* @returns {Promise<number>} size in bytes, <code>0</code> if there is no content yet
*/
async getSize() {
// if resource does not have any content it should have 0 bytes
if (!this._buffer && !this._createStream && !this._stream) {
return 0;
}
const buffer = await this.getBuffer();
return buffer.byteLength;
}
_getNameFromPath(virPath) {
return path.posix.basename(virPath);
}
/**
* Adds a resource collection name that was involved in locating this resource.
*
* @param {string} name Resource collection name
*/
pushCollection(name) {
this._collections.push(name);
}
/**
* Returns a clone of the resource. The clones content is independent from that of the original resource
*
* @public
* @returns {Promise<module:@ui5/fs.Resource>} Promise resolving with the clone
*/
clone() {
const options = {
path: this._path,
statInfo: clone(this._statInfo)
};
const addContentOption = () => {
if (this._stream) {
return this._getBufferFromStream().then(function(buffer) {
options.buffer = buffer;
});
} else {
if (this._createStream) {
options.createStream = this._createStream;
} else if (this._buffer) {
options.buffer = this._buffer;
}
return Promise.resolve();
}
};
return addContentOption().then(() => {
return new Resource(options);
});
}
/**
* Tracing: Get tree for printing out trace
*
* @returns {object} Trace tree
*/
getPathTree() {
const tree = {};
let pointer = tree[this._path] = {};
for (let i = this._collections.length - 1; i >= 0; i--) {
pointer = pointer[this._collections[i]] = {};
}
return tree;
}
/**
* Returns the content as stream.
*
* @private
* @returns {stream.Readable} Readable stream
*/
_getStream() {
if (this._streamDrained) {
throw new Error(`Content stream of Resource ${this._path} is flagged as drained.`);
}
if (this._createStream) {
return this._createStream();
}
this._streamDrained = true;
return this._stream;
}
/**
* Converts the buffer into a stream.
*
* @private
* @returns {Promise<Buffer>} Promise resolving with buffer.
*/
_getBufferFromStream() {
if (this._buffering) { // Prevent simultaneous buffering, causing unexpected access to drained stream
return this._buffering;
}
return this._buffering = new Promise((resolve, reject) => {
const contentStream = this._getStream();
const buffers = [];
contentStream.on("data", (data) => {
buffers.push(data);
});
contentStream.on("error", (err) => {
reject(err);
});
contentStream.on("end", () => {
const buffer = Buffer.concat(buffers);
this.setBuffer(buffer);
this._buffering = null;
resolve(buffer);
});
});
}
}
module.exports = Resource;