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