@gesslar/toolkit 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/lib/CappedDirectoryObject.js +341 -42
- package/src/lib/FileObject.js +51 -10
- package/src/lib/TempDirectoryObject.js +2 -2
- package/src/types/lib/CappedDirectoryObject.d.ts +53 -6
- package/src/types/lib/CappedDirectoryObject.d.ts.map +1 -1
- package/src/types/lib/FileObject.d.ts +21 -2
- package/src/types/lib/FileObject.d.ts.map +1 -1
package/package.json
CHANGED
|
@@ -13,6 +13,7 @@ import path from "node:path"
|
|
|
13
13
|
import {Data, Valid} from "../browser/index.js"
|
|
14
14
|
import DirectoryObject from "./DirectoryObject.js"
|
|
15
15
|
import FileObject from "./FileObject.js"
|
|
16
|
+
import FS from "./FS.js"
|
|
16
17
|
import Sass from "./Sass.js"
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -84,7 +85,8 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
84
85
|
"Parent must have the same cap as this directory.",
|
|
85
86
|
)
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
// Use real path for filesystem operations
|
|
89
|
+
const parentPath = parent.realPath || parent.path
|
|
88
90
|
|
|
89
91
|
// Validate parent's lineage traces back to the cap
|
|
90
92
|
let found = false
|
|
@@ -128,13 +130,13 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
128
130
|
*/
|
|
129
131
|
#validateCapPath() {
|
|
130
132
|
const cap = this.#cap
|
|
131
|
-
const resolved = path.resolve(this
|
|
133
|
+
const resolved = path.resolve(this.#realPath)
|
|
132
134
|
const capResolved = path.resolve(cap)
|
|
133
135
|
|
|
134
136
|
// Check if the resolved path starts with the cap directory
|
|
135
137
|
if(!resolved.startsWith(capResolved)) {
|
|
136
138
|
throw Sass.new(
|
|
137
|
-
`Path '${this
|
|
139
|
+
`Path '${this.#realPath}' is not within the cap directory '${cap}'`
|
|
138
140
|
)
|
|
139
141
|
}
|
|
140
142
|
}
|
|
@@ -157,6 +159,73 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
157
159
|
return true
|
|
158
160
|
}
|
|
159
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Returns the real filesystem path (for internal and subclass use).
|
|
164
|
+
*
|
|
165
|
+
* @protected
|
|
166
|
+
* @returns {string} The actual filesystem path
|
|
167
|
+
*/
|
|
168
|
+
get realPath() {
|
|
169
|
+
return super.path
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Private alias for realPath (for use in private methods).
|
|
174
|
+
*
|
|
175
|
+
* @private
|
|
176
|
+
* @returns {string} The actual filesystem path
|
|
177
|
+
*/
|
|
178
|
+
get #realPath() {
|
|
179
|
+
return this.realPath
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Returns the virtual path relative to the cap.
|
|
184
|
+
* This is the default path representation in the capped environment.
|
|
185
|
+
* Use `.real.path` to access the actual filesystem path.
|
|
186
|
+
*
|
|
187
|
+
* @returns {string} Path relative to cap, or "/" if at cap root
|
|
188
|
+
* @example
|
|
189
|
+
* const temp = new TempDirectoryObject("myapp")
|
|
190
|
+
* const subdir = temp.getDirectory("data/cache")
|
|
191
|
+
* console.log(subdir.path) // "/data/cache" (virtual, relative to cap)
|
|
192
|
+
* console.log(subdir.real.path) // "/tmp/myapp-ABC123/data/cache" (actual filesystem)
|
|
193
|
+
*/
|
|
194
|
+
get path() {
|
|
195
|
+
const capResolved = path.resolve(this.#cap)
|
|
196
|
+
const relative = path.relative(capResolved, this.#realPath)
|
|
197
|
+
|
|
198
|
+
// If at cap root or empty, return "/"
|
|
199
|
+
if(!relative || relative === ".") {
|
|
200
|
+
return "/"
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Return with leading slash to indicate it's cap-relative
|
|
204
|
+
return "/" + relative.split(path.sep).join("/")
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Returns a plain DirectoryObject representing the actual filesystem location.
|
|
209
|
+
* This provides an "escape hatch" from the capped environment to interact
|
|
210
|
+
* with the real filesystem when needed.
|
|
211
|
+
*
|
|
212
|
+
* @returns {DirectoryObject} Uncapped directory object at the real filesystem path
|
|
213
|
+
* @example
|
|
214
|
+
* const temp = new TempDirectoryObject("myapp")
|
|
215
|
+
* const subdir = temp.getDirectory("data")
|
|
216
|
+
*
|
|
217
|
+
* // Work within the capped environment (virtual paths)
|
|
218
|
+
* console.log(subdir.path) // "/data" (virtual)
|
|
219
|
+
* subdir.getFile("config.json") // Stays within cap
|
|
220
|
+
*
|
|
221
|
+
* // Break out to real filesystem when needed
|
|
222
|
+
* console.log(subdir.real.path) // "/tmp/myapp-ABC123/data" (real)
|
|
223
|
+
* subdir.real.parent // Can traverse outside the cap
|
|
224
|
+
*/
|
|
225
|
+
get real() {
|
|
226
|
+
return new DirectoryObject(this.#realPath)
|
|
227
|
+
}
|
|
228
|
+
|
|
160
229
|
/**
|
|
161
230
|
* Returns the parent directory of this capped directory.
|
|
162
231
|
* Returns null only if this directory is at the cap (the "root" of the capped tree).
|
|
@@ -175,12 +244,17 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
175
244
|
const capResolved = path.resolve(this.#cap)
|
|
176
245
|
|
|
177
246
|
// If we're at the cap, return null (cap is the "root")
|
|
178
|
-
if(this
|
|
247
|
+
if(this.#realPath === capResolved) {
|
|
179
248
|
return null
|
|
180
249
|
}
|
|
181
250
|
|
|
182
|
-
// Otherwise return the parent (plain DirectoryObject, not capped)
|
|
183
|
-
|
|
251
|
+
// Otherwise return the parent using real path (plain DirectoryObject, not capped)
|
|
252
|
+
const parentPath = path.dirname(this.#realPath)
|
|
253
|
+
const isRoot = parentPath === this.#realPath
|
|
254
|
+
|
|
255
|
+
return isRoot
|
|
256
|
+
? null
|
|
257
|
+
: new DirectoryObject(parentPath, this.temporary)
|
|
184
258
|
}
|
|
185
259
|
|
|
186
260
|
/**
|
|
@@ -201,19 +275,27 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
201
275
|
*#walkUpCapped() {
|
|
202
276
|
const capResolved = path.resolve(this.#cap)
|
|
203
277
|
|
|
204
|
-
//
|
|
205
|
-
|
|
278
|
+
// Build trail from real path
|
|
279
|
+
const trail = this.#realPath.split(path.sep).filter(Boolean)
|
|
280
|
+
const curr = [...trail]
|
|
281
|
+
|
|
282
|
+
while(curr.length > 0) {
|
|
283
|
+
const joined = path.sep + curr.join(path.sep)
|
|
284
|
+
|
|
206
285
|
// Don't yield anything beyond the cap
|
|
207
|
-
if(!
|
|
286
|
+
if(!joined.startsWith(capResolved)) {
|
|
208
287
|
break
|
|
209
288
|
}
|
|
210
289
|
|
|
211
|
-
|
|
290
|
+
// Yield plain DirectoryObject with real path
|
|
291
|
+
yield new DirectoryObject(joined, this.temporary)
|
|
212
292
|
|
|
213
293
|
// Stop after yielding the cap
|
|
214
|
-
if(
|
|
294
|
+
if(joined === capResolved) {
|
|
215
295
|
break
|
|
216
296
|
}
|
|
297
|
+
|
|
298
|
+
curr.pop()
|
|
217
299
|
}
|
|
218
300
|
}
|
|
219
301
|
|
|
@@ -229,69 +311,286 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
229
311
|
/**
|
|
230
312
|
* Creates a new CappedDirectoryObject by extending this directory's path.
|
|
231
313
|
*
|
|
232
|
-
*
|
|
314
|
+
* All paths are coerced to remain within the cap directory tree:
|
|
315
|
+
* - Absolute paths (e.g., "/foo") are treated as relative to the cap
|
|
316
|
+
* - Parent traversal ("..") is allowed but clamped at the cap boundary
|
|
317
|
+
* - The cap acts as the virtual root directory
|
|
233
318
|
*
|
|
234
|
-
* @param {string} newPath - The path
|
|
235
|
-
* @returns {CappedDirectoryObject} A new CappedDirectoryObject with the
|
|
236
|
-
* @throws {Sass} If the path would escape the cap directory
|
|
237
|
-
* @throws {Sass} If the path is absolute
|
|
238
|
-
* @throws {Sass} If the path contains traversal (..)
|
|
319
|
+
* @param {string} newPath - The path to resolve (can be absolute or contain ..)
|
|
320
|
+
* @returns {CappedDirectoryObject} A new CappedDirectoryObject with the coerced path
|
|
239
321
|
* @example
|
|
240
322
|
* const capped = new TempDirectoryObject("myapp")
|
|
241
323
|
* const subDir = capped.getDirectory("data")
|
|
242
324
|
* console.log(subDir.path) // "/tmp/myapp-ABC123/data"
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* // Absolute paths are relative to cap
|
|
328
|
+
* const abs = capped.getDirectory("/foo/bar")
|
|
329
|
+
* console.log(abs.path) // "/tmp/myapp-ABC123/foo/bar"
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* // Excessive .. traversal clamps to cap
|
|
333
|
+
* const up = capped.getDirectory("../../../etc/passwd")
|
|
334
|
+
* console.log(up.path) // "/tmp/myapp-ABC123" (clamped to cap)
|
|
243
335
|
*/
|
|
244
336
|
getDirectory(newPath) {
|
|
245
337
|
Valid.type(newPath, "String")
|
|
246
338
|
|
|
247
|
-
//
|
|
339
|
+
// Fast path: if it's a simple name (no separators, not absolute, no ..)
|
|
340
|
+
// use the subclass constructor directly to preserve type
|
|
341
|
+
const isSimpleName = !path.isAbsolute(newPath) &&
|
|
342
|
+
!newPath.includes("/") &&
|
|
343
|
+
!newPath.includes("\\") &&
|
|
344
|
+
!newPath.includes("..")
|
|
345
|
+
|
|
346
|
+
if(isSimpleName) {
|
|
347
|
+
// For CappedDirectoryObject, pass (name, cap, parent, temporary)
|
|
348
|
+
// For TempDirectoryObject subclass, it expects (name, parent) but will
|
|
349
|
+
// internally call super with the cap parameter
|
|
350
|
+
if(this.constructor === CappedDirectoryObject) {
|
|
351
|
+
return new CappedDirectoryObject(
|
|
352
|
+
newPath,
|
|
353
|
+
this.#cap,
|
|
354
|
+
this,
|
|
355
|
+
this.temporary
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// For subclasses like TempDirectoryObject
|
|
360
|
+
return new this.constructor(newPath, this)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Complex path - handle coercion
|
|
364
|
+
const capResolved = path.resolve(this.#cap)
|
|
365
|
+
let targetPath
|
|
366
|
+
|
|
367
|
+
// If absolute, treat as relative to cap (virtual root)
|
|
248
368
|
if(path.isAbsolute(newPath)) {
|
|
249
|
-
|
|
369
|
+
// Strip leading slashes to make relative
|
|
370
|
+
const relative = newPath.replace(/^[/\\]+/, "")
|
|
371
|
+
|
|
372
|
+
// Join with cap (unless empty, which means cap root)
|
|
373
|
+
targetPath = relative ? path.join(capResolved, relative) : capResolved
|
|
374
|
+
} else {
|
|
375
|
+
// Relative path - resolve from current directory
|
|
376
|
+
targetPath = FS.resolvePath(this.#realPath, newPath)
|
|
250
377
|
}
|
|
251
378
|
|
|
252
|
-
//
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
379
|
+
// Resolve to absolute path (handles .. and .)
|
|
380
|
+
const resolved = path.resolve(targetPath)
|
|
381
|
+
|
|
382
|
+
// Coerce: if path escaped cap, clamp to cap boundary
|
|
383
|
+
const coerced = resolved.startsWith(capResolved)
|
|
384
|
+
? resolved
|
|
385
|
+
: capResolved
|
|
386
|
+
|
|
387
|
+
// Compute path relative to cap for reconstruction
|
|
388
|
+
const relativeToCap = path.relative(capResolved, coerced)
|
|
389
|
+
|
|
390
|
+
// If we're at the cap root, return cap root directory
|
|
391
|
+
if(!relativeToCap || relativeToCap === ".") {
|
|
392
|
+
return this.#createCappedAtRoot()
|
|
256
393
|
}
|
|
257
394
|
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
395
|
+
// Build directory by traversing segments from cap
|
|
396
|
+
return this.#buildDirectoryFromRelativePath(relativeToCap)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Creates a CappedDirectoryObject at the cap root.
|
|
401
|
+
* Can be overridden by subclasses that have different root semantics.
|
|
402
|
+
*
|
|
403
|
+
* @private
|
|
404
|
+
* @returns {CappedDirectoryObject} Directory object at cap root
|
|
405
|
+
*/
|
|
406
|
+
#createCappedAtRoot() {
|
|
407
|
+
// Create a base CappedDirectoryObject at the cap path
|
|
408
|
+
// This works for direct usage of CappedDirectoryObject
|
|
409
|
+
// Subclasses may need to override if they have special semantics
|
|
410
|
+
return new CappedDirectoryObject(null, this.#cap, null, this.temporary)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Builds a directory by traversing path segments from cap.
|
|
415
|
+
*
|
|
416
|
+
* @private
|
|
417
|
+
* @param {string} relativePath - Path relative to cap
|
|
418
|
+
* @returns {CappedDirectoryObject} The directory at the final path
|
|
419
|
+
*/
|
|
420
|
+
#buildDirectoryFromRelativePath(relativePath) {
|
|
421
|
+
const segments = relativePath.split(path.sep).filter(Boolean)
|
|
422
|
+
|
|
423
|
+
// Start at cap root
|
|
424
|
+
let current = this.#createCappedAtRoot()
|
|
425
|
+
|
|
426
|
+
// Traverse each segment, creating CappedDirectoryObject instances
|
|
427
|
+
// (not subclass instances, to avoid constructor signature issues)
|
|
428
|
+
for(const segment of segments) {
|
|
429
|
+
current = new CappedDirectoryObject(
|
|
430
|
+
segment,
|
|
431
|
+
this.#cap,
|
|
432
|
+
current,
|
|
433
|
+
this.temporary
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return current
|
|
261
438
|
}
|
|
262
439
|
|
|
263
440
|
/**
|
|
264
441
|
* Creates a new FileObject by extending this directory's path.
|
|
265
442
|
*
|
|
266
|
-
*
|
|
443
|
+
* All paths are coerced to remain within the cap directory tree:
|
|
444
|
+
* - Absolute paths (e.g., "/config.json") are treated as relative to the cap
|
|
445
|
+
* - Parent traversal ("..") is allowed but clamped at the cap boundary
|
|
446
|
+
* - The cap acts as the virtual root directory
|
|
267
447
|
*
|
|
268
|
-
* @param {string} filename - The filename to
|
|
269
|
-
* @returns {FileObject} A new FileObject with the
|
|
270
|
-
* @throws {Sass} If the path would escape the cap directory
|
|
271
|
-
* @throws {Sass} If the path is absolute
|
|
272
|
-
* @throws {Sass} If the path contains traversal (..)
|
|
448
|
+
* @param {string} filename - The filename to resolve (can be absolute or contain ..)
|
|
449
|
+
* @returns {FileObject} A new FileObject with the coerced path
|
|
273
450
|
* @example
|
|
274
451
|
* const capped = new TempDirectoryObject("myapp")
|
|
275
452
|
* const file = capped.getFile("config.json")
|
|
276
453
|
* console.log(file.path) // "/tmp/myapp-ABC123/config.json"
|
|
454
|
+
*
|
|
455
|
+
* @example
|
|
456
|
+
* // Absolute paths are relative to cap
|
|
457
|
+
* const abs = capped.getFile("/data/config.json")
|
|
458
|
+
* console.log(abs.path) // "/tmp/myapp-ABC123/data/config.json"
|
|
459
|
+
*
|
|
460
|
+
* @example
|
|
461
|
+
* // Excessive .. traversal clamps to cap
|
|
462
|
+
* const up = capped.getFile("../../../etc/passwd")
|
|
463
|
+
* console.log(up.path) // "/tmp/myapp-ABC123/passwd" (clamped to cap)
|
|
277
464
|
*/
|
|
278
465
|
getFile(filename) {
|
|
279
466
|
Valid.type(filename, "String")
|
|
280
467
|
|
|
281
|
-
//
|
|
468
|
+
// Fast path: if it's a simple filename (no separators, not absolute, no ..)
|
|
469
|
+
// use this as the parent directly
|
|
470
|
+
const isSimpleName = !path.isAbsolute(filename) &&
|
|
471
|
+
!filename.includes("/") &&
|
|
472
|
+
!filename.includes("\\") &&
|
|
473
|
+
!filename.includes("..")
|
|
474
|
+
|
|
475
|
+
if(isSimpleName) {
|
|
476
|
+
// Simple filename - create directly with this as parent
|
|
477
|
+
return new FileObject(filename, this)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Complex path - handle coercion
|
|
481
|
+
const capResolved = path.resolve(this.#cap)
|
|
482
|
+
let targetPath
|
|
483
|
+
|
|
484
|
+
// If absolute, treat as relative to cap (virtual root)
|
|
282
485
|
if(path.isAbsolute(filename)) {
|
|
283
|
-
|
|
486
|
+
// Strip leading slashes to make relative
|
|
487
|
+
const relative = filename.replace(/^[/\\]+/, "")
|
|
488
|
+
|
|
489
|
+
// Join with cap
|
|
490
|
+
targetPath = path.join(capResolved, relative)
|
|
491
|
+
} else {
|
|
492
|
+
// Relative path - resolve from current directory
|
|
493
|
+
targetPath = FS.resolvePath(this.#realPath, filename)
|
|
284
494
|
}
|
|
285
495
|
|
|
286
|
-
//
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
496
|
+
// Resolve to absolute path (handles .. and .)
|
|
497
|
+
const resolved = path.resolve(targetPath)
|
|
498
|
+
|
|
499
|
+
// Coerce: if path escaped cap, clamp to cap boundary
|
|
500
|
+
const coerced = resolved.startsWith(capResolved)
|
|
501
|
+
? resolved
|
|
502
|
+
: capResolved
|
|
503
|
+
|
|
504
|
+
// Extract directory and filename parts
|
|
505
|
+
let fileDir = path.dirname(coerced)
|
|
506
|
+
let fileBasename = path.basename(coerced)
|
|
507
|
+
|
|
508
|
+
// Special case: if coerced is exactly the cap (file tried to escape),
|
|
509
|
+
// the file should be placed at the cap root with just the filename
|
|
510
|
+
if(coerced === capResolved) {
|
|
511
|
+
// Extract just the filename from the original path
|
|
512
|
+
fileBasename = path.basename(resolved)
|
|
513
|
+
fileDir = capResolved
|
|
290
514
|
}
|
|
291
515
|
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
516
|
+
// Get or create the parent directory
|
|
517
|
+
const relativeToCap = path.relative(capResolved, fileDir)
|
|
518
|
+
const parentDir = !relativeToCap || relativeToCap === "."
|
|
519
|
+
? this.#createCappedAtRoot()
|
|
520
|
+
: this.#buildDirectoryFromRelativePath(relativeToCap)
|
|
521
|
+
|
|
522
|
+
// Create FileObject with parent directory
|
|
523
|
+
return new FileObject(fileBasename, parentDir)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Override exists to use real filesystem path.
|
|
528
|
+
*
|
|
529
|
+
* @returns {Promise<boolean>} Whether the directory exists
|
|
530
|
+
*/
|
|
531
|
+
get exists() {
|
|
532
|
+
return this.real.exists
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Override read to use real filesystem path and return capped objects.
|
|
537
|
+
*
|
|
538
|
+
* @param {string} [pat=""] - Optional glob pattern
|
|
539
|
+
* @returns {Promise<{files: Array<FileObject>, directories: Array}>} Directory contents
|
|
540
|
+
*/
|
|
541
|
+
async read(pat="") {
|
|
542
|
+
const {files, directories} = await this.real.read(pat)
|
|
543
|
+
|
|
544
|
+
// Convert plain DirectoryObjects to CappedDirectoryObjects with same cap
|
|
545
|
+
const cappedDirectories = directories.map(dir => {
|
|
546
|
+
const name = dir.name
|
|
547
|
+
|
|
548
|
+
return new this.constructor(name, this)
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
return {files, directories: cappedDirectories}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Override assureExists to use real filesystem path.
|
|
556
|
+
*
|
|
557
|
+
* @param {object} [options] - Options for mkdir
|
|
558
|
+
* @returns {Promise<void>}
|
|
559
|
+
*/
|
|
560
|
+
async assureExists(options = {}) {
|
|
561
|
+
return await this.real.assureExists(options)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Override delete to use real filesystem path.
|
|
566
|
+
*
|
|
567
|
+
* @returns {Promise<void>}
|
|
568
|
+
*/
|
|
569
|
+
async delete() {
|
|
570
|
+
return await this.real.delete()
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Override remove to preserve temporary flag check.
|
|
575
|
+
*
|
|
576
|
+
* @returns {Promise<void>}
|
|
577
|
+
*/
|
|
578
|
+
async remove() {
|
|
579
|
+
if(!this.temporary)
|
|
580
|
+
throw Sass.new("This is not a temporary directory.")
|
|
581
|
+
|
|
582
|
+
const {files, directories} = await this.read()
|
|
583
|
+
|
|
584
|
+
// Remove subdirectories recursively
|
|
585
|
+
for(const dir of directories)
|
|
586
|
+
await dir.remove()
|
|
587
|
+
|
|
588
|
+
// Remove files
|
|
589
|
+
for(const file of files)
|
|
590
|
+
await file.delete()
|
|
591
|
+
|
|
592
|
+
// Delete the now-empty directory
|
|
593
|
+
await this.delete()
|
|
295
594
|
}
|
|
296
595
|
|
|
297
596
|
/**
|
|
@@ -300,6 +599,6 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
300
599
|
* @returns {string} string representation of the CappedDirectoryObject
|
|
301
600
|
*/
|
|
302
601
|
toString() {
|
|
303
|
-
return `[CappedDirectoryObject: ${this.path}]`
|
|
602
|
+
return `[CappedDirectoryObject: ${this.path} (real: ${this.#realPath})]`
|
|
304
603
|
}
|
|
305
604
|
}
|
package/src/lib/FileObject.js
CHANGED
|
@@ -96,6 +96,7 @@ export default class FileObject extends FS {
|
|
|
96
96
|
case "String":
|
|
97
97
|
return new DirectoryObject(parent)
|
|
98
98
|
case "DirectoryObject":
|
|
99
|
+
case "CappedDirectoryObject":
|
|
99
100
|
case "TempDirectoryObject":
|
|
100
101
|
return parent
|
|
101
102
|
default:
|
|
@@ -103,7 +104,9 @@ export default class FileObject extends FS {
|
|
|
103
104
|
}
|
|
104
105
|
})()
|
|
105
106
|
|
|
106
|
-
|
|
107
|
+
// Use real path if parent is capped, otherwise use path
|
|
108
|
+
const parentPath = parentObject.realPath || parentObject.path
|
|
109
|
+
const final = FS.resolvePath(parentPath ?? ".", fixedFile)
|
|
107
110
|
|
|
108
111
|
const resolved = final
|
|
109
112
|
const url = new URL(FS.pathToUri(resolved))
|
|
@@ -113,7 +116,9 @@ export default class FileObject extends FS {
|
|
|
113
116
|
|
|
114
117
|
// If the file is directly in the provided parent directory, reuse that object
|
|
115
118
|
// Otherwise, create a DirectoryObject for the actual parent directory
|
|
116
|
-
|
|
119
|
+
// Use real path for comparison if parent is capped
|
|
120
|
+
const parentRealPath = parentObject.realPath || parentObject.path
|
|
121
|
+
const actualParent = parentObject && actualParentPath === parentRealPath
|
|
117
122
|
? parentObject
|
|
118
123
|
: new DirectoryObject(actualParentPath)
|
|
119
124
|
|
|
@@ -184,12 +189,28 @@ export default class FileObject extends FS {
|
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
/**
|
|
187
|
-
*
|
|
192
|
+
* Returns the file path. If the parent is a capped directory, returns the
|
|
193
|
+
* virtual path relative to the cap. Otherwise returns the real filesystem path.
|
|
194
|
+
* Use `.real.path` to always get the actual filesystem path.
|
|
188
195
|
*
|
|
189
|
-
* @returns {string} The
|
|
196
|
+
* @returns {string} The file path (virtual if parent is capped, real otherwise)
|
|
190
197
|
*/
|
|
191
198
|
get path() {
|
|
192
|
-
|
|
199
|
+
const realPath = this.#meta.path
|
|
200
|
+
const parent = this.#meta.parent
|
|
201
|
+
|
|
202
|
+
// If parent is capped, return virtual path
|
|
203
|
+
if(parent?.capped) {
|
|
204
|
+
const cap = parent.cap
|
|
205
|
+
const capResolved = path.resolve(cap)
|
|
206
|
+
const relative = path.relative(capResolved, realPath)
|
|
207
|
+
|
|
208
|
+
// Return with leading slash to indicate it's cap-relative
|
|
209
|
+
return "/" + relative.split(path.sep).join("/")
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Otherwise return real path
|
|
213
|
+
return realPath
|
|
193
214
|
}
|
|
194
215
|
|
|
195
216
|
/**
|
|
@@ -264,6 +285,26 @@ export default class FileObject extends FS {
|
|
|
264
285
|
return this.#meta.parent
|
|
265
286
|
}
|
|
266
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Returns a plain FileObject representing the actual filesystem location.
|
|
290
|
+
* This provides an "escape hatch" when working with capped directories,
|
|
291
|
+
* allowing direct filesystem access when needed.
|
|
292
|
+
*
|
|
293
|
+
* @returns {FileObject} Uncapped file object at the real filesystem path
|
|
294
|
+
* @example
|
|
295
|
+
* const temp = new TempDirectoryObject("myapp")
|
|
296
|
+
* const file = temp.getFile("/config/app.json")
|
|
297
|
+
*
|
|
298
|
+
* // file.path shows virtual path
|
|
299
|
+
* console.log(file.path) // "/config/app.json"
|
|
300
|
+
* // file.real.path shows actual filesystem path
|
|
301
|
+
* console.log(file.real.path) // "/tmp/myapp-ABC123/config/app.json"
|
|
302
|
+
* file.real.parent.parent // Can traverse outside the cap
|
|
303
|
+
*/
|
|
304
|
+
get real() {
|
|
305
|
+
return new FileObject(this.#meta.path)
|
|
306
|
+
}
|
|
307
|
+
|
|
267
308
|
/**
|
|
268
309
|
* Check if a file can be read. Returns true if the file can be read, false
|
|
269
310
|
*
|
|
@@ -271,7 +312,7 @@ export default class FileObject extends FS {
|
|
|
271
312
|
*/
|
|
272
313
|
async canRead() {
|
|
273
314
|
try {
|
|
274
|
-
await fs.access(this.path, fs.constants.R_OK)
|
|
315
|
+
await fs.access(this.#meta.path, fs.constants.R_OK)
|
|
275
316
|
|
|
276
317
|
return true
|
|
277
318
|
} catch(_) {
|
|
@@ -286,7 +327,7 @@ export default class FileObject extends FS {
|
|
|
286
327
|
*/
|
|
287
328
|
async canWrite() {
|
|
288
329
|
try {
|
|
289
|
-
await fs.access(this.path, fs.constants.W_OK)
|
|
330
|
+
await fs.access(this.#meta.path, fs.constants.W_OK)
|
|
290
331
|
|
|
291
332
|
return true
|
|
292
333
|
} catch(_) {
|
|
@@ -301,7 +342,7 @@ export default class FileObject extends FS {
|
|
|
301
342
|
*/
|
|
302
343
|
async #fileExists() {
|
|
303
344
|
try {
|
|
304
|
-
await fs.access(this.path, fs.constants.F_OK)
|
|
345
|
+
await fs.access(this.#meta.path, fs.constants.F_OK)
|
|
305
346
|
|
|
306
347
|
return true
|
|
307
348
|
} catch(_) {
|
|
@@ -316,7 +357,7 @@ export default class FileObject extends FS {
|
|
|
316
357
|
*/
|
|
317
358
|
async size() {
|
|
318
359
|
try {
|
|
319
|
-
const stat = await fs.stat(this.path)
|
|
360
|
+
const stat = await fs.stat(this.#meta.path)
|
|
320
361
|
|
|
321
362
|
return stat.size
|
|
322
363
|
} catch(_) {
|
|
@@ -332,7 +373,7 @@ export default class FileObject extends FS {
|
|
|
332
373
|
*/
|
|
333
374
|
async modified() {
|
|
334
375
|
try {
|
|
335
|
-
const stat = await fs.stat(this.path)
|
|
376
|
+
const stat = await fs.stat(this.#meta.path)
|
|
336
377
|
|
|
337
378
|
return stat.mtime
|
|
338
379
|
} catch(_) {
|
|
@@ -86,12 +86,12 @@ export default class TempDirectoryObject extends CappedDirectoryObject {
|
|
|
86
86
|
*/
|
|
87
87
|
#createDirectory() {
|
|
88
88
|
try {
|
|
89
|
-
fs.mkdirSync(this.
|
|
89
|
+
fs.mkdirSync(this.realPath)
|
|
90
90
|
} catch(e) {
|
|
91
91
|
// EEXIST is fine - directory already exists
|
|
92
92
|
if(e.code !== "EEXIST") {
|
|
93
93
|
throw Sass.new(
|
|
94
|
-
`Unable to create temporary directory '${this.
|
|
94
|
+
`Unable to create temporary directory '${this.realPath}': ${e.message}`
|
|
95
95
|
)
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -38,6 +38,32 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
38
38
|
* @returns {boolean} Always true for CappedDirectoryObject instances
|
|
39
39
|
*/
|
|
40
40
|
get capped(): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Returns the real filesystem path (for internal and subclass use).
|
|
43
|
+
*
|
|
44
|
+
* @protected
|
|
45
|
+
* @returns {string} The actual filesystem path
|
|
46
|
+
*/
|
|
47
|
+
protected get realPath(): string;
|
|
48
|
+
/**
|
|
49
|
+
* Returns a plain DirectoryObject representing the actual filesystem location.
|
|
50
|
+
* This provides an "escape hatch" from the capped environment to interact
|
|
51
|
+
* with the real filesystem when needed.
|
|
52
|
+
*
|
|
53
|
+
* @returns {DirectoryObject} Uncapped directory object at the real filesystem path
|
|
54
|
+
* @example
|
|
55
|
+
* const temp = new TempDirectoryObject("myapp")
|
|
56
|
+
* const subdir = temp.getDirectory("data")
|
|
57
|
+
*
|
|
58
|
+
* // Work within the capped environment (virtual paths)
|
|
59
|
+
* console.log(subdir.path) // "/data" (virtual)
|
|
60
|
+
* subdir.getFile("config.json") // Stays within cap
|
|
61
|
+
*
|
|
62
|
+
* // Break out to real filesystem when needed
|
|
63
|
+
* console.log(subdir.real.path) // "/tmp/myapp-ABC123/data" (real)
|
|
64
|
+
* subdir.real.parent // Can traverse outside the cap
|
|
65
|
+
*/
|
|
66
|
+
get real(): DirectoryObject;
|
|
41
67
|
/**
|
|
42
68
|
* Returns a generator that walks up to the cap.
|
|
43
69
|
*
|
|
@@ -47,20 +73,41 @@ export default class CappedDirectoryObject extends DirectoryObject {
|
|
|
47
73
|
/**
|
|
48
74
|
* Creates a new CappedDirectoryObject by extending this directory's path.
|
|
49
75
|
*
|
|
50
|
-
*
|
|
76
|
+
* All paths are coerced to remain within the cap directory tree:
|
|
77
|
+
* - Absolute paths (e.g., "/foo") are treated as relative to the cap
|
|
78
|
+
* - Parent traversal ("..") is allowed but clamped at the cap boundary
|
|
79
|
+
* - The cap acts as the virtual root directory
|
|
51
80
|
*
|
|
52
|
-
* @param {string} newPath - The path
|
|
53
|
-
* @returns {CappedDirectoryObject} A new CappedDirectoryObject with the
|
|
54
|
-
* @throws {Sass} If the path would escape the cap directory
|
|
55
|
-
* @throws {Sass} If the path is absolute
|
|
56
|
-
* @throws {Sass} If the path contains traversal (..)
|
|
81
|
+
* @param {string} newPath - The path to resolve (can be absolute or contain ..)
|
|
82
|
+
* @returns {CappedDirectoryObject} A new CappedDirectoryObject with the coerced path
|
|
57
83
|
* @example
|
|
58
84
|
* const capped = new TempDirectoryObject("myapp")
|
|
59
85
|
* const subDir = capped.getDirectory("data")
|
|
60
86
|
* console.log(subDir.path) // "/tmp/myapp-ABC123/data"
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* // Absolute paths are relative to cap
|
|
90
|
+
* const abs = capped.getDirectory("/foo/bar")
|
|
91
|
+
* console.log(abs.path) // "/tmp/myapp-ABC123/foo/bar"
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* // Excessive .. traversal clamps to cap
|
|
95
|
+
* const up = capped.getDirectory("../../../etc/passwd")
|
|
96
|
+
* console.log(up.path) // "/tmp/myapp-ABC123" (clamped to cap)
|
|
61
97
|
*/
|
|
62
98
|
getDirectory(newPath: string): CappedDirectoryObject;
|
|
99
|
+
/**
|
|
100
|
+
* Override read to use real filesystem path and return capped objects.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} [pat=""] - Optional glob pattern
|
|
103
|
+
* @returns {Promise<{files: Array<FileObject>, directories: Array}>} Directory contents
|
|
104
|
+
*/
|
|
105
|
+
read(pat?: string): Promise<{
|
|
106
|
+
files: Array<FileObject>;
|
|
107
|
+
directories: any[];
|
|
108
|
+
}>;
|
|
63
109
|
#private;
|
|
64
110
|
}
|
|
65
111
|
import DirectoryObject from "./DirectoryObject.js";
|
|
112
|
+
import FileObject from "./FileObject.js";
|
|
66
113
|
//# sourceMappingURL=CappedDirectoryObject.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CappedDirectoryObject.d.ts","sourceRoot":"","sources":["../../lib/CappedDirectoryObject.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CappedDirectoryObject.d.ts","sourceRoot":"","sources":["../../lib/CappedDirectoryObject.js"],"names":[],"mappings":"AAkBA;;;;;;;;GAQG;AACH;IAGE;;;;;;;;;;;;;;;;OAgBG;IACH,kBAXW,MAAM,OAAC,OACP,MAAM,WACN,qBAAqB,OAAC,cACtB,OAAO,EAmFjB;IAqBD;;;;OAIG;IACH,WAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,cAFa,OAAO,CAInB;IAED;;;;;OAKG;IACH,0BAFa,MAAM,CAIlB;IAqCD;;;;;;;;;;;;;;;;;OAiBG;IACH,YAba,eAAe,CAe3B;IA2ED;;;;OAIG;IACH,cAFa,SAAS,CAAC,eAAe,CAAC,CAItC;IAED;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,sBAjBW,MAAM,GACJ,qBAAqB,CA6EjC;IA0ID;;;;;OAKG;IACH,WAHW,MAAM,GACJ,OAAO,CAAC;QAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;QAAC,WAAW,QAAO;KAAC,CAAC,CAanE;;CAoDF;4BA9kB2B,sBAAsB;uBAC3B,iBAAiB"}
|
|
@@ -50,9 +50,11 @@ export default class FileObject extends FS {
|
|
|
50
50
|
*/
|
|
51
51
|
get supplied(): string;
|
|
52
52
|
/**
|
|
53
|
-
*
|
|
53
|
+
* Returns the file path. If the parent is a capped directory, returns the
|
|
54
|
+
* virtual path relative to the cap. Otherwise returns the real filesystem path.
|
|
55
|
+
* Use `.real.path` to always get the actual filesystem path.
|
|
54
56
|
*
|
|
55
|
-
* @returns {string} The
|
|
57
|
+
* @returns {string} The file path (virtual if parent is capped, real otherwise)
|
|
56
58
|
*/
|
|
57
59
|
get path(): string;
|
|
58
60
|
/**
|
|
@@ -107,6 +109,23 @@ export default class FileObject extends FS {
|
|
|
107
109
|
* @returns {DirectoryObject} The parent directory object
|
|
108
110
|
*/
|
|
109
111
|
get parent(): DirectoryObject;
|
|
112
|
+
/**
|
|
113
|
+
* Returns a plain FileObject representing the actual filesystem location.
|
|
114
|
+
* This provides an "escape hatch" when working with capped directories,
|
|
115
|
+
* allowing direct filesystem access when needed.
|
|
116
|
+
*
|
|
117
|
+
* @returns {FileObject} Uncapped file object at the real filesystem path
|
|
118
|
+
* @example
|
|
119
|
+
* const temp = new TempDirectoryObject("myapp")
|
|
120
|
+
* const file = temp.getFile("/config/app.json")
|
|
121
|
+
*
|
|
122
|
+
* // file.path shows virtual path
|
|
123
|
+
* console.log(file.path) // "/config/app.json"
|
|
124
|
+
* // file.real.path shows actual filesystem path
|
|
125
|
+
* console.log(file.real.path) // "/tmp/myapp-ABC123/config/app.json"
|
|
126
|
+
* file.real.parent.parent // Can traverse outside the cap
|
|
127
|
+
*/
|
|
128
|
+
get real(): FileObject;
|
|
110
129
|
/**
|
|
111
130
|
* Check if a file can be read. Returns true if the file can be read, false
|
|
112
131
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileObject.d.ts","sourceRoot":"","sources":["../../lib/FileObject.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;;GAcG;AAEH;
|
|
1
|
+
{"version":3,"file":"FileObject.d.ts","sourceRoot":"","sources":["../../lib/FileObject.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;;GAcG;AAEH;uBAmIe,MAAM;IAlInB;;;;;OAKG;IACH,yBAFU;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC,OAAO,KAAK,GAAG,OAAO,IAAI,CAAC,CAAA;KAAC,CAO1D;IA2BF;;;;;OAKG;IACH,sBAHW,MAAM,GAAG,UAAU,WACnB,eAAe,GAAC,MAAM,GAAC,IAAI,EAuDrC;IAWD;;;;OAIG;IACH,UAFa,MAAM,CAclB;IAWD;;;;OAIG;IACH,cAFa,OAAO,CAAC,OAAO,CAAC,CAI5B;IAED;;;;OAIG;IACH,gBAFa,MAAM,CAIlB;IAED;;;;;;OAMG;IACH,YAFa,MAAM,CAkBlB;IAED;;;;OAIG;IACH,WAFa,GAAG,CAIf;IAED;;;;OAIG;IACH,YAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,cAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,iBAFa,MAAM,CAIlB;IACD;;;;OAIG;IACH,cAFa,OAAO,CAInB;IAED;;;;OAIG;IACH,mBAFa,OAAO,CAInB;IAED;;;;;;;;;;;;;;OAcG;IACH,cAFa,eAAe,CAI3B;IAED;;;;;;;;;;;;;;;OAeG;IACH,YAXa,UAAU,CAatB;IAED;;;;OAIG;IACH,WAFa,OAAO,CAAC,OAAO,CAAC,CAU5B;IAED;;;;OAIG;IACH,YAFa,OAAO,CAAC,OAAO,CAAC,CAU5B;IAiBD;;;;OAIG;IACH,QAFa,OAAO,CAAC,MAAM,OAAC,CAAC,CAU5B;IAED;;;;;OAKG;IACH,YAFa,OAAO,CAAC,IAAI,OAAC,CAAC,CAU1B;IAsBD;;;;;OAKG;IACH,gBAHW,MAAM,GACJ,OAAO,CAAC,MAAM,CAAC,CAY3B;IAED;;;;;;;;;;;OAWG;IACH,cARa,OAAO,CAAC,MAAM,CAAC,CAkB3B;IAED;;;;;;;;;;;OAWG;IACH,eARW,MAAM,aACN,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAezB;IAED;;;;;;;;;;;;;;;OAeG;IACH,kBAXW,WAAW,GAAC,IAAI,GAAC,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC,CAyBzB;IAED;;;;;;;;;;;;;;OAcG;IACH,gBAXW,MAAM,aACN,MAAM,GACJ,OAAO,CAAC,OAAO,CAAC,CAkC5B;IAED;;;;OAIG;IACH,UAFa,OAAO,CAAC,MAAM,CAAC,CAY3B;IAED;;;;;;;;;OASG;IACH,UAPa,OAAO,CAAC,IAAI,CAAC,CAiBzB;;CACF;eAtjBc,SAAS;4BADI,sBAAsB;kBARhC,OAAO;iBAIR,MAAM"}
|