@poe-platform/safe-fs 0.1.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/LICENSE +21 -0
- package/README.md +290 -0
- package/dist/safe-fs/bridge/confinement.d.ts +3 -0
- package/dist/safe-fs/bridge/confinement.js +171 -0
- package/dist/safe-fs/bridge/filesystem.d.ts +88 -0
- package/dist/safe-fs/bridge/filesystem.js +411 -0
- package/dist/safe-fs/bridge/index.d.ts +6 -0
- package/dist/safe-fs/bridge/index.js +18 -0
- package/dist/safe-fs/bridge/stats.d.ts +4 -0
- package/dist/safe-fs/bridge/stats.js +37 -0
- package/dist/safe-fs/bridge/types.d.ts +75 -0
- package/dist/safe-fs/bridge/types.js +1 -0
- package/dist/safe-fs/bridge/values.d.ts +6 -0
- package/dist/safe-fs/bridge/values.js +65 -0
- package/dist/safe-fs/config/memory.d.ts +3 -0
- package/dist/safe-fs/config/memory.js +9 -0
- package/dist/safe-fs/config/real.d.ts +5 -0
- package/dist/safe-fs/config/real.js +16 -0
- package/dist/safe-fs/config.d.ts +15 -0
- package/dist/safe-fs/config.js +46 -0
- package/dist/safe-fs/config.node.d.ts +2 -0
- package/dist/safe-fs/config.node.js +22 -0
- package/dist/safe-fs/contracts/abort.d.ts +5 -0
- package/dist/safe-fs/contracts/abort.js +34 -0
- package/dist/safe-fs/contracts/cleanup.d.ts +1 -0
- package/dist/safe-fs/contracts/cleanup.js +9 -0
- package/dist/safe-fs/contracts/errors.d.ts +50 -0
- package/dist/safe-fs/contracts/errors.js +77 -0
- package/dist/safe-fs/contracts/filesystem.d.ts +96 -0
- package/dist/safe-fs/contracts/filesystem.js +1 -0
- package/dist/safe-fs/contracts/index.d.ts +4 -0
- package/dist/safe-fs/contracts/index.js +4 -0
- package/dist/safe-fs/contracts/io.d.ts +8 -0
- package/dist/safe-fs/contracts/io.js +92 -0
- package/dist/safe-fs/contracts/path.d.ts +8 -0
- package/dist/safe-fs/contracts/path.js +8 -0
- package/dist/safe-fs/contracts/portable-path.d.ts +13 -0
- package/dist/safe-fs/contracts/portable-path.js +57 -0
- package/dist/safe-fs/contracts/virtual-path.d.ts +7 -0
- package/dist/safe-fs/contracts/virtual-path.js +66 -0
- package/dist/safe-fs/core.d.ts +12 -0
- package/dist/safe-fs/core.js +12 -0
- package/dist/safe-fs/fs/memory/index.d.ts +58 -0
- package/dist/safe-fs/fs/memory/index.js +532 -0
- package/dist/safe-fs/fs/mount/comparison.d.ts +19 -0
- package/dist/safe-fs/fs/mount/comparison.js +118 -0
- package/dist/safe-fs/fs/mount/identity.d.ts +2 -0
- package/dist/safe-fs/fs/mount/identity.js +12 -0
- package/dist/safe-fs/fs/mount/index.d.ts +44 -0
- package/dist/safe-fs/fs/mount/index.js +546 -0
- package/dist/safe-fs/fs/overlay/index.d.ts +73 -0
- package/dist/safe-fs/fs/overlay/index.js +927 -0
- package/dist/safe-fs/fs/readonly/index.d.ts +31 -0
- package/dist/safe-fs/fs/readonly/index.js +128 -0
- package/dist/safe-fs/fs/real/allocation.d.ts +1 -0
- package/dist/safe-fs/fs/real/allocation.js +8 -0
- package/dist/safe-fs/fs/real/index.d.ts +76 -0
- package/dist/safe-fs/fs/real/index.js +543 -0
- package/dist/safe-fs/fs/s3/authority.d.ts +7 -0
- package/dist/safe-fs/fs/s3/authority.js +31 -0
- package/dist/safe-fs/fs/s3/filesystem.d.ts +93 -0
- package/dist/safe-fs/fs/s3/filesystem.js +1024 -0
- package/dist/safe-fs/fs/s3/http/index.d.ts +2 -0
- package/dist/safe-fs/fs/s3/http/index.js +1 -0
- package/dist/safe-fs/fs/s3/http/request.d.ts +16 -0
- package/dist/safe-fs/fs/s3/http/request.js +196 -0
- package/dist/safe-fs/fs/s3/http/signature.d.ts +20 -0
- package/dist/safe-fs/fs/s3/http/signature.js +74 -0
- package/dist/safe-fs/fs/s3/http/transport.d.ts +3 -0
- package/dist/safe-fs/fs/s3/http/transport.js +455 -0
- package/dist/safe-fs/fs/s3/http/types.d.ts +30 -0
- package/dist/safe-fs/fs/s3/http/types.js +1 -0
- package/dist/safe-fs/fs/s3/http/xml.d.ts +11 -0
- package/dist/safe-fs/fs/s3/http/xml.js +188 -0
- package/dist/safe-fs/fs/s3/index.d.ts +6 -0
- package/dist/safe-fs/fs/s3/index.js +3 -0
- package/dist/safe-fs/fs/s3/mock.d.ts +48 -0
- package/dist/safe-fs/fs/s3/mock.js +233 -0
- package/dist/safe-fs/fs/s3/registry.d.ts +10 -0
- package/dist/safe-fs/fs/s3/registry.js +71 -0
- package/dist/safe-fs/fs/s3/transport.d.ts +103 -0
- package/dist/safe-fs/fs/s3/transport.js +28 -0
- package/dist/safe-fs/fs/webdav/index.d.ts +2 -0
- package/dist/safe-fs/fs/webdav/index.js +1 -0
- package/dist/safe-fs/fs/webdav/resource-id.d.ts +18 -0
- package/dist/safe-fs/fs/webdav/resource-id.js +109 -0
- package/dist/safe-fs/fs/webdav/webdav.d.ts +101 -0
- package/dist/safe-fs/fs/webdav/webdav.js +1226 -0
- package/dist/safe-fs/fs/webdav/xml.d.ts +15 -0
- package/dist/safe-fs/fs/webdav/xml.js +250 -0
- package/dist/safe-fs/index.d.ts +14 -0
- package/dist/safe-fs/index.js +12 -0
- package/dist/safe-fs/node/filesystem.d.ts +11 -0
- package/dist/safe-fs/node/filesystem.js +28 -0
- package/dist/safe-fs/node/index.d.ts +2 -0
- package/dist/safe-fs/node/index.js +1 -0
- package/dist/safe-fs/node/stats.d.ts +1 -0
- package/dist/safe-fs/node/stats.js +1 -0
- package/dist/safe-fs/node/values.d.ts +1 -0
- package/dist/safe-fs/node/values.js +1 -0
- package/dist/safe-fs/node-host.d.ts +1 -0
- package/dist/safe-fs/node-host.js +1 -0
- package/dist/safe-fs/node-unavailable.d.ts +1 -0
- package/dist/safe-fs/node-unavailable.js +1 -0
- package/dist/safe-fs/platform/browser.d.ts +12 -0
- package/dist/safe-fs/platform/browser.js +39 -0
- package/dist/safe-fs/platform/node.d.ts +13 -0
- package/dist/safe-fs/platform/node.js +26 -0
- package/package.json +108 -0
|
@@ -0,0 +1,1024 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { FsError, isFsError } from "../../contracts/errors.js";
|
|
3
|
+
import { composeAbortSignals } from "../../contracts/abort.js";
|
|
4
|
+
import { collectBytes, readBytes } from "../../contracts/io.js";
|
|
5
|
+
import { compareEntries, registerEntryAuthority } from "../mount/comparison.js";
|
|
6
|
+
import { compareOwnedS3Entries, queryS3Head, recordS3Stat, registerS3EntryOwner } from "./authority.js";
|
|
7
|
+
import { encodeCopySource } from "./transport.js";
|
|
8
|
+
export class S3RenameError extends FsError {
|
|
9
|
+
phase;
|
|
10
|
+
copiedKeys;
|
|
11
|
+
deletedKeys;
|
|
12
|
+
constructor(source, destination, phase, copied, deleted, cause) {
|
|
13
|
+
super(cause.code, {
|
|
14
|
+
syscall: "rename", path: source, dest: destination, cause,
|
|
15
|
+
message: `non-atomic S3 rename failed during ${phase}; destination copies or partial source deletions may remain`,
|
|
16
|
+
});
|
|
17
|
+
this.name = "S3RenameError";
|
|
18
|
+
this.phase = phase;
|
|
19
|
+
this.copiedKeys = Object.freeze([...copied]);
|
|
20
|
+
this.deletedKeys = Object.freeze([...deleted]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function serviceCode(error) {
|
|
24
|
+
if (typeof error !== "object" || error === null)
|
|
25
|
+
return undefined;
|
|
26
|
+
const record = error;
|
|
27
|
+
const value = record.Code ?? record.code ?? record.name;
|
|
28
|
+
return typeof value === "string" ? value : undefined;
|
|
29
|
+
}
|
|
30
|
+
function translate(error, syscall, path, precondition = "EAGAIN") {
|
|
31
|
+
if (error instanceof FsError)
|
|
32
|
+
return error;
|
|
33
|
+
const status = error?.$metadata?.httpStatusCode;
|
|
34
|
+
const name = serviceCode(error);
|
|
35
|
+
let code = "EIO";
|
|
36
|
+
if (name === "AbortError" || status === 499)
|
|
37
|
+
code = "ECANCELED";
|
|
38
|
+
else if (name === "NoSuchKey" || name === "NotFound" || name === "NoSuchBucket" || status === 404)
|
|
39
|
+
code = "ENOENT";
|
|
40
|
+
else if (["AccessDenied", "InvalidAccessKeyId", "SignatureDoesNotMatch", "ExpiredToken"].includes(name ?? "") || status === 401 || status === 403)
|
|
41
|
+
code = "EACCES";
|
|
42
|
+
else if (name === "PreconditionFailed" || status === 412)
|
|
43
|
+
code = precondition;
|
|
44
|
+
else if (name === "ConditionalRequestConflict" || name === "SlowDown" || status === 409 || status === 429 || status === 503)
|
|
45
|
+
code = "EAGAIN";
|
|
46
|
+
else if (name === "NotImplemented" || status === 501)
|
|
47
|
+
code = "ENOTSUP";
|
|
48
|
+
else if (name === "EntityTooLarge")
|
|
49
|
+
code = "EFBIG";
|
|
50
|
+
else if (name === "InvalidArgument" || name === "InvalidRequest" || status === 400)
|
|
51
|
+
code = "EINVAL";
|
|
52
|
+
else if (name === "TimeoutError" || name === "RequestTimeout" || status === 408)
|
|
53
|
+
code = "ETIMEDOUT";
|
|
54
|
+
return new FsError(code, { syscall, path, cause: error });
|
|
55
|
+
}
|
|
56
|
+
function fail(code, syscall, path, message) {
|
|
57
|
+
throw new FsError(code, { syscall, path, ...(message === undefined ? {} : { message }) });
|
|
58
|
+
}
|
|
59
|
+
function validateLimit(value, name, minimum, maximum = Number.MAX_SAFE_INTEGER) {
|
|
60
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
61
|
+
throw new FsError("EINVAL", { message: `${name} must be an integer from ${minimum} to ${maximum}` });
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function isWellFormed(value) {
|
|
66
|
+
return !/[\uD800-\uDFFF]/u.test(value);
|
|
67
|
+
}
|
|
68
|
+
export class S3FileSystem {
|
|
69
|
+
capabilities;
|
|
70
|
+
readStream;
|
|
71
|
+
writeStream;
|
|
72
|
+
transport;
|
|
73
|
+
bucket;
|
|
74
|
+
prefix;
|
|
75
|
+
allowRename;
|
|
76
|
+
pageSize;
|
|
77
|
+
maxReadBytes;
|
|
78
|
+
maxStreamBytes;
|
|
79
|
+
maxListEntries;
|
|
80
|
+
constructor(options) {
|
|
81
|
+
if (!options?.transport || ["headObject", "getObject", "putObject", "deleteObject", "copyObject", "listObjectsV2"]
|
|
82
|
+
.some((method) => typeof options.transport[method] !== "function")) {
|
|
83
|
+
throw new FsError("EINVAL", { message: "an explicit six-operation S3 transport is required; no default credentials or network client are created" });
|
|
84
|
+
}
|
|
85
|
+
if (options.compareEntry !== undefined && typeof options.compareEntry !== "function") {
|
|
86
|
+
throw new FsError("EINVAL", { message: "compareEntry must be a filesystem comparison callback" });
|
|
87
|
+
}
|
|
88
|
+
if (typeof options.bucket !== "string" || !options.bucket || /[/:\0]/.test(options.bucket)) {
|
|
89
|
+
throw new FsError("EINVAL", { message: "bucket must be a nonempty bucket name, not a URL or ARN" });
|
|
90
|
+
}
|
|
91
|
+
const prefix = options.prefix ?? "";
|
|
92
|
+
if (typeof prefix !== "string" || prefix.includes("\0") || !isWellFormed(prefix)
|
|
93
|
+
|| (prefix !== "" && prefix.replace(/\/$/, "").split("/").some((part) => !part || part === "." || part === ".."))) {
|
|
94
|
+
throw new FsError("EINVAL", { message: "prefix must be a canonical relative object-key prefix" });
|
|
95
|
+
}
|
|
96
|
+
this.prefix = prefix === "" || prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
97
|
+
if (Buffer.byteLength(this.prefix) > 1024)
|
|
98
|
+
throw new FsError("ENAMETOOLONG", { message: "S3 prefix exceeds 1024 UTF-8 bytes" });
|
|
99
|
+
this.transport = options.transport;
|
|
100
|
+
this.bucket = options.bucket;
|
|
101
|
+
this.allowRename = options.allowNonAtomicRename ?? true;
|
|
102
|
+
this.pageSize = validateLimit(options.pageSize ?? 1000, "pageSize", 1, 1000);
|
|
103
|
+
this.maxReadBytes = validateLimit(options.maxReadBytes ?? 64 * 1024 * 1024, "maxReadBytes", 0);
|
|
104
|
+
this.maxStreamBytes = validateLimit(options.maxStreamBytes ?? 5_000_000_000, "maxStreamBytes", 0, 5_000_000_000);
|
|
105
|
+
this.maxListEntries = validateLimit(options.maxListEntries ?? 100_000, "maxListEntries", 1);
|
|
106
|
+
this.capabilities = Object.freeze({
|
|
107
|
+
readOnly: options.readOnly ?? false,
|
|
108
|
+
symlinks: false, hardlinks: false, permissions: false,
|
|
109
|
+
timestamps: options.transport.capabilities?.conditionalCopy === true || options.transport.capabilities?.conditionalPut === true,
|
|
110
|
+
atomicRename: false, snapshotRmdir: true,
|
|
111
|
+
streamingRead: options.transport.capabilities?.streamingRead === true && typeof options.transport.getObjectStream === "function",
|
|
112
|
+
streamingWrite: options.transport.capabilities?.streamingWrite === true && typeof options.transport.putObjectStream === "function",
|
|
113
|
+
});
|
|
114
|
+
if (this.capabilities.streamingRead)
|
|
115
|
+
this.readStream = this.streamRead.bind(this);
|
|
116
|
+
if (this.capabilities.streamingWrite)
|
|
117
|
+
this.writeStream = this.streamWrite.bind(this);
|
|
118
|
+
const transport = this.transport;
|
|
119
|
+
const bucket = this.bucket;
|
|
120
|
+
const registeredPrefix = this.prefix;
|
|
121
|
+
registerS3EntryOwner(this, path => this.path(path), () => this.transport === transport
|
|
122
|
+
&& this.bucket === bucket && this.prefix === registeredPrefix, s3Comparison, options.compareEntry);
|
|
123
|
+
registerEntryAuthority(this, compareOwnedS3Entries);
|
|
124
|
+
}
|
|
125
|
+
async compareEntry(path, peer, peerPath, options = {}) {
|
|
126
|
+
return compareEntries(this, path, peer, peerPath, options);
|
|
127
|
+
}
|
|
128
|
+
path(input) {
|
|
129
|
+
if (typeof input !== "string" || input.includes("\0") || !isWellFormed(input))
|
|
130
|
+
fail("EINVAL", "resolve", input, "invalid path string");
|
|
131
|
+
const components = [];
|
|
132
|
+
for (const part of input.split("/")) {
|
|
133
|
+
if (!part || part === ".")
|
|
134
|
+
continue;
|
|
135
|
+
if (part === "..") {
|
|
136
|
+
if (components.length === 0)
|
|
137
|
+
fail("EACCES", "resolve", input, "path attempts to escape the configured prefix");
|
|
138
|
+
components.pop();
|
|
139
|
+
}
|
|
140
|
+
else
|
|
141
|
+
components.push(part);
|
|
142
|
+
}
|
|
143
|
+
const path = `/${components.join("/")}`;
|
|
144
|
+
if (Buffer.byteLength(this.key(path)) > 1024)
|
|
145
|
+
fail("ENAMETOOLONG", "resolve", input);
|
|
146
|
+
return path;
|
|
147
|
+
}
|
|
148
|
+
key(path) {
|
|
149
|
+
return this.prefix + path.slice(1);
|
|
150
|
+
}
|
|
151
|
+
directoryKey(path) {
|
|
152
|
+
const key = path === "/" ? this.prefix : `${this.key(path)}/`;
|
|
153
|
+
if (Buffer.byteLength(key) > 1024)
|
|
154
|
+
fail("ENAMETOOLONG", "resolve", path);
|
|
155
|
+
return key;
|
|
156
|
+
}
|
|
157
|
+
checkAbort(options, syscall, path) {
|
|
158
|
+
if (options.signal?.aborted)
|
|
159
|
+
fail("ECANCELED", syscall, path);
|
|
160
|
+
}
|
|
161
|
+
requestOptions(options) {
|
|
162
|
+
return options.signal === undefined ? {} : { abortSignal: options.signal };
|
|
163
|
+
}
|
|
164
|
+
async call(syscall, path, options, action, precondition) {
|
|
165
|
+
this.checkAbort(options, syscall, path);
|
|
166
|
+
let onAbort;
|
|
167
|
+
try {
|
|
168
|
+
const pending = action();
|
|
169
|
+
const result = options.signal ? await new Promise((resolve, reject) => {
|
|
170
|
+
const signal = options.signal;
|
|
171
|
+
onAbort = () => reject(new FsError("ECANCELED", { syscall, path, cause: signal.reason }));
|
|
172
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
173
|
+
if (signal.aborted)
|
|
174
|
+
onAbort();
|
|
175
|
+
pending.then(value => {
|
|
176
|
+
if (signal.aborted && typeof value === "object" && value !== null && "Body" in value)
|
|
177
|
+
this.dispose(value.Body);
|
|
178
|
+
resolve(value);
|
|
179
|
+
}, reject);
|
|
180
|
+
}) : await pending;
|
|
181
|
+
this.checkAbort(options, syscall, path);
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (options.signal?.aborted)
|
|
186
|
+
fail("ECANCELED", syscall, path);
|
|
187
|
+
throw translate(error, syscall, path, precondition);
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
if (onAbort)
|
|
191
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
writable(path, mode) {
|
|
195
|
+
if (this.capabilities.readOnly)
|
|
196
|
+
fail("EROFS", "write", path);
|
|
197
|
+
if (mode !== undefined)
|
|
198
|
+
validateLimit(mode, "mode", 0, 0o7777);
|
|
199
|
+
}
|
|
200
|
+
unsupported(operation, path) {
|
|
201
|
+
return fail("ENOTSUP", operation, path, `S3 does not support ${operation} with the configured transport`);
|
|
202
|
+
}
|
|
203
|
+
async head(key, path, options) {
|
|
204
|
+
try {
|
|
205
|
+
const input = { Bucket: this.bucket, Key: key };
|
|
206
|
+
return await this.call("headObject", path, options, () => queryS3Head(input, () => this.transport.headObject(input, this.requestOptions(options))));
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
if (isFsError(error, "ENOENT") && serviceCode(error.cause) !== "NoSuchBucket")
|
|
210
|
+
return undefined;
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
makeStat(type, metadata) {
|
|
215
|
+
const size = type === "directory" ? 0 : metadata?.ContentLength;
|
|
216
|
+
if (size === undefined || !Number.isSafeInteger(size) || size < 0)
|
|
217
|
+
fail("EIO", "stat", "", "transport omitted a valid object ContentLength");
|
|
218
|
+
const modified = metadata?.LastModified?.getTime() ?? 0;
|
|
219
|
+
if (!Number.isFinite(modified))
|
|
220
|
+
fail("EIO", "stat", "", "transport returned an invalid LastModified");
|
|
221
|
+
const timestamp = (key, fallback) => {
|
|
222
|
+
const value = metadata?.Metadata?.[key];
|
|
223
|
+
if (value === undefined)
|
|
224
|
+
return fallback;
|
|
225
|
+
const parsed = Number(value);
|
|
226
|
+
if (!value || !Number.isFinite(parsed))
|
|
227
|
+
fail("EIO", "stat", "", "invalid virtual timestamp metadata");
|
|
228
|
+
return parsed;
|
|
229
|
+
};
|
|
230
|
+
const storedMode = metadata?.Metadata?.["virtual-bash-mode"];
|
|
231
|
+
const mode = storedMode === undefined ? (type === "directory" ? 0o755 : 0o644) : Number(storedMode);
|
|
232
|
+
if (!Number.isSafeInteger(mode) || mode < 0 || mode > 0o7777 || storedMode === "")
|
|
233
|
+
fail("EIO", "stat", "", "invalid virtual mode metadata");
|
|
234
|
+
return { type, size, mode: (type === "directory" ? 0o40000 : 0o100000) | mode,
|
|
235
|
+
mtimeMs: timestamp("virtual-bash-mtime", modified), atimeMs: timestamp("virtual-bash-atime", 0), ctimeMs: modified };
|
|
236
|
+
}
|
|
237
|
+
async page(prefix, path, options, delimiter, token, maxKeys = this.pageSize) {
|
|
238
|
+
const result = await this.call("listObjectsV2", path, options, () => this.transport.listObjectsV2({
|
|
239
|
+
Bucket: this.bucket, Prefix: prefix, MaxKeys: maxKeys,
|
|
240
|
+
...(delimiter === undefined ? {} : { Delimiter: delimiter }),
|
|
241
|
+
...(token === undefined ? {} : { ContinuationToken: token }),
|
|
242
|
+
}, this.requestOptions(options)));
|
|
243
|
+
for (const item of result.Contents ?? []) {
|
|
244
|
+
if (typeof item.Key !== "string" || !item.Key.startsWith(prefix))
|
|
245
|
+
fail("EIO", "listObjectsV2", path, "transport returned an object outside the requested prefix");
|
|
246
|
+
this.validateObject(item, path);
|
|
247
|
+
}
|
|
248
|
+
for (const item of result.CommonPrefixes ?? []) {
|
|
249
|
+
if (delimiter === undefined || typeof item.Prefix !== "string" || !item.Prefix.startsWith(prefix)
|
|
250
|
+
|| !item.Prefix.endsWith("/") || item.Prefix === prefix)
|
|
251
|
+
fail("EIO", "listObjectsV2", path, "invalid common prefix");
|
|
252
|
+
this.validateKey(item.Prefix, path);
|
|
253
|
+
}
|
|
254
|
+
if (result.IsTruncated !== undefined && typeof result.IsTruncated !== "boolean")
|
|
255
|
+
fail("EIO", "listObjectsV2", path, "invalid IsTruncated flag");
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
validateKey(key, path) {
|
|
259
|
+
if (!key.startsWith(this.prefix))
|
|
260
|
+
fail("EIO", "listObjectsV2", path, "key escapes the configured prefix");
|
|
261
|
+
const relative = key.slice(this.prefix.length).replace(/\/$/, "");
|
|
262
|
+
if (!isWellFormed(key) || key.includes("\0") || Buffer.byteLength(key) > 1024
|
|
263
|
+
|| (relative !== "" && relative.split("/").some((part) => !part || part === "." || part === ".."))) {
|
|
264
|
+
fail("ENOTSUP", "listObjectsV2", path, "object key cannot be represented as a canonical filesystem path");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
validateObject(object, path) {
|
|
268
|
+
const key = object.Key;
|
|
269
|
+
this.validateKey(key, path);
|
|
270
|
+
if (object.Size === undefined || !Number.isSafeInteger(object.Size) || object.Size < 0)
|
|
271
|
+
fail("EIO", "listObjectsV2", path, "invalid object size");
|
|
272
|
+
if (key.endsWith("/") && object.Size !== 0)
|
|
273
|
+
fail("ENOTSUP", "listObjectsV2", path, "nonempty slash-suffixed objects cannot be treated as directories");
|
|
274
|
+
}
|
|
275
|
+
async *pages(prefix, path, options, delimiter, maxKeys = this.pageSize) {
|
|
276
|
+
const tokens = new Set();
|
|
277
|
+
let token;
|
|
278
|
+
let entries = 0;
|
|
279
|
+
do {
|
|
280
|
+
const page = await this.page(prefix, path, options, delimiter, token, maxKeys);
|
|
281
|
+
entries += (page.Contents?.length ?? 0) + (page.CommonPrefixes?.length ?? 0);
|
|
282
|
+
if (entries > this.maxListEntries || tokens.size >= this.maxListEntries)
|
|
283
|
+
fail("EFBIG", "listObjectsV2", path, "listing exceeds maxListEntries");
|
|
284
|
+
yield page;
|
|
285
|
+
if (!page.IsTruncated)
|
|
286
|
+
return;
|
|
287
|
+
token = page.NextContinuationToken;
|
|
288
|
+
if (typeof token !== "string" || !token || tokens.has(token))
|
|
289
|
+
fail("EIO", "listObjectsV2", path, "missing or repeated continuation token");
|
|
290
|
+
tokens.add(token);
|
|
291
|
+
} while (true);
|
|
292
|
+
}
|
|
293
|
+
async inspect(path, options) {
|
|
294
|
+
if (path === "/") {
|
|
295
|
+
const listing = await this.page(this.prefix, path, options, undefined, undefined, 1);
|
|
296
|
+
const marker = this.prefix && listing.Contents?.some(object => object.Key === this.prefix)
|
|
297
|
+
? await this.head(this.prefix, path, options) : undefined;
|
|
298
|
+
if (marker && marker.ContentLength !== 0)
|
|
299
|
+
this.unsupported("nonempty directory markers", path);
|
|
300
|
+
return { stat: this.makeStat("directory", marker), ...(marker ? { metadata: marker } : {}) };
|
|
301
|
+
}
|
|
302
|
+
const file = await this.head(this.key(path), path, options);
|
|
303
|
+
if (Buffer.byteLength(this.key(path)) === 1024) {
|
|
304
|
+
return file ? { stat: this.makeStat("file", file), metadata: file } : undefined;
|
|
305
|
+
}
|
|
306
|
+
const directoryKey = this.directoryKey(path);
|
|
307
|
+
const marker = await this.head(directoryKey, path, options);
|
|
308
|
+
if (marker && marker.ContentLength !== 0)
|
|
309
|
+
this.unsupported("nonempty directory markers", path);
|
|
310
|
+
let directory = marker !== undefined;
|
|
311
|
+
if (!directory) {
|
|
312
|
+
for await (const children of this.pages(directoryKey, path, options, undefined, 1)) {
|
|
313
|
+
if ((children.Contents?.length ?? 0) > 0) {
|
|
314
|
+
directory = true;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (file && directory)
|
|
320
|
+
this.unsupported("file/prefix collisions", path);
|
|
321
|
+
if (file)
|
|
322
|
+
return { stat: this.makeStat("file", file), metadata: file };
|
|
323
|
+
if (directory)
|
|
324
|
+
return { stat: this.makeStat("directory", marker), ...(marker ? { metadata: marker } : {}) };
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
async lookup(path, options) {
|
|
328
|
+
const ancestors = [];
|
|
329
|
+
for (let parent = posix.dirname(path); path !== "/"; parent = posix.dirname(parent)) {
|
|
330
|
+
ancestors.unshift(parent);
|
|
331
|
+
if (parent === "/")
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
for (const parent of ancestors) {
|
|
335
|
+
const ancestor = await this.inspect(parent, options);
|
|
336
|
+
if (!ancestor)
|
|
337
|
+
fail("ENOENT", "stat", parent);
|
|
338
|
+
if (ancestor.stat.type !== "directory")
|
|
339
|
+
fail("ENOTDIR", "stat", parent);
|
|
340
|
+
}
|
|
341
|
+
return this.inspect(path, options);
|
|
342
|
+
}
|
|
343
|
+
requireDirectorySuffix(input, info) {
|
|
344
|
+
if (input.endsWith("/") && info?.stat.type === "file")
|
|
345
|
+
fail("ENOTDIR", "stat", input);
|
|
346
|
+
}
|
|
347
|
+
async stat(input, options = {}) {
|
|
348
|
+
const path = this.path(input);
|
|
349
|
+
const info = await this.lookup(path, options);
|
|
350
|
+
this.requireDirectorySuffix(input, info);
|
|
351
|
+
if (!info)
|
|
352
|
+
fail("ENOENT", "stat", input);
|
|
353
|
+
recordS3Stat(this, path, info.stat, info.metadata);
|
|
354
|
+
return info.stat;
|
|
355
|
+
}
|
|
356
|
+
async lstat(path, options = {}) {
|
|
357
|
+
return this.stat(path, options);
|
|
358
|
+
}
|
|
359
|
+
async body(output, path, options) {
|
|
360
|
+
const limit = Math.min(this.maxReadBytes, validateLimit(options.maxBytes ?? this.maxReadBytes, "maxBytes", 0));
|
|
361
|
+
return this.call("readFile", path, options, async () => {
|
|
362
|
+
if (output.ContentLength !== undefined && output.ContentLength > limit)
|
|
363
|
+
fail("EFBIG", "readFile", path);
|
|
364
|
+
const body = output.Body;
|
|
365
|
+
if (!body)
|
|
366
|
+
fail("EIO", "readFile", path, "transport omitted the response body");
|
|
367
|
+
let bytes;
|
|
368
|
+
if (body instanceof Uint8Array) {
|
|
369
|
+
if (body.byteLength > limit)
|
|
370
|
+
fail("EFBIG", "readFile", path);
|
|
371
|
+
bytes = new Uint8Array(body);
|
|
372
|
+
}
|
|
373
|
+
else if (Symbol.asyncIterator in body) {
|
|
374
|
+
bytes = await collectBytes(body, { maxBytes: limit, ...(options.signal ? { signal: options.signal } : {}) });
|
|
375
|
+
}
|
|
376
|
+
else if ("transformToByteArray" in body) {
|
|
377
|
+
const converted = await body.transformToByteArray();
|
|
378
|
+
if (!(converted instanceof Uint8Array))
|
|
379
|
+
fail("EIO", "readFile", path, "transport body is not binary");
|
|
380
|
+
if (converted.byteLength > limit)
|
|
381
|
+
fail("EFBIG", "readFile", path);
|
|
382
|
+
bytes = new Uint8Array(converted);
|
|
383
|
+
}
|
|
384
|
+
else
|
|
385
|
+
fail("EIO", "readFile", path, "unsupported response body");
|
|
386
|
+
this.checkAbort(options, "readFile", path);
|
|
387
|
+
if (output.ContentLength !== undefined && output.ContentLength !== bytes.byteLength)
|
|
388
|
+
fail("EIO", "readFile", path, "response body length does not match ContentLength");
|
|
389
|
+
return bytes;
|
|
390
|
+
}).catch((error) => {
|
|
391
|
+
this.dispose(output.Body);
|
|
392
|
+
throw error;
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
async get(path, options) {
|
|
396
|
+
return this.call("getObject", path, options, () => this.transport.getObject({ Bucket: this.bucket, Key: this.key(path) }, this.requestOptions(options)));
|
|
397
|
+
}
|
|
398
|
+
async readFile(input, options = {}) {
|
|
399
|
+
const path = this.path(input);
|
|
400
|
+
validateLimit(options.maxBytes ?? this.maxReadBytes, "maxBytes", 0);
|
|
401
|
+
const info = await this.stat(input, options);
|
|
402
|
+
if (info.type === "directory")
|
|
403
|
+
fail("EISDIR", "readFile", path);
|
|
404
|
+
if (info.size > Math.min(this.maxReadBytes, options.maxBytes ?? this.maxReadBytes))
|
|
405
|
+
fail("EFBIG", "readFile", path);
|
|
406
|
+
return this.body(await this.get(path, options), path, options);
|
|
407
|
+
}
|
|
408
|
+
etag(metadata, path) {
|
|
409
|
+
if (!metadata?.ETag)
|
|
410
|
+
this.unsupported("conditional mutation without an ETag", path);
|
|
411
|
+
return metadata.ETag;
|
|
412
|
+
}
|
|
413
|
+
async writeFile(input, data, options = {}) {
|
|
414
|
+
const path = this.path(input);
|
|
415
|
+
this.writable(path, options.mode);
|
|
416
|
+
if (!(data instanceof Uint8Array))
|
|
417
|
+
fail("EINVAL", "writeFile", path, "data must be Uint8Array");
|
|
418
|
+
const bytes = new Uint8Array(data);
|
|
419
|
+
if (bytes.byteLength > 5_000_000_000)
|
|
420
|
+
fail("EFBIG", "writeFile", path, "multipart upload is not supported");
|
|
421
|
+
const flag = options.flag ?? "w";
|
|
422
|
+
if (!["w", "wx", "a", "ax"].includes(flag))
|
|
423
|
+
fail("EINVAL", "writeFile", path, "invalid write flag");
|
|
424
|
+
if (flag !== "w" && !this.transport.capabilities?.conditionalPut)
|
|
425
|
+
this.unsupported("conditional writes", path);
|
|
426
|
+
const info = await this.lookup(path, options);
|
|
427
|
+
this.requireDirectorySuffix(input, info);
|
|
428
|
+
if (info?.stat.type === "directory")
|
|
429
|
+
fail("EISDIR", "writeFile", path);
|
|
430
|
+
if (input.endsWith("/"))
|
|
431
|
+
fail("EISDIR", "writeFile", input);
|
|
432
|
+
const exclusive = flag === "wx" || flag === "ax";
|
|
433
|
+
if (exclusive && info)
|
|
434
|
+
fail("EEXIST", "writeFile", path);
|
|
435
|
+
let body = bytes;
|
|
436
|
+
let match;
|
|
437
|
+
let metadata = this.writeMetadata(info?.metadata, options.mode);
|
|
438
|
+
if (flag === "a" && info) {
|
|
439
|
+
const current = await this.get(path, options);
|
|
440
|
+
const previous = await this.body(current, path, options);
|
|
441
|
+
match = this.etag(current, path);
|
|
442
|
+
metadata = { ...current.Metadata };
|
|
443
|
+
delete metadata["virtual-bash-mtime"];
|
|
444
|
+
if (previous.length + bytes.length > this.maxReadBytes)
|
|
445
|
+
fail("EFBIG", "appendFile", path);
|
|
446
|
+
body = new Uint8Array(previous.length + bytes.length);
|
|
447
|
+
body.set(previous);
|
|
448
|
+
body.set(bytes, previous.length);
|
|
449
|
+
}
|
|
450
|
+
await this.call("putObject", path, options, () => this.transport.putObject({
|
|
451
|
+
Bucket: this.bucket, Key: this.key(path), Body: body,
|
|
452
|
+
...(match === undefined ? {} : { IfMatch: match }),
|
|
453
|
+
...(exclusive || (flag === "a" && !info) ? { IfNoneMatch: "*" } : {}),
|
|
454
|
+
...(metadata === undefined ? {} : { Metadata: metadata }),
|
|
455
|
+
}, this.requestOptions(options)), exclusive ? "EEXIST" : "EAGAIN");
|
|
456
|
+
}
|
|
457
|
+
writeMetadata(previous, mode) {
|
|
458
|
+
const metadata = { ...previous?.Metadata };
|
|
459
|
+
delete metadata["virtual-bash-mtime"];
|
|
460
|
+
if (previous === undefined && mode !== undefined)
|
|
461
|
+
metadata["virtual-bash-mode"] = String(mode);
|
|
462
|
+
return Object.keys(metadata).length === 0 ? undefined : metadata;
|
|
463
|
+
}
|
|
464
|
+
async appendFile(path, data, options = {}) {
|
|
465
|
+
return this.writeFile(path, data, { ...options, flag: "a" });
|
|
466
|
+
}
|
|
467
|
+
async mkdir(input, options = {}) {
|
|
468
|
+
const path = this.path(input);
|
|
469
|
+
this.writable(path, options.mode);
|
|
470
|
+
if (options.recursive && path !== "/") {
|
|
471
|
+
await this.mkdir(posix.dirname(path), { ...options, recursive: true });
|
|
472
|
+
}
|
|
473
|
+
const info = await this.lookup(path, options);
|
|
474
|
+
if (info) {
|
|
475
|
+
if (options.recursive && info.stat.type === "directory")
|
|
476
|
+
return;
|
|
477
|
+
fail("EEXIST", "mkdir", path);
|
|
478
|
+
}
|
|
479
|
+
await this.call("putObject", path, options, () => this.transport.putObject({
|
|
480
|
+
Bucket: this.bucket, Key: this.directoryKey(path), Body: new Uint8Array(),
|
|
481
|
+
...(options.mode === undefined ? {} : { Metadata: { "virtual-bash-mode": String(options.mode) } }),
|
|
482
|
+
...(this.transport.capabilities?.conditionalPut ? { IfNoneMatch: "*" } : {}),
|
|
483
|
+
}, this.requestOptions(options)), "EEXIST");
|
|
484
|
+
}
|
|
485
|
+
async readdir(input, options = {}) {
|
|
486
|
+
const path = this.path(input);
|
|
487
|
+
if ((await this.stat(input, options)).type !== "directory")
|
|
488
|
+
fail("ENOTDIR", "readdir", path);
|
|
489
|
+
const prefix = this.directoryKey(path);
|
|
490
|
+
const entries = new Map();
|
|
491
|
+
const add = (name, type) => {
|
|
492
|
+
if (!name || name.includes("/"))
|
|
493
|
+
fail("EIO", "readdir", path, "invalid direct child in delimited listing");
|
|
494
|
+
const existing = entries.get(name);
|
|
495
|
+
if (existing && existing.type !== type)
|
|
496
|
+
this.unsupported("file/prefix collisions", path);
|
|
497
|
+
entries.set(name, { name, type });
|
|
498
|
+
};
|
|
499
|
+
for await (const page of this.pages(prefix, path, options, "/")) {
|
|
500
|
+
for (const item of page.Contents ?? []) {
|
|
501
|
+
if (item.Key === prefix)
|
|
502
|
+
continue;
|
|
503
|
+
add(item.Key.slice(prefix.length), "file");
|
|
504
|
+
}
|
|
505
|
+
for (const item of page.CommonPrefixes ?? [])
|
|
506
|
+
add(item.Prefix.slice(prefix.length, -1), "directory");
|
|
507
|
+
}
|
|
508
|
+
return [...entries.values()].sort((left, right) => Buffer.compare(Buffer.from(left.name), Buffer.from(right.name)));
|
|
509
|
+
}
|
|
510
|
+
async tree(path, options) {
|
|
511
|
+
const objects = new Map();
|
|
512
|
+
for await (const page of this.pages(this.directoryKey(path), path, options)) {
|
|
513
|
+
for (const item of page.Contents ?? [])
|
|
514
|
+
objects.set(item.Key, item);
|
|
515
|
+
}
|
|
516
|
+
for (const key of objects.keys()) {
|
|
517
|
+
const withoutSlash = key.replace(/\/$/, "");
|
|
518
|
+
if (key.endsWith("/") && objects.has(withoutSlash))
|
|
519
|
+
this.unsupported("file/prefix collisions", path);
|
|
520
|
+
let parent = posix.dirname(withoutSlash);
|
|
521
|
+
while (parent !== "." && parent !== "/") {
|
|
522
|
+
if (objects.has(parent))
|
|
523
|
+
this.unsupported("file/prefix collisions", path);
|
|
524
|
+
parent = posix.dirname(parent);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return [...objects.values()];
|
|
528
|
+
}
|
|
529
|
+
async rmdir(input, options = {}) {
|
|
530
|
+
try {
|
|
531
|
+
const path = this.path(input);
|
|
532
|
+
this.checkAbort(options, "rmdir", input);
|
|
533
|
+
this.writable(path);
|
|
534
|
+
if (path === "/")
|
|
535
|
+
fail("EBUSY", "rmdir", input, "cannot remove the mounted root");
|
|
536
|
+
const info = await this.lookup(path, options);
|
|
537
|
+
this.checkAbort(options, "rmdir", input);
|
|
538
|
+
if (!info)
|
|
539
|
+
fail("ENOENT", "rmdir", input);
|
|
540
|
+
if (info.stat.type !== "directory")
|
|
541
|
+
fail("ENOTDIR", "rmdir", input);
|
|
542
|
+
const prefix = this.directoryKey(path);
|
|
543
|
+
let markerObserved = false;
|
|
544
|
+
for await (const page of this.pages(prefix, path, options, "/", Math.max(2, this.pageSize))) {
|
|
545
|
+
this.checkAbort(options, "rmdir", input);
|
|
546
|
+
if (page.Contents?.some(item => item.Key !== prefix) || page.CommonPrefixes?.length) {
|
|
547
|
+
fail("ENOTEMPTY", "rmdir", input);
|
|
548
|
+
}
|
|
549
|
+
if (page.IsTruncated === undefined)
|
|
550
|
+
fail("EIO", "rmdir", input, "listing did not confirm completeness");
|
|
551
|
+
if (page.Contents?.some(item => item.Key === prefix))
|
|
552
|
+
markerObserved = true;
|
|
553
|
+
}
|
|
554
|
+
this.checkAbort(options, "rmdir", input);
|
|
555
|
+
if (!info.metadata)
|
|
556
|
+
fail("ENOTSUP", "rmdir", input, "snapshot removal requires an explicit directory marker");
|
|
557
|
+
if (!markerObserved)
|
|
558
|
+
fail("ENOENT", "rmdir", input, "directory marker disappeared during inspection");
|
|
559
|
+
await this.call("deleteObject", path, options, () => this.transport.deleteObject({ Bucket: this.bucket, Key: prefix }, this.requestOptions(options)));
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
if (isFsError(error) && (error.syscall !== "rmdir" || error.path !== input)) {
|
|
563
|
+
throw new FsError(error.code, { syscall: "rmdir", path: input, cause: error });
|
|
564
|
+
}
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async rm(input, options = {}) {
|
|
569
|
+
const path = this.path(input);
|
|
570
|
+
this.writable(path);
|
|
571
|
+
if (path === "/")
|
|
572
|
+
fail("EBUSY", "rm", path, "cannot remove the mounted root");
|
|
573
|
+
let info;
|
|
574
|
+
try {
|
|
575
|
+
info = await this.lookup(path, options);
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
if (options.force && isFsError(error, "ENOENT") && serviceCode(error.cause) !== "NoSuchBucket")
|
|
579
|
+
return;
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
582
|
+
this.requireDirectorySuffix(input, info);
|
|
583
|
+
if (!info) {
|
|
584
|
+
if (options.force)
|
|
585
|
+
return;
|
|
586
|
+
fail("ENOENT", "rm", path);
|
|
587
|
+
}
|
|
588
|
+
const objects = info.stat.type === "directory" ? await this.tree(path, options) : [{ Key: this.key(path) }];
|
|
589
|
+
if (info.stat.type === "directory" && !options.recursive && objects.some((item) => item.Key !== this.directoryKey(path)))
|
|
590
|
+
fail("ENOTEMPTY", "rm", path);
|
|
591
|
+
for (const item of objects) {
|
|
592
|
+
await this.call("deleteObject", path, options, () => this.transport.deleteObject({ Bucket: this.bucket, Key: item.Key }, this.requestOptions(options)));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async copy(sourceKey, destinationKey, source, metadata, options, destination) {
|
|
596
|
+
if (metadata.ContentLength === undefined || metadata.ContentLength > 5_000_000_000)
|
|
597
|
+
this.unsupported("multipart copy", source);
|
|
598
|
+
const etag = this.etag(metadata, source);
|
|
599
|
+
const result = await this.call("copyObject", source, options, () => this.transport.copyObject({
|
|
600
|
+
Bucket: this.bucket, Key: destinationKey, CopySource: encodeCopySource(this.bucket, sourceKey),
|
|
601
|
+
CopySourceIfMatch: etag, MetadataDirective: "COPY",
|
|
602
|
+
...(options.exclusive ? { IfNoneMatch: "*" } : {}),
|
|
603
|
+
...(destination === undefined ? {} : destination === null ? { IfNoneMatch: "*" } : { IfMatch: this.etag(destination, source) }),
|
|
604
|
+
}, this.requestOptions(options)));
|
|
605
|
+
if (!result.CopyObjectResult?.ETag)
|
|
606
|
+
fail("EIO", "copyObject", source, "transport did not confirm a completed CopyObject result");
|
|
607
|
+
}
|
|
608
|
+
async copyFile(sourceInput, destinationInput, options = {}) {
|
|
609
|
+
const source = this.path(sourceInput);
|
|
610
|
+
const destination = this.path(destinationInput);
|
|
611
|
+
this.writable(destination);
|
|
612
|
+
if (options.exclusive && !this.transport.capabilities?.conditionalCopy)
|
|
613
|
+
this.unsupported("exclusive server-side copy", destination);
|
|
614
|
+
const origin = await this.lookup(source, options);
|
|
615
|
+
this.requireDirectorySuffix(sourceInput, origin);
|
|
616
|
+
if (!origin)
|
|
617
|
+
fail("ENOENT", "copyFile", source);
|
|
618
|
+
if (origin.stat.type === "directory")
|
|
619
|
+
fail("EISDIR", "copyFile", source);
|
|
620
|
+
const target = await this.lookup(destination, options);
|
|
621
|
+
this.requireDirectorySuffix(destinationInput, target);
|
|
622
|
+
if (target?.stat.type === "directory" || destinationInput.endsWith("/"))
|
|
623
|
+
fail("EISDIR", "copyFile", destination);
|
|
624
|
+
if (target && options.exclusive)
|
|
625
|
+
fail("EEXIST", "copyFile", destination);
|
|
626
|
+
if (source === destination)
|
|
627
|
+
return;
|
|
628
|
+
try {
|
|
629
|
+
await this.copy(this.key(source), this.key(destination), source, origin.metadata, options);
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
if (!options.exclusive || !isFsError(error, "EAGAIN"))
|
|
633
|
+
throw error;
|
|
634
|
+
const status = error.cause?.$metadata?.httpStatusCode;
|
|
635
|
+
if (serviceCode(error.cause) !== "PreconditionFailed" && status !== 412)
|
|
636
|
+
throw error;
|
|
637
|
+
let destinationExists = false;
|
|
638
|
+
try {
|
|
639
|
+
const currentSource = await this.head(this.key(source), source, options);
|
|
640
|
+
if (currentSource?.ETag === origin.metadata?.ETag) {
|
|
641
|
+
destinationExists = await this.head(this.key(destination), destination, options) !== undefined;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
catch (diagnosticError) {
|
|
645
|
+
if (isFsError(diagnosticError, "ECANCELED"))
|
|
646
|
+
throw diagnosticError;
|
|
647
|
+
}
|
|
648
|
+
if (destinationExists) {
|
|
649
|
+
throw new FsError("EEXIST", { syscall: "copyFile", path: source, dest: destination, cause: error });
|
|
650
|
+
}
|
|
651
|
+
throw error;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async rename(sourceInput, destinationInput, options = {}) {
|
|
655
|
+
const source = this.path(sourceInput);
|
|
656
|
+
const destination = this.path(destinationInput);
|
|
657
|
+
this.writable(destination);
|
|
658
|
+
if (!this.allowRename)
|
|
659
|
+
this.unsupported("atomic rename; explicitly enable allowNonAtomicRename for copy/delete semantics", source);
|
|
660
|
+
if (!this.transport.capabilities?.conditionalDelete)
|
|
661
|
+
this.unsupported("rename without conditional delete", source);
|
|
662
|
+
const conditionalCopy = this.transport.capabilities?.conditionalCopy === true;
|
|
663
|
+
if (!conditionalCopy && this.transport.capabilities?.conditionalPut !== true) {
|
|
664
|
+
this.unsupported("rename requires conditional destination copy or conditional PUT, plus conditional delete", source);
|
|
665
|
+
}
|
|
666
|
+
const streamFallback = this.capabilities.streamingRead && this.capabilities.streamingWrite;
|
|
667
|
+
if (source === "/" || destination === "/")
|
|
668
|
+
fail("EBUSY", "rename", source);
|
|
669
|
+
const origin = await this.lookup(source, options);
|
|
670
|
+
this.requireDirectorySuffix(sourceInput, origin);
|
|
671
|
+
if (!origin)
|
|
672
|
+
fail("ENOENT", "rename", source);
|
|
673
|
+
if (source === destination) {
|
|
674
|
+
this.requireDirectorySuffix(destinationInput, origin);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (destination.startsWith(`${source}/`))
|
|
678
|
+
fail("EINVAL", "rename", destination, "cannot move a path into itself");
|
|
679
|
+
const target = await this.lookup(destination, options);
|
|
680
|
+
this.requireDirectorySuffix(destinationInput, target);
|
|
681
|
+
if (destinationInput.endsWith("/") && origin.stat.type !== "directory")
|
|
682
|
+
fail("ENOTDIR", "rename", destination);
|
|
683
|
+
if (target && target.stat.type !== origin.stat.type)
|
|
684
|
+
fail(target.stat.type === "directory" ? "EISDIR" : "ENOTDIR", "rename", destination);
|
|
685
|
+
if (target?.stat.type === "directory" && (await this.readdir(destination, options)).length > 0)
|
|
686
|
+
fail("ENOTEMPTY", "rename", destination);
|
|
687
|
+
if (target?.metadata)
|
|
688
|
+
this.etag(target.metadata, destination);
|
|
689
|
+
const objects = origin.stat.type === "directory" ? await this.tree(source, options) : [{
|
|
690
|
+
Key: this.key(source), Size: origin.stat.size, ETag: origin.metadata?.ETag,
|
|
691
|
+
}];
|
|
692
|
+
for (const object of objects) {
|
|
693
|
+
this.etag({ ETag: object.ETag }, source);
|
|
694
|
+
if (object.Size === undefined || object.Size > 5_000_000_000)
|
|
695
|
+
this.unsupported("multipart copy", source);
|
|
696
|
+
if (!conditionalCopy && object.Size > (streamFallback ? this.maxStreamBytes : this.maxReadBytes)) {
|
|
697
|
+
fail("EFBIG", "rename", source, "conditional PUT fallback exceeds its configured transfer or buffered read limit");
|
|
698
|
+
}
|
|
699
|
+
if (Buffer.byteLength(this.key(destination) + object.Key.slice(this.key(source).length)) > 1024)
|
|
700
|
+
fail("ENAMETOOLONG", "rename", destination);
|
|
701
|
+
}
|
|
702
|
+
const copied = [];
|
|
703
|
+
const deleted = [];
|
|
704
|
+
let phase = "copy";
|
|
705
|
+
try {
|
|
706
|
+
for (const object of objects) {
|
|
707
|
+
const destinationKey = this.key(destination) + object.Key.slice(this.key(source).length);
|
|
708
|
+
const destinationMetadata = origin.stat.type === "file" || object.Key === this.directoryKey(source) ? target?.metadata ?? null : null;
|
|
709
|
+
const metadata = { ContentLength: object.Size, ETag: object.ETag };
|
|
710
|
+
if (conditionalCopy)
|
|
711
|
+
await this.copy(object.Key, destinationKey, source, metadata, options, destinationMetadata);
|
|
712
|
+
else
|
|
713
|
+
await this.copyWithPut(object.Key, destinationKey, source, metadata, destinationMetadata, options, streamFallback);
|
|
714
|
+
copied.push(destinationKey);
|
|
715
|
+
}
|
|
716
|
+
phase = "delete";
|
|
717
|
+
for (const object of objects) {
|
|
718
|
+
await this.call("deleteObject", source, options, () => this.transport.deleteObject({
|
|
719
|
+
Bucket: this.bucket, Key: object.Key, IfMatch: object.ETag,
|
|
720
|
+
}, this.requestOptions(options)));
|
|
721
|
+
deleted.push(object.Key);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
throw new S3RenameError(source, destination, phase, copied, deleted, translate(error, "rename", source));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async copyWithPut(sourceKey, destinationKey, source, expected, destination, options, streaming) {
|
|
729
|
+
const etag = this.etag(expected, source);
|
|
730
|
+
const condition = destination === null ? { IfNoneMatch: "*" } : { IfMatch: this.etag(destination, destinationKey) };
|
|
731
|
+
const controller = new AbortController();
|
|
732
|
+
const signalScope = options.signal ? composeAbortSignals([options.signal, controller.signal]) : undefined;
|
|
733
|
+
const signal = signalScope?.signal ?? controller.signal;
|
|
734
|
+
const operationOptions = { signal };
|
|
735
|
+
let output;
|
|
736
|
+
let upload;
|
|
737
|
+
let reading = false;
|
|
738
|
+
try {
|
|
739
|
+
output = await this.call("getObject", source, operationOptions, () => streaming
|
|
740
|
+
? this.transport.getObjectStream({ Bucket: this.bucket, Key: sourceKey, IfMatch: etag }, { abortSignal: signal })
|
|
741
|
+
: this.transport.getObject({ Bucket: this.bucket, Key: sourceKey }, { abortSignal: signal }));
|
|
742
|
+
if (this.etag(output, source) !== etag)
|
|
743
|
+
fail("EAGAIN", "rename", source, "source changed before conditional PUT copy");
|
|
744
|
+
if (output.ContentLength !== undefined && output.ContentLength !== expected.ContentLength) {
|
|
745
|
+
fail("EIO", "rename", source, "source response length differs from the enumerated object");
|
|
746
|
+
}
|
|
747
|
+
const input = { Bucket: this.bucket, Key: destinationKey, ...condition,
|
|
748
|
+
...(output.Metadata === undefined ? {} : { Metadata: { ...output.Metadata } }) };
|
|
749
|
+
if (!streaming) {
|
|
750
|
+
const body = await this.body(output, source, operationOptions);
|
|
751
|
+
if (body.length !== expected.ContentLength)
|
|
752
|
+
fail("EIO", "rename", source, "source body length differs from the enumerated object");
|
|
753
|
+
await this.call("putObject", source, operationOptions, () => this.transport.putObject({ ...input, Body: body }, { abortSignal: signal }));
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const body = output.Body;
|
|
757
|
+
if (!body || !(Symbol.asyncIterator in body))
|
|
758
|
+
fail("EIO", "rename", source, "streaming transport did not return an async binary body");
|
|
759
|
+
let finished = false;
|
|
760
|
+
upload = (async function* () {
|
|
761
|
+
let count = 0;
|
|
762
|
+
reading = true;
|
|
763
|
+
for await (const chunk of readBytes(body, signal)) {
|
|
764
|
+
if (chunk.length > expected.ContentLength - count)
|
|
765
|
+
fail("EIO", "rename", source, "source body exceeds its enumerated length");
|
|
766
|
+
count += chunk.length;
|
|
767
|
+
for (let offset = 0; offset < chunk.length; offset += 64 * 1024) {
|
|
768
|
+
signal.throwIfAborted();
|
|
769
|
+
yield new Uint8Array(chunk.subarray(offset, offset + 64 * 1024));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (count !== expected.ContentLength)
|
|
773
|
+
fail("EIO", "rename", source, "incomplete source body");
|
|
774
|
+
finished = true;
|
|
775
|
+
})();
|
|
776
|
+
await this.call("putObject", source, operationOptions, () => this.transport.putObjectStream({ ...input, Body: upload }, { abortSignal: signal }));
|
|
777
|
+
if (!finished)
|
|
778
|
+
fail("EIO", "rename", source, "transport completed without consuming the conditional copy body");
|
|
779
|
+
}
|
|
780
|
+
finally {
|
|
781
|
+
controller.abort();
|
|
782
|
+
signalScope?.dispose();
|
|
783
|
+
this.dispose(upload);
|
|
784
|
+
this.dispose(output?.Body, !reading);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
async realpath(input, options = {}) {
|
|
788
|
+
await this.stat(input, options);
|
|
789
|
+
return this.path(input);
|
|
790
|
+
}
|
|
791
|
+
async access(path, mode = 0, options = {}) {
|
|
792
|
+
if (!Number.isInteger(mode) || mode < 0 || mode > 7)
|
|
793
|
+
fail("EINVAL", "access", path);
|
|
794
|
+
const info = await this.stat(path, options);
|
|
795
|
+
if ((mode & 2) !== 0 && this.capabilities.readOnly)
|
|
796
|
+
fail("EROFS", "access", path);
|
|
797
|
+
if ((mode & 1) !== 0 && info.type !== "directory")
|
|
798
|
+
fail("EACCES", "access", path);
|
|
799
|
+
}
|
|
800
|
+
async readlink(path) { return this.unsupported("readlink", path); }
|
|
801
|
+
async symlink(_target, path) { this.unsupported("symlink", path); }
|
|
802
|
+
async link(_existingPath, path) { this.unsupported("link", path); }
|
|
803
|
+
async chmod(path, _mode) { this.unsupported("chmod", path); }
|
|
804
|
+
async utimes(input, atimeMs, mtimeMs, options = {}) {
|
|
805
|
+
const path = this.path(input);
|
|
806
|
+
this.writable(path);
|
|
807
|
+
if (!Number.isFinite(atimeMs) || !Number.isFinite(mtimeMs))
|
|
808
|
+
fail("EINVAL", "utimes", path);
|
|
809
|
+
const info = await this.lookup(path, options);
|
|
810
|
+
this.requireDirectorySuffix(input, info);
|
|
811
|
+
if (!info)
|
|
812
|
+
fail("ENOENT", "utimes", path);
|
|
813
|
+
if (!this.capabilities.timestamps)
|
|
814
|
+
this.unsupported("conditional metadata mutation", path);
|
|
815
|
+
const key = info.stat.type === "directory" ? this.directoryKey(path) : this.key(path);
|
|
816
|
+
if (!key)
|
|
817
|
+
this.unsupported("timestamps on an unprefixed bucket root", path);
|
|
818
|
+
const metadata = { ...info.metadata?.Metadata,
|
|
819
|
+
"virtual-bash-atime": String(atimeMs), "virtual-bash-mtime": String(mtimeMs) };
|
|
820
|
+
if (!info.metadata) {
|
|
821
|
+
if (!this.transport.capabilities?.conditionalPut)
|
|
822
|
+
this.unsupported("conditional directory marker creation", path);
|
|
823
|
+
await this.call("utimes", path, options, () => this.transport.putObject({
|
|
824
|
+
Bucket: this.bucket, Key: key, Body: new Uint8Array(), Metadata: metadata, IfNoneMatch: "*",
|
|
825
|
+
}, this.requestOptions(options)));
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (!this.transport.capabilities?.conditionalCopy) {
|
|
829
|
+
const current = await this.call("getObject", path, options, () => this.transport.getObject({
|
|
830
|
+
Bucket: this.bucket, Key: key,
|
|
831
|
+
}, this.requestOptions(options)));
|
|
832
|
+
const body = await this.body(current, path, options);
|
|
833
|
+
await this.call("utimes", path, options, () => this.transport.putObject({
|
|
834
|
+
Bucket: this.bucket, Key: key, Body: body, IfMatch: this.etag(current, path),
|
|
835
|
+
Metadata: { ...current.Metadata, "virtual-bash-atime": String(atimeMs), "virtual-bash-mtime": String(mtimeMs) },
|
|
836
|
+
}, this.requestOptions(options)));
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (info.stat.size > 5_000_000_000)
|
|
840
|
+
this.unsupported("multipart metadata copy", path);
|
|
841
|
+
const etag = this.etag(info.metadata, path);
|
|
842
|
+
const result = await this.call("utimes", path, options, () => this.transport.copyObject({
|
|
843
|
+
Bucket: this.bucket, Key: key, CopySource: encodeCopySource(this.bucket, key),
|
|
844
|
+
CopySourceIfMatch: etag, IfMatch: etag, MetadataDirective: "REPLACE", Metadata: metadata,
|
|
845
|
+
}, this.requestOptions(options)));
|
|
846
|
+
if (!result.CopyObjectResult?.ETag)
|
|
847
|
+
fail("EIO", "utimes", path, "transport did not confirm a completed metadata copy");
|
|
848
|
+
}
|
|
849
|
+
async truncate(input, length = 0, options = {}) {
|
|
850
|
+
const path = this.path(input);
|
|
851
|
+
this.writable(path);
|
|
852
|
+
validateLimit(length, "length", 0);
|
|
853
|
+
const info = await this.lookup(path, options);
|
|
854
|
+
this.requireDirectorySuffix(input, info);
|
|
855
|
+
if (!info)
|
|
856
|
+
fail("ENOENT", "truncate", path);
|
|
857
|
+
if (info.stat.type === "directory")
|
|
858
|
+
fail("EISDIR", "truncate", path);
|
|
859
|
+
if (!this.transport.capabilities?.conditionalPut)
|
|
860
|
+
this.unsupported("conditional truncate", path);
|
|
861
|
+
if (length > this.maxReadBytes)
|
|
862
|
+
fail("EFBIG", "truncate", path);
|
|
863
|
+
let current = info.metadata;
|
|
864
|
+
const body = new Uint8Array(length);
|
|
865
|
+
if (length !== 0 && info.stat.size !== 0) {
|
|
866
|
+
if (this.readStream) {
|
|
867
|
+
const previous = await this.call("truncate", path, options, () => collectBytes(this.readStream(input, { ...options, endExclusive: Math.min(length, info.stat.size) }), { ...options, maxBytes: length }));
|
|
868
|
+
body.set(previous);
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
const output = await this.get(path, options);
|
|
872
|
+
const previous = await this.body(output, path, options);
|
|
873
|
+
current = output;
|
|
874
|
+
body.set(previous.subarray(0, length));
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const metadata = { ...current.Metadata };
|
|
878
|
+
delete metadata["virtual-bash-mtime"];
|
|
879
|
+
await this.call("truncate", path, options, () => this.transport.putObject({
|
|
880
|
+
Bucket: this.bucket, Key: this.key(path), Body: body, IfMatch: this.etag(current, path), Metadata: metadata,
|
|
881
|
+
}, this.requestOptions(options)));
|
|
882
|
+
}
|
|
883
|
+
async *streamRead(input, options = {}) {
|
|
884
|
+
const path = this.path(input);
|
|
885
|
+
const start = validateLimit(options.start ?? 0, "start", 0);
|
|
886
|
+
const end = options.endExclusive === undefined ? undefined : validateLimit(options.endExclusive, "endExclusive", start);
|
|
887
|
+
const chunkSize = validateLimit(options.chunkSize ?? 64 * 1024, "chunkSize", 1);
|
|
888
|
+
const info = await this.lookup(path, options);
|
|
889
|
+
this.requireDirectorySuffix(input, info);
|
|
890
|
+
if (!info)
|
|
891
|
+
fail("ENOENT", "readStream", path);
|
|
892
|
+
if (info.stat.type === "directory")
|
|
893
|
+
fail("EISDIR", "readStream", path);
|
|
894
|
+
const stop = Math.min(end ?? info.stat.size, info.stat.size);
|
|
895
|
+
const expected = Math.max(0, stop - start);
|
|
896
|
+
if (expected > this.maxStreamBytes)
|
|
897
|
+
fail("EFBIG", "readStream", path);
|
|
898
|
+
if (expected === 0 && info.stat.size !== 0)
|
|
899
|
+
return;
|
|
900
|
+
let count = 0;
|
|
901
|
+
let finished = false;
|
|
902
|
+
const controller = new AbortController();
|
|
903
|
+
const signalScope = options.signal ? composeAbortSignals([options.signal, controller.signal]) : undefined;
|
|
904
|
+
const signal = signalScope?.signal ?? controller.signal;
|
|
905
|
+
let output;
|
|
906
|
+
let iterator;
|
|
907
|
+
try {
|
|
908
|
+
output = await this.call("getObject", path, options, () => this.transport.getObjectStream({
|
|
909
|
+
Bucket: this.bucket, Key: this.key(path), IfMatch: this.etag(info.metadata, path),
|
|
910
|
+
...(expected === 0 || (start === 0 && stop === info.stat.size) ? {} : { Range: `bytes=${start}-${stop - 1}` }),
|
|
911
|
+
}, { abortSignal: signal }));
|
|
912
|
+
if (output.ETag !== undefined && output.ETag !== info.metadata?.ETag)
|
|
913
|
+
fail("EIO", "readStream", path, "response ETag differs from requested snapshot");
|
|
914
|
+
if (output.ContentLength !== undefined && output.ContentLength !== expected)
|
|
915
|
+
fail("EIO", "readStream", path, "response length differs from requested snapshot");
|
|
916
|
+
iterator = readBytes(output.Body, signal)[Symbol.asyncIterator]();
|
|
917
|
+
while (true) {
|
|
918
|
+
const result = await iterator.next();
|
|
919
|
+
if (result.done)
|
|
920
|
+
break;
|
|
921
|
+
const chunk = result.value;
|
|
922
|
+
if (chunk.length > expected - count)
|
|
923
|
+
fail("EIO", "readStream", path, "response exceeds requested length");
|
|
924
|
+
count += chunk.length;
|
|
925
|
+
for (let offset = 0; offset < chunk.length; offset += chunkSize) {
|
|
926
|
+
this.checkAbort(options, "readStream", path);
|
|
927
|
+
yield new Uint8Array(chunk.subarray(offset, offset + chunkSize));
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (count !== expected)
|
|
931
|
+
fail("EIO", "readStream", path, "incomplete response body");
|
|
932
|
+
finished = true;
|
|
933
|
+
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
this.checkAbort(options, "readStream", path);
|
|
936
|
+
throw translate(error, "readStream", path);
|
|
937
|
+
}
|
|
938
|
+
finally {
|
|
939
|
+
controller.abort();
|
|
940
|
+
signalScope?.dispose();
|
|
941
|
+
if (!finished && output) {
|
|
942
|
+
this.dispose(iterator);
|
|
943
|
+
this.dispose(output.Body, iterator === undefined);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
dispose(body, returnIterator = true) {
|
|
948
|
+
const disposable = body;
|
|
949
|
+
try {
|
|
950
|
+
const result = typeof disposable?.destroy === "function" ? disposable.destroy()
|
|
951
|
+
: typeof disposable?.cancel === "function" ? disposable.cancel() : returnIterator ? disposable?.return?.() : undefined;
|
|
952
|
+
void Promise.resolve(result).catch(() => { });
|
|
953
|
+
}
|
|
954
|
+
catch (ignoredError) {
|
|
955
|
+
void ignoredError;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
async streamWrite(input, source, options = {}) {
|
|
959
|
+
const path = this.path(input);
|
|
960
|
+
this.writable(path, options.mode);
|
|
961
|
+
const flag = options.flag ?? "w";
|
|
962
|
+
if (!["w", "wx", "a", "ax"].includes(flag))
|
|
963
|
+
fail("EINVAL", "writeStream", path);
|
|
964
|
+
if (flag !== "w" && !this.transport.capabilities?.conditionalPut)
|
|
965
|
+
this.unsupported("conditional writes", path);
|
|
966
|
+
const info = await this.lookup(path, options);
|
|
967
|
+
this.requireDirectorySuffix(input, info);
|
|
968
|
+
if (info?.stat.type === "directory" || input.endsWith("/"))
|
|
969
|
+
fail("EISDIR", "writeStream", path);
|
|
970
|
+
const exclusive = flag === "wx" || flag === "ax";
|
|
971
|
+
if (exclusive && info)
|
|
972
|
+
fail("EEXIST", "writeStream", path);
|
|
973
|
+
if (flag === "a") {
|
|
974
|
+
const current = info ? await this.get(path, options) : undefined;
|
|
975
|
+
const previous = current ? await this.body(current, path, options) : new Uint8Array();
|
|
976
|
+
const limit = Math.min(this.maxReadBytes, this.maxStreamBytes);
|
|
977
|
+
if (previous.length > limit)
|
|
978
|
+
fail("EFBIG", "writeStream", path);
|
|
979
|
+
const bytes = await this.call("writeStream", path, options, () => collectBytes(source, { maxBytes: limit - previous.length, ...options }));
|
|
980
|
+
const body = new Uint8Array(previous.length + bytes.length);
|
|
981
|
+
body.set(previous);
|
|
982
|
+
body.set(bytes, previous.length);
|
|
983
|
+
const metadata = this.writeMetadata(current, options.mode);
|
|
984
|
+
await this.call("putObject", path, options, () => this.transport.putObject({
|
|
985
|
+
Bucket: this.bucket, Key: this.key(path), Body: body,
|
|
986
|
+
...(current ? { IfMatch: this.etag(current, path) } : { IfNoneMatch: "*" }),
|
|
987
|
+
...(metadata ? { Metadata: metadata } : {}),
|
|
988
|
+
}, this.requestOptions(options)));
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
const controller = new AbortController();
|
|
992
|
+
const signalScope = options.signal ? composeAbortSignals([options.signal, controller.signal]) : undefined;
|
|
993
|
+
const signal = signalScope?.signal ?? controller.signal;
|
|
994
|
+
let finished = false;
|
|
995
|
+
const body = (async function* () {
|
|
996
|
+
let total = 0;
|
|
997
|
+
for await (const chunk of readBytes(source, signal)) {
|
|
998
|
+
if (chunk.length > this.maxStreamBytes - total)
|
|
999
|
+
fail("EFBIG", "writeStream", path);
|
|
1000
|
+
total += chunk.length;
|
|
1001
|
+
for (let offset = 0; offset < chunk.length; offset += 64 * 1024) {
|
|
1002
|
+
this.checkAbort({ signal }, "writeStream", path);
|
|
1003
|
+
yield new Uint8Array(chunk.subarray(offset, offset + 64 * 1024));
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
finished = true;
|
|
1007
|
+
}).call(this);
|
|
1008
|
+
try {
|
|
1009
|
+
const metadata = this.writeMetadata(info?.metadata, options.mode);
|
|
1010
|
+
await this.call("putObject", path, options, () => this.transport.putObjectStream({
|
|
1011
|
+
Bucket: this.bucket, Key: this.key(path), Body: body, ...(exclusive ? { IfNoneMatch: "*" } : {}),
|
|
1012
|
+
...(metadata === undefined ? {} : { Metadata: metadata }),
|
|
1013
|
+
}, { abortSignal: signal }), exclusive ? "EEXIST" : "EAGAIN");
|
|
1014
|
+
if (!finished)
|
|
1015
|
+
fail("EIO", "writeStream", path, "transport completed without consuming the request body");
|
|
1016
|
+
}
|
|
1017
|
+
finally {
|
|
1018
|
+
controller.abort();
|
|
1019
|
+
signalScope?.dispose();
|
|
1020
|
+
this.dispose(body);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
const s3Comparison = S3FileSystem.prototype.compareEntry;
|