@decentnetwork/lan 0.1.227 → 0.1.229
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/daemon/ipc.d.ts +5 -1
- package/dist/daemon/ipc.js +8 -0
- package/dist/daemon/server.js +7 -0
- package/dist/ui/desktop/app.js +92 -16
- package/dist/ui/server.js +50 -0
- package/package.json +1 -1
package/dist/daemon/ipc.d.ts
CHANGED
|
@@ -44,6 +44,9 @@ export interface IpcHandlers {
|
|
|
44
44
|
/** Append a local-only chat history entry. Used for browser-local events such
|
|
45
45
|
* as WebRTC file receive; does not transmit anything to the peer. */
|
|
46
46
|
chatLogLocal: (userid: string, dir: "in" | "out", text: string) => Promise<void>;
|
|
47
|
+
/** Append a local-only file history entry after the UI server has saved a
|
|
48
|
+
* browser-received WebRTC file into downloads/. */
|
|
49
|
+
fileLogLocal: (userid: string, dir: "in" | "out", name: string, size: number) => Promise<void>;
|
|
47
50
|
/** Return persisted chat history. With `peer`, returns just that thread and
|
|
48
51
|
* honours `before`/`limit` pagination; without, every thread (legacy shape). */
|
|
49
52
|
chatHistory: (peer?: string, before?: number, limit?: number) => Promise<Record<string, unknown>>;
|
|
@@ -118,7 +121,7 @@ export interface IpcHandlers {
|
|
|
118
121
|
selfRestart: () => Promise<Record<string, unknown>>;
|
|
119
122
|
}
|
|
120
123
|
export interface IpcRequest {
|
|
121
|
-
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-log-local" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "friends-autoaccept" | "set-profile" | "file-send" | "file-delete" | "file-cancel" | "file-retry" | "chat-mark-read" | "subscribe" | "sign" | "call-signal" | "call-poll" | "proxy-reload" | "proxy-access" | "self-restart";
|
|
124
|
+
op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-log-local" | "file-log-local" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "friends-autoaccept" | "set-profile" | "file-send" | "file-delete" | "file-cancel" | "file-retry" | "chat-mark-read" | "subscribe" | "sign" | "call-signal" | "call-poll" | "proxy-reload" | "proxy-access" | "self-restart";
|
|
122
125
|
address?: string;
|
|
123
126
|
hello?: string;
|
|
124
127
|
userid?: string;
|
|
@@ -127,6 +130,7 @@ export interface IpcRequest {
|
|
|
127
130
|
alias?: string;
|
|
128
131
|
name?: string;
|
|
129
132
|
description?: string;
|
|
133
|
+
size?: number;
|
|
130
134
|
path?: string;
|
|
131
135
|
before?: number;
|
|
132
136
|
limit?: number;
|
package/dist/daemon/ipc.js
CHANGED
|
@@ -202,6 +202,14 @@ export class IpcServer {
|
|
|
202
202
|
await this.handlers.chatLogLocal(req.userid, req.dir === "out" ? "out" : "in", req.text ?? "");
|
|
203
203
|
return;
|
|
204
204
|
}
|
|
205
|
+
case "file-log-local": {
|
|
206
|
+
if (!req.userid)
|
|
207
|
+
throw new Error("userid is required");
|
|
208
|
+
if (!req.name)
|
|
209
|
+
throw new Error("name is required");
|
|
210
|
+
await this.handlers.fileLogLocal(req.userid, req.dir === "out" ? "out" : "in", req.name, Math.max(0, req.size ?? 0));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
205
213
|
case "chat-history":
|
|
206
214
|
return await this.handlers.chatHistory(req.userid, req.before, req.limit);
|
|
207
215
|
case "friends-list":
|
package/dist/daemon/server.js
CHANGED
|
@@ -386,6 +386,13 @@ export class DaemonServer {
|
|
|
386
386
|
return;
|
|
387
387
|
this.logChat(userid, dir, text);
|
|
388
388
|
},
|
|
389
|
+
fileLogLocal: async (userid, dir, name, size) => {
|
|
390
|
+
if (!name)
|
|
391
|
+
return;
|
|
392
|
+
this.messageStore?.appendFile(userid, dir, { name: sanitizeFileName(name), size });
|
|
393
|
+
this.friendMeta?.ensure(userid);
|
|
394
|
+
this.ipcEvents.emit("event", { type: "file", userid, dir, name });
|
|
395
|
+
},
|
|
389
396
|
chatHistory: async (peer, before, limit) => {
|
|
390
397
|
return { chats: this.messageStore?.history(peer, { before, limit }) ?? {} };
|
|
391
398
|
},
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -231,6 +231,22 @@ const dkApi = {
|
|
|
231
231
|
cancelSend: (userid, ids) => dkPost("/api/file-cancel", { userid, ids }),
|
|
232
232
|
retrySend: (userid, ids) => dkPost("/api/file-retry", { userid, ids }),
|
|
233
233
|
setProfile: (name, description) => dkPost("/api/set-profile", { name, description }),
|
|
234
|
+
saveWebrtcFile: async (userid, name, blob) => {
|
|
235
|
+
try {
|
|
236
|
+
const r = await fetch(
|
|
237
|
+
"/api/webrtc-file-save?userid=" + encodeURIComponent(userid) + "&name=" + encodeURIComponent(name),
|
|
238
|
+
{ method: "POST", body: blob }
|
|
239
|
+
);
|
|
240
|
+
const txt = await r.text();
|
|
241
|
+
try {
|
|
242
|
+
return JSON.parse(txt);
|
|
243
|
+
} catch {
|
|
244
|
+
return { ok: false, error: (txt || "HTTP " + r.status).slice(0, 120) };
|
|
245
|
+
}
|
|
246
|
+
} catch (e) {
|
|
247
|
+
return { ok: false, error: String(e) };
|
|
248
|
+
}
|
|
249
|
+
},
|
|
234
250
|
sendFile: async (userid, file) => {
|
|
235
251
|
try {
|
|
236
252
|
const r = await fetch(
|
|
@@ -1190,9 +1206,14 @@ function dkRtcFileFromText(peerId, text) {
|
|
|
1190
1206
|
}
|
|
1191
1207
|
return null;
|
|
1192
1208
|
}
|
|
1193
|
-
function
|
|
1209
|
+
function dkCallFromText(text) {
|
|
1210
|
+
const m = /^WebRTC (audio|video) call: (incoming|outgoing)$/.exec(String(text || ""));
|
|
1211
|
+
return m ? { kind: m[1], direction: m[2] } : null;
|
|
1212
|
+
}
|
|
1213
|
+
function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onCall, selMode, selected, onToggleSel, busy }) {
|
|
1194
1214
|
const mine = m.from === "me";
|
|
1195
1215
|
const rtcFile = !mine && !m.file ? dkRtcFileFromText(peer.userId || peer.id, m.text) : null;
|
|
1216
|
+
const callRec = !m.file ? dkCallFromText(m.text) : null;
|
|
1196
1217
|
return /* @__PURE__ */ React.createElement(
|
|
1197
1218
|
"div",
|
|
1198
1219
|
{
|
|
@@ -1222,7 +1243,16 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, sele
|
|
|
1222
1243
|
justifyContent: "center"
|
|
1223
1244
|
} }, selected && /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 3, color: "#fff" })),
|
|
1224
1245
|
!mine && /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 24, radius: 6, dot: false }),
|
|
1225
|
-
/* @__PURE__ */ React.createElement("div", { style: { maxWidth: m.file && (m.file.media === "image" || m.file.media === "video") || rtcFile && (rtcFile.media === "image" || rtcFile.media === "video") ? "min(680px, 88%)" : "64%", display: "flex", flexDirection: "column", alignItems: mine ? "flex-end" : "flex-start" } },
|
|
1246
|
+
/* @__PURE__ */ React.createElement("div", { style: { maxWidth: m.file && (m.file.media === "image" || m.file.media === "video") || rtcFile && (rtcFile.media === "image" || rtcFile.media === "video") ? "min(680px, 88%)" : "64%", display: "flex", flexDirection: "column", alignItems: mine ? "flex-end" : "flex-start" } }, callRec ? /* @__PURE__ */ React.createElement(
|
|
1247
|
+
"button",
|
|
1248
|
+
{
|
|
1249
|
+
onClick: () => onCall && onCall(peer.userId, callRec.kind === "video"),
|
|
1250
|
+
style: { display: "flex", alignItems: "center", gap: 10, padding: "9px 12px", borderRadius: 12, border: "1px solid var(--line)", background: mine ? "var(--bub-me)" : "var(--bub-them)", color: mine ? "#fff" : "var(--text)", cursor: "pointer", fontFamily: "var(--mono)", fontSize: 12.5 }
|
|
1251
|
+
},
|
|
1252
|
+
/* @__PURE__ */ React.createElement(Icon, { name: callRec.kind === "video" ? "video" : "phone", size: 17, stroke: 2, color: mine ? "#fff" : "var(--accent)" }),
|
|
1253
|
+
/* @__PURE__ */ React.createElement("span", null, callRec.direction === "incoming" ? "Incoming" : "Outgoing", " ", callRec.kind, " call"),
|
|
1254
|
+
/* @__PURE__ */ React.createElement("span", { style: { color: mine ? "rgba(255,255,255,0.75)" : "var(--faint)" } }, "\xB7 redial")
|
|
1255
|
+
) : rtcFile ? /* @__PURE__ */ React.createElement("div", { style: {
|
|
1226
1256
|
display: "flex",
|
|
1227
1257
|
flexDirection: "column",
|
|
1228
1258
|
gap: 6,
|
|
@@ -1603,8 +1633,14 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1603
1633
|
if (!files || !files.length || !sender) return;
|
|
1604
1634
|
const file = files[0];
|
|
1605
1635
|
followScrollRef.current = true;
|
|
1606
|
-
setSending({ name: file.name, via: mode, pct: 0 });
|
|
1607
|
-
const onProgress = (p) => setSending((s) => s && s.name === file.name ? {
|
|
1636
|
+
setSending({ name: file.name, via: mode, pct: 0, sent: 0, size: file.size });
|
|
1637
|
+
const onProgress = (p) => setSending((s) => s && s.name === file.name ? {
|
|
1638
|
+
...s,
|
|
1639
|
+
pct: p && p.pct,
|
|
1640
|
+
sent: p && p.sent,
|
|
1641
|
+
size: p && p.size || s.size,
|
|
1642
|
+
kbps: p && p.kbps
|
|
1643
|
+
} : s);
|
|
1608
1644
|
Promise.resolve(sender(file, onProgress)).then((r) => {
|
|
1609
1645
|
if (r && r.ok === false) window.alert((lang === "zh" ? "\u53D1\u9001\u5931\u8D25: " : "Send failed: ") + (r.error || ""));
|
|
1610
1646
|
else showFlash("ok", mode === "webrtc" ? lang === "zh" ? "WebRTC \u6587\u4EF6\u53D1\u9001\u5B8C\u6210" : "WebRTC file sent" : lang === "zh" ? "\u6587\u4EF6\u5DF2\u63D0\u4EA4\u53D1\u9001" : "File send started");
|
|
@@ -1664,6 +1700,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1664
1700
|
onDelete: (id) => doDelete([id]),
|
|
1665
1701
|
onCancel: doCancel,
|
|
1666
1702
|
onRetry: doRetry,
|
|
1703
|
+
onCall,
|
|
1667
1704
|
busy: m.id ? fileBusy[m.id] : void 0,
|
|
1668
1705
|
selMode,
|
|
1669
1706
|
selected: sel.has(m.id),
|
|
@@ -1691,7 +1728,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1691
1728
|
stroke: 2.2,
|
|
1692
1729
|
color: flash.kind === "err" ? "var(--danger)" : "var(--accent)"
|
|
1693
1730
|
}
|
|
1694
|
-
), flash.msg), sending && /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto 8px", display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "paperclip", size: 13, stroke: 2, color: "var(--accent)" }), (lang === "zh" ? "\u53D1\u9001\u4E2D: " : "Sending: ") + (sending.via === "webrtc" ? "WebRTC \xB7 " : "") + sending.name + (sending.pct != null ? " \xB7 " + Math.min(100, Math.max(0, sending.pct)).toFixed(1) + "%" : "")), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement(
|
|
1731
|
+
), flash.msg), sending && /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto 8px", display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "paperclip", size: 13, stroke: 2, color: "var(--accent)" }), (lang === "zh" ? "\u53D1\u9001\u4E2D: " : "Sending: ") + (sending.via === "webrtc" ? "WebRTC \xB7 " : "") + sending.name + (sending.size ? " \xB7 " + dkFileSize(sending.sent || 0) + " / " + dkFileSize(sending.size) : "") + (sending.pct != null ? " \xB7 " + Math.min(100, Math.max(0, sending.pct)).toFixed(1) + "%" : "") + (sending.kbps != null ? " \xB7 " + dkSpeed(sending.kbps) : "")), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement(
|
|
1695
1732
|
"input",
|
|
1696
1733
|
{
|
|
1697
1734
|
ref: fileRef,
|
|
@@ -2008,13 +2045,13 @@ function dkRememberRtcFile(peerId, fileId, name, size, url) {
|
|
|
2008
2045
|
window.__dkRtcFiles.set(peerId + "|" + name + "|" + size, item);
|
|
2009
2046
|
return item;
|
|
2010
2047
|
}
|
|
2011
|
-
function useRtcFileController(selfId,
|
|
2048
|
+
function useRtcFileController(selfId, onReceivedFile) {
|
|
2012
2049
|
const [incoming, setIncoming] = React.useState([]);
|
|
2013
2050
|
const peersRef = React.useRef(/* @__PURE__ */ new Map());
|
|
2014
|
-
const
|
|
2051
|
+
const onReceivedFileRef = React.useRef(onReceivedFile);
|
|
2015
2052
|
React.useEffect(() => {
|
|
2016
|
-
|
|
2017
|
-
}, [
|
|
2053
|
+
onReceivedFileRef.current = onReceivedFile;
|
|
2054
|
+
}, [onReceivedFile]);
|
|
2018
2055
|
React.useEffect(() => {
|
|
2019
2056
|
if (!selfId || !window.RTCPeerConnection || !window.dkRtcSignalBus) return;
|
|
2020
2057
|
const bus = dkRtcSignalBus();
|
|
@@ -2055,7 +2092,7 @@ function useRtcFileController(selfId, onReceivedText) {
|
|
|
2055
2092
|
const dl = dkFileDownload(blob, meta.name || "agentnet-file");
|
|
2056
2093
|
dkRememberRtcFile(peerId, fileId, dl.name, blob.size, dl.url);
|
|
2057
2094
|
setIncoming((arr) => [{ id: fileId, peerId, name: dl.name, size: blob.size, url: dl.url }].concat(arr).slice(0, 8));
|
|
2058
|
-
if (
|
|
2095
|
+
if (onReceivedFileRef.current) onReceivedFileRef.current(peerId, dl.name, blob);
|
|
2059
2096
|
cleanup(fileId);
|
|
2060
2097
|
} else if (msg.type === "cancel") {
|
|
2061
2098
|
cleanup(fileId);
|
|
@@ -2176,16 +2213,36 @@ function useRtcFileController(selfId, onReceivedText) {
|
|
|
2176
2213
|
await dkWaitForOpen(dc, DK_FILE_RTC_OPEN_TIMEOUT_MS, () => dkRtcState(pc));
|
|
2177
2214
|
dc.send(JSON.stringify({ type: "meta", name: file.name, size: file.size, mime: file.type || "application/octet-stream" }));
|
|
2178
2215
|
let sent = 0;
|
|
2216
|
+
const startedAt = Date.now();
|
|
2217
|
+
let lastProgressAt = 0;
|
|
2179
2218
|
const highWater = 4 * 1024 * 1024;
|
|
2219
|
+
const reportProgress = (force) => {
|
|
2220
|
+
if (!onProgress) return;
|
|
2221
|
+
const now = Date.now();
|
|
2222
|
+
if (!force && now - lastProgressAt < 250 && sent < file.size) return;
|
|
2223
|
+
lastProgressAt = now;
|
|
2224
|
+
const elapsedMs = Math.max(1, now - startedAt);
|
|
2225
|
+
const kbps = sent / 1024 / (elapsedMs / 1e3);
|
|
2226
|
+
onProgress({
|
|
2227
|
+
sent,
|
|
2228
|
+
size: file.size,
|
|
2229
|
+
pct: file.size ? sent / file.size * 100 : 100,
|
|
2230
|
+
kbps,
|
|
2231
|
+
elapsedMs,
|
|
2232
|
+
via: "webrtc"
|
|
2233
|
+
});
|
|
2234
|
+
};
|
|
2235
|
+
reportProgress(true);
|
|
2180
2236
|
while (sent < file.size) {
|
|
2181
2237
|
if (dc.readyState !== "open") throw new Error("WebRTC data channel closed");
|
|
2182
2238
|
const end = Math.min(file.size, sent + DK_FILE_RTC_CHUNK);
|
|
2183
2239
|
const buf = await file.slice(sent, end).arrayBuffer();
|
|
2184
2240
|
dc.send(buf);
|
|
2185
2241
|
sent = end;
|
|
2186
|
-
|
|
2242
|
+
reportProgress(false);
|
|
2187
2243
|
await dkWaitBufferedLow(dc, highWater);
|
|
2188
2244
|
}
|
|
2245
|
+
reportProgress(true);
|
|
2189
2246
|
dc.send(JSON.stringify({ type: "done" }));
|
|
2190
2247
|
await sendSignal("bye", { reason: "done" }).catch(() => {
|
|
2191
2248
|
});
|
|
@@ -2224,13 +2281,17 @@ const CALL_ICE_SERVERS = [
|
|
|
2224
2281
|
{ urls: "turn:tokyo.fi.chat:3478", username: "allcom", credential: "allcompass" },
|
|
2225
2282
|
{ urls: "turn:tokyo.fi.chat:3478?transport=tcp", username: "allcom", credential: "allcompass" }
|
|
2226
2283
|
];
|
|
2227
|
-
function useCallController(selfId) {
|
|
2284
|
+
function useCallController(selfId, onCallLog) {
|
|
2228
2285
|
const [incoming, setIncoming] = React.useState(null);
|
|
2229
2286
|
const [active, setActive] = React.useState(null);
|
|
2230
2287
|
const [localStream, setLocalStream] = React.useState(null);
|
|
2231
2288
|
const [remoteStream, setRemoteStream] = React.useState(null);
|
|
2232
2289
|
const [sharing, setSharing] = React.useState(false);
|
|
2233
2290
|
const engineRef = React.useRef(null);
|
|
2291
|
+
const onCallLogRef = React.useRef(onCallLog);
|
|
2292
|
+
React.useEffect(() => {
|
|
2293
|
+
onCallLogRef.current = onCallLog;
|
|
2294
|
+
}, [onCallLog]);
|
|
2234
2295
|
React.useEffect(() => {
|
|
2235
2296
|
if (!selfId || engineRef.current) return;
|
|
2236
2297
|
if (!window.PeerWebRTC || !window.RTCPeerConnection) {
|
|
@@ -2248,7 +2309,10 @@ function useCallController(selfId) {
|
|
|
2248
2309
|
iceServers: CALL_ICE_SERVERS,
|
|
2249
2310
|
logger: (m) => console.log(m)
|
|
2250
2311
|
});
|
|
2251
|
-
engine.on("incomingCall", (info) =>
|
|
2312
|
+
engine.on("incomingCall", (info) => {
|
|
2313
|
+
setIncoming(info);
|
|
2314
|
+
if (onCallLogRef.current) onCallLogRef.current(info.peerId, !!info.video, "incoming");
|
|
2315
|
+
});
|
|
2252
2316
|
engine.on("stateChanged", (info, state) => setActive((a) => a && a.callId === info.callId ? { ...a, state } : a));
|
|
2253
2317
|
engine.on("localStream", (id, s) => {
|
|
2254
2318
|
setLocalStream(s);
|
|
@@ -2281,6 +2345,7 @@ function useCallController(selfId) {
|
|
|
2281
2345
|
return;
|
|
2282
2346
|
}
|
|
2283
2347
|
setActive({ callId: null, peerId, video: !!video, direction: "outgoing", state: "ringing" });
|
|
2348
|
+
if (onCallLogRef.current) onCallLogRef.current(peerId, !!video, "outgoing");
|
|
2284
2349
|
eng.call(peerId, { audio: true, video: !!video }).then((callId) => setActive((a) => a && a.peerId === peerId && a.callId === null ? { ...a, callId } : a)).catch((e) => {
|
|
2285
2350
|
setActive((a) => a && a.peerId === peerId ? null : a);
|
|
2286
2351
|
alert("Could not start call: " + (e && e.message || e));
|
|
@@ -2748,10 +2813,21 @@ function DkApp() {
|
|
|
2748
2813
|
const T = STR[t.lang] || STR.en;
|
|
2749
2814
|
const vars = dkTheme(t.theme, t.accent);
|
|
2750
2815
|
const rowPad = t.density === "comfortable" ? "11px 12px" : "7px 10px";
|
|
2751
|
-
const callCtl = useCallController(me.userId)
|
|
2816
|
+
const callCtl = useCallController(me.userId, (peerId, video, direction) => {
|
|
2817
|
+
const text = `WebRTC ${video ? "video" : "audio"} call: ${direction}`;
|
|
2818
|
+
dkApi.logLocal(peerId, direction === "outgoing" ? "out" : "in", text).then(() => {
|
|
2819
|
+
data.loadThread(peerId);
|
|
2820
|
+
data.refresh();
|
|
2821
|
+
});
|
|
2822
|
+
});
|
|
2752
2823
|
const onCall = (peerId, video) => callCtl.start(peerId, !!video);
|
|
2753
|
-
const rtcFileCtl = useRtcFileController(me.userId, (peerId,
|
|
2754
|
-
|
|
2824
|
+
const rtcFileCtl = useRtcFileController(me.userId, (peerId, name, blob) => {
|
|
2825
|
+
if (!blob) return;
|
|
2826
|
+
dkApi.saveWebrtcFile(peerId, name, blob).then((r) => {
|
|
2827
|
+
if (!r || r.ok === false) {
|
|
2828
|
+
return dkApi.logLocal(peerId, "in", "received file via WebRTC: " + name + " (" + dkFileSize(blob.size) + ")");
|
|
2829
|
+
}
|
|
2830
|
+
}).then(() => {
|
|
2755
2831
|
data.loadThread(peerId);
|
|
2756
2832
|
data.refresh();
|
|
2757
2833
|
});
|
package/dist/ui/server.js
CHANGED
|
@@ -346,6 +346,56 @@ export function startFriendUi(opts) {
|
|
|
346
346
|
}
|
|
347
347
|
return;
|
|
348
348
|
}
|
|
349
|
+
// Persist a file received by browser WebRTC DataChannel. Unlike the
|
|
350
|
+
// ordinary daemon filetransfer path, the bytes arrive in the browser, so
|
|
351
|
+
// the browser uploads the completed Blob here; we save it under
|
|
352
|
+
// downloads/ and ask the daemon to append a normal inbound file chip.
|
|
353
|
+
if (req.method === "POST" && url === "/api/webrtc-file-save") {
|
|
354
|
+
if (!opts.downloadsDir) {
|
|
355
|
+
sendJson(res, 404, { ok: false, error: "downloads disabled" });
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const q = new URL(req.url || "", "http://x").searchParams;
|
|
359
|
+
const userid = q.get("userid") || "";
|
|
360
|
+
const rawName = q.get("name") || "agentnet-file";
|
|
361
|
+
const safe = rawName.replace(/[/\\]/g, "_").slice(0, 200) || "agentnet-file";
|
|
362
|
+
if (!userid) {
|
|
363
|
+
sendJson(res, 400, { ok: false, error: "userid required" });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const fs = await import("node:fs");
|
|
367
|
+
const fsp = await import("node:fs/promises");
|
|
368
|
+
await fsp.mkdir(opts.downloadsDir, { recursive: true });
|
|
369
|
+
let finalName = safe;
|
|
370
|
+
let finalPath = join(opts.downloadsDir, finalName);
|
|
371
|
+
const dot = safe.lastIndexOf(".");
|
|
372
|
+
const base = dot > 0 ? safe.slice(0, dot) : safe;
|
|
373
|
+
const ext = dot > 0 ? safe.slice(dot) : "";
|
|
374
|
+
for (let i = 1; existsSync(finalPath); i++) {
|
|
375
|
+
finalName = `${base}-${i}${ext}`;
|
|
376
|
+
finalPath = join(opts.downloadsDir, finalName);
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
await new Promise((resolve2, reject) => {
|
|
380
|
+
const ws = fs.createWriteStream(finalPath);
|
|
381
|
+
req.pipe(ws);
|
|
382
|
+
req.on("error", reject);
|
|
383
|
+
ws.on("error", reject);
|
|
384
|
+
ws.on("finish", () => resolve2());
|
|
385
|
+
});
|
|
386
|
+
const st = statSync(finalPath);
|
|
387
|
+
const r = await opts.call({ op: "file-log-local", userid, dir: "in", name: finalName, size: st.size });
|
|
388
|
+
if (!r.ok) {
|
|
389
|
+
sendJson(res, 502, r);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
sendJson(res, 200, { ok: true, name: finalName, size: st.size });
|
|
393
|
+
}
|
|
394
|
+
catch (e) {
|
|
395
|
+
sendJson(res, 500, { ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
396
|
+
}
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
349
399
|
// Serve a received file by name, from <configDir>/downloads. Used both for
|
|
350
400
|
// download (?dl=1 → attachment) and inline preview/playback (no dl → the
|
|
351
401
|
// right media content-type + inline disposition + HTTP Range so <video>/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.229",
|
|
4
4
|
"description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|