diff --git a/lib/adapters/ZipArchive.js b/lib/adapters/ZipArchive.js new file mode 100644 index 00000000..8a29eb5f --- /dev/null +++ b/lib/adapters/ZipArchive.js @@ -0,0 +1,236 @@ +import logger from "@ui5/logger"; +const log = logger.getLogger("resources:adapters:ZIPArchive"); +import micromatch from "micromatch"; +import AbstractAdapter from "./AbstractAdapter.js"; +const {default: StreamZip} = await import("node-stream-zip"); + +/** + * Virtual resource Adapter + * + * @public + * @memberof module:@ui5/fs.adapters + * @extends module:@ui5/fs.adapters.AbstractAdapter + */ +class ZIPArchive extends AbstractAdapter { + /** + * The constructor. + * + * @public + * @param {object} parameters Parameters + * @param {string} parameters.virBasePath Virtual base path + * @param {object} parameters.project + * @param {object} parameters.fsArchive + * @param {string} parameters.archivePath + * @param {string[]} [parameters.excludes] List of glob patterns to exclude + */ + constructor({virBasePath, project, fsArchive, archivePath = "/", excludes}) { + super({virBasePath, project, excludes}); + this._zipPath = fsArchive; + this._archivePath = archivePath; + this._zipLoaded = null; + this._virFiles = {}; // map full of files + this._virDirs = {}; // map full of directories + } + + async _prepare() { + if ( this._zipLoaded == null ) { + this._zipLoaded = new Promise((resolve, reject) => { + const zip = this._zip = new StreamZip({ + file: this._zipPath, + storeEntries: true + }); + + // Handle errors + zip.on("error", (err) => { + console.error(err); + reject(err); + }); + zip.on("ready", () => { + log.info("Entries read: " + zip.entriesCount); + for (const entry of Object.values(zip.entries())) { + const desc = entry.isDirectory ? "directory" : `${entry.size} bytes`; + if ( entry.name.startsWith("META-INF/resources/") || + entry.name.startsWith("META-INF/test-resources/") ) { + const virPath = entry.name.slice("META-INF".length); + if ( entry.isDirectory ) { + this._virDirs[virPath] = this._createResource({ + project: this.project, + statInfo: { // TODO: make closer to fs stat info + isDirectory: function() { + return true; + } + }, + path: virPath + }); + } else { + this._virFiles[virPath] = this._createResource({ + project: this.project, + statInfo: { // TODO: make closer to fs stat info + isDirectory: function() { + return false; + } + }, + path: virPath, + createStream: async () => { + return new Promise((resolve, reject) => { + zip.stream("META-INF" + virPath, (err, stm) => { + if ( err ) { + reject(err); + } else { + resolve(stm); + } + }); + }); + } + }); + } + log.info(`Entry ${virPath}: ${desc}`); + } else { + log.info(`Entry ignored: ${entry.name}`); + let virPath = "/" + entry.name; + if (virPath.startsWith(this._archivePath)) { + log.info("orig path: " + virPath); + log.info("archive path: " + this._archivePath); + virPath = virPath.replace(this._archivePath, ""); + log.info("new path: " + virPath); + if ( entry.isDirectory ) { + this._virDirs[virPath] = true; + } else { + this._virFiles[virPath] = true; + } + } + } + } + resolve(); + }); + }); + } + + return this._zipLoaded; + } + + /** + * Locate resources by glob. + * + * @private + * @param {Array} patterns array of glob patterns + * @param {object} [options={}] glob options + * @param {boolean} [options.nodir=true] Do not match directories + * @param {module:@ui5/fs.tracing.Trace} trace Trace instance + * @returns {Promise} Promise resolving to list of resources + */ + async _runGlob(patterns, options = {nodir: true}, trace) { + if (patterns[0] === "" && !options.nodir) { // Match virtual root directory + return [ + this._createResource({ + project: this.project, + statInfo: { // TODO: make closer to fs stat info + isDirectory: function() { + return true; + } + }, + path: this._virBasePath.slice(0, -1) + }) + ]; + } + + await this._prepare(); + + const filePaths = Object.keys(this._virFiles); + const matchedFilePaths = micromatch(filePaths, patterns, { + dot: true + }); + // log.info(matchedFilePaths); + let matchedResources = await Promise.all(matchedFilePaths.map(async (virPath) => { + const stream = await new Promise((resolve, reject) => { + this._zip.stream(this._archivePath.substring(1) + virPath, (err, stm) => { + if ( err ) { + reject(err); + } else { + resolve(stm); + } + }); + }); + return this._createResource({ + project: this.project, + statInfo: { // TODO: make closer to fs stat info + isDirectory: function() { + return false; + } + }, + path: this._virBasePath + virPath, + stream: stream + }); + })); + + if (!options.nodir) { + const dirPaths = Object.keys(this._virDirs); + const matchedDirs = micromatch(dirPaths, patterns, { + dot: true + }); + matchedResources = matchedResources.concat(matchedDirs.map((virPath) => { + return this._createResource({ + project: this.project, + statInfo: { // TODO: make closer to fs stat info + isDirectory: function() { + return true; + } + }, + path: this._virBasePath + virPath + }); + })); + } + + // log.info(matchedResources); + + return matchedResources; + } + + /** + * Locates resources by path. + * + * @private + * @param {string} virPath Virtual path + * @param {object} options Options + * @param {module:@ui5/fs.tracing.Trace} trace Trace instance + * @returns {Promise} Promise resolving to a single resource + */ + async _byPath(virPath, options, trace) { + if (this.isPathExcluded(virPath)) { + return null; + } + if (!virPath.startsWith(this._virBasePath) && virPath !== this._virBaseDir) { + // Neither starts with basePath, nor equals baseDirectory + return null; + } + + await this._prepare(); + + const relPath = virPath.substr(this._virBasePath.length); + trace.pathCall(); + if (!this._virFiles[relPath]) { + return null; + } + const stream = await new Promise((resolve, reject) => { + this._zip.stream(this._archivePath.substring(1) + relPath, (err, stm) => { + if ( err ) { + reject(err); + } else { + resolve(stm); + } + }); + }); + return this._createResource({ + project: this.project, + statInfo: { // TODO: make closer to fs stat info + isDirectory: function() { + return false; + } + }, + path: virPath, + stream: stream + }); + } +} + +export default ZIPArchive; diff --git a/lib/resourceFactory.js b/lib/resourceFactory.js index 8539c529..eeebd727 100644 --- a/lib/resourceFactory.js +++ b/lib/resourceFactory.js @@ -3,6 +3,7 @@ import minimatch from "minimatch"; import DuplexCollection from "./DuplexCollection.js"; import FileSystem from "./adapters/FileSystem.js"; import MemAdapter from "./adapters/Memory.js"; +import ZipAdapter from "./adapters/ZipArchive.js"; import ReaderCollection from "./ReaderCollection.js"; import ReaderCollectionPrioritized from "./ReaderCollectionPrioritized.js"; import Resource from "./Resource.js"; @@ -32,6 +33,18 @@ import Link from "./readers/Link.js"; */ export function createAdapter({fsBasePath, virBasePath, project, excludes}) { if (fsBasePath) { + let dotZipIdx = fsBasePath.indexOf(".zip/"); + if (dotZipIdx === -1) { + // Also support jar files + dotZipIdx = fsBasePath.indexOf(".jar/"); + } + if (dotZipIdx !== -1) { + return new ZipAdapter({ + virBasePath, + fsArchive: fsBasePath.substring(0, dotZipIdx + 4), + archivePath: fsBasePath.substring(dotZipIdx + 4) + "/" + }); + } const FsAdapter = FileSystem; return new FsAdapter({fsBasePath, virBasePath, project, excludes}); } else { diff --git a/package-lock.json b/package-lock.json index 98de096a..50c52900 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "graceful-fs": "^4.2.10", "micromatch": "^4.0.5", "minimatch": "^5.1.1", + "node-stream-zip": "^1.15.0", "pretty-hrtime": "^1.0.3", "random-int": "^3.0.0" }, @@ -4856,6 +4857,18 @@ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", "dev": true }, + "node_modules/node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", + "engines": { + "node": ">=0.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/antelle" + } + }, "node_modules/nofilter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", @@ -11070,6 +11083,11 @@ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", "dev": true }, + "node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==" + }, "nofilter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", diff --git a/package.json b/package.json index 4206c599..0265d9a8 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,8 @@ "micromatch": "^4.0.5", "minimatch": "^5.1.1", "pretty-hrtime": "^1.0.3", - "random-int": "^3.0.0" + "random-int": "^3.0.0", + "node-stream-zip": "^1.15.0" }, "devDependencies": { "@istanbuljs/esm-loader-hook": "^0.2.0",