@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
|
@@ -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 & 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
|
+
}
|