fs/lib/Resource.js

  1. const stream = require("stream");
  2. const clone = require("clone");
  3. const path = require("path");
  4. const fnTrue = () => true;
  5. const fnFalse = () => false;
  6. /**
  7. * Resource
  8. *
  9. * @public
  10. * @memberof module:@ui5/fs
  11. */
  12. class Resource {
  13. /**
  14. * Function for dynamic creation of content streams
  15. *
  16. * @public
  17. * @callback module:@ui5/fs.Resource~createStream
  18. * @returns {stream.Readable} A readable stream of a resources content
  19. */
  20. /**
  21. * The constructor.
  22. *
  23. * @public
  24. * @param {object} parameters Parameters
  25. * @param {string} parameters.path Virtual path
  26. * @param {fs.Stats|object} [parameters.statInfo] File information. Instance of
  27. * [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} or similar object
  28. * @param {Buffer} [parameters.buffer] Content of this resources as a Buffer instance
  29. * (cannot be used in conjunction with parameters string, stream or createStream)
  30. * @param {string} [parameters.string] Content of this resources as a string
  31. * (cannot be used in conjunction with parameters buffer, stream or createStream)
  32. * @param {Stream} [parameters.stream] Readable stream of the content of this resource
  33. * (cannot be used in conjunction with parameters buffer, string or createStream)
  34. * @param {module:@ui5/fs.Resource~createStream} [parameters.createStream] Function callback that returns a readable
  35. * stream of the content of this resource (cannot be used in conjunction with parameters buffer,
  36. * string or stream).
  37. * In some cases this is the most memory-efficient way to supply resource content
  38. * @param {object} [parameters.project] Experimental, internal parameter. Do not use
  39. */
  40. constructor({path, statInfo, buffer, string, createStream, stream, project}) {
  41. if (!path) {
  42. throw new Error("Cannot create Resource: path parameter missing");
  43. }
  44. if (buffer && createStream || buffer && string || string && createStream || buffer && stream ||
  45. string && stream || createStream && stream) {
  46. throw new Error("Cannot create Resource: Please set only one content parameter. " +
  47. "Buffer, string, stream or createStream");
  48. }
  49. this._path = path;
  50. this._name = this._getNameFromPath(path);
  51. this._project = project; // Experimental, internal parameter
  52. this._statInfo = statInfo || { // TODO
  53. isFile: fnTrue,
  54. isDirectory: fnFalse,
  55. isBlockDevice: fnFalse,
  56. isCharacterDevice: fnFalse,
  57. isSymbolicLink: fnFalse,
  58. isFIFO: fnFalse,
  59. isSocket: fnFalse,
  60. atimeMs: new Date().getTime(),
  61. mtimeMs: new Date().getTime(),
  62. ctimeMs: new Date().getTime(),
  63. birthtimeMs: new Date().getTime(),
  64. atime: new Date(),
  65. mtime: new Date(),
  66. ctime: new Date(),
  67. birthtime: new Date()
  68. };
  69. if (createStream) {
  70. this._createStream = createStream;
  71. } else if (stream) {
  72. this._stream = stream;
  73. } else if (buffer) {
  74. this.setBuffer(buffer);
  75. } else if (typeof string === "string" || string instanceof String) {
  76. this.setString(string);
  77. }
  78. // Tracing:
  79. this._collections = [];
  80. }
  81. /**
  82. * Gets a buffer with the resource content.
  83. *
  84. * @public
  85. * @returns {Promise<Buffer>} Promise resolving with a buffer of the resource content.
  86. */
  87. async getBuffer() {
  88. if (this._contentDrained) {
  89. throw new Error(`Content of Resource ${this._path} has been drained. ` +
  90. "This might be caused by requesting resource content after a content stream has been " +
  91. "requested and no new content (e.g. a new stream) has been set.");
  92. }
  93. if (this._buffer) {
  94. return this._buffer;
  95. } else if (this._createStream || this._stream) {
  96. return this._getBufferFromStream();
  97. } else {
  98. throw new Error(`Resource ${this._path} has no content`);
  99. }
  100. }
  101. /**
  102. * Sets a Buffer as content.
  103. *
  104. * @public
  105. * @param {Buffer} buffer Buffer instance
  106. */
  107. setBuffer(buffer) {
  108. this._createStream = null;
  109. // if (this._stream) { // TODO this may cause strange issues
  110. // this._stream.destroy();
  111. // }
  112. this._stream = null;
  113. this._buffer = buffer;
  114. this._contentDrained = false;
  115. this._streamDrained = false;
  116. }
  117. /**
  118. * Gets a string with the resource content.
  119. *
  120. * @public
  121. * @returns {Promise<string>} Promise resolving with the resource content.
  122. */
  123. getString() {
  124. if (this._contentDrained) {
  125. return Promise.reject(new Error(`Content of Resource ${this._path} has been drained. ` +
  126. "This might be caused by requesting resource content after a content stream has been " +
  127. "requested and no new content (e.g. a new stream) has been set."));
  128. }
  129. return this.getBuffer().then((buffer) => buffer.toString());
  130. }
  131. /**
  132. * Sets a String as content
  133. *
  134. * @public
  135. * @param {string} string Resource content
  136. */
  137. setString(string) {
  138. this.setBuffer(Buffer.from(string, "utf8"));
  139. }
  140. /**
  141. * Gets a readable stream for the resource content.
  142. *
  143. * Repetitive calls of this function are only possible if new content has been set in the meantime (through
  144. * [setStream]{@link module:@ui5/fs.Resource#setStream}, [setBuffer]{@link module:@ui5/fs.Resource#setBuffer}
  145. * or [setString]{@link module:@ui5/fs.Resource#setString}). This
  146. * is to prevent consumers from accessing drained streams.
  147. *
  148. * @public
  149. * @returns {stream.Readable} Readable stream for the resource content.
  150. */
  151. getStream() {
  152. if (this._contentDrained) {
  153. throw new Error(`Content of Resource ${this._path} has been drained. ` +
  154. "This might be caused by requesting resource content after a content stream has been " +
  155. "requested and no new content (e.g. a new stream) has been set.");
  156. }
  157. let contentStream;
  158. if (this._buffer) {
  159. const bufferStream = new stream.PassThrough();
  160. bufferStream.end(this._buffer);
  161. contentStream = bufferStream;
  162. } else if (this._createStream || this._stream) {
  163. contentStream = this._getStream();
  164. }
  165. if (!contentStream) {
  166. throw new Error(`Resource ${this._path} has no content`);
  167. }
  168. // If a stream instance is being returned, it will typically get drained be the consumer.
  169. // In that case, further content access will result in a "Content stream has been drained" error.
  170. // However, depending on the execution environment, a resources content stream might have been
  171. // transformed into a buffer. In that case further content access is possible as a buffer can't be
  172. // drained.
  173. // To prevent unexpected "Content stream has been drained" errors caused by changing environments, we flag
  174. // the resource content as "drained" every time a stream is requested. Even if actually a buffer or
  175. // createStream callback is being used.
  176. this._contentDrained = true;
  177. return contentStream;
  178. }
  179. /**
  180. * Sets a readable stream as content.
  181. *
  182. * @public
  183. * @param {stream.Readable|module:@ui5/fs.Resource~createStream} stream Readable stream of the resource content or
  184. callback for dynamic creation of a readable stream
  185. */
  186. setStream(stream) {
  187. this._buffer = null;
  188. // if (this._stream) { // TODO this may cause strange issues
  189. // this._stream.destroy();
  190. // }
  191. if (typeof stream === "function") {
  192. this._createStream = stream;
  193. this._stream = null;
  194. } else {
  195. this._stream = stream;
  196. this._createStream = null;
  197. }
  198. this._contentDrained = false;
  199. this._streamDrained = false;
  200. }
  201. /**
  202. * Gets the resources path
  203. *
  204. * @public
  205. * @returns {string} (Virtual) path of the resource
  206. */
  207. getPath() {
  208. return this._path;
  209. }
  210. /**
  211. * Sets the resources path
  212. *
  213. * @public
  214. * @param {string} path (Virtual) path of the resource
  215. */
  216. setPath(path) {
  217. this._path = path;
  218. this._name = this._getNameFromPath(path);
  219. }
  220. /**
  221. * Gets the resources stat info.
  222. * Note that a resources stat information is not updated when the resource is being modified.
  223. * Also, depending on the used adapter, some fields might be missing which would be present for a
  224. * [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} instance.
  225. *
  226. * @public
  227. * @returns {fs.Stats|object} Instance of [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats}
  228. * or similar object
  229. */
  230. getStatInfo() {
  231. return this._statInfo;
  232. }
  233. /**
  234. * Size in bytes allocated by the underlying buffer.
  235. *
  236. * @see {TypedArray#byteLength}
  237. * @returns {Promise<number>} size in bytes, <code>0</code> if there is no content yet
  238. */
  239. async getSize() {
  240. // if resource does not have any content it should have 0 bytes
  241. if (!this._buffer && !this._createStream && !this._stream) {
  242. return 0;
  243. }
  244. const buffer = await this.getBuffer();
  245. return buffer.byteLength;
  246. }
  247. _getNameFromPath(virPath) {
  248. return path.posix.basename(virPath);
  249. }
  250. /**
  251. * Adds a resource collection name that was involved in locating this resource.
  252. *
  253. * @param {string} name Resource collection name
  254. */
  255. pushCollection(name) {
  256. this._collections.push(name);
  257. }
  258. /**
  259. * Returns a clone of the resource. The clones content is independent from that of the original resource
  260. *
  261. * @public
  262. * @returns {Promise<module:@ui5/fs.Resource>} Promise resolving with the clone
  263. */
  264. clone() {
  265. const options = {
  266. path: this._path,
  267. statInfo: clone(this._statInfo)
  268. };
  269. const addContentOption = () => {
  270. if (this._stream) {
  271. return this._getBufferFromStream().then(function(buffer) {
  272. options.buffer = buffer;
  273. });
  274. } else {
  275. if (this._createStream) {
  276. options.createStream = this._createStream;
  277. } else if (this._buffer) {
  278. options.buffer = this._buffer;
  279. }
  280. return Promise.resolve();
  281. }
  282. };
  283. return addContentOption().then(() => {
  284. return new Resource(options);
  285. });
  286. }
  287. /**
  288. * Tracing: Get tree for printing out trace
  289. *
  290. * @returns {object} Trace tree
  291. */
  292. getPathTree() {
  293. const tree = {};
  294. let pointer = tree[this._path] = {};
  295. for (let i = this._collections.length - 1; i >= 0; i--) {
  296. pointer = pointer[this._collections[i]] = {};
  297. }
  298. return tree;
  299. }
  300. /**
  301. * Returns the content as stream.
  302. *
  303. * @private
  304. * @returns {stream.Readable} Readable stream
  305. */
  306. _getStream() {
  307. if (this._streamDrained) {
  308. throw new Error(`Content stream of Resource ${this._path} is flagged as drained.`);
  309. }
  310. if (this._createStream) {
  311. return this._createStream();
  312. }
  313. this._streamDrained = true;
  314. return this._stream;
  315. }
  316. /**
  317. * Converts the buffer into a stream.
  318. *
  319. * @private
  320. * @returns {Promise<Buffer>} Promise resolving with buffer.
  321. */
  322. _getBufferFromStream() {
  323. if (this._buffering) { // Prevent simultaneous buffering, causing unexpected access to drained stream
  324. return this._buffering;
  325. }
  326. return this._buffering = new Promise((resolve, reject) => {
  327. const contentStream = this._getStream();
  328. const buffers = [];
  329. contentStream.on("data", (data) => {
  330. buffers.push(data);
  331. });
  332. contentStream.on("error", (err) => {
  333. reject(err);
  334. });
  335. contentStream.on("end", () => {
  336. const buffer = Buffer.concat(buffers);
  337. this.setBuffer(buffer);
  338. this._buffering = null;
  339. resolve(buffer);
  340. });
  341. });
  342. }
  343. }
  344. module.exports = Resource;