UNPKG

@ui5/fs

Version:

UI5 CLI - File System Abstraction

320 lines (295 loc) 10.3 kB
import path from "node:path/posix"; import {getLogger} from "@ui5/logger"; const log = getLogger("resources:adapters:AbstractAdapter"); import {minimatch} from "minimatch"; import micromatch from "micromatch"; import AbstractReaderWriter from "../AbstractReaderWriter.js"; import Resource from "../Resource.js"; /** * Abstract Resource Adapter * * @abstract * @public * @class * @alias @ui5/fs/adapters/AbstractAdapter * @extends @ui5/fs/AbstractReaderWriter */ class AbstractAdapter extends AbstractReaderWriter { /** * The constructor * * @public * @param {object} parameters Parameters * @param {string} parameters.virBasePath * Virtual base path. Must be absolute, POSIX-style, and must end with a slash * @param {string[]} [parameters.excludes] List of glob patterns to exclude * @param {object} [parameters.project] Experimental, internal parameter. Do not use */ constructor({virBasePath, excludes = [], project}) { if (new.target === AbstractAdapter) { throw new TypeError("Class 'AbstractAdapter' is abstract"); } super(); if (!virBasePath) { throw new Error(`Unable to create adapter: Missing parameter 'virBasePath'`); } if (!path.isAbsolute(virBasePath)) { throw new Error(`Unable to create adapter: Virtual base path must be absolute but is '${virBasePath}'`); } if (!virBasePath.endsWith("/")) { throw new Error( `Unable to create adapter: Virtual base path must end with a slash but is '${virBasePath}'`); } this._virBasePath = virBasePath; this._virBaseDir = virBasePath.slice(0, -1); this._excludes = excludes; this._excludesNegated = excludes.map((pattern) => `!${pattern}`); this._project = project; } /** * Locates resources by glob. * * @abstract * @private * @param {string|string[]} virPattern glob pattern as string or an array of * glob patterns for virtual directory structure * @param {object} [options={}] glob options * @param {boolean} [options.nodir=true] Do not match directories * @param {@ui5/fs/tracing.Trace} trace Trace instance * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving to list of resources */ async _byGlob(virPattern, options = {nodir: true}, trace) { const excludes = this._excludesNegated; if (!(virPattern instanceof Array)) { virPattern = [virPattern]; } // Append static exclude patterns virPattern = Array.prototype.concat.apply(virPattern, excludes); const patterns = this._normalizePatterns(virPattern); if (patterns.length === 0) { return []; } if (!options.nodir) { for (let i = patterns.length - 1; i >= 0; i--) { const idx = this._virBaseDir.indexOf(patterns[i]); if (patterns[i] && idx !== -1 && idx < this._virBaseDir.length) { const subPath = patterns[i]; return [ this._createResource({ statInfo: { // TODO: make closer to fs stat info isDirectory: function() { return true; } }, source: { adapter: "Abstract" }, path: subPath }) ]; } } } return await this._runGlob(patterns, options, trace); } /** * Validate if virtual path should be excluded * * @param {string} virPath Virtual Path * @returns {boolean} True if path is excluded, otherwise false */ _isPathExcluded(virPath) { return micromatch(virPath, this._excludes).length > 0; } /** * Validate if virtual path should be handled by the adapter. * This means that it either starts with the virtual base path of the adapter * or equals the base directory (base path without a trailing slash) * * @param {string} virPath Virtual Path * @returns {boolean} True if path should be handled */ _isPathHandled(virPath) { // Check whether path starts with base path, or equals base directory return virPath.startsWith(this._virBasePath) || virPath === this._virBaseDir; } /** * Normalizes virtual glob patterns. * * @private * @param {string} virPattern glob pattern for virtual directory structure * @returns {string[]} A list of normalized glob patterns */ _normalizePattern(virPattern) { const that = this; const mm = new minimatch.Minimatch(virPattern); const basePathParts = this._virBaseDir.split("/"); function matchSubset(subset) { let i; for (i = 0; i < basePathParts.length; i++) { const globPart = subset[i]; if (globPart === undefined) { log.verbose("Ran out of glob parts to match (this should not happen):"); if (that._project) { // project is optional log.verbose(`Project: ${that._project.getName()}`); } log.verbose(`Virtual base path: ${that._virBaseDir}`); log.verbose(`Pattern to match: ${virPattern}`); log.verbose(`Current subset (tried index ${i}):`); log.verbose(subset); return {idx: i, virtualMatch: true}; } const basePathPart = basePathParts[i]; if (typeof globPart === "string") { if (globPart !== basePathPart) { return null; } else { continue; } } else if (globPart === minimatch.GLOBSTAR) { return {idx: i}; } else { // Regex if (!globPart.test(basePathPart)) { return null; } else { continue; } } } if (subset.length === basePathParts.length) { return {rootMatch: true}; } return {idx: i}; } const resultGlobs = []; for (let i = 0; i < mm.set.length; i++) { const match = matchSubset(mm.set[i]); if (match) { let resultPattern; if (match.virtualMatch) { resultPattern = basePathParts.slice(0, match.idx).join("/"); } else if (match.rootMatch) { // matched one up resultPattern = ""; // root "/" } else { // matched at some part of the glob resultPattern = mm.globParts[i].slice(match.idx).join("/"); if (resultPattern.startsWith("/")) { resultPattern = resultPattern.substr(1); } } if (mm.negate) { resultPattern = "!" + resultPattern; } resultGlobs.push(resultPattern); } } return resultGlobs; } /** * Normalizes virtual glob patterns and filters out negation-only pattern sets. * * @private * @param {string[]} virPatterns glob patterns for virtual directory structure * @returns {string[]} A list of normalized glob patterns */ _normalizePatterns(virPatterns) { let patterns = virPatterns.map(this._normalizePattern, this); patterns = Array.prototype.concat.apply([], patterns); if (patterns.length === 0) { return []; } // Check if only negation patterns remain (no positive patterns matched this adapter's base path) // This can happen when positive patterns target a different virtual base path // In globby v16+, negation-only patterns get '**/*' prepended, which would match all files // Note: Empty string "" IS a valid positive pattern (matches root directory) const hasOnlyNegativePatterns = patterns.every((pattern) => pattern.startsWith("!")); if (hasOnlyNegativePatterns) { return []; } return patterns; } _createResource(parameters) { if (this._project) { parameters.project = this._project; } return new Resource(parameters); } _migrateResource(resource) { // This function only returns a promise if a migration is necessary. // Since this is rarely the case, we therefore reduce the amount of // created Promises by making this differentiation // Check if its a fs/Resource v3, function 'hasProject' was // introduced with v3 therefore take it as the indicator if (resource.hasProject) { return resource; } return this._createFromLegacyResource(resource); } async _createFromLegacyResource(resource) { const options = { path: resource._path, statInfo: resource._statInfo, source: resource._source }; if (resource._stream) { options.buffer = await resource._getBufferFromStream(); } else if (resource._createStream) { options.createStream = resource._createStream; } else if (resource._buffer) { options.buffer = resource._buffer; } return new Resource(options); } _assignProjectToResource(resource) { if (this._project) { // Assign project to resource if necessary if (resource.hasProject()) { if (resource.getProject() !== this._project) { throw new Error( `Unable to write resource associated with project ` + `${resource.getProject().getName()} into adapter of project ${this._project.getName()}: ` + resource.getPath()); } return; } log.silly(`Associating resource ${resource.getPath()} with project ${this._project.getName()}`); resource.setProject(this._project); } } _resolveVirtualPathToBase(inputVirPath, writeMode = false) { if (!path.isAbsolute(inputVirPath)) { throw new Error(`Failed to resolve virtual path '${inputVirPath}': Path must be absolute`); } // Resolve any ".." segments to make sure we compare the effective start of the path // with the virBasePath const virPath = path.normalize(inputVirPath); if (!writeMode) { // When reading resources, validate against path excludes and return null if the given path // does not match this adapters base path if (!this._isPathHandled(virPath)) { if (log.isLevelEnabled("silly")) { log.silly(`Failed to resolve virtual path '${inputVirPath}': ` + `Resolved path does not start with adapter base path '${this._virBasePath}' or equals ` + `base dir: ${this._virBaseDir}`); } return null; } if (this._isPathExcluded(virPath)) { if (log.isLevelEnabled("silly")) { log.silly(`Failed to resolve virtual path '${inputVirPath}': ` + `Resolved path is excluded by configuration of adapter with base path '${this._virBasePath}'`); } return null; } } else if (!this._isPathHandled(virPath)) { // Resolved path is not within the configured base path and does // not equal the virtual base directory. // Since we don't want to write resources to foreign locations, we throw an error throw new Error( `Failed to write resource with virtual path '${inputVirPath}': Path must start with ` + `the configured virtual base path of the adapter. Base path: '${this._virBasePath}'`); } const relPath = virPath.substr(this._virBasePath.length); return relPath; } } export default AbstractAdapter;