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