1 | import { createReadStream, createWriteStream } from 'node:fs';
|
2 | import { chmod, lstat, mkdir, readdir, readlink, stat, symlink, utimes } from 'node:fs/promises';
|
3 | import { Readable } from 'node:stream';
|
4 | import { pipeline } from 'node:stream/promises';
|
5 | import { join as pathJoin, dirname, basename, resolve } from 'node:path';
|
6 | import { fsLchmod, fsLutimes, fsWalk, fsLstatExists } from '@shockpkg/archive-files';
|
7 | import { Queue } from "./queue.mjs";
|
8 | const userExec = 0b001000000;
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | export class Bundle {
|
18 | |
19 |
|
20 |
|
21 | excludes = [/^\./, /^ehthumbs\.db$/i, /^Thumbs\.db$/i];
|
22 |
|
23 | |
24 |
|
25 |
|
26 |
|
27 | |
28 |
|
29 |
|
30 |
|
31 | |
32 |
|
33 |
|
34 |
|
35 | |
36 |
|
37 |
|
38 | _isOpen = false;
|
39 |
|
40 | |
41 |
|
42 |
|
43 | _closeQueue = new Queue();
|
44 |
|
45 | |
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 | constructor(path, flat = false) {
|
52 | this.path = path;
|
53 | this.flat = flat;
|
54 | }
|
55 |
|
56 | |
57 |
|
58 |
|
59 |
|
60 |
|
61 | get isOpen() {
|
62 | return this._isOpen;
|
63 | }
|
64 |
|
65 | |
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 | isExcludedFile(name) {
|
72 | for (const exclude of this.excludes) {
|
73 | if (exclude.test(name)) {
|
74 | return true;
|
75 | }
|
76 | }
|
77 | return false;
|
78 | }
|
79 |
|
80 | |
81 |
|
82 |
|
83 | async open() {
|
84 | if (this._isOpen) {
|
85 | throw new Error('Already open');
|
86 | }
|
87 | await this._checkOutput();
|
88 | this._closeQueue.clear();
|
89 | await this._open();
|
90 | this._isOpen = true;
|
91 | }
|
92 |
|
93 | |
94 |
|
95 |
|
96 | async close() {
|
97 | this._assertIsOpen();
|
98 | try {
|
99 | await this._close();
|
100 | } finally {
|
101 | this._closeQueue.clear();
|
102 | }
|
103 | this._isOpen = false;
|
104 | }
|
105 |
|
106 | |
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 | async write(func = null) {
|
114 | await this.open();
|
115 | try {
|
116 | return func ? await func.call(this, this) : null;
|
117 | } finally {
|
118 | await this.close();
|
119 | }
|
120 | }
|
121 |
|
122 | |
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 | resourcePath(destination) {
|
129 | return pathJoin(dirname(this.projector.path), destination);
|
130 | }
|
131 |
|
132 | |
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 | async resourceExists(destination) {
|
139 | return !!(await fsLstatExists(this.resourcePath(destination)));
|
140 | }
|
141 |
|
142 | |
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 | async copyResource(destination, source, options = null) {
|
150 | this._assertIsOpen();
|
151 | const stat = await lstat(source);
|
152 | switch (true) {
|
153 | case stat.isSymbolicLink():
|
154 | {
|
155 | await this.copyResourceSymlink(destination, source, options);
|
156 | break;
|
157 | }
|
158 | case stat.isFile():
|
159 | {
|
160 | await this.copyResourceFile(destination, source, options);
|
161 | break;
|
162 | }
|
163 | case stat.isDirectory():
|
164 | {
|
165 | await this.copyResourceDirectory(destination, source, options);
|
166 | break;
|
167 | }
|
168 | default:
|
169 | {
|
170 | throw new Error(`Unsupported resource type: ${source}`);
|
171 | }
|
172 | }
|
173 | }
|
174 |
|
175 | |
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 | async copyResourceDirectory(destination, source, options = null) {
|
183 | this._assertIsOpen();
|
184 |
|
185 |
|
186 | await this.createResourceDirectory(destination, options ? await this._expandResourceOptionsCopy(options, async () => stat(source)) : options);
|
187 |
|
188 |
|
189 | if (options && options.noRecurse) {
|
190 | return;
|
191 | }
|
192 |
|
193 |
|
194 | const opts = {
|
195 | ...(options || {}),
|
196 | noRecurse: true
|
197 | };
|
198 | await fsWalk(source, async (path, stat) => {
|
199 |
|
200 | if (this.isExcludedFile(basename(path))) {
|
201 | return false;
|
202 | }
|
203 | await this.copyResource(pathJoin(destination, path), pathJoin(source, path), opts);
|
204 | return true;
|
205 | });
|
206 | }
|
207 |
|
208 | |
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
214 |
|
215 | async copyResourceFile(destination, source, options = null) {
|
216 | this._assertIsOpen();
|
217 | await this.streamResourceFile(destination, createReadStream(source), options ? await this._expandResourceOptionsCopy(options, async () => stat(source)) : options);
|
218 | }
|
219 |
|
220 | |
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 | async copyResourceSymlink(destination, source, options = null) {
|
228 | this._assertIsOpen();
|
229 | await this.createResourceSymlink(destination, await readlink(source), options ? await this._expandResourceOptionsCopy(options, async () => lstat(source)) : options);
|
230 | }
|
231 |
|
232 | |
233 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 | async createResourceDirectory(destination, options = null) {
|
239 | this._assertIsOpen();
|
240 | const dest = await this._assertNotResourceExists(destination, !!(options && options.merge));
|
241 | await mkdir(dest, {
|
242 | recursive: true
|
243 | });
|
244 |
|
245 |
|
246 | if (options && (options.atime || options.mtime)) {
|
247 |
|
248 |
|
249 | const abs = resolve(dest);
|
250 | this._closeQueue.push(this._setResourceAttributes.bind(this, abs, {
|
251 | ...options
|
252 | }), abs.length);
|
253 | }
|
254 | }
|
255 |
|
256 | |
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 | async createResourceFile(destination, data, options = null) {
|
264 | this._assertIsOpen();
|
265 | await this.streamResourceFile(destination, new Readable({
|
266 | |
267 |
|
268 |
|
269 | read() {
|
270 | this.push(data);
|
271 | this.push(null);
|
272 | }
|
273 | }), options);
|
274 | }
|
275 |
|
276 | |
277 |
|
278 |
|
279 |
|
280 |
|
281 |
|
282 |
|
283 | async createResourceSymlink(destination, target, options = null) {
|
284 | this._assertIsOpen();
|
285 | const dest = await this._assertNotResourceExists(destination);
|
286 | await mkdir(dirname(dest), {
|
287 | recursive: true
|
288 | });
|
289 | const t = typeof target === 'string' ? target : Buffer.from(target.buffer, target.byteOffset, target.byteLength);
|
290 | await symlink(t, dest);
|
291 | if (options) {
|
292 | await this._setResourceAttributes(dest, options);
|
293 | }
|
294 | }
|
295 |
|
296 | |
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 |
|
303 | async streamResourceFile(destination, data, options = null) {
|
304 | this._assertIsOpen();
|
305 | const dest = await this._assertNotResourceExists(destination);
|
306 | await mkdir(dirname(dest), {
|
307 | recursive: true
|
308 | });
|
309 | await pipeline(data, createWriteStream(dest));
|
310 | if (options) {
|
311 | await this._setResourceAttributes(dest, options);
|
312 | }
|
313 | }
|
314 |
|
315 | |
316 |
|
317 |
|
318 | async _checkOutput() {
|
319 | if (this.flat) {
|
320 | const p = dirname(this.path);
|
321 | if (await fsLstatExists(p)) {
|
322 | for (const n of await readdir(p)) {
|
323 | if (!this.isExcludedFile(n)) {
|
324 | throw new Error(`Output path not empty: ${p}`);
|
325 | }
|
326 | }
|
327 | }
|
328 | return;
|
329 | }
|
330 | await Promise.all([this.path, this.resourcePath('')].map(async p => {
|
331 | if (await fsLstatExists(p)) {
|
332 | throw new Error(`Output path already exists: ${p}`);
|
333 | }
|
334 | }));
|
335 | }
|
336 |
|
337 | |
338 |
|
339 |
|
340 |
|
341 |
|
342 |
|
343 |
|
344 | async _expandResourceOptionsCopy(options, stat) {
|
345 | const r = {
|
346 | ...options
|
347 | };
|
348 | let st;
|
349 | if (!r.atime && r.atimeCopy) {
|
350 | st = await stat();
|
351 | r.atime = st.atime;
|
352 | }
|
353 | if (!r.mtime && r.mtimeCopy) {
|
354 | st ??= await stat();
|
355 | r.mtime = st.mtime;
|
356 | }
|
357 | if (typeof r.executable !== 'boolean' && r.executableCopy) {
|
358 | st ??= await stat();
|
359 | r.executable = this._getResourceModeExecutable(st.mode);
|
360 | }
|
361 | return r;
|
362 | }
|
363 |
|
364 | |
365 |
|
366 |
|
367 |
|
368 |
|
369 |
|
370 | async _setResourceAttributes(path, options) {
|
371 | const {
|
372 | atime,
|
373 | mtime,
|
374 | executable
|
375 | } = options;
|
376 | const st = await lstat(path);
|
377 |
|
378 |
|
379 | if (typeof executable === 'boolean' && !st.isDirectory()) {
|
380 | if (st.isSymbolicLink()) {
|
381 | await fsLchmod(path, this._setResourceModeExecutable(
|
382 |
|
383 |
|
384 | st.mode & 0b111111111, executable));
|
385 | } else {
|
386 | await chmod(path, this._setResourceModeExecutable(st.mode, executable));
|
387 | }
|
388 | }
|
389 |
|
390 |
|
391 | if (atime || mtime) {
|
392 | if (st.isSymbolicLink()) {
|
393 | await fsLutimes(path, atime || st.atime, mtime || st.mtime);
|
394 | } else {
|
395 | await utimes(path, atime || st.atime, mtime || st.mtime);
|
396 | }
|
397 | }
|
398 | }
|
399 |
|
400 | |
401 |
|
402 |
|
403 |
|
404 |
|
405 |
|
406 | _getResourceModeExecutable(mode) {
|
407 |
|
408 | return !!(mode & userExec);
|
409 | }
|
410 |
|
411 | |
412 |
|
413 |
|
414 |
|
415 |
|
416 |
|
417 |
|
418 | _setResourceModeExecutable(mode, executable) {
|
419 |
|
420 | return (executable ? mode | userExec : mode & ~userExec) >>> 0;
|
421 | }
|
422 |
|
423 | |
424 |
|
425 |
|
426 | async _open() {
|
427 | await this.projector.write();
|
428 | }
|
429 |
|
430 | |
431 |
|
432 |
|
433 | async _close() {
|
434 | await this._closeQueue.run();
|
435 | }
|
436 |
|
437 | |
438 |
|
439 |
|
440 | _assertIsOpen() {
|
441 | if (!this._isOpen) {
|
442 | throw new Error('Not open');
|
443 | }
|
444 | }
|
445 |
|
446 | |
447 |
|
448 |
|
449 |
|
450 |
|
451 |
|
452 |
|
453 | async _assertNotResourceExists(destination, ignoreDirectory = false) {
|
454 | const dest = this.resourcePath(destination);
|
455 | const st = await fsLstatExists(dest);
|
456 | if (st && (!ignoreDirectory || !st.isDirectory())) {
|
457 | throw new Error(`Resource path exists: ${dest}`);
|
458 | }
|
459 | return dest;
|
460 | }
|
461 |
|
462 | |
463 |
|
464 |
|
465 |
|
466 |
|
467 | _getProjectorPath() {
|
468 | return this.flat ? this.path : this._getProjectorPathNested();
|
469 | }
|
470 |
|
471 | |
472 |
|
473 |
|
474 |
|
475 |
|
476 |
|
477 | |
478 |
|
479 |
|
480 |
|
481 |
|
482 | }
|
483 |
|
\ | No newline at end of file |