@putkoff/abstract-securefiles 0.0.45 → 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/FileHandlers/FileTable/imports.d.ts +1 -1
- package/dist/FileHandlers/imports.d.ts +1 -1
- package/dist/file_utils/imports.d.ts +1 -1
- package/dist/file_utils/src/delete_utils/imports.d.ts +1 -1
- package/dist/file_utils/src/download_utils/imports.d.ts +1 -1
- package/dist/file_utils/src/download_utils/src/download_select.d.ts +1 -1
- package/dist/file_utils/src/imports.d.ts +1 -1
- package/dist/file_utils/src/list_utils/imports.d.ts +0 -1
- package/dist/file_utils/src/share_utils/imports.d.ts +0 -1
- package/dist/hooks/imports.d.ts +1 -1
- package/dist/hooks/useFiles/imports.d.ts +1 -1
- package/dist/imports.d.ts +0 -1
- package/dist/index.cjs +196 -139
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +197 -140
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/FileHandlers/FileTable/imports.ts +0 -1
- package/src/FileHandlers/Upload/src/upload_utils.tsx +146 -44
- package/src/FileHandlers/imports.ts +0 -1
- package/src/file_utils/imports.ts +1 -1
- package/src/file_utils/src/delete_utils/imports.ts +1 -1
- package/src/file_utils/src/download_utils/imports.ts +1 -1
- package/src/file_utils/src/download_utils/src/download_select.ts +18 -13
- package/src/file_utils/src/imports.ts +1 -1
- package/src/file_utils/src/list_utils/imports.ts +1 -1
- package/src/file_utils/src/share_utils/imports.ts +0 -1
- package/src/hooks/imports.ts +1 -1
- package/src/hooks/useFiles/imports.ts +1 -1
- package/src/imports.ts +2 -3
|
@@ -14,12 +14,49 @@ import {
|
|
|
14
14
|
} from './../imports';
|
|
15
15
|
import type { QueuedFile, UploadProps } from './../imports';
|
|
16
16
|
|
|
17
|
+
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/** Strip filename from a webkitRelativePath, keep the trailing slash. */
|
|
20
|
+
function deriveRelPath(f: File): string {
|
|
21
|
+
const wrp = (f as any).webkitRelativePath as string | undefined;
|
|
22
|
+
if (!wrp) return '';
|
|
23
|
+
const idx = wrp.lastIndexOf('/');
|
|
24
|
+
return idx >= 0 ? wrp.slice(0, idx + 1) : '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Drain a DirectoryReader completely. The browser only returns ~100 entries
|
|
29
|
+
* per readEntries() call; you must call it repeatedly until it returns [].
|
|
30
|
+
*/
|
|
31
|
+
function readAllEntries(reader: any): Promise<any[]> {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const all: any[] = [];
|
|
34
|
+
const pump = () => {
|
|
35
|
+
reader.readEntries(
|
|
36
|
+
(batch: any[]) => {
|
|
37
|
+
if (batch.length === 0) resolve(all);
|
|
38
|
+
else { all.push(...batch); pump(); }
|
|
39
|
+
},
|
|
40
|
+
reject,
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
pump();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── component ───────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
17
49
|
export default function Upload({ onUploaded }: UploadProps) {
|
|
18
50
|
const [queue, setQueue] = useState<QueuedFile[]>([]);
|
|
19
51
|
const [busy, setBusy] = useState(false);
|
|
20
52
|
const [message, setMessage] = useState<string | null>(null);
|
|
21
53
|
const [drag, setDrag] = useState(false);
|
|
22
|
-
|
|
54
|
+
|
|
55
|
+
// Two refs: one for plain file picking, one for folder picking.
|
|
56
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
57
|
+
const dirInputRef = useRef<HTMLInputElement>(null);
|
|
58
|
+
const [pickMode, setPickMode] = useState<'files' | 'folder'>('files');
|
|
59
|
+
|
|
23
60
|
const [dirs, setDirs] = useState<string[]>([]);
|
|
24
61
|
|
|
25
62
|
// — existing files → directory list —
|
|
@@ -43,61 +80,95 @@ export default function Upload({ onUploaded }: UploadProps) {
|
|
|
43
80
|
const [newDirName, setNewDirName] = useState('');
|
|
44
81
|
|
|
45
82
|
// — queue logic —
|
|
46
|
-
const enqueue = useCallback((
|
|
83
|
+
const enqueue = useCallback((incoming: QueuedFile[]) => {
|
|
47
84
|
setQueue(prev => {
|
|
48
85
|
const map = new Map(prev.map(q => [`${q.file.name}-${q.file.size}`, q]));
|
|
49
|
-
|
|
86
|
+
incoming.forEach(q => map.set(`${q.file.name}-${q.file.size}`, q));
|
|
50
87
|
return Array.from(map.values());
|
|
51
88
|
});
|
|
52
89
|
}, []);
|
|
53
90
|
|
|
54
91
|
const extractFromDataTransfer = useCallback(
|
|
55
92
|
async (dt: DataTransfer): Promise<QueuedFile[]> => {
|
|
93
|
+
const items = Array.from(dt.items ?? []);
|
|
94
|
+
const hasEntryApi =
|
|
95
|
+
items.length > 0 &&
|
|
96
|
+
typeof (items[0] as any).webkitGetAsEntry === 'function';
|
|
97
|
+
|
|
98
|
+
// Fallback: no entry API → flat files only, no folder traversal.
|
|
99
|
+
if (!hasEntryApi) {
|
|
100
|
+
return Array.from(dt.files ?? []).map(file => ({
|
|
101
|
+
file,
|
|
102
|
+
relPath: '',
|
|
103
|
+
progress: 0,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
56
107
|
const out: QueuedFile[] = [];
|
|
57
|
-
|
|
108
|
+
|
|
109
|
+
const walk = async (entry: any, path = ''): Promise<void> => {
|
|
58
110
|
if (!entry) return;
|
|
59
111
|
if (entry.isFile) {
|
|
60
|
-
const file: File = await new Promise(res =>
|
|
112
|
+
const file: File = await new Promise((res, rej) =>
|
|
113
|
+
entry.file(res, rej),
|
|
114
|
+
);
|
|
61
115
|
out.push({ file, relPath: path, progress: 0 });
|
|
62
|
-
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (entry.isDirectory) {
|
|
63
119
|
const reader = entry.createReader();
|
|
64
|
-
const entries
|
|
65
|
-
await Promise.all(
|
|
120
|
+
const entries = await readAllEntries(reader);
|
|
121
|
+
await Promise.all(
|
|
122
|
+
entries.map(e => walk(e, `${path}${entry.name}/`)),
|
|
123
|
+
);
|
|
66
124
|
}
|
|
67
125
|
};
|
|
68
|
-
|
|
126
|
+
|
|
127
|
+
await Promise.all(
|
|
128
|
+
items.map(i => walk((i as any).webkitGetAsEntry?.())),
|
|
129
|
+
);
|
|
69
130
|
return out;
|
|
70
131
|
},
|
|
71
132
|
[]
|
|
72
133
|
);
|
|
73
134
|
|
|
74
135
|
const handleSelect = (e: ChangeEvent<HTMLInputElement>) => {
|
|
75
|
-
const
|
|
76
|
-
const queued:
|
|
136
|
+
const picked = Array.from(e.target.files ?? []);
|
|
137
|
+
const queued: QueuedFile[] = picked.map(f => ({
|
|
77
138
|
file: f,
|
|
78
|
-
relPath: (f
|
|
139
|
+
relPath: deriveRelPath(f),
|
|
79
140
|
progress: 0,
|
|
80
141
|
}));
|
|
81
142
|
enqueue(queued);
|
|
82
|
-
e.target.value =
|
|
143
|
+
e.target.value = '';
|
|
83
144
|
};
|
|
84
145
|
|
|
85
146
|
const handleDrop = async (e: React.DragEvent) => {
|
|
86
147
|
e.preventDefault();
|
|
87
|
-
|
|
88
|
-
|
|
148
|
+
e.stopPropagation();
|
|
149
|
+
const dropped = await extractFromDataTransfer(e.dataTransfer);
|
|
150
|
+
if (dropped.length) enqueue(dropped);
|
|
89
151
|
setDrag(false);
|
|
90
152
|
};
|
|
91
153
|
|
|
154
|
+
const openPicker = (e: React.MouseEvent) => {
|
|
155
|
+
// Only fire when the dropzone background itself is clicked, not children.
|
|
156
|
+
if (e.target !== e.currentTarget && !(e.target as HTMLElement).closest('[data-dz-passthrough]')) {
|
|
157
|
+
// children opt in via data-dz-passthrough if they should still open the picker
|
|
158
|
+
}
|
|
159
|
+
const ref = pickMode === 'folder' ? dirInputRef : fileInputRef;
|
|
160
|
+
ref.current?.click();
|
|
161
|
+
};
|
|
162
|
+
|
|
92
163
|
const handleUploadAll = async () => {
|
|
93
164
|
if (!queue.length) {
|
|
94
|
-
setMessage(
|
|
165
|
+
setMessage('No files queued.');
|
|
95
166
|
return;
|
|
96
167
|
}
|
|
97
168
|
setBusy(true);
|
|
98
169
|
setMessage(null);
|
|
99
170
|
const targetDir = creatingNew
|
|
100
|
-
? (newDirName.trim().replace(/^\/+|\/+$/g,
|
|
171
|
+
? (newDirName.trim().replace(/^\/+|\/+$/g, '') + '/')
|
|
101
172
|
: uploadDir;
|
|
102
173
|
try {
|
|
103
174
|
for (const { file, relPath } of queue) {
|
|
@@ -109,11 +180,11 @@ export default function Upload({ onUploaded }: UploadProps) {
|
|
|
109
180
|
);
|
|
110
181
|
}
|
|
111
182
|
setQueue([]);
|
|
112
|
-
await onUploaded();
|
|
113
|
-
setMessage(
|
|
114
|
-
load()
|
|
183
|
+
await onUploaded(); // SecureFiles passes filesHook.load here
|
|
184
|
+
setMessage('Upload complete');
|
|
185
|
+
// (removed the duplicate load() — onUploaded already does it)
|
|
115
186
|
} catch {
|
|
116
|
-
setMessage(
|
|
187
|
+
setMessage('Upload failed');
|
|
117
188
|
} finally {
|
|
118
189
|
setBusy(false);
|
|
119
190
|
}
|
|
@@ -122,25 +193,22 @@ export default function Upload({ onUploaded }: UploadProps) {
|
|
|
122
193
|
// — directory selector handlers —
|
|
123
194
|
const handleDirChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
|
124
195
|
const v = e.target.value;
|
|
125
|
-
if (v ===
|
|
196
|
+
if (v === '__new__') {
|
|
126
197
|
setCreatingNew(true);
|
|
127
|
-
setNewDirName(
|
|
198
|
+
setNewDirName('');
|
|
128
199
|
} else {
|
|
129
200
|
setCreatingNew(false);
|
|
130
201
|
setUploadDir(v);
|
|
131
202
|
}
|
|
132
203
|
};
|
|
133
204
|
const acceptNewDir = () => {
|
|
134
|
-
// normalize like before
|
|
135
205
|
const cleanName = newDirName.trim().replace(/^\/+|\/+$/g, '');
|
|
206
|
+
if (!cleanName) {
|
|
207
|
+
setCreatingNew(false);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
136
210
|
const normalized = `${uploadDir}${cleanName}/`;
|
|
137
|
-
|
|
138
|
-
setDirs(prev => {
|
|
139
|
-
if (prev.includes(normalized)) return prev;
|
|
140
|
-
return [...prev, normalized].sort();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// ② select it & flip back to the <select>
|
|
211
|
+
setDirs(prev => prev.includes(normalized) ? prev : [...prev, normalized].sort());
|
|
144
212
|
setUploadDir(normalized);
|
|
145
213
|
setCreatingNew(false);
|
|
146
214
|
setNewDirName('');
|
|
@@ -148,14 +216,39 @@ export default function Upload({ onUploaded }: UploadProps) {
|
|
|
148
216
|
|
|
149
217
|
return (
|
|
150
218
|
<div className="form-container max-w-2xl mx-auto">
|
|
219
|
+
{/* pick-mode toggle */}
|
|
220
|
+
<div className="flex gap-4 text-xs text-gray-700 mb-1">
|
|
221
|
+
<label className="flex items-center gap-1">
|
|
222
|
+
<input
|
|
223
|
+
type="radio"
|
|
224
|
+
name="pick-mode"
|
|
225
|
+
checked={pickMode === 'files'}
|
|
226
|
+
onChange={() => setPickMode('files')}
|
|
227
|
+
/>
|
|
228
|
+
Files
|
|
229
|
+
</label>
|
|
230
|
+
<label className="flex items-center gap-1">
|
|
231
|
+
<input
|
|
232
|
+
type="radio"
|
|
233
|
+
name="pick-mode"
|
|
234
|
+
checked={pickMode === 'folder'}
|
|
235
|
+
onChange={() => setPickMode('folder')}
|
|
236
|
+
/>
|
|
237
|
+
Folder
|
|
238
|
+
</label>
|
|
239
|
+
<span className="text-gray-400">
|
|
240
|
+
(drag-and-drop accepts either)
|
|
241
|
+
</span>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
151
244
|
{/* drop area */}
|
|
152
245
|
<div
|
|
153
246
|
className={`flex items-center border-2 border-dashed rounded-lg p-6 bg-white shadow-md space-x-4 transition-shadow duration-150
|
|
154
247
|
${drag ? 'border-indigo-500 bg-indigo-50 shadow-lg' : 'border-gray-300 hover:shadow-lg'}`}
|
|
155
|
-
onClick={
|
|
156
|
-
onDragEnter={e => { e.preventDefault(); setDrag(true); }}
|
|
157
|
-
onDragOver={e => e.preventDefault()}
|
|
158
|
-
onDragLeave={()
|
|
248
|
+
onClick={openPicker}
|
|
249
|
+
onDragEnter={e => { e.preventDefault(); e.stopPropagation(); setDrag(true); }}
|
|
250
|
+
onDragOver={e => { e.preventDefault(); e.stopPropagation(); }}
|
|
251
|
+
onDragLeave={e => { e.preventDefault(); e.stopPropagation(); setDrag(false); }}
|
|
159
252
|
onDrop={handleDrop}
|
|
160
253
|
role="region"
|
|
161
254
|
aria-label="File upload area"
|
|
@@ -169,6 +262,9 @@ export default function Upload({ onUploaded }: UploadProps) {
|
|
|
169
262
|
<p className="text-base font-semibold text-gray-700">
|
|
170
263
|
Drag & drop files or folders here
|
|
171
264
|
</p>
|
|
265
|
+
<p className="text-xs text-gray-500 mt-1">
|
|
266
|
+
Click to {pickMode === 'folder' ? 'choose a folder' : 'choose files'}
|
|
267
|
+
</p>
|
|
172
268
|
{queue.length > 0 && (
|
|
173
269
|
<p className="mt-1 text-sm text-gray-600">
|
|
174
270
|
{queue.length} item{queue.length > 1 && 's'} queued
|
|
@@ -177,19 +273,28 @@ export default function Upload({ onUploaded }: UploadProps) {
|
|
|
177
273
|
</div>
|
|
178
274
|
</div>
|
|
179
275
|
|
|
180
|
-
{/* hidden file input */}
|
|
276
|
+
{/* hidden file input — plain multi-file pick, works everywhere */}
|
|
181
277
|
<input
|
|
182
|
-
ref={
|
|
278
|
+
ref={fileInputRef}
|
|
183
279
|
type="file"
|
|
184
280
|
multiple
|
|
185
281
|
className="hidden"
|
|
186
|
-
{
|
|
282
|
+
onChange={handleSelect}
|
|
283
|
+
/>
|
|
284
|
+
|
|
285
|
+
{/* hidden directory input — Chromium, Safari 11.1+, Firefox 50+ */}
|
|
286
|
+
<input
|
|
287
|
+
ref={dirInputRef}
|
|
288
|
+
type="file"
|
|
289
|
+
multiple
|
|
290
|
+
className="hidden"
|
|
291
|
+
// non-standard attributes; both forms keep older Firefox happy
|
|
292
|
+
{...({ webkitdirectory: '', directory: '' } as any)}
|
|
187
293
|
onChange={handleSelect}
|
|
188
294
|
/>
|
|
189
295
|
|
|
190
296
|
{/* bottom controls */}
|
|
191
297
|
<div className="flex items-center justify-between mt-4">
|
|
192
|
-
{/* directory selector / new dir input */}
|
|
193
298
|
<div className="flex-1 mr-4">
|
|
194
299
|
{!creatingNew ? (
|
|
195
300
|
<select
|
|
@@ -220,7 +325,6 @@ export default function Upload({ onUploaded }: UploadProps) {
|
|
|
220
325
|
)}
|
|
221
326
|
</div>
|
|
222
327
|
|
|
223
|
-
{/* upload button */}
|
|
224
328
|
<Button
|
|
225
329
|
color="blue"
|
|
226
330
|
variant="primary"
|
|
@@ -233,15 +337,13 @@ export default function Upload({ onUploaded }: UploadProps) {
|
|
|
233
337
|
|
|
234
338
|
{message && (
|
|
235
339
|
<p
|
|
236
|
-
className={`text-center text-sm ${message.includes('failed') ? 'text-red-600' : 'text-gray-600'
|
|
237
|
-
}`}
|
|
340
|
+
className={`text-center text-sm ${message.includes('failed') ? 'text-red-600' : 'text-gray-600'}`}
|
|
238
341
|
role="alert"
|
|
239
342
|
>
|
|
240
343
|
{message}
|
|
241
344
|
</p>
|
|
242
345
|
)}
|
|
243
346
|
|
|
244
|
-
{/* queued list */}
|
|
245
347
|
{queue.length > 0 && (
|
|
246
348
|
<div className="wm-container mt-4">
|
|
247
349
|
<h3 className="mb-2 text-sm font-semibold text-gray-700">Queued Files</h3>
|
|
@@ -264,4 +366,4 @@ export default function Upload({ onUploaded }: UploadProps) {
|
|
|
264
366
|
)}
|
|
265
367
|
</div>
|
|
266
368
|
);
|
|
267
|
-
}
|
|
369
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { requireToken, alertit, fetchIt, stripPrefixes, make_path } from "./../imports";
|
|
2
2
|
export type { FileItem, ApiRow } from "./../types";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { requireToken,alertit } from "./../imports";
|
|
@@ -1,20 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {triggerDownload} from './trigger_download';
|
|
3
|
-
|
|
4
|
-
const token: string = requireToken();
|
|
5
|
-
const body = item
|
|
6
|
-
const headers = {
|
|
7
|
-
Authorization: `Bearer ${token}`,
|
|
8
|
-
Accept: "application/octet-stream",
|
|
9
|
-
"Content-Type": "application/json"
|
|
10
|
-
}
|
|
11
|
-
const blob = await secureFetchIt( "download", body, "POST", headers, true, true) as Blob;
|
|
1
|
+
import type { FileItem } from './../imports';
|
|
2
|
+
import { triggerDownload } from './trigger_download';
|
|
3
|
+
import { SECURE_FILES_API_BASE } from './../../cookieFetch';
|
|
12
4
|
|
|
5
|
+
export async function downloadSelect(item: FileItem): Promise<void> {
|
|
6
|
+
const res = await fetch(`${SECURE_FILES_API_BASE}/download`, {
|
|
7
|
+
method: "POST",
|
|
8
|
+
credentials: "include",
|
|
9
|
+
headers: {
|
|
10
|
+
"Content-Type": "application/json",
|
|
11
|
+
Accept: "application/octet-stream",
|
|
12
|
+
},
|
|
13
|
+
body: JSON.stringify(item),
|
|
14
|
+
});
|
|
13
15
|
|
|
16
|
+
if (res.status === 401) throw new Error("Not authenticated – please log in.");
|
|
17
|
+
if (!res.ok) throw new Error(`Download failed: HTTP ${res.status}`);
|
|
14
18
|
|
|
19
|
+
const blob = await res.blob();
|
|
15
20
|
if (!blob || blob.size === 0) {
|
|
16
|
-
throw new Error(`Received empty file data for
|
|
21
|
+
throw new Error(`Received empty file data for "${item.filename}"`);
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
triggerDownload(blob, item.filename);
|
|
20
|
-
}
|
|
25
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
2
|
export type { FileItem,ApiRow } from './../imports';
|
package/src/hooks/imports.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export type { FileItem,ApiRow } from './../imports';
|
|
2
|
-
export {
|
|
2
|
+
export { alertit,useCallback,isLoggedIn} from './../imports';
|
|
3
3
|
export {toggleShare,removeFile,listFiles,downloadFile} from './../file_utils'
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
|
|
2
2
|
export type { FileItem,ApiRow } from './../imports';
|
|
3
|
-
export {
|
|
3
|
+
export { alertit,toggleShare,removeFile,listFiles,useCallback,downloadFile,isLoggedIn } from './../imports';
|
package/src/imports.ts
CHANGED
|
@@ -11,9 +11,8 @@ export {
|
|
|
11
11
|
stripPrefixes,
|
|
12
12
|
make_path
|
|
13
13
|
} from "@putkoff/abstract-utilities";
|
|
14
|
-
export {
|
|
14
|
+
export {
|
|
15
15
|
Button,
|
|
16
16
|
Checkbox,
|
|
17
17
|
Input,
|
|
18
|
-
Spinner} from './UI';
|
|
19
|
-
export { secureFetchIt, fetchSharePatch } from "@putkoff/abstract-apis";
|
|
18
|
+
Spinner} from './UI';
|