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
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 paths = options.paths.map((p) => normalizePath(p));
258
- for (const path of paths) {
259
- await this.authorize({ action: "file.delete", path, ctx });
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
- 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
- );
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
- await this.copy(options, ctx);
335
- await this.deleteFolder({ path: fromPathWithSlash, recursive: true }, ctx);
336
- return;
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
- await this.deleteFiles({ paths: [fromPath] }, ctx);
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
- return { paths: requiredStringArray(obj.paths, "paths") };
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