@poe-platform/safe-fs 0.1.54 → 0.1.56
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,7 +96,9 @@ 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>;
|
|
101
|
+
private statWithAncestors;
|
|
98
102
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
99
103
|
readdir(path: string, options?: FsOptions): Promise<DirectoryEntry[]>;
|
|
100
104
|
readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
|
|
@@ -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,47 @@ 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");
|
|
703
|
+
return this.statWithAncestors(path, options);
|
|
704
|
+
}
|
|
705
|
+
async statWithAncestors(path, options) {
|
|
683
706
|
const normalized = normalize(path);
|
|
684
707
|
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")
|
|
708
|
+
return this.withWalkDeadline(options, async (scopedOptions) => {
|
|
709
|
+
try {
|
|
710
|
+
const stat = (await this.entries(normalized, "0", scopedOptions, collection)).get(normalized);
|
|
711
|
+
if (collection && stat.type !== "directory")
|
|
699
712
|
fail("ENOTDIR", "stat", path);
|
|
713
|
+
return stat;
|
|
700
714
|
}
|
|
701
|
-
|
|
702
|
-
|
|
715
|
+
catch (error) {
|
|
716
|
+
if (!isFsError(error, "ENOENT"))
|
|
717
|
+
throw error;
|
|
718
|
+
let parent = "";
|
|
719
|
+
for (const segment of normalized.slice(1).split("/").slice(0, -1)) {
|
|
720
|
+
parent += `/${segment}`;
|
|
721
|
+
const ancestor = (await this.entries(parent, "0", scopedOptions)).get(parent);
|
|
722
|
+
if (ancestor.type !== "directory")
|
|
723
|
+
fail("ENOTDIR", "stat", path);
|
|
724
|
+
}
|
|
725
|
+
throw error;
|
|
726
|
+
}
|
|
727
|
+
});
|
|
703
728
|
}
|
|
704
729
|
async lstat(path, options = {}) {
|
|
705
730
|
return this.stat(path, options);
|
|
@@ -751,6 +776,7 @@ export class WebDavFileSystem {
|
|
|
751
776
|
}.bind(this));
|
|
752
777
|
}
|
|
753
778
|
async prepareWrite(path, options) {
|
|
779
|
+
validateDirectoryWalkPath(path, "writeFile");
|
|
754
780
|
const normalized = normalize(path);
|
|
755
781
|
if (options.mode !== undefined)
|
|
756
782
|
this.unsupported("writeFile mode", path);
|
|
@@ -760,37 +786,39 @@ export class WebDavFileSystem {
|
|
|
760
786
|
fail("ECANCELED", "writeFile", path);
|
|
761
787
|
if (normalized === "/")
|
|
762
788
|
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
|
-
|
|
789
|
+
return this.withWalkDeadline(options, async (scopedOptions) => {
|
|
790
|
+
if (requiresCollection(path)) {
|
|
791
|
+
await this.stat(path, scopedOptions);
|
|
792
|
+
fail("EISDIR", "writeFile", path);
|
|
793
|
+
}
|
|
794
|
+
let parent = "";
|
|
795
|
+
for (const segment of normalized.slice(1).split("/").slice(0, -1)) {
|
|
796
|
+
parent += `/${segment}`;
|
|
797
|
+
await this.stat(`${parent}/`, scopedOptions);
|
|
798
|
+
}
|
|
799
|
+
const exclusive = options.flag === "wx" || options.flag === "ax";
|
|
800
|
+
const existing = exclusive ? undefined : await this.maybeStat(normalized, scopedOptions);
|
|
801
|
+
if (existing?.type === "directory")
|
|
802
|
+
fail("EISDIR", "writeFile", path);
|
|
803
|
+
let prefix = new Uint8Array();
|
|
804
|
+
const headers = { "Content-Type": "application/octet-stream" };
|
|
805
|
+
if (exclusive || (options.flag === "a" && !existing))
|
|
806
|
+
headers["If-None-Match"] = "*";
|
|
807
|
+
if (options.flag === "a" && existing) {
|
|
808
|
+
const snapshot = await this.request("GET", normalized, scopedOptions, { headers: { "Accept-Encoding": "identity" } }, async (response, signal) => {
|
|
809
|
+
if (response.status !== 200)
|
|
810
|
+
this.httpError(response.status, "GET", path);
|
|
811
|
+
const etag = strongEtag(response.headers.get("ETag"), path);
|
|
812
|
+
if (response.headers.get("Content-Encoding") && response.headers.get("Content-Encoding").toLowerCase() !== "identity") {
|
|
813
|
+
fail("ENOTSUP", "appendFile", path, "conditional append requires an identity representation");
|
|
814
|
+
}
|
|
815
|
+
return { etag, data: await this.bytes(response, this.maxResponseBytes, signal) };
|
|
816
|
+
});
|
|
817
|
+
prefix = snapshot.data;
|
|
818
|
+
headers["If-Match"] = snapshot.etag;
|
|
819
|
+
}
|
|
820
|
+
return { normalized, headers, prefix };
|
|
821
|
+
});
|
|
794
822
|
}
|
|
795
823
|
async writeFile(path, data, options = {}) {
|
|
796
824
|
if (!(data instanceof Uint8Array))
|
|
@@ -1165,8 +1193,9 @@ export class WebDavFileSystem {
|
|
|
1165
1193
|
if (mode & 2)
|
|
1166
1194
|
this.unsupported("access write/execute permission checks", path);
|
|
1167
1195
|
if (mode & 1)
|
|
1168
|
-
|
|
1169
|
-
|
|
1196
|
+
validateDirectoryWalkPath(path);
|
|
1197
|
+
// Non-execute probes do not inherit stat's raw-path limits.
|
|
1198
|
+
const stat = await (mode & 1 ? this.stat(path, options) : this.statWithAncestors(path, options));
|
|
1170
1199
|
if (options.signal?.aborted)
|
|
1171
1200
|
fail("ECANCELED", "access", path);
|
|
1172
1201
|
if ((mode & 1) && stat.type !== "directory")
|