@putkoff/abstract-securefiles 0.0.47 → 0.0.48
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.
- package/dist/index.cjs +82 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +82 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/FileHandlers/Upload/src/upload_utils.tsx +146 -44
package/dist/index.cjs
CHANGED
|
@@ -864,12 +864,45 @@ function FileTable({ files, loading, error, onToggleShare, onPatchShare, onDelet
|
|
|
864
864
|
} })] }));
|
|
865
865
|
}
|
|
866
866
|
|
|
867
|
+
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
868
|
+
/** Strip filename from a webkitRelativePath, keep the trailing slash. */
|
|
869
|
+
function deriveRelPath(f) {
|
|
870
|
+
const wrp = f.webkitRelativePath;
|
|
871
|
+
if (!wrp)
|
|
872
|
+
return '';
|
|
873
|
+
const idx = wrp.lastIndexOf('/');
|
|
874
|
+
return idx >= 0 ? wrp.slice(0, idx + 1) : '';
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Drain a DirectoryReader completely. The browser only returns ~100 entries
|
|
878
|
+
* per readEntries() call; you must call it repeatedly until it returns [].
|
|
879
|
+
*/
|
|
880
|
+
function readAllEntries(reader) {
|
|
881
|
+
return new Promise((resolve, reject) => {
|
|
882
|
+
const all = [];
|
|
883
|
+
const pump = () => {
|
|
884
|
+
reader.readEntries((batch) => {
|
|
885
|
+
if (batch.length === 0)
|
|
886
|
+
resolve(all);
|
|
887
|
+
else {
|
|
888
|
+
all.push(...batch);
|
|
889
|
+
pump();
|
|
890
|
+
}
|
|
891
|
+
}, reject);
|
|
892
|
+
};
|
|
893
|
+
pump();
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
// ── component ───────────────────────────────────────────────────────────────
|
|
867
897
|
function Upload({ onUploaded }) {
|
|
868
898
|
const [queue, setQueue] = React.useState([]);
|
|
869
899
|
const [busy, setBusy] = React.useState(false);
|
|
870
900
|
const [message, setMessage] = React.useState(null);
|
|
871
901
|
const [drag, setDrag] = React.useState(false);
|
|
872
|
-
|
|
902
|
+
// Two refs: one for plain file picking, one for folder picking.
|
|
903
|
+
const fileInputRef = React.useRef(null);
|
|
904
|
+
const dirInputRef = React.useRef(null);
|
|
905
|
+
const [pickMode, setPickMode] = React.useState('files');
|
|
873
906
|
const [dirs, setDirs] = React.useState([]);
|
|
874
907
|
// — existing files → directory list —
|
|
875
908
|
const { files, load } = useFiles();
|
|
@@ -889,56 +922,76 @@ function Upload({ onUploaded }) {
|
|
|
889
922
|
const [creatingNew, setCreatingNew] = React.useState(false);
|
|
890
923
|
const [newDirName, setNewDirName] = React.useState('');
|
|
891
924
|
// — queue logic —
|
|
892
|
-
const enqueue = React.useCallback((
|
|
925
|
+
const enqueue = React.useCallback((incoming) => {
|
|
893
926
|
setQueue(prev => {
|
|
894
927
|
const map = new Map(prev.map(q => [`${q.file.name}-${q.file.size}`, q]));
|
|
895
|
-
|
|
928
|
+
incoming.forEach(q => map.set(`${q.file.name}-${q.file.size}`, q));
|
|
896
929
|
return Array.from(map.values());
|
|
897
930
|
});
|
|
898
931
|
}, []);
|
|
899
932
|
const extractFromDataTransfer = React.useCallback(async (dt) => {
|
|
933
|
+
const items = Array.from(dt.items ?? []);
|
|
934
|
+
const hasEntryApi = items.length > 0 &&
|
|
935
|
+
typeof items[0].webkitGetAsEntry === 'function';
|
|
936
|
+
// Fallback: no entry API → flat files only, no folder traversal.
|
|
937
|
+
if (!hasEntryApi) {
|
|
938
|
+
return Array.from(dt.files ?? []).map(file => ({
|
|
939
|
+
file,
|
|
940
|
+
relPath: '',
|
|
941
|
+
progress: 0,
|
|
942
|
+
}));
|
|
943
|
+
}
|
|
900
944
|
const out = [];
|
|
901
|
-
const walk = async (entry, path =
|
|
945
|
+
const walk = async (entry, path = '') => {
|
|
902
946
|
if (!entry)
|
|
903
947
|
return;
|
|
904
948
|
if (entry.isFile) {
|
|
905
|
-
const file = await new Promise(res => entry.file(res));
|
|
949
|
+
const file = await new Promise((res, rej) => entry.file(res, rej));
|
|
906
950
|
out.push({ file, relPath: path, progress: 0 });
|
|
951
|
+
return;
|
|
907
952
|
}
|
|
908
|
-
|
|
953
|
+
if (entry.isDirectory) {
|
|
909
954
|
const reader = entry.createReader();
|
|
910
|
-
const entries = await
|
|
955
|
+
const entries = await readAllEntries(reader);
|
|
911
956
|
await Promise.all(entries.map(e => walk(e, `${path}${entry.name}/`)));
|
|
912
957
|
}
|
|
913
958
|
};
|
|
914
|
-
await Promise.all(
|
|
959
|
+
await Promise.all(items.map(i => walk(i.webkitGetAsEntry?.())));
|
|
915
960
|
return out;
|
|
916
961
|
}, []);
|
|
917
962
|
const handleSelect = (e) => {
|
|
918
|
-
const
|
|
919
|
-
const queued =
|
|
963
|
+
const picked = Array.from(e.target.files ?? []);
|
|
964
|
+
const queued = picked.map(f => ({
|
|
920
965
|
file: f,
|
|
921
|
-
relPath: f
|
|
966
|
+
relPath: deriveRelPath(f),
|
|
922
967
|
progress: 0,
|
|
923
968
|
}));
|
|
924
969
|
enqueue(queued);
|
|
925
|
-
e.target.value =
|
|
970
|
+
e.target.value = '';
|
|
926
971
|
};
|
|
927
972
|
const handleDrop = async (e) => {
|
|
928
973
|
e.preventDefault();
|
|
929
|
-
|
|
930
|
-
|
|
974
|
+
e.stopPropagation();
|
|
975
|
+
const dropped = await extractFromDataTransfer(e.dataTransfer);
|
|
976
|
+
if (dropped.length)
|
|
977
|
+
enqueue(dropped);
|
|
931
978
|
setDrag(false);
|
|
932
979
|
};
|
|
980
|
+
const openPicker = (e) => {
|
|
981
|
+
// Only fire when the dropzone background itself is clicked, not children.
|
|
982
|
+
if (e.target !== e.currentTarget && !e.target.closest('[data-dz-passthrough]')) ;
|
|
983
|
+
const ref = pickMode === 'folder' ? dirInputRef : fileInputRef;
|
|
984
|
+
ref.current?.click();
|
|
985
|
+
};
|
|
933
986
|
const handleUploadAll = async () => {
|
|
934
987
|
if (!queue.length) {
|
|
935
|
-
setMessage(
|
|
988
|
+
setMessage('No files queued.');
|
|
936
989
|
return;
|
|
937
990
|
}
|
|
938
991
|
setBusy(true);
|
|
939
992
|
setMessage(null);
|
|
940
993
|
const targetDir = creatingNew
|
|
941
|
-
? (newDirName.trim().replace(/^\/+|\/+$/g,
|
|
994
|
+
? (newDirName.trim().replace(/^\/+|\/+$/g, '') + '/')
|
|
942
995
|
: uploadDir;
|
|
943
996
|
try {
|
|
944
997
|
for (const { file, relPath } of queue) {
|
|
@@ -946,12 +999,12 @@ function Upload({ onUploaded }) {
|
|
|
946
999
|
setQueue(cur => cur.map(item => item.file === file ? { ...item, progress: 100 } : item));
|
|
947
1000
|
}
|
|
948
1001
|
setQueue([]);
|
|
949
|
-
await onUploaded();
|
|
950
|
-
setMessage(
|
|
951
|
-
load()
|
|
1002
|
+
await onUploaded(); // SecureFiles passes filesHook.load here
|
|
1003
|
+
setMessage('Upload complete');
|
|
1004
|
+
// (removed the duplicate load() — onUploaded already does it)
|
|
952
1005
|
}
|
|
953
1006
|
catch {
|
|
954
|
-
setMessage(
|
|
1007
|
+
setMessage('Upload failed');
|
|
955
1008
|
}
|
|
956
1009
|
finally {
|
|
957
1010
|
setBusy(false);
|
|
@@ -960,9 +1013,9 @@ function Upload({ onUploaded }) {
|
|
|
960
1013
|
// — directory selector handlers —
|
|
961
1014
|
const handleDirChange = (e) => {
|
|
962
1015
|
const v = e.target.value;
|
|
963
|
-
if (v ===
|
|
1016
|
+
if (v === '__new__') {
|
|
964
1017
|
setCreatingNew(true);
|
|
965
|
-
setNewDirName(
|
|
1018
|
+
setNewDirName('');
|
|
966
1019
|
}
|
|
967
1020
|
else {
|
|
968
1021
|
setCreatingNew(false);
|
|
@@ -970,22 +1023,19 @@ function Upload({ onUploaded }) {
|
|
|
970
1023
|
}
|
|
971
1024
|
};
|
|
972
1025
|
const acceptNewDir = () => {
|
|
973
|
-
// normalize like before
|
|
974
1026
|
const cleanName = newDirName.trim().replace(/^\/+|\/+$/g, '');
|
|
1027
|
+
if (!cleanName) {
|
|
1028
|
+
setCreatingNew(false);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
975
1031
|
const normalized = `${uploadDir}${cleanName}/`;
|
|
976
|
-
|
|
977
|
-
setDirs(prev => {
|
|
978
|
-
if (prev.includes(normalized))
|
|
979
|
-
return prev;
|
|
980
|
-
return [...prev, normalized].sort();
|
|
981
|
-
});
|
|
982
|
-
// ② select it & flip back to the <select>
|
|
1032
|
+
setDirs(prev => prev.includes(normalized) ? prev : [...prev, normalized].sort());
|
|
983
1033
|
setUploadDir(normalized);
|
|
984
1034
|
setCreatingNew(false);
|
|
985
1035
|
setNewDirName('');
|
|
986
1036
|
};
|
|
987
|
-
return (jsxRuntime.jsxs("div", { className: "form-container max-w-2xl mx-auto", children: [jsxRuntime.jsxs("div", { className: `flex items-center border-2 border-dashed rounded-lg p-6 bg-white shadow-md space-x-4 transition-shadow duration-150
|
|
988
|
-
${drag ? 'border-indigo-500 bg-indigo-50 shadow-lg' : 'border-gray-300 hover:shadow-lg'}`, onClick:
|
|
1037
|
+
return (jsxRuntime.jsxs("div", { className: "form-container max-w-2xl mx-auto", children: [jsxRuntime.jsxs("div", { className: "flex gap-4 text-xs text-gray-700 mb-1", children: [jsxRuntime.jsxs("label", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "radio", name: "pick-mode", checked: pickMode === 'files', onChange: () => setPickMode('files') }), "Files"] }), jsxRuntime.jsxs("label", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "radio", name: "pick-mode", checked: pickMode === 'folder', onChange: () => setPickMode('folder') }), "Folder"] }), jsxRuntime.jsx("span", { className: "text-gray-400", children: "(drag-and-drop accepts either)" })] }), jsxRuntime.jsxs("div", { className: `flex items-center border-2 border-dashed rounded-lg p-6 bg-white shadow-md space-x-4 transition-shadow duration-150
|
|
1038
|
+
${drag ? 'border-indigo-500 bg-indigo-50 shadow-lg' : 'border-gray-300 hover:shadow-lg'}`, onClick: openPicker, onDragEnter: e => { e.preventDefault(); e.stopPropagation(); setDrag(true); }, onDragOver: e => { e.preventDefault(); e.stopPropagation(); }, onDragLeave: e => { e.preventDefault(); e.stopPropagation(); setDrag(false); }, onDrop: handleDrop, role: "region", "aria-label": "File upload area", children: [jsxRuntime.jsx(ForwardRef, { style: { width: 300, height: 300 }, className: "text-gray-500 mb-3", "aria-hidden": "true" }), jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("p", { className: "text-base font-semibold text-gray-700", children: "Drag & drop files or folders here" }), jsxRuntime.jsxs("p", { className: "text-xs text-gray-500 mt-1", children: ["Click to ", pickMode === 'folder' ? 'choose a folder' : 'choose files'] }), queue.length > 0 && (jsxRuntime.jsxs("p", { className: "mt-1 text-sm text-gray-600", children: [queue.length, " item", queue.length > 1 && 's', " queued"] }))] })] }), jsxRuntime.jsx("input", { ref: fileInputRef, type: "file", multiple: true, className: "hidden", onChange: handleSelect }), jsxRuntime.jsx("input", { ref: dirInputRef, type: "file", multiple: true, className: "hidden", ...{ webkitdirectory: '', directory: '' }, onChange: handleSelect }), jsxRuntime.jsxs("div", { className: "flex items-center justify-between mt-4", children: [jsxRuntime.jsx("div", { className: "flex-1 mr-4", children: !creatingNew ? (jsxRuntime.jsxs("select", { value: uploadDir, onChange: handleDirChange, className: "form-select w-full", children: [dirs.map(d => (jsxRuntime.jsx("option", { value: d, children: d }, d))), jsxRuntime.jsx("option", { value: "__new__", children: "+ Create new directory\u2026" })] })) : (jsxRuntime.jsxs("div", { className: "flex", children: [jsxRuntime.jsx("input", { type: "text", value: newDirName, placeholder: "new-folder-name", onChange: e => setNewDirName(e.target.value), onBlur: acceptNewDir, onKeyDown: e => e.key === 'Enter' && acceptNewDir(), className: "form-input flex-1" }), jsxRuntime.jsx(Button, { onClick: acceptNewDir, className: "ml-2", children: "OK" })] })) }), jsxRuntime.jsx(Button, { color: "blue", variant: "primary", onClick: handleUploadAll, disabled: busy || !queue.length, children: busy ? 'Uploading…' : `Upload ${queue.length}` })] }), message && (jsxRuntime.jsx("p", { className: `text-center text-sm ${message.includes('failed') ? 'text-red-600' : 'text-gray-600'}`, role: "alert", children: message })), queue.length > 0 && (jsxRuntime.jsxs("div", { className: "wm-container mt-4", children: [jsxRuntime.jsx("h3", { className: "mb-2 text-sm font-semibold text-gray-700", children: "Queued Files" }), jsxRuntime.jsx("ul", { className: "max-h-64 space-y-4 overflow-y-auto", children: queue.map(({ file, relPath, progress }) => (jsxRuntime.jsxs("li", { className: "space-y-1", children: [jsxRuntime.jsxs("div", { className: "flex justify-between items-center", children: [jsxRuntime.jsxs("span", { className: "truncate text-sm text-gray-800", children: [relPath, file.name] }), jsxRuntime.jsxs("span", { className: "text-xs text-gray-500", children: [progress, "%"] })] }), jsxRuntime.jsx("div", { className: "w-full bg-gray-200 rounded-full h-2 overflow-hidden", children: jsxRuntime.jsx("div", { className: "bg-indigo-600 h-2", style: { width: `${progress}%` } }) })] }, `${file.name}-${file.size}`))) })] }))] }));
|
|
989
1039
|
}
|
|
990
1040
|
|
|
991
1041
|
const SecureFiles = () => {
|