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.
Files changed (54) hide show
  1. package/README.md +15 -1
  2. package/dist/adapters/express.cjs +99 -3
  3. package/dist/adapters/express.cjs.map +1 -1
  4. package/dist/adapters/express.d.cts +2 -2
  5. package/dist/adapters/express.d.ts +2 -2
  6. package/dist/adapters/express.js +99 -3
  7. package/dist/adapters/express.js.map +1 -1
  8. package/dist/adapters/fetch.cjs +99 -3
  9. package/dist/adapters/fetch.cjs.map +1 -1
  10. package/dist/adapters/fetch.d.cts +2 -2
  11. package/dist/adapters/fetch.d.ts +2 -2
  12. package/dist/adapters/fetch.js +99 -3
  13. package/dist/adapters/fetch.js.map +1 -1
  14. package/dist/adapters/next.cjs +386 -20
  15. package/dist/adapters/next.cjs.map +1 -1
  16. package/dist/adapters/next.d.cts +2 -2
  17. package/dist/adapters/next.d.ts +2 -2
  18. package/dist/adapters/next.js +387 -20
  19. package/dist/adapters/next.js.map +1 -1
  20. package/dist/client/index.cjs +15 -1
  21. package/dist/client/index.cjs.map +1 -1
  22. package/dist/client/index.d.cts +12 -2
  23. package/dist/client/index.d.ts +12 -2
  24. package/dist/client/index.js +15 -1
  25. package/dist/client/index.js.map +1 -1
  26. package/dist/core/index.cjs +300 -19
  27. package/dist/core/index.cjs.map +1 -1
  28. package/dist/core/index.d.cts +8 -3
  29. package/dist/core/index.d.ts +8 -3
  30. package/dist/core/index.js +299 -18
  31. package/dist/core/index.js.map +1 -1
  32. package/dist/http/index.cjs +99 -3
  33. package/dist/http/index.cjs.map +1 -1
  34. package/dist/http/index.d.cts +5 -2
  35. package/dist/http/index.d.ts +5 -2
  36. package/dist/http/index.js +99 -3
  37. package/dist/http/index.js.map +1 -1
  38. package/dist/index.cjs +403 -21
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +4 -4
  41. package/dist/index.d.ts +4 -4
  42. package/dist/index.js +403 -21
  43. package/dist/index.js.map +1 -1
  44. package/dist/{manager-BbmXpgXN.d.ts → manager-BtW1-sC0.d.ts} +11 -1
  45. package/dist/{manager-gIjo-t8h.d.cts → manager-DSsCYKEz.d.cts} +11 -1
  46. package/dist/react/index.cjs +334 -31
  47. package/dist/react/index.cjs.map +1 -1
  48. package/dist/react/index.d.cts +1 -1
  49. package/dist/react/index.d.ts +1 -1
  50. package/dist/react/index.js +334 -31
  51. package/dist/react/index.js.map +1 -1
  52. package/dist/{types-g2IYvH3O.d.cts → types-B0yU5sod.d.cts} +51 -3
  53. package/dist/{types-g2IYvH3O.d.ts → types-B0yU5sod.d.ts} +51 -3
  54. package/package.json +1 -1
@@ -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 paths = options.paths.map((p) => normalizePath(p));
238
- for (const path of paths) {
239
- await this.authorize({ action: "file.delete", path, ctx });
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
- await this.s3.send(
293
- new CopyObjectCommand({
294
- Bucket: this.bucket,
295
- Key: toKey,
296
- CopySource: encodeS3CopySource(this.bucket, fromKey)
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
- await this.copy(options, ctx);
315
- await this.deleteFolder({ path: fromPathWithSlash, recursive: true }, ctx);
316
- return;
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
- await this.deleteFiles({ paths: [fromPath] }, ctx);
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
- return { paths: requiredStringArray(obj.paths, "paths") };
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);