@putkoff/abstract-securefiles 0.0.51 → 0.0.53
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/MyComputers.d.ts +7 -0
- package/dist/api.d.ts +12 -1
- package/dist/index.cjs +76 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +76 -10
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +12 -1
- package/package.json +1 -1
- package/src/MyComputers.tsx +162 -0
- package/src/SecureFiles.tsx +24 -5
- package/src/api.ts +8 -2
- package/src/index.ts +1 -1
- package/src/styles.css +44 -0
- package/src/types.ts +13 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SecureFilesApi } from "./api";
|
|
2
|
+
import type { Device } from "./types";
|
|
3
|
+
export declare function MyComputers({ api, devices, onChange, }: {
|
|
4
|
+
api: SecureFilesApi;
|
|
5
|
+
devices: Device[];
|
|
6
|
+
onChange: () => void;
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BrowseResult, LinkRoot, RootChallenge, SecureFile, ShareUpdate } from "./types";
|
|
1
|
+
import type { BrowseResult, Device, LinkRoot, RootChallenge, SecureFile, ShareUpdate } from "./types";
|
|
2
2
|
export declare const DEFAULT_API_BASE = "https://api.abstractendeavors.com";
|
|
3
3
|
export declare class ApiError extends Error {
|
|
4
4
|
status: number;
|
|
@@ -9,6 +9,17 @@ export declare function createApi(apiBase?: string): {
|
|
|
9
9
|
user_name: string;
|
|
10
10
|
is_admin: boolean;
|
|
11
11
|
files: SecureFile[];
|
|
12
|
+
devices: Device[];
|
|
13
|
+
}>;
|
|
14
|
+
devices: () => Promise<{
|
|
15
|
+
devices: Device[];
|
|
16
|
+
}>;
|
|
17
|
+
pairDevice: () => Promise<{
|
|
18
|
+
code: string;
|
|
19
|
+
expires_in: number;
|
|
20
|
+
}>;
|
|
21
|
+
removeDevice: (id: number) => Promise<{
|
|
22
|
+
devices: Device[];
|
|
12
23
|
}>;
|
|
13
24
|
roots: () => Promise<{
|
|
14
25
|
roots: LinkRoot[];
|
package/dist/index.cjs
CHANGED
|
@@ -37,6 +37,9 @@ function createApi(apiBase = DEFAULT_API_BASE) {
|
|
|
37
37
|
}
|
|
38
38
|
return {
|
|
39
39
|
list: () => json("/userInfo"),
|
|
40
|
+
devices: () => json("/devices"),
|
|
41
|
+
pairDevice: () => json("/devices/pair", { method: "POST", body: "{}" }),
|
|
42
|
+
removeDevice: (id) => json(`/devices/${id}`, { method: "DELETE" }),
|
|
40
43
|
roots: () => json("/admin/roots"),
|
|
41
44
|
requestRoot: (path) => json("/admin/roots", { method: "POST", body: JSON.stringify({ path }) }),
|
|
42
45
|
confirmRoot: (nonce, signature, label) => json("/admin/roots/confirm", {
|
|
@@ -329,7 +332,7 @@ function formatSize(bytes) {
|
|
|
329
332
|
}
|
|
330
333
|
return `${n.toFixed(i ? 1 : 0)} ${units[i]}`;
|
|
331
334
|
}
|
|
332
|
-
const errText = (err) => (err instanceof Error ? err.message : String(err));
|
|
335
|
+
const errText$1 = (err) => (err instanceof Error ? err.message : String(err));
|
|
333
336
|
function AddRoot({ api, onAdded }) {
|
|
334
337
|
const [path, setPath] = react.useState("");
|
|
335
338
|
const [label, setLabel] = react.useState("");
|
|
@@ -345,7 +348,7 @@ function AddRoot({ api, onAdded }) {
|
|
|
345
348
|
setSignature("");
|
|
346
349
|
}
|
|
347
350
|
catch (err) {
|
|
348
|
-
setError(errText(err));
|
|
351
|
+
setError(errText$1(err));
|
|
349
352
|
}
|
|
350
353
|
finally {
|
|
351
354
|
setBusy(false);
|
|
@@ -364,7 +367,7 @@ function AddRoot({ api, onAdded }) {
|
|
|
364
367
|
setSignature("");
|
|
365
368
|
}
|
|
366
369
|
catch (err) {
|
|
367
|
-
setError(errText(err));
|
|
370
|
+
setError(errText$1(err));
|
|
368
371
|
}
|
|
369
372
|
finally {
|
|
370
373
|
setBusy(false);
|
|
@@ -387,7 +390,7 @@ function Browser({ api, roots, folders, onLinked, }) {
|
|
|
387
390
|
setPicked(new Set());
|
|
388
391
|
}
|
|
389
392
|
catch (err) {
|
|
390
|
-
setError(errText(err));
|
|
393
|
+
setError(errText$1(err));
|
|
391
394
|
}
|
|
392
395
|
}, [api]);
|
|
393
396
|
react.useEffect(() => {
|
|
@@ -415,7 +418,7 @@ function Browser({ api, roots, folders, onLinked, }) {
|
|
|
415
418
|
await open(listing.path);
|
|
416
419
|
}
|
|
417
420
|
catch (err) {
|
|
418
|
-
setResult(errText(err));
|
|
421
|
+
setResult(errText$1(err));
|
|
419
422
|
}
|
|
420
423
|
finally {
|
|
421
424
|
setBusy(false);
|
|
@@ -439,7 +442,7 @@ function ServerFiles({ api, folders, onLinked, }) {
|
|
|
439
442
|
setError(null);
|
|
440
443
|
}
|
|
441
444
|
catch (err) {
|
|
442
|
-
setError(errText(err));
|
|
445
|
+
setError(errText$1(err));
|
|
443
446
|
}
|
|
444
447
|
}, [api]);
|
|
445
448
|
return (jsxRuntime.jsxs("details", { className: "sf-admin", onToggle: (e) => e.currentTarget.open && !roots && load(), children: [jsxRuntime.jsx("summary", { children: "Server files (admin)" }), jsxRuntime.jsxs("div", { className: "sf-stack", children: [jsxRuntime.jsx("p", { className: "sf-muted", children: "Link files or whole folders that already live on the server instead of uploading copies. A linked folder downloads as one uncompressed .tar with an exact size, and interrupted downloads can be resumed. Only items inside allowed directories can be linked; removing a directory immediately disables its links. Deleting a linked entry never touches the original. Available from the local network only." }), error && jsxRuntime.jsx("p", { className: "sf-error", children: error }), roots && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "sf-card", children: [jsxRuntime.jsx("h4", { children: "Allowed directories" }), roots.length === 0 ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "None yet." })) : (jsxRuntime.jsx("ul", { className: "sf-roots", children: roots.map((r) => (jsxRuntime.jsxs("li", { children: [jsxRuntime.jsxs("span", { children: [jsxRuntime.jsx("code", { children: r.path }), " ", r.label && jsxRuntime.jsxs("span", { className: "sf-muted", children: ["\u2014 ", r.label] }), !r.exists && jsxRuntime.jsx("span", { className: "sf-error", children: " (missing)" })] }), jsxRuntime.jsx(ConfirmButton, { label: "Remove", onConfirm: async () => {
|
|
@@ -448,11 +451,72 @@ function ServerFiles({ api, folders, onLinked, }) {
|
|
|
448
451
|
onLinked();
|
|
449
452
|
}
|
|
450
453
|
catch (err) {
|
|
451
|
-
setError(errText(err));
|
|
454
|
+
setError(errText$1(err));
|
|
452
455
|
}
|
|
453
456
|
} })] }, r.id))) }))] }), jsxRuntime.jsx(AddRoot, { api: api, onAdded: setRoots }), jsxRuntime.jsx(Browser, { api: api, roots: roots, folders: folders, onLinked: onLinked })] }))] })] }));
|
|
454
457
|
}
|
|
455
458
|
|
|
459
|
+
const errText = (err) => (err instanceof Error ? err.message : String(err));
|
|
460
|
+
const UNIX_CMD = "pipx install abstract-securefiles && pipx ensurepath && ~/.local/bin/securefiles setup";
|
|
461
|
+
const WIN_CMD = "py -m pip install --user pipx; py -m pipx install abstract-securefiles; py -m pipx ensurepath";
|
|
462
|
+
function ago(iso) {
|
|
463
|
+
if (!iso)
|
|
464
|
+
return "never";
|
|
465
|
+
const secs = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1000);
|
|
466
|
+
if (secs < 90)
|
|
467
|
+
return "just now";
|
|
468
|
+
if (secs < 3600)
|
|
469
|
+
return `${Math.round(secs / 60)} min ago`;
|
|
470
|
+
if (secs < 86400)
|
|
471
|
+
return `${Math.round(secs / 3600)} h ago`;
|
|
472
|
+
return `${Math.round(secs / 86400)} d ago`;
|
|
473
|
+
}
|
|
474
|
+
function AddComputer({ api, onPaired }) {
|
|
475
|
+
const [code, setCode] = react.useState(null);
|
|
476
|
+
const [expiresAt, setExpiresAt] = react.useState(0);
|
|
477
|
+
const [, setTick] = react.useState(0);
|
|
478
|
+
const [error, setError] = react.useState(null);
|
|
479
|
+
react.useEffect(() => {
|
|
480
|
+
if (!code)
|
|
481
|
+
return;
|
|
482
|
+
const timer = setInterval(() => {
|
|
483
|
+
if (Date.now() > expiresAt)
|
|
484
|
+
setCode(null);
|
|
485
|
+
setTick((t) => t + 1);
|
|
486
|
+
onPaired();
|
|
487
|
+
}, 5000);
|
|
488
|
+
return () => clearInterval(timer);
|
|
489
|
+
}, [code, expiresAt, onPaired]);
|
|
490
|
+
const create = async () => {
|
|
491
|
+
setError(null);
|
|
492
|
+
try {
|
|
493
|
+
const res = await api.pairDevice();
|
|
494
|
+
setCode(res.code);
|
|
495
|
+
setExpiresAt(Date.now() + res.expires_in * 1000);
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
setError(errText(err));
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
const minutesLeft = Math.max(1, Math.round((expiresAt - Date.now()) / 60000));
|
|
502
|
+
if (!code) {
|
|
503
|
+
return (jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn sf-btn--primary", onClick: create, children: "Add a computer" }), error && jsxRuntime.jsx("span", { className: "sf-error", children: error })] }));
|
|
504
|
+
}
|
|
505
|
+
return (jsxRuntime.jsxs("div", { className: "sf-card", children: [jsxRuntime.jsx("h4", { children: "Connect a computer" }), jsxRuntime.jsxs("ol", { className: "sf-steps", children: [jsxRuntime.jsxs("li", { children: ["On that computer, install the agent (needs Python 3.9+):", jsxRuntime.jsxs("div", { className: "sf-os", children: [jsxRuntime.jsx("b", { children: "Linux / macOS" }), " (Terminal)", jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("code", { className: "sf-codeline", children: UNIX_CMD }), jsxRuntime.jsx(CopyButton, { text: UNIX_CMD, label: "Copy" })] }), jsxRuntime.jsxs("span", { className: "sf-muted", children: ["No pipx? ", jsxRuntime.jsx("code", { children: "python3 -m pip install --user pipx" }), " then use ", jsxRuntime.jsx("code", { children: "python3 -m pipx" }), ". On a Mac without Python, run ", jsxRuntime.jsx("code", { children: "python3" }), " once and accept Apple's developer-tools install."] })] }), jsxRuntime.jsxs("div", { className: "sf-os", children: [jsxRuntime.jsx("b", { children: "Windows" }), " (PowerShell; install Python from python.org first)", jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("code", { className: "sf-codeline", children: WIN_CMD }), jsxRuntime.jsx(CopyButton, { text: WIN_CMD, label: "Copy" })] }), jsxRuntime.jsxs("span", { className: "sf-muted", children: ["Then open a new PowerShell window and run ", jsxRuntime.jsx("code", { children: "securefiles setup" }), "."] })] })] }), jsxRuntime.jsxs("li", { children: ["In the panel that opens (", jsxRuntime.jsx("code", { children: "http://127.0.0.1:47821" }), "), enter this code, or run", " ", jsxRuntime.jsxs("code", { children: ["securefiles pair ", code] }), ":", jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsx("span", { className: "sf-paircode", children: code }), jsxRuntime.jsx(CopyButton, { text: code, label: "Copy" })] }), jsxRuntime.jsxs("span", { className: "sf-muted", children: ["Valid for about ", minutesLeft, " more minutes, once."] })] }), jsxRuntime.jsxs("li", { children: ["Pick files and folders to share in that panel (or ", jsxRuntime.jsx("code", { children: "securefiles share PATH" }), "). They appear here under ", jsxRuntime.jsx("code", { children: "devices/<computer>/" }), ", where you set links, passwords and limits."] })] }), jsxRuntime.jsx("div", { className: "sf-row", children: jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setCode(null), children: "Done" }) })] }));
|
|
506
|
+
}
|
|
507
|
+
function MyComputers({ api, devices, onChange, }) {
|
|
508
|
+
const [error, setError] = react.useState(null);
|
|
509
|
+
return (jsxRuntime.jsxs("details", { className: "sf-admin", children: [jsxRuntime.jsxs("summary", { children: ["My computers", " ", jsxRuntime.jsxs("span", { className: "sf-muted", children: ["(", devices.filter((d) => d.online).length, " online / ", devices.length, ")"] })] }), jsxRuntime.jsxs("div", { className: "sf-stack", children: [jsxRuntime.jsx("p", { className: "sf-muted", children: "Share files and folders straight from your own computers \u2014 nothing is uploaded. Downloads stream from the computer through this site with the same links, passwords, limits and resume, while it's online." }), devices.length > 0 && (jsxRuntime.jsx("ul", { className: "sf-roots", children: devices.map((d) => (jsxRuntime.jsxs("li", { children: [jsxRuntime.jsxs("span", { children: [jsxRuntime.jsx("span", { className: `sf-dot${d.online ? " sf-dot--on" : ""}` }), " ", jsxRuntime.jsx("b", { children: d.name }), " ", jsxRuntime.jsxs("span", { className: "sf-muted", children: [d.online ? "online" : `offline · last seen ${ago(d.last_seen)}`, d.platform ? ` · ${d.platform}` : ""] })] }), jsxRuntime.jsx(ConfirmButton, { label: "Remove", onConfirm: async () => {
|
|
510
|
+
try {
|
|
511
|
+
await api.removeDevice(d.id);
|
|
512
|
+
onChange();
|
|
513
|
+
}
|
|
514
|
+
catch (err) {
|
|
515
|
+
setError(errText(err));
|
|
516
|
+
}
|
|
517
|
+
} })] }, d.id))) })), error && jsxRuntime.jsx("p", { className: "sf-error", children: error }), jsxRuntime.jsx(AddComputer, { api: api, onPaired: onChange })] })] }));
|
|
518
|
+
}
|
|
519
|
+
|
|
456
520
|
function styleInject(css, ref) {
|
|
457
521
|
if ( ref === void 0 ) ref = {};
|
|
458
522
|
var insertAt = ref.insertAt;
|
|
@@ -480,7 +544,7 @@ function styleInject(css, ref) {
|
|
|
480
544
|
}
|
|
481
545
|
}
|
|
482
546
|
|
|
483
|
-
var css_248z = ".sf-root{--sf-border:color-mix(in srgb,currentColor 22%,transparent);--sf-subtle:color-mix(in srgb,currentColor 6%,transparent);--sf-accent:#2563eb;--sf-danger:#dc2626;display:flex;flex-direction:column;font-size:14px;gap:16px;margin:0 auto;max-width:1100px;padding:16px;text-align:left}.sf-root *,.sf-root :after,.sf-root :before{box-sizing:border-box}.sf-head{align-items:baseline;display:flex;gap:12px;justify-content:space-between}.sf-head h2{font-size:1.4em;margin:0}.sf-muted{opacity:.7}.sf-error{color:var(--sf-danger)}.sf-row{align-items:center;display:flex;flex-wrap:wrap;gap:8px}.sf-row--end{justify-content:flex-end}.sf-field{display:flex;flex:1;flex-direction:column;gap:4px;min-width:200px}.sf-input{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;font:inherit;min-width:0;padding:6px 10px;width:100%}.sf-row .sf-input{flex:1;width:auto}.sf-btn{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:5px 12px;white-space:nowrap}.sf-btn:hover:not(:disabled){border-color:var(--sf-accent)}.sf-btn:disabled{cursor:default;opacity:.45}.sf-btn--primary{background:var(--sf-accent);border-color:var(--sf-accent);color:#fff}.sf-btn--armed,.sf-btn--danger:hover:not(:disabled){border-color:var(--sf-danger);color:var(--sf-danger)}.sf-btn--armed{background:color-mix(in srgb,var(--sf-danger) 12%,transparent)}.sf-icon-btn{background:none;border:1px solid transparent;border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:3px 6px}.sf-icon-btn:hover{border-color:var(--sf-border)}.sf-upload{display:flex;flex-direction:column;gap:10px}.sf-drop{align-items:center;border:2px dashed var(--sf-border);border-radius:12px;cursor:pointer;display:flex;gap:20px;padding:28px 24px;transition:border-color .15s,background .15s,box-shadow .15s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.sf-drop:focus-visible,.sf-drop:hover{border-color:var(--sf-accent);box-shadow:0 4px 14px color-mix(in srgb,currentColor 10%,transparent);outline:none}.sf-drop p{margin:0}.sf-drop__icon{flex-shrink:0;height:72px;opacity:.6;width:72px}.sf-drop__title{font-size:1.1em;font-weight:600;margin-bottom:4px!important}.sf-drop__count{color:var(--sf-accent);font-weight:600;margin-top:6px!important}.sf-drop--active{background:color-mix(in srgb,var(--sf-accent) 8%,transparent);border-color:var(--sf-accent)}.sf-drop--active .sf-drop__icon{color:var(--sf-accent);opacity:1}.sf-mode{gap:14px}.sf-queue progress{width:120px}.sf-upload>.sf-row{align-items:flex-end}.sf-queue{display:flex;flex-direction:column;gap:4px;list-style:none;margin:0;max-height:200px;overflow:auto;padding:0}.sf-queue li{display:flex;gap:12px;justify-content:space-between}.sf-queue__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-toolbar{display:flex;flex-wrap:wrap;gap:12px;justify-content:space-between}.sf-search{max-width:320px}.sf-table-wrap{border:1px solid var(--sf-border);border-radius:8px;overflow-x:auto}.sf-table{border-collapse:collapse;width:100%}.sf-table td,.sf-table th{border-bottom:1px solid var(--sf-border);padding:8px 10px;text-align:left;vertical-align:middle}.sf-table tbody tr:last-child td{border-bottom:none}.sf-table th{background:var(--sf-subtle);font-weight:600}.sf-col-check{width:36px}.sf-col-actions{text-align:right!important}.sf-dir td{background:var(--sf-subtle)}.sf-dir__toggle{background:none;border:none;color:inherit;cursor:pointer;font:inherit;font-weight:600;padding:0}.sf-busy{opacity:.55}.sf-pill{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:999px;color:inherit;cursor:pointer;font:inherit;font-size:12px;padding:2px 10px}.sf-pill--on{background:color-mix(in srgb,#16a34a 18%,transparent);border-color:#16a34a}.sf-overlay{align-items:center;background:rgba(0,0,0,.5);display:flex;inset:0;justify-content:center;padding:16px;position:fixed;z-index:1000}.sf-dialog{--sf-border:color-mix(in srgb,CanvasText 22%,transparent);--sf-subtle:color-mix(in srgb,CanvasText 6%,transparent);background:Canvas;border-radius:10px;color:CanvasText;display:flex;flex-direction:column;gap:12px;max-height:90vh;overflow:auto;padding:18px 20px;text-align:left;width:min(520px,100%)}.sf-dialog__head{align-items:center;display:flex;justify-content:space-between}.sf-dialog__head h3{margin:0}.sf-dialog>.sf-muted{margin:0;word-break:break-all}.sf-dialog__foot{justify-content:flex-end}.sf-fieldset{border:none;display:flex;flex-direction:column;gap:12px;margin:0;padding:0}.sf-fieldset:disabled{opacity:.5}.sf-fieldset p{margin:0}.sf-check{align-items:center;cursor:pointer;display:flex;gap:8px}.sf-check--inline{display:inline-flex}.sf-stack{display:flex;flex-direction:column;gap:12px}.sf-stack p{margin:0}.sf-row--bottom{align-items:flex-end}.sf-field--narrow{flex:0 1 200px;min-width:140px}.sf-admin{border:1px solid var(--sf-border);border-radius:10px;padding:10px 14px}.sf-admin>summary{cursor:pointer;font-weight:600}.sf-admin[open]>summary{margin-bottom:12px}.sf-card{border:1px solid var(--sf-border);border-radius:8px;display:flex;flex-direction:column;gap:10px;padding:12px}.sf-card h4,.sf-card p{margin:0}.sf-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;resize:vertical}.sf-roots{display:flex;flex-direction:column;gap:6px;list-style:none;margin:0;padding:0}.sf-roots li{align-items:center;display:flex;gap:12px;justify-content:space-between}.sf-root-select{max-width:420px}.sf-browser{border:1px solid var(--sf-border);border-radius:6px;display:flex;flex-direction:column;max-height:320px;overflow:auto}.sf-browser>*{border-bottom:1px solid var(--sf-border);padding:5px 10px}.sf-browser>:last-child{border-bottom:none}.sf-browser__dir{background:none;border:none;border-bottom:1px solid var(--sf-border);color:inherit;cursor:pointer;font:inherit;text-align:left}.sf-browser__all,.sf-browser__dir:hover{background:var(--sf-subtle)}.sf-browser__all{font-weight:600}.sf-browser__name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-tag{border:1px solid var(--sf-border);border-radius:999px;display:inline-block;font-size:11px;margin-left:8px;padding:0 7px;vertical-align:middle}.sf-tag--warn{border-color:var(--sf-danger);color:var(--sf-danger)}.sf-strike{opacity:.7;text-decoration:line-through}.sf-browser__row{align-items:center;display:flex;gap:8px}.sf-browser__row .sf-browser__dir{border-bottom:none;flex:1;padding:0}";
|
|
547
|
+
var css_248z = ".sf-root{--sf-border:color-mix(in srgb,currentColor 22%,transparent);--sf-subtle:color-mix(in srgb,currentColor 6%,transparent);--sf-accent:#2563eb;--sf-danger:#dc2626;display:flex;flex-direction:column;font-size:14px;gap:16px;margin:0 auto;max-width:1100px;padding:16px;text-align:left}.sf-root *,.sf-root :after,.sf-root :before{box-sizing:border-box}.sf-head{align-items:baseline;display:flex;gap:12px;justify-content:space-between}.sf-head h2{font-size:1.4em;margin:0}.sf-muted{opacity:.7}.sf-error{color:var(--sf-danger)}.sf-row{align-items:center;display:flex;flex-wrap:wrap;gap:8px}.sf-row--end{justify-content:flex-end}.sf-field{display:flex;flex:1;flex-direction:column;gap:4px;min-width:200px}.sf-input{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;font:inherit;min-width:0;padding:6px 10px;width:100%}.sf-row .sf-input{flex:1;width:auto}.sf-btn{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:5px 12px;white-space:nowrap}.sf-btn:hover:not(:disabled){border-color:var(--sf-accent)}.sf-btn:disabled{cursor:default;opacity:.45}.sf-btn--primary{background:var(--sf-accent);border-color:var(--sf-accent);color:#fff}.sf-btn--armed,.sf-btn--danger:hover:not(:disabled){border-color:var(--sf-danger);color:var(--sf-danger)}.sf-btn--armed{background:color-mix(in srgb,var(--sf-danger) 12%,transparent)}.sf-icon-btn{background:none;border:1px solid transparent;border-radius:6px;color:inherit;cursor:pointer;font:inherit;padding:3px 6px}.sf-icon-btn:hover{border-color:var(--sf-border)}.sf-upload{display:flex;flex-direction:column;gap:10px}.sf-drop{align-items:center;border:2px dashed var(--sf-border);border-radius:12px;cursor:pointer;display:flex;gap:20px;padding:28px 24px;transition:border-color .15s,background .15s,box-shadow .15s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.sf-drop:focus-visible,.sf-drop:hover{border-color:var(--sf-accent);box-shadow:0 4px 14px color-mix(in srgb,currentColor 10%,transparent);outline:none}.sf-drop p{margin:0}.sf-drop__icon{flex-shrink:0;height:72px;opacity:.6;width:72px}.sf-drop__title{font-size:1.1em;font-weight:600;margin-bottom:4px!important}.sf-drop__count{color:var(--sf-accent);font-weight:600;margin-top:6px!important}.sf-drop--active{background:color-mix(in srgb,var(--sf-accent) 8%,transparent);border-color:var(--sf-accent)}.sf-drop--active .sf-drop__icon{color:var(--sf-accent);opacity:1}.sf-mode{gap:14px}.sf-queue progress{width:120px}.sf-upload>.sf-row{align-items:flex-end}.sf-queue{display:flex;flex-direction:column;gap:4px;list-style:none;margin:0;max-height:200px;overflow:auto;padding:0}.sf-queue li{display:flex;gap:12px;justify-content:space-between}.sf-queue__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-toolbar{display:flex;flex-wrap:wrap;gap:12px;justify-content:space-between}.sf-search{max-width:320px}.sf-table-wrap{border:1px solid var(--sf-border);border-radius:8px;overflow-x:auto}.sf-table{border-collapse:collapse;width:100%}.sf-table td,.sf-table th{border-bottom:1px solid var(--sf-border);padding:8px 10px;text-align:left;vertical-align:middle}.sf-table tbody tr:last-child td{border-bottom:none}.sf-table th{background:var(--sf-subtle);font-weight:600}.sf-col-check{width:36px}.sf-col-actions{text-align:right!important}.sf-dir td{background:var(--sf-subtle)}.sf-dir__toggle{background:none;border:none;color:inherit;cursor:pointer;font:inherit;font-weight:600;padding:0}.sf-busy{opacity:.55}.sf-pill{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:999px;color:inherit;cursor:pointer;font:inherit;font-size:12px;padding:2px 10px}.sf-pill--on{background:color-mix(in srgb,#16a34a 18%,transparent);border-color:#16a34a}.sf-overlay{align-items:center;background:rgba(0,0,0,.5);display:flex;inset:0;justify-content:center;padding:16px;position:fixed;z-index:1000}.sf-dialog{--sf-border:color-mix(in srgb,CanvasText 22%,transparent);--sf-subtle:color-mix(in srgb,CanvasText 6%,transparent);background:Canvas;border-radius:10px;color:CanvasText;display:flex;flex-direction:column;gap:12px;max-height:90vh;overflow:auto;padding:18px 20px;text-align:left;width:min(520px,100%)}.sf-dialog__head{align-items:center;display:flex;justify-content:space-between}.sf-dialog__head h3{margin:0}.sf-dialog>.sf-muted{margin:0;word-break:break-all}.sf-dialog__foot{justify-content:flex-end}.sf-fieldset{border:none;display:flex;flex-direction:column;gap:12px;margin:0;padding:0}.sf-fieldset:disabled{opacity:.5}.sf-fieldset p{margin:0}.sf-check{align-items:center;cursor:pointer;display:flex;gap:8px}.sf-check--inline{display:inline-flex}.sf-stack{display:flex;flex-direction:column;gap:12px}.sf-stack p{margin:0}.sf-row--bottom{align-items:flex-end}.sf-field--narrow{flex:0 1 200px;min-width:140px}.sf-admin{border:1px solid var(--sf-border);border-radius:10px;padding:10px 14px}.sf-admin>summary{cursor:pointer;font-weight:600}.sf-admin[open]>summary{margin-bottom:12px}.sf-card{border:1px solid var(--sf-border);border-radius:8px;display:flex;flex-direction:column;gap:10px;padding:12px}.sf-card h4,.sf-card p{margin:0}.sf-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;resize:vertical}.sf-roots{display:flex;flex-direction:column;gap:6px;list-style:none;margin:0;padding:0}.sf-roots li{align-items:center;display:flex;gap:12px;justify-content:space-between}.sf-root-select{max-width:420px}.sf-browser{border:1px solid var(--sf-border);border-radius:6px;display:flex;flex-direction:column;max-height:320px;overflow:auto}.sf-browser>*{border-bottom:1px solid var(--sf-border);padding:5px 10px}.sf-browser>:last-child{border-bottom:none}.sf-browser__dir{background:none;border:none;border-bottom:1px solid var(--sf-border);color:inherit;cursor:pointer;font:inherit;text-align:left}.sf-browser__all,.sf-browser__dir:hover{background:var(--sf-subtle)}.sf-browser__all{font-weight:600}.sf-browser__name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-tag{border:1px solid var(--sf-border);border-radius:999px;display:inline-block;font-size:11px;margin-left:8px;padding:0 7px;vertical-align:middle}.sf-tag--warn{border-color:var(--sf-danger);color:var(--sf-danger)}.sf-strike{opacity:.7;text-decoration:line-through}.sf-browser__row{align-items:center;display:flex;gap:8px}.sf-browser__row .sf-browser__dir{border-bottom:none;flex:1;padding:0}.sf-dot{background:var(--sf-danger);border-radius:50%;display:inline-block;height:9px;vertical-align:middle;width:9px}.sf-dot--on{background:#16a34a}.sf-steps{display:flex;flex-direction:column;gap:10px;margin:0;padding-left:20px}.sf-steps li>.sf-row{margin:4px 0}.sf-codeline{background:var(--sf-subtle);border:1px solid var(--sf-border);border-radius:6px;font-size:12px;padding:4px 8px}.sf-codeline,.sf-paircode{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.sf-paircode{font-size:22px;font-weight:700;letter-spacing:.12em}.sf-small{font-size:12px}.sf-os{display:flex;flex-direction:column;gap:4px;margin-top:6px}";
|
|
484
548
|
styleInject(css_248z);
|
|
485
549
|
|
|
486
550
|
function buildTree(files) {
|
|
@@ -506,6 +570,7 @@ function SecureFiles({ apiBase }) {
|
|
|
506
570
|
const [files, setFiles] = react.useState([]);
|
|
507
571
|
const [user, setUser] = react.useState(null);
|
|
508
572
|
const [isAdmin, setIsAdmin] = react.useState(false);
|
|
573
|
+
const [devices, setDevices] = react.useState([]);
|
|
509
574
|
const [loading, setLoading] = react.useState(true);
|
|
510
575
|
const [error, setError] = react.useState(null);
|
|
511
576
|
const [notice, setNotice] = react.useState(null);
|
|
@@ -520,6 +585,7 @@ function SecureFiles({ apiBase }) {
|
|
|
520
585
|
const data = await api.list();
|
|
521
586
|
setUser(data.user_name);
|
|
522
587
|
setIsAdmin(data.is_admin);
|
|
588
|
+
setDevices(data.devices || []);
|
|
523
589
|
setFiles(data.files);
|
|
524
590
|
setSelected((prev) => new Set([...prev].filter((id) => data.files.some((f) => f.id === id))));
|
|
525
591
|
}
|
|
@@ -602,7 +668,7 @@ function SecureFiles({ apiBase }) {
|
|
|
602
668
|
const searching = search.trim().length > 0;
|
|
603
669
|
const renderFile = (f, depth) => {
|
|
604
670
|
const busy = busyIds.has(f.id);
|
|
605
|
-
return (jsxRuntime.jsxs("tr", { className: busy ? "sf-busy" : undefined, children: [jsxRuntime.jsx("td", { className: "sf-col-check", children: jsxRuntime.jsx("input", { type: "checkbox", "aria-label": `Select ${f.filename}`, checked: selected.has(f.id), onChange: (e) => toggle([f.id], e.target.checked) }) }), jsxRuntime.jsxs("td", { style: { paddingLeft: 10 + depth * 20 }, children: [jsxRuntime.jsx("span", { className: f.available ? undefined : "sf-strike", children: f.filename }), f.kind === "folder" && (jsxRuntime.jsx("span", { className: "sf-tag", title: "Downloads as one uncompressed, resumable .tar of the whole folder", children: "folder \u00B7 .tar" })), f.linked && (jsxRuntime.jsx("span", { className: "sf-tag", title: f.source_path ?? undefined, children: "linked" })), !f.available && (jsxRuntime.jsx("span", { className: "sf-tag sf-tag--warn", title: f.linked ? "Source file is gone or its directory is no longer allowed" : "File is missing on disk", children: "missing" }))] }), jsxRuntime.jsxs("td", { children: [jsxRuntime.jsx("button", { type: "button", className: `sf-pill${f.shareable ? " sf-pill--on" : ""}`, disabled: busy || !f.available, onClick: () => setShared([f], !f.shareable), title: f.shareable ? "Click to stop sharing" : "Click to share", children: f.shareable ? "Shared" : "Private" }), f.shareable && f.has_password && jsxRuntime.jsx("span", { title: "Password protected", children: " \uD83D\uDD12" })] }), jsxRuntime.jsxs("td", { children: [f.download_count, " / ", f.download_limit ?? "∞"] }), jsxRuntime.jsx("td", { className: "sf-col-actions", children: jsxRuntime.jsxs("div", { className: "sf-row sf-row--end", children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: busy || !f.available, onClick: () => download([f]), children: "Download" }), f.shareable && f.available && f.share_url && jsxRuntime.jsx(CopyButton, { text: f.share_url }), jsxRuntime.jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": `Share settings for ${f.filename}`, title: "Share settings", onClick: () => setEditing(f), children: "\u2699\uFE0F" }), jsxRuntime.jsx(ConfirmButton, { label: f.linked ? "Unlink" : "Delete", disabled: busy, onConfirm: () => remove([f]) })] }) })] }, f.id));
|
|
671
|
+
return (jsxRuntime.jsxs("tr", { className: busy ? "sf-busy" : undefined, children: [jsxRuntime.jsx("td", { className: "sf-col-check", children: jsxRuntime.jsx("input", { type: "checkbox", "aria-label": `Select ${f.filename}`, checked: selected.has(f.id), onChange: (e) => toggle([f.id], e.target.checked) }) }), jsxRuntime.jsxs("td", { style: { paddingLeft: 10 + depth * 20 }, children: [jsxRuntime.jsx("span", { className: f.available || f.device ? undefined : "sf-strike", children: f.filename }), f.kind === "folder" && (jsxRuntime.jsx("span", { className: "sf-tag", title: "Downloads as one uncompressed, resumable .tar of the whole folder", children: "folder \u00B7 .tar" })), f.linked && (jsxRuntime.jsx("span", { className: "sf-tag", title: f.source_path ?? undefined, children: "linked" })), f.device && (jsxRuntime.jsxs("span", { className: `sf-tag${f.device.online ? "" : " sf-tag--warn"}`, title: "Served from your computer through the relay", children: ["on ", f.device.name, " \u00B7 ", f.device.online ? "online" : "offline"] })), !f.available && !f.device && (jsxRuntime.jsx("span", { className: "sf-tag sf-tag--warn", title: f.linked ? "Source file is gone or its directory is no longer allowed" : "File is missing on disk", children: "missing" }))] }), jsxRuntime.jsxs("td", { children: [jsxRuntime.jsx("button", { type: "button", className: `sf-pill${f.shareable ? " sf-pill--on" : ""}`, disabled: busy || !f.available, onClick: () => setShared([f], !f.shareable), title: f.shareable ? "Click to stop sharing" : "Click to share", children: f.shareable ? "Shared" : "Private" }), f.shareable && f.has_password && jsxRuntime.jsx("span", { title: "Password protected", children: " \uD83D\uDD12" })] }), jsxRuntime.jsxs("td", { children: [f.download_count, " / ", f.download_limit ?? "∞"] }), jsxRuntime.jsx("td", { className: "sf-col-actions", children: jsxRuntime.jsxs("div", { className: "sf-row sf-row--end", children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: busy || !f.available, onClick: () => download([f]), children: "Download" }), f.shareable && f.available && f.share_url && jsxRuntime.jsx(CopyButton, { text: f.share_url }), jsxRuntime.jsx("button", { type: "button", className: "sf-icon-btn", "aria-label": `Share settings for ${f.filename}`, title: "Share settings", onClick: () => setEditing(f), children: "\u2699\uFE0F" }), f.device ? (jsxRuntime.jsxs("span", { className: "sf-muted sf-small", title: "Stop sharing it from that computer's Secure Files panel", children: ["managed on ", f.device.name] })) : (jsxRuntime.jsx(ConfirmButton, { label: f.linked ? "Unlink" : "Delete", disabled: busy, onConfirm: () => remove([f]) }))] }) })] }, f.id));
|
|
606
672
|
};
|
|
607
673
|
const renderFolder = (node, depth) => {
|
|
608
674
|
const rows = [];
|
|
@@ -619,7 +685,7 @@ function SecureFiles({ apiBase }) {
|
|
|
619
685
|
rows.push(...ownFiles.map((f) => renderFile(f, depth)));
|
|
620
686
|
return rows;
|
|
621
687
|
};
|
|
622
|
-
return (jsxRuntime.jsxs("div", { className: "sf-root", children: [jsxRuntime.jsxs("header", { className: "sf-head", children: [jsxRuntime.jsx("h2", { children: "Secure Files" }), user && jsxRuntime.jsxs("span", { className: "sf-muted", children: ["Signed in as ", user] })] }), jsxRuntime.jsx(UploadPanel, { api: api, folders: folders, onUploaded: load }), isAdmin && jsxRuntime.jsx(ServerFiles, { api: api, folders: folders, onLinked: load }), jsxRuntime.jsxs("section", { className: "sf-toolbar", children: [jsxRuntime.jsx("input", { className: "sf-input sf-search", type: "search", placeholder: "Search files\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsxs("span", { className: "sf-muted", children: [selected.size, " selected"] }), folders.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set()), children: "Expand all" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set(folders)), children: "Collapse all" })] })), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => download(selectedFiles), children: "Download" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, true), children: "Share" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, false), children: "Unshare" }), jsxRuntime.jsx(ConfirmButton, { label: "Delete", disabled: !selected.size, onConfirm: () => remove(selectedFiles) })] })] }), notice && (jsxRuntime.jsx("p", { className: "sf-error", role: "alert", children: notice })), loading ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "Loading\u2026" })) : error ? (jsxRuntime.jsx("p", { className: "sf-error", role: "alert", children: error })) : files.length === 0 ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "No files yet \u2014 upload something above." })) : (jsxRuntime.jsx("div", { className: "sf-table-wrap", children: jsxRuntime.jsxs("table", { className: "sf-table", children: [jsxRuntime.jsx("thead", { children: jsxRuntime.jsxs("tr", { children: [jsxRuntime.jsx("th", { className: "sf-col-check", children: jsxRuntime.jsx("input", { type: "checkbox", "aria-label": "Select all", checked: allVisibleSelected, onChange: (e) => toggle(filtered.map((f) => f.id), e.target.checked) }) }), jsxRuntime.jsx("th", { children: "Name" }), jsxRuntime.jsx("th", { children: "Sharing" }), jsxRuntime.jsx("th", { children: "Downloads" }), jsxRuntime.jsx("th", { className: "sf-col-actions", children: "Actions" })] }) }), jsxRuntime.jsx("tbody", { children: renderFolder(tree, 0) })] }) })), editing && (jsxRuntime.jsx(ShareDialog, { file: files.find((f) => f.id === editing.id) ?? editing, onClose: () => setEditing(null), onSave: saveShare }, editing.id))] }));
|
|
688
|
+
return (jsxRuntime.jsxs("div", { className: "sf-root", children: [jsxRuntime.jsxs("header", { className: "sf-head", children: [jsxRuntime.jsx("h2", { children: "Secure Files" }), user && jsxRuntime.jsxs("span", { className: "sf-muted", children: ["Signed in as ", user] })] }), jsxRuntime.jsx(UploadPanel, { api: api, folders: folders.filter((d) => d !== "devices" && !d.startsWith("devices/")), onUploaded: load }), jsxRuntime.jsx(MyComputers, { api: api, devices: devices, onChange: load }), isAdmin && jsxRuntime.jsx(ServerFiles, { api: api, folders: folders, onLinked: load }), jsxRuntime.jsxs("section", { className: "sf-toolbar", children: [jsxRuntime.jsx("input", { className: "sf-input sf-search", type: "search", placeholder: "Search files\u2026", value: search, onChange: (e) => setSearch(e.target.value) }), jsxRuntime.jsxs("div", { className: "sf-row", children: [jsxRuntime.jsxs("span", { className: "sf-muted", children: [selected.size, " selected"] }), folders.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set()), children: "Expand all" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", onClick: () => setCollapsed(new Set(folders)), children: "Collapse all" })] })), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => download(selectedFiles), children: "Download" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, true), children: "Share" }), jsxRuntime.jsx("button", { type: "button", className: "sf-btn", disabled: !selected.size, onClick: () => setShared(selectedFiles, false), children: "Unshare" }), jsxRuntime.jsx(ConfirmButton, { label: "Delete", disabled: !selected.size, onConfirm: () => remove(selectedFiles) })] })] }), notice && (jsxRuntime.jsx("p", { className: "sf-error", role: "alert", children: notice })), loading ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "Loading\u2026" })) : error ? (jsxRuntime.jsx("p", { className: "sf-error", role: "alert", children: error })) : files.length === 0 ? (jsxRuntime.jsx("p", { className: "sf-muted", children: "No files yet \u2014 upload something above." })) : (jsxRuntime.jsx("div", { className: "sf-table-wrap", children: jsxRuntime.jsxs("table", { className: "sf-table", children: [jsxRuntime.jsx("thead", { children: jsxRuntime.jsxs("tr", { children: [jsxRuntime.jsx("th", { className: "sf-col-check", children: jsxRuntime.jsx("input", { type: "checkbox", "aria-label": "Select all", checked: allVisibleSelected, onChange: (e) => toggle(filtered.map((f) => f.id), e.target.checked) }) }), jsxRuntime.jsx("th", { children: "Name" }), jsxRuntime.jsx("th", { children: "Sharing" }), jsxRuntime.jsx("th", { children: "Downloads" }), jsxRuntime.jsx("th", { className: "sf-col-actions", children: "Actions" })] }) }), jsxRuntime.jsx("tbody", { children: renderFolder(tree, 0) })] }) })), editing && (jsxRuntime.jsx(ShareDialog, { file: files.find((f) => f.id === editing.id) ?? editing, onClose: () => setEditing(null), onSave: saveShare }, editing.id))] }));
|
|
623
689
|
}
|
|
624
690
|
|
|
625
691
|
exports.ApiError = ApiError;
|