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
@@ -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 paths = options.paths.map((p) => normalizePath(p));
258
- for (const path of paths) {
259
- await this.authorize({ action: "file.delete", path, ctx });
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
- await this.s3.send(
313
- new import_client_s3.CopyObjectCommand({
314
- Bucket: this.bucket,
315
- Key: toKey,
316
- CopySource: encodeS3CopySource(this.bucket, fromKey)
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
- await this.copy(options, ctx);
335
- await this.deleteFolder({ path: fromPathWithSlash, recursive: true }, ctx);
336
- return;
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
- await this.deleteFiles({ paths: [fromPath] }, ctx);
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
- return { paths: requiredStringArray(obj.paths, "paths") };
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);