@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.
Files changed (197) hide show
  1. package/dist/ConfirmButton.d.ts +5 -0
  2. package/dist/CopyButton.d.ts +4 -0
  3. package/dist/FolderSelect.d.ts +6 -0
  4. package/dist/SecureFiles.d.ts +3 -0
  5. package/dist/ServerFiles.d.ts +6 -0
  6. package/dist/ShareDialog.d.ts +6 -0
  7. package/dist/UploadPanel.d.ts +6 -0
  8. package/dist/api.d.ts +47 -0
  9. package/dist/index.cjs +529 -1004
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.ts +4 -8
  12. package/dist/index.js +522 -951
  13. package/dist/index.js.map +1 -1
  14. package/dist/types.d.ts +60 -0
  15. package/package.json +15 -11
  16. package/src/ConfirmButton.tsx +35 -0
  17. package/src/CopyButton.tsx +24 -0
  18. package/src/FolderSelect.tsx +68 -0
  19. package/src/SecureFiles.tsx +346 -0
  20. package/src/ServerFiles.tsx +339 -0
  21. package/src/ShareDialog.tsx +148 -0
  22. package/src/UploadPanel.tsx +217 -0
  23. package/src/api.ts +108 -0
  24. package/src/index.ts +4 -8
  25. package/src/styles.css +442 -0
  26. package/src/types.ts +56 -0
  27. package/css.d.ts +0 -2
  28. package/dist/App/SecureFiles.d.ts +0 -4
  29. package/dist/App/index.d.ts +0 -1
  30. package/dist/FileHandlers/FileRow/imports.d.ts +0 -2
  31. package/dist/FileHandlers/FileRow/index.d.ts +0 -1
  32. package/dist/FileHandlers/FileRow/src/index.d.ts +0 -1
  33. package/dist/FileHandlers/FileRow/src/row_utils.d.ts +0 -2
  34. package/dist/FileHandlers/FileTable/imports.d.ts +0 -4
  35. package/dist/FileHandlers/FileTable/index.d.ts +0 -1
  36. package/dist/FileHandlers/FileTable/src/index.d.ts +0 -1
  37. package/dist/FileHandlers/FileTable/src/table_utils.d.ts +0 -2
  38. package/dist/FileHandlers/ShareModal/imports.d.ts +0 -2
  39. package/dist/FileHandlers/ShareModal/index.d.ts +0 -1
  40. package/dist/FileHandlers/ShareModal/src/index.d.ts +0 -1
  41. package/dist/FileHandlers/ShareModal/src/share_utils.d.ts +0 -2
  42. package/dist/FileHandlers/Upload/imports.d.ts +0 -2
  43. package/dist/FileHandlers/Upload/index.d.ts +0 -1
  44. package/dist/FileHandlers/Upload/src/index.d.ts +0 -2
  45. package/dist/FileHandlers/Upload/src/upload_utils.d.ts +0 -2
  46. package/dist/FileHandlers/imports.d.ts +0 -4
  47. package/dist/FileHandlers/index.d.ts +0 -4
  48. package/dist/UI/index.d.ts +0 -28
  49. package/dist/file_utils/imports.d.ts +0 -2
  50. package/dist/file_utils/index.d.ts +0 -1
  51. package/dist/file_utils/src/cookieFetch.d.ts +0 -33
  52. package/dist/file_utils/src/delete_utils/imports.d.ts +0 -1
  53. package/dist/file_utils/src/delete_utils/index.d.ts +0 -1
  54. package/dist/file_utils/src/delete_utils/src/delete_row.d.ts +0 -2
  55. package/dist/file_utils/src/delete_utils/src/delete_select.d.ts +0 -4
  56. package/dist/file_utils/src/delete_utils/src/delete_selected.d.ts +0 -2
  57. package/dist/file_utils/src/delete_utils/src/index.d.ts +0 -3
  58. package/dist/file_utils/src/download_utils/imports.d.ts +0 -2
  59. package/dist/file_utils/src/download_utils/index.d.ts +0 -1
  60. package/dist/file_utils/src/download_utils/src/direct_download.d.ts +0 -3
  61. package/dist/file_utils/src/download_utils/src/download_file.d.ts +0 -6
  62. package/dist/file_utils/src/download_utils/src/download_select.d.ts +0 -2
  63. package/dist/file_utils/src/download_utils/src/index.d.ts +0 -5
  64. package/dist/file_utils/src/download_utils/src/target_downloads.d.ts +0 -4
  65. package/dist/file_utils/src/download_utils/src/trigger_download.d.ts +0 -2
  66. package/dist/file_utils/src/imports.d.ts +0 -2
  67. package/dist/file_utils/src/index.d.ts +0 -6
  68. package/dist/file_utils/src/list_utils/imports.d.ts +0 -1
  69. package/dist/file_utils/src/list_utils/index.d.ts +0 -1
  70. package/dist/file_utils/src/list_utils/src/index.d.ts +0 -1
  71. package/dist/file_utils/src/list_utils/src/list_utils.d.ts +0 -2
  72. package/dist/file_utils/src/select_utils/index.d.ts +0 -1
  73. package/dist/file_utils/src/select_utils/src/index.d.ts +0 -3
  74. package/dist/file_utils/src/select_utils/src/init_select_all.d.ts +0 -2
  75. package/dist/file_utils/src/select_utils/src/sync_header_checkbox.d.ts +0 -5
  76. package/dist/file_utils/src/select_utils/src/toggle_checkboxes.d.ts +0 -2
  77. package/dist/file_utils/src/share_utils/imports.d.ts +0 -1
  78. package/dist/file_utils/src/share_utils/index.d.ts +0 -1
  79. package/dist/file_utils/src/share_utils/src/index.d.ts +0 -1
  80. package/dist/file_utils/src/share_utils/src/share_utils.d.ts +0 -7
  81. package/dist/file_utils/src/upload_utils/imports.d.ts +0 -0
  82. package/dist/file_utils/src/upload_utils/index.d.ts +0 -1
  83. package/dist/file_utils/src/upload_utils/src/index.d.ts +0 -3
  84. package/dist/file_utils/src/upload_utils/src/patch_file_settings.d.ts +0 -2
  85. package/dist/file_utils/src/upload_utils/src/upload_file.d.ts +0 -2
  86. package/dist/file_utils/src/upload_utils/src/upload_files.d.ts +0 -5
  87. package/dist/hooks/imports.d.ts +0 -3
  88. package/dist/hooks/index.d.ts +0 -1
  89. package/dist/hooks/useFiles/imports.d.ts +0 -2
  90. package/dist/hooks/useFiles/index.d.ts +0 -1
  91. package/dist/hooks/useFiles/src/deleteFile.d.ts +0 -3
  92. package/dist/hooks/useFiles/src/index.d.ts +0 -6
  93. package/dist/hooks/useFiles/src/load.d.ts +0 -3
  94. package/dist/hooks/useFiles/src/patchShareSettings.d.ts +0 -3
  95. package/dist/hooks/useFiles/src/toggleShare.d.ts +0 -3
  96. package/dist/hooks/useFiles/src/useFiles.d.ts +0 -13
  97. package/dist/hooks/useFiles/src/useSaveShareSettings.d.ts +0 -7
  98. package/dist/imports.d.ts +0 -6
  99. package/dist/index.css +0 -1
  100. package/dist/styles/index.d.ts +0 -1
  101. package/dist/types/index.d.ts +0 -1
  102. package/dist/types/src/index.d.ts +0 -1
  103. package/dist/types/src/types.d.ts +0 -75
  104. package/index.ts +0 -1
  105. package/postcss.config.cjs +0 -8
  106. package/rollup.config.js +0 -38
  107. package/rollup.config.mjs +0 -35
  108. package/src/App/SecureFiles.tsx +0 -45
  109. package/src/App/index.ts +0 -1
  110. package/src/FileHandlers/FileRow/imports.ts +0 -2
  111. package/src/FileHandlers/FileRow/index.ts +0 -1
  112. package/src/FileHandlers/FileRow/src/index.ts +0 -1
  113. package/src/FileHandlers/FileRow/src/row_utils.tsx +0 -143
  114. package/src/FileHandlers/FileTable/imports.ts +0 -15
  115. package/src/FileHandlers/FileTable/index.ts +0 -1
  116. package/src/FileHandlers/FileTable/src/index.ts +0 -1
  117. package/src/FileHandlers/FileTable/src/table_utils.tsx +0 -251
  118. package/src/FileHandlers/ShareModal/imports.ts +0 -2
  119. package/src/FileHandlers/ShareModal/index.ts +0 -1
  120. package/src/FileHandlers/ShareModal/src/index.ts +0 -1
  121. package/src/FileHandlers/ShareModal/src/share_utils.tsx +0 -155
  122. package/src/FileHandlers/Upload/imports.ts +0 -3
  123. package/src/FileHandlers/Upload/index.ts +0 -1
  124. package/src/FileHandlers/Upload/src/index.ts +0 -2
  125. package/src/FileHandlers/Upload/src/upload_utils.tsx +0 -369
  126. package/src/FileHandlers/imports.ts +0 -17
  127. package/src/FileHandlers/index.ts +0 -4
  128. package/src/UI/index.tsx +0 -122
  129. package/src/custom.d.ts +0 -7
  130. package/src/file_utils/imports.ts +0 -2
  131. package/src/file_utils/index.ts +0 -1
  132. package/src/file_utils/src/cookieFetch.ts +0 -109
  133. package/src/file_utils/src/delete_utils/imports.ts +0 -1
  134. package/src/file_utils/src/delete_utils/index.ts +0 -1
  135. package/src/file_utils/src/delete_utils/src/delete_row.ts +0 -11
  136. package/src/file_utils/src/delete_utils/src/delete_select.ts +0 -10
  137. package/src/file_utils/src/delete_utils/src/delete_selected.ts +0 -15
  138. package/src/file_utils/src/delete_utils/src/index.ts +0 -3
  139. package/src/file_utils/src/download_utils/imports.ts +0 -3
  140. package/src/file_utils/src/download_utils/index.ts +0 -1
  141. package/src/file_utils/src/download_utils/src/direct_download.ts +0 -22
  142. package/src/file_utils/src/download_utils/src/download_file.ts +0 -25
  143. package/src/file_utils/src/download_utils/src/download_select.ts +0 -25
  144. package/src/file_utils/src/download_utils/src/index.ts +0 -5
  145. package/src/file_utils/src/download_utils/src/target_downloads.ts +0 -21
  146. package/src/file_utils/src/download_utils/src/trigger_download.ts +0 -14
  147. package/src/file_utils/src/imports.ts +0 -3
  148. package/src/file_utils/src/index.ts +0 -6
  149. package/src/file_utils/src/list_utils/imports.ts +0 -2
  150. package/src/file_utils/src/list_utils/index.ts +0 -1
  151. package/src/file_utils/src/list_utils/src/index.ts +0 -1
  152. package/src/file_utils/src/list_utils/src/list_utils.ts +0 -28
  153. package/src/file_utils/src/select_utils/index.ts +0 -1
  154. package/src/file_utils/src/select_utils/src/index.ts +0 -3
  155. package/src/file_utils/src/select_utils/src/init_select_all.ts +0 -37
  156. package/src/file_utils/src/select_utils/src/sync_header_checkbox.ts +0 -18
  157. package/src/file_utils/src/select_utils/src/toggle_checkboxes.ts +0 -11
  158. package/src/file_utils/src/share_utils/imports.ts +0 -1
  159. package/src/file_utils/src/share_utils/index.ts +0 -1
  160. package/src/file_utils/src/share_utils/src/index.ts +0 -1
  161. package/src/file_utils/src/share_utils/src/share_utils.ts +0 -20
  162. package/src/file_utils/src/upload_utils/imports.ts +0 -1
  163. package/src/file_utils/src/upload_utils/index.ts +0 -1
  164. package/src/file_utils/src/upload_utils/src/index.ts +0 -3
  165. package/src/file_utils/src/upload_utils/src/patch_file_settings.ts +0 -20
  166. package/src/file_utils/src/upload_utils/src/upload_file.ts +0 -27
  167. package/src/file_utils/src/upload_utils/src/upload_files.ts +0 -19
  168. package/src/hooks/imports.ts +0 -3
  169. package/src/hooks/index.ts +0 -1
  170. package/src/hooks/useFiles/imports.ts +0 -3
  171. package/src/hooks/useFiles/index.ts +0 -1
  172. package/src/hooks/useFiles/src/deleteFile.ts +0 -23
  173. package/src/hooks/useFiles/src/index.ts +0 -6
  174. package/src/hooks/useFiles/src/load.ts +0 -39
  175. package/src/hooks/useFiles/src/logs/clipit_logs.log +0 -0
  176. package/src/hooks/useFiles/src/logs/routes_bp.log +0 -0
  177. package/src/hooks/useFiles/src/patchShareSettings.ts +0 -27
  178. package/src/hooks/useFiles/src/toggleShare.ts +0 -28
  179. package/src/hooks/useFiles/src/useFiles.ts +0 -35
  180. package/src/hooks/useFiles/src/useSaveShareSettings.ts +0 -31
  181. package/src/imports.ts +0 -18
  182. package/src/styles/SecureFiles/App.css +0 -42
  183. package/src/styles/SecureFiles/index.css +0 -10
  184. package/src/styles/SecureFiles/logs/clipit_logs.log +0 -0
  185. package/src/styles/SecureFiles/logs/routes_bp.log +0 -0
  186. package/src/styles/SecureFiles/tailwind.css +0 -3
  187. package/src/styles/SecureFiles/window-manager.css +0 -283
  188. package/src/styles/SecureFilesStyles.module.css +0 -287
  189. package/src/styles/index.ts +0 -1
  190. package/src/types/index.ts +0 -1
  191. package/src/types/src/index.ts +0 -1
  192. package/src/types/src/types.ts +0 -88
  193. package/tailwind.config.cjs +0 -7
  194. package/testit.mjs +0 -11
  195. package/testit.ts +0 -3
  196. package/tsconfig.json +0 -27
  197. /package/src/{global.d.ts → css.d.ts} +0 -0
@@ -0,0 +1,339 @@
1
+ import React, { useCallback, useEffect, useState } from "react";
2
+ import type { SecureFilesApi } from "./api";
3
+ import type { BrowseResult, LinkRoot, RootChallenge } from "./types";
4
+ import { ConfirmButton } from "./ConfirmButton";
5
+ import { CopyButton } from "./CopyButton";
6
+ import { FolderSelect } from "./FolderSelect";
7
+
8
+ function formatSize(bytes: number) {
9
+ const units = ["B", "KB", "MB", "GB", "TB"];
10
+ let n = bytes;
11
+ let i = 0;
12
+ while (n >= 1024 && i < units.length - 1) {
13
+ n /= 1024;
14
+ i++;
15
+ }
16
+ return `${n.toFixed(i ? 1 : 0)} ${units[i]}`;
17
+ }
18
+
19
+ const errText = (err: unknown) => (err instanceof Error ? err.message : String(err));
20
+
21
+ function AddRoot({ api, onAdded }: { api: SecureFilesApi; onAdded: (roots: LinkRoot[]) => void }) {
22
+ const [path, setPath] = useState("");
23
+ const [label, setLabel] = useState("");
24
+ const [challenge, setChallenge] = useState<RootChallenge | null>(null);
25
+ const [signature, setSignature] = useState("");
26
+ const [error, setError] = useState<string | null>(null);
27
+ const [busy, setBusy] = useState(false);
28
+
29
+ const request = async () => {
30
+ setBusy(true);
31
+ setError(null);
32
+ try {
33
+ setChallenge((await api.requestRoot(path.trim())).challenge);
34
+ setSignature("");
35
+ } catch (err) {
36
+ setError(errText(err));
37
+ } finally {
38
+ setBusy(false);
39
+ }
40
+ };
41
+
42
+ const confirm = async () => {
43
+ if (!challenge) return;
44
+ setBusy(true);
45
+ setError(null);
46
+ try {
47
+ onAdded((await api.confirmRoot(challenge.nonce, signature, label)).roots);
48
+ setChallenge(null);
49
+ setPath("");
50
+ setLabel("");
51
+ setSignature("");
52
+ } catch (err) {
53
+ setError(errText(err));
54
+ } finally {
55
+ setBusy(false);
56
+ }
57
+ };
58
+
59
+ const minutesLeft = challenge ? Math.max(0, Math.round((challenge.expires * 1000 - Date.now()) / 60000)) : 0;
60
+
61
+ return (
62
+ <div className="sf-card">
63
+ <h4>Allow a directory</h4>
64
+ {!challenge ? (
65
+ <div className="sf-row sf-row--bottom">
66
+ <label className="sf-field">
67
+ <span>Absolute path on the server</span>
68
+ <input
69
+ className="sf-input"
70
+ value={path}
71
+ onChange={(e) => setPath(e.target.value)}
72
+ placeholder="/srv/shared/docs"
73
+ onKeyDown={(e) => e.key === "Enter" && path.trim() && request()}
74
+ />
75
+ </label>
76
+ <label className="sf-field sf-field--narrow">
77
+ <span>Label (optional)</span>
78
+ <input className="sf-input" value={label} onChange={(e) => setLabel(e.target.value)} />
79
+ </label>
80
+ <button type="button" className="sf-btn sf-btn--primary" disabled={busy || !path.trim()} onClick={request}>
81
+ Request
82
+ </button>
83
+ </div>
84
+ ) : (
85
+ <div className="sf-stack">
86
+ <p>
87
+ Allowing <code>{challenge.message.split("|")[3]}</code> requires a signature from a trusted SSH key. Run
88
+ this where that key lives (expires in about {minutesLeft} min):
89
+ </p>
90
+ <div className="sf-row">
91
+ <textarea className="sf-input sf-code" readOnly rows={3} value={challenge.command} />
92
+ <CopyButton text={challenge.command} label="Copy command" />
93
+ </div>
94
+ <label className="sf-field">
95
+ <span>Paste the full output (BEGIN … END SSH SIGNATURE)</span>
96
+ <textarea
97
+ className="sf-input sf-code"
98
+ rows={6}
99
+ value={signature}
100
+ onChange={(e) => setSignature(e.target.value)}
101
+ placeholder="-----BEGIN SSH SIGNATURE-----"
102
+ />
103
+ </label>
104
+ <div className="sf-row">
105
+ <button
106
+ type="button"
107
+ className="sf-btn sf-btn--primary"
108
+ disabled={busy || !signature.includes("END SSH SIGNATURE")}
109
+ onClick={confirm}
110
+ >
111
+ Verify &amp; allow
112
+ </button>
113
+ <button type="button" className="sf-btn" onClick={() => setChallenge(null)}>
114
+ Cancel
115
+ </button>
116
+ </div>
117
+ </div>
118
+ )}
119
+ {error && <p className="sf-error">{error}</p>}
120
+ </div>
121
+ );
122
+ }
123
+
124
+ function Browser({
125
+ api,
126
+ roots,
127
+ folders,
128
+ onLinked,
129
+ }: {
130
+ api: SecureFilesApi;
131
+ roots: LinkRoot[];
132
+ folders: string[];
133
+ onLinked: () => void;
134
+ }) {
135
+ const [listing, setListing] = useState<BrowseResult | null>(null);
136
+ const [picked, setPicked] = useState<Set<string>>(new Set());
137
+ const [target, setTarget] = useState("");
138
+ const [error, setError] = useState<string | null>(null);
139
+ const [result, setResult] = useState<string | null>(null);
140
+ const [busy, setBusy] = useState(false);
141
+
142
+ const open = useCallback(
143
+ async (path: string) => {
144
+ setError(null);
145
+ try {
146
+ setListing(await api.browse(path));
147
+ setPicked(new Set());
148
+ } catch (err) {
149
+ setError(errText(err));
150
+ }
151
+ },
152
+ [api]
153
+ );
154
+
155
+ useEffect(() => {
156
+ if (!listing && roots.length) open(roots[0].path);
157
+ if (listing && !roots.some((r) => listing.path.startsWith(r.path))) setListing(null);
158
+ }, [roots, listing, open]);
159
+
160
+ const togglePick = (path: string, on: boolean) =>
161
+ setPicked((p) => {
162
+ const next = new Set(p);
163
+ on ? next.add(path) : next.delete(path);
164
+ return next;
165
+ });
166
+
167
+ const link = async () => {
168
+ setBusy(true);
169
+ setResult(null);
170
+ try {
171
+ const res = await api.link([...picked], target);
172
+ const parts = [`Linked ${res.linked.length} file(s)`];
173
+ if (res.errors.length) parts.push(`${res.errors.length} skipped: ${res.errors.map((e) => e.error).join("; ")}`);
174
+ setResult(parts.join(". "));
175
+ onLinked();
176
+ if (listing) await open(listing.path);
177
+ } catch (err) {
178
+ setResult(errText(err));
179
+ } finally {
180
+ setBusy(false);
181
+ }
182
+ };
183
+
184
+ if (!roots.length) return <p className="sf-muted">Allow a directory above to browse and link its files.</p>;
185
+
186
+ const linkable = [
187
+ ...(listing?.dirs.filter((d) => !d.linked_id) ?? []),
188
+ ...(listing?.files.filter((f) => !f.linked_id) ?? []),
189
+ ];
190
+ const relative = listing ? listing.path.slice(listing.root.length) || "/" : "";
191
+
192
+ return (
193
+ <div className="sf-card">
194
+ <h4>Link files or folders</h4>
195
+ <div className="sf-row">
196
+ <select
197
+ className="sf-input sf-root-select"
198
+ value={listing?.root ?? ""}
199
+ onChange={(e) => open(e.target.value)}
200
+ >
201
+ {roots.map((r) => (
202
+ <option key={r.id} value={r.path}>
203
+ {r.label ? `${r.label} (${r.path})` : r.path}
204
+ </option>
205
+ ))}
206
+ </select>
207
+ {listing && <code className="sf-muted">{relative}</code>}
208
+ </div>
209
+ {error && <p className="sf-error">{error}</p>}
210
+ {listing && (
211
+ <div className="sf-browser">
212
+ {listing.parent && (
213
+ <button type="button" className="sf-browser__dir" onClick={() => open(listing.parent!)}>
214
+ ↩ ..
215
+ </button>
216
+ )}
217
+ {listing.dirs.map((d) => (
218
+ <div key={d.path} className={`sf-browser__row${d.linked_id ? " sf-muted" : ""}`}>
219
+ <input
220
+ type="checkbox"
221
+ aria-label={`Select folder ${d.name}`}
222
+ disabled={!!d.linked_id}
223
+ checked={picked.has(d.path)}
224
+ onChange={(e) => togglePick(d.path, e.target.checked)}
225
+ />
226
+ <button type="button" className="sf-browser__dir" onClick={() => open(d.path)} title="Open folder">
227
+ 📁 {d.name}
228
+ </button>
229
+ <span className="sf-muted">{d.linked_id ? "already linked" : "folder → .tar"}</span>
230
+ </div>
231
+ ))}
232
+ {linkable.length > 0 && (
233
+ <label className="sf-check sf-browser__all">
234
+ <input
235
+ type="checkbox"
236
+ checked={linkable.length > 0 && linkable.every((f) => picked.has(f.path))}
237
+ disabled={!linkable.length}
238
+ onChange={(e) => setPicked(e.target.checked ? new Set(linkable.map((f) => f.path)) : new Set())}
239
+ />
240
+ Select everything here
241
+ </label>
242
+ )}
243
+ {listing.files.map((f) => (
244
+ <label key={f.path} className={`sf-check sf-browser__file${f.linked_id ? " sf-muted" : ""}`}>
245
+ <input
246
+ type="checkbox"
247
+ disabled={!!f.linked_id}
248
+ checked={picked.has(f.path)}
249
+ onChange={(e) => togglePick(f.path, e.target.checked)}
250
+ />
251
+ <span className="sf-browser__name">{f.name}</span>
252
+ <span className="sf-muted">{f.linked_id ? "already linked" : formatSize(f.size)}</span>
253
+ </label>
254
+ ))}
255
+ {!listing.dirs.length && !listing.files.length && <p className="sf-muted">Empty directory</p>}
256
+ {listing.truncated && <p className="sf-muted">Showing the first 5000 entries.</p>}
257
+ </div>
258
+ )}
259
+ <div className="sf-row sf-row--bottom">
260
+ <FolderSelect label="Show in Secure Files under" folders={folders} value={target} onChange={setTarget} />
261
+ <button type="button" className="sf-btn sf-btn--primary" disabled={busy || !picked.size} onClick={link}>
262
+ {busy ? "Linking…" : `Link ${picked.size || ""}`.trim()}
263
+ </button>
264
+ </div>
265
+ {result && <p className={result.includes("skipped") ? "sf-error" : "sf-muted"}>{result}</p>}
266
+ </div>
267
+ );
268
+ }
269
+
270
+ export function ServerFiles({
271
+ api,
272
+ folders,
273
+ onLinked,
274
+ }: {
275
+ api: SecureFilesApi;
276
+ folders: string[];
277
+ onLinked: () => void;
278
+ }) {
279
+ const [roots, setRoots] = useState<LinkRoot[] | null>(null);
280
+ const [error, setError] = useState<string | null>(null);
281
+
282
+ const load = useCallback(async () => {
283
+ try {
284
+ setRoots((await api.roots()).roots);
285
+ setError(null);
286
+ } catch (err) {
287
+ setError(errText(err));
288
+ }
289
+ }, [api]);
290
+
291
+ return (
292
+ <details className="sf-admin" onToggle={(e) => (e.currentTarget as HTMLDetailsElement).open && !roots && load()}>
293
+ <summary>Server files (admin)</summary>
294
+ <div className="sf-stack">
295
+ <p className="sf-muted">
296
+ Link files or whole folders that already live on the server instead of uploading copies. A linked folder
297
+ downloads as one uncompressed .tar with an exact size, and interrupted downloads can be resumed. Only
298
+ items inside allowed directories can be linked; removing a directory immediately disables its links.
299
+ Deleting a linked entry never touches the original. Available from the local network only.
300
+ </p>
301
+ {error && <p className="sf-error">{error}</p>}
302
+ {roots && (
303
+ <>
304
+ <div className="sf-card">
305
+ <h4>Allowed directories</h4>
306
+ {roots.length === 0 ? (
307
+ <p className="sf-muted">None yet.</p>
308
+ ) : (
309
+ <ul className="sf-roots">
310
+ {roots.map((r) => (
311
+ <li key={r.id}>
312
+ <span>
313
+ <code>{r.path}</code> {r.label && <span className="sf-muted">— {r.label}</span>}
314
+ {!r.exists && <span className="sf-error"> (missing)</span>}
315
+ </span>
316
+ <ConfirmButton
317
+ label="Remove"
318
+ onConfirm={async () => {
319
+ try {
320
+ setRoots((await api.removeRoot(r.id)).roots);
321
+ onLinked();
322
+ } catch (err) {
323
+ setError(errText(err));
324
+ }
325
+ }}
326
+ />
327
+ </li>
328
+ ))}
329
+ </ul>
330
+ )}
331
+ </div>
332
+ <AddRoot api={api} onAdded={setRoots} />
333
+ <Browser api={api} roots={roots} folders={folders} onLinked={onLinked} />
334
+ </>
335
+ )}
336
+ </div>
337
+ </details>
338
+ );
339
+ }
@@ -0,0 +1,148 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import type { SecureFile, ShareUpdate } from "./types";
3
+ import { CopyButton } from "./CopyButton";
4
+
5
+ type PasswordMode = "keep" | "set" | "remove";
6
+
7
+ export function ShareDialog({
8
+ file,
9
+ onClose,
10
+ onSave,
11
+ }: {
12
+ file: SecureFile;
13
+ onClose: () => void;
14
+ onSave: (update: ShareUpdate) => Promise<void>;
15
+ }) {
16
+ const [shareable, setShareable] = useState(file.shareable);
17
+ const [limit, setLimit] = useState(file.download_limit ? String(file.download_limit) : "");
18
+ const [pwMode, setPwMode] = useState<PasswordMode>(file.has_password ? "keep" : "remove");
19
+ const [password, setPassword] = useState("");
20
+ const [resetCount, setResetCount] = useState(false);
21
+ const [saving, setSaving] = useState(false);
22
+ const [error, setError] = useState<string | null>(null);
23
+
24
+ useEffect(() => {
25
+ const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose();
26
+ window.addEventListener("keydown", onKey);
27
+ return () => window.removeEventListener("keydown", onKey);
28
+ }, [onClose]);
29
+
30
+ const limitValid = limit === "" || /^\d+$/.test(limit);
31
+ const passwordValid = pwMode !== "set" || password.length > 0;
32
+
33
+ const save = async () => {
34
+ setSaving(true);
35
+ setError(null);
36
+ try {
37
+ const update: ShareUpdate = { id: file.id, shareable };
38
+ if (shareable) {
39
+ update.download_limit = limit === "" ? null : Number(limit);
40
+ if (pwMode === "set") update.password = password;
41
+ else if (pwMode === "remove" && file.has_password) update.password = "";
42
+ if (resetCount) update.reset_count = true;
43
+ }
44
+ await onSave(update);
45
+ onClose();
46
+ } catch (err) {
47
+ setError(err instanceof Error ? err.message : String(err));
48
+ } finally {
49
+ setSaving(false);
50
+ }
51
+ };
52
+
53
+ return (
54
+ <div className="sf-overlay" onMouseDown={(e) => e.target === e.currentTarget && onClose()}>
55
+ <div className="sf-dialog" role="dialog" aria-modal="true" aria-label={`Share settings for ${file.filename}`}>
56
+ <header className="sf-dialog__head">
57
+ <h3>Share settings</h3>
58
+ <button type="button" className="sf-icon-btn" aria-label="Close" onClick={onClose}>
59
+ ✕
60
+ </button>
61
+ </header>
62
+ <p className="sf-muted">{file.filepath}</p>
63
+
64
+ <label className="sf-check">
65
+ <input type="checkbox" checked={shareable} onChange={(e) => setShareable(e.target.checked)} />
66
+ Anyone with the link can download
67
+ </label>
68
+
69
+ <fieldset disabled={!shareable} className="sf-fieldset">
70
+ <label className="sf-field">
71
+ <span>Download limit (blank = unlimited)</span>
72
+ <input
73
+ className="sf-input"
74
+ inputMode="numeric"
75
+ value={limit}
76
+ onChange={(e) => setLimit(e.target.value.trim())}
77
+ placeholder="unlimited"
78
+ />
79
+ {!limitValid && <span className="sf-error">Enter a whole number</span>}
80
+ </label>
81
+ <p className="sf-muted">
82
+ Downloaded {file.download_count} time{file.download_count === 1 ? "" : "s"}
83
+ {file.download_limit ? ` of ${file.download_limit}` : ""}.{" "}
84
+ <label className="sf-check sf-check--inline">
85
+ <input type="checkbox" checked={resetCount} onChange={(e) => setResetCount(e.target.checked)} />
86
+ reset count
87
+ </label>
88
+ </p>
89
+
90
+ <div className="sf-field">
91
+ <span>Password</span>
92
+ <div className="sf-row">
93
+ {file.has_password && (
94
+ <label className="sf-check sf-check--inline">
95
+ <input type="radio" checked={pwMode === "keep"} onChange={() => setPwMode("keep")} />
96
+ keep current
97
+ </label>
98
+ )}
99
+ <label className="sf-check sf-check--inline">
100
+ <input type="radio" checked={pwMode === "set"} onChange={() => setPwMode("set")} />
101
+ {file.has_password ? "change" : "require password"}
102
+ </label>
103
+ <label className="sf-check sf-check--inline">
104
+ <input type="radio" checked={pwMode === "remove"} onChange={() => setPwMode("remove")} />
105
+ {file.has_password ? "remove" : "none"}
106
+ </label>
107
+ </div>
108
+ {pwMode === "set" && (
109
+ <input
110
+ className="sf-input"
111
+ type="password"
112
+ autoComplete="new-password"
113
+ value={password}
114
+ onChange={(e) => setPassword(e.target.value)}
115
+ placeholder="New password"
116
+ />
117
+ )}
118
+ </div>
119
+
120
+ <div className="sf-field">
121
+ <span>Share link</span>
122
+ <div className="sf-row">
123
+ <input className="sf-input" readOnly value={file.share_url} onFocus={(e) => e.target.select()} />
124
+ <CopyButton text={file.share_url} />
125
+ </div>
126
+ {!file.shareable && <span className="sf-muted">The link works once you save with sharing on.</span>}
127
+ </div>
128
+ </fieldset>
129
+
130
+ {error && <p className="sf-error">{error}</p>}
131
+
132
+ <footer className="sf-row sf-dialog__foot">
133
+ <button type="button" className="sf-btn" onClick={onClose}>
134
+ Cancel
135
+ </button>
136
+ <button
137
+ type="button"
138
+ className="sf-btn sf-btn--primary"
139
+ disabled={saving || !limitValid || !passwordValid}
140
+ onClick={save}
141
+ >
142
+ {saving ? "Saving…" : "Save"}
143
+ </button>
144
+ </footer>
145
+ </div>
146
+ </div>
147
+ );
148
+ }