@poe-platform/safe-fs 0.1.54 → 0.1.55
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.
|
@@ -26,6 +26,7 @@ export interface WebDavFileSystemOptions {
|
|
|
26
26
|
readonly maxResponseBytes?: number;
|
|
27
27
|
readonly maxXmlBytes?: number;
|
|
28
28
|
readonly maxEntries?: number;
|
|
29
|
+
/** Per-request and aggregate stat/write-preflight walk timeout; defaults to 30,000 ms. */
|
|
29
30
|
readonly timeoutMs?: number;
|
|
30
31
|
readonly overwritePolicy?: "lock" | "etag";
|
|
31
32
|
readonly atomicEmptyDirectory?: WebDavAtomicEmptyDirectoryBinding;
|
|
@@ -72,6 +73,7 @@ export declare class WebDavFileSystem implements FileSystem {
|
|
|
72
73
|
private readonly maxXmlBytes;
|
|
73
74
|
private readonly maxEntries;
|
|
74
75
|
private readonly timeoutMs;
|
|
76
|
+
private readonly walkDeadlines;
|
|
75
77
|
private readonly overwritePolicy;
|
|
76
78
|
private readonly configuredComparison;
|
|
77
79
|
private readonly atomicEmptyDirectory;
|
|
@@ -94,6 +96,7 @@ export declare class WebDavFileSystem implements FileSystem {
|
|
|
94
96
|
private mutation;
|
|
95
97
|
private unsupported;
|
|
96
98
|
private maybeStat;
|
|
99
|
+
private withWalkDeadline;
|
|
97
100
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
98
101
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
99
102
|
readdir(path: string, options?: FsOptions): Promise<DirectoryEntry[]>;
|
|
@@ -87,7 +87,7 @@ function normalize(path) {
|
|
|
87
87
|
}
|
|
88
88
|
return `/${segments.join("/")}`;
|
|
89
89
|
}
|
|
90
|
-
function
|
|
90
|
+
function validateDirectoryWalkPath(path, syscall = "access") {
|
|
91
91
|
if (typeof path !== "string")
|
|
92
92
|
fail("EINVAL", "resolve", String(path), "invalid WebDAV path");
|
|
93
93
|
let bytes = 0;
|
|
@@ -105,7 +105,7 @@ function validateDirectoryAccessPath(path) {
|
|
|
105
105
|
if (point > 0xffff)
|
|
106
106
|
offset++;
|
|
107
107
|
if (bytes > 65_536 || components > 256) {
|
|
108
|
-
fail("ENAMETOOLONG",
|
|
108
|
+
fail("ENAMETOOLONG", syscall, path, "directory access exceeds the 64KiB path or 256 component limit");
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
}
|
|
@@ -157,6 +157,7 @@ export class WebDavFileSystem {
|
|
|
157
157
|
maxXmlBytes;
|
|
158
158
|
maxEntries;
|
|
159
159
|
timeoutMs;
|
|
160
|
+
walkDeadlines = new WeakMap();
|
|
160
161
|
overwritePolicy;
|
|
161
162
|
configuredComparison;
|
|
162
163
|
atomicEmptyDirectory;
|
|
@@ -320,7 +321,10 @@ export class WebDavFileSystem {
|
|
|
320
321
|
async *requestStream(method, path, options, init, consume, collection = false, received) {
|
|
321
322
|
if (options.signal?.aborted)
|
|
322
323
|
fail("ECANCELED", method, path);
|
|
323
|
-
const
|
|
324
|
+
const walk = this.walkDeadlines.get(options);
|
|
325
|
+
const deadline = walk?.deadline ?? createRequestTimeout(this.timeoutMs);
|
|
326
|
+
if (walk)
|
|
327
|
+
walk.deadline = deadline;
|
|
324
328
|
const timeout = deadline.signal;
|
|
325
329
|
try {
|
|
326
330
|
const headers = new Headers(this.headers);
|
|
@@ -409,7 +413,8 @@ export class WebDavFileSystem {
|
|
|
409
413
|
}
|
|
410
414
|
}
|
|
411
415
|
finally {
|
|
412
|
-
|
|
416
|
+
if (!walk)
|
|
417
|
+
deadline.dispose();
|
|
413
418
|
}
|
|
414
419
|
}
|
|
415
420
|
async bytes(response, limit, signal) {
|
|
@@ -679,27 +684,44 @@ export class WebDavFileSystem {
|
|
|
679
684
|
throw error;
|
|
680
685
|
}
|
|
681
686
|
}
|
|
687
|
+
async withWalkDeadline(options, walk) {
|
|
688
|
+
if (this.walkDeadlines.has(options))
|
|
689
|
+
return walk(options);
|
|
690
|
+
const scopedOptions = { ...options };
|
|
691
|
+
const owned = {};
|
|
692
|
+
this.walkDeadlines.set(scopedOptions, owned);
|
|
693
|
+
try {
|
|
694
|
+
return await walk(scopedOptions);
|
|
695
|
+
}
|
|
696
|
+
finally {
|
|
697
|
+
owned.deadline?.dispose();
|
|
698
|
+
this.walkDeadlines.delete(scopedOptions);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
682
701
|
async stat(path, options = {}) {
|
|
702
|
+
validateDirectoryWalkPath(path, "stat");
|
|
683
703
|
const normalized = normalize(path);
|
|
684
704
|
const collection = requiresCollection(path);
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
return stat;
|
|
690
|
-
}
|
|
691
|
-
catch (error) {
|
|
692
|
-
if (!isFsError(error, "ENOENT"))
|
|
693
|
-
throw error;
|
|
694
|
-
let parent = "";
|
|
695
|
-
for (const segment of normalized.slice(1).split("/").slice(0, -1)) {
|
|
696
|
-
parent += `/${segment}`;
|
|
697
|
-
const ancestor = (await this.entries(parent, "0", options)).get(parent);
|
|
698
|
-
if (ancestor.type !== "directory")
|
|
705
|
+
return this.withWalkDeadline(options, async (scopedOptions) => {
|
|
706
|
+
try {
|
|
707
|
+
const stat = (await this.entries(normalized, "0", scopedOptions, collection)).get(normalized);
|
|
708
|
+
if (collection && stat.type !== "directory")
|
|
699
709
|
fail("ENOTDIR", "stat", path);
|
|
710
|
+
return stat;
|
|
700
711
|
}
|
|
701
|
-
|
|
702
|
-
|
|
712
|
+
catch (error) {
|
|
713
|
+
if (!isFsError(error, "ENOENT"))
|
|
714
|
+
throw error;
|
|
715
|
+
let parent = "";
|
|
716
|
+
for (const segment of normalized.slice(1).split("/").slice(0, -1)) {
|
|
717
|
+
parent += `/${segment}`;
|
|
718
|
+
const ancestor = (await this.entries(parent, "0", scopedOptions)).get(parent);
|
|
719
|
+
if (ancestor.type !== "directory")
|
|
720
|
+
fail("ENOTDIR", "stat", path);
|
|
721
|
+
}
|
|
722
|
+
throw error;
|
|
723
|
+
}
|
|
724
|
+
});
|
|
703
725
|
}
|
|
704
726
|
async lstat(path, options = {}) {
|
|
705
727
|
return this.stat(path, options);
|
|
@@ -751,6 +773,7 @@ export class WebDavFileSystem {
|
|
|
751
773
|
}.bind(this));
|
|
752
774
|
}
|
|
753
775
|
async prepareWrite(path, options) {
|
|
776
|
+
validateDirectoryWalkPath(path, "writeFile");
|
|
754
777
|
const normalized = normalize(path);
|
|
755
778
|
if (options.mode !== undefined)
|
|
756
779
|
this.unsupported("writeFile mode", path);
|
|
@@ -760,37 +783,39 @@ export class WebDavFileSystem {
|
|
|
760
783
|
fail("ECANCELED", "writeFile", path);
|
|
761
784
|
if (normalized === "/")
|
|
762
785
|
fail("EISDIR", "writeFile", path);
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
786
|
+
return this.withWalkDeadline(options, async (scopedOptions) => {
|
|
787
|
+
if (requiresCollection(path)) {
|
|
788
|
+
await this.stat(path, scopedOptions);
|
|
789
|
+
fail("EISDIR", "writeFile", path);
|
|
790
|
+
}
|
|
791
|
+
let parent = "";
|
|
792
|
+
for (const segment of normalized.slice(1).split("/").slice(0, -1)) {
|
|
793
|
+
parent += `/${segment}`;
|
|
794
|
+
await this.stat(`${parent}/`, scopedOptions);
|
|
795
|
+
}
|
|
796
|
+
const exclusive = options.flag === "wx" || options.flag === "ax";
|
|
797
|
+
const existing = exclusive ? undefined : await this.maybeStat(normalized, scopedOptions);
|
|
798
|
+
if (existing?.type === "directory")
|
|
799
|
+
fail("EISDIR", "writeFile", path);
|
|
800
|
+
let prefix = new Uint8Array();
|
|
801
|
+
const headers = { "Content-Type": "application/octet-stream" };
|
|
802
|
+
if (exclusive || (options.flag === "a" && !existing))
|
|
803
|
+
headers["If-None-Match"] = "*";
|
|
804
|
+
if (options.flag === "a" && existing) {
|
|
805
|
+
const snapshot = await this.request("GET", normalized, scopedOptions, { headers: { "Accept-Encoding": "identity" } }, async (response, signal) => {
|
|
806
|
+
if (response.status !== 200)
|
|
807
|
+
this.httpError(response.status, "GET", path);
|
|
808
|
+
const etag = strongEtag(response.headers.get("ETag"), path);
|
|
809
|
+
if (response.headers.get("Content-Encoding") && response.headers.get("Content-Encoding").toLowerCase() !== "identity") {
|
|
810
|
+
fail("ENOTSUP", "appendFile", path, "conditional append requires an identity representation");
|
|
811
|
+
}
|
|
812
|
+
return { etag, data: await this.bytes(response, this.maxResponseBytes, signal) };
|
|
813
|
+
});
|
|
814
|
+
prefix = snapshot.data;
|
|
815
|
+
headers["If-Match"] = snapshot.etag;
|
|
816
|
+
}
|
|
817
|
+
return { normalized, headers, prefix };
|
|
818
|
+
});
|
|
794
819
|
}
|
|
795
820
|
async writeFile(path, data, options = {}) {
|
|
796
821
|
if (!(data instanceof Uint8Array))
|
|
@@ -1165,7 +1190,7 @@ export class WebDavFileSystem {
|
|
|
1165
1190
|
if (mode & 2)
|
|
1166
1191
|
this.unsupported("access write/execute permission checks", path);
|
|
1167
1192
|
if (mode & 1)
|
|
1168
|
-
|
|
1193
|
+
validateDirectoryWalkPath(path);
|
|
1169
1194
|
const stat = await this.stat(path, options);
|
|
1170
1195
|
if (options.signal?.aborted)
|
|
1171
1196
|
fail("ECANCELED", "access", path);
|