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
@@ -123,11 +123,25 @@ var S3FileManagerClient = class {
123
123
  getPreviewUrl(options) {
124
124
  return fetchJson(this.f, this.endpoint("/preview"), options);
125
125
  }
126
+ getFolderLock(options) {
127
+ return fetchJson(this.f, this.endpoint("/folder/lock/get"), options);
128
+ }
129
+ getFileAttributes(options) {
130
+ return fetchJson(this.f, this.endpoint("/file/attributes/get"), options);
131
+ }
132
+ setFileAttributes(options) {
133
+ return fetchJson(this.f, this.endpoint("/file/attributes/set"), options);
134
+ }
126
135
  async uploadFiles(args) {
127
136
  const prepare = {
128
137
  items: args.files.map((f) => ({
129
138
  path: f.path,
130
- contentType: f.contentType ?? f.file.type
139
+ contentType: f.contentType ?? f.file.type,
140
+ ...f.cacheControl !== void 0 ? { cacheControl: f.cacheControl } : {},
141
+ ...f.contentDisposition !== void 0 ? { contentDisposition: f.contentDisposition } : {},
142
+ ...f.metadata !== void 0 ? { metadata: f.metadata } : {},
143
+ ...f.expiresAt !== void 0 ? { expiresAt: f.expiresAt } : {},
144
+ ...f.ifNoneMatch !== void 0 ? { ifNoneMatch: f.ifNoneMatch } : {}
131
145
  })),
132
146
  ...args.expiresInSeconds !== void 0 ? { expiresInSeconds: args.expiresInSeconds } : {}
133
147
  };
@@ -605,10 +619,15 @@ function FileManager({
605
619
  const [hoverRow, setHoverRow] = (0, import_react6.useState)(null);
606
620
  const [previewData, setPreviewData] = (0, import_react6.useState)(null);
607
621
  const [previewDisplay, setPreviewDisplay] = (0, import_react6.useState)(null);
622
+ const [fileAttributes, setFileAttributes] = (0, import_react6.useState)(null);
623
+ const [attributesLoading, setAttributesLoading] = (0, import_react6.useState)(false);
624
+ const [attributesError, setAttributesError] = (0, import_react6.useState)(null);
608
625
  const [isPreviewClosing, setIsPreviewClosing] = (0, import_react6.useState)(false);
609
626
  const [sidebarWidth, setSidebarWidth] = (0, import_react6.useState)(320);
610
627
  const [isResizing, setIsResizing] = (0, import_react6.useState)(false);
611
628
  const [inlinePreviews, setInlinePreviews] = (0, import_react6.useState)({});
629
+ const [folderLocks, setFolderLocks] = (0, import_react6.useState)({});
630
+ const [pendingFolderMoves, setPendingFolderMoves] = (0, import_react6.useState)(/* @__PURE__ */ new Set());
612
631
  const [isSelectionMode, setIsSelectionMode] = (0, import_react6.useState)(false);
613
632
  const [searchQuery, setSearchQuery] = (0, import_react6.useState)("");
614
633
  const [sortBy, setSortBy] = (0, import_react6.useState)("name");
@@ -659,7 +678,111 @@ function FileManager({
659
678
  const longPressTimerRef = (0, import_react6.useRef)(null);
660
679
  const suppressClickRef = (0, import_react6.useRef)(false);
661
680
  const dragSelectionBaseRef = (0, import_react6.useRef)(/* @__PURE__ */ new Set());
681
+ const requestFolderLock = (0, import_react6.useCallback)(
682
+ (path2) => {
683
+ if (folderLocks[path2]) return;
684
+ setFolderLocks((prev) => ({ ...prev, [path2]: { status: "loading" } }));
685
+ const run = async () => {
686
+ try {
687
+ const out = await client.getFolderLock({ path: path2 });
688
+ const isActive = out?.expiresAt && new Date(out.expiresAt).getTime() > Date.now() ? true : false;
689
+ setFolderLocks((prev) => ({
690
+ ...prev,
691
+ [path2]: isActive && out ? { status: "locked", lock: out } : { status: "unlocked" }
692
+ }));
693
+ } catch {
694
+ setFolderLocks((prev) => ({ ...prev, [path2]: { status: "unlocked" } }));
695
+ }
696
+ };
697
+ void run();
698
+ },
699
+ [client, folderLocks]
700
+ );
701
+ const getFolderLockLabel = (0, import_react6.useCallback)(
702
+ (entry) => {
703
+ if (entry.type !== "folder") return null;
704
+ if (entry.path === `${TRASH_PATH}/`) return null;
705
+ if (pendingFolderMoves.has(entry.path)) return "Renaming";
706
+ const lock = folderLocks[entry.path];
707
+ if (lock?.status === "locked") return "Locked";
708
+ return null;
709
+ },
710
+ [folderLocks, pendingFolderMoves]
711
+ );
712
+ const handleEntryHover = (0, import_react6.useCallback)(
713
+ (entry) => {
714
+ setHoverRow(entry.path);
715
+ if (entry.type === "folder" && entry.path !== `${TRASH_PATH}/`) {
716
+ requestFolderLock(entry.path);
717
+ }
718
+ },
719
+ [requestFolderLock]
720
+ );
721
+ const resolveEntryEtag = (0, import_react6.useCallback)(
722
+ (entry) => {
723
+ if (entry.type !== "file") return void 0;
724
+ if (fileAttributes?.path === entry.path && fileAttributes.etag) return fileAttributes.etag;
725
+ return entry.etag;
726
+ },
727
+ [fileAttributes]
728
+ );
729
+ const parseApiError = (0, import_react6.useCallback)((err) => {
730
+ const fallback = err instanceof Error ? { code: void 0, message: err.message } : { code: void 0, message: "Request failed" };
731
+ if (!(err instanceof Error)) return fallback;
732
+ const raw = err.message;
733
+ if (!raw.trim().startsWith("{")) return fallback;
734
+ try {
735
+ const parsed = JSON.parse(raw);
736
+ if (parsed?.error) {
737
+ return { code: parsed.error.code, message: parsed.error.message ?? raw };
738
+ }
739
+ } catch {
740
+ return fallback;
741
+ }
742
+ return fallback;
743
+ }, []);
744
+ const isConflictError = (0, import_react6.useCallback)(
745
+ (err) => {
746
+ const info = parseApiError(err);
747
+ if (info.code === "conflict") return true;
748
+ if (info.message && /conflict|already in progress/i.test(info.message)) return true;
749
+ return false;
750
+ },
751
+ [parseApiError]
752
+ );
753
+ const showConflictAlert = (0, import_react6.useCallback)(() => {
754
+ window.alert("This item changed elsewhere. Refresh and try again.");
755
+ }, []);
756
+ const renderFolderLockBadge = (0, import_react6.useCallback)(
757
+ (entry, variant) => {
758
+ const label = getFolderLockLabel(entry);
759
+ if (!label) return null;
760
+ const isPending = label === "Renaming";
761
+ const style2 = {
762
+ display: "inline-flex",
763
+ alignItems: "center",
764
+ fontSize: 10,
765
+ fontWeight: 600,
766
+ textTransform: "uppercase",
767
+ letterSpacing: "0.04em",
768
+ padding: "4px 6px",
769
+ borderRadius: 6,
770
+ border: `1px solid ${isPending ? theme.accent : theme.border}`,
771
+ backgroundColor: isPending ? theme.accent : theme.bgSecondary,
772
+ color: isPending ? theme.bg : theme.textSecondary
773
+ };
774
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
775
+ "div",
776
+ {
777
+ style: variant === "grid" ? { ...style2, position: "absolute", top: 10, right: 10 } : { ...style2, marginLeft: 8 },
778
+ children: label
779
+ }
780
+ );
781
+ },
782
+ [getFolderLockLabel, theme]
783
+ );
662
784
  const lastSelectionSigRef = (0, import_react6.useRef)("");
785
+ const lastSelectedPathsRef = (0, import_react6.useRef)(/* @__PURE__ */ new Set());
663
786
  const [dragSelect, setDragSelect] = (0, import_react6.useState)(null);
664
787
  const handleKeyDown = (0, import_react6.useCallback)(
665
788
  (e) => {
@@ -925,6 +1048,9 @@ function FileManager({
925
1048
  setPath("");
926
1049
  }
927
1050
  }, [hideTrash, view]);
1051
+ (0, import_react6.useEffect)(() => {
1052
+ lastSelectedPathsRef.current = /* @__PURE__ */ new Set();
1053
+ }, [mode]);
928
1054
  (0, import_react6.useEffect)(() => {
929
1055
  const timeoutId = setTimeout(() => {
930
1056
  performSearch(searchQuery);
@@ -941,7 +1067,16 @@ function FileManager({
941
1067
  if (sig === lastSelectionSigRef.current) return;
942
1068
  lastSelectionSigRef.current = sig;
943
1069
  onSelectionChange?.(selectedEntries);
944
- }, [entries, searchResults, selected, searchQuery, onSelectionChange]);
1070
+ const nextSelectedPaths = new Set(selectedEntries.map((entry) => entry.path));
1071
+ if (mode === "picker" && onFileSelect) {
1072
+ selectedEntries.forEach((entry) => {
1073
+ if (entry.type === "file" && !lastSelectedPathsRef.current.has(entry.path)) {
1074
+ onFileSelect(entry);
1075
+ }
1076
+ });
1077
+ }
1078
+ lastSelectedPathsRef.current = nextSelectedPaths;
1079
+ }, [entries, searchResults, selected, searchQuery, onSelectionChange, onFileSelect, mode]);
945
1080
  (0, import_react6.useEffect)(() => {
946
1081
  const source = searchQuery.trim() ? searchResults : entries;
947
1082
  const selectedFiles = source.filter((entry) => selected.has(entry.path)).filter((entry) => entry.type === "file");
@@ -983,6 +1118,45 @@ function FileManager({
983
1118
  setPreviewData(null);
984
1119
  }
985
1120
  }, [selected, lastSelected, client]);
1121
+ (0, import_react6.useEffect)(() => {
1122
+ if (selected.size !== 1) return;
1123
+ const selectedPath = Array.from(selected)[0];
1124
+ if (!selectedPath) return;
1125
+ const source = searchQuery.trim() ? searchResults : entries;
1126
+ const entry = source.find((item) => item.path === selectedPath);
1127
+ if (entry?.type === "folder") {
1128
+ requestFolderLock(entry.path);
1129
+ }
1130
+ }, [entries, requestFolderLock, searchQuery, searchResults, selected]);
1131
+ (0, import_react6.useEffect)(() => {
1132
+ const entry = previewData?.entry;
1133
+ if (!entry || entry.type !== "file") {
1134
+ setFileAttributes(null);
1135
+ setAttributesError(null);
1136
+ return;
1137
+ }
1138
+ let cancelled = false;
1139
+ setAttributesLoading(true);
1140
+ setAttributesError(null);
1141
+ const run = async () => {
1142
+ try {
1143
+ const out = await client.getFileAttributes({ path: entry.path });
1144
+ if (cancelled) return;
1145
+ setFileAttributes(out);
1146
+ } catch (e) {
1147
+ if (cancelled) return;
1148
+ const message = e instanceof Error ? e.message : "Failed to load attributes";
1149
+ setAttributesError(message);
1150
+ setFileAttributes(null);
1151
+ } finally {
1152
+ if (!cancelled) setAttributesLoading(false);
1153
+ }
1154
+ };
1155
+ void run();
1156
+ return () => {
1157
+ cancelled = true;
1158
+ };
1159
+ }, [client, previewData?.entry]);
986
1160
  (0, import_react6.useEffect)(() => {
987
1161
  if (previewData) {
988
1162
  setPreviewDisplay(previewData);
@@ -1123,6 +1297,7 @@ function FileManager({
1123
1297
  if (isRenaming) return;
1124
1298
  const target = renameTarget ?? lastSelected;
1125
1299
  if (!target) return;
1300
+ const isFolder = target.type === "folder";
1126
1301
  const oldPath = target.path;
1127
1302
  const parent = getParentPath(oldPath);
1128
1303
  const existingNames = new Set(
@@ -1137,16 +1312,40 @@ function FileManager({
1137
1312
  return;
1138
1313
  }
1139
1314
  try {
1315
+ if (isFolder) {
1316
+ setPendingFolderMoves((prev) => {
1317
+ const next = new Set(prev);
1318
+ next.add(oldPath);
1319
+ return next;
1320
+ });
1321
+ }
1140
1322
  setIsRenaming(true);
1141
- await client.move({ fromPath: oldPath, toPath: newPath });
1323
+ const renameEtag = target.type === "file" ? resolveEntryEtag(target) : void 0;
1324
+ await client.move({
1325
+ fromPath: oldPath,
1326
+ toPath: newPath,
1327
+ ...renameEtag ? { ifMatch: renameEtag } : {}
1328
+ });
1142
1329
  setRenameOpen(false);
1143
1330
  setRenameTarget(null);
1144
1331
  refresh();
1145
1332
  } catch (e) {
1146
1333
  console.error("Rename failed", e);
1147
- alert("Rename failed");
1334
+ if (isConflictError(e)) {
1335
+ showConflictAlert();
1336
+ } else {
1337
+ alert("Rename failed");
1338
+ }
1148
1339
  } finally {
1149
1340
  setIsRenaming(false);
1341
+ if (isFolder) {
1342
+ setPendingFolderMoves((prev) => {
1343
+ const next = new Set(prev);
1344
+ next.delete(oldPath);
1345
+ return next;
1346
+ });
1347
+ setFolderLocks((prev) => ({ ...prev, [oldPath]: { status: "unlocked" } }));
1348
+ }
1150
1349
  }
1151
1350
  }
1152
1351
  async function deleteEntries(targets) {
@@ -1155,12 +1354,26 @@ function FileManager({
1155
1354
  for (const target of targets) {
1156
1355
  if (target.path.startsWith(TRASH_PATH)) continue;
1157
1356
  const dest = joinPath(TRASH_PATH, target.path);
1158
- await client.move({ fromPath: target.path, toPath: dest });
1357
+ const moveEtag = target.type === "file" ? resolveEntryEtag(target) : void 0;
1358
+ await client.move({
1359
+ fromPath: target.path,
1360
+ toPath: dest,
1361
+ ...moveEtag ? { ifMatch: moveEtag } : {}
1362
+ });
1159
1363
  }
1160
1364
  } else {
1161
- const files = targets.filter((e) => e.type === "file").map((e) => e.path);
1365
+ const files = targets.filter((e) => e.type === "file");
1162
1366
  const folders = targets.filter((e) => e.type === "folder");
1163
- if (files.length > 0) await client.deleteFiles({ paths: files });
1367
+ if (files.length > 0)
1368
+ await client.deleteFiles({
1369
+ items: files.map((file) => {
1370
+ const deleteEtag = resolveEntryEtag(file);
1371
+ return {
1372
+ path: file.path,
1373
+ ...deleteEtag ? { ifMatch: deleteEtag } : {}
1374
+ };
1375
+ })
1376
+ });
1164
1377
  for (const folder of folders) {
1165
1378
  await client.deleteFolder({ path: folder.path, recursive: true });
1166
1379
  }
@@ -1178,7 +1391,11 @@ function FileManager({
1178
1391
  refresh();
1179
1392
  } catch (e) {
1180
1393
  console.error("Delete failed", e);
1181
- alert("Delete failed");
1394
+ if (isConflictError(e)) {
1395
+ showConflictAlert();
1396
+ } else {
1397
+ alert("Delete failed");
1398
+ }
1182
1399
  } finally {
1183
1400
  setIsDeleting(false);
1184
1401
  }
@@ -1189,15 +1406,29 @@ function FileManager({
1189
1406
  if (!target.path.startsWith(TRASH_PATH)) continue;
1190
1407
  const originalPath = target.path.slice(TRASH_PATH.length + 1);
1191
1408
  if (!originalPath) continue;
1192
- await client.move({ fromPath: target.path, toPath: originalPath });
1409
+ const restoreEtag = target.type === "file" ? resolveEntryEtag(target) : void 0;
1410
+ await client.move({
1411
+ fromPath: target.path,
1412
+ toPath: originalPath,
1413
+ ...restoreEtag ? { ifMatch: restoreEtag } : {}
1414
+ });
1193
1415
  }
1194
1416
  }
1195
1417
  async function onRestore() {
1196
1418
  if (!can.restore) return;
1197
1419
  const source = searchQuery.trim() ? searchResults : entries;
1198
1420
  const targets = source.filter((e) => selected.has(e.path));
1199
- await restoreEntries(targets);
1200
- refresh();
1421
+ try {
1422
+ await restoreEntries(targets);
1423
+ refresh();
1424
+ } catch (e) {
1425
+ console.error("Restore failed", e);
1426
+ if (isConflictError(e)) {
1427
+ showConflictAlert();
1428
+ } else {
1429
+ alert("Restore failed");
1430
+ }
1431
+ }
1201
1432
  }
1202
1433
  async function onEmptyTrash() {
1203
1434
  if (!can.restore) return;
@@ -1506,11 +1737,26 @@ function FileManager({
1506
1737
  const dest = window.prompt("Copy to folder path", path || "");
1507
1738
  if (!dest) return;
1508
1739
  const baseDest = dest.replace(/\/+$/, "");
1509
- for (const entry of entriesToCopy) {
1510
- if (entry.path.startsWith(TRASH_PATH)) continue;
1511
- const targetName = entry.name || entry.path.split("/").pop() || entry.path;
1512
- const toPath = entry.type === "folder" ? `${joinPath(baseDest, targetName)}/` : joinPath(baseDest, targetName);
1513
- await client.copy({ fromPath: entry.path, toPath });
1740
+ try {
1741
+ for (const entry of entriesToCopy) {
1742
+ if (entry.path.startsWith(TRASH_PATH)) continue;
1743
+ const targetName = entry.name || entry.path.split("/").pop() || entry.path;
1744
+ const toPath = entry.type === "folder" ? `${joinPath(baseDest, targetName)}/` : joinPath(baseDest, targetName);
1745
+ const copyEtag = entry.type === "file" ? resolveEntryEtag(entry) : void 0;
1746
+ await client.copy({
1747
+ fromPath: entry.path,
1748
+ toPath,
1749
+ ...copyEtag ? { ifMatch: copyEtag } : {}
1750
+ });
1751
+ }
1752
+ } catch (e) {
1753
+ console.error("Copy failed", e);
1754
+ if (isConflictError(e)) {
1755
+ showConflictAlert();
1756
+ } else {
1757
+ alert("Copy failed");
1758
+ }
1759
+ return;
1514
1760
  }
1515
1761
  refresh();
1516
1762
  }
@@ -1519,11 +1765,26 @@ function FileManager({
1519
1765
  const dest = window.prompt("Move to folder path", path || "");
1520
1766
  if (!dest) return;
1521
1767
  const baseDest = dest.replace(/\/+$/, "");
1522
- for (const entry of entriesToMove) {
1523
- if (entry.path.startsWith(TRASH_PATH)) continue;
1524
- const targetName = entry.name || entry.path.split("/").pop() || entry.path;
1525
- const toPath = entry.type === "folder" ? `${joinPath(baseDest, targetName)}/` : joinPath(baseDest, targetName);
1526
- await client.move({ fromPath: entry.path, toPath });
1768
+ try {
1769
+ for (const entry of entriesToMove) {
1770
+ if (entry.path.startsWith(TRASH_PATH)) continue;
1771
+ const targetName = entry.name || entry.path.split("/").pop() || entry.path;
1772
+ const toPath = entry.type === "folder" ? `${joinPath(baseDest, targetName)}/` : joinPath(baseDest, targetName);
1773
+ const moveEtag = entry.type === "file" ? resolveEntryEtag(entry) : void 0;
1774
+ await client.move({
1775
+ fromPath: entry.path,
1776
+ toPath,
1777
+ ...moveEtag ? { ifMatch: moveEtag } : {}
1778
+ });
1779
+ }
1780
+ } catch (e) {
1781
+ console.error("Move failed", e);
1782
+ if (isConflictError(e)) {
1783
+ showConflictAlert();
1784
+ } else {
1785
+ alert("Move failed");
1786
+ }
1787
+ return;
1527
1788
  }
1528
1789
  refresh();
1529
1790
  }
@@ -2314,7 +2575,7 @@ function FileManager({
2314
2575
  },
2315
2576
  onClick: (e) => handleEntryClickWithSelection(entry, index, filteredEntries, e),
2316
2577
  onDoubleClick: () => openEntry(entry),
2317
- onMouseEnter: () => setHoverRow(entry.path),
2578
+ onMouseEnter: () => handleEntryHover(entry),
2318
2579
  onMouseLeave: () => setHoverRow(null),
2319
2580
  onTouchStart: () => {
2320
2581
  if (!isMobile) return;
@@ -2350,7 +2611,8 @@ function FileManager({
2350
2611
  },
2351
2612
  children: entryLabel
2352
2613
  }
2353
- )
2614
+ ),
2615
+ renderFolderLockBadge(entry, "grid")
2354
2616
  ]
2355
2617
  },
2356
2618
  entry.path
@@ -2380,7 +2642,7 @@ function FileManager({
2380
2642
  },
2381
2643
  onClick: (e) => handleEntryClickWithSelection(entry, index, filteredEntries, e),
2382
2644
  onDoubleClick: () => openEntry(entry),
2383
- onMouseEnter: () => setHoverRow(entry.path),
2645
+ onMouseEnter: () => handleEntryHover(entry),
2384
2646
  onMouseLeave: () => setHoverRow(null),
2385
2647
  onTouchStart: () => {
2386
2648
  if (!isMobile) return;
@@ -2466,7 +2728,7 @@ function FileManager({
2466
2728
  e.stopPropagation();
2467
2729
  }
2468
2730
  },
2469
- onMouseEnter: () => setHoverRow(entry.path),
2731
+ onMouseEnter: () => handleEntryHover(entry),
2470
2732
  onMouseLeave: () => setHoverRow(null),
2471
2733
  onTouchStart: () => {
2472
2734
  if (!isMobile) return;
@@ -2502,7 +2764,8 @@ function FileManager({
2502
2764
  },
2503
2765
  children: entryLabel
2504
2766
  }
2505
- )
2767
+ ),
2768
+ renderFolderLockBadge(entry, "grid")
2506
2769
  ]
2507
2770
  }
2508
2771
  ),
@@ -2743,7 +3006,7 @@ function FileManager({
2743
3006
  handleEntryClickWithSelection(entry, index, filteredEntries, e);
2744
3007
  },
2745
3008
  onDoubleClick: () => openEntry(entry),
2746
- onMouseEnter: () => setHoverRow(entry.path),
3009
+ onMouseEnter: () => handleEntryHover(entry),
2747
3010
  onMouseLeave: () => setHoverRow(null),
2748
3011
  onTouchStart: () => {
2749
3012
  if (!isMobile) return;
@@ -2840,7 +3103,7 @@ function FileManager({
2840
3103
  handleEntryClickWithSelection(entry, index, filteredEntries, e);
2841
3104
  },
2842
3105
  onDoubleClick: () => openEntry(entry),
2843
- onMouseEnter: () => setHoverRow(entry.path),
3106
+ onMouseEnter: () => handleEntryHover(entry),
2844
3107
  onMouseLeave: () => setHoverRow(null),
2845
3108
  onTouchStart: () => {
2846
3109
  if (!isMobile) return;
@@ -2923,7 +3186,8 @@ function FileManager({
2923
3186
  }
2924
3187
  return getFileIcon(entry.name);
2925
3188
  })(),
2926
- entryLabel
3189
+ entryLabel,
3190
+ renderFolderLockBadge(entry, "list")
2927
3191
  ]
2928
3192
  }
2929
3193
  ),
@@ -2998,7 +3262,7 @@ function FileManager({
2998
3262
  }
2999
3263
  e.stopPropagation();
3000
3264
  },
3001
- onMouseEnter: () => setHoverRow(entry.path),
3265
+ onMouseEnter: () => handleEntryHover(entry),
3002
3266
  onMouseLeave: () => setHoverRow(null),
3003
3267
  onTouchStart: () => {
3004
3268
  if (!isMobile) return;
@@ -3081,7 +3345,8 @@ function FileManager({
3081
3345
  }
3082
3346
  return getFileIcon(entry.name);
3083
3347
  })(),
3084
- entryLabel
3348
+ entryLabel,
3349
+ renderFolderLockBadge(entry, "list")
3085
3350
  ]
3086
3351
  }
3087
3352
  ),
@@ -3735,6 +4000,44 @@ function FileManager({
3735
4000
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaLabel, children: "Modified" }),
3736
4001
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaValue, children: previewDisplay.entry.type === "file" && previewDisplay.entry.lastModified ? new Date(previewDisplay.entry.lastModified).toLocaleString() : "--" })
3737
4002
  ] }),
4003
+ attributesLoading && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: FileManager_default.metaItem, children: [
4004
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaLabel, children: "Attributes" }),
4005
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaValue, children: "Loading..." })
4006
+ ] }),
4007
+ attributesError && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: FileManager_default.metaItem, children: [
4008
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaLabel, children: "Attributes" }),
4009
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaValue, children: attributesError })
4010
+ ] }),
4011
+ !attributesLoading && !attributesError && previewDisplay.entry.type === "file" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
4012
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: FileManager_default.metaItem, children: [
4013
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaLabel, children: "Content Type" }),
4014
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaValue, children: fileAttributes?.contentType ?? "--" })
4015
+ ] }),
4016
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: FileManager_default.metaItem, children: [
4017
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaLabel, children: "Cache Control" }),
4018
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaValue, children: fileAttributes?.cacheControl ?? "--" })
4019
+ ] }),
4020
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: FileManager_default.metaItem, children: [
4021
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaLabel, children: "Disposition" }),
4022
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaValue, children: fileAttributes?.contentDisposition ?? "--" })
4023
+ ] }),
4024
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: FileManager_default.metaItem, children: [
4025
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaLabel, children: "ETag" }),
4026
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaValue, children: fileAttributes?.etag ?? previewDisplay.entry.etag ?? "--" })
4027
+ ] }),
4028
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: FileManager_default.metaItem, children: [
4029
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaLabel, children: "Expires" }),
4030
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaValue, children: fileAttributes?.expiresAt ? new Date(fileAttributes.expiresAt).toLocaleString() : "--" })
4031
+ ] }),
4032
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: FileManager_default.metaItem, children: [
4033
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaLabel, children: "Metadata" }),
4034
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: FileManager_default.metaValue, children: fileAttributes?.metadata && Object.keys(fileAttributes.metadata).length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: Object.entries(fileAttributes.metadata).map(([k, v]) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
4035
+ k,
4036
+ ": ",
4037
+ v
4038
+ ] }, k)) }) : "--" })
4039
+ ] })
4040
+ ] }),
3738
4041
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3739
4042
  "div",
3740
4043
  {