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
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { CSSProperties } from 'react';
3
- import { i as S3Entry } from '../types-g2IYvH3O.cjs';
3
+ import { i as S3Entry } from '../types-B0yU5sod.cjs';
4
4
 
5
5
  interface FileManagerProps {
6
6
  /** API endpoint URL, e.g., "/api/s3" */
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { CSSProperties } from 'react';
3
- import { i as S3Entry } from '../types-g2IYvH3O.js';
3
+ import { i as S3Entry } from '../types-B0yU5sod.js';
4
4
 
5
5
  interface FileManagerProps {
6
6
  /** API endpoint URL, e.g., "/api/s3" */
@@ -96,11 +96,25 @@ var S3FileManagerClient = class {
96
96
  getPreviewUrl(options) {
97
97
  return fetchJson(this.f, this.endpoint("/preview"), options);
98
98
  }
99
+ getFolderLock(options) {
100
+ return fetchJson(this.f, this.endpoint("/folder/lock/get"), options);
101
+ }
102
+ getFileAttributes(options) {
103
+ return fetchJson(this.f, this.endpoint("/file/attributes/get"), options);
104
+ }
105
+ setFileAttributes(options) {
106
+ return fetchJson(this.f, this.endpoint("/file/attributes/set"), options);
107
+ }
99
108
  async uploadFiles(args) {
100
109
  const prepare = {
101
110
  items: args.files.map((f) => ({
102
111
  path: f.path,
103
- contentType: f.contentType ?? f.file.type
112
+ contentType: f.contentType ?? f.file.type,
113
+ ...f.cacheControl !== void 0 ? { cacheControl: f.cacheControl } : {},
114
+ ...f.contentDisposition !== void 0 ? { contentDisposition: f.contentDisposition } : {},
115
+ ...f.metadata !== void 0 ? { metadata: f.metadata } : {},
116
+ ...f.expiresAt !== void 0 ? { expiresAt: f.expiresAt } : {},
117
+ ...f.ifNoneMatch !== void 0 ? { ifNoneMatch: f.ifNoneMatch } : {}
104
118
  })),
105
119
  ...args.expiresInSeconds !== void 0 ? { expiresInSeconds: args.expiresInSeconds } : {}
106
120
  };
@@ -601,10 +615,15 @@ function FileManager({
601
615
  const [hoverRow, setHoverRow] = useState2(null);
602
616
  const [previewData, setPreviewData] = useState2(null);
603
617
  const [previewDisplay, setPreviewDisplay] = useState2(null);
618
+ const [fileAttributes, setFileAttributes] = useState2(null);
619
+ const [attributesLoading, setAttributesLoading] = useState2(false);
620
+ const [attributesError, setAttributesError] = useState2(null);
604
621
  const [isPreviewClosing, setIsPreviewClosing] = useState2(false);
605
622
  const [sidebarWidth, setSidebarWidth] = useState2(320);
606
623
  const [isResizing, setIsResizing] = useState2(false);
607
624
  const [inlinePreviews, setInlinePreviews] = useState2({});
625
+ const [folderLocks, setFolderLocks] = useState2({});
626
+ const [pendingFolderMoves, setPendingFolderMoves] = useState2(/* @__PURE__ */ new Set());
608
627
  const [isSelectionMode, setIsSelectionMode] = useState2(false);
609
628
  const [searchQuery, setSearchQuery] = useState2("");
610
629
  const [sortBy, setSortBy] = useState2("name");
@@ -655,7 +674,111 @@ function FileManager({
655
674
  const longPressTimerRef = useRef(null);
656
675
  const suppressClickRef = useRef(false);
657
676
  const dragSelectionBaseRef = useRef(/* @__PURE__ */ new Set());
677
+ const requestFolderLock = useCallback(
678
+ (path2) => {
679
+ if (folderLocks[path2]) return;
680
+ setFolderLocks((prev) => ({ ...prev, [path2]: { status: "loading" } }));
681
+ const run = async () => {
682
+ try {
683
+ const out = await client.getFolderLock({ path: path2 });
684
+ const isActive = out?.expiresAt && new Date(out.expiresAt).getTime() > Date.now() ? true : false;
685
+ setFolderLocks((prev) => ({
686
+ ...prev,
687
+ [path2]: isActive && out ? { status: "locked", lock: out } : { status: "unlocked" }
688
+ }));
689
+ } catch {
690
+ setFolderLocks((prev) => ({ ...prev, [path2]: { status: "unlocked" } }));
691
+ }
692
+ };
693
+ void run();
694
+ },
695
+ [client, folderLocks]
696
+ );
697
+ const getFolderLockLabel = useCallback(
698
+ (entry) => {
699
+ if (entry.type !== "folder") return null;
700
+ if (entry.path === `${TRASH_PATH}/`) return null;
701
+ if (pendingFolderMoves.has(entry.path)) return "Renaming";
702
+ const lock = folderLocks[entry.path];
703
+ if (lock?.status === "locked") return "Locked";
704
+ return null;
705
+ },
706
+ [folderLocks, pendingFolderMoves]
707
+ );
708
+ const handleEntryHover = useCallback(
709
+ (entry) => {
710
+ setHoverRow(entry.path);
711
+ if (entry.type === "folder" && entry.path !== `${TRASH_PATH}/`) {
712
+ requestFolderLock(entry.path);
713
+ }
714
+ },
715
+ [requestFolderLock]
716
+ );
717
+ const resolveEntryEtag = useCallback(
718
+ (entry) => {
719
+ if (entry.type !== "file") return void 0;
720
+ if (fileAttributes?.path === entry.path && fileAttributes.etag) return fileAttributes.etag;
721
+ return entry.etag;
722
+ },
723
+ [fileAttributes]
724
+ );
725
+ const parseApiError = useCallback((err) => {
726
+ const fallback = err instanceof Error ? { code: void 0, message: err.message } : { code: void 0, message: "Request failed" };
727
+ if (!(err instanceof Error)) return fallback;
728
+ const raw = err.message;
729
+ if (!raw.trim().startsWith("{")) return fallback;
730
+ try {
731
+ const parsed = JSON.parse(raw);
732
+ if (parsed?.error) {
733
+ return { code: parsed.error.code, message: parsed.error.message ?? raw };
734
+ }
735
+ } catch {
736
+ return fallback;
737
+ }
738
+ return fallback;
739
+ }, []);
740
+ const isConflictError = useCallback(
741
+ (err) => {
742
+ const info = parseApiError(err);
743
+ if (info.code === "conflict") return true;
744
+ if (info.message && /conflict|already in progress/i.test(info.message)) return true;
745
+ return false;
746
+ },
747
+ [parseApiError]
748
+ );
749
+ const showConflictAlert = useCallback(() => {
750
+ window.alert("This item changed elsewhere. Refresh and try again.");
751
+ }, []);
752
+ const renderFolderLockBadge = useCallback(
753
+ (entry, variant) => {
754
+ const label = getFolderLockLabel(entry);
755
+ if (!label) return null;
756
+ const isPending = label === "Renaming";
757
+ const style2 = {
758
+ display: "inline-flex",
759
+ alignItems: "center",
760
+ fontSize: 10,
761
+ fontWeight: 600,
762
+ textTransform: "uppercase",
763
+ letterSpacing: "0.04em",
764
+ padding: "4px 6px",
765
+ borderRadius: 6,
766
+ border: `1px solid ${isPending ? theme.accent : theme.border}`,
767
+ backgroundColor: isPending ? theme.accent : theme.bgSecondary,
768
+ color: isPending ? theme.bg : theme.textSecondary
769
+ };
770
+ return /* @__PURE__ */ jsx5(
771
+ "div",
772
+ {
773
+ style: variant === "grid" ? { ...style2, position: "absolute", top: 10, right: 10 } : { ...style2, marginLeft: 8 },
774
+ children: label
775
+ }
776
+ );
777
+ },
778
+ [getFolderLockLabel, theme]
779
+ );
658
780
  const lastSelectionSigRef = useRef("");
781
+ const lastSelectedPathsRef = useRef(/* @__PURE__ */ new Set());
659
782
  const [dragSelect, setDragSelect] = useState2(null);
660
783
  const handleKeyDown = useCallback(
661
784
  (e) => {
@@ -921,6 +1044,9 @@ function FileManager({
921
1044
  setPath("");
922
1045
  }
923
1046
  }, [hideTrash, view]);
1047
+ useEffect2(() => {
1048
+ lastSelectedPathsRef.current = /* @__PURE__ */ new Set();
1049
+ }, [mode]);
924
1050
  useEffect2(() => {
925
1051
  const timeoutId = setTimeout(() => {
926
1052
  performSearch(searchQuery);
@@ -937,7 +1063,16 @@ function FileManager({
937
1063
  if (sig === lastSelectionSigRef.current) return;
938
1064
  lastSelectionSigRef.current = sig;
939
1065
  onSelectionChange?.(selectedEntries);
940
- }, [entries, searchResults, selected, searchQuery, onSelectionChange]);
1066
+ const nextSelectedPaths = new Set(selectedEntries.map((entry) => entry.path));
1067
+ if (mode === "picker" && onFileSelect) {
1068
+ selectedEntries.forEach((entry) => {
1069
+ if (entry.type === "file" && !lastSelectedPathsRef.current.has(entry.path)) {
1070
+ onFileSelect(entry);
1071
+ }
1072
+ });
1073
+ }
1074
+ lastSelectedPathsRef.current = nextSelectedPaths;
1075
+ }, [entries, searchResults, selected, searchQuery, onSelectionChange, onFileSelect, mode]);
941
1076
  useEffect2(() => {
942
1077
  const source = searchQuery.trim() ? searchResults : entries;
943
1078
  const selectedFiles = source.filter((entry) => selected.has(entry.path)).filter((entry) => entry.type === "file");
@@ -979,6 +1114,45 @@ function FileManager({
979
1114
  setPreviewData(null);
980
1115
  }
981
1116
  }, [selected, lastSelected, client]);
1117
+ useEffect2(() => {
1118
+ if (selected.size !== 1) return;
1119
+ const selectedPath = Array.from(selected)[0];
1120
+ if (!selectedPath) return;
1121
+ const source = searchQuery.trim() ? searchResults : entries;
1122
+ const entry = source.find((item) => item.path === selectedPath);
1123
+ if (entry?.type === "folder") {
1124
+ requestFolderLock(entry.path);
1125
+ }
1126
+ }, [entries, requestFolderLock, searchQuery, searchResults, selected]);
1127
+ useEffect2(() => {
1128
+ const entry = previewData?.entry;
1129
+ if (!entry || entry.type !== "file") {
1130
+ setFileAttributes(null);
1131
+ setAttributesError(null);
1132
+ return;
1133
+ }
1134
+ let cancelled = false;
1135
+ setAttributesLoading(true);
1136
+ setAttributesError(null);
1137
+ const run = async () => {
1138
+ try {
1139
+ const out = await client.getFileAttributes({ path: entry.path });
1140
+ if (cancelled) return;
1141
+ setFileAttributes(out);
1142
+ } catch (e) {
1143
+ if (cancelled) return;
1144
+ const message = e instanceof Error ? e.message : "Failed to load attributes";
1145
+ setAttributesError(message);
1146
+ setFileAttributes(null);
1147
+ } finally {
1148
+ if (!cancelled) setAttributesLoading(false);
1149
+ }
1150
+ };
1151
+ void run();
1152
+ return () => {
1153
+ cancelled = true;
1154
+ };
1155
+ }, [client, previewData?.entry]);
982
1156
  useEffect2(() => {
983
1157
  if (previewData) {
984
1158
  setPreviewDisplay(previewData);
@@ -1119,6 +1293,7 @@ function FileManager({
1119
1293
  if (isRenaming) return;
1120
1294
  const target = renameTarget ?? lastSelected;
1121
1295
  if (!target) return;
1296
+ const isFolder = target.type === "folder";
1122
1297
  const oldPath = target.path;
1123
1298
  const parent = getParentPath(oldPath);
1124
1299
  const existingNames = new Set(
@@ -1133,16 +1308,40 @@ function FileManager({
1133
1308
  return;
1134
1309
  }
1135
1310
  try {
1311
+ if (isFolder) {
1312
+ setPendingFolderMoves((prev) => {
1313
+ const next = new Set(prev);
1314
+ next.add(oldPath);
1315
+ return next;
1316
+ });
1317
+ }
1136
1318
  setIsRenaming(true);
1137
- await client.move({ fromPath: oldPath, toPath: newPath });
1319
+ const renameEtag = target.type === "file" ? resolveEntryEtag(target) : void 0;
1320
+ await client.move({
1321
+ fromPath: oldPath,
1322
+ toPath: newPath,
1323
+ ...renameEtag ? { ifMatch: renameEtag } : {}
1324
+ });
1138
1325
  setRenameOpen(false);
1139
1326
  setRenameTarget(null);
1140
1327
  refresh();
1141
1328
  } catch (e) {
1142
1329
  console.error("Rename failed", e);
1143
- alert("Rename failed");
1330
+ if (isConflictError(e)) {
1331
+ showConflictAlert();
1332
+ } else {
1333
+ alert("Rename failed");
1334
+ }
1144
1335
  } finally {
1145
1336
  setIsRenaming(false);
1337
+ if (isFolder) {
1338
+ setPendingFolderMoves((prev) => {
1339
+ const next = new Set(prev);
1340
+ next.delete(oldPath);
1341
+ return next;
1342
+ });
1343
+ setFolderLocks((prev) => ({ ...prev, [oldPath]: { status: "unlocked" } }));
1344
+ }
1146
1345
  }
1147
1346
  }
1148
1347
  async function deleteEntries(targets) {
@@ -1151,12 +1350,26 @@ function FileManager({
1151
1350
  for (const target of targets) {
1152
1351
  if (target.path.startsWith(TRASH_PATH)) continue;
1153
1352
  const dest = joinPath(TRASH_PATH, target.path);
1154
- await client.move({ fromPath: target.path, toPath: dest });
1353
+ const moveEtag = target.type === "file" ? resolveEntryEtag(target) : void 0;
1354
+ await client.move({
1355
+ fromPath: target.path,
1356
+ toPath: dest,
1357
+ ...moveEtag ? { ifMatch: moveEtag } : {}
1358
+ });
1155
1359
  }
1156
1360
  } else {
1157
- const files = targets.filter((e) => e.type === "file").map((e) => e.path);
1361
+ const files = targets.filter((e) => e.type === "file");
1158
1362
  const folders = targets.filter((e) => e.type === "folder");
1159
- if (files.length > 0) await client.deleteFiles({ paths: files });
1363
+ if (files.length > 0)
1364
+ await client.deleteFiles({
1365
+ items: files.map((file) => {
1366
+ const deleteEtag = resolveEntryEtag(file);
1367
+ return {
1368
+ path: file.path,
1369
+ ...deleteEtag ? { ifMatch: deleteEtag } : {}
1370
+ };
1371
+ })
1372
+ });
1160
1373
  for (const folder of folders) {
1161
1374
  await client.deleteFolder({ path: folder.path, recursive: true });
1162
1375
  }
@@ -1174,7 +1387,11 @@ function FileManager({
1174
1387
  refresh();
1175
1388
  } catch (e) {
1176
1389
  console.error("Delete failed", e);
1177
- alert("Delete failed");
1390
+ if (isConflictError(e)) {
1391
+ showConflictAlert();
1392
+ } else {
1393
+ alert("Delete failed");
1394
+ }
1178
1395
  } finally {
1179
1396
  setIsDeleting(false);
1180
1397
  }
@@ -1185,15 +1402,29 @@ function FileManager({
1185
1402
  if (!target.path.startsWith(TRASH_PATH)) continue;
1186
1403
  const originalPath = target.path.slice(TRASH_PATH.length + 1);
1187
1404
  if (!originalPath) continue;
1188
- await client.move({ fromPath: target.path, toPath: originalPath });
1405
+ const restoreEtag = target.type === "file" ? resolveEntryEtag(target) : void 0;
1406
+ await client.move({
1407
+ fromPath: target.path,
1408
+ toPath: originalPath,
1409
+ ...restoreEtag ? { ifMatch: restoreEtag } : {}
1410
+ });
1189
1411
  }
1190
1412
  }
1191
1413
  async function onRestore() {
1192
1414
  if (!can.restore) return;
1193
1415
  const source = searchQuery.trim() ? searchResults : entries;
1194
1416
  const targets = source.filter((e) => selected.has(e.path));
1195
- await restoreEntries(targets);
1196
- refresh();
1417
+ try {
1418
+ await restoreEntries(targets);
1419
+ refresh();
1420
+ } catch (e) {
1421
+ console.error("Restore failed", e);
1422
+ if (isConflictError(e)) {
1423
+ showConflictAlert();
1424
+ } else {
1425
+ alert("Restore failed");
1426
+ }
1427
+ }
1197
1428
  }
1198
1429
  async function onEmptyTrash() {
1199
1430
  if (!can.restore) return;
@@ -1502,11 +1733,26 @@ function FileManager({
1502
1733
  const dest = window.prompt("Copy to folder path", path || "");
1503
1734
  if (!dest) return;
1504
1735
  const baseDest = dest.replace(/\/+$/, "");
1505
- for (const entry of entriesToCopy) {
1506
- if (entry.path.startsWith(TRASH_PATH)) continue;
1507
- const targetName = entry.name || entry.path.split("/").pop() || entry.path;
1508
- const toPath = entry.type === "folder" ? `${joinPath(baseDest, targetName)}/` : joinPath(baseDest, targetName);
1509
- await client.copy({ fromPath: entry.path, toPath });
1736
+ try {
1737
+ for (const entry of entriesToCopy) {
1738
+ if (entry.path.startsWith(TRASH_PATH)) continue;
1739
+ const targetName = entry.name || entry.path.split("/").pop() || entry.path;
1740
+ const toPath = entry.type === "folder" ? `${joinPath(baseDest, targetName)}/` : joinPath(baseDest, targetName);
1741
+ const copyEtag = entry.type === "file" ? resolveEntryEtag(entry) : void 0;
1742
+ await client.copy({
1743
+ fromPath: entry.path,
1744
+ toPath,
1745
+ ...copyEtag ? { ifMatch: copyEtag } : {}
1746
+ });
1747
+ }
1748
+ } catch (e) {
1749
+ console.error("Copy failed", e);
1750
+ if (isConflictError(e)) {
1751
+ showConflictAlert();
1752
+ } else {
1753
+ alert("Copy failed");
1754
+ }
1755
+ return;
1510
1756
  }
1511
1757
  refresh();
1512
1758
  }
@@ -1515,11 +1761,26 @@ function FileManager({
1515
1761
  const dest = window.prompt("Move to folder path", path || "");
1516
1762
  if (!dest) return;
1517
1763
  const baseDest = dest.replace(/\/+$/, "");
1518
- for (const entry of entriesToMove) {
1519
- if (entry.path.startsWith(TRASH_PATH)) continue;
1520
- const targetName = entry.name || entry.path.split("/").pop() || entry.path;
1521
- const toPath = entry.type === "folder" ? `${joinPath(baseDest, targetName)}/` : joinPath(baseDest, targetName);
1522
- await client.move({ fromPath: entry.path, toPath });
1764
+ try {
1765
+ for (const entry of entriesToMove) {
1766
+ if (entry.path.startsWith(TRASH_PATH)) continue;
1767
+ const targetName = entry.name || entry.path.split("/").pop() || entry.path;
1768
+ const toPath = entry.type === "folder" ? `${joinPath(baseDest, targetName)}/` : joinPath(baseDest, targetName);
1769
+ const moveEtag = entry.type === "file" ? resolveEntryEtag(entry) : void 0;
1770
+ await client.move({
1771
+ fromPath: entry.path,
1772
+ toPath,
1773
+ ...moveEtag ? { ifMatch: moveEtag } : {}
1774
+ });
1775
+ }
1776
+ } catch (e) {
1777
+ console.error("Move failed", e);
1778
+ if (isConflictError(e)) {
1779
+ showConflictAlert();
1780
+ } else {
1781
+ alert("Move failed");
1782
+ }
1783
+ return;
1523
1784
  }
1524
1785
  refresh();
1525
1786
  }
@@ -2310,7 +2571,7 @@ function FileManager({
2310
2571
  },
2311
2572
  onClick: (e) => handleEntryClickWithSelection(entry, index, filteredEntries, e),
2312
2573
  onDoubleClick: () => openEntry(entry),
2313
- onMouseEnter: () => setHoverRow(entry.path),
2574
+ onMouseEnter: () => handleEntryHover(entry),
2314
2575
  onMouseLeave: () => setHoverRow(null),
2315
2576
  onTouchStart: () => {
2316
2577
  if (!isMobile) return;
@@ -2346,7 +2607,8 @@ function FileManager({
2346
2607
  },
2347
2608
  children: entryLabel
2348
2609
  }
2349
- )
2610
+ ),
2611
+ renderFolderLockBadge(entry, "grid")
2350
2612
  ]
2351
2613
  },
2352
2614
  entry.path
@@ -2376,7 +2638,7 @@ function FileManager({
2376
2638
  },
2377
2639
  onClick: (e) => handleEntryClickWithSelection(entry, index, filteredEntries, e),
2378
2640
  onDoubleClick: () => openEntry(entry),
2379
- onMouseEnter: () => setHoverRow(entry.path),
2641
+ onMouseEnter: () => handleEntryHover(entry),
2380
2642
  onMouseLeave: () => setHoverRow(null),
2381
2643
  onTouchStart: () => {
2382
2644
  if (!isMobile) return;
@@ -2462,7 +2724,7 @@ function FileManager({
2462
2724
  e.stopPropagation();
2463
2725
  }
2464
2726
  },
2465
- onMouseEnter: () => setHoverRow(entry.path),
2727
+ onMouseEnter: () => handleEntryHover(entry),
2466
2728
  onMouseLeave: () => setHoverRow(null),
2467
2729
  onTouchStart: () => {
2468
2730
  if (!isMobile) return;
@@ -2498,7 +2760,8 @@ function FileManager({
2498
2760
  },
2499
2761
  children: entryLabel
2500
2762
  }
2501
- )
2763
+ ),
2764
+ renderFolderLockBadge(entry, "grid")
2502
2765
  ]
2503
2766
  }
2504
2767
  ),
@@ -2739,7 +3002,7 @@ function FileManager({
2739
3002
  handleEntryClickWithSelection(entry, index, filteredEntries, e);
2740
3003
  },
2741
3004
  onDoubleClick: () => openEntry(entry),
2742
- onMouseEnter: () => setHoverRow(entry.path),
3005
+ onMouseEnter: () => handleEntryHover(entry),
2743
3006
  onMouseLeave: () => setHoverRow(null),
2744
3007
  onTouchStart: () => {
2745
3008
  if (!isMobile) return;
@@ -2836,7 +3099,7 @@ function FileManager({
2836
3099
  handleEntryClickWithSelection(entry, index, filteredEntries, e);
2837
3100
  },
2838
3101
  onDoubleClick: () => openEntry(entry),
2839
- onMouseEnter: () => setHoverRow(entry.path),
3102
+ onMouseEnter: () => handleEntryHover(entry),
2840
3103
  onMouseLeave: () => setHoverRow(null),
2841
3104
  onTouchStart: () => {
2842
3105
  if (!isMobile) return;
@@ -2919,7 +3182,8 @@ function FileManager({
2919
3182
  }
2920
3183
  return getFileIcon(entry.name);
2921
3184
  })(),
2922
- entryLabel
3185
+ entryLabel,
3186
+ renderFolderLockBadge(entry, "list")
2923
3187
  ]
2924
3188
  }
2925
3189
  ),
@@ -2994,7 +3258,7 @@ function FileManager({
2994
3258
  }
2995
3259
  e.stopPropagation();
2996
3260
  },
2997
- onMouseEnter: () => setHoverRow(entry.path),
3261
+ onMouseEnter: () => handleEntryHover(entry),
2998
3262
  onMouseLeave: () => setHoverRow(null),
2999
3263
  onTouchStart: () => {
3000
3264
  if (!isMobile) return;
@@ -3077,7 +3341,8 @@ function FileManager({
3077
3341
  }
3078
3342
  return getFileIcon(entry.name);
3079
3343
  })(),
3080
- entryLabel
3344
+ entryLabel,
3345
+ renderFolderLockBadge(entry, "list")
3081
3346
  ]
3082
3347
  }
3083
3348
  ),
@@ -3731,6 +3996,44 @@ function FileManager({
3731
3996
  /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaLabel, children: "Modified" }),
3732
3997
  /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaValue, children: previewDisplay.entry.type === "file" && previewDisplay.entry.lastModified ? new Date(previewDisplay.entry.lastModified).toLocaleString() : "--" })
3733
3998
  ] }),
3999
+ attributesLoading && /* @__PURE__ */ jsxs2("div", { className: FileManager_default.metaItem, children: [
4000
+ /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaLabel, children: "Attributes" }),
4001
+ /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaValue, children: "Loading..." })
4002
+ ] }),
4003
+ attributesError && /* @__PURE__ */ jsxs2("div", { className: FileManager_default.metaItem, children: [
4004
+ /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaLabel, children: "Attributes" }),
4005
+ /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaValue, children: attributesError })
4006
+ ] }),
4007
+ !attributesLoading && !attributesError && previewDisplay.entry.type === "file" && /* @__PURE__ */ jsxs2(Fragment, { children: [
4008
+ /* @__PURE__ */ jsxs2("div", { className: FileManager_default.metaItem, children: [
4009
+ /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaLabel, children: "Content Type" }),
4010
+ /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaValue, children: fileAttributes?.contentType ?? "--" })
4011
+ ] }),
4012
+ /* @__PURE__ */ jsxs2("div", { className: FileManager_default.metaItem, children: [
4013
+ /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaLabel, children: "Cache Control" }),
4014
+ /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaValue, children: fileAttributes?.cacheControl ?? "--" })
4015
+ ] }),
4016
+ /* @__PURE__ */ jsxs2("div", { className: FileManager_default.metaItem, children: [
4017
+ /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaLabel, children: "Disposition" }),
4018
+ /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaValue, children: fileAttributes?.contentDisposition ?? "--" })
4019
+ ] }),
4020
+ /* @__PURE__ */ jsxs2("div", { className: FileManager_default.metaItem, children: [
4021
+ /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaLabel, children: "ETag" }),
4022
+ /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaValue, children: fileAttributes?.etag ?? previewDisplay.entry.etag ?? "--" })
4023
+ ] }),
4024
+ /* @__PURE__ */ jsxs2("div", { className: FileManager_default.metaItem, children: [
4025
+ /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaLabel, children: "Expires" }),
4026
+ /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaValue, children: fileAttributes?.expiresAt ? new Date(fileAttributes.expiresAt).toLocaleString() : "--" })
4027
+ ] }),
4028
+ /* @__PURE__ */ jsxs2("div", { className: FileManager_default.metaItem, children: [
4029
+ /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaLabel, children: "Metadata" }),
4030
+ /* @__PURE__ */ jsx5("div", { className: FileManager_default.metaValue, children: fileAttributes?.metadata && Object.keys(fileAttributes.metadata).length > 0 ? /* @__PURE__ */ jsx5("div", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: Object.entries(fileAttributes.metadata).map(([k, v]) => /* @__PURE__ */ jsxs2("div", { children: [
4031
+ k,
4032
+ ": ",
4033
+ v
4034
+ ] }, k)) }) : "--" })
4035
+ ] })
4036
+ ] }),
3734
4037
  /* @__PURE__ */ jsx5(
3735
4038
  "div",
3736
4039
  {