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