@putkoff/abstract-securefiles 0.0.49 → 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/api.d.ts +2 -1
- package/dist/index.cjs +18 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +18 -26
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +7 -1
- package/package.json +1 -1
- package/src/SecureFiles.tsx +6 -4
- package/src/ServerFiles.tsx +32 -17
- package/src/api.ts +4 -15
- package/src/styles.css +10 -0
- package/src/types.ts +3 -1
package/src/ServerFiles.tsx
CHANGED
|
@@ -157,6 +157,13 @@ function Browser({
|
|
|
157
157
|
if (listing && !roots.some((r) => listing.path.startsWith(r.path))) setListing(null);
|
|
158
158
|
}, [roots, listing, open]);
|
|
159
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
|
+
|
|
160
167
|
const link = async () => {
|
|
161
168
|
setBusy(true);
|
|
162
169
|
setResult(null);
|
|
@@ -176,12 +183,15 @@ function Browser({
|
|
|
176
183
|
|
|
177
184
|
if (!roots.length) return <p className="sf-muted">Allow a directory above to browse and link its files.</p>;
|
|
178
185
|
|
|
179
|
-
const linkable =
|
|
186
|
+
const linkable = [
|
|
187
|
+
...(listing?.dirs.filter((d) => !d.linked_id) ?? []),
|
|
188
|
+
...(listing?.files.filter((f) => !f.linked_id) ?? []),
|
|
189
|
+
];
|
|
180
190
|
const relative = listing ? listing.path.slice(listing.root.length) || "/" : "";
|
|
181
191
|
|
|
182
192
|
return (
|
|
183
193
|
<div className="sf-card">
|
|
184
|
-
<h4>Link files</h4>
|
|
194
|
+
<h4>Link files or folders</h4>
|
|
185
195
|
<div className="sf-row">
|
|
186
196
|
<select
|
|
187
197
|
className="sf-input sf-root-select"
|
|
@@ -205,11 +215,21 @@ function Browser({
|
|
|
205
215
|
</button>
|
|
206
216
|
)}
|
|
207
217
|
{listing.dirs.map((d) => (
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
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>
|
|
211
231
|
))}
|
|
212
|
-
{
|
|
232
|
+
{linkable.length > 0 && (
|
|
213
233
|
<label className="sf-check sf-browser__all">
|
|
214
234
|
<input
|
|
215
235
|
type="checkbox"
|
|
@@ -217,7 +237,7 @@ function Browser({
|
|
|
217
237
|
disabled={!linkable.length}
|
|
218
238
|
onChange={(e) => setPicked(e.target.checked ? new Set(linkable.map((f) => f.path)) : new Set())}
|
|
219
239
|
/>
|
|
220
|
-
Select
|
|
240
|
+
Select everything here
|
|
221
241
|
</label>
|
|
222
242
|
)}
|
|
223
243
|
{listing.files.map((f) => (
|
|
@@ -226,13 +246,7 @@ function Browser({
|
|
|
226
246
|
type="checkbox"
|
|
227
247
|
disabled={!!f.linked_id}
|
|
228
248
|
checked={picked.has(f.path)}
|
|
229
|
-
onChange={(e) =>
|
|
230
|
-
setPicked((p) => {
|
|
231
|
-
const next = new Set(p);
|
|
232
|
-
e.target.checked ? next.add(f.path) : next.delete(f.path);
|
|
233
|
-
return next;
|
|
234
|
-
})
|
|
235
|
-
}
|
|
249
|
+
onChange={(e) => togglePick(f.path, e.target.checked)}
|
|
236
250
|
/>
|
|
237
251
|
<span className="sf-browser__name">{f.name}</span>
|
|
238
252
|
<span className="sf-muted">{f.linked_id ? "already linked" : formatSize(f.size)}</span>
|
|
@@ -279,9 +293,10 @@ export function ServerFiles({
|
|
|
279
293
|
<summary>Server files (admin)</summary>
|
|
280
294
|
<div className="sf-stack">
|
|
281
295
|
<p className="sf-muted">
|
|
282
|
-
Link files that already live on the server instead of uploading copies.
|
|
283
|
-
|
|
284
|
-
|
|
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.
|
|
285
300
|
</p>
|
|
286
301
|
{error && <p className="sf-error">{error}</p>}
|
|
287
302
|
{roots && (
|
package/src/api.ts
CHANGED
|
@@ -66,25 +66,14 @@ export function createApi(apiBase: string = DEFAULT_API_BASE) {
|
|
|
66
66
|
body: JSON.stringify({ ids }),
|
|
67
67
|
}),
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
method: "POST",
|
|
72
|
-
credentials: "include",
|
|
73
|
-
headers: { "Content-Type": "application/json" },
|
|
74
|
-
body: JSON.stringify({ ids }),
|
|
75
|
-
});
|
|
76
|
-
if (!res.ok) throw await errorFrom(res);
|
|
77
|
-
const disposition = res.headers.get("Content-Disposition") || "";
|
|
78
|
-
const match = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition);
|
|
79
|
-
const name = match ? decodeURIComponent(match[1]) : fallbackName;
|
|
80
|
-
const url = URL.createObjectURL(await res.blob());
|
|
69
|
+
/** Hand the download to the browser so it streams to disk (native progress + resume, no memory limit). */
|
|
70
|
+
download(ids: number[]): void {
|
|
81
71
|
const a = document.createElement("a");
|
|
82
|
-
a.href =
|
|
83
|
-
a.
|
|
72
|
+
a.href = `${root}/download?ids=${ids.join(",")}`;
|
|
73
|
+
a.rel = "noopener";
|
|
84
74
|
document.body.appendChild(a);
|
|
85
75
|
a.click();
|
|
86
76
|
a.remove();
|
|
87
|
-
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
88
77
|
},
|
|
89
78
|
|
|
90
79
|
upload(file: File, relPath: string, onProgress?: (fraction: number) => void): Promise<void> {
|
package/src/styles.css
CHANGED
package/src/types.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface SecureFile {
|
|
|
11
11
|
share_url: string;
|
|
12
12
|
/** true when the entry points at a file elsewhere on the server instead of an upload */
|
|
13
13
|
linked: boolean;
|
|
14
|
+
/** "folder" entries download as an uncompressed, resumable .tar of the whole directory */
|
|
15
|
+
kind: "file" | "folder";
|
|
14
16
|
source_path: string | null;
|
|
15
17
|
/** false when the file is missing on disk (or its allowed directory was removed) */
|
|
16
18
|
available: boolean;
|
|
@@ -34,7 +36,7 @@ export interface BrowseResult {
|
|
|
34
36
|
path: string;
|
|
35
37
|
root: string;
|
|
36
38
|
parent: string | null;
|
|
37
|
-
dirs: string[];
|
|
39
|
+
dirs: { name: string; path: string; linked_id: number | null }[];
|
|
38
40
|
files: { name: string; path: string; size: number; linked_id: number | null }[];
|
|
39
41
|
truncated: boolean;
|
|
40
42
|
}
|