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.cjs
CHANGED
|
@@ -42,6 +42,15 @@ var S3FileManagerAuthorizationError = class extends Error {
|
|
|
42
42
|
this.code = code;
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
|
+
var S3FileManagerConflictError = class extends Error {
|
|
46
|
+
status;
|
|
47
|
+
code;
|
|
48
|
+
constructor(message, status = 409, code = "conflict") {
|
|
49
|
+
super(message);
|
|
50
|
+
this.status = status;
|
|
51
|
+
this.code = code;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
45
54
|
|
|
46
55
|
// src/core/manager.ts
|
|
47
56
|
var DEFAULT_DELIMITER = "/";
|
|
@@ -74,6 +83,60 @@ function isNoSuchKeyError(err) {
|
|
|
74
83
|
}
|
|
75
84
|
return false;
|
|
76
85
|
}
|
|
86
|
+
function isNotFoundError(err) {
|
|
87
|
+
if (!err || typeof err !== "object") return false;
|
|
88
|
+
if ("name" in err && (err.name === "NotFound" || err.name === "NoSuchKey")) return true;
|
|
89
|
+
if ("$metadata" in err) {
|
|
90
|
+
const meta = err.$metadata;
|
|
91
|
+
if (meta?.httpStatusCode === 404) return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
function isPreconditionFailedError(err) {
|
|
96
|
+
if (!err || typeof err !== "object") return false;
|
|
97
|
+
if ("name" in err && err.name === "PreconditionFailed") return true;
|
|
98
|
+
if ("Code" in err && err.Code === "PreconditionFailed") return true;
|
|
99
|
+
if ("$metadata" in err) {
|
|
100
|
+
const meta = err.$metadata;
|
|
101
|
+
if (meta?.httpStatusCode === 412) return true;
|
|
102
|
+
}
|
|
103
|
+
if ("message" in err && typeof err.message === "string") {
|
|
104
|
+
return err.message.includes("PreconditionFailed");
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
function resolveExpiresAt(expiresAt) {
|
|
109
|
+
if (expiresAt === null) return void 0;
|
|
110
|
+
if (!expiresAt) return void 0;
|
|
111
|
+
const parsed = new Date(expiresAt);
|
|
112
|
+
if (!Number.isFinite(parsed.getTime())) {
|
|
113
|
+
throw new Error("Invalid expiresAt value");
|
|
114
|
+
}
|
|
115
|
+
return parsed;
|
|
116
|
+
}
|
|
117
|
+
function toAttributes(path, obj) {
|
|
118
|
+
const {
|
|
119
|
+
ContentLength,
|
|
120
|
+
LastModified,
|
|
121
|
+
ETag,
|
|
122
|
+
ContentType,
|
|
123
|
+
CacheControl,
|
|
124
|
+
ContentDisposition,
|
|
125
|
+
Metadata,
|
|
126
|
+
Expires
|
|
127
|
+
} = obj;
|
|
128
|
+
return {
|
|
129
|
+
path,
|
|
130
|
+
...ContentLength !== void 0 ? { size: ContentLength } : {},
|
|
131
|
+
...LastModified ? { lastModified: LastModified.toISOString() } : {},
|
|
132
|
+
...ETag !== void 0 ? { etag: ETag } : {},
|
|
133
|
+
...ContentType ? { contentType: ContentType } : {},
|
|
134
|
+
...CacheControl ? { cacheControl: CacheControl } : {},
|
|
135
|
+
...ContentDisposition ? { contentDisposition: ContentDisposition } : {},
|
|
136
|
+
...Metadata ? { metadata: Metadata } : {},
|
|
137
|
+
...Expires ? { expiresAt: Expires.toISOString() } : {}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
77
140
|
async function* listAllKeys(s3, bucket, prefix) {
|
|
78
141
|
let cursor;
|
|
79
142
|
while (true) {
|
|
@@ -113,6 +176,9 @@ var S3FileManager = class {
|
|
|
113
176
|
delimiter;
|
|
114
177
|
hooks;
|
|
115
178
|
authorizationMode;
|
|
179
|
+
lockFolderMoves;
|
|
180
|
+
lockPrefix;
|
|
181
|
+
lockTtlSeconds;
|
|
116
182
|
constructor(s3, options) {
|
|
117
183
|
this.s3 = s3;
|
|
118
184
|
this.bucket = options.bucket;
|
|
@@ -120,6 +186,9 @@ var S3FileManager = class {
|
|
|
120
186
|
this.rootPrefix = ensureTrailingDelimiter(trimSlashes(options.rootPrefix ?? ""), this.delimiter);
|
|
121
187
|
this.hooks = options.hooks;
|
|
122
188
|
this.authorizationMode = options.authorizationMode ?? "deny-by-default";
|
|
189
|
+
this.lockFolderMoves = options.lockFolderMoves ?? true;
|
|
190
|
+
this.lockPrefix = options.lockPrefix ?? ".s3kit/locks";
|
|
191
|
+
this.lockTtlSeconds = options.lockTtlSeconds ?? 60 * 15;
|
|
123
192
|
}
|
|
124
193
|
async authorize(args) {
|
|
125
194
|
const hasAuthHooks = Boolean(this.hooks?.authorize || this.hooks?.allowAction);
|
|
@@ -153,6 +222,87 @@ var S3FileManager = class {
|
|
|
153
222
|
}
|
|
154
223
|
return key.slice(this.rootPrefix.length);
|
|
155
224
|
}
|
|
225
|
+
lockKeyForPath(path) {
|
|
226
|
+
const safe = encodeURIComponent(path).replace(/%2F/g, "__");
|
|
227
|
+
const prefix = ensureTrailingDelimiter(trimSlashes(this.lockPrefix), this.delimiter);
|
|
228
|
+
return `${this.rootPrefix}${prefix}${safe}.json`;
|
|
229
|
+
}
|
|
230
|
+
async readFolderLockByKey(key) {
|
|
231
|
+
try {
|
|
232
|
+
const out = await this.s3.send(
|
|
233
|
+
new import_client_s3.HeadObjectCommand({
|
|
234
|
+
Bucket: this.bucket,
|
|
235
|
+
Key: key
|
|
236
|
+
})
|
|
237
|
+
);
|
|
238
|
+
const meta = out.Metadata ?? {};
|
|
239
|
+
const operation = meta.op === "folder.move" ? "folder.move" : void 0;
|
|
240
|
+
const fromPath = meta.from;
|
|
241
|
+
const toPath = meta.to;
|
|
242
|
+
const startedAt = meta.startedat;
|
|
243
|
+
const expiresAt = meta.expiresat ?? (out.Expires ? out.Expires.toISOString() : void 0);
|
|
244
|
+
if (!operation || !fromPath || !toPath || !startedAt || !expiresAt) return null;
|
|
245
|
+
return {
|
|
246
|
+
path: fromPath,
|
|
247
|
+
operation,
|
|
248
|
+
fromPath,
|
|
249
|
+
toPath,
|
|
250
|
+
startedAt,
|
|
251
|
+
expiresAt,
|
|
252
|
+
...meta.owner ? { owner: meta.owner } : {}
|
|
253
|
+
};
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if (isNotFoundError(err)) return null;
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async acquireFolderMoveLock(fromPath, toPath, ctx) {
|
|
260
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
261
|
+
const expiresAt = new Date(Date.now() + this.lockTtlSeconds * 1e3).toISOString();
|
|
262
|
+
const key = this.lockKeyForPath(fromPath);
|
|
263
|
+
const writeLock = async () => {
|
|
264
|
+
await this.s3.send(
|
|
265
|
+
new import_client_s3.PutObjectCommand({
|
|
266
|
+
Bucket: this.bucket,
|
|
267
|
+
Key: key,
|
|
268
|
+
ContentType: "application/json",
|
|
269
|
+
Expires: new Date(expiresAt),
|
|
270
|
+
Metadata: {
|
|
271
|
+
op: "folder.move",
|
|
272
|
+
from: fromPath,
|
|
273
|
+
to: toPath,
|
|
274
|
+
startedat: startedAt,
|
|
275
|
+
expiresat: expiresAt,
|
|
276
|
+
...ctx.userId ? { owner: String(ctx.userId) } : {}
|
|
277
|
+
},
|
|
278
|
+
IfNoneMatch: "*",
|
|
279
|
+
Body: ""
|
|
280
|
+
})
|
|
281
|
+
);
|
|
282
|
+
};
|
|
283
|
+
try {
|
|
284
|
+
await writeLock();
|
|
285
|
+
return key;
|
|
286
|
+
} catch (err) {
|
|
287
|
+
if (!isPreconditionFailedError(err)) throw err;
|
|
288
|
+
const existing = await this.readFolderLockByKey(key);
|
|
289
|
+
if (existing?.expiresAt) {
|
|
290
|
+
const exp = new Date(existing.expiresAt);
|
|
291
|
+
if (Number.isFinite(exp.getTime()) && exp.getTime() <= Date.now()) {
|
|
292
|
+
await this.s3.send(new import_client_s3.DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
293
|
+
await writeLock();
|
|
294
|
+
return key;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
throw new S3FileManagerConflictError("Folder rename already in progress");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async releaseFolderMoveLock(key) {
|
|
301
|
+
try {
|
|
302
|
+
await this.s3.send(new import_client_s3.DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
}
|
|
156
306
|
makeFolderEntry(path) {
|
|
157
307
|
const p = normalizePath(path);
|
|
158
308
|
const name = p === "" ? "" : p.split("/").at(-1) ?? "";
|
|
@@ -254,12 +404,39 @@ var S3FileManager = class {
|
|
|
254
404
|
await deleteKeysInBatches(this.s3, this.bucket, keys);
|
|
255
405
|
}
|
|
256
406
|
async deleteFiles(options, ctx = {}) {
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
407
|
+
const items = options.items ?? options.paths?.map((path) => ({ path }));
|
|
408
|
+
if (!items || items.length === 0) return;
|
|
409
|
+
const normalizedItems = items.map((item) => ({
|
|
410
|
+
path: normalizePath(item.path),
|
|
411
|
+
ifMatch: item.ifMatch,
|
|
412
|
+
ifNoneMatch: item.ifNoneMatch
|
|
413
|
+
}));
|
|
414
|
+
for (const item of normalizedItems) {
|
|
415
|
+
await this.authorize({ action: "file.delete", path: item.path, ctx });
|
|
416
|
+
}
|
|
417
|
+
const hasConditions = normalizedItems.some((item) => item.ifMatch || item.ifNoneMatch);
|
|
418
|
+
if (!hasConditions) {
|
|
419
|
+
const keys = normalizedItems.map((item) => this.pathToKey(item.path));
|
|
420
|
+
await deleteKeysInBatches(this.s3, this.bucket, keys);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
for (const item of normalizedItems) {
|
|
424
|
+
try {
|
|
425
|
+
await this.s3.send(
|
|
426
|
+
new import_client_s3.DeleteObjectCommand({
|
|
427
|
+
Bucket: this.bucket,
|
|
428
|
+
Key: this.pathToKey(item.path),
|
|
429
|
+
...item.ifMatch ? { IfMatch: item.ifMatch } : {},
|
|
430
|
+
...item.ifNoneMatch ? { IfNoneMatch: item.ifNoneMatch } : {}
|
|
431
|
+
})
|
|
432
|
+
);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
if (isPreconditionFailedError(err)) {
|
|
435
|
+
throw new S3FileManagerConflictError("Delete conflict");
|
|
436
|
+
}
|
|
437
|
+
throw err;
|
|
438
|
+
}
|
|
260
439
|
}
|
|
261
|
-
const keys = paths.map((p) => this.pathToKey(p));
|
|
262
|
-
await deleteKeysInBatches(this.s3, this.bucket, keys);
|
|
263
440
|
}
|
|
264
441
|
async copy(options, ctx = {}) {
|
|
265
442
|
const isFolder = options.fromPath.endsWith(this.delimiter);
|
|
@@ -309,13 +486,21 @@ var S3FileManager = class {
|
|
|
309
486
|
await this.authorize({ action: "file.copy", fromPath, toPath, ctx });
|
|
310
487
|
const fromKey = this.pathToKey(fromPath);
|
|
311
488
|
const toKey = this.pathToKey(toPath);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
489
|
+
try {
|
|
490
|
+
await this.s3.send(
|
|
491
|
+
new import_client_s3.CopyObjectCommand({
|
|
492
|
+
Bucket: this.bucket,
|
|
493
|
+
Key: toKey,
|
|
494
|
+
CopySource: encodeS3CopySource(this.bucket, fromKey),
|
|
495
|
+
...options.ifMatch ? { CopySourceIfMatch: options.ifMatch } : {}
|
|
496
|
+
})
|
|
497
|
+
);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
if (isPreconditionFailedError(err)) {
|
|
500
|
+
throw new S3FileManagerConflictError("Copy conflict");
|
|
501
|
+
}
|
|
502
|
+
throw err;
|
|
503
|
+
}
|
|
319
504
|
}
|
|
320
505
|
async move(options, ctx = {}) {
|
|
321
506
|
const isFolder = options.fromPath.endsWith(this.delimiter);
|
|
@@ -325,19 +510,49 @@ var S3FileManager = class {
|
|
|
325
510
|
if (isFolder) {
|
|
326
511
|
const fromPathWithSlash = ensureTrailingDelimiter(fromPath, this.delimiter);
|
|
327
512
|
const toPathWithSlash = ensureTrailingDelimiter(toPath, this.delimiter);
|
|
513
|
+
let lockKey = null;
|
|
328
514
|
await this.authorize({
|
|
329
515
|
action: "folder.move",
|
|
330
516
|
fromPath: fromPathWithSlash,
|
|
331
517
|
toPath: toPathWithSlash,
|
|
332
518
|
ctx
|
|
333
519
|
});
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
520
|
+
try {
|
|
521
|
+
if (this.lockFolderMoves) {
|
|
522
|
+
lockKey = await this.acquireFolderMoveLock(fromPathWithSlash, toPathWithSlash, ctx);
|
|
523
|
+
}
|
|
524
|
+
await this.copy(options, ctx);
|
|
525
|
+
await this.deleteFolder({ path: fromPathWithSlash, recursive: true }, ctx);
|
|
526
|
+
return;
|
|
527
|
+
} finally {
|
|
528
|
+
if (lockKey) {
|
|
529
|
+
await this.releaseFolderMoveLock(lockKey);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
337
532
|
}
|
|
338
533
|
await this.authorize({ action: "file.move", fromPath, toPath, ctx });
|
|
339
534
|
await this.copy(options, ctx);
|
|
340
|
-
|
|
535
|
+
try {
|
|
536
|
+
await this.deleteFiles(
|
|
537
|
+
{
|
|
538
|
+
items: [
|
|
539
|
+
{
|
|
540
|
+
path: fromPath,
|
|
541
|
+
...options.ifMatch ? { ifMatch: options.ifMatch } : {}
|
|
542
|
+
}
|
|
543
|
+
]
|
|
544
|
+
},
|
|
545
|
+
ctx
|
|
546
|
+
);
|
|
547
|
+
} catch (err) {
|
|
548
|
+
if (err instanceof S3FileManagerConflictError) {
|
|
549
|
+
try {
|
|
550
|
+
await this.deleteFiles({ paths: [toPath] }, ctx);
|
|
551
|
+
} catch {
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
throw err;
|
|
555
|
+
}
|
|
341
556
|
}
|
|
342
557
|
async prepareUploads(options, ctx = {}) {
|
|
343
558
|
const expiresIn = options.expiresInSeconds ?? 60 * 5;
|
|
@@ -346,19 +561,24 @@ var S3FileManager = class {
|
|
|
346
561
|
const path = normalizePath(item.path);
|
|
347
562
|
await this.authorize({ action: "upload.prepare", path, ctx });
|
|
348
563
|
const key = this.pathToKey(path);
|
|
564
|
+
const expiresAt = resolveExpiresAt(item.expiresAt);
|
|
349
565
|
const cmd = new import_client_s3.PutObjectCommand({
|
|
350
566
|
Bucket: this.bucket,
|
|
351
567
|
Key: key,
|
|
352
568
|
ContentType: item.contentType,
|
|
353
569
|
CacheControl: item.cacheControl,
|
|
354
570
|
ContentDisposition: item.contentDisposition,
|
|
355
|
-
Metadata: item.metadata
|
|
571
|
+
Metadata: item.metadata,
|
|
572
|
+
...expiresAt ? { Expires: expiresAt } : {},
|
|
573
|
+
...item.ifNoneMatch ? { IfNoneMatch: item.ifNoneMatch } : {}
|
|
356
574
|
});
|
|
357
575
|
const url = await (0, import_s3_request_presigner.getSignedUrl)(this.s3, cmd, { expiresIn });
|
|
358
576
|
const headers = {};
|
|
359
577
|
if (item.contentType) headers["Content-Type"] = item.contentType;
|
|
360
578
|
if (item.cacheControl) headers["Cache-Control"] = item.cacheControl;
|
|
361
579
|
if (item.contentDisposition) headers["Content-Disposition"] = item.contentDisposition;
|
|
580
|
+
if (expiresAt) headers.Expires = expiresAt.toUTCString();
|
|
581
|
+
if (item.ifNoneMatch) headers["If-None-Match"] = item.ifNoneMatch;
|
|
362
582
|
if (item.metadata) {
|
|
363
583
|
for (const [k, v] of Object.entries(item.metadata)) {
|
|
364
584
|
headers[`x-amz-meta-${k}`] = v;
|
|
@@ -373,6 +593,59 @@ var S3FileManager = class {
|
|
|
373
593
|
}
|
|
374
594
|
return result;
|
|
375
595
|
}
|
|
596
|
+
async getFileAttributes(options, ctx = {}) {
|
|
597
|
+
const path = normalizePath(options.path);
|
|
598
|
+
await this.authorize({ action: "file.attributes.get", path, ctx });
|
|
599
|
+
const key = this.pathToKey(path);
|
|
600
|
+
const out = await this.s3.send(
|
|
601
|
+
new import_client_s3.HeadObjectCommand({
|
|
602
|
+
Bucket: this.bucket,
|
|
603
|
+
Key: key
|
|
604
|
+
})
|
|
605
|
+
);
|
|
606
|
+
return toAttributes(path, out);
|
|
607
|
+
}
|
|
608
|
+
async setFileAttributes(options, ctx = {}) {
|
|
609
|
+
const path = normalizePath(options.path);
|
|
610
|
+
await this.authorize({ action: "file.attributes.set", path, ctx });
|
|
611
|
+
const key = this.pathToKey(path);
|
|
612
|
+
const current = await this.s3.send(
|
|
613
|
+
new import_client_s3.HeadObjectCommand({
|
|
614
|
+
Bucket: this.bucket,
|
|
615
|
+
Key: key
|
|
616
|
+
})
|
|
617
|
+
);
|
|
618
|
+
const baseMetadata = options.metadata ?? (current.Metadata ? { ...current.Metadata } : {});
|
|
619
|
+
const resolvedExpires = options.expiresAt === void 0 ? current.Expires : resolveExpiresAt(options.expiresAt);
|
|
620
|
+
try {
|
|
621
|
+
await this.s3.send(
|
|
622
|
+
new import_client_s3.CopyObjectCommand({
|
|
623
|
+
Bucket: this.bucket,
|
|
624
|
+
Key: key,
|
|
625
|
+
CopySource: encodeS3CopySource(this.bucket, key),
|
|
626
|
+
MetadataDirective: "REPLACE",
|
|
627
|
+
ContentType: options.contentType ?? current.ContentType,
|
|
628
|
+
CacheControl: options.cacheControl ?? current.CacheControl,
|
|
629
|
+
ContentDisposition: options.contentDisposition ?? current.ContentDisposition,
|
|
630
|
+
Metadata: baseMetadata,
|
|
631
|
+
...resolvedExpires ? { Expires: resolvedExpires } : {},
|
|
632
|
+
...options.ifMatch ? { CopySourceIfMatch: options.ifMatch } : {}
|
|
633
|
+
})
|
|
634
|
+
);
|
|
635
|
+
} catch (err) {
|
|
636
|
+
if (isPreconditionFailedError(err)) {
|
|
637
|
+
throw new S3FileManagerConflictError("Attribute update conflict");
|
|
638
|
+
}
|
|
639
|
+
throw err;
|
|
640
|
+
}
|
|
641
|
+
const updated = await this.s3.send(
|
|
642
|
+
new import_client_s3.HeadObjectCommand({
|
|
643
|
+
Bucket: this.bucket,
|
|
644
|
+
Key: key
|
|
645
|
+
})
|
|
646
|
+
);
|
|
647
|
+
return toAttributes(path, updated);
|
|
648
|
+
}
|
|
376
649
|
async search(options, ctx = {}) {
|
|
377
650
|
const query = options.query.toLowerCase().trim();
|
|
378
651
|
if (!query) {
|
|
@@ -445,6 +718,12 @@ var S3FileManager = class {
|
|
|
445
718
|
const expiresAt = new Date(Date.now() + expiresIn * 1e3).toISOString();
|
|
446
719
|
return { path, url, expiresAt };
|
|
447
720
|
}
|
|
721
|
+
async getFolderLock(options, ctx = {}) {
|
|
722
|
+
const path = ensureTrailingDelimiter(normalizePath(options.path), this.delimiter);
|
|
723
|
+
await this.authorize({ action: "folder.lock.get", path, ctx });
|
|
724
|
+
const key = this.lockKeyForPath(path);
|
|
725
|
+
return await this.readFolderLockByKey(key);
|
|
726
|
+
}
|
|
448
727
|
};
|
|
449
728
|
|
|
450
729
|
// src/http/handler.ts
|
|
@@ -484,6 +763,21 @@ function optionalString(value, key) {
|
|
|
484
763
|
if (typeof value === "string") return value;
|
|
485
764
|
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a string`);
|
|
486
765
|
}
|
|
766
|
+
function optionalStringOrNull(value, key) {
|
|
767
|
+
if (value === void 0) return void 0;
|
|
768
|
+
if (value === null) return null;
|
|
769
|
+
if (typeof value === "string") return value;
|
|
770
|
+
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a string`);
|
|
771
|
+
}
|
|
772
|
+
function optionalDateStringOrNull(value, key) {
|
|
773
|
+
const raw = optionalStringOrNull(value, key);
|
|
774
|
+
if (raw === void 0 || raw === null) return raw;
|
|
775
|
+
const parsed = new Date(raw);
|
|
776
|
+
if (!Number.isFinite(parsed.getTime())) {
|
|
777
|
+
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a date string`);
|
|
778
|
+
}
|
|
779
|
+
return raw;
|
|
780
|
+
}
|
|
487
781
|
function requiredString(value, key) {
|
|
488
782
|
if (typeof value === "string") return value;
|
|
489
783
|
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a string`);
|
|
@@ -566,13 +860,38 @@ function parseDeleteFolderOptions(body) {
|
|
|
566
860
|
}
|
|
567
861
|
function parseDeleteFilesOptions(body) {
|
|
568
862
|
const obj = ensureObject(body);
|
|
569
|
-
|
|
863
|
+
const itemsValue = obj.items;
|
|
864
|
+
const items = Array.isArray(itemsValue) ? itemsValue.map((raw, idx) => {
|
|
865
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
866
|
+
throw new S3FileManagerHttpError(
|
|
867
|
+
400,
|
|
868
|
+
"invalid_body",
|
|
869
|
+
`Expected 'items[${idx}]' to be an object`
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
const item = raw;
|
|
873
|
+
return {
|
|
874
|
+
path: requiredString(item.path, `items[${idx}].path`),
|
|
875
|
+
ifMatch: optionalString(item.ifMatch, `items[${idx}].ifMatch`),
|
|
876
|
+
ifNoneMatch: optionalString(item.ifNoneMatch, `items[${idx}].ifNoneMatch`)
|
|
877
|
+
};
|
|
878
|
+
}) : void 0;
|
|
879
|
+
const paths = obj.paths !== void 0 ? requiredStringArray(obj.paths, "paths") : void 0;
|
|
880
|
+
if (!paths && !items) {
|
|
881
|
+
throw new S3FileManagerHttpError(
|
|
882
|
+
400,
|
|
883
|
+
"invalid_body",
|
|
884
|
+
"Expected 'paths' or 'items' to be provided"
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
return { ...paths ? { paths } : {}, ...items ? { items } : {} };
|
|
570
888
|
}
|
|
571
889
|
function parseCopyMoveOptions(body) {
|
|
572
890
|
const obj = ensureObject(body);
|
|
573
891
|
return {
|
|
574
892
|
fromPath: requiredString(obj.fromPath, "fromPath"),
|
|
575
|
-
toPath: requiredString(obj.toPath, "toPath")
|
|
893
|
+
toPath: requiredString(obj.toPath, "toPath"),
|
|
894
|
+
ifMatch: optionalString(obj.ifMatch, "ifMatch")
|
|
576
895
|
};
|
|
577
896
|
}
|
|
578
897
|
function parsePrepareUploadsOptions(body) {
|
|
@@ -598,7 +917,9 @@ function parsePrepareUploadsOptions(body) {
|
|
|
598
917
|
item.contentDisposition,
|
|
599
918
|
`items[${idx}].contentDisposition`
|
|
600
919
|
),
|
|
601
|
-
metadata: optionalStringRecord(item.metadata, `items[${idx}].metadata`)
|
|
920
|
+
metadata: optionalStringRecord(item.metadata, `items[${idx}].metadata`),
|
|
921
|
+
expiresAt: optionalDateStringOrNull(item.expiresAt, `items[${idx}].expiresAt`),
|
|
922
|
+
ifNoneMatch: optionalString(item.ifNoneMatch, `items[${idx}].ifNoneMatch`)
|
|
602
923
|
};
|
|
603
924
|
});
|
|
604
925
|
return {
|
|
@@ -614,6 +935,30 @@ function parsePreviewOptions(body) {
|
|
|
614
935
|
inline: optionalBoolean(obj.inline, "inline")
|
|
615
936
|
};
|
|
616
937
|
}
|
|
938
|
+
function parseGetFolderLockOptions(body) {
|
|
939
|
+
const obj = ensureObject(body);
|
|
940
|
+
return {
|
|
941
|
+
path: requiredString(obj.path, "path")
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
function parseGetFileAttributesOptions(body) {
|
|
945
|
+
const obj = ensureObject(body);
|
|
946
|
+
return {
|
|
947
|
+
path: requiredString(obj.path, "path")
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function parseSetFileAttributesOptions(body) {
|
|
951
|
+
const obj = ensureObject(body);
|
|
952
|
+
return {
|
|
953
|
+
path: requiredString(obj.path, "path"),
|
|
954
|
+
contentType: optionalString(obj.contentType, "contentType"),
|
|
955
|
+
cacheControl: optionalString(obj.cacheControl, "cacheControl"),
|
|
956
|
+
contentDisposition: optionalString(obj.contentDisposition, "contentDisposition"),
|
|
957
|
+
metadata: optionalStringRecord(obj.metadata, "metadata"),
|
|
958
|
+
expiresAt: optionalDateStringOrNull(obj.expiresAt, "expiresAt"),
|
|
959
|
+
ifMatch: optionalString(obj.ifMatch, "ifMatch")
|
|
960
|
+
};
|
|
961
|
+
}
|
|
617
962
|
function createS3FileManagerHttpHandler(options) {
|
|
618
963
|
const basePath = normalizeBasePath(options.api?.basePath);
|
|
619
964
|
if (!options.manager && !options.getManager) {
|
|
@@ -661,6 +1006,24 @@ function createS3FileManagerHttpHandler(options) {
|
|
|
661
1006
|
const out = await manager.getPreviewUrl(parsePreviewOptions(req.body), ctx);
|
|
662
1007
|
return { status: 200, headers: { "content-type": "application/json" }, body: out };
|
|
663
1008
|
}
|
|
1009
|
+
if (method === "POST" && path === "/folder/lock/get") {
|
|
1010
|
+
const out = await manager.getFolderLock(parseGetFolderLockOptions(req.body), ctx);
|
|
1011
|
+
return { status: 200, headers: { "content-type": "application/json" }, body: out };
|
|
1012
|
+
}
|
|
1013
|
+
if (method === "POST" && path === "/file/attributes/get") {
|
|
1014
|
+
const out = await manager.getFileAttributes(
|
|
1015
|
+
parseGetFileAttributesOptions(req.body),
|
|
1016
|
+
ctx
|
|
1017
|
+
);
|
|
1018
|
+
return { status: 200, headers: { "content-type": "application/json" }, body: out };
|
|
1019
|
+
}
|
|
1020
|
+
if (method === "POST" && path === "/file/attributes/set") {
|
|
1021
|
+
const out = await manager.setFileAttributes(
|
|
1022
|
+
parseSetFileAttributesOptions(req.body),
|
|
1023
|
+
ctx
|
|
1024
|
+
);
|
|
1025
|
+
return { status: 200, headers: { "content-type": "application/json" }, body: out };
|
|
1026
|
+
}
|
|
664
1027
|
return jsonError(404, "not_found", "Route not found");
|
|
665
1028
|
} catch (err) {
|
|
666
1029
|
if (err instanceof S3FileManagerHttpError) {
|
|
@@ -669,6 +1032,9 @@ function createS3FileManagerHttpHandler(options) {
|
|
|
669
1032
|
if (err instanceof S3FileManagerAuthorizationError) {
|
|
670
1033
|
return jsonError(err.status, err.code, err.message);
|
|
671
1034
|
}
|
|
1035
|
+
if (err instanceof S3FileManagerConflictError) {
|
|
1036
|
+
return jsonError(err.status, err.code, err.message);
|
|
1037
|
+
}
|
|
672
1038
|
console.error("[S3FileManager Error]", err);
|
|
673
1039
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
674
1040
|
return jsonError(500, "internal_error", message);
|