@decentnetwork/lan 0.1.226 → 0.1.228
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 +124 -12
- 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(
|
|
@@ -1181,8 +1197,23 @@ const mediaBtnStyle = {
|
|
|
1181
1197
|
justifyContent: "center",
|
|
1182
1198
|
lineHeight: 0
|
|
1183
1199
|
};
|
|
1184
|
-
function
|
|
1200
|
+
function dkRtcFileFromText(peerId, text) {
|
|
1201
|
+
const m = /^received file via WebRTC: (.+) \(([^)]+)\)$/.exec(String(text || ""));
|
|
1202
|
+
if (!m || !window.__dkRtcFiles) return null;
|
|
1203
|
+
const name = m[1];
|
|
1204
|
+
for (const item of window.__dkRtcFiles.values()) {
|
|
1205
|
+
if (item && item.peerId === peerId && item.name === name) return item;
|
|
1206
|
+
}
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
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 }) {
|
|
1185
1214
|
const mine = m.from === "me";
|
|
1215
|
+
const rtcFile = !mine && !m.file ? dkRtcFileFromText(peer.userId || peer.id, m.text) : null;
|
|
1216
|
+
const callRec = !m.file ? dkCallFromText(m.text) : null;
|
|
1186
1217
|
return /* @__PURE__ */ React.createElement(
|
|
1187
1218
|
"div",
|
|
1188
1219
|
{
|
|
@@ -1212,7 +1243,61 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, sele
|
|
|
1212
1243
|
justifyContent: "center"
|
|
1213
1244
|
} }, selected && /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 3, color: "#fff" })),
|
|
1214
1245
|
!mine && /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 24, radius: 6, dot: false }),
|
|
1215
|
-
/* @__PURE__ */ React.createElement("div", { style: { maxWidth: m.file && (m.file.media === "image" || m.file.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: {
|
|
1256
|
+
display: "flex",
|
|
1257
|
+
flexDirection: "column",
|
|
1258
|
+
gap: 6,
|
|
1259
|
+
maxWidth: rtcFile.media === "image" || rtcFile.media === "video" ? "100%" : 300,
|
|
1260
|
+
width: rtcFile.media === "image" || rtcFile.media === "video" ? "100%" : void 0,
|
|
1261
|
+
background: "var(--bub-them)",
|
|
1262
|
+
border: "1px solid var(--line)",
|
|
1263
|
+
borderRadius: 12,
|
|
1264
|
+
padding: 6
|
|
1265
|
+
} }, (rtcFile.media === "image" || rtcFile.media === "video") && /* @__PURE__ */ React.createElement("div", { "data-media": true, style: { position: "relative", lineHeight: 0 } }, rtcFile.media === "image" ? /* @__PURE__ */ React.createElement(
|
|
1266
|
+
"img",
|
|
1267
|
+
{
|
|
1268
|
+
src: rtcFile.url,
|
|
1269
|
+
alt: rtcFile.name,
|
|
1270
|
+
style: { display: "block", width: "100%", maxHeight: "72vh", borderRadius: 8, objectFit: "contain" }
|
|
1271
|
+
}
|
|
1272
|
+
) : /* @__PURE__ */ React.createElement(
|
|
1273
|
+
"video",
|
|
1274
|
+
{
|
|
1275
|
+
src: rtcFile.url,
|
|
1276
|
+
controls: true,
|
|
1277
|
+
preload: "metadata",
|
|
1278
|
+
style: { display: "block", width: "100%", maxHeight: "72vh", borderRadius: 8, objectFit: "contain", background: "#000" }
|
|
1279
|
+
}
|
|
1280
|
+
), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", top: 8, right: 8, display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(
|
|
1281
|
+
"button",
|
|
1282
|
+
{
|
|
1283
|
+
onClick: () => onTheater({ kind: rtcFile.media, url: rtcFile.url, name: rtcFile.name }),
|
|
1284
|
+
title: "theater / \u7F51\u9875\u5168\u5C4F",
|
|
1285
|
+
style: mediaBtnStyle
|
|
1286
|
+
},
|
|
1287
|
+
/* @__PURE__ */ React.createElement(Icon, { name: "theater", size: 16, stroke: 2, color: "#fff" })
|
|
1288
|
+
), /* @__PURE__ */ React.createElement(
|
|
1289
|
+
"button",
|
|
1290
|
+
{
|
|
1291
|
+
onClick: (e) => {
|
|
1292
|
+
const w = e.currentTarget.closest("[data-media]");
|
|
1293
|
+
const el = w && w.querySelector("video,img");
|
|
1294
|
+
if (el && el.requestFullscreen) el.requestFullscreen();
|
|
1295
|
+
},
|
|
1296
|
+
title: "fullscreen / \u5168\u5C4F",
|
|
1297
|
+
style: mediaBtnStyle
|
|
1298
|
+
},
|
|
1299
|
+
/* @__PURE__ */ React.createElement(Icon, { name: "maximize", size: 16, stroke: 2, color: "#fff" })
|
|
1300
|
+
))), rtcFile.media === "audio" && /* @__PURE__ */ React.createElement("audio", { src: rtcFile.url, controls: true, style: { width: "100%" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "0 2px" } }, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, rtcFile.name), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", flexShrink: 0 } }, dkFileSize(rtcFile.size)), /* @__PURE__ */ React.createElement("a", { href: rtcFile.url, download: rtcFile.name, title: "download", style: { display: "inline-flex", flexShrink: 0 } }, /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 15, stroke: 2, color: "var(--accent)" })))) : m.file ? (
|
|
1216
1301
|
// Inline preview/player. Received media: as soon as it's saved.
|
|
1217
1302
|
// Sent media: once it's fully delivered ('sent') — the daemon keeps a
|
|
1218
1303
|
// local copy so it plays on the sender's side too.
|
|
@@ -1609,6 +1694,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1609
1694
|
onDelete: (id) => doDelete([id]),
|
|
1610
1695
|
onCancel: doCancel,
|
|
1611
1696
|
onRetry: doRetry,
|
|
1697
|
+
onCall,
|
|
1612
1698
|
busy: m.id ? fileBusy[m.id] : void 0,
|
|
1613
1699
|
selMode,
|
|
1614
1700
|
selected: sel.has(m.id),
|
|
@@ -1947,13 +2033,19 @@ function dkFileDownload(blob, name) {
|
|
|
1947
2033
|
a.textContent = name || "agentnet-file";
|
|
1948
2034
|
return { url, name: a.download };
|
|
1949
2035
|
}
|
|
1950
|
-
function
|
|
2036
|
+
function dkRememberRtcFile(peerId, fileId, name, size, url) {
|
|
2037
|
+
if (!window.__dkRtcFiles) window.__dkRtcFiles = /* @__PURE__ */ new Map();
|
|
2038
|
+
const item = { id: fileId, peerId, name, size, url, media: dkFileMediaKind(name) };
|
|
2039
|
+
window.__dkRtcFiles.set(peerId + "|" + name + "|" + size, item);
|
|
2040
|
+
return item;
|
|
2041
|
+
}
|
|
2042
|
+
function useRtcFileController(selfId, onReceivedFile) {
|
|
1951
2043
|
const [incoming, setIncoming] = React.useState([]);
|
|
1952
2044
|
const peersRef = React.useRef(/* @__PURE__ */ new Map());
|
|
1953
|
-
const
|
|
2045
|
+
const onReceivedFileRef = React.useRef(onReceivedFile);
|
|
1954
2046
|
React.useEffect(() => {
|
|
1955
|
-
|
|
1956
|
-
}, [
|
|
2047
|
+
onReceivedFileRef.current = onReceivedFile;
|
|
2048
|
+
}, [onReceivedFile]);
|
|
1957
2049
|
React.useEffect(() => {
|
|
1958
2050
|
if (!selfId || !window.RTCPeerConnection || !window.dkRtcSignalBus) return;
|
|
1959
2051
|
const bus = dkRtcSignalBus();
|
|
@@ -1992,8 +2084,9 @@ function useRtcFileController(selfId, onReceivedText) {
|
|
|
1992
2084
|
const meta = sess.meta || {};
|
|
1993
2085
|
const blob = new Blob(sess.chunks, { type: meta.mime || "application/octet-stream" });
|
|
1994
2086
|
const dl = dkFileDownload(blob, meta.name || "agentnet-file");
|
|
2087
|
+
dkRememberRtcFile(peerId, fileId, dl.name, blob.size, dl.url);
|
|
1995
2088
|
setIncoming((arr) => [{ id: fileId, peerId, name: dl.name, size: blob.size, url: dl.url }].concat(arr).slice(0, 8));
|
|
1996
|
-
if (
|
|
2089
|
+
if (onReceivedFileRef.current) onReceivedFileRef.current(peerId, dl.name, blob);
|
|
1997
2090
|
cleanup(fileId);
|
|
1998
2091
|
} else if (msg.type === "cancel") {
|
|
1999
2092
|
cleanup(fileId);
|
|
@@ -2162,13 +2255,17 @@ const CALL_ICE_SERVERS = [
|
|
|
2162
2255
|
{ urls: "turn:tokyo.fi.chat:3478", username: "allcom", credential: "allcompass" },
|
|
2163
2256
|
{ urls: "turn:tokyo.fi.chat:3478?transport=tcp", username: "allcom", credential: "allcompass" }
|
|
2164
2257
|
];
|
|
2165
|
-
function useCallController(selfId) {
|
|
2258
|
+
function useCallController(selfId, onCallLog) {
|
|
2166
2259
|
const [incoming, setIncoming] = React.useState(null);
|
|
2167
2260
|
const [active, setActive] = React.useState(null);
|
|
2168
2261
|
const [localStream, setLocalStream] = React.useState(null);
|
|
2169
2262
|
const [remoteStream, setRemoteStream] = React.useState(null);
|
|
2170
2263
|
const [sharing, setSharing] = React.useState(false);
|
|
2171
2264
|
const engineRef = React.useRef(null);
|
|
2265
|
+
const onCallLogRef = React.useRef(onCallLog);
|
|
2266
|
+
React.useEffect(() => {
|
|
2267
|
+
onCallLogRef.current = onCallLog;
|
|
2268
|
+
}, [onCallLog]);
|
|
2172
2269
|
React.useEffect(() => {
|
|
2173
2270
|
if (!selfId || engineRef.current) return;
|
|
2174
2271
|
if (!window.PeerWebRTC || !window.RTCPeerConnection) {
|
|
@@ -2186,7 +2283,10 @@ function useCallController(selfId) {
|
|
|
2186
2283
|
iceServers: CALL_ICE_SERVERS,
|
|
2187
2284
|
logger: (m) => console.log(m)
|
|
2188
2285
|
});
|
|
2189
|
-
engine.on("incomingCall", (info) =>
|
|
2286
|
+
engine.on("incomingCall", (info) => {
|
|
2287
|
+
setIncoming(info);
|
|
2288
|
+
if (onCallLogRef.current) onCallLogRef.current(info.peerId, !!info.video, "incoming");
|
|
2289
|
+
});
|
|
2190
2290
|
engine.on("stateChanged", (info, state) => setActive((a) => a && a.callId === info.callId ? { ...a, state } : a));
|
|
2191
2291
|
engine.on("localStream", (id, s) => {
|
|
2192
2292
|
setLocalStream(s);
|
|
@@ -2219,6 +2319,7 @@ function useCallController(selfId) {
|
|
|
2219
2319
|
return;
|
|
2220
2320
|
}
|
|
2221
2321
|
setActive({ callId: null, peerId, video: !!video, direction: "outgoing", state: "ringing" });
|
|
2322
|
+
if (onCallLogRef.current) onCallLogRef.current(peerId, !!video, "outgoing");
|
|
2222
2323
|
eng.call(peerId, { audio: true, video: !!video }).then((callId) => setActive((a) => a && a.peerId === peerId && a.callId === null ? { ...a, callId } : a)).catch((e) => {
|
|
2223
2324
|
setActive((a) => a && a.peerId === peerId ? null : a);
|
|
2224
2325
|
alert("Could not start call: " + (e && e.message || e));
|
|
@@ -2686,10 +2787,21 @@ function DkApp() {
|
|
|
2686
2787
|
const T = STR[t.lang] || STR.en;
|
|
2687
2788
|
const vars = dkTheme(t.theme, t.accent);
|
|
2688
2789
|
const rowPad = t.density === "comfortable" ? "11px 12px" : "7px 10px";
|
|
2689
|
-
const callCtl = useCallController(me.userId)
|
|
2790
|
+
const callCtl = useCallController(me.userId, (peerId, video, direction) => {
|
|
2791
|
+
const text = `WebRTC ${video ? "video" : "audio"} call: ${direction}`;
|
|
2792
|
+
dkApi.logLocal(peerId, direction === "outgoing" ? "out" : "in", text).then(() => {
|
|
2793
|
+
data.loadThread(peerId);
|
|
2794
|
+
data.refresh();
|
|
2795
|
+
});
|
|
2796
|
+
});
|
|
2690
2797
|
const onCall = (peerId, video) => callCtl.start(peerId, !!video);
|
|
2691
|
-
const rtcFileCtl = useRtcFileController(me.userId, (peerId,
|
|
2692
|
-
|
|
2798
|
+
const rtcFileCtl = useRtcFileController(me.userId, (peerId, name, blob) => {
|
|
2799
|
+
if (!blob) return;
|
|
2800
|
+
dkApi.saveWebrtcFile(peerId, name, blob).then((r) => {
|
|
2801
|
+
if (!r || r.ok === false) {
|
|
2802
|
+
return dkApi.logLocal(peerId, "in", "received file via WebRTC: " + name + " (" + dkFileSize(blob.size) + ")");
|
|
2803
|
+
}
|
|
2804
|
+
}).then(() => {
|
|
2693
2805
|
data.loadThread(peerId);
|
|
2694
2806
|
data.refresh();
|
|
2695
2807
|
});
|
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.228",
|
|
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",
|