@putkoff/abstract-securefiles 0.0.48 → 0.0.50
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/ConfirmButton.d.ts +5 -0
- package/dist/CopyButton.d.ts +4 -0
- package/dist/FolderSelect.d.ts +6 -0
- package/dist/SecureFiles.d.ts +3 -0
- package/dist/ServerFiles.d.ts +6 -0
- package/dist/ShareDialog.d.ts +6 -0
- package/dist/UploadPanel.d.ts +6 -0
- package/dist/api.d.ts +47 -0
- package/dist/index.cjs +529 -1004
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -8
- package/dist/index.js +522 -951
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +60 -0
- package/package.json +15 -11
- package/src/ConfirmButton.tsx +35 -0
- package/src/CopyButton.tsx +24 -0
- package/src/FolderSelect.tsx +68 -0
- package/src/SecureFiles.tsx +346 -0
- package/src/ServerFiles.tsx +339 -0
- package/src/ShareDialog.tsx +148 -0
- package/src/UploadPanel.tsx +217 -0
- package/src/api.ts +108 -0
- package/src/index.ts +4 -8
- package/src/styles.css +442 -0
- package/src/types.ts +56 -0
- package/css.d.ts +0 -2
- package/dist/App/SecureFiles.d.ts +0 -4
- package/dist/App/index.d.ts +0 -1
- package/dist/FileHandlers/FileRow/imports.d.ts +0 -2
- package/dist/FileHandlers/FileRow/index.d.ts +0 -1
- package/dist/FileHandlers/FileRow/src/index.d.ts +0 -1
- package/dist/FileHandlers/FileRow/src/row_utils.d.ts +0 -2
- package/dist/FileHandlers/FileTable/imports.d.ts +0 -4
- package/dist/FileHandlers/FileTable/index.d.ts +0 -1
- package/dist/FileHandlers/FileTable/src/index.d.ts +0 -1
- package/dist/FileHandlers/FileTable/src/table_utils.d.ts +0 -2
- package/dist/FileHandlers/ShareModal/imports.d.ts +0 -2
- package/dist/FileHandlers/ShareModal/index.d.ts +0 -1
- package/dist/FileHandlers/ShareModal/src/index.d.ts +0 -1
- package/dist/FileHandlers/ShareModal/src/share_utils.d.ts +0 -2
- package/dist/FileHandlers/Upload/imports.d.ts +0 -2
- package/dist/FileHandlers/Upload/index.d.ts +0 -1
- package/dist/FileHandlers/Upload/src/index.d.ts +0 -2
- package/dist/FileHandlers/Upload/src/upload_utils.d.ts +0 -2
- package/dist/FileHandlers/imports.d.ts +0 -4
- package/dist/FileHandlers/index.d.ts +0 -4
- package/dist/UI/index.d.ts +0 -28
- package/dist/file_utils/imports.d.ts +0 -2
- package/dist/file_utils/index.d.ts +0 -1
- package/dist/file_utils/src/cookieFetch.d.ts +0 -33
- package/dist/file_utils/src/delete_utils/imports.d.ts +0 -1
- package/dist/file_utils/src/delete_utils/index.d.ts +0 -1
- package/dist/file_utils/src/delete_utils/src/delete_row.d.ts +0 -2
- package/dist/file_utils/src/delete_utils/src/delete_select.d.ts +0 -4
- package/dist/file_utils/src/delete_utils/src/delete_selected.d.ts +0 -2
- package/dist/file_utils/src/delete_utils/src/index.d.ts +0 -3
- package/dist/file_utils/src/download_utils/imports.d.ts +0 -2
- package/dist/file_utils/src/download_utils/index.d.ts +0 -1
- package/dist/file_utils/src/download_utils/src/direct_download.d.ts +0 -3
- package/dist/file_utils/src/download_utils/src/download_file.d.ts +0 -6
- package/dist/file_utils/src/download_utils/src/download_select.d.ts +0 -2
- package/dist/file_utils/src/download_utils/src/index.d.ts +0 -5
- package/dist/file_utils/src/download_utils/src/target_downloads.d.ts +0 -4
- package/dist/file_utils/src/download_utils/src/trigger_download.d.ts +0 -2
- package/dist/file_utils/src/imports.d.ts +0 -2
- package/dist/file_utils/src/index.d.ts +0 -6
- package/dist/file_utils/src/list_utils/imports.d.ts +0 -1
- package/dist/file_utils/src/list_utils/index.d.ts +0 -1
- package/dist/file_utils/src/list_utils/src/index.d.ts +0 -1
- package/dist/file_utils/src/list_utils/src/list_utils.d.ts +0 -2
- package/dist/file_utils/src/select_utils/index.d.ts +0 -1
- package/dist/file_utils/src/select_utils/src/index.d.ts +0 -3
- package/dist/file_utils/src/select_utils/src/init_select_all.d.ts +0 -2
- package/dist/file_utils/src/select_utils/src/sync_header_checkbox.d.ts +0 -5
- package/dist/file_utils/src/select_utils/src/toggle_checkboxes.d.ts +0 -2
- package/dist/file_utils/src/share_utils/imports.d.ts +0 -1
- package/dist/file_utils/src/share_utils/index.d.ts +0 -1
- package/dist/file_utils/src/share_utils/src/index.d.ts +0 -1
- package/dist/file_utils/src/share_utils/src/share_utils.d.ts +0 -7
- package/dist/file_utils/src/upload_utils/imports.d.ts +0 -0
- package/dist/file_utils/src/upload_utils/index.d.ts +0 -1
- package/dist/file_utils/src/upload_utils/src/index.d.ts +0 -3
- package/dist/file_utils/src/upload_utils/src/patch_file_settings.d.ts +0 -2
- package/dist/file_utils/src/upload_utils/src/upload_file.d.ts +0 -2
- package/dist/file_utils/src/upload_utils/src/upload_files.d.ts +0 -5
- package/dist/hooks/imports.d.ts +0 -3
- package/dist/hooks/index.d.ts +0 -1
- package/dist/hooks/useFiles/imports.d.ts +0 -2
- package/dist/hooks/useFiles/index.d.ts +0 -1
- package/dist/hooks/useFiles/src/deleteFile.d.ts +0 -3
- package/dist/hooks/useFiles/src/index.d.ts +0 -6
- package/dist/hooks/useFiles/src/load.d.ts +0 -3
- package/dist/hooks/useFiles/src/patchShareSettings.d.ts +0 -3
- package/dist/hooks/useFiles/src/toggleShare.d.ts +0 -3
- package/dist/hooks/useFiles/src/useFiles.d.ts +0 -13
- package/dist/hooks/useFiles/src/useSaveShareSettings.d.ts +0 -7
- package/dist/imports.d.ts +0 -6
- package/dist/index.css +0 -1
- package/dist/styles/index.d.ts +0 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/types.d.ts +0 -75
- package/index.ts +0 -1
- package/postcss.config.cjs +0 -8
- package/rollup.config.js +0 -38
- package/rollup.config.mjs +0 -35
- package/src/App/SecureFiles.tsx +0 -45
- package/src/App/index.ts +0 -1
- package/src/FileHandlers/FileRow/imports.ts +0 -2
- package/src/FileHandlers/FileRow/index.ts +0 -1
- package/src/FileHandlers/FileRow/src/index.ts +0 -1
- package/src/FileHandlers/FileRow/src/row_utils.tsx +0 -143
- package/src/FileHandlers/FileTable/imports.ts +0 -15
- package/src/FileHandlers/FileTable/index.ts +0 -1
- package/src/FileHandlers/FileTable/src/index.ts +0 -1
- package/src/FileHandlers/FileTable/src/table_utils.tsx +0 -251
- package/src/FileHandlers/ShareModal/imports.ts +0 -2
- package/src/FileHandlers/ShareModal/index.ts +0 -1
- package/src/FileHandlers/ShareModal/src/index.ts +0 -1
- package/src/FileHandlers/ShareModal/src/share_utils.tsx +0 -155
- package/src/FileHandlers/Upload/imports.ts +0 -3
- package/src/FileHandlers/Upload/index.ts +0 -1
- package/src/FileHandlers/Upload/src/index.ts +0 -2
- package/src/FileHandlers/Upload/src/upload_utils.tsx +0 -369
- package/src/FileHandlers/imports.ts +0 -17
- package/src/FileHandlers/index.ts +0 -4
- package/src/UI/index.tsx +0 -122
- package/src/custom.d.ts +0 -7
- package/src/file_utils/imports.ts +0 -2
- package/src/file_utils/index.ts +0 -1
- package/src/file_utils/src/cookieFetch.ts +0 -109
- package/src/file_utils/src/delete_utils/imports.ts +0 -1
- package/src/file_utils/src/delete_utils/index.ts +0 -1
- package/src/file_utils/src/delete_utils/src/delete_row.ts +0 -11
- package/src/file_utils/src/delete_utils/src/delete_select.ts +0 -10
- package/src/file_utils/src/delete_utils/src/delete_selected.ts +0 -15
- package/src/file_utils/src/delete_utils/src/index.ts +0 -3
- package/src/file_utils/src/download_utils/imports.ts +0 -3
- package/src/file_utils/src/download_utils/index.ts +0 -1
- package/src/file_utils/src/download_utils/src/direct_download.ts +0 -22
- package/src/file_utils/src/download_utils/src/download_file.ts +0 -25
- package/src/file_utils/src/download_utils/src/download_select.ts +0 -25
- package/src/file_utils/src/download_utils/src/index.ts +0 -5
- package/src/file_utils/src/download_utils/src/target_downloads.ts +0 -21
- package/src/file_utils/src/download_utils/src/trigger_download.ts +0 -14
- package/src/file_utils/src/imports.ts +0 -3
- package/src/file_utils/src/index.ts +0 -6
- package/src/file_utils/src/list_utils/imports.ts +0 -2
- package/src/file_utils/src/list_utils/index.ts +0 -1
- package/src/file_utils/src/list_utils/src/index.ts +0 -1
- package/src/file_utils/src/list_utils/src/list_utils.ts +0 -28
- package/src/file_utils/src/select_utils/index.ts +0 -1
- package/src/file_utils/src/select_utils/src/index.ts +0 -3
- package/src/file_utils/src/select_utils/src/init_select_all.ts +0 -37
- package/src/file_utils/src/select_utils/src/sync_header_checkbox.ts +0 -18
- package/src/file_utils/src/select_utils/src/toggle_checkboxes.ts +0 -11
- package/src/file_utils/src/share_utils/imports.ts +0 -1
- package/src/file_utils/src/share_utils/index.ts +0 -1
- package/src/file_utils/src/share_utils/src/index.ts +0 -1
- package/src/file_utils/src/share_utils/src/share_utils.ts +0 -20
- package/src/file_utils/src/upload_utils/imports.ts +0 -1
- package/src/file_utils/src/upload_utils/index.ts +0 -1
- package/src/file_utils/src/upload_utils/src/index.ts +0 -3
- package/src/file_utils/src/upload_utils/src/patch_file_settings.ts +0 -20
- package/src/file_utils/src/upload_utils/src/upload_file.ts +0 -27
- package/src/file_utils/src/upload_utils/src/upload_files.ts +0 -19
- package/src/hooks/imports.ts +0 -3
- package/src/hooks/index.ts +0 -1
- package/src/hooks/useFiles/imports.ts +0 -3
- package/src/hooks/useFiles/index.ts +0 -1
- package/src/hooks/useFiles/src/deleteFile.ts +0 -23
- package/src/hooks/useFiles/src/index.ts +0 -6
- package/src/hooks/useFiles/src/load.ts +0 -39
- 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 +0 -27
- package/src/hooks/useFiles/src/toggleShare.ts +0 -28
- package/src/hooks/useFiles/src/useFiles.ts +0 -35
- package/src/hooks/useFiles/src/useSaveShareSettings.ts +0 -31
- package/src/imports.ts +0 -18
- package/src/styles/SecureFiles/App.css +0 -42
- package/src/styles/SecureFiles/index.css +0 -10
- package/src/styles/SecureFiles/logs/clipit_logs.log +0 -0
- package/src/styles/SecureFiles/logs/routes_bp.log +0 -0
- package/src/styles/SecureFiles/tailwind.css +0 -3
- package/src/styles/SecureFiles/window-manager.css +0 -283
- package/src/styles/SecureFilesStyles.module.css +0 -287
- package/src/styles/index.ts +0 -1
- package/src/types/index.ts +0 -1
- package/src/types/src/index.ts +0 -1
- package/src/types/src/types.ts +0 -88
- package/tailwind.config.cjs +0 -7
- package/testit.mjs +0 -11
- package/testit.ts +0 -3
- package/tsconfig.json +0 -27
- /package/src/{global.d.ts → css.d.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,1040 +1,611 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
titleId,
|
|
10
|
-
...props
|
|
11
|
-
}, svgRef) {
|
|
12
|
-
return /*#__PURE__*/React.createElement("svg", Object.assign({
|
|
13
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
14
|
-
fill: "none",
|
|
15
|
-
viewBox: "0 0 24 24",
|
|
16
|
-
strokeWidth: 1.5,
|
|
17
|
-
stroke: "currentColor",
|
|
18
|
-
"aria-hidden": "true",
|
|
19
|
-
"data-slot": "icon",
|
|
20
|
-
ref: svgRef,
|
|
21
|
-
"aria-labelledby": titleId
|
|
22
|
-
}, props), title ? /*#__PURE__*/React.createElement("title", {
|
|
23
|
-
id: titleId
|
|
24
|
-
}, title) : null, /*#__PURE__*/React.createElement("path", {
|
|
25
|
-
strokeLinecap: "round",
|
|
26
|
-
strokeLinejoin: "round",
|
|
27
|
-
d: "M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184"
|
|
28
|
-
}));
|
|
29
|
-
}
|
|
30
|
-
const ForwardRef$1 = /*#__PURE__*/ React.forwardRef(ClipboardIcon);
|
|
31
|
-
|
|
32
|
-
function CloudArrowUpIcon({
|
|
33
|
-
title,
|
|
34
|
-
titleId,
|
|
35
|
-
...props
|
|
36
|
-
}, svgRef) {
|
|
37
|
-
return /*#__PURE__*/React.createElement("svg", Object.assign({
|
|
38
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
39
|
-
fill: "none",
|
|
40
|
-
viewBox: "0 0 24 24",
|
|
41
|
-
strokeWidth: 1.5,
|
|
42
|
-
stroke: "currentColor",
|
|
43
|
-
"aria-hidden": "true",
|
|
44
|
-
"data-slot": "icon",
|
|
45
|
-
ref: svgRef,
|
|
46
|
-
"aria-labelledby": titleId
|
|
47
|
-
}, props), title ? /*#__PURE__*/React.createElement("title", {
|
|
48
|
-
id: titleId
|
|
49
|
-
}, title) : null, /*#__PURE__*/React.createElement("path", {
|
|
50
|
-
strokeLinecap: "round",
|
|
51
|
-
strokeLinejoin: "round",
|
|
52
|
-
d: "M12 16.5V9.75m0 0 3 3m-3-3-3 3M6.75 19.5a4.5 4.5 0 0 1-1.41-8.775 5.25 5.25 0 0 1 10.233-2.33 3 3 0 0 1 3.758 3.848A3.752 3.752 0 0 1 18 19.5H6.75Z"
|
|
53
|
-
}));
|
|
54
|
-
}
|
|
55
|
-
const ForwardRef = /*#__PURE__*/ React.forwardRef(CloudArrowUpIcon);
|
|
56
|
-
|
|
57
|
-
// Utility to convert input to array
|
|
58
|
-
function ensure_list(obj) {
|
|
59
|
-
if (Array.isArray(obj))
|
|
60
|
-
return obj.filter(Boolean);
|
|
61
|
-
if (typeof obj === 'string')
|
|
62
|
-
return obj.split(' ').filter(Boolean);
|
|
63
|
-
return [];
|
|
64
|
-
}
|
|
65
|
-
// Get list and length safely
|
|
66
|
-
function getListAndLen(obj) {
|
|
67
|
-
const return_obj = ensure_list(obj);
|
|
68
|
-
const ob_len = return_obj.length;
|
|
69
|
-
return { return_obj, ob_len };
|
|
70
|
-
}
|
|
71
|
-
// Combine class names with defaults
|
|
72
|
-
function combineClassNames(classNames = [], defaultClasses = ['form-group', 'form-label', 'flex', 'gap-2']) {
|
|
73
|
-
const { return_obj: inputClasses } = getListAndLen(classNames);
|
|
74
|
-
const { return_obj: defaultClassList } = getListAndLen(defaultClasses);
|
|
75
|
-
// Combine and remove duplicates
|
|
76
|
-
const combined = [...new Set([...defaultClassList, ...inputClasses])];
|
|
77
|
-
console.log(`Combined classes (length: ${combined.length}): ${combined.join(' ')}`);
|
|
78
|
-
return combined;
|
|
79
|
-
}
|
|
80
|
-
function Button({ children, color = 'gray', variant = 'default', className = '', ...rest }) {
|
|
81
|
-
const variantClasses = {
|
|
82
|
-
default: 'btn',
|
|
83
|
-
icon: 'btn-icon',
|
|
84
|
-
primary: 'btn-primary',
|
|
85
|
-
secondary: 'btn-secondary',
|
|
86
|
-
};
|
|
87
|
-
const colorClasses = {
|
|
88
|
-
gray: '',
|
|
89
|
-
green: variant === 'primary' ? 'btn-success' : 'btn-secondary',
|
|
90
|
-
blue: variant === 'primary' ? 'btn-primary' : 'btn-secondary',
|
|
91
|
-
};
|
|
92
|
-
const classes = combineClassNames([variantClasses[variant], colorClasses[color], className]);
|
|
93
|
-
return (jsx("button", { className: classes.join(' '), ...rest, children: children }));
|
|
94
|
-
}
|
|
95
|
-
function Checkbox({ label, className = '', classNames = [], ...rest }) {
|
|
96
|
-
const classes = combineClassNames(classNames, ['checkbox', 'wm-checkbox']);
|
|
97
|
-
return (jsxs("label", { className: classes.join(' '), children: [jsx("input", { type: "checkbox", className: "wm-checkbox", ...rest }), label && jsx("span", { children: label })] }));
|
|
98
|
-
}
|
|
99
|
-
function Input({ label, trailing, className = '', classNames = [], ...rest }) {
|
|
100
|
-
const classes = combineClassNames(classNames, ['form-group', 'flex', 'gap-2']);
|
|
101
|
-
const inputClasses = combineClassNames(className, ['form-input']);
|
|
102
|
-
return (jsxs("label", { className: classes.join(' '), children: [label && jsx("span", { className: "form-label", children: label }), jsxs("div", { className: "flex gap-2", children: [jsx("input", { className: inputClasses.join(' '), ...rest }), trailing] })] }));
|
|
103
|
-
}
|
|
104
|
-
function Spinner() {
|
|
105
|
-
const classes = combineClassNames('animate-pulse');
|
|
106
|
-
return jsx("p", { className: classes.join(' '), children: "Loading\u2026" });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Helper function to trigger file download */
|
|
110
|
-
function triggerDownload(blob, filename) {
|
|
111
|
-
const url = URL.createObjectURL(blob);
|
|
112
|
-
const a = Object.assign(document.createElement("a"), {
|
|
113
|
-
href: url,
|
|
114
|
-
download: filename,
|
|
115
|
-
});
|
|
116
|
-
document.body.appendChild(a);
|
|
117
|
-
a.click();
|
|
118
|
-
a.remove();
|
|
119
|
-
URL.revokeObjectURL(url);
|
|
120
|
-
console.log(`✅ Successfully downloaded: ${filename}`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Non-React “direct download” used by some old links (kept for parity) */
|
|
124
|
-
async function directDownload(item) {
|
|
125
|
-
const token = localStorage.getItem("token");
|
|
126
|
-
if (!token)
|
|
127
|
-
throw new Error("No auth token found");
|
|
128
|
-
const res = await fetch("https://abstractendeavors.com/api/secure-files/secure-download/", {
|
|
129
|
-
method: "POST",
|
|
130
|
-
headers: {
|
|
131
|
-
Authorization: `Bearer ${token}`,
|
|
132
|
-
"Content-Type": "application/json",
|
|
133
|
-
Accept: "application/octet-stream",
|
|
134
|
-
},
|
|
135
|
-
body: JSON.stringify(item),
|
|
136
|
-
});
|
|
137
|
-
if (!res.ok)
|
|
138
|
-
throw new Error(`Download failed: ${res.status}`);
|
|
139
|
-
const blob = await res.blob();
|
|
140
|
-
triggerDownload(blob, item.filename);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Download a file using the current session cookie.
|
|
145
|
-
* No JWT / localStorage token required.
|
|
146
|
-
*/
|
|
147
|
-
async function downloadFile(item) {
|
|
148
|
-
const res = await fetch("/api/secure-files/download", {
|
|
149
|
-
method: "POST",
|
|
150
|
-
credentials: "include",
|
|
151
|
-
headers: { "Content-Type": "application/json" },
|
|
152
|
-
body: JSON.stringify({
|
|
153
|
-
id: item.id,
|
|
154
|
-
filepath: item.filepath,
|
|
155
|
-
filename: item.filename,
|
|
156
|
-
}),
|
|
157
|
-
});
|
|
158
|
-
if (res.status === 401)
|
|
159
|
-
throw new Error("Not authenticated – please log in.");
|
|
160
|
-
if (!res.ok)
|
|
161
|
-
throw new Error(`Download failed: HTTP ${res.status}`);
|
|
162
|
-
const blob = await res.blob();
|
|
163
|
-
triggerDownload(blob, item.filename || "download");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Cookie-authenticated API helpers.
|
|
168
|
-
* All requests include credentials: 'include' so the browser sends the
|
|
169
|
-
* session cookie (auth_session) automatically. No JWT tokens are used.
|
|
170
|
-
*
|
|
171
|
-
* API base URL resolution order (first truthy value wins):
|
|
172
|
-
* 1. window.__SECURE_FILES_API_BASE__ – runtime override (any bundler)
|
|
173
|
-
* 2. VITE_SECURE_FILES_API_BASE – Vite env var
|
|
174
|
-
* 3. REACT_APP_SECURE_FILES_API_BASE – Create React App env var
|
|
175
|
-
* 4. /api/secure-files – same-origin default
|
|
176
|
-
*
|
|
177
|
-
* Subdomain API example (set one of the above to the full origin + path):
|
|
178
|
-
* VITE_SECURE_FILES_API_BASE=https://api.example.com/secure-files
|
|
179
|
-
*/
|
|
180
|
-
function resolveApiBase() {
|
|
181
|
-
// 1. Runtime window config — works regardless of bundler
|
|
182
|
-
if (typeof window !== "undefined" && window.__SECURE_FILES_API_BASE__) {
|
|
183
|
-
return String(window.__SECURE_FILES_API_BASE__).replace(/\/+$/, "");
|
|
1
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_API_BASE = "https://api.abstractendeavors.com";
|
|
5
|
+
class ApiError extends Error {
|
|
6
|
+
constructor(message, status) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
184
9
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (v)
|
|
189
|
-
return String(v).replace(/\/+$/, "");
|
|
190
|
-
}
|
|
191
|
-
catch { /* not a Vite build */ }
|
|
192
|
-
// 3. Create React App / plain Node
|
|
10
|
+
}
|
|
11
|
+
async function errorFrom(res) {
|
|
12
|
+
let message = `${res.status} ${res.statusText}`;
|
|
193
13
|
try {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
return String(v).replace(/\/+$/, "");
|
|
14
|
+
const body = await res.json();
|
|
15
|
+
message = body?.message || body?.error || message;
|
|
197
16
|
}
|
|
198
|
-
catch {
|
|
199
|
-
|
|
200
|
-
return "/api/secure-files";
|
|
201
|
-
}
|
|
202
|
-
const API_BASE$1 = resolveApiBase();
|
|
203
|
-
function apiUrl(path) {
|
|
204
|
-
const p = path.startsWith("/") ? path : `/${path}`;
|
|
205
|
-
return `${API_BASE$1}${p}`;
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Generic JSON fetch with session-cookie auth.
|
|
209
|
-
* Throws Error("UNAUTHENTICATED") on 401 so callers can redirect to login.
|
|
210
|
-
*/
|
|
211
|
-
async function cookieFetch(path, body, method) {
|
|
212
|
-
const m = method ?? (body !== undefined ? "POST" : "GET");
|
|
213
|
-
const init = { method: m, credentials: "include" };
|
|
214
|
-
if (body !== undefined && body !== null) {
|
|
215
|
-
init.headers = { "Content-Type": "application/json" };
|
|
216
|
-
init.body = JSON.stringify(body);
|
|
17
|
+
catch {
|
|
18
|
+
/* non-JSON error body */
|
|
217
19
|
}
|
|
218
|
-
const res = await fetch(apiUrl(path), init);
|
|
219
20
|
if (res.status === 401)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const msg = await res.text().catch(() => res.statusText);
|
|
223
|
-
throw new Error(`HTTP ${res.status}: ${msg}`);
|
|
224
|
-
}
|
|
225
|
-
const ct = res.headers.get("Content-Type") ?? "";
|
|
226
|
-
return (ct.includes("application/json") ? res.json() : res.text());
|
|
21
|
+
message = "Please log in to use Secure Files.";
|
|
22
|
+
return new ApiError(message, res.status);
|
|
227
23
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
*/
|
|
233
|
-
async function checkAuth() {
|
|
234
|
-
try {
|
|
235
|
-
const res = await fetch(apiUrl("/userInfo"), {
|
|
236
|
-
method: "GET",
|
|
24
|
+
function createApi(apiBase = DEFAULT_API_BASE) {
|
|
25
|
+
const root = `${apiBase.replace(/\/+$/, "")}/secure-files`;
|
|
26
|
+
async function json(path, init = {}) {
|
|
27
|
+
const res = await fetch(`${root}${path}`, {
|
|
237
28
|
credentials: "include",
|
|
29
|
+
...init,
|
|
30
|
+
headers: { Accept: "application/json", ...(init.body ? { "Content-Type": "application/json" } : {}) },
|
|
238
31
|
});
|
|
239
|
-
|
|
32
|
+
if (!res.ok)
|
|
33
|
+
throw await errorFrom(res);
|
|
34
|
+
return res.json();
|
|
240
35
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
async function downloadSelect(item) {
|
|
270
|
-
const res = await fetch(`${SECURE_FILES_API_BASE}/download`, {
|
|
271
|
-
method: "POST",
|
|
272
|
-
credentials: "include",
|
|
273
|
-
headers: {
|
|
274
|
-
"Content-Type": "application/json",
|
|
275
|
-
Accept: "application/octet-stream",
|
|
36
|
+
return {
|
|
37
|
+
list: () => json("/userInfo"),
|
|
38
|
+
roots: () => json("/admin/roots"),
|
|
39
|
+
requestRoot: (path) => json("/admin/roots", { method: "POST", body: JSON.stringify({ path }) }),
|
|
40
|
+
confirmRoot: (nonce, signature, label) => json("/admin/roots/confirm", {
|
|
41
|
+
method: "POST",
|
|
42
|
+
body: JSON.stringify({ nonce, signature, label }),
|
|
43
|
+
}),
|
|
44
|
+
removeRoot: (id) => json(`/admin/roots/${id}`, { method: "DELETE" }),
|
|
45
|
+
browse: (path) => json(`/admin/browse?path=${encodeURIComponent(path)}`),
|
|
46
|
+
link: (paths, folder) => json("/admin/link", {
|
|
47
|
+
method: "POST",
|
|
48
|
+
body: JSON.stringify({ paths, folder }),
|
|
49
|
+
}),
|
|
50
|
+
updateShare: (update) => json("/files/share", { method: "PATCH", body: JSON.stringify(update) }),
|
|
51
|
+
remove: (ids) => json("/remove", {
|
|
52
|
+
method: "POST",
|
|
53
|
+
body: JSON.stringify({ ids }),
|
|
54
|
+
}),
|
|
55
|
+
/** Hand the download to the browser so it streams to disk (native progress + resume, no memory limit). */
|
|
56
|
+
download(ids) {
|
|
57
|
+
const a = document.createElement("a");
|
|
58
|
+
a.href = `${root}/download?ids=${ids.join(",")}`;
|
|
59
|
+
a.rel = "noopener";
|
|
60
|
+
document.body.appendChild(a);
|
|
61
|
+
a.click();
|
|
62
|
+
a.remove();
|
|
276
63
|
},
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Classic “select all rows” logic (still handy in non-React views
|
|
312
|
-
* such as the admin dashboard).
|
|
313
|
-
*/
|
|
314
|
-
function syncHeaderCheckbox() {
|
|
315
|
-
const all = document.querySelectorAll(".file-checkbox");
|
|
316
|
-
const checked = document.querySelectorAll(".file-checkbox:checked");
|
|
317
|
-
const header = document.getElementById("select-all-checkbox");
|
|
318
|
-
header.indeterminate =
|
|
319
|
-
checked.length > 0 && checked.length < all.length;
|
|
320
|
-
header.checked = checked.length === all.length;
|
|
64
|
+
upload(file, relPath, onProgress) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const form = new FormData();
|
|
67
|
+
form.append("file", file);
|
|
68
|
+
form.append("rel_path", relPath);
|
|
69
|
+
const xhr = new XMLHttpRequest();
|
|
70
|
+
xhr.open("POST", `${root}/upload`);
|
|
71
|
+
xhr.withCredentials = true;
|
|
72
|
+
xhr.upload.onprogress = (e) => {
|
|
73
|
+
if (e.lengthComputable)
|
|
74
|
+
onProgress?.(e.loaded / e.total);
|
|
75
|
+
};
|
|
76
|
+
xhr.onload = () => {
|
|
77
|
+
if (xhr.status >= 200 && xhr.status < 300)
|
|
78
|
+
return resolve();
|
|
79
|
+
let message = `Upload failed (${xhr.status})`;
|
|
80
|
+
try {
|
|
81
|
+
message = JSON.parse(xhr.responseText)?.message || message;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
/* non-JSON */
|
|
85
|
+
}
|
|
86
|
+
if (xhr.status === 401)
|
|
87
|
+
message = "Please log in to use Secure Files.";
|
|
88
|
+
reject(new ApiError(message, xhr.status));
|
|
89
|
+
};
|
|
90
|
+
xhr.onerror = () => reject(new ApiError("Network error during upload", 0));
|
|
91
|
+
xhr.send(form);
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
};
|
|
321
95
|
}
|
|
322
96
|
|
|
323
|
-
|
|
324
|
-
function
|
|
325
|
-
const
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
// 2) button → toggle all
|
|
335
|
-
btn.addEventListener("click", () => {
|
|
336
|
-
const cbs = Array.from(document.querySelectorAll(".file-checkbox"));
|
|
337
|
-
if (!cbs.length) {
|
|
338
|
-
alert("No files to select.");
|
|
339
|
-
return;
|
|
97
|
+
const NEW_FOLDER = "__new__";
|
|
98
|
+
function FolderSelect({ label, folders, value, onChange, }) {
|
|
99
|
+
const [creating, setCreating] = useState(false);
|
|
100
|
+
const [draft, setDraft] = useState("");
|
|
101
|
+
const [extra, setExtra] = useState([]);
|
|
102
|
+
const options = [...new Set([...folders, ...extra, ...(value ? [value] : [])])].sort();
|
|
103
|
+
const accept = () => {
|
|
104
|
+
const clean = draft.trim().replace(/^\/+|\/+$/g, "");
|
|
105
|
+
if (clean) {
|
|
106
|
+
setExtra((e) => (e.includes(clean) ? e : [...e, clean]));
|
|
107
|
+
onChange(clean);
|
|
340
108
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/** Utility for other screens: toggle a list of checkboxes on/off. */
|
|
353
|
-
function toggleCheckboxes(cbs) {
|
|
354
|
-
if (!cbs.length) {
|
|
355
|
-
alert("No files to select.");
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
const anyUnchecked = cbs.some((cb) => !cb.checked);
|
|
359
|
-
cbs.forEach((cb) => (cb.checked = anyUnchecked));
|
|
360
|
-
syncHeaderCheckbox();
|
|
109
|
+
setCreating(false);
|
|
110
|
+
setDraft("");
|
|
111
|
+
};
|
|
112
|
+
return (jsxs("div", { className: "sf-field", children: [jsx("span", { children: label }), creating ? (jsxs("div", { className: "sf-row", children: [jsx("input", { className: "sf-input", autoFocus: true, value: draft, placeholder: "new-folder-name (use / for subfolders)", onChange: (e) => setDraft(e.target.value), onKeyDown: (e) => {
|
|
113
|
+
if (e.key === "Enter")
|
|
114
|
+
accept();
|
|
115
|
+
if (e.key === "Escape")
|
|
116
|
+
setCreating(false);
|
|
117
|
+
} }), jsx("button", { type: "button", className: "sf-btn", onClick: accept, children: "OK" })] })) : (jsxs("select", { className: "sf-input", value: value, onChange: (e) => (e.target.value === NEW_FOLDER ? setCreating(true) : onChange(e.target.value)), children: [jsx("option", { value: "", children: "/ (top level)" }), options.map((d) => (jsxs("option", { value: d, children: [d, "/"] }, d))), jsx("option", { value: NEW_FOLDER, children: "+ Create new directory\u2026" })] }))] }));
|
|
361
118
|
}
|
|
362
119
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
async function patchFileSetting(fileId, field, value) {
|
|
366
|
-
const res = await fetch(`${API_BASE}/files/${fileId}/settings/${field}`, {
|
|
367
|
-
method: 'PATCH',
|
|
368
|
-
credentials: 'include',
|
|
369
|
-
headers: { 'Content-Type': 'application/json' },
|
|
370
|
-
body: JSON.stringify({ value }),
|
|
371
|
-
});
|
|
372
|
-
if (!res.ok) {
|
|
373
|
-
const err = await res.json().catch(() => ({}));
|
|
374
|
-
throw new Error(err.error || res.statusText);
|
|
375
|
-
}
|
|
376
|
-
return (await res.json());
|
|
120
|
+
function readEntries(reader) {
|
|
121
|
+
return new Promise((resolve, reject) => reader.readEntries(resolve, reject));
|
|
377
122
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const subdirInput = subdir || document.getElementById('subdir');
|
|
383
|
-
const banner = document.getElementById('banner');
|
|
384
|
-
if (!fileInput.files?.length) {
|
|
385
|
-
alert('No file selected.');
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
const f = fileInput.files[0];
|
|
389
|
-
const dir = (subdirInput?.value ?? '').trim();
|
|
390
|
-
const relPath = dir ? `${dir}/${f.name}` : f.name;
|
|
391
|
-
try {
|
|
392
|
-
const json = await cookieUpload(f, relPath);
|
|
393
|
-
if (banner)
|
|
394
|
-
banner.textContent = `Uploaded: ${json.filepath}`;
|
|
395
|
-
fileInput.value = '';
|
|
396
|
-
if (subdirInput)
|
|
397
|
-
subdirInput.value = '';
|
|
398
|
-
window.location.reload();
|
|
399
|
-
}
|
|
400
|
-
catch (err) {
|
|
401
|
-
console.error('Upload failed', err);
|
|
123
|
+
async function walkEntry(entry, prefix, out) {
|
|
124
|
+
if (entry.isFile) {
|
|
125
|
+
const file = await new Promise((resolve, reject) => entry.file(resolve, reject));
|
|
126
|
+
out.push({ file, relPath: prefix + file.name });
|
|
402
127
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const json = await cookieUpload(file, relPath);
|
|
412
|
-
if (!json?.filepath)
|
|
413
|
-
throw new Error('Server response missing filepath.');
|
|
414
|
-
return { filepath: json.filepath };
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
async function removeFile(item) {
|
|
418
|
-
const resp = await cookieFetch("remove", { id: item.id }, "POST");
|
|
419
|
-
if (!resp || resp === false || resp?.result === false) {
|
|
420
|
-
throw new Error(`Could not delete "${item.filename ?? item.id}"`);
|
|
128
|
+
else if (entry.isDirectory) {
|
|
129
|
+
const reader = entry.createReader();
|
|
130
|
+
let batch;
|
|
131
|
+
do {
|
|
132
|
+
batch = await readEntries(reader);
|
|
133
|
+
for (const child of batch)
|
|
134
|
+
await walkEntry(child, `${prefix}${entry.name}/`, out);
|
|
135
|
+
} while (batch.length > 0);
|
|
421
136
|
}
|
|
422
137
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
138
|
+
async function filesFromDrop(dt) {
|
|
139
|
+
const out = [];
|
|
140
|
+
const entries = Array.from(dt.items || [])
|
|
141
|
+
.map((item) => item.webkitGetAsEntry?.())
|
|
142
|
+
.filter(Boolean);
|
|
143
|
+
if (entries.length) {
|
|
144
|
+
for (const entry of entries)
|
|
145
|
+
await walkEntry(entry, "", out);
|
|
430
146
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
147
|
+
else {
|
|
148
|
+
for (const file of Array.from(dt.files))
|
|
149
|
+
out.push({ file, relPath: file.name });
|
|
434
150
|
}
|
|
151
|
+
return out;
|
|
435
152
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const row = cb.closest("tr");
|
|
440
|
-
alertit(row);
|
|
441
|
-
if (row && row?.id)
|
|
442
|
-
await removeFile(row);
|
|
153
|
+
function joinTarget(target, relPath) {
|
|
154
|
+
const clean = target.trim().replace(/^\/+|\/+$/g, "");
|
|
155
|
+
return clean ? `${clean}/${relPath}` : relPath;
|
|
443
156
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const result = await cookieFetch("list", undefined, "GET");
|
|
447
|
-
const apiRows = Array.isArray(result)
|
|
448
|
-
? result
|
|
449
|
-
: (result?.result ?? []);
|
|
450
|
-
const rows = apiRows.map((r) => ({
|
|
451
|
-
id: r.id,
|
|
452
|
-
filename: r.filename,
|
|
453
|
-
uploader_id: r.uploader_id,
|
|
454
|
-
filepath: r.filepath,
|
|
455
|
-
created_at: r.created_at,
|
|
456
|
-
download_count: r.download_count,
|
|
457
|
-
download_limit: r.download_limit,
|
|
458
|
-
shareable: Boolean(r.shareable),
|
|
459
|
-
needsPassword: !!r.share_password,
|
|
460
|
-
share_password: r.share_password,
|
|
461
|
-
password_str: r.password_str,
|
|
462
|
-
}));
|
|
463
|
-
if (setFiles)
|
|
464
|
-
setFiles(rows);
|
|
465
|
-
return rows;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* Toggle sharing on a file.
|
|
470
|
-
* Sends the updated settings to /api/secure-files/files/share.
|
|
471
|
-
* Does NOT update local state – the caller is responsible for that.
|
|
472
|
-
*/
|
|
473
|
-
async function toggleShare(file) {
|
|
474
|
-
const updated = { ...file, shareable: !file.shareable };
|
|
475
|
-
if (!updated.shareable)
|
|
476
|
-
updated.needsPassword = false;
|
|
477
|
-
await cookieFetch("files/share", {
|
|
478
|
-
id: updated.id,
|
|
479
|
-
shareable: updated.shareable,
|
|
480
|
-
share_password: updated.share_password ?? null,
|
|
481
|
-
password_str: updated.password_str ?? null,
|
|
482
|
-
download_limit: updated.download_limit ?? null,
|
|
483
|
-
}, "POST");
|
|
157
|
+
function CloudIcon() {
|
|
158
|
+
return (jsx("svg", { className: "sf-drop__icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.5, "aria-hidden": "true", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 16.5V9.75m0 0 3 3m-3-3-3 3M6.75 19.5a4.5 4.5 0 0 1-1.41-8.775 5.25 5.25 0 0 1 10.233-2.33 3 3 0 0 1 3.758 3.848A3.752 3.752 0 0 1 18 19.5H6.75Z" }) }));
|
|
484
159
|
}
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
160
|
+
function UploadPanel({ api, folders, onUploaded, }) {
|
|
161
|
+
const [queue, setQueue] = useState([]);
|
|
162
|
+
const [mode, setMode] = useState("files");
|
|
163
|
+
const [target, setTarget] = useState("");
|
|
164
|
+
const [dragging, setDragging] = useState(false);
|
|
165
|
+
const [busy, setBusy] = useState(false);
|
|
166
|
+
const [message, setMessage] = useState(null);
|
|
167
|
+
const fileInput = useRef(null);
|
|
168
|
+
const folderInput = useRef(null);
|
|
169
|
+
const enqueue = (items) => {
|
|
170
|
+
setMessage(null);
|
|
171
|
+
setQueue((q) => {
|
|
172
|
+
const kept = q.filter((x) => !x.done);
|
|
173
|
+
const seen = new Set(kept.map((x) => x.relPath));
|
|
174
|
+
return [...kept, ...items.filter((i) => !seen.has(i.relPath)).map((i) => ({ ...i, progress: 0 }))];
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
const openPicker = () => (mode === "folder" ? folderInput : fileInput).current?.click();
|
|
178
|
+
const fromInput = (e) => {
|
|
179
|
+
const files = Array.from(e.target.files || []);
|
|
180
|
+
enqueue(files.map((file) => ({ file, relPath: file.webkitRelativePath || file.name })));
|
|
181
|
+
e.target.value = "";
|
|
182
|
+
};
|
|
183
|
+
const uploadAll = async () => {
|
|
184
|
+
setBusy(true);
|
|
185
|
+
setMessage(null);
|
|
186
|
+
let failed = 0;
|
|
187
|
+
for (let i = 0; i < queue.length; i++) {
|
|
188
|
+
const item = queue[i];
|
|
189
|
+
if (item.done)
|
|
190
|
+
continue;
|
|
191
|
+
const set = (patch) => setQueue((q) => q.map((x, idx) => (idx === i ? { ...x, ...patch } : x)));
|
|
192
|
+
try {
|
|
193
|
+
await api.upload(item.file, joinTarget(target, item.relPath), (p) => set({ progress: p }));
|
|
194
|
+
set({ progress: 1, done: true, error: undefined });
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
failed++;
|
|
198
|
+
set({ error: err instanceof Error ? err.message : String(err) });
|
|
199
|
+
}
|
|
514
200
|
}
|
|
515
|
-
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
201
|
+
setBusy(false);
|
|
202
|
+
setMessage(failed ? `${failed} upload(s) failed` : "Upload complete");
|
|
203
|
+
if (!failed)
|
|
204
|
+
setQueue([]);
|
|
205
|
+
onUploaded();
|
|
206
|
+
};
|
|
207
|
+
const pending = queue.filter((q) => !q.done);
|
|
208
|
+
return (jsxs("section", { className: "sf-upload", children: [jsxs("div", { className: "sf-row sf-mode", role: "radiogroup", "aria-label": "Choose what clicking the upload area picks", children: [jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "radio", checked: mode === "files", onChange: () => setMode("files") }), " Files"] }), jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "radio", checked: mode === "folder", onChange: () => setMode("folder") }), " Folder"] }), jsx("span", { className: "sf-muted", children: "(drag-and-drop accepts either)" })] }), jsxs("div", { className: `sf-drop${dragging ? " sf-drop--active" : ""}`, role: "button", tabIndex: 0, "aria-label": `Upload area: click to choose ${mode === "folder" ? "a folder" : "files"}, or drop files or folders`, onClick: openPicker, onKeyDown: (e) => {
|
|
209
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
openPicker();
|
|
212
|
+
}
|
|
213
|
+
}, onDragEnter: (e) => {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
setDragging(true);
|
|
216
|
+
}, onDragOver: (e) => e.preventDefault(), onDragLeave: (e) => {
|
|
217
|
+
if (!e.currentTarget.contains(e.relatedTarget))
|
|
218
|
+
setDragging(false);
|
|
219
|
+
}, onDrop: async (e) => {
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
setDragging(false);
|
|
222
|
+
enqueue(await filesFromDrop(e.dataTransfer));
|
|
223
|
+
}, children: [jsx(CloudIcon, {}), jsxs("div", { children: [jsx("p", { className: "sf-drop__title", children: "Drag & drop files or folders here" }), jsxs("p", { className: "sf-muted", children: ["Click anywhere to choose ", mode === "folder" ? "a folder" : "files"] }), pending.length > 0 && (jsxs("p", { className: "sf-drop__count", children: [pending.length, " item", pending.length === 1 ? "" : "s", " queued"] }))] })] }), jsx("input", { ref: fileInput, type: "file", multiple: true, hidden: true, onChange: fromInput }), jsx("input", { ref: folderInput, type: "file", multiple: true, hidden: true, onChange: fromInput, ...{ webkitdirectory: "", directory: "" } }), jsxs("div", { className: "sf-row", children: [jsx(FolderSelect, { label: "Upload into", folders: folders, value: target, onChange: setTarget }), jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: busy || !pending.length, onClick: uploadAll, children: busy ? "Uploading…" : `Upload ${pending.length}` }), queue.length > 0 && !busy && (jsx("button", { type: "button", className: "sf-btn", onClick: () => setQueue([]), children: "Clear" }))] }), message && (jsx("p", { className: message.includes("failed") ? "sf-error" : "sf-muted", role: "status", children: message })), queue.length > 0 && (jsx("ul", { className: "sf-queue", children: queue.map((q, i) => (jsxs("li", { children: [jsx("span", { className: "sf-queue__name", children: joinTarget(target, q.relPath) }), q.error ? (jsx("span", { className: "sf-error", children: q.error })) : (jsxs("span", { className: "sf-row", children: [jsx("progress", { max: 1, value: q.progress }), jsxs("span", { className: "sf-muted", children: [Math.round(q.progress * 100), "%"] })] }))] }, `${q.relPath}-${i}`))) }))] }));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function CopyButton({ text, label = "Copy link" }) {
|
|
227
|
+
const [copied, setCopied] = useState(false);
|
|
228
|
+
const copy = async () => {
|
|
523
229
|
try {
|
|
524
|
-
await
|
|
525
|
-
id: updated.id,
|
|
526
|
-
shareable: updated.shareable,
|
|
527
|
-
share_password: updated.share_password ?? null,
|
|
528
|
-
password_str: updated.password_str ?? null,
|
|
529
|
-
download_limit: updated.download_limit ?? null,
|
|
530
|
-
}, "POST");
|
|
531
|
-
setFiles(curr => curr.map(f => f.id === file.id ? updated : f));
|
|
230
|
+
await navigator.clipboard.writeText(text);
|
|
532
231
|
}
|
|
533
|
-
catch
|
|
534
|
-
|
|
232
|
+
catch {
|
|
233
|
+
const ta = document.createElement("textarea");
|
|
234
|
+
ta.value = text;
|
|
235
|
+
document.body.appendChild(ta);
|
|
236
|
+
ta.select();
|
|
237
|
+
document.execCommand("copy");
|
|
238
|
+
ta.remove();
|
|
535
239
|
}
|
|
536
|
-
|
|
537
|
-
|
|
240
|
+
setCopied(true);
|
|
241
|
+
setTimeout(() => setCopied(false), 1500);
|
|
242
|
+
};
|
|
243
|
+
return (jsx("button", { type: "button", className: "sf-btn", onClick: copy, children: copied ? "Copied" : label }));
|
|
244
|
+
}
|
|
538
245
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
246
|
+
function ShareDialog({ file, onClose, onSave, }) {
|
|
247
|
+
const [shareable, setShareable] = useState(file.shareable);
|
|
248
|
+
const [limit, setLimit] = useState(file.download_limit ? String(file.download_limit) : "");
|
|
249
|
+
const [pwMode, setPwMode] = useState(file.has_password ? "keep" : "remove");
|
|
250
|
+
const [password, setPassword] = useState("");
|
|
251
|
+
const [resetCount, setResetCount] = useState(false);
|
|
252
|
+
const [saving, setSaving] = useState(false);
|
|
253
|
+
const [error, setError] = useState(null);
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
const onKey = (e) => e.key === "Escape" && onClose();
|
|
256
|
+
window.addEventListener("keydown", onKey);
|
|
257
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
258
|
+
}, [onClose]);
|
|
259
|
+
const limitValid = limit === "" || /^\d+$/.test(limit);
|
|
260
|
+
const passwordValid = pwMode !== "set" || password.length > 0;
|
|
261
|
+
const save = async () => {
|
|
262
|
+
setSaving(true);
|
|
263
|
+
setError(null);
|
|
548
264
|
try {
|
|
549
|
-
|
|
550
|
-
|
|
265
|
+
const update = { id: file.id, shareable };
|
|
266
|
+
if (shareable) {
|
|
267
|
+
update.download_limit = limit === "" ? null : Number(limit);
|
|
268
|
+
if (pwMode === "set")
|
|
269
|
+
update.password = password;
|
|
270
|
+
else if (pwMode === "remove" && file.has_password)
|
|
271
|
+
update.password = "";
|
|
272
|
+
if (resetCount)
|
|
273
|
+
update.reset_count = true;
|
|
274
|
+
}
|
|
275
|
+
await onSave(update);
|
|
276
|
+
onClose();
|
|
551
277
|
}
|
|
552
278
|
catch (err) {
|
|
553
|
-
|
|
279
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
554
280
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
/**
|
|
559
|
-
* Hook alias used by useFiles.ts. Patches share settings via the session-cookie
|
|
560
|
-
* authenticated /files/share endpoint.
|
|
561
|
-
*/
|
|
562
|
-
const useSaveShareSettings = (setFiles) => {
|
|
563
|
-
return useCallback(async (file) => {
|
|
564
|
-
const body = {
|
|
565
|
-
id: file.id,
|
|
566
|
-
shareable: file.shareable,
|
|
567
|
-
share_password: file.share_password ?? null,
|
|
568
|
-
password_str: file.password_str ?? null,
|
|
569
|
-
download_limit: file.download_limit ?? null,
|
|
570
|
-
};
|
|
571
|
-
try {
|
|
572
|
-
await cookieFetch("files/share", body, "POST");
|
|
573
|
-
setFiles(curr => curr.map(f => f.id === body.id ? { ...f, ...body } : f));
|
|
574
|
-
}
|
|
575
|
-
catch (err) {
|
|
576
|
-
console.error('Failed to save share settings:', err);
|
|
281
|
+
finally {
|
|
282
|
+
setSaving(false);
|
|
577
283
|
}
|
|
578
|
-
}
|
|
579
|
-
};
|
|
284
|
+
};
|
|
285
|
+
return (jsx("div", { className: "sf-overlay", onMouseDown: (e) => e.target === e.currentTarget && onClose(), children: jsxs("div", { className: "sf-dialog", role: "dialog", "aria-modal": "true", "aria-label": `Share settings for ${file.filename}`, children: [jsxs("header", { className: "sf-dialog__head", children: [jsx("h3", { children: "Share settings" }), jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": "Close", onClick: onClose, children: "\u2715" })] }), jsx("p", { className: "sf-muted", children: file.filepath }), jsxs("label", { className: "sf-check", children: [jsx("input", { type: "checkbox", checked: shareable, onChange: (e) => setShareable(e.target.checked) }), "Anyone with the link can download"] }), jsxs("fieldset", { disabled: !shareable, className: "sf-fieldset", children: [jsxs("label", { className: "sf-field", children: [jsx("span", { children: "Download limit (blank = unlimited)" }), jsx("input", { className: "sf-input", inputMode: "numeric", value: limit, onChange: (e) => setLimit(e.target.value.trim()), placeholder: "unlimited" }), !limitValid && jsx("span", { className: "sf-error", children: "Enter a whole number" })] }), jsxs("p", { className: "sf-muted", children: ["Downloaded ", file.download_count, " time", file.download_count === 1 ? "" : "s", file.download_limit ? ` of ${file.download_limit}` : "", ".", " ", jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "checkbox", checked: resetCount, onChange: (e) => setResetCount(e.target.checked) }), "reset count"] })] }), jsxs("div", { className: "sf-field", children: [jsx("span", { children: "Password" }), jsxs("div", { className: "sf-row", children: [file.has_password && (jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "radio", checked: pwMode === "keep", onChange: () => setPwMode("keep") }), "keep current"] })), jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "radio", checked: pwMode === "set", onChange: () => setPwMode("set") }), file.has_password ? "change" : "require password"] }), jsxs("label", { className: "sf-check sf-check--inline", children: [jsx("input", { type: "radio", checked: pwMode === "remove", onChange: () => setPwMode("remove") }), file.has_password ? "remove" : "none"] })] }), pwMode === "set" && (jsx("input", { className: "sf-input", type: "password", autoComplete: "new-password", value: password, onChange: (e) => setPassword(e.target.value), placeholder: "New password" }))] }), jsxs("div", { className: "sf-field", children: [jsx("span", { children: "Share link" }), jsxs("div", { className: "sf-row", children: [jsx("input", { className: "sf-input", readOnly: true, value: file.share_url, onFocus: (e) => e.target.select() }), jsx(CopyButton, { text: file.share_url })] }), !file.shareable && jsx("span", { className: "sf-muted", children: "The link works once you save with sharing on." })] })] }), error && jsx("p", { className: "sf-error", children: error }), jsxs("footer", { className: "sf-row sf-dialog__foot", children: [jsx("button", { type: "button", className: "sf-btn", onClick: onClose, children: "Cancel" }), jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: saving || !limitValid || !passwordValid, onClick: save, children: saving ? "Saving…" : "Save" })] })] }) }));
|
|
286
|
+
}
|
|
580
287
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
288
|
+
function ConfirmButton({ label, onConfirm, disabled, }) {
|
|
289
|
+
const [armed, setArmed] = useState(false);
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
if (!armed)
|
|
292
|
+
return;
|
|
293
|
+
const t = setTimeout(() => setArmed(false), 3000);
|
|
294
|
+
return () => clearTimeout(t);
|
|
295
|
+
}, [armed]);
|
|
296
|
+
return (jsx("button", { type: "button", className: `sf-btn sf-btn--danger${armed ? " sf-btn--armed" : ""}`, disabled: disabled, onClick: () => {
|
|
297
|
+
if (armed) {
|
|
298
|
+
setArmed(false);
|
|
299
|
+
onConfirm();
|
|
588
300
|
}
|
|
589
|
-
|
|
590
|
-
|
|
301
|
+
else {
|
|
302
|
+
setArmed(true);
|
|
591
303
|
}
|
|
592
|
-
}
|
|
593
|
-
}, [setFiles]);
|
|
594
|
-
};
|
|
595
|
-
|
|
596
|
-
function useFiles() {
|
|
597
|
-
const [files, setFiles] = useState([]);
|
|
598
|
-
const [loading, setLoading] = useState(false);
|
|
599
|
-
const [error, setError] = useState(null);
|
|
600
|
-
const loadCallback = useLoadFiles(setLoading, setError, setFiles);
|
|
601
|
-
const toggleShareCallback = useToggleShare(setFiles);
|
|
602
|
-
const patchShareSettingsCallback = useSaveShareSettings(setFiles);
|
|
603
|
-
const deleteFileCallback = useDeleteFile(setFiles);
|
|
604
|
-
useEffect(() => {
|
|
605
|
-
loadCallback();
|
|
606
|
-
}, [loadCallback]);
|
|
607
|
-
return {
|
|
608
|
-
files,
|
|
609
|
-
loading,
|
|
610
|
-
error,
|
|
611
|
-
load: loadCallback,
|
|
612
|
-
toggleShare: toggleShareCallback,
|
|
613
|
-
patchShareSettings: patchShareSettingsCallback,
|
|
614
|
-
deleteFile: deleteFileCallback,
|
|
615
|
-
setFiles,
|
|
616
|
-
setLoading,
|
|
617
|
-
setError,
|
|
618
|
-
};
|
|
304
|
+
}, children: armed ? "Confirm" : label }));
|
|
619
305
|
}
|
|
620
306
|
|
|
621
|
-
function
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
307
|
+
function formatSize(bytes) {
|
|
308
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
309
|
+
let n = bytes;
|
|
310
|
+
let i = 0;
|
|
311
|
+
while (n >= 1024 && i < units.length - 1) {
|
|
312
|
+
n /= 1024;
|
|
313
|
+
i++;
|
|
314
|
+
}
|
|
315
|
+
return `${n.toFixed(i ? 1 : 0)} ${units[i]}`;
|
|
316
|
+
}
|
|
317
|
+
const errText = (err) => (err instanceof Error ? err.message : String(err));
|
|
318
|
+
function AddRoot({ api, onAdded }) {
|
|
319
|
+
const [path, setPath] = useState("");
|
|
320
|
+
const [label, setLabel] = useState("");
|
|
321
|
+
const [challenge, setChallenge] = useState(null);
|
|
322
|
+
const [signature, setSignature] = useState("");
|
|
323
|
+
const [error, setError] = useState(null);
|
|
324
|
+
const [busy, setBusy] = useState(false);
|
|
325
|
+
const request = async () => {
|
|
326
|
+
setBusy(true);
|
|
327
|
+
setError(null);
|
|
632
328
|
try {
|
|
633
|
-
await
|
|
329
|
+
setChallenge((await api.requestRoot(path.trim())).challenge);
|
|
330
|
+
setSignature("");
|
|
634
331
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
};
|
|
639
|
-
const handleToggleShare = async () => {
|
|
640
|
-
setIsTogglingShare(true);
|
|
641
|
-
try {
|
|
642
|
-
await onToggleShare();
|
|
332
|
+
catch (err) {
|
|
333
|
+
setError(errText(err));
|
|
643
334
|
}
|
|
644
335
|
finally {
|
|
645
|
-
|
|
336
|
+
setBusy(false);
|
|
646
337
|
}
|
|
647
338
|
};
|
|
648
|
-
const
|
|
649
|
-
|
|
339
|
+
const confirm = async () => {
|
|
340
|
+
if (!challenge)
|
|
341
|
+
return;
|
|
342
|
+
setBusy(true);
|
|
343
|
+
setError(null);
|
|
650
344
|
try {
|
|
651
|
-
await
|
|
345
|
+
onAdded((await api.confirmRoot(challenge.nonce, signature, label)).roots);
|
|
346
|
+
setChallenge(null);
|
|
347
|
+
setPath("");
|
|
348
|
+
setLabel("");
|
|
349
|
+
setSignature("");
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
setError(errText(err));
|
|
652
353
|
}
|
|
653
354
|
finally {
|
|
654
|
-
|
|
355
|
+
setBusy(false);
|
|
655
356
|
}
|
|
656
357
|
};
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
onSettings();
|
|
660
|
-
}, "aria-label": "Open share settings", className: "wm-btn-icon text-lg", children: "\u2699\uFE0F" })) }), jsx("td", { className: "wm-cell w-12 align-middle", children: isOwner && (jsx(Button, { color: file.shareable ? 'blue' : 'gray', variant: file.shareable ? 'primary' : 'secondary', onClick: handleToggleShare, disabled: isTogglingShare, className: "btn", children: isTogglingShare
|
|
661
|
-
? 'Toggling...'
|
|
662
|
-
: file.shareable
|
|
663
|
-
? 'Sharing'
|
|
664
|
-
: 'Share' })) }), jsx("td", { className: "wm-cell w-12 align-middle", children: isOwner && (jsx(Button, { color: "gray", variant: "icon", onClick: handleDeleteFile, disabled: isDeleting, className: "wm-btn-icon text-lg", children: "\uD83D\uDDD1" })) }), jsxs("td", { className: "wm-cell", children: [file.download_count, " / ", file.download_limit ?? "∞"] })] }));
|
|
358
|
+
const minutesLeft = challenge ? Math.max(0, Math.round((challenge.expires * 1000 - Date.now()) / 60000)) : 0;
|
|
359
|
+
return (jsxs("div", { className: "sf-card", children: [jsx("h4", { children: "Allow a directory" }), !challenge ? (jsxs("div", { className: "sf-row sf-row--bottom", children: [jsxs("label", { className: "sf-field", children: [jsx("span", { children: "Absolute path on the server" }), jsx("input", { className: "sf-input", value: path, onChange: (e) => setPath(e.target.value), placeholder: "/srv/shared/docs", onKeyDown: (e) => e.key === "Enter" && path.trim() && request() })] }), jsxs("label", { className: "sf-field sf-field--narrow", children: [jsx("span", { children: "Label (optional)" }), jsx("input", { className: "sf-input", value: label, onChange: (e) => setLabel(e.target.value) })] }), jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: busy || !path.trim(), onClick: request, children: "Request" })] })) : (jsxs("div", { className: "sf-stack", children: [jsxs("p", { children: ["Allowing ", jsx("code", { children: challenge.message.split("|")[3] }), " requires a signature from a trusted SSH key. Run this where that key lives (expires in about ", minutesLeft, " min):"] }), jsxs("div", { className: "sf-row", children: [jsx("textarea", { className: "sf-input sf-code", readOnly: true, rows: 3, value: challenge.command }), jsx(CopyButton, { text: challenge.command, label: "Copy command" })] }), jsxs("label", { className: "sf-field", children: [jsx("span", { children: "Paste the full output (BEGIN \u2026 END SSH SIGNATURE)" }), jsx("textarea", { className: "sf-input sf-code", rows: 6, value: signature, onChange: (e) => setSignature(e.target.value), placeholder: "-----BEGIN SSH SIGNATURE-----" })] }), jsxs("div", { className: "sf-row", children: [jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: busy || !signature.includes("END SSH SIGNATURE"), onClick: confirm, children: "Verify & allow" }), jsx("button", { type: "button", className: "sf-btn", onClick: () => setChallenge(null), children: "Cancel" })] })] })), error && jsx("p", { className: "sf-error", children: error })] }));
|
|
665
360
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
const [
|
|
669
|
-
const [
|
|
670
|
-
const [
|
|
671
|
-
const [
|
|
672
|
-
const [download_limit, setDownloadLimit] = useState(0);
|
|
673
|
-
const [error, setError] = useState("");
|
|
361
|
+
function Browser({ api, roots, folders, onLinked, }) {
|
|
362
|
+
const [listing, setListing] = useState(null);
|
|
363
|
+
const [picked, setPicked] = useState(new Set());
|
|
364
|
+
const [target, setTarget] = useState("");
|
|
365
|
+
const [error, setError] = useState(null);
|
|
366
|
+
const [result, setResult] = useState(null);
|
|
674
367
|
const [busy, setBusy] = useState(false);
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
setEnabled(file.shareable);
|
|
684
|
-
setPassword(file.share_password);
|
|
685
|
-
setStrPassword(file.password_str ?? "");
|
|
686
|
-
setDownloadLimit(file.download_limit ?? 0);
|
|
687
|
-
setError("");
|
|
688
|
-
modalRef.current?.focus();
|
|
689
|
-
}, [file]);
|
|
690
|
-
useEffect(() => {
|
|
691
|
-
const h = (e) => e.key === "Escape" && onClose();
|
|
692
|
-
window.addEventListener("keydown", h);
|
|
693
|
-
return () => window.removeEventListener("keydown", h);
|
|
694
|
-
}, [onClose]);
|
|
695
|
-
if (!file)
|
|
696
|
-
return null;
|
|
697
|
-
async function handleSave() {
|
|
698
|
-
if (strPassword && strPassword.length < 4) {
|
|
699
|
-
setError("Password must be at least 4 characters");
|
|
700
|
-
return;
|
|
368
|
+
const open = useCallback(async (path) => {
|
|
369
|
+
setError(null);
|
|
370
|
+
try {
|
|
371
|
+
setListing(await api.browse(path));
|
|
372
|
+
setPicked(new Set());
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
setError(errText(err));
|
|
701
376
|
}
|
|
377
|
+
}, [api]);
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
if (!listing && roots.length)
|
|
380
|
+
open(roots[0].path);
|
|
381
|
+
if (listing && !roots.some((r) => listing.path.startsWith(r.path)))
|
|
382
|
+
setListing(null);
|
|
383
|
+
}, [roots, listing, open]);
|
|
384
|
+
const togglePick = (path, on) => setPicked((p) => {
|
|
385
|
+
const next = new Set(p);
|
|
386
|
+
on ? next.add(path) : next.delete(path);
|
|
387
|
+
return next;
|
|
388
|
+
});
|
|
389
|
+
const link = async () => {
|
|
702
390
|
setBusy(true);
|
|
703
|
-
|
|
704
|
-
id: fileId,
|
|
705
|
-
shareable: enabled,
|
|
706
|
-
needsPassword: !!strPassword,
|
|
707
|
-
share_password: strPassword || null,
|
|
708
|
-
password_str: strPassword || null,
|
|
709
|
-
download_limit: download_limit === "" ? null : Number(download_limit),
|
|
710
|
-
};
|
|
391
|
+
setResult(null);
|
|
711
392
|
try {
|
|
712
|
-
const
|
|
713
|
-
|
|
393
|
+
const res = await api.link([...picked], target);
|
|
394
|
+
const parts = [`Linked ${res.linked.length} file(s)`];
|
|
395
|
+
if (res.errors.length)
|
|
396
|
+
parts.push(`${res.errors.length} skipped: ${res.errors.map((e) => e.error).join("; ")}`);
|
|
397
|
+
setResult(parts.join(". "));
|
|
398
|
+
onLinked();
|
|
399
|
+
if (listing)
|
|
400
|
+
await open(listing.path);
|
|
714
401
|
}
|
|
715
402
|
catch (err) {
|
|
716
|
-
|
|
403
|
+
setResult(errText(err));
|
|
717
404
|
}
|
|
718
405
|
finally {
|
|
719
406
|
setBusy(false);
|
|
720
407
|
}
|
|
408
|
+
};
|
|
409
|
+
if (!roots.length)
|
|
410
|
+
return jsx("p", { className: "sf-muted", children: "Allow a directory above to browse and link its files." });
|
|
411
|
+
const linkable = [
|
|
412
|
+
...(listing?.dirs.filter((d) => !d.linked_id) ?? []),
|
|
413
|
+
...(listing?.files.filter((f) => !f.linked_id) ?? []),
|
|
414
|
+
];
|
|
415
|
+
const relative = listing ? listing.path.slice(listing.root.length) || "/" : "";
|
|
416
|
+
return (jsxs("div", { className: "sf-card", children: [jsx("h4", { children: "Link files or folders" }), jsxs("div", { className: "sf-row", children: [jsx("select", { className: "sf-input sf-root-select", value: listing?.root ?? "", onChange: (e) => open(e.target.value), children: roots.map((r) => (jsx("option", { value: r.path, children: r.label ? `${r.label} (${r.path})` : r.path }, r.id))) }), listing && jsx("code", { className: "sf-muted", children: relative })] }), error && jsx("p", { className: "sf-error", children: error }), listing && (jsxs("div", { className: "sf-browser", children: [listing.parent && (jsx("button", { type: "button", className: "sf-browser__dir", onClick: () => open(listing.parent), children: "\u21A9 .." })), listing.dirs.map((d) => (jsxs("div", { className: `sf-browser__row${d.linked_id ? " sf-muted" : ""}`, children: [jsx("input", { type: "checkbox", "aria-label": `Select folder ${d.name}`, disabled: !!d.linked_id, checked: picked.has(d.path), onChange: (e) => togglePick(d.path, e.target.checked) }), jsxs("button", { type: "button", className: "sf-browser__dir", onClick: () => open(d.path), title: "Open folder", children: ["\uD83D\uDCC1 ", d.name] }), jsx("span", { className: "sf-muted", children: d.linked_id ? "already linked" : "folder → .tar" })] }, d.path))), linkable.length > 0 && (jsxs("label", { className: "sf-check sf-browser__all", children: [jsx("input", { type: "checkbox", checked: linkable.length > 0 && linkable.every((f) => picked.has(f.path)), disabled: !linkable.length, onChange: (e) => setPicked(e.target.checked ? new Set(linkable.map((f) => f.path)) : new Set()) }), "Select everything here"] })), listing.files.map((f) => (jsxs("label", { className: `sf-check sf-browser__file${f.linked_id ? " sf-muted" : ""}`, children: [jsx("input", { type: "checkbox", disabled: !!f.linked_id, checked: picked.has(f.path), onChange: (e) => togglePick(f.path, e.target.checked) }), jsx("span", { className: "sf-browser__name", children: f.name }), jsx("span", { className: "sf-muted", children: f.linked_id ? "already linked" : formatSize(f.size) })] }, f.path))), !listing.dirs.length && !listing.files.length && jsx("p", { className: "sf-muted", children: "Empty directory" }), listing.truncated && jsx("p", { className: "sf-muted", children: "Showing the first 5000 entries." })] })), jsxs("div", { className: "sf-row sf-row--bottom", children: [jsx(FolderSelect, { label: "Show in Secure Files under", folders: folders, value: target, onChange: setTarget }), jsx("button", { type: "button", className: "sf-btn sf-btn--primary", disabled: busy || !picked.size, onClick: link, children: busy ? "Linking…" : `Link ${picked.size || ""}`.trim() })] }), result && jsx("p", { className: result.includes("skipped") ? "sf-error" : "sf-muted", children: result })] }));
|
|
417
|
+
}
|
|
418
|
+
function ServerFiles({ api, folders, onLinked, }) {
|
|
419
|
+
const [roots, setRoots] = useState(null);
|
|
420
|
+
const [error, setError] = useState(null);
|
|
421
|
+
const load = useCallback(async () => {
|
|
422
|
+
try {
|
|
423
|
+
setRoots((await api.roots()).roots);
|
|
424
|
+
setError(null);
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
setError(errText(err));
|
|
428
|
+
}
|
|
429
|
+
}, [api]);
|
|
430
|
+
return (jsxs("details", { className: "sf-admin", onToggle: (e) => e.currentTarget.open && !roots && load(), children: [jsx("summary", { children: "Server files (admin)" }), jsxs("div", { className: "sf-stack", children: [jsx("p", { className: "sf-muted", children: "Link files or whole folders that already live on the server instead of uploading copies. A linked folder downloads as one uncompressed .tar with an exact size, and interrupted downloads can be resumed. Only items inside allowed directories can be linked; removing a directory immediately disables its links. Deleting a linked entry never touches the original. Available from the local network only." }), error && jsx("p", { className: "sf-error", children: error }), roots && (jsxs(Fragment, { children: [jsxs("div", { className: "sf-card", children: [jsx("h4", { children: "Allowed directories" }), roots.length === 0 ? (jsx("p", { className: "sf-muted", children: "None yet." })) : (jsx("ul", { className: "sf-roots", children: roots.map((r) => (jsxs("li", { children: [jsxs("span", { children: [jsx("code", { children: r.path }), " ", r.label && jsxs("span", { className: "sf-muted", children: ["\u2014 ", r.label] }), !r.exists && jsx("span", { className: "sf-error", children: " (missing)" })] }), jsx(ConfirmButton, { label: "Remove", onConfirm: async () => {
|
|
431
|
+
try {
|
|
432
|
+
setRoots((await api.removeRoot(r.id)).roots);
|
|
433
|
+
onLinked();
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
setError(errText(err));
|
|
437
|
+
}
|
|
438
|
+
} })] }, r.id))) }))] }), jsx(AddRoot, { api: api, onAdded: setRoots }), jsx(Browser, { api: api, roots: roots, folders: folders, onLinked: onLinked })] }))] })] }));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function styleInject(css, ref) {
|
|
442
|
+
if ( ref === void 0 ) ref = {};
|
|
443
|
+
var insertAt = ref.insertAt;
|
|
444
|
+
|
|
445
|
+
if (typeof document === 'undefined') { return; }
|
|
446
|
+
|
|
447
|
+
var head = document.head || document.getElementsByTagName('head')[0];
|
|
448
|
+
var style = document.createElement('style');
|
|
449
|
+
style.type = 'text/css';
|
|
450
|
+
|
|
451
|
+
if (insertAt === 'top') {
|
|
452
|
+
if (head.firstChild) {
|
|
453
|
+
head.insertBefore(style, head.firstChild);
|
|
454
|
+
} else {
|
|
455
|
+
head.appendChild(style);
|
|
721
456
|
}
|
|
722
|
-
|
|
457
|
+
} else {
|
|
458
|
+
head.appendChild(style);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (style.styleSheet) {
|
|
462
|
+
style.styleSheet.cssText = css;
|
|
463
|
+
} else {
|
|
464
|
+
style.appendChild(document.createTextNode(css));
|
|
465
|
+
}
|
|
723
466
|
}
|
|
724
467
|
|
|
725
|
-
|
|
468
|
+
var css_248z = ".sf-root{--sf-border:color-mix(in srgb,currentColor 22%,transparent);--sf-subtle:color-mix(in srgb,currentColor 6%,transparent);--sf-accent:#2563eb;--sf-danger:#dc2626;display:flex;flex-direction:column;font-size:14px;gap:16px;margin:0 auto;max-width:1100px;padding:16px;text-align:left}.sf-root *,.sf-root :after,.sf-root :before{box-sizing:border-box}.sf-head{align-items:baseline;display:flex;gap:12px;justify-content:space-between}.sf-head h2{font-size:1.4em;margin:0}.sf-muted{opacity:.7}.sf-error{color:var(--sf-danger)}.sf-row{align-items:center;display:flex;flex-wrap:wrap;gap:8px}.sf-row--end{justify-content:flex-end}.sf-field{display:flex;flex:1;flex-direction:column;gap:4px;min-width:200px}.sf-input{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;font:inherit;min-width:0;padding:6px 10px;width:100%}.sf-row .sf-input{flex:1;width:auto}.sf-btn{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:5px 12px;white-space:nowrap}.sf-btn:hover:not(:disabled){border-color:var(--sf-accent)}.sf-btn:disabled{cursor:default;opacity:.45}.sf-btn--primary{background:var(--sf-accent);border-color:var(--sf-accent);color:#fff}.sf-btn--armed,.sf-btn--danger:hover:not(:disabled){border-color:var(--sf-danger);color:var(--sf-danger)}.sf-btn--armed{background:color-mix(in srgb,var(--sf-danger) 12%,transparent)}.sf-icon-btn{background:none;border:1px solid transparent;border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:3px 6px}.sf-icon-btn:hover{border-color:var(--sf-border)}.sf-upload{display:flex;flex-direction:column;gap:10px}.sf-drop{align-items:center;border:2px dashed var(--sf-border);border-radius:12px;cursor:pointer;display:flex;gap:20px;padding:28px 24px;transition:border-color .15s,background .15s,box-shadow .15s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.sf-drop:focus-visible,.sf-drop:hover{border-color:var(--sf-accent);box-shadow:0 4px 14px color-mix(in srgb,currentColor 10%,transparent);outline:none}.sf-drop p{margin:0}.sf-drop__icon{flex-shrink:0;height:72px;opacity:.6;width:72px}.sf-drop__title{font-size:1.1em;font-weight:600;margin-bottom:4px!important}.sf-drop__count{color:var(--sf-accent);font-weight:600;margin-top:6px!important}.sf-drop--active{background:color-mix(in srgb,var(--sf-accent) 8%,transparent);border-color:var(--sf-accent)}.sf-drop--active .sf-drop__icon{color:var(--sf-accent);opacity:1}.sf-mode{gap:14px}.sf-queue progress{width:120px}.sf-upload>.sf-row{align-items:flex-end}.sf-queue{display:flex;flex-direction:column;gap:4px;list-style:none;margin:0;max-height:200px;overflow:auto;padding:0}.sf-queue li{display:flex;gap:12px;justify-content:space-between}.sf-queue__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-toolbar{display:flex;flex-wrap:wrap;gap:12px;justify-content:space-between}.sf-search{max-width:320px}.sf-table-wrap{border:1px solid var(--sf-border);border-radius:8px;overflow-x:auto}.sf-table{border-collapse:collapse;width:100%}.sf-table td,.sf-table th{border-bottom:1px solid var(--sf-border);padding:8px 10px;text-align:left;vertical-align:middle}.sf-table tbody tr:last-child td{border-bottom:none}.sf-table th{background:var(--sf-subtle);font-weight:600}.sf-col-check{width:36px}.sf-col-actions{text-align:right!important}.sf-dir td{background:var(--sf-subtle)}.sf-dir__toggle{background:none;border:none;color:inherit;cursor:pointer;font:inherit;font-weight:600;padding:0}.sf-busy{opacity:.55}.sf-pill{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:999px;color:inherit;cursor:pointer;font:inherit;font-size:12px;padding:2px 10px}.sf-pill--on{background:color-mix(in srgb,#16a34a 18%,transparent);border-color:#16a34a}.sf-overlay{align-items:center;background:rgba(0,0,0,.5);display:flex;inset:0;justify-content:center;padding:16px;position:fixed;z-index:1000}.sf-dialog{--sf-border:color-mix(in srgb,CanvasText 22%,transparent);--sf-subtle:color-mix(in srgb,CanvasText 6%,transparent);background:Canvas;border-radius:10px;color:CanvasText;display:flex;flex-direction:column;gap:12px;max-height:90vh;overflow:auto;padding:18px 20px;text-align:left;width:min(520px,100%)}.sf-dialog__head{align-items:center;display:flex;justify-content:space-between}.sf-dialog__head h3{margin:0}.sf-dialog>.sf-muted{margin:0;word-break:break-all}.sf-dialog__foot{justify-content:flex-end}.sf-fieldset{border:none;display:flex;flex-direction:column;gap:12px;margin:0;padding:0}.sf-fieldset:disabled{opacity:.5}.sf-fieldset p{margin:0}.sf-check{align-items:center;cursor:pointer;display:flex;gap:8px}.sf-check--inline{display:inline-flex}.sf-stack{display:flex;flex-direction:column;gap:12px}.sf-stack p{margin:0}.sf-row--bottom{align-items:flex-end}.sf-field--narrow{flex:0 1 200px;min-width:140px}.sf-admin{border:1px solid var(--sf-border);border-radius:10px;padding:10px 14px}.sf-admin>summary{cursor:pointer;font-weight:600}.sf-admin[open]>summary{margin-bottom:12px}.sf-card{border:1px solid var(--sf-border);border-radius:8px;display:flex;flex-direction:column;gap:10px;padding:12px}.sf-card h4,.sf-card p{margin:0}.sf-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;resize:vertical}.sf-roots{display:flex;flex-direction:column;gap:6px;list-style:none;margin:0;padding:0}.sf-roots li{align-items:center;display:flex;gap:12px;justify-content:space-between}.sf-root-select{max-width:420px}.sf-browser{border:1px solid var(--sf-border);border-radius:6px;display:flex;flex-direction:column;max-height:320px;overflow:auto}.sf-browser>*{border-bottom:1px solid var(--sf-border);padding:5px 10px}.sf-browser>:last-child{border-bottom:none}.sf-browser__dir{background:none;border:none;border-bottom:1px solid var(--sf-border);color:inherit;cursor:pointer;font:inherit;text-align:left}.sf-browser__all,.sf-browser__dir:hover{background:var(--sf-subtle)}.sf-browser__all{font-weight:600}.sf-browser__name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-tag{border:1px solid var(--sf-border);border-radius:999px;display:inline-block;font-size:11px;margin-left:8px;padding:0 7px;vertical-align:middle}.sf-tag--warn{border-color:var(--sf-danger);color:var(--sf-danger)}.sf-strike{opacity:.7;text-decoration:line-through}.sf-browser__row{align-items:center;display:flex;gap:8px}.sf-browser__row .sf-browser__dir{border-bottom:none;flex:1;padding:0}";
|
|
469
|
+
styleInject(css_248z);
|
|
470
|
+
|
|
726
471
|
function buildTree(files) {
|
|
727
|
-
const root = { name:
|
|
728
|
-
|
|
729
|
-
const parts =
|
|
472
|
+
const root = { name: "", path: "", folders: new Map(), files: [] };
|
|
473
|
+
for (const f of files) {
|
|
474
|
+
const parts = f.filepath.split("/");
|
|
730
475
|
let node = root;
|
|
731
|
-
parts.slice(0, -1).forEach((
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
node.children.set(seg, { name: seg, path: subPath, children: new Map(), files: [] });
|
|
476
|
+
parts.slice(0, -1).forEach((part, i) => {
|
|
477
|
+
if (!node.folders.has(part)) {
|
|
478
|
+
node.folders.set(part, { name: part, path: parts.slice(0, i + 1).join("/"), folders: new Map(), files: [] });
|
|
735
479
|
}
|
|
736
|
-
node = node.
|
|
480
|
+
node = node.folders.get(part);
|
|
737
481
|
});
|
|
738
|
-
node.files.push(
|
|
739
|
-
}
|
|
482
|
+
node.files.push(f);
|
|
483
|
+
}
|
|
740
484
|
return root;
|
|
741
485
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
let ids = [];
|
|
745
|
-
node.files.forEach(f => ids.push(f.id));
|
|
746
|
-
node.children.forEach(child => {
|
|
747
|
-
ids = ids.concat(collectNodeIds(child));
|
|
748
|
-
});
|
|
749
|
-
return ids;
|
|
486
|
+
function allFiles(node) {
|
|
487
|
+
return [...node.files, ...[...node.folders.values()].flatMap(allFiles)];
|
|
750
488
|
}
|
|
751
|
-
function
|
|
752
|
-
const
|
|
753
|
-
const [
|
|
754
|
-
const [
|
|
489
|
+
function SecureFiles({ apiBase }) {
|
|
490
|
+
const api = useMemo(() => createApi(apiBase), [apiBase]);
|
|
491
|
+
const [files, setFiles] = useState([]);
|
|
492
|
+
const [user, setUser] = useState(null);
|
|
493
|
+
const [isAdmin, setIsAdmin] = useState(false);
|
|
494
|
+
const [loading, setLoading] = useState(true);
|
|
495
|
+
const [error, setError] = useState(null);
|
|
496
|
+
const [notice, setNotice] = useState(null);
|
|
497
|
+
const [search, setSearch] = useState("");
|
|
755
498
|
const [selected, setSelected] = useState(new Set());
|
|
756
|
-
const [
|
|
757
|
-
|
|
758
|
-
const [
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
node.children.forEach(child => {
|
|
768
|
-
paths.push(child.path);
|
|
769
|
-
collect(child);
|
|
770
|
-
});
|
|
499
|
+
const [collapsed, setCollapsed] = useState(new Set());
|
|
500
|
+
const [editing, setEditing] = useState(null);
|
|
501
|
+
const [busyIds, setBusyIds] = useState(new Set());
|
|
502
|
+
const load = useCallback(async () => {
|
|
503
|
+
setError(null);
|
|
504
|
+
try {
|
|
505
|
+
const data = await api.list();
|
|
506
|
+
setUser(data.user_name);
|
|
507
|
+
setIsAdmin(data.is_admin);
|
|
508
|
+
setFiles(data.files);
|
|
509
|
+
setSelected((prev) => new Set([...prev].filter((id) => data.files.some((f) => f.id === id))));
|
|
771
510
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
}, [tree]);
|
|
775
|
-
// Selection & expand/collapse logic
|
|
776
|
-
const allIds = filtered.map(f => f.id);
|
|
777
|
-
const toggleAllSelection = () => selected.size < allIds.length ? setSelected(new Set(allIds)) : setSelected(new Set());
|
|
778
|
-
const toggleDirOpen = (path) => setExpanded(prev => {
|
|
779
|
-
const next = new Set(prev);
|
|
780
|
-
next.has(path) ? next.delete(path) : next.add(path);
|
|
781
|
-
return next;
|
|
782
|
-
});
|
|
783
|
-
const expandAll = () => setExpanded(new Set(allDirPaths));
|
|
784
|
-
const collapseAll = () => setExpanded(new Set());
|
|
785
|
-
// Recursive renderer: directory row with select-all checkbox, then its files, then subdirectories
|
|
786
|
-
const renderNode = (node, depth = 0) => {
|
|
787
|
-
const rows = [];
|
|
788
|
-
if (node.path) {
|
|
789
|
-
const isOpen = expanded.has(node.path);
|
|
790
|
-
const nodeIds = collectNodeIds(node);
|
|
791
|
-
const allSelected = nodeIds.every(id => selected.has(id));
|
|
792
|
-
const someSelected = nodeIds.some(id => selected.has(id));
|
|
793
|
-
rows.push(jsxs("tr", { children: [jsx("td", { className: "px-4 py-2 bg-gray-100", children: jsx("input", { type: "checkbox", checked: allSelected, ref: el => { if (el)
|
|
794
|
-
el.indeterminate = !allSelected && someSelected; }, onChange: () => {
|
|
795
|
-
setSelected(prev => {
|
|
796
|
-
const next = new Set(prev);
|
|
797
|
-
if (allSelected) {
|
|
798
|
-
nodeIds.forEach(id => next.delete(id));
|
|
799
|
-
}
|
|
800
|
-
else {
|
|
801
|
-
nodeIds.forEach(id => next.add(id));
|
|
802
|
-
}
|
|
803
|
-
return next;
|
|
804
|
-
});
|
|
805
|
-
} }) }), jsx("td", { className: "px-4 py-2 bg-gray-100 font-bold", colSpan: 5, style: { textAlign: 'left' }, children: node.path }), jsx("td", { className: "px-4 py-2 bg-gray-100 text-right", children: jsx("button", { onClick: () => toggleDirOpen(node.path), children: isOpen ? '▾' : '▸' }) })] }, node.path));
|
|
806
|
-
if (!isOpen)
|
|
807
|
-
return rows;
|
|
511
|
+
catch (err) {
|
|
512
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
808
513
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
const isOwner = f.uploader_id.toLowerCase() === (currentUsername() ?? '').toLowerCase();
|
|
812
|
-
const canDownload = isOwner || f.shareable;
|
|
813
|
-
rows.push(jsxs("tr", { children: [jsx("td", { className: "px-4 py-2", children: jsx("input", { type: "checkbox", checked: selected.has(f.id), onChange: () => setSelected(prev => {
|
|
814
|
-
const next = new Set(prev);
|
|
815
|
-
next.has(f.id) ? next.delete(f.id) : next.add(f.id);
|
|
816
|
-
return next;
|
|
817
|
-
}) }) }), jsx("td", { className: "px-4 py-2", style: { paddingLeft: (depth + 1) * 20 }, children: f.filename }), jsx("td", { className: "px-4 py-2", children: canDownload ? jsx(Button, { variant: "secondary", onClick: () => downloadFile(f), children: "Download" }) : jsx("span", { children: "\uD83D\uDD12" }) }), jsx("td", { className: "px-4 py-2", children: isOwner && jsx(Button, { variant: "icon", onClick: () => setModalFile(f), children: "\u2699\uFE0F" }) }), jsx("td", { className: "px-4 py-2", children: isOwner && jsx(Button, { variant: f.shareable ? 'primary' : 'secondary', onClick: () => onToggleShare(f), children: f.shareable ? 'Sharing' : 'Share' }) }), jsx("td", { className: "px-4 py-2", children: isOwner && jsx(Button, { variant: "icon", onClick: () => onDelete(f).then(load), children: "\uD83D\uDDD1" }) }), jsxs("td", { className: "px-4 py-2", children: [f.download_count, " / ", f.download_limit ?? '∞'] })] }, f.id));
|
|
818
|
-
});
|
|
819
|
-
// Render subdirectories
|
|
820
|
-
Array.from(node.children.values())
|
|
821
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
822
|
-
.forEach(child => rows.push(...renderNode(child, depth + 1)));
|
|
823
|
-
return rows;
|
|
824
|
-
};
|
|
825
|
-
// Batch actions
|
|
826
|
-
const handleDownloadAll = async () => {
|
|
827
|
-
const inputs = filtered.filter(f => selected.has(f.id)).map(f => document.createElement('input'));
|
|
828
|
-
inputs.forEach((cb, idx) => cb.value = filtered.filter(f => selected.has(f.id))[idx].filepath);
|
|
829
|
-
await downloadSelected(inputs);
|
|
830
|
-
};
|
|
831
|
-
const handleDeleteAll = async () => { await onDelete(filtered.filter(f => selected.has(f.id))); setSelected(new Set()); await load(); };
|
|
832
|
-
const handleShareAll = async () => { for (const f of filtered.filter(f => selected.has(f.id)))
|
|
833
|
-
await onToggleShare(f); };
|
|
834
|
-
if (loading)
|
|
835
|
-
return jsx(Spinner, {});
|
|
836
|
-
if (error)
|
|
837
|
-
return jsx("p", { className: "text-red-600", children: error.message });
|
|
838
|
-
return (jsxs(Fragment, { children: [jsxs("div", { className: "mb-4 flex flex-col md:flex-row md:justify-between gap-4", children: [jsx(Input, { label: "Search files", type: "text", value: search, onChange: (e) => setSearch(e.target.value), placeholder: "Enter filename\u2026", className: "form-input flex-1" }), jsxs("div", { className: "flex gap-2", children: [jsx(Button, { onClick: expandAll, disabled: !allDirPaths.length, children: "Expand All" }), jsx(Button, { onClick: collapseAll, disabled: !allDirPaths.length, children: "Collapse All" }), jsx(Button, { onClick: handleDownloadAll, disabled: !selected.size, children: "Download Selected" }), jsx(Button, { color: "gray", onClick: handleDeleteAll, disabled: !selected.size, children: "Delete Selected" }), jsx(Button, { onClick: handleShareAll, disabled: !selected.size, children: "Share Selected" })] })] }), jsx("div", { className: "overflow-x-auto wm-container", children: jsxs("table", { className: "file-table w-full", children: [jsx("thead", { className: "wm-thead", children: jsxs("tr", { children: [jsx("th", { className: "px-4 py-2 w-12", children: jsx("input", { type: "checkbox", checked: selected.size === filtered.length && filtered.length > 0, onChange: toggleAllSelection }) }), jsx("th", { className: "px-4 py-2", children: "Filename" }), jsx("th", { className: "px-4 py-2", children: "Download" }), jsx("th", { className: "px-4 py-2", children: "Settings" }), jsx("th", { className: "px-4 py-2", children: "Share" }), jsx("th", { className: "px-4 py-2", children: "Delete" }), jsx("th", { className: "px-4 py-2", children: "Downloads" })] }) }), jsx("tbody", { children: renderNode(tree) })] }) }), jsx(ShareModal, { file: modalFile, onClose: () => setModalFile(null), onSave: async (patch) => {
|
|
839
|
-
if (!modalFile)
|
|
840
|
-
return;
|
|
841
|
-
const updated = { ...modalFile, ...patch };
|
|
842
|
-
await onPatchShare(updated);
|
|
843
|
-
setModalFile(null);
|
|
844
|
-
} })] }));
|
|
845
|
-
}
|
|
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 ───────────────────────────────────────────────────────────────
|
|
877
|
-
function Upload({ onUploaded }) {
|
|
878
|
-
const [queue, setQueue] = useState([]);
|
|
879
|
-
const [busy, setBusy] = useState(false);
|
|
880
|
-
const [message, setMessage] = useState(null);
|
|
881
|
-
const [drag, setDrag] = useState(false);
|
|
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');
|
|
886
|
-
const [dirs, setDirs] = useState([]);
|
|
887
|
-
// — existing files → directory list —
|
|
888
|
-
const { files, load } = useFiles();
|
|
889
|
-
useEffect(() => {
|
|
890
|
-
const dirSet = new Set();
|
|
891
|
-
files.forEach(f => {
|
|
892
|
-
const parts = f.filepath.split('/');
|
|
893
|
-
if (parts.length > 1) {
|
|
894
|
-
parts.pop();
|
|
895
|
-
dirSet.add(parts.join('/') + '/');
|
|
896
|
-
}
|
|
897
|
-
});
|
|
898
|
-
setDirs(Array.from(dirSet).sort());
|
|
899
|
-
}, [files]);
|
|
900
|
-
// — directory selection state —
|
|
901
|
-
const [uploadDir, setUploadDir] = useState('');
|
|
902
|
-
const [creatingNew, setCreatingNew] = useState(false);
|
|
903
|
-
const [newDirName, setNewDirName] = useState('');
|
|
904
|
-
// — queue logic —
|
|
905
|
-
const enqueue = useCallback((incoming) => {
|
|
906
|
-
setQueue(prev => {
|
|
907
|
-
const map = new Map(prev.map(q => [`${q.file.name}-${q.file.size}`, q]));
|
|
908
|
-
incoming.forEach(q => map.set(`${q.file.name}-${q.file.size}`, q));
|
|
909
|
-
return Array.from(map.values());
|
|
910
|
-
});
|
|
911
|
-
}, []);
|
|
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
|
-
}));
|
|
514
|
+
finally {
|
|
515
|
+
setLoading(false);
|
|
923
516
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
if (entry.isDirectory) {
|
|
934
|
-
const reader = entry.createReader();
|
|
935
|
-
const entries = await readAllEntries(reader);
|
|
936
|
-
await Promise.all(entries.map(e => walk(e, `${path}${entry.name}/`)));
|
|
937
|
-
}
|
|
517
|
+
}, [api]);
|
|
518
|
+
useEffect(() => {
|
|
519
|
+
load();
|
|
520
|
+
const onVisible = () => document.visibilityState === "visible" && load();
|
|
521
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
522
|
+
window.addEventListener("focus", onVisible);
|
|
523
|
+
return () => {
|
|
524
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
525
|
+
window.removeEventListener("focus", onVisible);
|
|
938
526
|
};
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
const picked = Array.from(e.target.files ?? []);
|
|
944
|
-
const queued = picked.map(f => ({
|
|
945
|
-
file: f,
|
|
946
|
-
relPath: deriveRelPath(f),
|
|
947
|
-
progress: 0,
|
|
948
|
-
}));
|
|
949
|
-
enqueue(queued);
|
|
950
|
-
e.target.value = '';
|
|
951
|
-
};
|
|
952
|
-
const handleDrop = async (e) => {
|
|
953
|
-
e.preventDefault();
|
|
954
|
-
e.stopPropagation();
|
|
955
|
-
const dropped = await extractFromDataTransfer(e.dataTransfer);
|
|
956
|
-
if (dropped.length)
|
|
957
|
-
enqueue(dropped);
|
|
958
|
-
setDrag(false);
|
|
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
|
-
};
|
|
966
|
-
const handleUploadAll = async () => {
|
|
967
|
-
if (!queue.length) {
|
|
968
|
-
setMessage('No files queued.');
|
|
969
|
-
return;
|
|
970
|
-
}
|
|
971
|
-
setBusy(true);
|
|
972
|
-
setMessage(null);
|
|
973
|
-
const targetDir = creatingNew
|
|
974
|
-
? (newDirName.trim().replace(/^\/+|\/+$/g, '') + '/')
|
|
975
|
-
: uploadDir;
|
|
527
|
+
}, [load]);
|
|
528
|
+
const run = async (ids, action) => {
|
|
529
|
+
setBusyIds((s) => new Set([...s, ...ids]));
|
|
530
|
+
setNotice(null);
|
|
976
531
|
try {
|
|
977
|
-
|
|
978
|
-
await uploadFiles(file, targetDir + relPath);
|
|
979
|
-
setQueue(cur => cur.map(item => item.file === file ? { ...item, progress: 100 } : item));
|
|
980
|
-
}
|
|
981
|
-
setQueue([]);
|
|
982
|
-
await onUploaded(); // SecureFiles passes filesHook.load here
|
|
983
|
-
setMessage('Upload complete');
|
|
984
|
-
// (removed the duplicate load() — onUploaded already does it)
|
|
532
|
+
await action();
|
|
985
533
|
}
|
|
986
|
-
catch {
|
|
987
|
-
|
|
534
|
+
catch (err) {
|
|
535
|
+
setNotice(err instanceof Error ? err.message : String(err));
|
|
988
536
|
}
|
|
989
537
|
finally {
|
|
990
|
-
|
|
538
|
+
setBusyIds((s) => new Set([...s].filter((id) => !ids.includes(id))));
|
|
991
539
|
}
|
|
992
540
|
};
|
|
993
|
-
|
|
994
|
-
const
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
setCreatingNew(true);
|
|
998
|
-
setNewDirName('');
|
|
999
|
-
}
|
|
1000
|
-
else {
|
|
1001
|
-
setCreatingNew(false);
|
|
1002
|
-
setUploadDir(v);
|
|
1003
|
-
}
|
|
541
|
+
const replaceFile = (file) => setFiles((fs) => fs.map((f) => (f.id === file.id ? file : f)));
|
|
542
|
+
const saveShare = async (update) => {
|
|
543
|
+
const { file } = await api.updateShare(update);
|
|
544
|
+
replaceFile(file);
|
|
1004
545
|
};
|
|
1005
|
-
const
|
|
1006
|
-
const
|
|
1007
|
-
if (!
|
|
1008
|
-
setCreatingNew(false);
|
|
546
|
+
const download = (items) => {
|
|
547
|
+
const ready = items.filter((f) => f.available);
|
|
548
|
+
if (!ready.length)
|
|
1009
549
|
return;
|
|
550
|
+
api.download(ready.map((f) => f.id));
|
|
551
|
+
};
|
|
552
|
+
const setShared = (items, shareable) => run(items.map((f) => f.id), async () => {
|
|
553
|
+
for (const f of items)
|
|
554
|
+
await saveShare({ id: f.id, shareable });
|
|
555
|
+
});
|
|
556
|
+
const remove = (items) => run(items.map((f) => f.id), async () => {
|
|
557
|
+
const res = await api.remove(items.map((f) => f.id));
|
|
558
|
+
if (res.errors.length)
|
|
559
|
+
setNotice(`${res.errors.length} file(s) could not be deleted`);
|
|
560
|
+
await load();
|
|
561
|
+
});
|
|
562
|
+
const filtered = useMemo(() => {
|
|
563
|
+
const q = search.trim().toLowerCase();
|
|
564
|
+
return q ? files.filter((f) => f.filepath.toLowerCase().includes(q)) : files;
|
|
565
|
+
}, [files, search]);
|
|
566
|
+
const tree = useMemo(() => buildTree(filtered), [filtered]);
|
|
567
|
+
const folders = useMemo(() => {
|
|
568
|
+
const set = new Set();
|
|
569
|
+
for (const f of files) {
|
|
570
|
+
const parts = f.filepath.split("/").slice(0, -1);
|
|
571
|
+
parts.forEach((_, i) => set.add(parts.slice(0, i + 1).join("/")));
|
|
1010
572
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
573
|
+
return [...set];
|
|
574
|
+
}, [files]);
|
|
575
|
+
const selectedFiles = files.filter((f) => selected.has(f.id));
|
|
576
|
+
const toggle = (ids, on) => setSelected((prev) => {
|
|
577
|
+
const next = new Set(prev);
|
|
578
|
+
ids.forEach((id) => (on ? next.add(id) : next.delete(id)));
|
|
579
|
+
return next;
|
|
580
|
+
});
|
|
581
|
+
const toggleFolder = (path) => setCollapsed((c) => {
|
|
582
|
+
const next = new Set(c);
|
|
583
|
+
next.has(path) ? next.delete(path) : next.add(path);
|
|
584
|
+
return next;
|
|
585
|
+
});
|
|
586
|
+
const allVisibleSelected = filtered.length > 0 && filtered.every((f) => selected.has(f.id));
|
|
587
|
+
const searching = search.trim().length > 0;
|
|
588
|
+
const renderFile = (f, depth) => {
|
|
589
|
+
const busy = busyIds.has(f.id);
|
|
590
|
+
return (jsxs("tr", { className: busy ? "sf-busy" : undefined, children: [jsx("td", { className: "sf-col-check", children: jsx("input", { type: "checkbox", "aria-label": `Select ${f.filename}`, checked: selected.has(f.id), onChange: (e) => toggle([f.id], e.target.checked) }) }), jsxs("td", { style: { paddingLeft: 10 + depth * 20 }, children: [jsx("span", { className: f.available ? undefined : "sf-strike", children: f.filename }), f.kind === "folder" && (jsx("span", { className: "sf-tag", title: "Downloads as one uncompressed, resumable .tar of the whole folder", children: "folder \u00B7 .tar" })), f.linked && (jsx("span", { className: "sf-tag", title: f.source_path ?? undefined, children: "linked" })), !f.available && (jsx("span", { className: "sf-tag sf-tag--warn", title: f.linked ? "Source file is gone or its directory is no longer allowed" : "File is missing on disk", children: "missing" }))] }), jsxs("td", { children: [jsx("button", { type: "button", className: `sf-pill${f.shareable ? " sf-pill--on" : ""}`, disabled: busy || !f.available, onClick: () => setShared([f], !f.shareable), title: f.shareable ? "Click to stop sharing" : "Click to share", children: f.shareable ? "Shared" : "Private" }), f.shareable && f.has_password && jsx("span", { title: "Password protected", children: " \uD83D\uDD12" })] }), jsxs("td", { children: [f.download_count, " / ", f.download_limit ?? "∞"] }), jsx("td", { className: "sf-col-actions", children: jsxs("div", { className: "sf-row sf-row--end", children: [jsx("button", { type: "button", className: "sf-btn", disabled: busy || !f.available, onClick: () => download([f]), children: "Download" }), f.shareable && f.available && jsx(CopyButton, { text: f.share_url }), jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": `Share settings for ${f.filename}`, title: "Share settings", onClick: () => setEditing(f), children: "\u2699\uFE0F" }), jsx(ConfirmButton, { label: f.linked ? "Unlink" : "Delete", disabled: busy, onConfirm: () => remove([f]) })] }) })] }, f.id));
|
|
1016
591
|
};
|
|
1017
|
-
|
|
1018
|
-
|
|
592
|
+
const renderFolder = (node, depth) => {
|
|
593
|
+
const rows = [];
|
|
594
|
+
const subFolders = [...node.folders.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
595
|
+
const ownFiles = [...node.files].sort((a, b) => a.filename.localeCompare(b.filename));
|
|
596
|
+
for (const folder of subFolders) {
|
|
597
|
+
const contents = allFiles(folder);
|
|
598
|
+
const ids = contents.map((f) => f.id);
|
|
599
|
+
const open = searching || !collapsed.has(folder.path);
|
|
600
|
+
rows.push(jsxs("tr", { className: "sf-dir", children: [jsx("td", { className: "sf-col-check", children: jsx("input", { type: "checkbox", "aria-label": `Select folder ${folder.path}`, checked: ids.length > 0 && ids.every((id) => selected.has(id)), onChange: (e) => toggle(ids, e.target.checked) }) }), jsx("td", { colSpan: 4, style: { paddingLeft: 10 + depth * 20 }, children: jsxs("button", { type: "button", className: "sf-dir__toggle", onClick: () => toggleFolder(folder.path), children: [open ? "▾" : "▸", " \uD83D\uDCC1 ", folder.name, " ", jsxs("span", { className: "sf-muted", children: ["(", contents.length, ")"] })] }) })] }, `dir:${folder.path}`));
|
|
601
|
+
if (open)
|
|
602
|
+
rows.push(...renderFolder(folder, depth + 1));
|
|
603
|
+
}
|
|
604
|
+
rows.push(...ownFiles.map((f) => renderFile(f, depth)));
|
|
605
|
+
return rows;
|
|
606
|
+
};
|
|
607
|
+
return (jsxs("div", { className: "sf-root", children: [jsxs("header", { className: "sf-head", children: [jsx("h2", { children: "Secure Files" }), user && jsxs("span", { className: "sf-muted", children: ["Signed in as ", user] })] }), jsx(UploadPanel, { api: api, folders: folders, onUploaded: load }), isAdmin && jsx(ServerFiles, { api: api, folders: folders, onLinked: load }), jsxs("section", { className: "sf-toolbar", children: [jsx("input", { className: "sf-input sf-search", type: "search", placeholder: "Search files\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), jsxs("div", { className: "sf-row", children: [jsxs("span", { className: "sf-muted", children: [selected.size, " selected"] }), folders.length > 0 && (jsxs(Fragment, { children: [jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set()), children: "Expand all" }), jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set(folders)), children: "Collapse all" })] })), jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => download(selectedFiles), children: "Download" }), jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, true), children: "Share" }), jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, false), children: "Unshare" }), jsx(ConfirmButton, { label: "Delete", disabled: !selected.size, onConfirm: () => remove(selectedFiles) })] })] }), notice && (jsx("p", { className: "sf-error", role: "alert", children: notice })), loading ? (jsx("p", { className: "sf-muted", children: "Loading\u2026" })) : error ? (jsx("p", { className: "sf-error", role: "alert", children: error })) : files.length === 0 ? (jsx("p", { className: "sf-muted", children: "No files yet \u2014 upload something above." })) : (jsx("div", { className: "sf-table-wrap", children: jsxs("table", { className: "sf-table", children: [jsx("thead", { children: jsxs("tr", { children: [jsx("th", { className: "sf-col-check", children: jsx("input", { type: "checkbox", "aria-label": "Select all", checked: allVisibleSelected, onChange: (e) => toggle(filtered.map((f) => f.id), e.target.checked) }) }), jsx("th", { children: "Name" }), jsx("th", { children: "Sharing" }), jsx("th", { children: "Downloads" }), jsx("th", { className: "sf-col-actions", children: "Actions" })] }) }), jsx("tbody", { children: renderFolder(tree, 0) })] }) })), editing && (jsx(ShareDialog, { file: files.find((f) => f.id === editing.id) ?? editing, onClose: () => setEditing(null), onSave: saveShare }, editing.id))] }));
|
|
1019
608
|
}
|
|
1020
609
|
|
|
1021
|
-
|
|
1022
|
-
const [authChecked, setAuthChecked] = useState(false);
|
|
1023
|
-
const [loggedIn, setLoggedIn] = useState(false);
|
|
1024
|
-
const filesHook = useFiles();
|
|
1025
|
-
useEffect(() => {
|
|
1026
|
-
checkAuth().then(ok => {
|
|
1027
|
-
setLoggedIn(ok);
|
|
1028
|
-
setAuthChecked(true);
|
|
1029
|
-
});
|
|
1030
|
-
}, []);
|
|
1031
|
-
if (!authChecked)
|
|
1032
|
-
return null;
|
|
1033
|
-
if (!loggedIn) {
|
|
1034
|
-
return (jsx("p", { style: { textAlign: 'center', marginTop: '2rem', color: '#6b7280' }, children: "Please log in to access your files." }));
|
|
1035
|
-
}
|
|
1036
|
-
return (jsxs(Fragment, { children: [jsx(Upload, { onUploaded: filesHook.load }), jsx(FileTable, { files: filesHook.files, loading: filesHook.loading, error: filesHook.error, onDownload: filesHook.toggleShare, onToggleShare: filesHook.toggleShare, onPatchShare: filesHook.patchShareSettings, onDelete: filesHook.deleteFile })] }));
|
|
1037
|
-
};
|
|
1038
|
-
|
|
1039
|
-
export { Button, Checkbox, FileRow, FileTable, Input, SecureFiles, ShareModal, Spinner, Upload, directDownload, downloadFile, downloadRow, downloadSelect, downloadSelected, initSelectAll, listFiles, patchFileSetting, removeFile, removeRow, removeSelected, syncHeaderCheckbox, toggleCheckboxes, toggleShare, triggerDownload, uploadFile, uploadFiles, useDeleteFile, useFiles, useLoadFiles, usePatchShareSettings, useSaveShareSettings, useToggleShare };
|
|
610
|
+
export { ApiError, DEFAULT_API_BASE, SecureFiles, createApi };
|
|
1040
611
|
//# sourceMappingURL=index.js.map
|