s3kit 0.1.0 → 0.1.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 +15 -1
- package/dist/adapters/express.cjs +99 -3
- package/dist/adapters/express.cjs.map +1 -1
- package/dist/adapters/express.d.cts +2 -2
- package/dist/adapters/express.d.ts +2 -2
- package/dist/adapters/express.js +99 -3
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/fetch.cjs +99 -3
- package/dist/adapters/fetch.cjs.map +1 -1
- package/dist/adapters/fetch.d.cts +2 -2
- package/dist/adapters/fetch.d.ts +2 -2
- package/dist/adapters/fetch.js +99 -3
- package/dist/adapters/fetch.js.map +1 -1
- package/dist/adapters/next.cjs +386 -20
- package/dist/adapters/next.cjs.map +1 -1
- package/dist/adapters/next.d.cts +2 -2
- package/dist/adapters/next.d.ts +2 -2
- package/dist/adapters/next.js +387 -20
- package/dist/adapters/next.js.map +1 -1
- package/dist/client/index.cjs +15 -1
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +12 -2
- package/dist/client/index.d.ts +12 -2
- package/dist/client/index.js +15 -1
- package/dist/client/index.js.map +1 -1
- package/dist/core/index.cjs +300 -19
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +8 -3
- package/dist/core/index.d.ts +8 -3
- package/dist/core/index.js +299 -18
- package/dist/core/index.js.map +1 -1
- package/dist/http/index.cjs +99 -3
- package/dist/http/index.cjs.map +1 -1
- package/dist/http/index.d.cts +5 -2
- package/dist/http/index.d.ts +5 -2
- package/dist/http/index.js +99 -3
- package/dist/http/index.js.map +1 -1
- package/dist/index.cjs +403 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +403 -21
- package/dist/index.js.map +1 -1
- package/dist/{manager-BbmXpgXN.d.ts → manager-BtW1-sC0.d.ts} +11 -1
- package/dist/{manager-gIjo-t8h.d.cts → manager-DSsCYKEz.d.cts} +11 -1
- package/dist/react/index.cjs +334 -31
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +334 -31
- package/dist/react/index.js.map +1 -1
- package/dist/{types-g2IYvH3O.d.cts → types-B0yU5sod.d.cts} +51 -3
- package/dist/{types-g2IYvH3O.d.ts → types-B0yU5sod.d.ts} +51 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
CopyObjectCommand,
|
|
4
4
|
DeleteObjectCommand,
|
|
5
5
|
DeleteObjectsCommand,
|
|
6
|
+
HeadObjectCommand,
|
|
6
7
|
ListObjectsV2Command,
|
|
7
8
|
PutObjectCommand
|
|
8
9
|
} from "@aws-sdk/client-s3";
|
|
@@ -19,6 +20,15 @@ var S3FileManagerAuthorizationError = class extends Error {
|
|
|
19
20
|
this.code = code;
|
|
20
21
|
}
|
|
21
22
|
};
|
|
23
|
+
var S3FileManagerConflictError = class extends Error {
|
|
24
|
+
status;
|
|
25
|
+
code;
|
|
26
|
+
constructor(message, status = 409, code = "conflict") {
|
|
27
|
+
super(message);
|
|
28
|
+
this.status = status;
|
|
29
|
+
this.code = code;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
22
32
|
|
|
23
33
|
// src/core/manager.ts
|
|
24
34
|
var DEFAULT_DELIMITER = "/";
|
|
@@ -51,6 +61,60 @@ function isNoSuchKeyError(err) {
|
|
|
51
61
|
}
|
|
52
62
|
return false;
|
|
53
63
|
}
|
|
64
|
+
function isNotFoundError(err) {
|
|
65
|
+
if (!err || typeof err !== "object") return false;
|
|
66
|
+
if ("name" in err && (err.name === "NotFound" || err.name === "NoSuchKey")) return true;
|
|
67
|
+
if ("$metadata" in err) {
|
|
68
|
+
const meta = err.$metadata;
|
|
69
|
+
if (meta?.httpStatusCode === 404) return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
function isPreconditionFailedError(err) {
|
|
74
|
+
if (!err || typeof err !== "object") return false;
|
|
75
|
+
if ("name" in err && err.name === "PreconditionFailed") return true;
|
|
76
|
+
if ("Code" in err && err.Code === "PreconditionFailed") return true;
|
|
77
|
+
if ("$metadata" in err) {
|
|
78
|
+
const meta = err.$metadata;
|
|
79
|
+
if (meta?.httpStatusCode === 412) return true;
|
|
80
|
+
}
|
|
81
|
+
if ("message" in err && typeof err.message === "string") {
|
|
82
|
+
return err.message.includes("PreconditionFailed");
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
function resolveExpiresAt(expiresAt) {
|
|
87
|
+
if (expiresAt === null) return void 0;
|
|
88
|
+
if (!expiresAt) return void 0;
|
|
89
|
+
const parsed = new Date(expiresAt);
|
|
90
|
+
if (!Number.isFinite(parsed.getTime())) {
|
|
91
|
+
throw new Error("Invalid expiresAt value");
|
|
92
|
+
}
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|
|
95
|
+
function toAttributes(path, obj) {
|
|
96
|
+
const {
|
|
97
|
+
ContentLength,
|
|
98
|
+
LastModified,
|
|
99
|
+
ETag,
|
|
100
|
+
ContentType,
|
|
101
|
+
CacheControl,
|
|
102
|
+
ContentDisposition,
|
|
103
|
+
Metadata,
|
|
104
|
+
Expires
|
|
105
|
+
} = obj;
|
|
106
|
+
return {
|
|
107
|
+
path,
|
|
108
|
+
...ContentLength !== void 0 ? { size: ContentLength } : {},
|
|
109
|
+
...LastModified ? { lastModified: LastModified.toISOString() } : {},
|
|
110
|
+
...ETag !== void 0 ? { etag: ETag } : {},
|
|
111
|
+
...ContentType ? { contentType: ContentType } : {},
|
|
112
|
+
...CacheControl ? { cacheControl: CacheControl } : {},
|
|
113
|
+
...ContentDisposition ? { contentDisposition: ContentDisposition } : {},
|
|
114
|
+
...Metadata ? { metadata: Metadata } : {},
|
|
115
|
+
...Expires ? { expiresAt: Expires.toISOString() } : {}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
54
118
|
async function* listAllKeys(s3, bucket, prefix) {
|
|
55
119
|
let cursor;
|
|
56
120
|
while (true) {
|
|
@@ -90,6 +154,9 @@ var S3FileManager = class {
|
|
|
90
154
|
delimiter;
|
|
91
155
|
hooks;
|
|
92
156
|
authorizationMode;
|
|
157
|
+
lockFolderMoves;
|
|
158
|
+
lockPrefix;
|
|
159
|
+
lockTtlSeconds;
|
|
93
160
|
constructor(s3, options) {
|
|
94
161
|
this.s3 = s3;
|
|
95
162
|
this.bucket = options.bucket;
|
|
@@ -97,6 +164,9 @@ var S3FileManager = class {
|
|
|
97
164
|
this.rootPrefix = ensureTrailingDelimiter(trimSlashes(options.rootPrefix ?? ""), this.delimiter);
|
|
98
165
|
this.hooks = options.hooks;
|
|
99
166
|
this.authorizationMode = options.authorizationMode ?? "deny-by-default";
|
|
167
|
+
this.lockFolderMoves = options.lockFolderMoves ?? true;
|
|
168
|
+
this.lockPrefix = options.lockPrefix ?? ".s3kit/locks";
|
|
169
|
+
this.lockTtlSeconds = options.lockTtlSeconds ?? 60 * 15;
|
|
100
170
|
}
|
|
101
171
|
async authorize(args) {
|
|
102
172
|
const hasAuthHooks = Boolean(this.hooks?.authorize || this.hooks?.allowAction);
|
|
@@ -130,6 +200,87 @@ var S3FileManager = class {
|
|
|
130
200
|
}
|
|
131
201
|
return key.slice(this.rootPrefix.length);
|
|
132
202
|
}
|
|
203
|
+
lockKeyForPath(path) {
|
|
204
|
+
const safe = encodeURIComponent(path).replace(/%2F/g, "__");
|
|
205
|
+
const prefix = ensureTrailingDelimiter(trimSlashes(this.lockPrefix), this.delimiter);
|
|
206
|
+
return `${this.rootPrefix}${prefix}${safe}.json`;
|
|
207
|
+
}
|
|
208
|
+
async readFolderLockByKey(key) {
|
|
209
|
+
try {
|
|
210
|
+
const out = await this.s3.send(
|
|
211
|
+
new HeadObjectCommand({
|
|
212
|
+
Bucket: this.bucket,
|
|
213
|
+
Key: key
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
const meta = out.Metadata ?? {};
|
|
217
|
+
const operation = meta.op === "folder.move" ? "folder.move" : void 0;
|
|
218
|
+
const fromPath = meta.from;
|
|
219
|
+
const toPath = meta.to;
|
|
220
|
+
const startedAt = meta.startedat;
|
|
221
|
+
const expiresAt = meta.expiresat ?? (out.Expires ? out.Expires.toISOString() : void 0);
|
|
222
|
+
if (!operation || !fromPath || !toPath || !startedAt || !expiresAt) return null;
|
|
223
|
+
return {
|
|
224
|
+
path: fromPath,
|
|
225
|
+
operation,
|
|
226
|
+
fromPath,
|
|
227
|
+
toPath,
|
|
228
|
+
startedAt,
|
|
229
|
+
expiresAt,
|
|
230
|
+
...meta.owner ? { owner: meta.owner } : {}
|
|
231
|
+
};
|
|
232
|
+
} catch (err) {
|
|
233
|
+
if (isNotFoundError(err)) return null;
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async acquireFolderMoveLock(fromPath, toPath, ctx) {
|
|
238
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
239
|
+
const expiresAt = new Date(Date.now() + this.lockTtlSeconds * 1e3).toISOString();
|
|
240
|
+
const key = this.lockKeyForPath(fromPath);
|
|
241
|
+
const writeLock = async () => {
|
|
242
|
+
await this.s3.send(
|
|
243
|
+
new PutObjectCommand({
|
|
244
|
+
Bucket: this.bucket,
|
|
245
|
+
Key: key,
|
|
246
|
+
ContentType: "application/json",
|
|
247
|
+
Expires: new Date(expiresAt),
|
|
248
|
+
Metadata: {
|
|
249
|
+
op: "folder.move",
|
|
250
|
+
from: fromPath,
|
|
251
|
+
to: toPath,
|
|
252
|
+
startedat: startedAt,
|
|
253
|
+
expiresat: expiresAt,
|
|
254
|
+
...ctx.userId ? { owner: String(ctx.userId) } : {}
|
|
255
|
+
},
|
|
256
|
+
IfNoneMatch: "*",
|
|
257
|
+
Body: ""
|
|
258
|
+
})
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
try {
|
|
262
|
+
await writeLock();
|
|
263
|
+
return key;
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (!isPreconditionFailedError(err)) throw err;
|
|
266
|
+
const existing = await this.readFolderLockByKey(key);
|
|
267
|
+
if (existing?.expiresAt) {
|
|
268
|
+
const exp = new Date(existing.expiresAt);
|
|
269
|
+
if (Number.isFinite(exp.getTime()) && exp.getTime() <= Date.now()) {
|
|
270
|
+
await this.s3.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
271
|
+
await writeLock();
|
|
272
|
+
return key;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
throw new S3FileManagerConflictError("Folder rename already in progress");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async releaseFolderMoveLock(key) {
|
|
279
|
+
try {
|
|
280
|
+
await this.s3.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
281
|
+
} catch {
|
|
282
|
+
}
|
|
283
|
+
}
|
|
133
284
|
makeFolderEntry(path) {
|
|
134
285
|
const p = normalizePath(path);
|
|
135
286
|
const name = p === "" ? "" : p.split("/").at(-1) ?? "";
|
|
@@ -231,12 +382,39 @@ var S3FileManager = class {
|
|
|
231
382
|
await deleteKeysInBatches(this.s3, this.bucket, keys);
|
|
232
383
|
}
|
|
233
384
|
async deleteFiles(options, ctx = {}) {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
385
|
+
const items = options.items ?? options.paths?.map((path) => ({ path }));
|
|
386
|
+
if (!items || items.length === 0) return;
|
|
387
|
+
const normalizedItems = items.map((item) => ({
|
|
388
|
+
path: normalizePath(item.path),
|
|
389
|
+
ifMatch: item.ifMatch,
|
|
390
|
+
ifNoneMatch: item.ifNoneMatch
|
|
391
|
+
}));
|
|
392
|
+
for (const item of normalizedItems) {
|
|
393
|
+
await this.authorize({ action: "file.delete", path: item.path, ctx });
|
|
394
|
+
}
|
|
395
|
+
const hasConditions = normalizedItems.some((item) => item.ifMatch || item.ifNoneMatch);
|
|
396
|
+
if (!hasConditions) {
|
|
397
|
+
const keys = normalizedItems.map((item) => this.pathToKey(item.path));
|
|
398
|
+
await deleteKeysInBatches(this.s3, this.bucket, keys);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
for (const item of normalizedItems) {
|
|
402
|
+
try {
|
|
403
|
+
await this.s3.send(
|
|
404
|
+
new DeleteObjectCommand({
|
|
405
|
+
Bucket: this.bucket,
|
|
406
|
+
Key: this.pathToKey(item.path),
|
|
407
|
+
...item.ifMatch ? { IfMatch: item.ifMatch } : {},
|
|
408
|
+
...item.ifNoneMatch ? { IfNoneMatch: item.ifNoneMatch } : {}
|
|
409
|
+
})
|
|
410
|
+
);
|
|
411
|
+
} catch (err) {
|
|
412
|
+
if (isPreconditionFailedError(err)) {
|
|
413
|
+
throw new S3FileManagerConflictError("Delete conflict");
|
|
414
|
+
}
|
|
415
|
+
throw err;
|
|
416
|
+
}
|
|
237
417
|
}
|
|
238
|
-
const keys = paths.map((p) => this.pathToKey(p));
|
|
239
|
-
await deleteKeysInBatches(this.s3, this.bucket, keys);
|
|
240
418
|
}
|
|
241
419
|
async copy(options, ctx = {}) {
|
|
242
420
|
const isFolder = options.fromPath.endsWith(this.delimiter);
|
|
@@ -286,13 +464,21 @@ var S3FileManager = class {
|
|
|
286
464
|
await this.authorize({ action: "file.copy", fromPath, toPath, ctx });
|
|
287
465
|
const fromKey = this.pathToKey(fromPath);
|
|
288
466
|
const toKey = this.pathToKey(toPath);
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
467
|
+
try {
|
|
468
|
+
await this.s3.send(
|
|
469
|
+
new CopyObjectCommand({
|
|
470
|
+
Bucket: this.bucket,
|
|
471
|
+
Key: toKey,
|
|
472
|
+
CopySource: encodeS3CopySource(this.bucket, fromKey),
|
|
473
|
+
...options.ifMatch ? { CopySourceIfMatch: options.ifMatch } : {}
|
|
474
|
+
})
|
|
475
|
+
);
|
|
476
|
+
} catch (err) {
|
|
477
|
+
if (isPreconditionFailedError(err)) {
|
|
478
|
+
throw new S3FileManagerConflictError("Copy conflict");
|
|
479
|
+
}
|
|
480
|
+
throw err;
|
|
481
|
+
}
|
|
296
482
|
}
|
|
297
483
|
async move(options, ctx = {}) {
|
|
298
484
|
const isFolder = options.fromPath.endsWith(this.delimiter);
|
|
@@ -302,19 +488,49 @@ var S3FileManager = class {
|
|
|
302
488
|
if (isFolder) {
|
|
303
489
|
const fromPathWithSlash = ensureTrailingDelimiter(fromPath, this.delimiter);
|
|
304
490
|
const toPathWithSlash = ensureTrailingDelimiter(toPath, this.delimiter);
|
|
491
|
+
let lockKey = null;
|
|
305
492
|
await this.authorize({
|
|
306
493
|
action: "folder.move",
|
|
307
494
|
fromPath: fromPathWithSlash,
|
|
308
495
|
toPath: toPathWithSlash,
|
|
309
496
|
ctx
|
|
310
497
|
});
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
498
|
+
try {
|
|
499
|
+
if (this.lockFolderMoves) {
|
|
500
|
+
lockKey = await this.acquireFolderMoveLock(fromPathWithSlash, toPathWithSlash, ctx);
|
|
501
|
+
}
|
|
502
|
+
await this.copy(options, ctx);
|
|
503
|
+
await this.deleteFolder({ path: fromPathWithSlash, recursive: true }, ctx);
|
|
504
|
+
return;
|
|
505
|
+
} finally {
|
|
506
|
+
if (lockKey) {
|
|
507
|
+
await this.releaseFolderMoveLock(lockKey);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
314
510
|
}
|
|
315
511
|
await this.authorize({ action: "file.move", fromPath, toPath, ctx });
|
|
316
512
|
await this.copy(options, ctx);
|
|
317
|
-
|
|
513
|
+
try {
|
|
514
|
+
await this.deleteFiles(
|
|
515
|
+
{
|
|
516
|
+
items: [
|
|
517
|
+
{
|
|
518
|
+
path: fromPath,
|
|
519
|
+
...options.ifMatch ? { ifMatch: options.ifMatch } : {}
|
|
520
|
+
}
|
|
521
|
+
]
|
|
522
|
+
},
|
|
523
|
+
ctx
|
|
524
|
+
);
|
|
525
|
+
} catch (err) {
|
|
526
|
+
if (err instanceof S3FileManagerConflictError) {
|
|
527
|
+
try {
|
|
528
|
+
await this.deleteFiles({ paths: [toPath] }, ctx);
|
|
529
|
+
} catch {
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
throw err;
|
|
533
|
+
}
|
|
318
534
|
}
|
|
319
535
|
async prepareUploads(options, ctx = {}) {
|
|
320
536
|
const expiresIn = options.expiresInSeconds ?? 60 * 5;
|
|
@@ -323,19 +539,24 @@ var S3FileManager = class {
|
|
|
323
539
|
const path = normalizePath(item.path);
|
|
324
540
|
await this.authorize({ action: "upload.prepare", path, ctx });
|
|
325
541
|
const key = this.pathToKey(path);
|
|
542
|
+
const expiresAt = resolveExpiresAt(item.expiresAt);
|
|
326
543
|
const cmd = new PutObjectCommand({
|
|
327
544
|
Bucket: this.bucket,
|
|
328
545
|
Key: key,
|
|
329
546
|
ContentType: item.contentType,
|
|
330
547
|
CacheControl: item.cacheControl,
|
|
331
548
|
ContentDisposition: item.contentDisposition,
|
|
332
|
-
Metadata: item.metadata
|
|
549
|
+
Metadata: item.metadata,
|
|
550
|
+
...expiresAt ? { Expires: expiresAt } : {},
|
|
551
|
+
...item.ifNoneMatch ? { IfNoneMatch: item.ifNoneMatch } : {}
|
|
333
552
|
});
|
|
334
553
|
const url = await getSignedUrl(this.s3, cmd, { expiresIn });
|
|
335
554
|
const headers = {};
|
|
336
555
|
if (item.contentType) headers["Content-Type"] = item.contentType;
|
|
337
556
|
if (item.cacheControl) headers["Cache-Control"] = item.cacheControl;
|
|
338
557
|
if (item.contentDisposition) headers["Content-Disposition"] = item.contentDisposition;
|
|
558
|
+
if (expiresAt) headers.Expires = expiresAt.toUTCString();
|
|
559
|
+
if (item.ifNoneMatch) headers["If-None-Match"] = item.ifNoneMatch;
|
|
339
560
|
if (item.metadata) {
|
|
340
561
|
for (const [k, v] of Object.entries(item.metadata)) {
|
|
341
562
|
headers[`x-amz-meta-${k}`] = v;
|
|
@@ -350,6 +571,59 @@ var S3FileManager = class {
|
|
|
350
571
|
}
|
|
351
572
|
return result;
|
|
352
573
|
}
|
|
574
|
+
async getFileAttributes(options, ctx = {}) {
|
|
575
|
+
const path = normalizePath(options.path);
|
|
576
|
+
await this.authorize({ action: "file.attributes.get", path, ctx });
|
|
577
|
+
const key = this.pathToKey(path);
|
|
578
|
+
const out = await this.s3.send(
|
|
579
|
+
new HeadObjectCommand({
|
|
580
|
+
Bucket: this.bucket,
|
|
581
|
+
Key: key
|
|
582
|
+
})
|
|
583
|
+
);
|
|
584
|
+
return toAttributes(path, out);
|
|
585
|
+
}
|
|
586
|
+
async setFileAttributes(options, ctx = {}) {
|
|
587
|
+
const path = normalizePath(options.path);
|
|
588
|
+
await this.authorize({ action: "file.attributes.set", path, ctx });
|
|
589
|
+
const key = this.pathToKey(path);
|
|
590
|
+
const current = await this.s3.send(
|
|
591
|
+
new HeadObjectCommand({
|
|
592
|
+
Bucket: this.bucket,
|
|
593
|
+
Key: key
|
|
594
|
+
})
|
|
595
|
+
);
|
|
596
|
+
const baseMetadata = options.metadata ?? (current.Metadata ? { ...current.Metadata } : {});
|
|
597
|
+
const resolvedExpires = options.expiresAt === void 0 ? current.Expires : resolveExpiresAt(options.expiresAt);
|
|
598
|
+
try {
|
|
599
|
+
await this.s3.send(
|
|
600
|
+
new CopyObjectCommand({
|
|
601
|
+
Bucket: this.bucket,
|
|
602
|
+
Key: key,
|
|
603
|
+
CopySource: encodeS3CopySource(this.bucket, key),
|
|
604
|
+
MetadataDirective: "REPLACE",
|
|
605
|
+
ContentType: options.contentType ?? current.ContentType,
|
|
606
|
+
CacheControl: options.cacheControl ?? current.CacheControl,
|
|
607
|
+
ContentDisposition: options.contentDisposition ?? current.ContentDisposition,
|
|
608
|
+
Metadata: baseMetadata,
|
|
609
|
+
...resolvedExpires ? { Expires: resolvedExpires } : {},
|
|
610
|
+
...options.ifMatch ? { CopySourceIfMatch: options.ifMatch } : {}
|
|
611
|
+
})
|
|
612
|
+
);
|
|
613
|
+
} catch (err) {
|
|
614
|
+
if (isPreconditionFailedError(err)) {
|
|
615
|
+
throw new S3FileManagerConflictError("Attribute update conflict");
|
|
616
|
+
}
|
|
617
|
+
throw err;
|
|
618
|
+
}
|
|
619
|
+
const updated = await this.s3.send(
|
|
620
|
+
new HeadObjectCommand({
|
|
621
|
+
Bucket: this.bucket,
|
|
622
|
+
Key: key
|
|
623
|
+
})
|
|
624
|
+
);
|
|
625
|
+
return toAttributes(path, updated);
|
|
626
|
+
}
|
|
353
627
|
async search(options, ctx = {}) {
|
|
354
628
|
const query = options.query.toLowerCase().trim();
|
|
355
629
|
if (!query) {
|
|
@@ -422,6 +696,12 @@ var S3FileManager = class {
|
|
|
422
696
|
const expiresAt = new Date(Date.now() + expiresIn * 1e3).toISOString();
|
|
423
697
|
return { path, url, expiresAt };
|
|
424
698
|
}
|
|
699
|
+
async getFolderLock(options, ctx = {}) {
|
|
700
|
+
const path = ensureTrailingDelimiter(normalizePath(options.path), this.delimiter);
|
|
701
|
+
await this.authorize({ action: "folder.lock.get", path, ctx });
|
|
702
|
+
const key = this.lockKeyForPath(path);
|
|
703
|
+
return await this.readFolderLockByKey(key);
|
|
704
|
+
}
|
|
425
705
|
};
|
|
426
706
|
|
|
427
707
|
// src/http/handler.ts
|
|
@@ -461,6 +741,21 @@ function optionalString(value, key) {
|
|
|
461
741
|
if (typeof value === "string") return value;
|
|
462
742
|
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a string`);
|
|
463
743
|
}
|
|
744
|
+
function optionalStringOrNull(value, key) {
|
|
745
|
+
if (value === void 0) return void 0;
|
|
746
|
+
if (value === null) return null;
|
|
747
|
+
if (typeof value === "string") return value;
|
|
748
|
+
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a string`);
|
|
749
|
+
}
|
|
750
|
+
function optionalDateStringOrNull(value, key) {
|
|
751
|
+
const raw = optionalStringOrNull(value, key);
|
|
752
|
+
if (raw === void 0 || raw === null) return raw;
|
|
753
|
+
const parsed = new Date(raw);
|
|
754
|
+
if (!Number.isFinite(parsed.getTime())) {
|
|
755
|
+
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a date string`);
|
|
756
|
+
}
|
|
757
|
+
return raw;
|
|
758
|
+
}
|
|
464
759
|
function requiredString(value, key) {
|
|
465
760
|
if (typeof value === "string") return value;
|
|
466
761
|
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a string`);
|
|
@@ -543,13 +838,38 @@ function parseDeleteFolderOptions(body) {
|
|
|
543
838
|
}
|
|
544
839
|
function parseDeleteFilesOptions(body) {
|
|
545
840
|
const obj = ensureObject(body);
|
|
546
|
-
|
|
841
|
+
const itemsValue = obj.items;
|
|
842
|
+
const items = Array.isArray(itemsValue) ? itemsValue.map((raw, idx) => {
|
|
843
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
844
|
+
throw new S3FileManagerHttpError(
|
|
845
|
+
400,
|
|
846
|
+
"invalid_body",
|
|
847
|
+
`Expected 'items[${idx}]' to be an object`
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
const item = raw;
|
|
851
|
+
return {
|
|
852
|
+
path: requiredString(item.path, `items[${idx}].path`),
|
|
853
|
+
ifMatch: optionalString(item.ifMatch, `items[${idx}].ifMatch`),
|
|
854
|
+
ifNoneMatch: optionalString(item.ifNoneMatch, `items[${idx}].ifNoneMatch`)
|
|
855
|
+
};
|
|
856
|
+
}) : void 0;
|
|
857
|
+
const paths = obj.paths !== void 0 ? requiredStringArray(obj.paths, "paths") : void 0;
|
|
858
|
+
if (!paths && !items) {
|
|
859
|
+
throw new S3FileManagerHttpError(
|
|
860
|
+
400,
|
|
861
|
+
"invalid_body",
|
|
862
|
+
"Expected 'paths' or 'items' to be provided"
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
return { ...paths ? { paths } : {}, ...items ? { items } : {} };
|
|
547
866
|
}
|
|
548
867
|
function parseCopyMoveOptions(body) {
|
|
549
868
|
const obj = ensureObject(body);
|
|
550
869
|
return {
|
|
551
870
|
fromPath: requiredString(obj.fromPath, "fromPath"),
|
|
552
|
-
toPath: requiredString(obj.toPath, "toPath")
|
|
871
|
+
toPath: requiredString(obj.toPath, "toPath"),
|
|
872
|
+
ifMatch: optionalString(obj.ifMatch, "ifMatch")
|
|
553
873
|
};
|
|
554
874
|
}
|
|
555
875
|
function parsePrepareUploadsOptions(body) {
|
|
@@ -575,7 +895,9 @@ function parsePrepareUploadsOptions(body) {
|
|
|
575
895
|
item.contentDisposition,
|
|
576
896
|
`items[${idx}].contentDisposition`
|
|
577
897
|
),
|
|
578
|
-
metadata: optionalStringRecord(item.metadata, `items[${idx}].metadata`)
|
|
898
|
+
metadata: optionalStringRecord(item.metadata, `items[${idx}].metadata`),
|
|
899
|
+
expiresAt: optionalDateStringOrNull(item.expiresAt, `items[${idx}].expiresAt`),
|
|
900
|
+
ifNoneMatch: optionalString(item.ifNoneMatch, `items[${idx}].ifNoneMatch`)
|
|
579
901
|
};
|
|
580
902
|
});
|
|
581
903
|
return {
|
|
@@ -591,6 +913,30 @@ function parsePreviewOptions(body) {
|
|
|
591
913
|
inline: optionalBoolean(obj.inline, "inline")
|
|
592
914
|
};
|
|
593
915
|
}
|
|
916
|
+
function parseGetFolderLockOptions(body) {
|
|
917
|
+
const obj = ensureObject(body);
|
|
918
|
+
return {
|
|
919
|
+
path: requiredString(obj.path, "path")
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
function parseGetFileAttributesOptions(body) {
|
|
923
|
+
const obj = ensureObject(body);
|
|
924
|
+
return {
|
|
925
|
+
path: requiredString(obj.path, "path")
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
function parseSetFileAttributesOptions(body) {
|
|
929
|
+
const obj = ensureObject(body);
|
|
930
|
+
return {
|
|
931
|
+
path: requiredString(obj.path, "path"),
|
|
932
|
+
contentType: optionalString(obj.contentType, "contentType"),
|
|
933
|
+
cacheControl: optionalString(obj.cacheControl, "cacheControl"),
|
|
934
|
+
contentDisposition: optionalString(obj.contentDisposition, "contentDisposition"),
|
|
935
|
+
metadata: optionalStringRecord(obj.metadata, "metadata"),
|
|
936
|
+
expiresAt: optionalDateStringOrNull(obj.expiresAt, "expiresAt"),
|
|
937
|
+
ifMatch: optionalString(obj.ifMatch, "ifMatch")
|
|
938
|
+
};
|
|
939
|
+
}
|
|
594
940
|
function createS3FileManagerHttpHandler(options) {
|
|
595
941
|
const basePath = normalizeBasePath(options.api?.basePath);
|
|
596
942
|
if (!options.manager && !options.getManager) {
|
|
@@ -638,6 +984,24 @@ function createS3FileManagerHttpHandler(options) {
|
|
|
638
984
|
const out = await manager.getPreviewUrl(parsePreviewOptions(req.body), ctx);
|
|
639
985
|
return { status: 200, headers: { "content-type": "application/json" }, body: out };
|
|
640
986
|
}
|
|
987
|
+
if (method === "POST" && path === "/folder/lock/get") {
|
|
988
|
+
const out = await manager.getFolderLock(parseGetFolderLockOptions(req.body), ctx);
|
|
989
|
+
return { status: 200, headers: { "content-type": "application/json" }, body: out };
|
|
990
|
+
}
|
|
991
|
+
if (method === "POST" && path === "/file/attributes/get") {
|
|
992
|
+
const out = await manager.getFileAttributes(
|
|
993
|
+
parseGetFileAttributesOptions(req.body),
|
|
994
|
+
ctx
|
|
995
|
+
);
|
|
996
|
+
return { status: 200, headers: { "content-type": "application/json" }, body: out };
|
|
997
|
+
}
|
|
998
|
+
if (method === "POST" && path === "/file/attributes/set") {
|
|
999
|
+
const out = await manager.setFileAttributes(
|
|
1000
|
+
parseSetFileAttributesOptions(req.body),
|
|
1001
|
+
ctx
|
|
1002
|
+
);
|
|
1003
|
+
return { status: 200, headers: { "content-type": "application/json" }, body: out };
|
|
1004
|
+
}
|
|
641
1005
|
return jsonError(404, "not_found", "Route not found");
|
|
642
1006
|
} catch (err) {
|
|
643
1007
|
if (err instanceof S3FileManagerHttpError) {
|
|
@@ -646,6 +1010,9 @@ function createS3FileManagerHttpHandler(options) {
|
|
|
646
1010
|
if (err instanceof S3FileManagerAuthorizationError) {
|
|
647
1011
|
return jsonError(err.status, err.code, err.message);
|
|
648
1012
|
}
|
|
1013
|
+
if (err instanceof S3FileManagerConflictError) {
|
|
1014
|
+
return jsonError(err.status, err.code, err.message);
|
|
1015
|
+
}
|
|
649
1016
|
console.error("[S3FileManager Error]", err);
|
|
650
1017
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
651
1018
|
return jsonError(500, "internal_error", message);
|
|
@@ -748,11 +1115,25 @@ var S3FileManagerClient = class {
|
|
|
748
1115
|
getPreviewUrl(options) {
|
|
749
1116
|
return fetchJson(this.f, this.endpoint("/preview"), options);
|
|
750
1117
|
}
|
|
1118
|
+
getFolderLock(options) {
|
|
1119
|
+
return fetchJson(this.f, this.endpoint("/folder/lock/get"), options);
|
|
1120
|
+
}
|
|
1121
|
+
getFileAttributes(options) {
|
|
1122
|
+
return fetchJson(this.f, this.endpoint("/file/attributes/get"), options);
|
|
1123
|
+
}
|
|
1124
|
+
setFileAttributes(options) {
|
|
1125
|
+
return fetchJson(this.f, this.endpoint("/file/attributes/set"), options);
|
|
1126
|
+
}
|
|
751
1127
|
async uploadFiles(args) {
|
|
752
1128
|
const prepare = {
|
|
753
1129
|
items: args.files.map((f) => ({
|
|
754
1130
|
path: f.path,
|
|
755
|
-
contentType: f.contentType ?? f.file.type
|
|
1131
|
+
contentType: f.contentType ?? f.file.type,
|
|
1132
|
+
...f.cacheControl !== void 0 ? { cacheControl: f.cacheControl } : {},
|
|
1133
|
+
...f.contentDisposition !== void 0 ? { contentDisposition: f.contentDisposition } : {},
|
|
1134
|
+
...f.metadata !== void 0 ? { metadata: f.metadata } : {},
|
|
1135
|
+
...f.expiresAt !== void 0 ? { expiresAt: f.expiresAt } : {},
|
|
1136
|
+
...f.ifNoneMatch !== void 0 ? { ifNoneMatch: f.ifNoneMatch } : {}
|
|
756
1137
|
})),
|
|
757
1138
|
...args.expiresInSeconds !== void 0 ? { expiresInSeconds: args.expiresInSeconds } : {}
|
|
758
1139
|
};
|
|
@@ -779,6 +1160,7 @@ export {
|
|
|
779
1160
|
S3FileManager,
|
|
780
1161
|
S3FileManagerAuthorizationError,
|
|
781
1162
|
S3FileManagerClient,
|
|
1163
|
+
S3FileManagerConflictError,
|
|
782
1164
|
createS3FileManagerHttpHandler
|
|
783
1165
|
};
|
|
784
1166
|
//# sourceMappingURL=index.js.map
|