@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.
@@ -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 = listing?.files.filter((f) => !f.linked_id) ?? [];
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
- <button key={d} type="button" className="sf-browser__dir" onClick={() => open(`${listing.path}/${d}`)}>
209
- 📁 {d}
210
- </button>
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
- {listing.files.length > 0 && (
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 all files here
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. Only files inside allowed
283
- directories can be linked; removing a directory immediately disables its links. Deleting a linked entry
284
- never touches the original file. Available from the local network only.
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
- async download(ids: number[], fallbackName: string): Promise<void> {
70
- const res = await fetch(`${root}/download`, {
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 = url;
83
- a.download = name;
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
@@ -430,3 +430,13 @@
430
430
  text-decoration: line-through;
431
431
  opacity: 0.7;
432
432
  }
433
+ .sf-browser__row {
434
+ display: flex;
435
+ align-items: center;
436
+ gap: 8px;
437
+ }
438
+ .sf-browser__row .sf-browser__dir {
439
+ flex: 1;
440
+ border-bottom: none;
441
+ padding: 0;
442
+ }
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
  }