Skip to content

Commit b8e8ebc

Browse files
committed
[FEATURE] Add archive adapter
1 parent 3454bc1 commit b8e8ebc

File tree

4 files changed

+269
-1
lines changed

4 files changed

+269
-1
lines changed

lib/adapters/ZipArchive.js

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import logger from "@ui5/logger";
2+
const log = logger.getLogger("resources:adapters:ZIPArchive");
3+
import micromatch from "micromatch";
4+
import AbstractAdapter from "./AbstractAdapter.js";
5+
const {default: StreamZip} = await import("node-stream-zip");
6+
7+
/**
8+
* Virtual resource Adapter
9+
*
10+
* @public
11+
* @memberof module:@ui5/fs.adapters
12+
* @extends module:@ui5/fs.adapters.AbstractAdapter
13+
*/
14+
class ZIPArchive extends AbstractAdapter {
15+
/**
16+
* The constructor.
17+
*
18+
* @public
19+
* @param {object} parameters Parameters
20+
* @param {string} parameters.virBasePath Virtual base path
21+
* @param {object} parameters.project
22+
* @param {object} parameters.fsArchive
23+
* @param {string} parameters.archivePath
24+
* @param {string[]} [parameters.excludes] List of glob patterns to exclude
25+
*/
26+
constructor({virBasePath, project, fsArchive, archivePath = "/", excludes}) {
27+
super({virBasePath, project, excludes});
28+
this._zipPath = fsArchive;
29+
this._archivePath = archivePath;
30+
this._zipLoaded = null;
31+
this._virFiles = {}; // map full of files
32+
this._virDirs = {}; // map full of directories
33+
}
34+
35+
async _prepare() {
36+
if ( this._zipLoaded == null ) {
37+
this._zipLoaded = new Promise((resolve, reject) => {
38+
const zip = this._zip = new StreamZip({
39+
file: this._zipPath,
40+
storeEntries: true
41+
});
42+
43+
// Handle errors
44+
zip.on("error", (err) => {
45+
console.error(err);
46+
reject(err);
47+
});
48+
zip.on("ready", () => {
49+
log.info("Entries read: " + zip.entriesCount);
50+
for (const entry of Object.values(zip.entries())) {
51+
const desc = entry.isDirectory ? "directory" : `${entry.size} bytes`;
52+
if ( entry.name.startsWith("META-INF/resources/") ||
53+
entry.name.startsWith("META-INF/test-resources/") ) {
54+
const virPath = entry.name.slice("META-INF".length);
55+
if ( entry.isDirectory ) {
56+
this._virDirs[virPath] = this._createResource({
57+
project: this.project,
58+
statInfo: { // TODO: make closer to fs stat info
59+
isDirectory: function() {
60+
return true;
61+
}
62+
},
63+
path: virPath
64+
});
65+
} else {
66+
this._virFiles[virPath] = this._createResource({
67+
project: this.project,
68+
statInfo: { // TODO: make closer to fs stat info
69+
isDirectory: function() {
70+
return false;
71+
}
72+
},
73+
path: virPath,
74+
createStream: async () => {
75+
return new Promise((resolve, reject) => {
76+
zip.stream("META-INF" + virPath, (err, stm) => {
77+
if ( err ) {
78+
reject(err);
79+
} else {
80+
resolve(stm);
81+
}
82+
});
83+
});
84+
}
85+
});
86+
}
87+
log.info(`Entry ${virPath}: ${desc}`);
88+
} else {
89+
log.info(`Entry ignored: ${entry.name}`);
90+
let virPath = "/" + entry.name;
91+
if (virPath.startsWith(this._archivePath)) {
92+
log.info("orig path: " + virPath);
93+
log.info("archive path: " + this._archivePath);
94+
virPath = virPath.replace(this._archivePath, "");
95+
log.info("new path: " + virPath);
96+
if ( entry.isDirectory ) {
97+
this._virDirs[virPath] = true;
98+
} else {
99+
this._virFiles[virPath] = true;
100+
}
101+
}
102+
}
103+
}
104+
resolve();
105+
});
106+
});
107+
}
108+
109+
return this._zipLoaded;
110+
}
111+
112+
/**
113+
* Locate resources by glob.
114+
*
115+
* @private
116+
* @param {Array} patterns array of glob patterns
117+
* @param {object} [options={}] glob options
118+
* @param {boolean} [options.nodir=true] Do not match directories
119+
* @param {module:@ui5/fs.tracing.Trace} trace Trace instance
120+
* @returns {Promise<module:@ui5/fs.Resource[]>} Promise resolving to list of resources
121+
*/
122+
async _runGlob(patterns, options = {nodir: true}, trace) {
123+
if (patterns[0] === "" && !options.nodir) { // Match virtual root directory
124+
return [
125+
this._createResource({
126+
project: this.project,
127+
statInfo: { // TODO: make closer to fs stat info
128+
isDirectory: function() {
129+
return true;
130+
}
131+
},
132+
path: this._virBasePath.slice(0, -1)
133+
})
134+
];
135+
}
136+
137+
await this._prepare();
138+
139+
const filePaths = Object.keys(this._virFiles);
140+
const matchedFilePaths = micromatch(filePaths, patterns, {
141+
dot: true
142+
});
143+
// log.info(matchedFilePaths);
144+
let matchedResources = await Promise.all(matchedFilePaths.map(async (virPath) => {
145+
const stream = await new Promise((resolve, reject) => {
146+
this._zip.stream(this._archivePath.substring(1) + virPath, (err, stm) => {
147+
if ( err ) {
148+
reject(err);
149+
} else {
150+
resolve(stm);
151+
}
152+
});
153+
});
154+
return this._createResource({
155+
project: this.project,
156+
statInfo: { // TODO: make closer to fs stat info
157+
isDirectory: function() {
158+
return false;
159+
}
160+
},
161+
path: this._virBasePath + virPath,
162+
stream: stream
163+
});
164+
}));
165+
166+
if (!options.nodir) {
167+
const dirPaths = Object.keys(this._virDirs);
168+
const matchedDirs = micromatch(dirPaths, patterns, {
169+
dot: true
170+
});
171+
matchedResources = matchedResources.concat(matchedDirs.map((virPath) => {
172+
return this._createResource({
173+
project: this.project,
174+
statInfo: { // TODO: make closer to fs stat info
175+
isDirectory: function() {
176+
return true;
177+
}
178+
},
179+
path: this._virBasePath + virPath
180+
});
181+
}));
182+
}
183+
184+
// log.info(matchedResources);
185+
186+
return matchedResources;
187+
}
188+
189+
/**
190+
* Locates resources by path.
191+
*
192+
* @private
193+
* @param {string} virPath Virtual path
194+
* @param {object} options Options
195+
* @param {module:@ui5/fs.tracing.Trace} trace Trace instance
196+
* @returns {Promise<module:@ui5/fs.Resource>} Promise resolving to a single resource
197+
*/
198+
async _byPath(virPath, options, trace) {
199+
if (this.isPathExcluded(virPath)) {
200+
return null;
201+
}
202+
if (!virPath.startsWith(this._virBasePath) && virPath !== this._virBaseDir) {
203+
// Neither starts with basePath, nor equals baseDirectory
204+
return null;
205+
}
206+
207+
await this._prepare();
208+
209+
const relPath = virPath.substr(this._virBasePath.length);
210+
trace.pathCall();
211+
if (!this._virFiles[relPath]) {
212+
return null;
213+
}
214+
const stream = await new Promise((resolve, reject) => {
215+
this._zip.stream(this._archivePath.substring(1) + relPath, (err, stm) => {
216+
if ( err ) {
217+
reject(err);
218+
} else {
219+
resolve(stm);
220+
}
221+
});
222+
});
223+
return this._createResource({
224+
project: this.project,
225+
statInfo: { // TODO: make closer to fs stat info
226+
isDirectory: function() {
227+
return false;
228+
}
229+
},
230+
path: virPath,
231+
stream: stream
232+
});
233+
}
234+
}
235+
236+
export default ZIPArchive;

lib/resourceFactory.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import minimatch from "minimatch";
33
import DuplexCollection from "./DuplexCollection.js";
44
import FileSystem from "./adapters/FileSystem.js";
55
import MemAdapter from "./adapters/Memory.js";
6+
import ZipAdapter from "./adapters/ZipArchive.js";
67
import ReaderCollection from "./ReaderCollection.js";
78
import ReaderCollectionPrioritized from "./ReaderCollectionPrioritized.js";
89
import Resource from "./Resource.js";
@@ -32,6 +33,18 @@ import Link from "./readers/Link.js";
3233
*/
3334
export function createAdapter({fsBasePath, virBasePath, project, excludes}) {
3435
if (fsBasePath) {
36+
let dotZipIdx = fsBasePath.indexOf(".zip/");
37+
if (dotZipIdx === -1) {
38+
// Also support jar files
39+
dotZipIdx = fsBasePath.indexOf(".jar/");
40+
}
41+
if (dotZipIdx !== -1) {
42+
return new ZipAdapter({
43+
virBasePath,
44+
fsArchive: fsBasePath.substring(0, dotZipIdx + 4),
45+
archivePath: fsBasePath.substring(dotZipIdx + 4) + "/"
46+
});
47+
}
3548
const FsAdapter = FileSystem;
3649
return new FsAdapter({fsBasePath, virBasePath, project, excludes});
3750
} else {

package-lock.json

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@
129129
"micromatch": "^4.0.5",
130130
"minimatch": "^5.1.1",
131131
"pretty-hrtime": "^1.0.3",
132-
"random-int": "^3.0.0"
132+
"random-int": "^3.0.0",
133+
"node-stream-zip": "^1.15.0"
133134
},
134135
"devDependencies": {
135136
"@istanbuljs/esm-loader-hook": "^0.2.0",

0 commit comments

Comments
 (0)