@putkoff/abstract-securefiles 0.0.33 → 0.0.35
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/css.d.ts +0 -0
- package/dist/App/SecureFiles.d.ts +4 -0
- package/dist/App/index.d.ts +1 -0
- package/dist/FileHandlers/FileRow/imports.d.ts +2 -0
- package/dist/FileHandlers/FileRow/index.d.ts +1 -0
- package/dist/FileHandlers/FileRow/src/index.d.ts +1 -0
- package/dist/FileHandlers/FileRow/src/row_utils.d.ts +2 -0
- package/dist/FileHandlers/FileTable/imports.d.ts +4 -0
- package/dist/FileHandlers/FileTable/index.d.ts +1 -0
- package/dist/FileHandlers/FileTable/src/index.d.ts +1 -0
- package/dist/FileHandlers/FileTable/src/table_utils.d.ts +2 -0
- package/dist/FileHandlers/ShareModal/imports.d.ts +2 -0
- package/dist/FileHandlers/ShareModal/index.d.ts +1 -0
- package/dist/FileHandlers/ShareModal/src/index.d.ts +1 -0
- package/dist/FileHandlers/ShareModal/src/share_utils.d.ts +2 -0
- package/dist/FileHandlers/Upload/imports.d.ts +2 -0
- package/dist/FileHandlers/Upload/index.d.ts +1 -0
- package/dist/FileHandlers/Upload/src/index.d.ts +2 -0
- package/dist/FileHandlers/Upload/src/upload_utils.d.ts +2 -0
- package/dist/FileHandlers/imports.d.ts +4 -0
- package/dist/FileHandlers/index.d.ts +4 -0
- package/dist/UI/index.d.ts +28 -0
- package/dist/file_utils/imports.d.ts +2 -0
- package/dist/file_utils/index.d.ts +1 -0
- package/dist/file_utils/src/cookieFetch.d.ts +22 -0
- package/dist/file_utils/src/delete_utils/imports.d.ts +1 -0
- package/dist/file_utils/src/delete_utils/index.d.ts +1 -0
- package/dist/file_utils/src/delete_utils/src/delete_row.d.ts +2 -0
- package/dist/file_utils/src/delete_utils/src/delete_select.d.ts +4 -0
- package/dist/file_utils/src/delete_utils/src/delete_selected.d.ts +2 -0
- package/dist/file_utils/src/delete_utils/src/index.d.ts +3 -0
- package/dist/file_utils/src/download_utils/imports.d.ts +2 -0
- package/dist/file_utils/src/download_utils/index.d.ts +1 -0
- package/dist/file_utils/src/download_utils/src/direct_download.d.ts +3 -0
- package/dist/file_utils/src/download_utils/src/download_file.d.ts +6 -0
- package/dist/file_utils/src/download_utils/src/download_select.d.ts +2 -0
- package/dist/file_utils/src/download_utils/src/index.d.ts +5 -0
- package/dist/file_utils/src/download_utils/src/target_downloads.d.ts +4 -0
- package/dist/file_utils/src/download_utils/src/trigger_download.d.ts +2 -0
- package/dist/file_utils/src/imports.d.ts +2 -0
- package/dist/file_utils/src/index.d.ts +6 -0
- package/dist/file_utils/src/list_utils/imports.d.ts +2 -0
- package/dist/file_utils/src/list_utils/index.d.ts +1 -0
- package/dist/file_utils/src/list_utils/src/index.d.ts +1 -0
- package/dist/file_utils/src/list_utils/src/list_utils.d.ts +2 -0
- package/dist/file_utils/src/select_utils/index.d.ts +1 -0
- package/dist/file_utils/src/select_utils/src/index.d.ts +3 -0
- package/dist/file_utils/src/select_utils/src/init_select_all.d.ts +2 -0
- package/dist/file_utils/src/select_utils/src/sync_header_checkbox.d.ts +5 -0
- package/dist/file_utils/src/select_utils/src/toggle_checkboxes.d.ts +2 -0
- package/dist/file_utils/src/share_utils/imports.d.ts +2 -0
- package/dist/file_utils/src/share_utils/index.d.ts +1 -0
- package/dist/file_utils/src/share_utils/src/index.d.ts +1 -0
- package/dist/file_utils/src/share_utils/src/share_utils.d.ts +7 -0
- package/{package.tmp.json → dist/file_utils/src/upload_utils/imports.d.ts} +0 -0
- package/dist/file_utils/src/upload_utils/index.d.ts +1 -0
- package/dist/file_utils/src/upload_utils/src/index.d.ts +3 -0
- package/dist/file_utils/src/upload_utils/src/patch_file_settings.d.ts +2 -0
- package/dist/file_utils/src/upload_utils/src/upload_file.d.ts +2 -0
- package/dist/file_utils/src/upload_utils/src/upload_files.d.ts +5 -0
- package/dist/hooks/imports.d.ts +3 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useFiles/imports.d.ts +2 -0
- package/dist/hooks/useFiles/index.d.ts +1 -0
- package/dist/hooks/useFiles/src/deleteFile.d.ts +3 -0
- package/dist/hooks/useFiles/src/index.d.ts +6 -0
- package/dist/hooks/useFiles/src/load.d.ts +3 -0
- package/dist/hooks/useFiles/src/patchShareSettings.d.ts +3 -0
- package/dist/hooks/useFiles/src/toggleShare.d.ts +3 -0
- package/dist/hooks/useFiles/src/useFiles.d.ts +13 -0
- package/dist/hooks/useFiles/src/useSaveShareSettings.d.ts +7 -0
- package/dist/imports.d.ts +7 -0
- package/dist/index.cjs +992 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +945 -10
- package/dist/index.js.map +1 -1
- package/dist/styles/index.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/types.d.ts +75 -0
- package/index.ts +0 -0
- package/package.json +4 -4
- package/postcss.config.cjs +0 -0
- package/rollup.config.js +0 -0
- package/rollup.config.mjs +0 -0
- package/src/App/SecureFiles.tsx +45 -0
- package/src/App/index.ts +1 -0
- package/src/FileHandlers/FileRow/imports.ts +2 -0
- package/src/FileHandlers/FileRow/index.ts +1 -0
- package/src/FileHandlers/FileRow/src/index.ts +1 -0
- package/src/FileHandlers/FileRow/src/row_utils.tsx +143 -0
- package/src/FileHandlers/FileTable/imports.ts +16 -0
- package/src/FileHandlers/FileTable/index.ts +1 -0
- package/src/FileHandlers/FileTable/src/index.ts +1 -0
- package/src/FileHandlers/FileTable/src/table_utils.tsx +251 -0
- package/src/FileHandlers/ShareModal/imports.ts +2 -0
- package/src/FileHandlers/ShareModal/index.ts +1 -0
- package/src/FileHandlers/ShareModal/src/index.ts +1 -0
- package/src/FileHandlers/ShareModal/src/share_utils.tsx +155 -0
- package/src/FileHandlers/Upload/imports.ts +3 -0
- package/src/FileHandlers/Upload/index.ts +1 -0
- package/src/FileHandlers/Upload/src/index.ts +2 -0
- package/src/FileHandlers/Upload/src/upload_utils.tsx +267 -0
- package/src/FileHandlers/imports.ts +18 -0
- package/src/FileHandlers/index.ts +4 -0
- package/src/UI/index.tsx +122 -0
- package/src/custom.d.ts +7 -0
- package/src/file_utils/imports.ts +2 -0
- package/src/file_utils/index.ts +1 -0
- package/src/file_utils/src/cookieFetch.ts +78 -0
- package/src/file_utils/src/delete_utils/imports.ts +1 -0
- package/src/file_utils/src/delete_utils/index.ts +1 -0
- package/src/file_utils/src/delete_utils/src/delete_row.ts +11 -0
- package/src/file_utils/src/delete_utils/src/delete_select.ts +10 -0
- package/src/file_utils/src/delete_utils/src/delete_selected.ts +15 -0
- package/src/file_utils/src/delete_utils/src/index.ts +3 -0
- package/src/file_utils/src/download_utils/imports.ts +3 -0
- package/src/file_utils/src/download_utils/index.ts +1 -0
- package/src/file_utils/src/download_utils/src/direct_download.ts +22 -0
- package/src/file_utils/src/download_utils/src/download_file.ts +25 -0
- package/src/file_utils/src/download_utils/src/download_select.ts +20 -0
- package/src/file_utils/src/download_utils/src/index.ts +5 -0
- package/src/file_utils/src/download_utils/src/target_downloads.ts +21 -0
- package/src/file_utils/src/download_utils/src/trigger_download.ts +14 -0
- package/src/file_utils/src/imports.ts +3 -0
- package/src/file_utils/src/index.ts +6 -0
- package/src/file_utils/src/list_utils/imports.ts +2 -0
- package/src/file_utils/src/list_utils/index.ts +1 -0
- package/src/file_utils/src/list_utils/src/index.ts +1 -0
- package/src/file_utils/src/list_utils/src/list_utils.ts +28 -0
- package/src/file_utils/src/select_utils/index.ts +1 -0
- package/src/file_utils/src/select_utils/src/index.ts +3 -0
- package/src/file_utils/src/select_utils/src/init_select_all.ts +37 -0
- package/src/file_utils/src/select_utils/src/sync_header_checkbox.ts +18 -0
- package/src/file_utils/src/select_utils/src/toggle_checkboxes.ts +11 -0
- package/src/file_utils/src/share_utils/imports.ts +2 -0
- package/src/file_utils/src/share_utils/index.ts +1 -0
- package/src/file_utils/src/share_utils/src/index.ts +1 -0
- package/src/file_utils/src/share_utils/src/share_utils.ts +20 -0
- package/src/file_utils/src/upload_utils/imports.ts +1 -0
- package/src/file_utils/src/upload_utils/index.ts +1 -0
- package/src/file_utils/src/upload_utils/src/index.ts +3 -0
- package/src/file_utils/src/upload_utils/src/patch_file_settings.ts +20 -0
- package/src/file_utils/src/upload_utils/src/upload_file.ts +27 -0
- package/src/file_utils/src/upload_utils/src/upload_files.ts +19 -0
- package/src/global.d.ts +1 -0
- package/src/hooks/imports.ts +3 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useFiles/imports.ts +3 -0
- package/src/hooks/useFiles/index.ts +1 -0
- package/src/hooks/useFiles/src/deleteFile.ts +23 -0
- package/src/hooks/useFiles/src/index.ts +6 -0
- package/src/hooks/useFiles/src/load.ts +39 -0
- package/src/hooks/useFiles/src/logs/clipit_logs.log +0 -0
- package/src/hooks/useFiles/src/logs/routes_bp.log +0 -0
- package/src/hooks/useFiles/src/patchShareSettings.ts +27 -0
- package/src/hooks/useFiles/src/toggleShare.ts +28 -0
- package/src/hooks/useFiles/src/useFiles.ts +35 -0
- package/src/hooks/useFiles/src/useSaveShareSettings.ts +31 -0
- package/src/imports.ts +19 -0
- package/src/index.ts +8 -1
- package/src/styles/{App.css → SecureFiles/App.css} +0 -0
- package/src/styles/{index.css → SecureFiles/index.css} +0 -0
- package/src/styles/SecureFiles/logs/clipit_logs.log +0 -0
- package/src/styles/SecureFiles/logs/routes_bp.log +0 -0
- package/src/styles/{tailwind.css → SecureFiles/tailwind.css} +0 -0
- package/src/styles/{window-manager.css → SecureFiles/window-manager.css} +0 -0
- package/src/styles/SecureFilesStyles.module.css +287 -0
- package/src/styles/index.ts +1 -0
- package/src/types/index.ts +1 -0
- package/src/types/src/index.ts +1 -0
- package/src/types/src/types.ts +88 -0
- package/tailwind.config.cjs +0 -0
- package/testit.mjs +0 -0
- package/testit.ts +0 -0
- package/tsconfig.json +0 -0
- package/.github/workflows/release.yml +0 -17
- package/dist/secure_files.d.ts +0 -4
- package/src/secure_files.tsx +0 -41
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { FileItem, FileTableProps } from './../imports';
|
|
2
|
+
import React, { useState, useMemo, ChangeEvent } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
useFiles,
|
|
5
|
+
downloadFile,
|
|
6
|
+
Spinner,
|
|
7
|
+
ShareModal,
|
|
8
|
+
Input,
|
|
9
|
+
Button,
|
|
10
|
+
currentUsername
|
|
11
|
+
} from './../imports';
|
|
12
|
+
import { downloadSelected } from './../../../file_utils';
|
|
13
|
+
|
|
14
|
+
// Tree node type
|
|
15
|
+
interface FileNode {
|
|
16
|
+
name: string;
|
|
17
|
+
path: string;
|
|
18
|
+
children: Map<string, FileNode>;
|
|
19
|
+
files: FileItem[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Build a directory tree from a flat file list
|
|
23
|
+
function buildTree(files: FileItem[]): FileNode {
|
|
24
|
+
const root: FileNode = { name: '', path: '', children: new Map(), files: [] };
|
|
25
|
+
files.forEach(file => {
|
|
26
|
+
const parts = file.filepath.split('/');
|
|
27
|
+
let node = root;
|
|
28
|
+
parts.slice(0, -1).forEach((seg, idx) => {
|
|
29
|
+
const subPath = parts.slice(0, idx + 1).join('/');
|
|
30
|
+
if (!node.children.has(seg)) {
|
|
31
|
+
node.children.set(seg, { name: seg, path: subPath, children: new Map(), files: [] });
|
|
32
|
+
}
|
|
33
|
+
node = node.children.get(seg)!;
|
|
34
|
+
});
|
|
35
|
+
node.files.push(file);
|
|
36
|
+
});
|
|
37
|
+
return root;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Collect all file IDs under a node (recursive)
|
|
41
|
+
function collectNodeIds(node: FileNode): Array<number | string> {
|
|
42
|
+
let ids: Array<number | string> = [];
|
|
43
|
+
node.files.forEach(f => ids.push(f.id));
|
|
44
|
+
node.children.forEach(child => {
|
|
45
|
+
ids = ids.concat(collectNodeIds(child));
|
|
46
|
+
});
|
|
47
|
+
return ids;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default function FileTable({
|
|
51
|
+
files,
|
|
52
|
+
loading,
|
|
53
|
+
error,
|
|
54
|
+
onToggleShare,
|
|
55
|
+
onPatchShare,
|
|
56
|
+
onDelete
|
|
57
|
+
}: FileTableProps) {
|
|
58
|
+
const { load } = useFiles();
|
|
59
|
+
const [search, setSearch] = useState('');
|
|
60
|
+
const [modalFile, setModalFile] = useState<FileItem | null>(null);
|
|
61
|
+
const [selected, setSelected] = useState<Set<number | string>>(new Set());
|
|
62
|
+
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
|
63
|
+
|
|
64
|
+
// Upload controls
|
|
65
|
+
const [uploadDir, setUploadDir] = useState<string>('');
|
|
66
|
+
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
|
67
|
+
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => setUploadFile(e.target.files?.[0] ?? null);
|
|
68
|
+
const handleUpload = async () => {
|
|
69
|
+
if (!uploadFile) return;
|
|
70
|
+
const form = new FormData();
|
|
71
|
+
form.append('file', uploadFile);
|
|
72
|
+
const dirPart = uploadDir ? `${uploadDir}/` : '';
|
|
73
|
+
const url = `/api/secure-files/upload/${dirPart}${encodeURIComponent(uploadFile.name)}`;
|
|
74
|
+
await fetch(url, { method: 'POST', credentials: 'include', body: form });
|
|
75
|
+
setUploadFile(null);
|
|
76
|
+
load();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Filter files by search term
|
|
80
|
+
const filtered = useMemo(
|
|
81
|
+
() => files.filter(f => f.filename.toLowerCase().includes(search.trim().toLowerCase())),
|
|
82
|
+
[files, search]
|
|
83
|
+
);
|
|
84
|
+
// Build tree and directory paths list
|
|
85
|
+
const tree = useMemo(() => buildTree(filtered), [filtered]);
|
|
86
|
+
const allDirPaths = useMemo(() => {
|
|
87
|
+
const paths: string[] = [''];
|
|
88
|
+
function collect(node: FileNode) {
|
|
89
|
+
node.children.forEach(child => {
|
|
90
|
+
paths.push(child.path);
|
|
91
|
+
collect(child);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
collect(tree);
|
|
95
|
+
return paths;
|
|
96
|
+
}, [tree]);
|
|
97
|
+
|
|
98
|
+
// Selection & expand/collapse logic
|
|
99
|
+
const allIds = filtered.map(f => f.id);
|
|
100
|
+
const toggleAllSelection = () => selected.size < allIds.length ? setSelected(new Set(allIds)) : setSelected(new Set());
|
|
101
|
+
const toggleDirOpen = (path: string) => setExpanded(prev => {
|
|
102
|
+
const next = new Set(prev);
|
|
103
|
+
next.has(path) ? next.delete(path) : next.add(path);
|
|
104
|
+
return next;
|
|
105
|
+
});
|
|
106
|
+
const expandAll = () => setExpanded(new Set(allDirPaths));
|
|
107
|
+
const collapseAll = () => setExpanded(new Set());
|
|
108
|
+
|
|
109
|
+
// Recursive renderer: directory row with select-all checkbox, then its files, then subdirectories
|
|
110
|
+
const renderNode = (node: FileNode, depth = 0): React.ReactNode[] => {
|
|
111
|
+
const rows: React.ReactNode[] = [];
|
|
112
|
+
if (node.path) {
|
|
113
|
+
const isOpen = expanded.has(node.path);
|
|
114
|
+
const nodeIds = collectNodeIds(node);
|
|
115
|
+
const allSelected = nodeIds.every(id => selected.has(id));
|
|
116
|
+
const someSelected = nodeIds.some(id => selected.has(id));
|
|
117
|
+
rows.push(
|
|
118
|
+
<tr key={node.path}>
|
|
119
|
+
{/* Directory-level checkbox */}
|
|
120
|
+
<td className="px-4 py-2 bg-gray-100">
|
|
121
|
+
<input
|
|
122
|
+
type="checkbox"
|
|
123
|
+
checked={allSelected}
|
|
124
|
+
ref={el => { if (el) el.indeterminate = !allSelected && someSelected; }}
|
|
125
|
+
onChange={() => {
|
|
126
|
+
setSelected(prev => {
|
|
127
|
+
const next = new Set(prev);
|
|
128
|
+
if (allSelected) {
|
|
129
|
+
nodeIds.forEach(id => next.delete(id));
|
|
130
|
+
} else {
|
|
131
|
+
nodeIds.forEach(id => next.add(id));
|
|
132
|
+
}
|
|
133
|
+
return next;
|
|
134
|
+
});
|
|
135
|
+
}}
|
|
136
|
+
/>
|
|
137
|
+
</td>
|
|
138
|
+
{/* Directory label pushed left */}
|
|
139
|
+
<td className="px-4 py-2 bg-gray-100 font-bold" colSpan={5} style={{ textAlign: 'left' }}>
|
|
140
|
+
{node.path}
|
|
141
|
+
</td>
|
|
142
|
+
{/* Expand/collapse ticker on right */}
|
|
143
|
+
<td className="px-4 py-2 bg-gray-100 text-right">
|
|
144
|
+
<button onClick={() => toggleDirOpen(node.path)}>
|
|
145
|
+
{isOpen ? '▾' : '▸'}
|
|
146
|
+
</button>
|
|
147
|
+
</td>
|
|
148
|
+
</tr>
|
|
149
|
+
);
|
|
150
|
+
if (!isOpen) return rows;
|
|
151
|
+
}
|
|
152
|
+
// Render files in this directory
|
|
153
|
+
node.files.sort((a, b) => a.filename.localeCompare(b.filename)).forEach(f => {
|
|
154
|
+
const isOwner = f.uploader_id.toLowerCase() === (currentUsername() ?? '').toLowerCase();
|
|
155
|
+
const canDownload = isOwner || f.shareable;
|
|
156
|
+
rows.push(
|
|
157
|
+
<tr key={f.id}>
|
|
158
|
+
<td className="px-4 py-2">
|
|
159
|
+
<input
|
|
160
|
+
type="checkbox"
|
|
161
|
+
checked={selected.has(f.id)}
|
|
162
|
+
onChange={() => setSelected(prev => {
|
|
163
|
+
const next = new Set(prev);
|
|
164
|
+
next.has(f.id) ? next.delete(f.id) : next.add(f.id);
|
|
165
|
+
return next;
|
|
166
|
+
})}
|
|
167
|
+
/>
|
|
168
|
+
</td>
|
|
169
|
+
<td className="px-4 py-2" style={{ paddingLeft: (depth + 1) * 20 }}>
|
|
170
|
+
{f.filename}
|
|
171
|
+
</td>
|
|
172
|
+
<td className="px-4 py-2">
|
|
173
|
+
{canDownload ? <Button variant="secondary" onClick={() => downloadFile(f)}>Download</Button> : <span>🔒</span>}
|
|
174
|
+
</td>
|
|
175
|
+
<td className="px-4 py-2">
|
|
176
|
+
{isOwner && <Button variant="icon" onClick={() => setModalFile(f)}>⚙️</Button>}
|
|
177
|
+
</td>
|
|
178
|
+
<td className="px-4 py-2">
|
|
179
|
+
{isOwner && <Button variant={f.shareable ? 'primary' : 'secondary'} onClick={() => onToggleShare(f)}>{f.shareable ? 'Sharing' : 'Share'}</Button>}
|
|
180
|
+
</td>
|
|
181
|
+
<td className="px-4 py-2">
|
|
182
|
+
{isOwner && <Button variant="icon" onClick={() => onDelete(f).then(load)}>🗑</Button>}
|
|
183
|
+
</td>
|
|
184
|
+
<td className="px-4 py-2">
|
|
185
|
+
{f.download_count} / {f.download_limit ?? '∞'}
|
|
186
|
+
</td>
|
|
187
|
+
</tr>
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
// Render subdirectories
|
|
191
|
+
Array.from(node.children.values())
|
|
192
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
193
|
+
.forEach(child => rows.push(...renderNode(child, depth + 1)));
|
|
194
|
+
return rows;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Batch actions
|
|
198
|
+
const handleDownloadAll = async () => {
|
|
199
|
+
const inputs = filtered.filter(f => selected.has(f.id)).map(f => document.createElement('input'));
|
|
200
|
+
inputs.forEach((cb, idx) => cb.value = filtered.filter(f => selected.has(f.id))[idx].filepath);
|
|
201
|
+
await downloadSelected(inputs);
|
|
202
|
+
};
|
|
203
|
+
const handleDeleteAll = async () => { await onDelete(filtered.filter(f => selected.has(f.id))); setSelected(new Set()); await load(); };
|
|
204
|
+
const handleShareAll = async () => { for (const f of filtered.filter(f => selected.has(f.id))) await onToggleShare(f); };
|
|
205
|
+
|
|
206
|
+
if (loading) return <Spinner />;
|
|
207
|
+
if (error) return <p className="text-red-600">{error.message}</p>;
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<>
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
{/* Search & actions */}
|
|
214
|
+
<div className="mb-4 flex flex-col md:flex-row md:justify-between gap-4">
|
|
215
|
+
<Input label="Search files" type="text" value={search} onChange={(e:any) => setSearch(e.target.value)} placeholder="Enter filename…" className="form-input flex-1" />
|
|
216
|
+
<div className="flex gap-2">
|
|
217
|
+
<Button onClick={expandAll} disabled={!allDirPaths.length}>Expand All</Button>
|
|
218
|
+
<Button onClick={collapseAll} disabled={!allDirPaths.length}>Collapse All</Button>
|
|
219
|
+
<Button onClick={handleDownloadAll} disabled={!selected.size}>Download Selected</Button>
|
|
220
|
+
<Button color="gray" onClick={handleDeleteAll} disabled={!selected.size}>Delete Selected</Button>
|
|
221
|
+
<Button onClick={handleShareAll} disabled={!selected.size}>Share Selected</Button>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
{/* File tree table */}
|
|
226
|
+
<div className="overflow-x-auto wm-container">
|
|
227
|
+
<table className="file-table w-full">
|
|
228
|
+
<thead className="wm-thead">
|
|
229
|
+
<tr>
|
|
230
|
+
<th className="px-4 py-2 w-12"><input type="checkbox" checked={selected.size === filtered.length && filtered.length > 0} onChange={toggleAllSelection} /></th>
|
|
231
|
+
<th className="px-4 py-2">Filename</th>
|
|
232
|
+
<th className="px-4 py-2">Download</th>
|
|
233
|
+
<th className="px-4 py-2">Settings</th>
|
|
234
|
+
<th className="px-4 py-2">Share</th>
|
|
235
|
+
<th className="px-4 py-2">Delete</th>
|
|
236
|
+
<th className="px-4 py-2">Downloads</th>
|
|
237
|
+
</tr>
|
|
238
|
+
</thead>
|
|
239
|
+
<tbody>{renderNode(tree)}</tbody>
|
|
240
|
+
</table>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<ShareModal file={modalFile} onClose={() => setModalFile(null)} onSave={async patch => {
|
|
244
|
+
if (!modalFile) return;
|
|
245
|
+
const updated: FileItem = { ...modalFile, ...patch };
|
|
246
|
+
await onPatchShare(updated);
|
|
247
|
+
setModalFile(null);
|
|
248
|
+
}} />
|
|
249
|
+
</>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {ShareModal} from './src';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {default as ShareModal} from './share_utils';
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ShareModalProps
|
|
3
|
+
} from './../../imports';
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
Checkbox,
|
|
7
|
+
Input,
|
|
8
|
+
ClipboardIcon,
|
|
9
|
+
createPortal,
|
|
10
|
+
useEffect,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from './../imports';
|
|
14
|
+
|
|
15
|
+
export default function ShareModal({ file, onClose, onSave }: ShareModalProps) {
|
|
16
|
+
const [fileId, setFileId] = useState<string | number | "">("");
|
|
17
|
+
const [enabled, setEnabled] = useState<any>(false);
|
|
18
|
+
const [password, setPassword] = useState<any>(undefined);
|
|
19
|
+
const [strPassword, setStrPassword] = useState<any>("");
|
|
20
|
+
const [download_limit, setDownloadLimit] = useState<number | "">(0);
|
|
21
|
+
const [error, setError] = useState("");
|
|
22
|
+
const [busy, setBusy] = useState(false);
|
|
23
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
24
|
+
|
|
25
|
+
const shareLink =
|
|
26
|
+
file && enabled
|
|
27
|
+
? `/api/secure-files/secure-download/${file.id}`
|
|
28
|
+
: "";
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!file) return;
|
|
32
|
+
setFileId(file.id);
|
|
33
|
+
setEnabled(file.shareable);
|
|
34
|
+
setPassword(file.share_password);
|
|
35
|
+
setStrPassword(file.password_str ?? "");
|
|
36
|
+
setDownloadLimit(file.download_limit ?? 0);
|
|
37
|
+
setError("");
|
|
38
|
+
|
|
39
|
+
modalRef.current?.focus();
|
|
40
|
+
}, [file]);
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const h = (e: KeyboardEvent) => e.key === "Escape" && onClose();
|
|
45
|
+
window.addEventListener("keydown", h);
|
|
46
|
+
return () => window.removeEventListener("keydown", h);
|
|
47
|
+
}, [onClose]);
|
|
48
|
+
|
|
49
|
+
if (!file) return null;
|
|
50
|
+
|
|
51
|
+
async function handleSave() {
|
|
52
|
+
if (strPassword && strPassword.length < 4) {
|
|
53
|
+
setError("Password must be at least 4 characters");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
setBusy(true);
|
|
57
|
+
const save_dict = {
|
|
58
|
+
id: fileId,
|
|
59
|
+
shareable: enabled,
|
|
60
|
+
needsPassword: !!strPassword,
|
|
61
|
+
share_password: strPassword || null,
|
|
62
|
+
password_str: strPassword || null,
|
|
63
|
+
download_limit: download_limit === "" ? null : Number(download_limit),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const resp = await onSave(save_dict);
|
|
68
|
+
|
|
69
|
+
onClose();
|
|
70
|
+
} catch (err) {
|
|
71
|
+
setError(`${err} Failed to save settings. Please try again.`);
|
|
72
|
+
} finally {
|
|
73
|
+
setBusy(false);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return createPortal(
|
|
78
|
+
<>
|
|
79
|
+
<div
|
|
80
|
+
className="modal-backdrop z-100"
|
|
81
|
+
onClick={(e) => e.target === e.currentTarget && onClose()}
|
|
82
|
+
/>
|
|
83
|
+
<div className="modal-wrapper z-110">
|
|
84
|
+
<div
|
|
85
|
+
ref={modalRef}
|
|
86
|
+
role="dialog"
|
|
87
|
+
aria-modal="true"
|
|
88
|
+
tabIndex={-1}
|
|
89
|
+
className="modal-panel"
|
|
90
|
+
>
|
|
91
|
+
<h3 className="form-title">
|
|
92
|
+
Share settings for <span className="font-mono">{file.filename}</span>
|
|
93
|
+
</h3>
|
|
94
|
+
|
|
95
|
+
{error && <p className="form-error">{error}</p>}
|
|
96
|
+
|
|
97
|
+
<Checkbox
|
|
98
|
+
label="Enable sharing"
|
|
99
|
+
checked={enabled}
|
|
100
|
+
onChange={(e:any) => setEnabled(e.target.checked)}
|
|
101
|
+
className="checkbox"
|
|
102
|
+
/>
|
|
103
|
+
|
|
104
|
+
<Input
|
|
105
|
+
label="Password (optional)"
|
|
106
|
+
type="text"
|
|
107
|
+
value={strPassword}
|
|
108
|
+
onChange={(e:any) => setStrPassword(e.target.value)}
|
|
109
|
+
disabled={!enabled}
|
|
110
|
+
className="form-input"
|
|
111
|
+
/>
|
|
112
|
+
<Input
|
|
113
|
+
label="Download limit (0 = ∞)"
|
|
114
|
+
type="number"
|
|
115
|
+
min={0}
|
|
116
|
+
value={download_limit}
|
|
117
|
+
onChange={(e:any) =>
|
|
118
|
+
setDownloadLimit(e.target.value === "" ? "" : Number(e.target.value))
|
|
119
|
+
}
|
|
120
|
+
disabled={!enabled}
|
|
121
|
+
className="form-input"
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
{enabled && shareLink && (
|
|
125
|
+
<Input
|
|
126
|
+
label="Share link"
|
|
127
|
+
readOnly
|
|
128
|
+
value={shareLink}
|
|
129
|
+
className="form-input"
|
|
130
|
+
trailing={
|
|
131
|
+
<Button
|
|
132
|
+
variant="icon"
|
|
133
|
+
className="btn-icon"
|
|
134
|
+
onClick={() => navigator.clipboard.writeText(shareLink)}
|
|
135
|
+
>
|
|
136
|
+
<ClipboardIcon className="h-5 w-5" />
|
|
137
|
+
</Button>
|
|
138
|
+
}
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
<div className="mt-6 flex justify-end gap-2">
|
|
143
|
+
<Button color="green" variant="primary" onClick={handleSave} disabled={busy} className="btn-primary">
|
|
144
|
+
{busy ? "Saving…" : "Save"}
|
|
145
|
+
</Button>
|
|
146
|
+
<Button variant="secondary" onClick={onClose} disabled={busy} className="btn-secondary">
|
|
147
|
+
Cancel
|
|
148
|
+
</Button>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</>,
|
|
153
|
+
document.body
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {Upload} from './src';
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
ChangeEvent,
|
|
3
|
+
useRef,
|
|
4
|
+
useState,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import {
|
|
9
|
+
CloudArrowUpIcon,
|
|
10
|
+
Button,
|
|
11
|
+
uploadFiles,
|
|
12
|
+
useFiles,
|
|
13
|
+
currentUsername
|
|
14
|
+
} from './../imports';
|
|
15
|
+
import type { QueuedFile, UploadProps } from './../imports';
|
|
16
|
+
|
|
17
|
+
export default function Upload({ onUploaded }: UploadProps) {
|
|
18
|
+
const [queue, setQueue] = useState<QueuedFile[]>([]);
|
|
19
|
+
const [busy, setBusy] = useState(false);
|
|
20
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
21
|
+
const [drag, setDrag] = useState(false);
|
|
22
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
23
|
+
const [dirs, setDirs] = useState<string[]>([]);
|
|
24
|
+
|
|
25
|
+
// — existing files → directory list —
|
|
26
|
+
const { files, load } = useFiles();
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const dirSet = new Set<string>();
|
|
30
|
+
files.forEach(f => {
|
|
31
|
+
const parts = f.filepath.split('/');
|
|
32
|
+
if (parts.length > 1) {
|
|
33
|
+
parts.pop();
|
|
34
|
+
dirSet.add(parts.join('/') + '/');
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
setDirs(Array.from(dirSet).sort());
|
|
38
|
+
}, [files]);
|
|
39
|
+
|
|
40
|
+
// — directory selection state —
|
|
41
|
+
const [uploadDir, setUploadDir] = useState<string>('');
|
|
42
|
+
const [creatingNew, setCreatingNew] = useState(false);
|
|
43
|
+
const [newDirName, setNewDirName] = useState('');
|
|
44
|
+
|
|
45
|
+
// — queue logic —
|
|
46
|
+
const enqueue = useCallback((files: QueuedFile[]) => {
|
|
47
|
+
setQueue(prev => {
|
|
48
|
+
const map = new Map(prev.map(q => [`${q.file.name}-${q.file.size}`, q]));
|
|
49
|
+
files.forEach(q => map.set(`${q.file.name}-${q.file.size}`, q));
|
|
50
|
+
return Array.from(map.values());
|
|
51
|
+
});
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const extractFromDataTransfer = useCallback(
|
|
55
|
+
async (dt: DataTransfer): Promise<QueuedFile[]> => {
|
|
56
|
+
const out: QueuedFile[] = [];
|
|
57
|
+
const walk = async (entry: any, path = "") => {
|
|
58
|
+
if (!entry) return;
|
|
59
|
+
if (entry.isFile) {
|
|
60
|
+
const file: File = await new Promise(res => entry.file(res));
|
|
61
|
+
out.push({ file, relPath: path, progress: 0 });
|
|
62
|
+
} else if (entry.isDirectory) {
|
|
63
|
+
const reader = entry.createReader();
|
|
64
|
+
const entries: any[] = await new Promise(r => reader.readEntries(r));
|
|
65
|
+
await Promise.all(entries.map(e => walk(e, `${path}${entry.name}/`)));
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
await Promise.all(Array.from(dt.items).map(i => walk(i.webkitGetAsEntry())));
|
|
69
|
+
return out;
|
|
70
|
+
},
|
|
71
|
+
[]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const handleSelect = (e: ChangeEvent<HTMLInputElement>) => {
|
|
75
|
+
const files = Array.from(e.target.files || []);
|
|
76
|
+
const queued: any = files.map(f => ({
|
|
77
|
+
file: f,
|
|
78
|
+
relPath: (f as any).webkitRelativePath.replace(/[^/]*$/, ""),
|
|
79
|
+
progress: 0,
|
|
80
|
+
}));
|
|
81
|
+
enqueue(queued);
|
|
82
|
+
e.target.value = "";
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const handleDrop = async (e: React.DragEvent) => {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
const files = await extractFromDataTransfer(e.dataTransfer);
|
|
88
|
+
enqueue(files.length ? files : []);
|
|
89
|
+
setDrag(false);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handleUploadAll = async () => {
|
|
93
|
+
if (!queue.length) {
|
|
94
|
+
setMessage("No files queued.");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
setBusy(true);
|
|
98
|
+
setMessage(null);
|
|
99
|
+
const targetDir = creatingNew
|
|
100
|
+
? (newDirName.trim().replace(/^\/+|\/+$/g, "") + "/")
|
|
101
|
+
: uploadDir;
|
|
102
|
+
try {
|
|
103
|
+
for (const { file, relPath } of queue) {
|
|
104
|
+
await uploadFiles(file, targetDir + relPath);
|
|
105
|
+
setQueue(cur =>
|
|
106
|
+
cur.map(item =>
|
|
107
|
+
item.file === file ? { ...item, progress: 100 } : item
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
setQueue([]);
|
|
112
|
+
await onUploaded();
|
|
113
|
+
setMessage("Upload complete");
|
|
114
|
+
load();
|
|
115
|
+
} catch {
|
|
116
|
+
setMessage("Upload failed");
|
|
117
|
+
} finally {
|
|
118
|
+
setBusy(false);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// — directory selector handlers —
|
|
123
|
+
const handleDirChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
|
124
|
+
const v = e.target.value;
|
|
125
|
+
if (v === "__new__") {
|
|
126
|
+
setCreatingNew(true);
|
|
127
|
+
setNewDirName("");
|
|
128
|
+
} else {
|
|
129
|
+
setCreatingNew(false);
|
|
130
|
+
setUploadDir(v);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const acceptNewDir = () => {
|
|
134
|
+
// normalize like before
|
|
135
|
+
const cleanName = newDirName.trim().replace(/^\/+|\/+$/g, '');
|
|
136
|
+
const normalized = `${uploadDir}${cleanName}/`;
|
|
137
|
+
// ① add it to our dirs list if it isn't there already
|
|
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>
|
|
144
|
+
setUploadDir(normalized);
|
|
145
|
+
setCreatingNew(false);
|
|
146
|
+
setNewDirName('');
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div className="form-container max-w-2xl mx-auto">
|
|
151
|
+
{/* drop area */}
|
|
152
|
+
<div
|
|
153
|
+
className={`flex items-center border-2 border-dashed rounded-lg p-6 bg-white shadow-md space-x-4 transition-shadow duration-150
|
|
154
|
+
${drag ? 'border-indigo-500 bg-indigo-50 shadow-lg' : 'border-gray-300 hover:shadow-lg'}`}
|
|
155
|
+
onClick={() => inputRef.current?.click()}
|
|
156
|
+
onDragEnter={e => { e.preventDefault(); setDrag(true); }}
|
|
157
|
+
onDragOver={e => e.preventDefault()}
|
|
158
|
+
onDragLeave={() => setDrag(false)}
|
|
159
|
+
onDrop={handleDrop}
|
|
160
|
+
role="region"
|
|
161
|
+
aria-label="File upload area"
|
|
162
|
+
>
|
|
163
|
+
<CloudArrowUpIcon
|
|
164
|
+
style={{ width: 300, height: 300 }}
|
|
165
|
+
className="text-gray-500 mb-3"
|
|
166
|
+
aria-hidden="true"
|
|
167
|
+
/>
|
|
168
|
+
<div className="flex-1">
|
|
169
|
+
<p className="text-base font-semibold text-gray-700">
|
|
170
|
+
Drag & drop files or folders here
|
|
171
|
+
</p>
|
|
172
|
+
{queue.length > 0 && (
|
|
173
|
+
<p className="mt-1 text-sm text-gray-600">
|
|
174
|
+
{queue.length} item{queue.length > 1 && 's'} queued
|
|
175
|
+
</p>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
{/* hidden file input */}
|
|
181
|
+
<input
|
|
182
|
+
ref={inputRef}
|
|
183
|
+
type="file"
|
|
184
|
+
multiple
|
|
185
|
+
className="hidden"
|
|
186
|
+
{...({ webkitdirectory: true, directory: '' } as any)}
|
|
187
|
+
onChange={handleSelect}
|
|
188
|
+
/>
|
|
189
|
+
|
|
190
|
+
{/* bottom controls */}
|
|
191
|
+
<div className="flex items-center justify-between mt-4">
|
|
192
|
+
{/* directory selector / new dir input */}
|
|
193
|
+
<div className="flex-1 mr-4">
|
|
194
|
+
{!creatingNew ? (
|
|
195
|
+
<select
|
|
196
|
+
value={uploadDir}
|
|
197
|
+
onChange={handleDirChange}
|
|
198
|
+
className="form-select w-full"
|
|
199
|
+
>
|
|
200
|
+
{dirs.map(d => (
|
|
201
|
+
<option key={d} value={d}>{d}</option>
|
|
202
|
+
))}
|
|
203
|
+
<option value="__new__">+ Create new directory…</option>
|
|
204
|
+
</select>
|
|
205
|
+
) : (
|
|
206
|
+
<div className="flex">
|
|
207
|
+
<input
|
|
208
|
+
type="text"
|
|
209
|
+
value={newDirName}
|
|
210
|
+
placeholder="new-folder-name"
|
|
211
|
+
onChange={e => setNewDirName(e.target.value)}
|
|
212
|
+
onBlur={acceptNewDir}
|
|
213
|
+
onKeyDown={e => e.key === 'Enter' && acceptNewDir()}
|
|
214
|
+
className="form-input flex-1"
|
|
215
|
+
/>
|
|
216
|
+
<Button onClick={acceptNewDir} className="ml-2">
|
|
217
|
+
OK
|
|
218
|
+
</Button>
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
{/* upload button */}
|
|
224
|
+
<Button
|
|
225
|
+
color="blue"
|
|
226
|
+
variant="primary"
|
|
227
|
+
onClick={handleUploadAll}
|
|
228
|
+
disabled={busy || !queue.length}
|
|
229
|
+
>
|
|
230
|
+
{busy ? 'Uploading…' : `Upload ${queue.length}`}
|
|
231
|
+
</Button>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
{message && (
|
|
235
|
+
<p
|
|
236
|
+
className={`text-center text-sm ${message.includes('failed') ? 'text-red-600' : 'text-gray-600'
|
|
237
|
+
}`}
|
|
238
|
+
role="alert"
|
|
239
|
+
>
|
|
240
|
+
{message}
|
|
241
|
+
</p>
|
|
242
|
+
)}
|
|
243
|
+
|
|
244
|
+
{/* queued list */}
|
|
245
|
+
{queue.length > 0 && (
|
|
246
|
+
<div className="wm-container mt-4">
|
|
247
|
+
<h3 className="mb-2 text-sm font-semibold text-gray-700">Queued Files</h3>
|
|
248
|
+
<ul className="max-h-64 space-y-4 overflow-y-auto">
|
|
249
|
+
{queue.map(({ file, relPath, progress }) => (
|
|
250
|
+
<li key={`${file.name}-${file.size}`} className="space-y-1">
|
|
251
|
+
<div className="flex justify-between items-center">
|
|
252
|
+
<span className="truncate text-sm text-gray-800">
|
|
253
|
+
{relPath}{file.name}
|
|
254
|
+
</span>
|
|
255
|
+
<span className="text-xs text-gray-500">{progress}%</span>
|
|
256
|
+
</div>
|
|
257
|
+
<div className="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
|
|
258
|
+
<div className="bg-indigo-600 h-2" style={{ width: `${progress}%` }} />
|
|
259
|
+
</div>
|
|
260
|
+
</li>
|
|
261
|
+
))}
|
|
262
|
+
</ul>
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
}
|