fs/lib/adapters/FileSystem.js

  1. const log = require("@ui5/logger").getLogger("resources:adapters:FileSystem");
  2. const path = require("path");
  3. const fs = require("graceful-fs");
  4. const globby = require("globby");
  5. const makeDir = require("make-dir");
  6. const {PassThrough} = require("stream");
  7. const Resource = require("../Resource");
  8. const AbstractAdapter = require("./AbstractAdapter");
  9. /**
  10. * File system resource adapter
  11. *
  12. * @public
  13. * @alias module:@ui5/fs.adapters.FileSystem
  14. * @augments module:@ui5/fs.adapters.AbstractAdapter
  15. */
  16. class FileSystem extends AbstractAdapter {
  17. /**
  18. * The Constructor.
  19. *
  20. * @param {object} parameters Parameters
  21. * @param {string} parameters.virBasePath Virtual base path
  22. * @param {string} parameters.fsBasePath (Physical) File system path
  23. * @param {string[]} [parameters.excludes] List of glob patterns to exclude
  24. * @param {object} [parameters.project] Experimental, internal parameter. Do not use
  25. */
  26. constructor({virBasePath, project, fsBasePath, excludes}) {
  27. super({virBasePath, project, excludes});
  28. this._fsBasePath = fsBasePath;
  29. }
  30. /**
  31. * Locate resources by glob.
  32. *
  33. * @private
  34. * @param {Array} patterns Array of glob patterns
  35. * @param {object} [options={}] glob options
  36. * @param {boolean} [options.nodir=true] Do not match directories
  37. * @param {module:@ui5/fs.tracing.Trace} trace Trace instance
  38. * @returns {Promise<module:@ui5/fs.Resource[]>} Promise resolving to list of resources
  39. */
  40. async _runGlob(patterns, options = {nodir: true}, trace) {
  41. const opt = {
  42. cwd: this._fsBasePath,
  43. dot: true,
  44. onlyFiles: options.nodir,
  45. followSymbolicLinks: false
  46. };
  47. trace.globCall();
  48. const promises = [];
  49. if (!opt.onlyFiles && patterns.includes("")) { // Match physical root directory
  50. promises.push(new Promise((resolve, reject) => {
  51. fs.stat(this._fsBasePath, (err, stat) => {
  52. if (err) {
  53. reject(err);
  54. } else {
  55. resolve(new Resource({
  56. project: this._project,
  57. statInfo: stat,
  58. path: this._virBaseDir,
  59. createStream: () => {
  60. return fs.createReadStream(this._fsBasePath);
  61. }
  62. }));
  63. }
  64. });
  65. }));
  66. }
  67. // Remove empty string glob patterns
  68. // Starting with globby v8 or v9 empty glob patterns "" act like "**"
  69. // Micromatch throws on empty strings. We just ignore them since they are
  70. // typically caused by our normalization in the AbstractAdapter
  71. const globbyPatterns = patterns.filter((pattern) => {
  72. return pattern !== "";
  73. });
  74. if (globbyPatterns.length > 0) {
  75. const matches = await globby(globbyPatterns, opt);
  76. for (let i = matches.length - 1; i >= 0; i--) {
  77. promises.push(new Promise((resolve, reject) => {
  78. const fsPath = path.join(this._fsBasePath, matches[i]);
  79. const virPath = (this._virBasePath + matches[i]);
  80. // Workaround for not getting the stat from the glob
  81. fs.stat(fsPath, (err, stat) => {
  82. if (err) {
  83. reject(err);
  84. } else {
  85. resolve(new Resource({
  86. project: this._project,
  87. statInfo: stat,
  88. path: virPath,
  89. createStream: () => {
  90. return fs.createReadStream(fsPath);
  91. }
  92. }));
  93. }
  94. });
  95. }));
  96. }
  97. }
  98. const results = await Promise.all(promises);
  99. // Flatten results
  100. return Array.prototype.concat.apply([], results);
  101. }
  102. /**
  103. * Locate a resource by path.
  104. *
  105. * @private
  106. * @param {string} virPath Virtual path
  107. * @param {object} options Options
  108. * @param {module:@ui5/fs.tracing.Trace} trace Trace instance
  109. * @returns {Promise<module:@ui5/fs.Resource>} Promise resolving to a single resource or null if not found
  110. */
  111. _byPath(virPath, options, trace) {
  112. if (this.isPathExcluded(virPath)) {
  113. return Promise.resolve(null);
  114. }
  115. return new Promise((resolve, reject) => {
  116. if (!virPath.startsWith(this._virBasePath) && virPath !== this._virBaseDir) {
  117. // Neither starts with basePath, nor equals baseDirectory
  118. if (!options.nodir && this._virBasePath.startsWith(virPath)) {
  119. resolve(new Resource({
  120. project: this._project,
  121. statInfo: { // TODO: make closer to fs stat info
  122. isDirectory: function() {
  123. return true;
  124. }
  125. },
  126. path: virPath
  127. }));
  128. } else {
  129. resolve(null);
  130. }
  131. return;
  132. }
  133. const relPath = virPath.substr(this._virBasePath.length);
  134. const fsPath = path.join(this._fsBasePath, relPath);
  135. trace.pathCall();
  136. fs.stat(fsPath, (err, stat) => {
  137. if (err) {
  138. if (err.code === "ENOENT") { // "File or directory does not exist"
  139. resolve(null);
  140. } else {
  141. reject(err);
  142. }
  143. } else if (options.nodir && stat.isDirectory()) {
  144. resolve(null);
  145. } else {
  146. const options = {
  147. project: this._project,
  148. statInfo: stat,
  149. path: virPath,
  150. fsPath
  151. };
  152. if (!stat.isDirectory()) {
  153. // Add content
  154. options.createStream = function() {
  155. return fs.createReadStream(fsPath);
  156. };
  157. }
  158. resolve(new Resource(options));
  159. }
  160. });
  161. });
  162. }
  163. /**
  164. * Writes the content of a resource to a path.
  165. *
  166. * @private
  167. * @param {module:@ui5/fs.Resource} resource Resource to write
  168. * @param {object} [options]
  169. * @param {boolean} [options.readOnly] Whether the resource content shall be written read-only
  170. * Do not use in conjunction with the <code>drain</code> option.
  171. * The written file will be used as the new source of this resources content.
  172. * Therefore the written file should not be altered by any means.
  173. * Activating this option might improve overall memory consumption.
  174. * @param {boolean} [options.drain] Whether the resource content shall be emptied during the write process.
  175. * Do not use in conjunction with the <code>readOnly</code> option.
  176. * Activating this option might improve overall memory consumption.
  177. * This should be used in cases where this is the last access to the resource.
  178. * E.g. the final write of a resource after all processing is finished.
  179. * @returns {Promise<undefined>} Promise resolving once data has been written
  180. */
  181. async _write(resource, {drain, readOnly}) {
  182. if (drain && readOnly) {
  183. throw new Error(`Error while writing resource ${resource.getPath()}: ` +
  184. "Do not use options 'drain' and 'readOnly' at the same time.");
  185. }
  186. const relPath = resource.getPath().substr(this._virBasePath.length);
  187. const fsPath = path.join(this._fsBasePath, relPath);
  188. const dirPath = path.dirname(fsPath);
  189. log.verbose("Writing to %s", fsPath);
  190. await makeDir(dirPath, {fs});
  191. return new Promise((resolve, reject) => {
  192. let contentStream;
  193. if (drain || readOnly) {
  194. // Stream will be drained
  195. contentStream = resource.getStream();
  196. contentStream.on("error", (err) => {
  197. reject(err);
  198. });
  199. } else {
  200. // Transform stream into buffer before writing
  201. contentStream = new PassThrough();
  202. const buffers = [];
  203. contentStream.on("error", (err) => {
  204. reject(err);
  205. });
  206. contentStream.on("data", (data) => {
  207. buffers.push(data);
  208. });
  209. contentStream.on("end", () => {
  210. const buffer = Buffer.concat(buffers);
  211. resource.setBuffer(buffer);
  212. });
  213. resource.getStream().pipe(contentStream);
  214. }
  215. const writeOptions = {};
  216. if (readOnly) {
  217. writeOptions.mode = 0o444; // read only
  218. }
  219. const write = fs.createWriteStream(fsPath, writeOptions);
  220. write.on("error", (err) => {
  221. reject(err);
  222. });
  223. write.on("close", (ex) => {
  224. if (readOnly) {
  225. // Create new stream from written file
  226. resource.setStream(function() {
  227. return fs.createReadStream(fsPath);
  228. });
  229. }
  230. resolve();
  231. });
  232. contentStream.pipe(write);
  233. });
  234. }
  235. }
  236. module.exports = FileSystem;