fs/lib/adapters/AbstractAdapter.js

  1. import path from "node:path/posix";
  2. import {getLogger} from "@ui5/logger";
  3. const log = getLogger("resources:adapters:AbstractAdapter");
  4. import {minimatch} from "minimatch";
  5. import micromatch from "micromatch";
  6. import AbstractReaderWriter from "../AbstractReaderWriter.js";
  7. import Resource from "../Resource.js";
  8. /**
  9. * Abstract Resource Adapter
  10. *
  11. * @abstract
  12. * @public
  13. * @class
  14. * @alias @ui5/fs/adapters/AbstractAdapter
  15. * @extends @ui5/fs/AbstractReaderWriter
  16. */
  17. class AbstractAdapter extends AbstractReaderWriter {
  18. /**
  19. * The constructor
  20. *
  21. * @public
  22. * @param {object} parameters Parameters
  23. * @param {string} parameters.virBasePath
  24. * Virtual base path. Must be absolute, POSIX-style, and must end with a slash
  25. * @param {string[]} [parameters.excludes] List of glob patterns to exclude
  26. * @param {object} [parameters.project] Experimental, internal parameter. Do not use
  27. */
  28. constructor({virBasePath, excludes = [], project}) {
  29. if (new.target === AbstractAdapter) {
  30. throw new TypeError("Class 'AbstractAdapter' is abstract");
  31. }
  32. super();
  33. if (!virBasePath) {
  34. throw new Error(`Unable to create adapter: Missing parameter 'virBasePath'`);
  35. }
  36. if (!path.isAbsolute(virBasePath)) {
  37. throw new Error(`Unable to create adapter: Virtual base path must be absolute but is '${virBasePath}'`);
  38. }
  39. if (!virBasePath.endsWith("/")) {
  40. throw new Error(
  41. `Unable to create adapter: Virtual base path must end with a slash but is '${virBasePath}'`);
  42. }
  43. this._virBasePath = virBasePath;
  44. this._virBaseDir = virBasePath.slice(0, -1);
  45. this._excludes = excludes;
  46. this._excludesNegated = excludes.map((pattern) => `!${pattern}`);
  47. this._project = project;
  48. }
  49. /**
  50. * Locates resources by glob.
  51. *
  52. * @abstract
  53. * @private
  54. * @param {string|string[]} virPattern glob pattern as string or an array of
  55. * glob patterns for virtual directory structure
  56. * @param {object} [options={}] glob options
  57. * @param {boolean} [options.nodir=true] Do not match directories
  58. * @param {@ui5/fs/tracing.Trace} trace Trace instance
  59. * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources
  60. */
  61. async _byGlob(virPattern, options = {nodir: true}, trace) {
  62. const excludes = this._excludesNegated;
  63. if (!(virPattern instanceof Array)) {
  64. virPattern = [virPattern];
  65. }
  66. // Append static exclude patterns
  67. virPattern = Array.prototype.concat.apply(virPattern, excludes);
  68. let patterns = virPattern.map(this._normalizePattern, this);
  69. patterns = Array.prototype.concat.apply([], patterns);
  70. if (patterns.length === 0) {
  71. return [];
  72. }
  73. if (!options.nodir) {
  74. for (let i = patterns.length - 1; i >= 0; i--) {
  75. const idx = this._virBaseDir.indexOf(patterns[i]);
  76. if (patterns[i] && idx !== -1 && idx < this._virBaseDir.length) {
  77. const subPath = patterns[i];
  78. return [
  79. this._createResource({
  80. statInfo: { // TODO: make closer to fs stat info
  81. isDirectory: function() {
  82. return true;
  83. }
  84. },
  85. source: {
  86. adapter: "Abstract"
  87. },
  88. path: subPath
  89. })
  90. ];
  91. }
  92. }
  93. }
  94. return await this._runGlob(patterns, options, trace);
  95. }
  96. /**
  97. * Validate if virtual path should be excluded
  98. *
  99. * @param {string} virPath Virtual Path
  100. * @returns {boolean} True if path is excluded, otherwise false
  101. */
  102. _isPathExcluded(virPath) {
  103. return micromatch(virPath, this._excludes).length > 0;
  104. }
  105. /**
  106. * Validate if virtual path should be handled by the adapter.
  107. * This means that it either starts with the virtual base path of the adapter
  108. * or equals the base directory (base path without a trailing slash)
  109. *
  110. * @param {string} virPath Virtual Path
  111. * @returns {boolean} True if path should be handled
  112. */
  113. _isPathHandled(virPath) {
  114. // Check whether path starts with base path, or equals base directory
  115. return virPath.startsWith(this._virBasePath) || virPath === this._virBaseDir;
  116. }
  117. /**
  118. * Normalizes virtual glob patterns.
  119. *
  120. * @private
  121. * @param {string} virPattern glob pattern for virtual directory structure
  122. * @returns {string[]} A list of normalized glob patterns
  123. */
  124. _normalizePattern(virPattern) {
  125. const that = this;
  126. const mm = new minimatch.Minimatch(virPattern);
  127. const basePathParts = this._virBaseDir.split("/");
  128. function matchSubset(subset) {
  129. let i;
  130. for (i = 0; i < basePathParts.length; i++) {
  131. const globPart = subset[i];
  132. if (globPart === undefined) {
  133. log.verbose("Ran out of glob parts to match (this should not happen):");
  134. if (that._project) { // project is optional
  135. log.verbose(`Project: ${that._project.getName()}`);
  136. }
  137. log.verbose(`Virtual base path: ${that._virBaseDir}`);
  138. log.verbose(`Pattern to match: ${virPattern}`);
  139. log.verbose(`Current subset (tried index ${i}):`);
  140. log.verbose(subset);
  141. return {idx: i, virtualMatch: true};
  142. }
  143. const basePathPart = basePathParts[i];
  144. if (typeof globPart === "string") {
  145. if (globPart !== basePathPart) {
  146. return null;
  147. } else {
  148. continue;
  149. }
  150. } else if (globPart === minimatch.GLOBSTAR) {
  151. return {idx: i};
  152. } else { // Regex
  153. if (!globPart.test(basePathPart)) {
  154. return null;
  155. } else {
  156. continue;
  157. }
  158. }
  159. }
  160. if (subset.length === basePathParts.length) {
  161. return {rootMatch: true};
  162. }
  163. return {idx: i};
  164. }
  165. const resultGlobs = [];
  166. for (let i = 0; i < mm.set.length; i++) {
  167. const match = matchSubset(mm.set[i]);
  168. if (match) {
  169. let resultPattern;
  170. if (match.virtualMatch) {
  171. resultPattern = basePathParts.slice(0, match.idx).join("/");
  172. } else if (match.rootMatch) { // matched one up
  173. resultPattern = ""; // root "/"
  174. } else { // matched at some part of the glob
  175. resultPattern = mm.globParts[i].slice(match.idx).join("/");
  176. if (resultPattern.startsWith("/")) {
  177. resultPattern = resultPattern.substr(1);
  178. }
  179. }
  180. if (mm.negate) {
  181. resultPattern = "!" + resultPattern;
  182. }
  183. resultGlobs.push(resultPattern);
  184. }
  185. }
  186. return resultGlobs;
  187. }
  188. _createResource(parameters) {
  189. if (this._project) {
  190. parameters.project = this._project;
  191. }
  192. return new Resource(parameters);
  193. }
  194. _migrateResource(resource) {
  195. // This function only returns a promise if a migration is necessary.
  196. // Since this is rarely the case, we therefore reduce the amount of
  197. // created Promises by making this differentiation
  198. // Check if its a fs/Resource v3, function 'hasProject' was
  199. // introduced with v3 therefore take it as the indicator
  200. if (resource.hasProject) {
  201. return resource;
  202. }
  203. return this._createFromLegacyResource(resource);
  204. }
  205. async _createFromLegacyResource(resource) {
  206. const options = {
  207. path: resource._path,
  208. statInfo: resource._statInfo,
  209. source: resource._source
  210. };
  211. if (resource._stream) {
  212. options.buffer = await resource._getBufferFromStream();
  213. } else if (resource._createStream) {
  214. options.createStream = resource._createStream;
  215. } else if (resource._buffer) {
  216. options.buffer = resource._buffer;
  217. }
  218. return new Resource(options);
  219. }
  220. _assignProjectToResource(resource) {
  221. if (this._project) {
  222. // Assign project to resource if necessary
  223. if (resource.hasProject()) {
  224. if (resource.getProject() !== this._project) {
  225. throw new Error(
  226. `Unable to write resource associated with project ` +
  227. `${resource.getProject().getName()} into adapter of project ${this._project.getName()}: ` +
  228. resource.getPath());
  229. }
  230. return;
  231. }
  232. log.silly(`Associating resource ${resource.getPath()} with project ${this._project.getName()}`);
  233. resource.setProject(this._project);
  234. }
  235. }
  236. _resolveVirtualPathToBase(inputVirPath, writeMode = false) {
  237. if (!path.isAbsolute(inputVirPath)) {
  238. throw new Error(`Failed to resolve virtual path '${inputVirPath}': Path must be absolute`);
  239. }
  240. // Resolve any ".." segments to make sure we compare the effective start of the path
  241. // with the virBasePath
  242. const virPath = path.normalize(inputVirPath);
  243. if (!writeMode) {
  244. // When reading resources, validate against path excludes and return null if the given path
  245. // does not match this adapters base path
  246. if (!this._isPathHandled(virPath)) {
  247. if (log.isLevelEnabled("silly")) {
  248. log.silly(`Failed to resolve virtual path '${inputVirPath}': ` +
  249. `Resolved path does not start with adapter base path '${this._virBasePath}' or equals ` +
  250. `base dir: ${this._virBaseDir}`);
  251. }
  252. return null;
  253. }
  254. if (this._isPathExcluded(virPath)) {
  255. if (log.isLevelEnabled("silly")) {
  256. log.silly(`Failed to resolve virtual path '${inputVirPath}': ` +
  257. `Resolved path is excluded by configuration of adapter with base path '${this._virBasePath}'`);
  258. }
  259. return null;
  260. }
  261. } else if (!this._isPathHandled(virPath)) {
  262. // Resolved path is not within the configured base path and does
  263. // not equal the virtual base directory.
  264. // Since we don't want to write resources to foreign locations, we throw an error
  265. throw new Error(
  266. `Failed to write resource with virtual path '${inputVirPath}': Path must start with ` +
  267. `the configured virtual base path of the adapter. Base path: '${this._virBasePath}'`);
  268. }
  269. const relPath = virPath.substr(this._virBasePath.length);
  270. return relPath;
  271. }
  272. }
  273. export default AbstractAdapter;