@decentnetwork/beagle 0.1.37 → 0.1.39
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/desktop/app.js +60 -25
- package/dist/server.js +70 -16
- package/package.json +1 -1
package/dist/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.38";
|
|
2
2
|
const ICON_PATHS = {
|
|
3
3
|
// ---- tab bar (the four must feel like one set) ----
|
|
4
4
|
users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
|
|
@@ -2056,33 +2056,68 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onC
|
|
|
2056
2056
|
const meta = /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: mine ? "#fff" : "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: mine ? "rgba(255,255,255,0.7)" : "var(--faint)", marginTop: 1 } }, busy === "cancel" ? `${m.file.size} \xB7 ${T.cancelling || "Cancelling\u2026"}` : busy === "retry" ? `${m.file.size} \xB7 ${T.retrying || "Retrying\u2026"}` : m.file.status === "queued" ? `${m.file.size} \xB7 ${T.queued || "queued"}` : m.file.status === "sending" ? `${m.file.size} \xB7 ${dkPct(m.file.pct)}${m.file.kbps ? " \xB7 " + dkSpeed(m.file.kbps) : ""}` : m.file.status === "failed" ? `${m.file.size} \xB7 failed` : m.file.status === "cancelled" ? `${m.file.size} \xB7 ${T.cancelled || "cancelled"}` : mine ? `${m.file.size} \xB7 sent` : `${m.file.size} \xB7 ${T.open || "open"}`));
|
|
2057
2057
|
if (mine) {
|
|
2058
2058
|
const btnBusy = !!busy;
|
|
2059
|
-
return /* @__PURE__ */ React.createElement(
|
|
2060
|
-
"
|
|
2059
|
+
return /* @__PURE__ */ React.createElement(
|
|
2060
|
+
"div",
|
|
2061
2061
|
{
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
onClick: (
|
|
2065
|
-
e.stopPropagation();
|
|
2066
|
-
if (!btnBusy)
|
|
2067
|
-
onCancel(m.id);
|
|
2068
|
-
},
|
|
2069
|
-
style: { background: "rgba(255,255,255,0.16)", border: "none", borderRadius: 6, cursor: btnBusy ? "wait" : "pointer", padding: 3, marginLeft: 2, display: "inline-flex", flexShrink: 0, opacity: btnBusy ? 0.5 : 1 }
|
|
2062
|
+
style: { ...cardStyle, cursor: "pointer", opacity: btnBusy ? 0.85 : 1 },
|
|
2063
|
+
title: T.open || "open",
|
|
2064
|
+
onClick: () => window.open(dkFileUrl(m.file.name), "_blank", "noopener")
|
|
2070
2065
|
},
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2066
|
+
icon,
|
|
2067
|
+
meta,
|
|
2068
|
+
onReveal && /* @__PURE__ */ React.createElement(
|
|
2069
|
+
"button",
|
|
2070
|
+
{
|
|
2071
|
+
title: T.revealInFinder || "\u5728\u6587\u4EF6\u7BA1\u7406\u5668\u4E2D\u663E\u793A / Show in file manager",
|
|
2072
|
+
onClick: (e) => {
|
|
2073
|
+
e.stopPropagation();
|
|
2074
|
+
onReveal(m.file.name);
|
|
2075
|
+
},
|
|
2076
|
+
style: { background: "none", border: "none", cursor: "pointer", padding: 0, display: "inline-flex", flexShrink: 0 }
|
|
2081
2077
|
},
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
/* @__PURE__ */ React.createElement(
|
|
2085
|
-
|
|
2078
|
+
/* @__PURE__ */ React.createElement(Icon, { name: "folderOpen", size: 15, stroke: 2, color: "rgba(255,255,255,0.85)" })
|
|
2079
|
+
),
|
|
2080
|
+
/* @__PURE__ */ React.createElement(
|
|
2081
|
+
"a",
|
|
2082
|
+
{
|
|
2083
|
+
href: dkFileDownloadUrl(m.file.name),
|
|
2084
|
+
download: m.file.name,
|
|
2085
|
+
title: "download",
|
|
2086
|
+
onClick: (e) => e.stopPropagation(),
|
|
2087
|
+
style: { display: "inline-flex", flexShrink: 0 }
|
|
2088
|
+
},
|
|
2089
|
+
/* @__PURE__ */ React.createElement(Icon, { name: "download", size: 15, stroke: 2, color: "rgba(255,255,255,0.85)" })
|
|
2090
|
+
),
|
|
2091
|
+
busy === "cancel" || busy === "retry" ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : m.file.status === "queued" ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : m.file.status === "sending" ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, color: "rgba(255,255,255,0.9)", whiteSpace: "nowrap" } }, dkPct(m.file.pct), m.file.kbps ? " \xB7 " + dkSpeed(m.file.kbps) : "") : m.file.status === "failed" || m.file.status === "cancelled" ? /* @__PURE__ */ React.createElement(Icon, { name: "x", size: 16, stroke: 2.4, color: "rgba(255,200,190,0.95)" }) : /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }),
|
|
2092
|
+
(m.file.status === "sending" || m.file.status === "queued" || busy === "cancel") && m.id && /* @__PURE__ */ React.createElement(
|
|
2093
|
+
"button",
|
|
2094
|
+
{
|
|
2095
|
+
title: T.cancel || "cancel",
|
|
2096
|
+
disabled: btnBusy,
|
|
2097
|
+
onClick: (e) => {
|
|
2098
|
+
e.stopPropagation();
|
|
2099
|
+
if (!btnBusy)
|
|
2100
|
+
onCancel(m.id);
|
|
2101
|
+
},
|
|
2102
|
+
style: { background: "rgba(255,255,255,0.16)", border: "none", borderRadius: 6, cursor: btnBusy ? "wait" : "pointer", padding: 3, marginLeft: 2, display: "inline-flex", flexShrink: 0, opacity: btnBusy ? 0.5 : 1 }
|
|
2103
|
+
},
|
|
2104
|
+
/* @__PURE__ */ React.createElement(Icon, { name: "x", size: 13, stroke: 2.4, color: "#fff" })
|
|
2105
|
+
),
|
|
2106
|
+
m.file.status !== "sent" && m.id && /* @__PURE__ */ React.createElement(
|
|
2107
|
+
"button",
|
|
2108
|
+
{
|
|
2109
|
+
title: T.retry || "retry",
|
|
2110
|
+
disabled: btnBusy,
|
|
2111
|
+
onClick: (e) => {
|
|
2112
|
+
e.stopPropagation();
|
|
2113
|
+
if (!btnBusy)
|
|
2114
|
+
onRetry(m.id);
|
|
2115
|
+
},
|
|
2116
|
+
style: { background: "rgba(255,255,255,0.16)", border: "none", borderRadius: 6, cursor: btnBusy ? "wait" : "pointer", padding: 3, marginLeft: 2, display: "inline-flex", flexShrink: 0, opacity: btnBusy ? 0.5 : 1 }
|
|
2117
|
+
},
|
|
2118
|
+
/* @__PURE__ */ React.createElement(Icon, { name: "refresh", size: 13, stroke: 2.2, color: "#fff" })
|
|
2119
|
+
)
|
|
2120
|
+
);
|
|
2086
2121
|
}
|
|
2087
2122
|
return /* @__PURE__ */ React.createElement(
|
|
2088
2123
|
"div",
|
package/dist/server.js
CHANGED
|
@@ -305,6 +305,39 @@ async function agentnetRestart() {
|
|
|
305
305
|
}
|
|
306
306
|
return { ok: false, error: lastErr };
|
|
307
307
|
}
|
|
308
|
+
// ── saved-files locations ───────────────────────────────────────────────────
|
|
309
|
+
// The daemon (often running as root) owns downloads/ — on such machines this
|
|
310
|
+
// user-level process gets EACCES writing there. Files WE save (sent copies,
|
|
311
|
+
// browser-WebRTC receives) then go to a sibling `sent/` dir we own; readers
|
|
312
|
+
// (open / download / reveal) look in downloads first, then sent.
|
|
313
|
+
function sentDirFor(downloadsDir) {
|
|
314
|
+
return join(dirname(downloadsDir), "sent");
|
|
315
|
+
}
|
|
316
|
+
async function pickWritableSaveDir(downloadsDir) {
|
|
317
|
+
const fsp = await import("node:fs/promises");
|
|
318
|
+
try {
|
|
319
|
+
await fsp.mkdir(downloadsDir, { recursive: true });
|
|
320
|
+
await fsp.access(downloadsDir, (await import("node:fs")).constants.W_OK);
|
|
321
|
+
return downloadsDir;
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
const alt = sentDirFor(downloadsDir);
|
|
325
|
+
await fsp.mkdir(alt, { recursive: true });
|
|
326
|
+
return alt;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/** Resolve a saved file by name across downloads/ and sent/, with a
|
|
330
|
+
* path-traversal guard. Returns null when absent from both. */
|
|
331
|
+
function findSavedFile(downloadsDir, name) {
|
|
332
|
+
if (!name)
|
|
333
|
+
return null;
|
|
334
|
+
for (const dir of [downloadsDir, sentDirFor(downloadsDir)]) {
|
|
335
|
+
const f = join(dir, name);
|
|
336
|
+
if (f.startsWith(dir) && existsSync(f))
|
|
337
|
+
return f;
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
308
341
|
/** True only when the request came from the local machine. Used to gate the
|
|
309
342
|
* "Sign in with Decent" routes so binding the UI to a LAN IP can't expose
|
|
310
343
|
* identity signing to other hosts. The popup always runs in the local user's
|
|
@@ -956,8 +989,10 @@ export function startBeagleServer(opts) {
|
|
|
956
989
|
}
|
|
957
990
|
// ── File transfer ──────────────────────────────────────────────────
|
|
958
991
|
// Upload: the browser POSTs raw file bytes with ?userid=&name= in the
|
|
959
|
-
// query.
|
|
960
|
-
//
|
|
992
|
+
// query. The upload is saved INTO downloads/ (collision-renamed) and
|
|
993
|
+
// kept — the sender's own chip can then open / reveal / download the
|
|
994
|
+
// file just like the receiver's. (It used to stream to a temp file
|
|
995
|
+
// deleted right after send, which made every sent chip a dead end.)
|
|
961
996
|
if (req.method === "POST" && url === "/api/file-send") {
|
|
962
997
|
const q = new URL(req.url || "", "http://x").searchParams;
|
|
963
998
|
const userid = q.get("userid") || "";
|
|
@@ -970,17 +1005,36 @@ export function startBeagleServer(opts) {
|
|
|
970
1005
|
const os = await import("node:os");
|
|
971
1006
|
const fs = await import("node:fs");
|
|
972
1007
|
const fsp = await import("node:fs/promises");
|
|
973
|
-
|
|
974
|
-
|
|
1008
|
+
// Keep the copy under downloads when available (normal case); fall
|
|
1009
|
+
// back to the old ephemeral temp only when downloads are disabled.
|
|
1010
|
+
const keep = !!opts.downloadsDir;
|
|
1011
|
+
let destDir;
|
|
1012
|
+
if (keep) {
|
|
1013
|
+
destDir = await pickWritableSaveDir(opts.downloadsDir);
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
destDir = await fsp.mkdtemp(join(os.tmpdir(), "agentnet-up-"));
|
|
1017
|
+
}
|
|
1018
|
+
let destName = safe;
|
|
1019
|
+
let destPath = join(destDir, destName);
|
|
1020
|
+
if (keep) {
|
|
1021
|
+
const dot = safe.lastIndexOf(".");
|
|
1022
|
+
const base = dot > 0 ? safe.slice(0, dot) : safe;
|
|
1023
|
+
const ext = dot > 0 ? safe.slice(dot) : "";
|
|
1024
|
+
for (let i = 1; existsSync(destPath); i++) {
|
|
1025
|
+
destName = `${base}-${i}${ext}`;
|
|
1026
|
+
destPath = join(destDir, destName);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
975
1029
|
try {
|
|
976
1030
|
await new Promise((resolve2, reject) => {
|
|
977
|
-
const ws = fs.createWriteStream(
|
|
1031
|
+
const ws = fs.createWriteStream(destPath);
|
|
978
1032
|
req.pipe(ws);
|
|
979
1033
|
req.on("error", reject);
|
|
980
1034
|
ws.on("error", reject);
|
|
981
1035
|
ws.on("finish", () => resolve2());
|
|
982
1036
|
});
|
|
983
|
-
const r = await opts.call({ op: "file-send", userid, path:
|
|
1037
|
+
const r = await opts.call({ op: "file-send", userid, path: destPath });
|
|
984
1038
|
if (!r.ok) {
|
|
985
1039
|
sendJson(res, 502, r);
|
|
986
1040
|
return;
|
|
@@ -991,7 +1045,8 @@ export function startBeagleServer(opts) {
|
|
|
991
1045
|
sendJson(res, 500, { ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
992
1046
|
}
|
|
993
1047
|
finally {
|
|
994
|
-
|
|
1048
|
+
if (!keep)
|
|
1049
|
+
await fsp.rm(destDir, { recursive: true, force: true }).catch(() => undefined);
|
|
995
1050
|
}
|
|
996
1051
|
return;
|
|
997
1052
|
}
|
|
@@ -1013,16 +1068,15 @@ export function startBeagleServer(opts) {
|
|
|
1013
1068
|
return;
|
|
1014
1069
|
}
|
|
1015
1070
|
const fs = await import("node:fs");
|
|
1016
|
-
const
|
|
1017
|
-
await fsp.mkdir(opts.downloadsDir, { recursive: true });
|
|
1071
|
+
const saveDir = await pickWritableSaveDir(opts.downloadsDir);
|
|
1018
1072
|
let finalName = safe;
|
|
1019
|
-
let finalPath = join(
|
|
1073
|
+
let finalPath = join(saveDir, finalName);
|
|
1020
1074
|
const dot = safe.lastIndexOf(".");
|
|
1021
1075
|
const base = dot > 0 ? safe.slice(0, dot) : safe;
|
|
1022
1076
|
const ext = dot > 0 ? safe.slice(dot) : "";
|
|
1023
1077
|
for (let i = 1; existsSync(finalPath); i++) {
|
|
1024
1078
|
finalName = `${base}-${i}${ext}`;
|
|
1025
|
-
finalPath = join(
|
|
1079
|
+
finalPath = join(saveDir, finalName);
|
|
1026
1080
|
}
|
|
1027
1081
|
try {
|
|
1028
1082
|
await new Promise((resolve2, reject) => {
|
|
@@ -1065,8 +1119,8 @@ export function startBeagleServer(opts) {
|
|
|
1065
1119
|
}
|
|
1066
1120
|
const body = await readBody(req);
|
|
1067
1121
|
const name = (typeof body.name === "string" ? body.name : "").replace(/[/\\]/g, "");
|
|
1068
|
-
const file =
|
|
1069
|
-
if (!
|
|
1122
|
+
const file = findSavedFile(opts.downloadsDir, name);
|
|
1123
|
+
if (!file) {
|
|
1070
1124
|
sendJson(res, 404, { ok: false, error: "not found" });
|
|
1071
1125
|
return;
|
|
1072
1126
|
}
|
|
@@ -1082,9 +1136,9 @@ export function startBeagleServer(opts) {
|
|
|
1082
1136
|
}
|
|
1083
1137
|
const q = new URL(req.url || "", "http://x").searchParams;
|
|
1084
1138
|
const name = (q.get("name") || "").replace(/[/\\]/g, "");
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
if (!
|
|
1139
|
+
// Looks in downloads/ then sent/ (traversal-guarded in the helper).
|
|
1140
|
+
const file = findSavedFile(opts.downloadsDir, name);
|
|
1141
|
+
if (!file) {
|
|
1088
1142
|
res.writeHead(404);
|
|
1089
1143
|
res.end("not found");
|
|
1090
1144
|
return;
|
package/package.json
CHANGED