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.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  CopyObjectCommand,
4
4
  DeleteObjectCommand,
5
5
  DeleteObjectsCommand,
6
+ HeadObjectCommand,
6
7
  ListObjectsV2Command,
7
8
  PutObjectCommand
8
9
  } from "@aws-sdk/client-s3";
@@ -19,6 +20,15 @@ var S3FileManagerAuthorizationError = class extends Error {
19
20
  this.code = code;
20
21
  }
21
22
  };
23
+ var S3FileManagerConflictError = class extends Error {
24
+ status;
25
+ code;
26
+ constructor(message, status = 409, code = "conflict") {
27
+ super(message);
28
+ this.status = status;
29
+ this.code = code;
30
+ }
31
+ };
22
32
 
23
33
  // src/core/manager.ts
24
34
  var DEFAULT_DELIMITER = "/";
@@ -51,6 +61,60 @@ function isNoSuchKeyError(err) {
51
61
  }
52
62
  return false;
53
63
  }
64
+ function isNotFoundError(err) {
65
+ if (!err || typeof err !== "object") return false;
66
+ if ("name" in err && (err.name === "NotFound" || err.name === "NoSuchKey")) return true;
67
+ if ("$metadata" in err) {
68
+ const meta = err.$metadata;
69
+ if (meta?.httpStatusCode === 404) return true;
70
+ }
71
+ return false;
72
+ }
73
+ function isPreconditionFailedError(err) {
74
+ if (!err || typeof err !== "object") return false;
75
+ if ("name" in err && err.name === "PreconditionFailed") return true;
76
+ if ("Code" in err && err.Code === "PreconditionFailed") return true;
77
+ if ("$metadata" in err) {
78
+ const meta = err.$metadata;
79
+ if (meta?.httpStatusCode === 412) return true;
80
+ }
81
+ if ("message" in err && typeof err.message === "string") {
82
+ return err.message.includes("PreconditionFailed");
83
+ }
84
+ return false;
85
+ }
86
+ function resolveExpiresAt(expiresAt) {
87
+ if (expiresAt === null) return void 0;
88
+ if (!expiresAt) return void 0;
89
+ const parsed = new Date(expiresAt);
90
+ if (!Number.isFinite(parsed.getTime())) {
91
+ throw new Error("Invalid expiresAt value");
92
+ }
93
+ return parsed;
94
+ }
95
+ function toAttributes(path, obj) {
96
+ const {
97
+ ContentLength,
98
+ LastModified,
99
+ ETag,
100
+ ContentType,
101
+ CacheControl,
102
+ ContentDisposition,
103
+ Metadata,
104
+ Expires
105
+ } = obj;
106
+ return {
107
+ path,
108
+ ...ContentLength !== void 0 ? { size: ContentLength } : {},
109
+ ...LastModified ? { lastModified: LastModified.toISOString() } : {},
110
+ ...ETag !== void 0 ? { etag: ETag } : {},
111
+ ...ContentType ? { contentType: ContentType } : {},
112
+ ...CacheControl ? { cacheControl: CacheControl } : {},
113
+ ...ContentDisposition ? { contentDisposition: ContentDisposition } : {},
114
+ ...Metadata ? { metadata: Metadata } : {},
115
+ ...Expires ? { expiresAt: Expires.toISOString() } : {}
116
+ };
117
+ }
54
118
  async function* listAllKeys(s3, bucket, prefix) {
55
119
  let cursor;
56
120
  while (true) {
@@ -90,6 +154,9 @@ var S3FileManager = class {
90
154
  delimiter;
91
155
  hooks;
92
156
  authorizationMode;
157
+ lockFolderMoves;
158
+ lockPrefix;
159
+ lockTtlSeconds;
93
160
  constructor(s3, options) {
94
161
  this.s3 = s3;
95
162
  this.bucket = options.bucket;
@@ -97,6 +164,9 @@ var S3FileManager = class {
97
164
  this.rootPrefix = ensureTrailingDelimiter(trimSlashes(options.rootPrefix ?? ""), this.delimiter);
98
165
  this.hooks = options.hooks;
99
166
  this.authorizationMode = options.authorizationMode ?? "deny-by-default";
167
+ this.lockFolderMoves = options.lockFolderMoves ?? true;
168
+ this.lockPrefix = options.lockPrefix ?? ".s3kit/locks";
169
+ this.lockTtlSeconds = options.lockTtlSeconds ?? 60 * 15;
100
170
  }
101
171
  async authorize(args) {
102
172
  const hasAuthHooks = Boolean(this.hooks?.authorize || this.hooks?.allowAction);
@@ -130,6 +200,87 @@ var S3FileManager = class {
130
200
  }
131
201
  return key.slice(this.rootPrefix.length);
132
202
  }
203
+ lockKeyForPath(path) {
204
+ const safe = encodeURIComponent(path).replace(/%2F/g, "__");
205
+ const prefix = ensureTrailingDelimiter(trimSlashes(this.lockPrefix), this.delimiter);
206
+ return `${this.rootPrefix}${prefix}${safe}.json`;
207
+ }
208
+ async readFolderLockByKey(key) {
209
+ try {
210
+ const out = await this.s3.send(
211
+ new HeadObjectCommand({
212
+ Bucket: this.bucket,
213
+ Key: key
214
+ })
215
+ );
216
+ const meta = out.Metadata ?? {};
217
+ const operation = meta.op === "folder.move" ? "folder.move" : void 0;
218
+ const fromPath = meta.from;
219
+ const toPath = meta.to;
220
+ const startedAt = meta.startedat;
221
+ const expiresAt = meta.expiresat ?? (out.Expires ? out.Expires.toISOString() : void 0);
222
+ if (!operation || !fromPath || !toPath || !startedAt || !expiresAt) return null;
223
+ return {
224
+ path: fromPath,
225
+ operation,
226
+ fromPath,
227
+ toPath,
228
+ startedAt,
229
+ expiresAt,
230
+ ...meta.owner ? { owner: meta.owner } : {}
231
+ };
232
+ } catch (err) {
233
+ if (isNotFoundError(err)) return null;
234
+ throw err;
235
+ }
236
+ }
237
+ async acquireFolderMoveLock(fromPath, toPath, ctx) {
238
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
239
+ const expiresAt = new Date(Date.now() + this.lockTtlSeconds * 1e3).toISOString();
240
+ const key = this.lockKeyForPath(fromPath);
241
+ const writeLock = async () => {
242
+ await this.s3.send(
243
+ new PutObjectCommand({
244
+ Bucket: this.bucket,
245
+ Key: key,
246
+ ContentType: "application/json",
247
+ Expires: new Date(expiresAt),
248
+ Metadata: {
249
+ op: "folder.move",
250
+ from: fromPath,
251
+ to: toPath,
252
+ startedat: startedAt,
253
+ expiresat: expiresAt,
254
+ ...ctx.userId ? { owner: String(ctx.userId) } : {}
255
+ },
256
+ IfNoneMatch: "*",
257
+ Body: ""
258
+ })
259
+ );
260
+ };
261
+ try {
262
+ await writeLock();
263
+ return key;
264
+ } catch (err) {
265
+ if (!isPreconditionFailedError(err)) throw err;
266
+ const existing = await this.readFolderLockByKey(key);
267
+ if (existing?.expiresAt) {
268
+ const exp = new Date(existing.expiresAt);
269
+ if (Number.isFinite(exp.getTime()) && exp.getTime() <= Date.now()) {
270
+ await this.s3.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
271
+ await writeLock();
272
+ return key;
273
+ }
274
+ }
275
+ throw new S3FileManagerConflictError("Folder rename already in progress");
276
+ }
277
+ }
278
+ async releaseFolderMoveLock(key) {
279
+ try {
280
+ await this.s3.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
281
+ } catch {
282
+ }
283
+ }
133
284
  makeFolderEntry(path) {
134
285
  const p = normalizePath(path);
135
286
  const name = p === "" ? "" : p.split("/").at(-1) ?? "";
@@ -231,12 +382,39 @@ var S3FileManager = class {
231
382
  await deleteKeysInBatches(this.s3, this.bucket, keys);
232
383
  }
233
384
  async deleteFiles(options, ctx = {}) {
234
- const paths = options.paths.map((p) => normalizePath(p));
235
- for (const path of paths) {
236
- await this.authorize({ action: "file.delete", path, ctx });
385
+ const items = options.items ?? options.paths?.map((path) => ({ path }));
386
+ if (!items || items.length === 0) return;
387
+ const normalizedItems = items.map((item) => ({
388
+ path: normalizePath(item.path),
389
+ ifMatch: item.ifMatch,
390
+ ifNoneMatch: item.ifNoneMatch
391
+ }));
392
+ for (const item of normalizedItems) {
393
+ await this.authorize({ action: "file.delete", path: item.path, ctx });
394
+ }
395
+ const hasConditions = normalizedItems.some((item) => item.ifMatch || item.ifNoneMatch);
396
+ if (!hasConditions) {
397
+ const keys = normalizedItems.map((item) => this.pathToKey(item.path));
398
+ await deleteKeysInBatches(this.s3, this.bucket, keys);
399
+ return;
400
+ }
401
+ for (const item of normalizedItems) {
402
+ try {
403
+ await this.s3.send(
404
+ new DeleteObjectCommand({
405
+ Bucket: this.bucket,
406
+ Key: this.pathToKey(item.path),
407
+ ...item.ifMatch ? { IfMatch: item.ifMatch } : {},
408
+ ...item.ifNoneMatch ? { IfNoneMatch: item.ifNoneMatch } : {}
409
+ })
410
+ );
411
+ } catch (err) {
412
+ if (isPreconditionFailedError(err)) {
413
+ throw new S3FileManagerConflictError("Delete conflict");
414
+ }
415
+ throw err;
416
+ }
237
417
  }
238
- const keys = paths.map((p) => this.pathToKey(p));
239
- await deleteKeysInBatches(this.s3, this.bucket, keys);
240
418
  }
241
419
  async copy(options, ctx = {}) {
242
420
  const isFolder = options.fromPath.endsWith(this.delimiter);
@@ -286,13 +464,21 @@ var S3FileManager = class {
286
464
  await this.authorize({ action: "file.copy", fromPath, toPath, ctx });
287
465
  const fromKey = this.pathToKey(fromPath);
288
466
  const toKey = this.pathToKey(toPath);
289
- await this.s3.send(
290
- new CopyObjectCommand({
291
- Bucket: this.bucket,
292
- Key: toKey,
293
- CopySource: encodeS3CopySource(this.bucket, fromKey)
294
- })
295
- );
467
+ try {
468
+ await this.s3.send(
469
+ new CopyObjectCommand({
470
+ Bucket: this.bucket,
471
+ Key: toKey,
472
+ CopySource: encodeS3CopySource(this.bucket, fromKey),
473
+ ...options.ifMatch ? { CopySourceIfMatch: options.ifMatch } : {}
474
+ })
475
+ );
476
+ } catch (err) {
477
+ if (isPreconditionFailedError(err)) {
478
+ throw new S3FileManagerConflictError("Copy conflict");
479
+ }
480
+ throw err;
481
+ }
296
482
  }
297
483
  async move(options, ctx = {}) {
298
484
  const isFolder = options.fromPath.endsWith(this.delimiter);
@@ -302,19 +488,49 @@ var S3FileManager = class {
302
488
  if (isFolder) {
303
489
  const fromPathWithSlash = ensureTrailingDelimiter(fromPath, this.delimiter);
304
490
  const toPathWithSlash = ensureTrailingDelimiter(toPath, this.delimiter);
491
+ let lockKey = null;
305
492
  await this.authorize({
306
493
  action: "folder.move",
307
494
  fromPath: fromPathWithSlash,
308
495
  toPath: toPathWithSlash,
309
496
  ctx
310
497
  });
311
- await this.copy(options, ctx);
312
- await this.deleteFolder({ path: fromPathWithSlash, recursive: true }, ctx);
313
- return;
498
+ try {
499
+ if (this.lockFolderMoves) {
500
+ lockKey = await this.acquireFolderMoveLock(fromPathWithSlash, toPathWithSlash, ctx);
501
+ }
502
+ await this.copy(options, ctx);
503
+ await this.deleteFolder({ path: fromPathWithSlash, recursive: true }, ctx);
504
+ return;
505
+ } finally {
506
+ if (lockKey) {
507
+ await this.releaseFolderMoveLock(lockKey);
508
+ }
509
+ }
314
510
  }
315
511
  await this.authorize({ action: "file.move", fromPath, toPath, ctx });
316
512
  await this.copy(options, ctx);
317
- await this.deleteFiles({ paths: [fromPath] }, ctx);
513
+ try {
514
+ await this.deleteFiles(
515
+ {
516
+ items: [
517
+ {
518
+ path: fromPath,
519
+ ...options.ifMatch ? { ifMatch: options.ifMatch } : {}
520
+ }
521
+ ]
522
+ },
523
+ ctx
524
+ );
525
+ } catch (err) {
526
+ if (err instanceof S3FileManagerConflictError) {
527
+ try {
528
+ await this.deleteFiles({ paths: [toPath] }, ctx);
529
+ } catch {
530
+ }
531
+ }
532
+ throw err;
533
+ }
318
534
  }
319
535
  async prepareUploads(options, ctx = {}) {
320
536
  const expiresIn = options.expiresInSeconds ?? 60 * 5;
@@ -323,19 +539,24 @@ var S3FileManager = class {
323
539
  const path = normalizePath(item.path);
324
540
  await this.authorize({ action: "upload.prepare", path, ctx });
325
541
  const key = this.pathToKey(path);
542
+ const expiresAt = resolveExpiresAt(item.expiresAt);
326
543
  const cmd = new PutObjectCommand({
327
544
  Bucket: this.bucket,
328
545
  Key: key,
329
546
  ContentType: item.contentType,
330
547
  CacheControl: item.cacheControl,
331
548
  ContentDisposition: item.contentDisposition,
332
- Metadata: item.metadata
549
+ Metadata: item.metadata,
550
+ ...expiresAt ? { Expires: expiresAt } : {},
551
+ ...item.ifNoneMatch ? { IfNoneMatch: item.ifNoneMatch } : {}
333
552
  });
334
553
  const url = await getSignedUrl(this.s3, cmd, { expiresIn });
335
554
  const headers = {};
336
555
  if (item.contentType) headers["Content-Type"] = item.contentType;
337
556
  if (item.cacheControl) headers["Cache-Control"] = item.cacheControl;
338
557
  if (item.contentDisposition) headers["Content-Disposition"] = item.contentDisposition;
558
+ if (expiresAt) headers.Expires = expiresAt.toUTCString();
559
+ if (item.ifNoneMatch) headers["If-None-Match"] = item.ifNoneMatch;
339
560
  if (item.metadata) {
340
561
  for (const [k, v] of Object.entries(item.metadata)) {
341
562
  headers[`x-amz-meta-${k}`] = v;
@@ -350,6 +571,59 @@ var S3FileManager = class {
350
571
  }
351
572
  return result;
352
573
  }
574
+ async getFileAttributes(options, ctx = {}) {
575
+ const path = normalizePath(options.path);
576
+ await this.authorize({ action: "file.attributes.get", path, ctx });
577
+ const key = this.pathToKey(path);
578
+ const out = await this.s3.send(
579
+ new HeadObjectCommand({
580
+ Bucket: this.bucket,
581
+ Key: key
582
+ })
583
+ );
584
+ return toAttributes(path, out);
585
+ }
586
+ async setFileAttributes(options, ctx = {}) {
587
+ const path = normalizePath(options.path);
588
+ await this.authorize({ action: "file.attributes.set", path, ctx });
589
+ const key = this.pathToKey(path);
590
+ const current = await this.s3.send(
591
+ new HeadObjectCommand({
592
+ Bucket: this.bucket,
593
+ Key: key
594
+ })
595
+ );
596
+ const baseMetadata = options.metadata ?? (current.Metadata ? { ...current.Metadata } : {});
597
+ const resolvedExpires = options.expiresAt === void 0 ? current.Expires : resolveExpiresAt(options.expiresAt);
598
+ try {
599
+ await this.s3.send(
600
+ new CopyObjectCommand({
601
+ Bucket: this.bucket,
602
+ Key: key,
603
+ CopySource: encodeS3CopySource(this.bucket, key),
604
+ MetadataDirective: "REPLACE",
605
+ ContentType: options.contentType ?? current.ContentType,
606
+ CacheControl: options.cacheControl ?? current.CacheControl,
607
+ ContentDisposition: options.contentDisposition ?? current.ContentDisposition,
608
+ Metadata: baseMetadata,
609
+ ...resolvedExpires ? { Expires: resolvedExpires } : {},
610
+ ...options.ifMatch ? { CopySourceIfMatch: options.ifMatch } : {}
611
+ })
612
+ );
613
+ } catch (err) {
614
+ if (isPreconditionFailedError(err)) {
615
+ throw new S3FileManagerConflictError("Attribute update conflict");
616
+ }
617
+ throw err;
618
+ }
619
+ const updated = await this.s3.send(
620
+ new HeadObjectCommand({
621
+ Bucket: this.bucket,
622
+ Key: key
623
+ })
624
+ );
625
+ return toAttributes(path, updated);
626
+ }
353
627
  async search(options, ctx = {}) {
354
628
  const query = options.query.toLowerCase().trim();
355
629
  if (!query) {
@@ -422,6 +696,12 @@ var S3FileManager = class {
422
696
  const expiresAt = new Date(Date.now() + expiresIn * 1e3).toISOString();
423
697
  return { path, url, expiresAt };
424
698
  }
699
+ async getFolderLock(options, ctx = {}) {
700
+ const path = ensureTrailingDelimiter(normalizePath(options.path), this.delimiter);
701
+ await this.authorize({ action: "folder.lock.get", path, ctx });
702
+ const key = this.lockKeyForPath(path);
703
+ return await this.readFolderLockByKey(key);
704
+ }
425
705
  };
426
706
 
427
707
  // src/http/handler.ts
@@ -461,6 +741,21 @@ function optionalString(value, key) {
461
741
  if (typeof value === "string") return value;
462
742
  throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a string`);
463
743
  }
744
+ function optionalStringOrNull(value, key) {
745
+ if (value === void 0) return void 0;
746
+ if (value === null) return null;
747
+ if (typeof value === "string") return value;
748
+ throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a string`);
749
+ }
750
+ function optionalDateStringOrNull(value, key) {
751
+ const raw = optionalStringOrNull(value, key);
752
+ if (raw === void 0 || raw === null) return raw;
753
+ const parsed = new Date(raw);
754
+ if (!Number.isFinite(parsed.getTime())) {
755
+ throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a date string`);
756
+ }
757
+ return raw;
758
+ }
464
759
  function requiredString(value, key) {
465
760
  if (typeof value === "string") return value;
466
761
  throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a string`);
@@ -543,13 +838,38 @@ function parseDeleteFolderOptions(body) {
543
838
  }
544
839
  function parseDeleteFilesOptions(body) {
545
840
  const obj = ensureObject(body);
546
- return { paths: requiredStringArray(obj.paths, "paths") };
841
+ const itemsValue = obj.items;
842
+ const items = Array.isArray(itemsValue) ? itemsValue.map((raw, idx) => {
843
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
844
+ throw new S3FileManagerHttpError(
845
+ 400,
846
+ "invalid_body",
847
+ `Expected 'items[${idx}]' to be an object`
848
+ );
849
+ }
850
+ const item = raw;
851
+ return {
852
+ path: requiredString(item.path, `items[${idx}].path`),
853
+ ifMatch: optionalString(item.ifMatch, `items[${idx}].ifMatch`),
854
+ ifNoneMatch: optionalString(item.ifNoneMatch, `items[${idx}].ifNoneMatch`)
855
+ };
856
+ }) : void 0;
857
+ const paths = obj.paths !== void 0 ? requiredStringArray(obj.paths, "paths") : void 0;
858
+ if (!paths && !items) {
859
+ throw new S3FileManagerHttpError(
860
+ 400,
861
+ "invalid_body",
862
+ "Expected 'paths' or 'items' to be provided"
863
+ );
864
+ }
865
+ return { ...paths ? { paths } : {}, ...items ? { items } : {} };
547
866
  }
548
867
  function parseCopyMoveOptions(body) {
549
868
  const obj = ensureObject(body);
550
869
  return {
551
870
  fromPath: requiredString(obj.fromPath, "fromPath"),
552
- toPath: requiredString(obj.toPath, "toPath")
871
+ toPath: requiredString(obj.toPath, "toPath"),
872
+ ifMatch: optionalString(obj.ifMatch, "ifMatch")
553
873
  };
554
874
  }
555
875
  function parsePrepareUploadsOptions(body) {
@@ -575,7 +895,9 @@ function parsePrepareUploadsOptions(body) {
575
895
  item.contentDisposition,
576
896
  `items[${idx}].contentDisposition`
577
897
  ),
578
- metadata: optionalStringRecord(item.metadata, `items[${idx}].metadata`)
898
+ metadata: optionalStringRecord(item.metadata, `items[${idx}].metadata`),
899
+ expiresAt: optionalDateStringOrNull(item.expiresAt, `items[${idx}].expiresAt`),
900
+ ifNoneMatch: optionalString(item.ifNoneMatch, `items[${idx}].ifNoneMatch`)
579
901
  };
580
902
  });
581
903
  return {
@@ -591,6 +913,30 @@ function parsePreviewOptions(body) {
591
913
  inline: optionalBoolean(obj.inline, "inline")
592
914
  };
593
915
  }
916
+ function parseGetFolderLockOptions(body) {
917
+ const obj = ensureObject(body);
918
+ return {
919
+ path: requiredString(obj.path, "path")
920
+ };
921
+ }
922
+ function parseGetFileAttributesOptions(body) {
923
+ const obj = ensureObject(body);
924
+ return {
925
+ path: requiredString(obj.path, "path")
926
+ };
927
+ }
928
+ function parseSetFileAttributesOptions(body) {
929
+ const obj = ensureObject(body);
930
+ return {
931
+ path: requiredString(obj.path, "path"),
932
+ contentType: optionalString(obj.contentType, "contentType"),
933
+ cacheControl: optionalString(obj.cacheControl, "cacheControl"),
934
+ contentDisposition: optionalString(obj.contentDisposition, "contentDisposition"),
935
+ metadata: optionalStringRecord(obj.metadata, "metadata"),
936
+ expiresAt: optionalDateStringOrNull(obj.expiresAt, "expiresAt"),
937
+ ifMatch: optionalString(obj.ifMatch, "ifMatch")
938
+ };
939
+ }
594
940
  function createS3FileManagerHttpHandler(options) {
595
941
  const basePath = normalizeBasePath(options.api?.basePath);
596
942
  if (!options.manager && !options.getManager) {
@@ -638,6 +984,24 @@ function createS3FileManagerHttpHandler(options) {
638
984
  const out = await manager.getPreviewUrl(parsePreviewOptions(req.body), ctx);
639
985
  return { status: 200, headers: { "content-type": "application/json" }, body: out };
640
986
  }
987
+ if (method === "POST" && path === "/folder/lock/get") {
988
+ const out = await manager.getFolderLock(parseGetFolderLockOptions(req.body), ctx);
989
+ return { status: 200, headers: { "content-type": "application/json" }, body: out };
990
+ }
991
+ if (method === "POST" && path === "/file/attributes/get") {
992
+ const out = await manager.getFileAttributes(
993
+ parseGetFileAttributesOptions(req.body),
994
+ ctx
995
+ );
996
+ return { status: 200, headers: { "content-type": "application/json" }, body: out };
997
+ }
998
+ if (method === "POST" && path === "/file/attributes/set") {
999
+ const out = await manager.setFileAttributes(
1000
+ parseSetFileAttributesOptions(req.body),
1001
+ ctx
1002
+ );
1003
+ return { status: 200, headers: { "content-type": "application/json" }, body: out };
1004
+ }
641
1005
  return jsonError(404, "not_found", "Route not found");
642
1006
  } catch (err) {
643
1007
  if (err instanceof S3FileManagerHttpError) {
@@ -646,6 +1010,9 @@ function createS3FileManagerHttpHandler(options) {
646
1010
  if (err instanceof S3FileManagerAuthorizationError) {
647
1011
  return jsonError(err.status, err.code, err.message);
648
1012
  }
1013
+ if (err instanceof S3FileManagerConflictError) {
1014
+ return jsonError(err.status, err.code, err.message);
1015
+ }
649
1016
  console.error("[S3FileManager Error]", err);
650
1017
  const message = err instanceof Error ? err.message : "Unknown error";
651
1018
  return jsonError(500, "internal_error", message);
@@ -748,11 +1115,25 @@ var S3FileManagerClient = class {
748
1115
  getPreviewUrl(options) {
749
1116
  return fetchJson(this.f, this.endpoint("/preview"), options);
750
1117
  }
1118
+ getFolderLock(options) {
1119
+ return fetchJson(this.f, this.endpoint("/folder/lock/get"), options);
1120
+ }
1121
+ getFileAttributes(options) {
1122
+ return fetchJson(this.f, this.endpoint("/file/attributes/get"), options);
1123
+ }
1124
+ setFileAttributes(options) {
1125
+ return fetchJson(this.f, this.endpoint("/file/attributes/set"), options);
1126
+ }
751
1127
  async uploadFiles(args) {
752
1128
  const prepare = {
753
1129
  items: args.files.map((f) => ({
754
1130
  path: f.path,
755
- contentType: f.contentType ?? f.file.type
1131
+ contentType: f.contentType ?? f.file.type,
1132
+ ...f.cacheControl !== void 0 ? { cacheControl: f.cacheControl } : {},
1133
+ ...f.contentDisposition !== void 0 ? { contentDisposition: f.contentDisposition } : {},
1134
+ ...f.metadata !== void 0 ? { metadata: f.metadata } : {},
1135
+ ...f.expiresAt !== void 0 ? { expiresAt: f.expiresAt } : {},
1136
+ ...f.ifNoneMatch !== void 0 ? { ifNoneMatch: f.ifNoneMatch } : {}
756
1137
  })),
757
1138
  ...args.expiresInSeconds !== void 0 ? { expiresInSeconds: args.expiresInSeconds } : {}
758
1139
  };
@@ -779,6 +1160,7 @@ export {
779
1160
  S3FileManager,
780
1161
  S3FileManagerAuthorizationError,
781
1162
  S3FileManagerClient,
1163
+ S3FileManagerConflictError,
782
1164
  createS3FileManagerHttpHandler
783
1165
  };
784
1166
  //# sourceMappingURL=index.js.map