@putkoff/abstract-securefiles 0.0.49 → 0.0.51
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 +51 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +51 -46
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +11 -2
- package/package.json +1 -1
- package/src/SecureFiles.tsx +7 -5
- package/src/ServerFiles.tsx +32 -17
- package/src/ShareDialog.tsx +26 -5
- package/src/api.ts +4 -15
- package/src/styles.css +10 -0
- package/src/types.ts +7 -2
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/ShareDialog.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useState } from "react";
|
|
2
2
|
import type { SecureFile, ShareUpdate } from "./types";
|
|
3
3
|
import { CopyButton } from "./CopyButton";
|
|
4
|
+
import { ConfirmButton } from "./ConfirmButton";
|
|
4
5
|
|
|
5
6
|
type PasswordMode = "keep" | "set" | "remove";
|
|
6
7
|
|
|
@@ -30,6 +31,18 @@ export function ShareDialog({
|
|
|
30
31
|
const limitValid = limit === "" || /^\d+$/.test(limit);
|
|
31
32
|
const passwordValid = pwMode !== "set" || password.length > 0;
|
|
32
33
|
|
|
34
|
+
const newLink = async () => {
|
|
35
|
+
setSaving(true);
|
|
36
|
+
setError(null);
|
|
37
|
+
try {
|
|
38
|
+
await onSave({ id: file.id, shareable: true, regenerate_link: true });
|
|
39
|
+
} catch (err) {
|
|
40
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
41
|
+
} finally {
|
|
42
|
+
setSaving(false);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
33
46
|
const save = async () => {
|
|
34
47
|
setSaving(true);
|
|
35
48
|
setError(null);
|
|
@@ -119,11 +132,19 @@ export function ShareDialog({
|
|
|
119
132
|
|
|
120
133
|
<div className="sf-field">
|
|
121
134
|
<span>Share link</span>
|
|
122
|
-
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
135
|
+
{file.share_url ? (
|
|
136
|
+
<div className="sf-row">
|
|
137
|
+
<input className="sf-input" readOnly value={file.share_url} onFocus={(e) => e.target.select()} />
|
|
138
|
+
<CopyButton text={file.share_url} />
|
|
139
|
+
<ConfirmButton label="New link" disabled={saving} onConfirm={newLink} />
|
|
140
|
+
</div>
|
|
141
|
+
) : (
|
|
142
|
+
<span className="sf-muted">A private link is created when you save with sharing on.</span>
|
|
143
|
+
)}
|
|
144
|
+
{file.share_url && !file.shareable && (
|
|
145
|
+
<span className="sf-muted">The link works once you save with sharing on.</span>
|
|
146
|
+
)}
|
|
147
|
+
<span className="sf-muted">"New link" invalidates the current one immediately.</span>
|
|
127
148
|
</div>
|
|
128
149
|
</fieldset>
|
|
129
150
|
|
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
|
@@ -8,9 +8,12 @@ export interface SecureFile {
|
|
|
8
8
|
download_count: number;
|
|
9
9
|
download_limit: number | null;
|
|
10
10
|
created_at: string | null;
|
|
11
|
-
|
|
11
|
+
/** unguessable public link (/secure-files/s/<token>); null until the file is first shared */
|
|
12
|
+
share_url: string | null;
|
|
12
13
|
/** true when the entry points at a file elsewhere on the server instead of an upload */
|
|
13
14
|
linked: boolean;
|
|
15
|
+
/** "folder" entries download as an uncompressed, resumable .tar of the whole directory */
|
|
16
|
+
kind: "file" | "folder";
|
|
14
17
|
source_path: string | null;
|
|
15
18
|
/** false when the file is missing on disk (or its allowed directory was removed) */
|
|
16
19
|
available: boolean;
|
|
@@ -34,7 +37,7 @@ export interface BrowseResult {
|
|
|
34
37
|
path: string;
|
|
35
38
|
root: string;
|
|
36
39
|
parent: string | null;
|
|
37
|
-
dirs: string[];
|
|
40
|
+
dirs: { name: string; path: string; linked_id: number | null }[];
|
|
38
41
|
files: { name: string; path: string; size: number; linked_id: number | null }[];
|
|
39
42
|
truncated: boolean;
|
|
40
43
|
}
|
|
@@ -46,6 +49,8 @@ export interface ShareUpdate {
|
|
|
46
49
|
/** null/undefined = leave unchanged, "" = remove, otherwise the new password */
|
|
47
50
|
password?: string | null;
|
|
48
51
|
reset_count?: boolean;
|
|
52
|
+
/** issue a new share link; the old one stops working */
|
|
53
|
+
regenerate_link?: boolean;
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
export interface SecureFilesProps {
|