@valon-technologies/gestalt 0.0.1-alpha.1
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/README.md +160 -0
- package/gen/v1/auth_pb.ts +212 -0
- package/gen/v1/cache_pb.ts +357 -0
- package/gen/v1/datastore_pb.ts +922 -0
- package/gen/v1/plugin_pb.ts +772 -0
- package/gen/v1/runtime_pb.ts +216 -0
- package/gen/v1/s3_pb.ts +640 -0
- package/gen/v1/secrets_pb.ts +63 -0
- package/package.json +55 -0
- package/src/api.ts +98 -0
- package/src/auth.ts +103 -0
- package/src/build.ts +181 -0
- package/src/cache.ts +304 -0
- package/src/catalog.ts +188 -0
- package/src/index.ts +182 -0
- package/src/indexeddb.ts +740 -0
- package/src/plugin.ts +402 -0
- package/src/provider.ts +133 -0
- package/src/runtime.ts +871 -0
- package/src/s3.ts +1128 -0
- package/src/schema.ts +219 -0
- package/src/secrets.ts +36 -0
- package/src/target.ts +192 -0
- package/tsconfig.json +22 -0
package/src/s3.ts
ADDED
|
@@ -0,0 +1,1128 @@
|
|
|
1
|
+
import { connect } from "node:net";
|
|
2
|
+
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import { EmptySchema } from "@bufbuild/protobuf/wkt";
|
|
5
|
+
import {
|
|
6
|
+
Code,
|
|
7
|
+
ConnectError,
|
|
8
|
+
createClient,
|
|
9
|
+
type Client,
|
|
10
|
+
type ServiceImpl,
|
|
11
|
+
} from "@connectrpc/connect";
|
|
12
|
+
import { createGrpcTransport } from "@connectrpc/connect-node";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
CopyObjectResponseSchema,
|
|
16
|
+
HeadObjectResponseSchema,
|
|
17
|
+
ListObjectsResponseSchema,
|
|
18
|
+
PresignMethod as ProtoPresignMethod,
|
|
19
|
+
PresignObjectResponseSchema,
|
|
20
|
+
ReadObjectChunkSchema,
|
|
21
|
+
S3 as S3Service,
|
|
22
|
+
WriteObjectResponseSchema,
|
|
23
|
+
} from "../gen/v1/s3_pb.ts";
|
|
24
|
+
import { errorMessage, type MaybePromise } from "./api.ts";
|
|
25
|
+
import { RuntimeProvider, type RuntimeProviderOptions } from "./provider.ts";
|
|
26
|
+
|
|
27
|
+
const ENV_S3_SOCKET = "GESTALT_S3_SOCKET";
|
|
28
|
+
const WRITE_CHUNK_SIZE = 64 * 1024;
|
|
29
|
+
const textEncoder = new TextEncoder();
|
|
30
|
+
|
|
31
|
+
export function s3SocketEnv(name?: string): string {
|
|
32
|
+
const trimmed = name?.trim() ?? "";
|
|
33
|
+
if (!trimmed) return ENV_S3_SOCKET;
|
|
34
|
+
return `${ENV_S3_SOCKET}_${trimmed.replace(/[^A-Za-z0-9]/g, "_").toUpperCase()}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class S3NotFoundError extends Error {
|
|
38
|
+
constructor(message?: string) {
|
|
39
|
+
super(message ?? "s3: not found");
|
|
40
|
+
this.name = "S3NotFoundError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class S3PreconditionFailedError extends Error {
|
|
45
|
+
constructor(message?: string) {
|
|
46
|
+
super(message ?? "s3: precondition failed");
|
|
47
|
+
this.name = "S3PreconditionFailedError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class S3InvalidRangeError extends Error {
|
|
52
|
+
constructor(message?: string) {
|
|
53
|
+
super(message ?? "s3: invalid range");
|
|
54
|
+
this.name = "S3InvalidRangeError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ObjectRef {
|
|
59
|
+
bucket: string;
|
|
60
|
+
key: string;
|
|
61
|
+
versionId?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ObjectMeta {
|
|
65
|
+
ref: ObjectRef;
|
|
66
|
+
etag: string;
|
|
67
|
+
size: bigint;
|
|
68
|
+
contentType: string;
|
|
69
|
+
lastModified?: Date;
|
|
70
|
+
metadata: Record<string, string>;
|
|
71
|
+
storageClass: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ByteRange {
|
|
75
|
+
start?: number | bigint;
|
|
76
|
+
end?: number | bigint;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ReadOptions {
|
|
80
|
+
range?: ByteRange;
|
|
81
|
+
ifMatch?: string;
|
|
82
|
+
ifNoneMatch?: string;
|
|
83
|
+
ifModifiedSince?: Date;
|
|
84
|
+
ifUnmodifiedSince?: Date;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface WriteOptions {
|
|
88
|
+
contentType?: string;
|
|
89
|
+
cacheControl?: string;
|
|
90
|
+
contentDisposition?: string;
|
|
91
|
+
contentEncoding?: string;
|
|
92
|
+
contentLanguage?: string;
|
|
93
|
+
metadata?: Record<string, string>;
|
|
94
|
+
ifMatch?: string;
|
|
95
|
+
ifNoneMatch?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ListOptions {
|
|
99
|
+
bucket: string;
|
|
100
|
+
prefix?: string;
|
|
101
|
+
delimiter?: string;
|
|
102
|
+
continuationToken?: string;
|
|
103
|
+
startAfter?: string;
|
|
104
|
+
maxKeys?: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface ListPage {
|
|
108
|
+
objects: ObjectMeta[];
|
|
109
|
+
commonPrefixes: string[];
|
|
110
|
+
nextContinuationToken: string;
|
|
111
|
+
hasMore: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface CopyOptions {
|
|
115
|
+
ifMatch?: string;
|
|
116
|
+
ifNoneMatch?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export enum PresignMethod {
|
|
120
|
+
Get = "GET",
|
|
121
|
+
Put = "PUT",
|
|
122
|
+
Delete = "DELETE",
|
|
123
|
+
Head = "HEAD",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface PresignOptions {
|
|
127
|
+
method?: PresignMethod;
|
|
128
|
+
expiresSeconds?: number | bigint;
|
|
129
|
+
contentType?: string;
|
|
130
|
+
contentDisposition?: string;
|
|
131
|
+
headers?: Record<string, string>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface PresignResult {
|
|
135
|
+
url: string;
|
|
136
|
+
method: PresignMethod;
|
|
137
|
+
expiresAt?: Date;
|
|
138
|
+
headers: Record<string, string>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type S3BodySource =
|
|
142
|
+
| string
|
|
143
|
+
| Uint8Array
|
|
144
|
+
| ArrayBuffer
|
|
145
|
+
| ArrayBufferView
|
|
146
|
+
| Blob
|
|
147
|
+
| ReadableStream<Uint8Array>
|
|
148
|
+
| AsyncIterable<Uint8Array>
|
|
149
|
+
| null
|
|
150
|
+
| undefined;
|
|
151
|
+
|
|
152
|
+
export interface ReadResult {
|
|
153
|
+
meta: ObjectMeta;
|
|
154
|
+
stream: AsyncIterable<Uint8Array>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface ProviderReadResult {
|
|
158
|
+
meta: ObjectMeta;
|
|
159
|
+
body?: S3BodySource;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface S3ProviderOptions extends RuntimeProviderOptions {
|
|
163
|
+
headObject: (ref: ObjectRef) => MaybePromise<ObjectMeta>;
|
|
164
|
+
readObject: (ref: ObjectRef, options?: ReadOptions) => MaybePromise<ProviderReadResult>;
|
|
165
|
+
writeObject: (
|
|
166
|
+
ref: ObjectRef,
|
|
167
|
+
body: AsyncIterable<Uint8Array>,
|
|
168
|
+
options?: WriteOptions,
|
|
169
|
+
) => MaybePromise<ObjectMeta>;
|
|
170
|
+
deleteObject: (ref: ObjectRef) => MaybePromise<void>;
|
|
171
|
+
listObjects: (options: ListOptions) => MaybePromise<ListPage>;
|
|
172
|
+
copyObject: (
|
|
173
|
+
source: ObjectRef,
|
|
174
|
+
destination: ObjectRef,
|
|
175
|
+
options?: CopyOptions,
|
|
176
|
+
) => MaybePromise<ObjectMeta>;
|
|
177
|
+
presignObject: (
|
|
178
|
+
ref: ObjectRef,
|
|
179
|
+
options?: PresignOptions,
|
|
180
|
+
) => MaybePromise<PresignResult>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export class S3Provider extends RuntimeProvider {
|
|
184
|
+
readonly kind = "s3" as const;
|
|
185
|
+
|
|
186
|
+
private readonly headObjectHandler: S3ProviderOptions["headObject"];
|
|
187
|
+
private readonly readObjectHandler: S3ProviderOptions["readObject"];
|
|
188
|
+
private readonly writeObjectHandler: S3ProviderOptions["writeObject"];
|
|
189
|
+
private readonly deleteObjectHandler: S3ProviderOptions["deleteObject"];
|
|
190
|
+
private readonly listObjectsHandler: S3ProviderOptions["listObjects"];
|
|
191
|
+
private readonly copyObjectHandler: S3ProviderOptions["copyObject"];
|
|
192
|
+
private readonly presignObjectHandler: S3ProviderOptions["presignObject"];
|
|
193
|
+
|
|
194
|
+
constructor(options: S3ProviderOptions) {
|
|
195
|
+
super(options);
|
|
196
|
+
this.headObjectHandler = options.headObject;
|
|
197
|
+
this.readObjectHandler = options.readObject;
|
|
198
|
+
this.writeObjectHandler = options.writeObject;
|
|
199
|
+
this.deleteObjectHandler = options.deleteObject;
|
|
200
|
+
this.listObjectsHandler = options.listObjects;
|
|
201
|
+
this.copyObjectHandler = options.copyObject;
|
|
202
|
+
this.presignObjectHandler = options.presignObject;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async headObject(ref: ObjectRef): Promise<ObjectMeta> {
|
|
206
|
+
return await this.headObjectHandler(ref);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async readObject(ref: ObjectRef, options?: ReadOptions): Promise<ProviderReadResult> {
|
|
210
|
+
return await this.readObjectHandler(ref, options);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async writeObject(
|
|
214
|
+
ref: ObjectRef,
|
|
215
|
+
body: AsyncIterable<Uint8Array>,
|
|
216
|
+
options?: WriteOptions,
|
|
217
|
+
): Promise<ObjectMeta> {
|
|
218
|
+
return await this.writeObjectHandler(ref, body, options);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async deleteObject(ref: ObjectRef): Promise<void> {
|
|
222
|
+
await this.deleteObjectHandler(ref);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async listObjects(options: ListOptions): Promise<ListPage> {
|
|
226
|
+
return await this.listObjectsHandler(options);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async copyObject(
|
|
230
|
+
source: ObjectRef,
|
|
231
|
+
destination: ObjectRef,
|
|
232
|
+
options?: CopyOptions,
|
|
233
|
+
): Promise<ObjectMeta> {
|
|
234
|
+
return await this.copyObjectHandler(source, destination, options);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async presignObject(ref: ObjectRef, options?: PresignOptions): Promise<PresignResult> {
|
|
238
|
+
return await this.presignObjectHandler(ref, options);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function defineS3Provider(options: S3ProviderOptions): S3Provider {
|
|
243
|
+
return new S3Provider(options);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function isS3Provider(value: unknown): value is S3Provider {
|
|
247
|
+
return (
|
|
248
|
+
value instanceof S3Provider ||
|
|
249
|
+
(typeof value === "object" &&
|
|
250
|
+
value !== null &&
|
|
251
|
+
"kind" in value &&
|
|
252
|
+
(value as { kind?: unknown }).kind === "s3" &&
|
|
253
|
+
"headObject" in value &&
|
|
254
|
+
"readObject" in value &&
|
|
255
|
+
"writeObject" in value &&
|
|
256
|
+
"deleteObject" in value &&
|
|
257
|
+
"listObjects" in value &&
|
|
258
|
+
"copyObject" in value &&
|
|
259
|
+
"presignObject" in value)
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function createS3Service(
|
|
264
|
+
provider: S3Provider,
|
|
265
|
+
): Partial<ServiceImpl<typeof S3Service>> {
|
|
266
|
+
return {
|
|
267
|
+
async headObject(request) {
|
|
268
|
+
const meta = await invokeS3Provider("head object", () =>
|
|
269
|
+
provider.headObject(fromProtoObjectRef(request.ref)),
|
|
270
|
+
);
|
|
271
|
+
return create(HeadObjectResponseSchema, { meta: toProtoObjectMeta(meta) });
|
|
272
|
+
},
|
|
273
|
+
async *readObject(request) {
|
|
274
|
+
const result = await invokeS3Provider("read object", () =>
|
|
275
|
+
provider.readObject(fromProtoObjectRef(request.ref), fromProtoReadOptions(request)),
|
|
276
|
+
);
|
|
277
|
+
yield create(ReadObjectChunkSchema, {
|
|
278
|
+
result: {
|
|
279
|
+
case: "meta",
|
|
280
|
+
value: toProtoObjectMeta(result.meta),
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
try {
|
|
284
|
+
for await (const chunk of toAsyncByteStream(result.body)) {
|
|
285
|
+
if (chunk.byteLength === 0) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
yield create(ReadObjectChunkSchema, {
|
|
289
|
+
result: {
|
|
290
|
+
case: "data",
|
|
291
|
+
value: chunk,
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
} catch (error) {
|
|
296
|
+
throw toS3ConnectError(error, "read object");
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
async writeObject(request) {
|
|
300
|
+
const iterator = request[Symbol.asyncIterator]();
|
|
301
|
+
const first = await readNextRequest(iterator, "write object");
|
|
302
|
+
if (first.done || first.value.msg.case !== "open") {
|
|
303
|
+
throw new ConnectError(
|
|
304
|
+
"write object stream must begin with an open frame",
|
|
305
|
+
Code.InvalidArgument,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
const open = first.value.msg.value;
|
|
309
|
+
const body = writeBodyFromStream(iterator);
|
|
310
|
+
try {
|
|
311
|
+
const meta = await invokeS3Provider("write object", () =>
|
|
312
|
+
provider.writeObject(
|
|
313
|
+
fromProtoObjectRef(open.ref),
|
|
314
|
+
body,
|
|
315
|
+
fromProtoWriteOptions(open),
|
|
316
|
+
),
|
|
317
|
+
);
|
|
318
|
+
return create(WriteObjectResponseSchema, {
|
|
319
|
+
meta: toProtoObjectMeta(meta),
|
|
320
|
+
});
|
|
321
|
+
} finally {
|
|
322
|
+
if (typeof body.return === "function") {
|
|
323
|
+
await body.return();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
async deleteObject(request) {
|
|
328
|
+
await invokeS3Provider("delete object", () =>
|
|
329
|
+
provider.deleteObject(fromProtoObjectRef(request.ref)),
|
|
330
|
+
);
|
|
331
|
+
return create(EmptySchema, {});
|
|
332
|
+
},
|
|
333
|
+
async listObjects(request) {
|
|
334
|
+
const options: ListOptions = {
|
|
335
|
+
bucket: request.bucket,
|
|
336
|
+
};
|
|
337
|
+
if (request.prefix) {
|
|
338
|
+
options.prefix = request.prefix;
|
|
339
|
+
}
|
|
340
|
+
if (request.delimiter) {
|
|
341
|
+
options.delimiter = request.delimiter;
|
|
342
|
+
}
|
|
343
|
+
if (request.continuationToken) {
|
|
344
|
+
options.continuationToken = request.continuationToken;
|
|
345
|
+
}
|
|
346
|
+
if (request.startAfter) {
|
|
347
|
+
options.startAfter = request.startAfter;
|
|
348
|
+
}
|
|
349
|
+
if (request.maxKeys > 0) {
|
|
350
|
+
options.maxKeys = request.maxKeys;
|
|
351
|
+
}
|
|
352
|
+
const page = await invokeS3Provider("list objects", () =>
|
|
353
|
+
provider.listObjects(options),
|
|
354
|
+
);
|
|
355
|
+
return create(ListObjectsResponseSchema, {
|
|
356
|
+
objects: page.objects.map(toProtoObjectMeta),
|
|
357
|
+
commonPrefixes: [...page.commonPrefixes],
|
|
358
|
+
nextContinuationToken: page.nextContinuationToken,
|
|
359
|
+
hasMore: page.hasMore,
|
|
360
|
+
});
|
|
361
|
+
},
|
|
362
|
+
async copyObject(request) {
|
|
363
|
+
const options: CopyOptions = {};
|
|
364
|
+
if (request.ifMatch) {
|
|
365
|
+
options.ifMatch = request.ifMatch;
|
|
366
|
+
}
|
|
367
|
+
if (request.ifNoneMatch) {
|
|
368
|
+
options.ifNoneMatch = request.ifNoneMatch;
|
|
369
|
+
}
|
|
370
|
+
const meta = await invokeS3Provider("copy object", () =>
|
|
371
|
+
provider.copyObject(
|
|
372
|
+
fromProtoObjectRef(request.source),
|
|
373
|
+
fromProtoObjectRef(request.destination),
|
|
374
|
+
options,
|
|
375
|
+
),
|
|
376
|
+
);
|
|
377
|
+
return create(CopyObjectResponseSchema, { meta: toProtoObjectMeta(meta) });
|
|
378
|
+
},
|
|
379
|
+
async presignObject(request) {
|
|
380
|
+
const options: PresignOptions = {
|
|
381
|
+
method: fromProtoPresignMethod(request.method),
|
|
382
|
+
headers: cloneStringMap(request.headers),
|
|
383
|
+
};
|
|
384
|
+
if (request.expiresSeconds !== 0n) {
|
|
385
|
+
options.expiresSeconds = request.expiresSeconds;
|
|
386
|
+
}
|
|
387
|
+
if (request.contentType) {
|
|
388
|
+
options.contentType = request.contentType;
|
|
389
|
+
}
|
|
390
|
+
if (request.contentDisposition) {
|
|
391
|
+
options.contentDisposition = request.contentDisposition;
|
|
392
|
+
}
|
|
393
|
+
const result = await invokeS3Provider("presign object", () =>
|
|
394
|
+
provider.presignObject(fromProtoObjectRef(request.ref), options),
|
|
395
|
+
);
|
|
396
|
+
const response = {
|
|
397
|
+
url: result.url,
|
|
398
|
+
method: toProtoPresignMethod(result.method),
|
|
399
|
+
headers: cloneStringMap(result.headers),
|
|
400
|
+
} as {
|
|
401
|
+
url: string;
|
|
402
|
+
method: ProtoPresignMethod;
|
|
403
|
+
headers: Record<string, string>;
|
|
404
|
+
expiresAt?: { seconds: bigint; nanos: number };
|
|
405
|
+
};
|
|
406
|
+
if (result.expiresAt) {
|
|
407
|
+
response.expiresAt = toProtoTimestamp(result.expiresAt);
|
|
408
|
+
}
|
|
409
|
+
return create(PresignObjectResponseSchema, response);
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export class S3 {
|
|
415
|
+
private readonly client: Client<typeof S3Service>;
|
|
416
|
+
|
|
417
|
+
constructor(name?: string) {
|
|
418
|
+
const envName = s3SocketEnv(name);
|
|
419
|
+
const socketPath = process.env[envName];
|
|
420
|
+
if (!socketPath) {
|
|
421
|
+
throw new Error(`${envName} is not set`);
|
|
422
|
+
}
|
|
423
|
+
const transport = createGrpcTransport({
|
|
424
|
+
baseUrl: "http://localhost",
|
|
425
|
+
nodeOptions: {
|
|
426
|
+
createConnection: () => connect(socketPath),
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
this.client = createClient(S3Service, transport);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
object(bucket: string, key: string): S3Object {
|
|
433
|
+
return new S3Object(this, { bucket, key });
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
objectVersion(bucket: string, key: string, versionId: string): S3Object {
|
|
437
|
+
return new S3Object(this, { bucket, key, versionId });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async headObject(ref: ObjectRef): Promise<ObjectMeta> {
|
|
441
|
+
const response = await s3Rpc(() =>
|
|
442
|
+
this.client.headObject({
|
|
443
|
+
ref: toProtoObjectRef(ref),
|
|
444
|
+
}),
|
|
445
|
+
);
|
|
446
|
+
return fromProtoObjectMeta(response.meta);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async readObject(ref: ObjectRef, options?: ReadOptions): Promise<ReadResult> {
|
|
450
|
+
const response = this.client.readObject({
|
|
451
|
+
ref: toProtoObjectRef(ref),
|
|
452
|
+
...toProtoReadOptions(options),
|
|
453
|
+
});
|
|
454
|
+
const iterator = response[Symbol.asyncIterator]();
|
|
455
|
+
const first = await readNextResponse(iterator);
|
|
456
|
+
if (first.done || first.value.result.case !== "meta") {
|
|
457
|
+
throw new Error("s3 read stream did not start with object metadata");
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
meta: fromProtoObjectMeta(first.value.result.value),
|
|
461
|
+
stream: readDataChunks(iterator),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async writeObject(
|
|
466
|
+
ref: ObjectRef,
|
|
467
|
+
body?: S3BodySource,
|
|
468
|
+
options?: WriteOptions,
|
|
469
|
+
): Promise<ObjectMeta> {
|
|
470
|
+
const snapshot = snapshotS3Body(body);
|
|
471
|
+
const response = await s3Rpc(() =>
|
|
472
|
+
this.client.writeObject(writeRequests(ref, snapshot, options)),
|
|
473
|
+
);
|
|
474
|
+
return fromProtoObjectMeta(response.meta);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async deleteObject(ref: ObjectRef): Promise<void> {
|
|
478
|
+
await s3Rpc(() =>
|
|
479
|
+
this.client.deleteObject({
|
|
480
|
+
ref: toProtoObjectRef(ref),
|
|
481
|
+
}),
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async listObjects(options: ListOptions): Promise<ListPage> {
|
|
486
|
+
const response = await s3Rpc(() =>
|
|
487
|
+
this.client.listObjects({
|
|
488
|
+
bucket: options.bucket,
|
|
489
|
+
prefix: options.prefix ?? "",
|
|
490
|
+
delimiter: options.delimiter ?? "",
|
|
491
|
+
continuationToken: options.continuationToken ?? "",
|
|
492
|
+
startAfter: options.startAfter ?? "",
|
|
493
|
+
maxKeys: options.maxKeys ?? 0,
|
|
494
|
+
}),
|
|
495
|
+
);
|
|
496
|
+
return {
|
|
497
|
+
objects: response.objects.map(fromProtoObjectMeta),
|
|
498
|
+
commonPrefixes: [...response.commonPrefixes],
|
|
499
|
+
nextContinuationToken: response.nextContinuationToken,
|
|
500
|
+
hasMore: response.hasMore,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async copyObject(
|
|
505
|
+
source: ObjectRef,
|
|
506
|
+
destination: ObjectRef,
|
|
507
|
+
options?: CopyOptions,
|
|
508
|
+
): Promise<ObjectMeta> {
|
|
509
|
+
const response = await s3Rpc(() =>
|
|
510
|
+
this.client.copyObject({
|
|
511
|
+
source: toProtoObjectRef(source),
|
|
512
|
+
destination: toProtoObjectRef(destination),
|
|
513
|
+
ifMatch: options?.ifMatch ?? "",
|
|
514
|
+
ifNoneMatch: options?.ifNoneMatch ?? "",
|
|
515
|
+
}),
|
|
516
|
+
);
|
|
517
|
+
return fromProtoObjectMeta(response.meta);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async presignObject(ref: ObjectRef, options?: PresignOptions): Promise<PresignResult> {
|
|
521
|
+
const requestedMethod = options?.method ?? PresignMethod.Get;
|
|
522
|
+
const response = await s3Rpc(() =>
|
|
523
|
+
this.client.presignObject({
|
|
524
|
+
ref: toProtoObjectRef(ref),
|
|
525
|
+
method: toProtoPresignMethod(requestedMethod),
|
|
526
|
+
expiresSeconds: normalizeProtoInt(options?.expiresSeconds),
|
|
527
|
+
contentType: options?.contentType ?? "",
|
|
528
|
+
contentDisposition: options?.contentDisposition ?? "",
|
|
529
|
+
headers: cloneStringMap(options?.headers),
|
|
530
|
+
}),
|
|
531
|
+
);
|
|
532
|
+
const result: PresignResult = {
|
|
533
|
+
url: response.url,
|
|
534
|
+
method: response.method === ProtoPresignMethod.UNSPECIFIED
|
|
535
|
+
? requestedMethod
|
|
536
|
+
: fromProtoPresignMethod(response.method),
|
|
537
|
+
headers: cloneStringMap(response.headers),
|
|
538
|
+
};
|
|
539
|
+
if (response.expiresAt) {
|
|
540
|
+
result.expiresAt = fromProtoTimestamp(response.expiresAt);
|
|
541
|
+
}
|
|
542
|
+
return result;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export class S3Object {
|
|
547
|
+
constructor(
|
|
548
|
+
private readonly client: S3,
|
|
549
|
+
readonly ref: ObjectRef,
|
|
550
|
+
) {}
|
|
551
|
+
|
|
552
|
+
async stat(): Promise<ObjectMeta> {
|
|
553
|
+
return await this.client.headObject(this.ref);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async exists(): Promise<boolean> {
|
|
557
|
+
try {
|
|
558
|
+
await this.stat();
|
|
559
|
+
return true;
|
|
560
|
+
} catch (error) {
|
|
561
|
+
if (error instanceof S3NotFoundError) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
throw error;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async read(options?: ReadOptions): Promise<ReadResult> {
|
|
569
|
+
return await this.client.readObject(this.ref, options);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async stream(options?: ReadOptions): Promise<AsyncIterable<Uint8Array>> {
|
|
573
|
+
const result = await this.read(options);
|
|
574
|
+
return result.stream;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async bytes(options?: ReadOptions): Promise<Uint8Array> {
|
|
578
|
+
const result = await this.read(options);
|
|
579
|
+
return await collectBytes(result.stream);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async text(options?: ReadOptions, encoding = "utf-8"): Promise<string> {
|
|
583
|
+
return new TextDecoder(encoding).decode(await this.bytes(options));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async json<T = unknown>(options?: ReadOptions): Promise<T> {
|
|
587
|
+
return JSON.parse(await this.text(options)) as T;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async write(body?: S3BodySource, options?: WriteOptions): Promise<ObjectMeta> {
|
|
591
|
+
return await this.client.writeObject(this.ref, body, options);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async writeBytes(body: Uint8Array | ArrayBuffer | ArrayBufferView): Promise<ObjectMeta> {
|
|
595
|
+
return await this.write(body);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async writeString(body: string, options?: WriteOptions): Promise<ObjectMeta> {
|
|
599
|
+
return await this.write(body, options);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async writeJSON(value: unknown, options: WriteOptions = {}): Promise<ObjectMeta> {
|
|
603
|
+
return await this.write(JSON.stringify(value), {
|
|
604
|
+
...options,
|
|
605
|
+
contentType: options.contentType ?? "application/json",
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async delete(): Promise<void> {
|
|
610
|
+
await this.client.deleteObject(this.ref);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async presign(options?: PresignOptions): Promise<PresignResult> {
|
|
614
|
+
return await this.client.presignObject(this.ref, options);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function invokeS3Provider<T>(label: string, fn: () => Promise<T>): Promise<T> {
|
|
619
|
+
try {
|
|
620
|
+
return await fn();
|
|
621
|
+
} catch (error) {
|
|
622
|
+
throw toS3ConnectError(error, label);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async function readNextRequest<T>(
|
|
627
|
+
iterator: AsyncIterator<T>,
|
|
628
|
+
label: string,
|
|
629
|
+
): Promise<IteratorResult<T>> {
|
|
630
|
+
try {
|
|
631
|
+
return await iterator.next();
|
|
632
|
+
} catch (error) {
|
|
633
|
+
throw toS3ConnectError(error, label);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function readNextResponse<T>(iterator: AsyncIterator<T>): Promise<IteratorResult<T>> {
|
|
638
|
+
try {
|
|
639
|
+
return await iterator.next();
|
|
640
|
+
} catch (error) {
|
|
641
|
+
throw mapS3RpcError(error);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async function* readDataChunks(
|
|
646
|
+
iterator: AsyncIterator<{ result: { case: "meta"; value: unknown } | { case: "data"; value: Uint8Array } | { case: undefined; value?: undefined } }>,
|
|
647
|
+
): AsyncIterable<Uint8Array> {
|
|
648
|
+
let finished = false;
|
|
649
|
+
try {
|
|
650
|
+
while (true) {
|
|
651
|
+
const next = await readNextResponse(iterator);
|
|
652
|
+
if (next.done) {
|
|
653
|
+
finished = true;
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (next.value.result.case !== "data") {
|
|
657
|
+
throw new Error("s3 read stream emitted an unexpected metadata frame");
|
|
658
|
+
}
|
|
659
|
+
yield cloneBytes(next.value.result.value);
|
|
660
|
+
}
|
|
661
|
+
} finally {
|
|
662
|
+
if (!finished && typeof iterator.return === "function") {
|
|
663
|
+
await iterator.return();
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function* writeRequests(
|
|
669
|
+
ref: ObjectRef,
|
|
670
|
+
body?: S3BodySource,
|
|
671
|
+
options?: WriteOptions,
|
|
672
|
+
): AsyncIterable<{
|
|
673
|
+
msg:
|
|
674
|
+
| { case: "open"; value: Record<string, unknown> }
|
|
675
|
+
| { case: "data"; value: Uint8Array };
|
|
676
|
+
}> {
|
|
677
|
+
yield {
|
|
678
|
+
msg: {
|
|
679
|
+
case: "open",
|
|
680
|
+
value: {
|
|
681
|
+
ref: toProtoObjectRef(ref),
|
|
682
|
+
contentType: options?.contentType ?? "",
|
|
683
|
+
cacheControl: options?.cacheControl ?? "",
|
|
684
|
+
contentDisposition: options?.contentDisposition ?? "",
|
|
685
|
+
contentEncoding: options?.contentEncoding ?? "",
|
|
686
|
+
contentLanguage: options?.contentLanguage ?? "",
|
|
687
|
+
metadata: cloneStringMap(options?.metadata),
|
|
688
|
+
ifMatch: options?.ifMatch ?? "",
|
|
689
|
+
ifNoneMatch: options?.ifNoneMatch ?? "",
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
for await (const chunk of toAsyncByteStream(body)) {
|
|
694
|
+
if (chunk.byteLength === 0) {
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
yield {
|
|
698
|
+
msg: {
|
|
699
|
+
case: "data",
|
|
700
|
+
value: chunk,
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
async function* writeBodyFromStream(
|
|
707
|
+
iterator: AsyncIterator<{ msg: { case: "open" | "data" | undefined; value?: any } }>,
|
|
708
|
+
): AsyncGenerator<Uint8Array, void, undefined> {
|
|
709
|
+
try {
|
|
710
|
+
while (true) {
|
|
711
|
+
const next = await readNextRequest(iterator, "write object");
|
|
712
|
+
if (next.done) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (next.value.msg.case !== "data") {
|
|
716
|
+
throw new ConnectError(
|
|
717
|
+
"write object frames after open must carry data",
|
|
718
|
+
Code.InvalidArgument,
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
const chunk = cloneBytes(next.value.msg.value as Uint8Array);
|
|
722
|
+
if (chunk.byteLength === 0) {
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
yield chunk;
|
|
726
|
+
}
|
|
727
|
+
} finally {
|
|
728
|
+
if (typeof iterator.return === "function") {
|
|
729
|
+
await iterator.return();
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function collectBytes(stream: AsyncIterable<Uint8Array>): Promise<Uint8Array> {
|
|
735
|
+
const parts: Uint8Array[] = [];
|
|
736
|
+
let total = 0;
|
|
737
|
+
for await (const chunk of stream) {
|
|
738
|
+
parts.push(chunk);
|
|
739
|
+
total += chunk.byteLength;
|
|
740
|
+
}
|
|
741
|
+
const out = new Uint8Array(total);
|
|
742
|
+
let offset = 0;
|
|
743
|
+
for (const part of parts) {
|
|
744
|
+
out.set(part, offset);
|
|
745
|
+
offset += part.byteLength;
|
|
746
|
+
}
|
|
747
|
+
return out;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async function* toAsyncByteStream(body?: S3BodySource): AsyncIterable<Uint8Array> {
|
|
751
|
+
if (body == null) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (typeof body === "string") {
|
|
755
|
+
yield* chunkBytes(textEncoder.encode(body));
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (body instanceof Uint8Array) {
|
|
759
|
+
yield* chunkBytes(body);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (body instanceof ArrayBuffer) {
|
|
763
|
+
yield* chunkBytes(new Uint8Array(body));
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (ArrayBuffer.isView(body)) {
|
|
767
|
+
yield* chunkBytes(new Uint8Array(body.buffer, body.byteOffset, body.byteLength));
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (body instanceof Blob) {
|
|
771
|
+
yield* readableStreamToAsyncIterable(body.stream() as ReadableStream<Uint8Array>);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
if (isReadableStream(body)) {
|
|
775
|
+
yield* readableStreamToAsyncIterable(body);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (isAsyncIterable(body)) {
|
|
779
|
+
for await (const chunk of body) {
|
|
780
|
+
yield cloneBytes(chunk);
|
|
781
|
+
}
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
throw new Error("unsupported s3 body source");
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function* chunkBytes(bytes: Uint8Array): Iterable<Uint8Array> {
|
|
788
|
+
for (let offset = 0; offset < bytes.byteLength; offset += WRITE_CHUNK_SIZE) {
|
|
789
|
+
yield cloneBytes(bytes.subarray(offset, offset + WRITE_CHUNK_SIZE));
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function snapshotS3Body(body?: S3BodySource): S3BodySource | undefined {
|
|
794
|
+
if (body == null || typeof body === "string") {
|
|
795
|
+
return body;
|
|
796
|
+
}
|
|
797
|
+
if (body instanceof Uint8Array) {
|
|
798
|
+
return cloneBytes(body);
|
|
799
|
+
}
|
|
800
|
+
if (body instanceof ArrayBuffer) {
|
|
801
|
+
return body.slice(0);
|
|
802
|
+
}
|
|
803
|
+
if (ArrayBuffer.isView(body)) {
|
|
804
|
+
return new Uint8Array(body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength));
|
|
805
|
+
}
|
|
806
|
+
return body;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
async function* readableStreamToAsyncIterable(
|
|
810
|
+
stream: ReadableStream<Uint8Array>,
|
|
811
|
+
): AsyncIterable<Uint8Array> {
|
|
812
|
+
const reader = stream.getReader();
|
|
813
|
+
let exhausted = false;
|
|
814
|
+
try {
|
|
815
|
+
while (true) {
|
|
816
|
+
const { value, done } = await reader.read();
|
|
817
|
+
if (done) {
|
|
818
|
+
exhausted = true;
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (!value) {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
yield cloneBytes(value);
|
|
825
|
+
}
|
|
826
|
+
} finally {
|
|
827
|
+
try {
|
|
828
|
+
if (!exhausted) {
|
|
829
|
+
await reader.cancel();
|
|
830
|
+
}
|
|
831
|
+
} catch {
|
|
832
|
+
// Ignore cancellation failures and preserve the original stream result.
|
|
833
|
+
} finally {
|
|
834
|
+
reader.releaseLock();
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function isAsyncIterable(value: unknown): value is AsyncIterable<Uint8Array> {
|
|
840
|
+
return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function isReadableStream(value: unknown): value is ReadableStream<Uint8Array> {
|
|
844
|
+
return typeof value === "object" && value !== null && "getReader" in value;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function toProtoObjectRef(ref: ObjectRef) {
|
|
848
|
+
return {
|
|
849
|
+
bucket: ref.bucket,
|
|
850
|
+
key: ref.key,
|
|
851
|
+
versionId: ref.versionId ?? "",
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function fromProtoObjectRef(ref: { bucket?: string; key?: string; versionId?: string } | undefined): ObjectRef {
|
|
856
|
+
const value: ObjectRef = {
|
|
857
|
+
bucket: ref?.bucket ?? "",
|
|
858
|
+
key: ref?.key ?? "",
|
|
859
|
+
};
|
|
860
|
+
if (ref?.versionId) {
|
|
861
|
+
value.versionId = ref.versionId;
|
|
862
|
+
}
|
|
863
|
+
return value;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function toProtoObjectMeta(meta: ObjectMeta) {
|
|
867
|
+
const value: {
|
|
868
|
+
ref: ReturnType<typeof toProtoObjectRef>;
|
|
869
|
+
etag: string;
|
|
870
|
+
size: bigint;
|
|
871
|
+
contentType: string;
|
|
872
|
+
metadata: Record<string, string>;
|
|
873
|
+
storageClass: string;
|
|
874
|
+
lastModified?: { seconds: bigint; nanos: number };
|
|
875
|
+
} = {
|
|
876
|
+
ref: toProtoObjectRef(meta.ref),
|
|
877
|
+
etag: meta.etag,
|
|
878
|
+
size: meta.size,
|
|
879
|
+
contentType: meta.contentType,
|
|
880
|
+
metadata: cloneStringMap(meta.metadata),
|
|
881
|
+
storageClass: meta.storageClass,
|
|
882
|
+
};
|
|
883
|
+
if (meta.lastModified) {
|
|
884
|
+
value.lastModified = toProtoTimestamp(meta.lastModified);
|
|
885
|
+
}
|
|
886
|
+
return value;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function fromProtoObjectMeta(meta: {
|
|
890
|
+
ref?: { bucket?: string; key?: string; versionId?: string };
|
|
891
|
+
etag?: string;
|
|
892
|
+
size?: bigint;
|
|
893
|
+
contentType?: string;
|
|
894
|
+
lastModified?: { seconds?: bigint; nanos?: number };
|
|
895
|
+
metadata?: Record<string, string>;
|
|
896
|
+
storageClass?: string;
|
|
897
|
+
} | undefined): ObjectMeta {
|
|
898
|
+
const value: ObjectMeta = {
|
|
899
|
+
ref: fromProtoObjectRef(meta?.ref),
|
|
900
|
+
etag: meta?.etag ?? "",
|
|
901
|
+
size: meta?.size ?? 0n,
|
|
902
|
+
contentType: meta?.contentType ?? "",
|
|
903
|
+
metadata: cloneStringMap(meta?.metadata),
|
|
904
|
+
storageClass: meta?.storageClass ?? "",
|
|
905
|
+
};
|
|
906
|
+
if (meta?.lastModified) {
|
|
907
|
+
value.lastModified = fromProtoTimestamp(meta.lastModified);
|
|
908
|
+
}
|
|
909
|
+
return value;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function toProtoReadOptions(options?: ReadOptions) {
|
|
913
|
+
const proto: Record<string, unknown> = {
|
|
914
|
+
ifMatch: options?.ifMatch ?? "",
|
|
915
|
+
ifNoneMatch: options?.ifNoneMatch ?? "",
|
|
916
|
+
};
|
|
917
|
+
if (options?.range) {
|
|
918
|
+
proto.range = toProtoByteRange(options.range);
|
|
919
|
+
}
|
|
920
|
+
if (options?.ifModifiedSince) {
|
|
921
|
+
proto.ifModifiedSince = toProtoTimestamp(options.ifModifiedSince);
|
|
922
|
+
}
|
|
923
|
+
if (options?.ifUnmodifiedSince) {
|
|
924
|
+
proto.ifUnmodifiedSince = toProtoTimestamp(options.ifUnmodifiedSince);
|
|
925
|
+
}
|
|
926
|
+
return proto;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function fromProtoReadOptions(request: {
|
|
930
|
+
range?: { start?: bigint; end?: bigint };
|
|
931
|
+
ifMatch?: string;
|
|
932
|
+
ifNoneMatch?: string;
|
|
933
|
+
ifModifiedSince?: { seconds?: bigint; nanos?: number };
|
|
934
|
+
ifUnmodifiedSince?: { seconds?: bigint; nanos?: number };
|
|
935
|
+
}): ReadOptions {
|
|
936
|
+
const options: ReadOptions = {};
|
|
937
|
+
if (request.range) {
|
|
938
|
+
options.range = fromProtoByteRange(request.range);
|
|
939
|
+
}
|
|
940
|
+
if (request.ifMatch) {
|
|
941
|
+
options.ifMatch = request.ifMatch;
|
|
942
|
+
}
|
|
943
|
+
if (request.ifNoneMatch) {
|
|
944
|
+
options.ifNoneMatch = request.ifNoneMatch;
|
|
945
|
+
}
|
|
946
|
+
if (request.ifModifiedSince) {
|
|
947
|
+
options.ifModifiedSince = fromProtoTimestamp(request.ifModifiedSince);
|
|
948
|
+
}
|
|
949
|
+
if (request.ifUnmodifiedSince) {
|
|
950
|
+
options.ifUnmodifiedSince = fromProtoTimestamp(request.ifUnmodifiedSince);
|
|
951
|
+
}
|
|
952
|
+
return options;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function fromProtoWriteOptions(open: {
|
|
956
|
+
contentType?: string;
|
|
957
|
+
cacheControl?: string;
|
|
958
|
+
contentDisposition?: string;
|
|
959
|
+
contentEncoding?: string;
|
|
960
|
+
contentLanguage?: string;
|
|
961
|
+
metadata?: Record<string, string>;
|
|
962
|
+
ifMatch?: string;
|
|
963
|
+
ifNoneMatch?: string;
|
|
964
|
+
}): WriteOptions {
|
|
965
|
+
const options: WriteOptions = {};
|
|
966
|
+
if (open.contentType) {
|
|
967
|
+
options.contentType = open.contentType;
|
|
968
|
+
}
|
|
969
|
+
if (open.cacheControl) {
|
|
970
|
+
options.cacheControl = open.cacheControl;
|
|
971
|
+
}
|
|
972
|
+
if (open.contentDisposition) {
|
|
973
|
+
options.contentDisposition = open.contentDisposition;
|
|
974
|
+
}
|
|
975
|
+
if (open.contentEncoding) {
|
|
976
|
+
options.contentEncoding = open.contentEncoding;
|
|
977
|
+
}
|
|
978
|
+
if (open.contentLanguage) {
|
|
979
|
+
options.contentLanguage = open.contentLanguage;
|
|
980
|
+
}
|
|
981
|
+
if (open.metadata && Object.keys(open.metadata).length > 0) {
|
|
982
|
+
options.metadata = cloneStringMap(open.metadata);
|
|
983
|
+
}
|
|
984
|
+
if (open.ifMatch) {
|
|
985
|
+
options.ifMatch = open.ifMatch;
|
|
986
|
+
}
|
|
987
|
+
if (open.ifNoneMatch) {
|
|
988
|
+
options.ifNoneMatch = open.ifNoneMatch;
|
|
989
|
+
}
|
|
990
|
+
return options;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function toProtoByteRange(range: ByteRange) {
|
|
994
|
+
const proto: Record<string, unknown> = {};
|
|
995
|
+
if (range.start !== undefined) {
|
|
996
|
+
proto.start = normalizeProtoInt(range.start);
|
|
997
|
+
}
|
|
998
|
+
if (range.end !== undefined) {
|
|
999
|
+
proto.end = normalizeProtoInt(range.end);
|
|
1000
|
+
}
|
|
1001
|
+
return proto;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function fromProtoByteRange(range: { start?: bigint; end?: bigint }): ByteRange {
|
|
1005
|
+
const value: ByteRange = {};
|
|
1006
|
+
if (range.start !== undefined) {
|
|
1007
|
+
value.start = range.start;
|
|
1008
|
+
}
|
|
1009
|
+
if (range.end !== undefined) {
|
|
1010
|
+
value.end = range.end;
|
|
1011
|
+
}
|
|
1012
|
+
return value;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function toProtoPresignMethod(method?: PresignMethod): ProtoPresignMethod {
|
|
1016
|
+
switch (method ?? PresignMethod.Get) {
|
|
1017
|
+
case PresignMethod.Get:
|
|
1018
|
+
return ProtoPresignMethod.GET;
|
|
1019
|
+
case PresignMethod.Put:
|
|
1020
|
+
return ProtoPresignMethod.PUT;
|
|
1021
|
+
case PresignMethod.Delete:
|
|
1022
|
+
return ProtoPresignMethod.DELETE;
|
|
1023
|
+
case PresignMethod.Head:
|
|
1024
|
+
return ProtoPresignMethod.HEAD;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function fromProtoPresignMethod(method: ProtoPresignMethod): PresignMethod {
|
|
1029
|
+
switch (method) {
|
|
1030
|
+
case ProtoPresignMethod.PUT:
|
|
1031
|
+
return PresignMethod.Put;
|
|
1032
|
+
case ProtoPresignMethod.DELETE:
|
|
1033
|
+
return PresignMethod.Delete;
|
|
1034
|
+
case ProtoPresignMethod.HEAD:
|
|
1035
|
+
return PresignMethod.Head;
|
|
1036
|
+
case ProtoPresignMethod.GET:
|
|
1037
|
+
case ProtoPresignMethod.UNSPECIFIED:
|
|
1038
|
+
default:
|
|
1039
|
+
return PresignMethod.Get;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function toProtoTimestamp(value: Date) {
|
|
1044
|
+
const millis = value.getTime();
|
|
1045
|
+
const seconds = Math.floor(millis / 1000);
|
|
1046
|
+
const nanos = Math.trunc((millis - (seconds * 1000)) * 1_000_000);
|
|
1047
|
+
return {
|
|
1048
|
+
seconds: BigInt(seconds),
|
|
1049
|
+
nanos,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function fromProtoTimestamp(value: { seconds?: bigint; nanos?: number }): Date {
|
|
1054
|
+
const seconds = Number(value.seconds ?? 0n);
|
|
1055
|
+
const nanos = Number(value.nanos ?? 0);
|
|
1056
|
+
return new Date((seconds * 1000) + Math.trunc(nanos / 1_000_000));
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function normalizeProtoInt(value: number | bigint | undefined): bigint {
|
|
1060
|
+
if (typeof value === "bigint") {
|
|
1061
|
+
return value;
|
|
1062
|
+
}
|
|
1063
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
1064
|
+
return 0n;
|
|
1065
|
+
}
|
|
1066
|
+
return BigInt(Math.trunc(value));
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function cloneStringMap(values: Record<string, string> | undefined): Record<string, string> {
|
|
1070
|
+
if (!values) {
|
|
1071
|
+
return {};
|
|
1072
|
+
}
|
|
1073
|
+
return { ...values };
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function cloneBytes(value: Uint8Array): Uint8Array {
|
|
1077
|
+
return new Uint8Array(value);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function toS3ConnectError(error: unknown, label: string): ConnectError {
|
|
1081
|
+
if (error instanceof ConnectError) {
|
|
1082
|
+
return error;
|
|
1083
|
+
}
|
|
1084
|
+
if (error instanceof S3NotFoundError) {
|
|
1085
|
+
return new ConnectError(error.message, Code.NotFound);
|
|
1086
|
+
}
|
|
1087
|
+
if (error instanceof S3PreconditionFailedError) {
|
|
1088
|
+
return new ConnectError(error.message, Code.FailedPrecondition);
|
|
1089
|
+
}
|
|
1090
|
+
if (error instanceof S3InvalidRangeError) {
|
|
1091
|
+
return new ConnectError(error.message, Code.OutOfRange);
|
|
1092
|
+
}
|
|
1093
|
+
return new ConnectError(`${label}: ${errorMessage(error)}`, Code.Unknown);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function mapS3RpcError(error: unknown): Error {
|
|
1097
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
1098
|
+
? (error as { code?: Code }).code
|
|
1099
|
+
: undefined;
|
|
1100
|
+
if (code === Code.NotFound) {
|
|
1101
|
+
return new S3NotFoundError(messageFromError(error));
|
|
1102
|
+
}
|
|
1103
|
+
if (code === Code.FailedPrecondition) {
|
|
1104
|
+
return new S3PreconditionFailedError(messageFromError(error));
|
|
1105
|
+
}
|
|
1106
|
+
if (code === Code.OutOfRange) {
|
|
1107
|
+
return new S3InvalidRangeError(messageFromError(error));
|
|
1108
|
+
}
|
|
1109
|
+
if (error instanceof Error) {
|
|
1110
|
+
return error;
|
|
1111
|
+
}
|
|
1112
|
+
return new Error(messageFromError(error));
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
async function s3Rpc<T>(fn: () => Promise<T>): Promise<T> {
|
|
1116
|
+
try {
|
|
1117
|
+
return await fn();
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
throw mapS3RpcError(error);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function messageFromError(error: unknown): string {
|
|
1124
|
+
if (error instanceof Error && error.message) {
|
|
1125
|
+
return error.message;
|
|
1126
|
+
}
|
|
1127
|
+
return errorMessage(error);
|
|
1128
|
+
}
|