dsh-any-background 0.2.5 → 0.2.7
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/README.md +163 -150
- package/README.zh.md +163 -150
- package/lib/client.js +1095 -417
- package/lib/client.js.map +1 -1
- package/lib/index.js +357 -24
- package/package.json +126 -121
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
3
4
|
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
4
5
|
//#region src/index.ts
|
|
5
6
|
/**
|
|
@@ -24,9 +25,17 @@ const WALLPAPER_FILE = "wallpaper.jpg";
|
|
|
24
25
|
const ROTATION_DIR = "rotation";
|
|
25
26
|
const VIDEO_ROUTE = "/dsh-any-background/video";
|
|
26
27
|
const UPLOAD_ROUTE = "/dsh-any-background/video/upload";
|
|
28
|
+
const WALLPAPER_ROUTE = "/dsh-any-background/wallpaper";
|
|
29
|
+
const WALLPAPER_UPLOAD_ROUTE = "/dsh-any-background/wallpaper/upload";
|
|
27
30
|
const UPLOAD_TMP = "wallpaper.upload.tmp";
|
|
31
|
+
const VIDEO_UPLOAD_TMP = "video.upload.tmp";
|
|
32
|
+
const WALLPAPER_UPLOAD_MAX = 104857600;
|
|
33
|
+
const VIDEO_UPLOAD_MAX = 2147483648;
|
|
28
34
|
const WALLPAPER_FETCH_MAX = 26214400;
|
|
29
35
|
const WALLPAPER_FETCH_TIMEOUT = 2e4;
|
|
36
|
+
const VIDEO_FETCH_MAX = 2147483648;
|
|
37
|
+
const VIDEO_FETCH_TIMEOUT = 6e4;
|
|
38
|
+
const VIDEO_FETCH_IDLE_TIMEOUT = 6e4;
|
|
30
39
|
function videoFileName(mime) {
|
|
31
40
|
switch (mime) {
|
|
32
41
|
case "video/mp4": return "wallpaper.mp4";
|
|
@@ -60,7 +69,8 @@ const DEFAULT_CONFIG = {
|
|
|
60
69
|
settings: 0,
|
|
61
70
|
chat: 0,
|
|
62
71
|
trajectory: 0,
|
|
63
|
-
input: 0
|
|
72
|
+
input: 0,
|
|
73
|
+
panel: 0
|
|
64
74
|
},
|
|
65
75
|
settingsOpacity: 1,
|
|
66
76
|
wallpaperOpacity: 1,
|
|
@@ -86,6 +96,7 @@ const DEFAULT_CONFIG = {
|
|
|
86
96
|
regenerateOnReload: false,
|
|
87
97
|
chatTextOpacity: 0,
|
|
88
98
|
trajectoryOpacity: 1,
|
|
99
|
+
panelOpacity: 1,
|
|
89
100
|
profiles: [],
|
|
90
101
|
rotation: {
|
|
91
102
|
enabled: false,
|
|
@@ -194,7 +205,8 @@ function normalizeConfig(raw) {
|
|
|
194
205
|
"settings",
|
|
195
206
|
"chat",
|
|
196
207
|
"trajectory",
|
|
197
|
-
"input"
|
|
208
|
+
"input",
|
|
209
|
+
"panel"
|
|
198
210
|
]) blurs[k] = clamp(bl[k], 0, 60, DEFAULT_CONFIG.blurs[k]);
|
|
199
211
|
return {
|
|
200
212
|
color,
|
|
@@ -217,6 +229,7 @@ function normalizeConfig(raw) {
|
|
|
217
229
|
regenerateOnReload: typeof r.regenerateOnReload === "boolean" ? r.regenerateOnReload : DEFAULT_CONFIG.regenerateOnReload,
|
|
218
230
|
chatTextOpacity: clamp(r.chatTextOpacity, 0, 1, DEFAULT_CONFIG.chatTextOpacity),
|
|
219
231
|
trajectoryOpacity: clamp(r.trajectoryOpacity, 0, 1, DEFAULT_CONFIG.trajectoryOpacity),
|
|
232
|
+
panelOpacity: clamp(r.panelOpacity, 0, 1, DEFAULT_CONFIG.panelOpacity),
|
|
220
233
|
profiles: normalizeProfiles(r.profiles),
|
|
221
234
|
rotation: normalizeRotation(r.rotation),
|
|
222
235
|
schedule: normalizeSchedule(r.schedule),
|
|
@@ -236,7 +249,8 @@ function normalizeGeneratedBg(p) {
|
|
|
236
249
|
preset: [
|
|
237
250
|
"aurora",
|
|
238
251
|
"nebula",
|
|
239
|
-
"noise"
|
|
252
|
+
"noise",
|
|
253
|
+
"starfield"
|
|
240
254
|
].includes(p.preset) ? p.preset : "aurora",
|
|
241
255
|
speed: clamp(p.speed, 0, 2, .3),
|
|
242
256
|
scale: clamp(p.scale, .3, 3, 1),
|
|
@@ -247,7 +261,10 @@ function normalizeGeneratedBg(p) {
|
|
|
247
261
|
preset: [
|
|
248
262
|
"dots",
|
|
249
263
|
"waves",
|
|
250
|
-
"poly"
|
|
264
|
+
"poly",
|
|
265
|
+
"rain",
|
|
266
|
+
"contour",
|
|
267
|
+
"meta"
|
|
251
268
|
].includes(p.preset) ? p.preset : "dots",
|
|
252
269
|
density: clamp(p.density, 0, 1, .5),
|
|
253
270
|
scale: clamp(p.scale, .3, 3, 1),
|
|
@@ -273,7 +290,8 @@ function normalizeProfileAppearance(raw) {
|
|
|
273
290
|
"settings",
|
|
274
291
|
"chat",
|
|
275
292
|
"trajectory",
|
|
276
|
-
"input"
|
|
293
|
+
"input",
|
|
294
|
+
"panel"
|
|
277
295
|
]) blurs[k] = clamp(bl[k], 0, 60, DEFAULT_CONFIG.blurs[k]);
|
|
278
296
|
return {
|
|
279
297
|
color: Array.isArray(c) && c.length === 3 && c.every((x) => typeof x === "number" && isFinite(x)) ? [
|
|
@@ -292,7 +310,8 @@ function normalizeProfileAppearance(raw) {
|
|
|
292
310
|
wallpaperOpacity: clamp(a.wallpaperOpacity, 0, 1, DEFAULT_CONFIG.wallpaperOpacity),
|
|
293
311
|
blur: clamp(a.blur, 0, 60, DEFAULT_CONFIG.blur),
|
|
294
312
|
chatTextOpacity: clamp(a.chatTextOpacity, 0, 1, DEFAULT_CONFIG.chatTextOpacity),
|
|
295
|
-
trajectoryOpacity: clamp(a.trajectoryOpacity, 0, 1, DEFAULT_CONFIG.trajectoryOpacity)
|
|
313
|
+
trajectoryOpacity: clamp(a.trajectoryOpacity, 0, 1, DEFAULT_CONFIG.trajectoryOpacity),
|
|
314
|
+
panelOpacity: clamp(a.panelOpacity, 0, 1, DEFAULT_CONFIG.panelOpacity)
|
|
296
315
|
};
|
|
297
316
|
}
|
|
298
317
|
function normalizeProfiles(raw) {
|
|
@@ -376,10 +395,12 @@ async function writeConfig(config) {
|
|
|
376
395
|
return false;
|
|
377
396
|
}
|
|
378
397
|
}
|
|
379
|
-
|
|
398
|
+
/** The wallpaper slot is served over HTTP (never shipped as base64 inside the
|
|
399
|
+
* read RPC): the browser decodes it natively through the same pipeline as any
|
|
400
|
+
* <img>, so boot only transfers a tiny URL instead of the whole image. */
|
|
401
|
+
async function wallpaperServeUrl() {
|
|
380
402
|
try {
|
|
381
|
-
|
|
382
|
-
return `data:${sniffImageMime(buf)};base64,${buf.toString("base64")}`;
|
|
403
|
+
return (await stat(wallpaperPath())).size > 0 ? WALLPAPER_ROUTE : null;
|
|
383
404
|
} catch {
|
|
384
405
|
return null;
|
|
385
406
|
}
|
|
@@ -410,15 +431,16 @@ function sniffImageMime(buf) {
|
|
|
410
431
|
return "image/jpeg";
|
|
411
432
|
}
|
|
412
433
|
/** Download a wallpaper from a network URL and persist it into the local
|
|
413
|
-
* wallpaper.jpg slot (replacing whatever was stored), so type switches
|
|
414
|
-
*
|
|
415
|
-
*
|
|
434
|
+
* wallpaper.jpg slot (replacing whatever was stored), so type switches and
|
|
435
|
+
* rotation keep working through the single active slot. The response carries
|
|
436
|
+
* the serve URL, never the bytes. null removes the wallpaper.
|
|
437
|
+
* Returns { ok, wallpaperUrl?, error? }. */
|
|
416
438
|
async function writeWallpaperFromUrl(url) {
|
|
417
439
|
if (url === null) {
|
|
418
440
|
const ok = await writeWallpaper(null);
|
|
419
441
|
return {
|
|
420
442
|
ok,
|
|
421
|
-
|
|
443
|
+
wallpaperUrl: null,
|
|
422
444
|
error: ok ? void 0 : "remove failed"
|
|
423
445
|
};
|
|
424
446
|
}
|
|
@@ -480,13 +502,19 @@ async function writeWallpaperFromUrl(url) {
|
|
|
480
502
|
error: "read failed"
|
|
481
503
|
};
|
|
482
504
|
}
|
|
483
|
-
|
|
484
|
-
|
|
505
|
+
await ensureDir();
|
|
506
|
+
try {
|
|
507
|
+
await writeFile(wallpaperPath(), buf);
|
|
508
|
+
} catch (e) {
|
|
509
|
+
console.error("dsh-any-background: failed to write the downloaded wallpaper", e);
|
|
510
|
+
return {
|
|
511
|
+
ok: false,
|
|
512
|
+
error: "write failed"
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
485
516
|
ok: true,
|
|
486
|
-
|
|
487
|
-
} : {
|
|
488
|
-
ok: false,
|
|
489
|
-
error: "write failed"
|
|
517
|
+
wallpaperUrl: WALLPAPER_ROUTE
|
|
490
518
|
};
|
|
491
519
|
}
|
|
492
520
|
const rotationDir = () => dshHomePath(DATA_DIR, ROTATION_DIR);
|
|
@@ -578,7 +606,8 @@ async function handleRotationRemove(payload) {
|
|
|
578
606
|
};
|
|
579
607
|
}
|
|
580
608
|
/** Activate a rotation item: copy its bytes over the active wallpaper slot and
|
|
581
|
-
* return the
|
|
609
|
+
* return the serve URL so the client applies it live (bytes never round-trip
|
|
610
|
+
* through the RPC response). */
|
|
582
611
|
async function handleRotationSet(payload) {
|
|
583
612
|
const idx = payload?.index;
|
|
584
613
|
if (typeof idx !== "number" || !isFinite(idx)) return {
|
|
@@ -612,12 +641,155 @@ async function handleRotationSet(payload) {
|
|
|
612
641
|
}
|
|
613
642
|
return {
|
|
614
643
|
ok: true,
|
|
615
|
-
|
|
644
|
+
wallpaperUrl: WALLPAPER_ROUTE
|
|
616
645
|
};
|
|
617
646
|
}
|
|
618
647
|
async function videoUrl() {
|
|
619
648
|
return await findVideoFile() ? VIDEO_ROUTE : null;
|
|
620
649
|
}
|
|
650
|
+
/** Guess a video MIME from the URL's path extension (fallback for servers that
|
|
651
|
+
* send no precise Content-Type). */
|
|
652
|
+
function videoMimeFromUrl(u) {
|
|
653
|
+
const p = u.pathname.toLowerCase();
|
|
654
|
+
if (/\.(mp4|m4v)$/.test(p)) return "video/mp4";
|
|
655
|
+
if (/\.webm$/.test(p)) return "video/webm";
|
|
656
|
+
if (/\.(ogg|ogv)$/.test(p)) return "video/ogg";
|
|
657
|
+
if (/\.(mov|qt)$/.test(p)) return "video/quicktime";
|
|
658
|
+
if (/\.(mkv|mk3d|mka)$/.test(p)) return "video/x-matroska";
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
/** Download a background video from a network URL and store it in the video
|
|
662
|
+
* slot (streamed to a temp file — never buffered whole), then record the MIME
|
|
663
|
+
* in the config so findVideoFile/serve resolve immediately. */
|
|
664
|
+
async function writeVideoFromUrl(url) {
|
|
665
|
+
if (url === null) return {
|
|
666
|
+
ok: false,
|
|
667
|
+
error: "invalid url"
|
|
668
|
+
};
|
|
669
|
+
let u;
|
|
670
|
+
try {
|
|
671
|
+
u = new URL(url);
|
|
672
|
+
} catch {
|
|
673
|
+
return {
|
|
674
|
+
ok: false,
|
|
675
|
+
error: "invalid url"
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return {
|
|
679
|
+
ok: false,
|
|
680
|
+
error: "unsupported scheme"
|
|
681
|
+
};
|
|
682
|
+
let res;
|
|
683
|
+
try {
|
|
684
|
+
const ctl = new AbortController();
|
|
685
|
+
const timer = setTimeout(() => ctl.abort(), VIDEO_FETCH_TIMEOUT);
|
|
686
|
+
try {
|
|
687
|
+
res = await fetch(url, {
|
|
688
|
+
redirect: "follow",
|
|
689
|
+
signal: ctl.signal
|
|
690
|
+
});
|
|
691
|
+
} finally {
|
|
692
|
+
clearTimeout(timer);
|
|
693
|
+
}
|
|
694
|
+
} catch (e) {
|
|
695
|
+
return {
|
|
696
|
+
ok: false,
|
|
697
|
+
error: e instanceof Error && e.name === "AbortError" ? "timeout" : "network error"
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
if (!res.ok) return {
|
|
701
|
+
ok: false,
|
|
702
|
+
error: `http ${res.status}`
|
|
703
|
+
};
|
|
704
|
+
let mime = (res.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
|
|
705
|
+
if (mime === "" || mime === "application/octet-stream" || mime === "binary/octet-stream") mime = videoMimeFromUrl(u) ?? "video/mp4";
|
|
706
|
+
if (!mime.startsWith("video/")) return {
|
|
707
|
+
ok: false,
|
|
708
|
+
error: "not a video"
|
|
709
|
+
};
|
|
710
|
+
const declared = Number(res.headers.get("content-length") ?? "");
|
|
711
|
+
if (Number.isFinite(declared) && declared > VIDEO_FETCH_MAX) return {
|
|
712
|
+
ok: false,
|
|
713
|
+
error: "too large"
|
|
714
|
+
};
|
|
715
|
+
await ensureDir();
|
|
716
|
+
const tmp = dshHomePath(DATA_DIR, "video.download.tmp");
|
|
717
|
+
const target = videoPathFor(mime);
|
|
718
|
+
try {
|
|
719
|
+
if (res.body === null) return {
|
|
720
|
+
ok: false,
|
|
721
|
+
error: "empty response"
|
|
722
|
+
};
|
|
723
|
+
const out = createWriteStream(tmp);
|
|
724
|
+
let received = 0;
|
|
725
|
+
let failed = false;
|
|
726
|
+
const fail = () => {
|
|
727
|
+
if (failed) return;
|
|
728
|
+
failed = true;
|
|
729
|
+
out.destroy();
|
|
730
|
+
rm(tmp, { force: true });
|
|
731
|
+
};
|
|
732
|
+
const nodeStream = Readable.fromWeb(res.body);
|
|
733
|
+
let idle = null;
|
|
734
|
+
const pokeIdle = () => {
|
|
735
|
+
if (idle) clearTimeout(idle);
|
|
736
|
+
idle = setTimeout(() => {
|
|
737
|
+
nodeStream.destroy();
|
|
738
|
+
fail();
|
|
739
|
+
}, VIDEO_FETCH_IDLE_TIMEOUT);
|
|
740
|
+
};
|
|
741
|
+
nodeStream.on("data", (chunk) => {
|
|
742
|
+
pokeIdle();
|
|
743
|
+
received += chunk.byteLength;
|
|
744
|
+
if (received > VIDEO_FETCH_MAX) {
|
|
745
|
+
nodeStream.destroy();
|
|
746
|
+
fail();
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
nodeStream.on("end", () => {
|
|
750
|
+
if (idle) clearTimeout(idle);
|
|
751
|
+
idle = null;
|
|
752
|
+
});
|
|
753
|
+
nodeStream.on("aborted", fail);
|
|
754
|
+
nodeStream.on("error", fail);
|
|
755
|
+
out.on("error", fail);
|
|
756
|
+
pokeIdle();
|
|
757
|
+
await new Promise((resolve, reject) => {
|
|
758
|
+
nodeStream.pipe(out);
|
|
759
|
+
out.on("finish", () => resolve());
|
|
760
|
+
out.on("close", () => {
|
|
761
|
+
if (failed) reject(/* @__PURE__ */ new Error("download failed"));
|
|
762
|
+
});
|
|
763
|
+
nodeStream.on("error", () => reject(/* @__PURE__ */ new Error("download failed")));
|
|
764
|
+
});
|
|
765
|
+
if (failed) return {
|
|
766
|
+
ok: false,
|
|
767
|
+
error: "read failed"
|
|
768
|
+
};
|
|
769
|
+
for (const name of VIDEO_CANDIDATES) {
|
|
770
|
+
const p = dshHomePath(DATA_DIR, name);
|
|
771
|
+
if (p !== target) await rm(p, { force: true });
|
|
772
|
+
}
|
|
773
|
+
await rm(target, { force: true });
|
|
774
|
+
await rename(tmp, target);
|
|
775
|
+
const cfg = await readConfig();
|
|
776
|
+
if (cfg.videoMime !== mime) {
|
|
777
|
+
cfg.videoMime = mime;
|
|
778
|
+
await writeConfig(cfg);
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
ok: true,
|
|
782
|
+
mime
|
|
783
|
+
};
|
|
784
|
+
} catch (e) {
|
|
785
|
+
console.error("dsh-any-background: failed to download the background video", e);
|
|
786
|
+
rm(tmp, { force: true });
|
|
787
|
+
return {
|
|
788
|
+
ok: false,
|
|
789
|
+
error: e instanceof Error && e.message === "download failed" ? "read failed" : "write failed"
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
}
|
|
621
793
|
/** Persist a video from a data URL (null removes every variant); only used
|
|
622
794
|
* for removal and small legacy/import payloads. */
|
|
623
795
|
async function writeVideo(dataUrl) {
|
|
@@ -735,9 +907,10 @@ async function handleVideoUpload(req, res) {
|
|
|
735
907
|
}
|
|
736
908
|
try {
|
|
737
909
|
await ensureDir();
|
|
738
|
-
const tmp = dshHomePath(DATA_DIR,
|
|
910
|
+
const tmp = dshHomePath(DATA_DIR, VIDEO_UPLOAD_TMP);
|
|
739
911
|
const target = videoPathFor(mime);
|
|
740
912
|
const out = createWriteStream(tmp);
|
|
913
|
+
let received = 0;
|
|
741
914
|
let failed = false;
|
|
742
915
|
const fail = () => {
|
|
743
916
|
if (failed) return;
|
|
@@ -747,6 +920,13 @@ async function handleVideoUpload(req, res) {
|
|
|
747
920
|
};
|
|
748
921
|
req.on("aborted", fail);
|
|
749
922
|
req.on("error", fail);
|
|
923
|
+
req.on("data", (chunk) => {
|
|
924
|
+
received += chunk.byteLength;
|
|
925
|
+
if (received > VIDEO_UPLOAD_MAX) {
|
|
926
|
+
req.destroy();
|
|
927
|
+
fail();
|
|
928
|
+
}
|
|
929
|
+
});
|
|
750
930
|
out.on("error", () => {
|
|
751
931
|
fail();
|
|
752
932
|
try {
|
|
@@ -764,6 +944,11 @@ async function handleVideoUpload(req, res) {
|
|
|
764
944
|
}
|
|
765
945
|
await rm(target, { force: true });
|
|
766
946
|
await rename(tmp, target);
|
|
947
|
+
const cfg = await readConfig();
|
|
948
|
+
if (cfg.videoMime !== mime) {
|
|
949
|
+
cfg.videoMime = mime;
|
|
950
|
+
await writeConfig(cfg);
|
|
951
|
+
}
|
|
767
952
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
768
953
|
res.end(JSON.stringify({ ok: true }));
|
|
769
954
|
} catch (e) {
|
|
@@ -783,6 +968,131 @@ async function handleVideoUpload(req, res) {
|
|
|
783
968
|
} catch {}
|
|
784
969
|
}
|
|
785
970
|
}
|
|
971
|
+
/** Stream the stored wallpaper: sniffed MIME, no caching (uploads and rotation
|
|
972
|
+
* replace the file in place). */
|
|
973
|
+
async function serveWallpaper(req, res) {
|
|
974
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
975
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
976
|
+
res.end(JSON.stringify({
|
|
977
|
+
ok: false,
|
|
978
|
+
error: "wallpaper route only serves GET/HEAD"
|
|
979
|
+
}));
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
try {
|
|
983
|
+
const st = await stat(wallpaperPath());
|
|
984
|
+
if (st.size === 0) {
|
|
985
|
+
res.writeHead(404);
|
|
986
|
+
res.end();
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const mime = sniffImageMime(await new Promise((resolve, reject) => {
|
|
990
|
+
const chunks = [];
|
|
991
|
+
const s = createReadStream(wallpaperPath(), {
|
|
992
|
+
start: 0,
|
|
993
|
+
end: 15
|
|
994
|
+
});
|
|
995
|
+
s.on("data", (c) => chunks.push(c));
|
|
996
|
+
s.on("end", () => resolve(Buffer.concat(chunks)));
|
|
997
|
+
s.on("error", reject);
|
|
998
|
+
}));
|
|
999
|
+
res.writeHead(200, {
|
|
1000
|
+
"Content-Type": mime,
|
|
1001
|
+
"Content-Length": st.size,
|
|
1002
|
+
"Cache-Control": "no-store"
|
|
1003
|
+
});
|
|
1004
|
+
if (req.method === "HEAD") {
|
|
1005
|
+
res.end();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
createReadStream(wallpaperPath()).pipe(res);
|
|
1009
|
+
} catch {
|
|
1010
|
+
res.writeHead(404);
|
|
1011
|
+
res.end("no wallpaper stored");
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
/** Accept a raw wallpaper upload (POST): pipe the body straight into the
|
|
1015
|
+
* wallpaper slot — no base64 inflation, original pixels preserved. */
|
|
1016
|
+
async function handleWallpaperUpload(req, res) {
|
|
1017
|
+
if (req.method !== "POST") {
|
|
1018
|
+
res.writeHead(405);
|
|
1019
|
+
res.end();
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
if (!(typeof req.headers["content-type"] === "string" ? req.headers["content-type"] : "").split(";")[0].trim().startsWith("image/")) {
|
|
1023
|
+
req.resume();
|
|
1024
|
+
res.writeHead(415, { "Content-Type": "application/json" });
|
|
1025
|
+
res.end(JSON.stringify({
|
|
1026
|
+
ok: false,
|
|
1027
|
+
error: "unsupported media type, expected image/*"
|
|
1028
|
+
}));
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
try {
|
|
1032
|
+
await ensureDir();
|
|
1033
|
+
const tmp = dshHomePath(DATA_DIR, UPLOAD_TMP);
|
|
1034
|
+
const out = createWriteStream(tmp);
|
|
1035
|
+
let received = 0;
|
|
1036
|
+
let failed = false;
|
|
1037
|
+
const fail = () => {
|
|
1038
|
+
if (failed) return;
|
|
1039
|
+
failed = true;
|
|
1040
|
+
out.destroy();
|
|
1041
|
+
rm(tmp, { force: true });
|
|
1042
|
+
};
|
|
1043
|
+
req.on("aborted", fail);
|
|
1044
|
+
req.on("error", fail);
|
|
1045
|
+
out.on("error", () => {
|
|
1046
|
+
fail();
|
|
1047
|
+
try {
|
|
1048
|
+
res.writeHead(500);
|
|
1049
|
+
res.end();
|
|
1050
|
+
} catch {}
|
|
1051
|
+
});
|
|
1052
|
+
req.on("data", (chunk) => {
|
|
1053
|
+
received += chunk.byteLength;
|
|
1054
|
+
if (received > WALLPAPER_UPLOAD_MAX) {
|
|
1055
|
+
req.destroy();
|
|
1056
|
+
fail();
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
req.pipe(out);
|
|
1060
|
+
out.on("finish", async () => {
|
|
1061
|
+
if (failed) return;
|
|
1062
|
+
try {
|
|
1063
|
+
if (received === 0) {
|
|
1064
|
+
fail();
|
|
1065
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1066
|
+
res.end(JSON.stringify({
|
|
1067
|
+
ok: false,
|
|
1068
|
+
error: "empty upload"
|
|
1069
|
+
}));
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
await rm(wallpaperPath(), { force: true });
|
|
1073
|
+
await rename(tmp, wallpaperPath());
|
|
1074
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1075
|
+
res.end(JSON.stringify({
|
|
1076
|
+
ok: true,
|
|
1077
|
+
wallpaperUrl: WALLPAPER_ROUTE
|
|
1078
|
+
}));
|
|
1079
|
+
} catch (e) {
|
|
1080
|
+
console.error("dsh-any-background: failed to finalize the wallpaper upload", e);
|
|
1081
|
+
rm(tmp, { force: true });
|
|
1082
|
+
try {
|
|
1083
|
+
res.writeHead(500);
|
|
1084
|
+
res.end();
|
|
1085
|
+
} catch {}
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
} catch (e) {
|
|
1089
|
+
console.error("dsh-any-background: failed to accept the wallpaper upload", e);
|
|
1090
|
+
try {
|
|
1091
|
+
res.writeHead(500);
|
|
1092
|
+
res.end();
|
|
1093
|
+
} catch {}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
786
1096
|
const NS = "dshAnyBackground";
|
|
787
1097
|
const RPC_CHANNEL = "/dsh-any-background";
|
|
788
1098
|
const RPC_BODY_MAX = 314572800;
|
|
@@ -841,7 +1151,7 @@ async function handleRpcMethod(endpoint, payload) {
|
|
|
841
1151
|
ok: true,
|
|
842
1152
|
value: {
|
|
843
1153
|
config: await readConfig(),
|
|
844
|
-
|
|
1154
|
+
wallpaperUrl: await wallpaperServeUrl(),
|
|
845
1155
|
videoUrl: await videoUrl(),
|
|
846
1156
|
rotated
|
|
847
1157
|
}
|
|
@@ -863,6 +1173,10 @@ async function handleRpcMethod(endpoint, payload) {
|
|
|
863
1173
|
ok: true,
|
|
864
1174
|
value: await writeWallpaperFromUrl(payload?.url ?? null)
|
|
865
1175
|
};
|
|
1176
|
+
case "setVideoUrl": return {
|
|
1177
|
+
ok: true,
|
|
1178
|
+
value: await writeVideoFromUrl(payload?.url ?? null)
|
|
1179
|
+
};
|
|
866
1180
|
case "rotationAdd": return {
|
|
867
1181
|
ok: true,
|
|
868
1182
|
value: await handleRotationAdd(payload)
|
|
@@ -991,6 +1305,15 @@ function apply(ctx) {
|
|
|
991
1305
|
}));
|
|
992
1306
|
}
|
|
993
1307
|
}), "dsh-any-background: rpc channel");
|
|
1308
|
+
const fenceUpload = (handler) => (req, res) => {
|
|
1309
|
+
const rejection = webCtx.connection.requestRejection(req);
|
|
1310
|
+
if (rejection !== void 0) {
|
|
1311
|
+
res.writeHead(rejection);
|
|
1312
|
+
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
handler(req, res);
|
|
1316
|
+
};
|
|
994
1317
|
webCtx.effect(() => webCtx.webServer.register({
|
|
995
1318
|
kind: "prefix",
|
|
996
1319
|
path: VIDEO_ROUTE,
|
|
@@ -999,8 +1322,18 @@ function apply(ctx) {
|
|
|
999
1322
|
webCtx.effect(() => webCtx.webServer.register({
|
|
1000
1323
|
kind: "exact",
|
|
1001
1324
|
path: UPLOAD_ROUTE,
|
|
1002
|
-
handler: handleVideoUpload
|
|
1325
|
+
handler: fenceUpload(handleVideoUpload)
|
|
1003
1326
|
}), "dsh-any-background: upload route");
|
|
1327
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
1328
|
+
kind: "prefix",
|
|
1329
|
+
path: WALLPAPER_ROUTE,
|
|
1330
|
+
handler: serveWallpaper
|
|
1331
|
+
}), "dsh-any-background: wallpaper route");
|
|
1332
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
1333
|
+
kind: "exact",
|
|
1334
|
+
path: WALLPAPER_UPLOAD_ROUTE,
|
|
1335
|
+
handler: fenceUpload(handleWallpaperUpload)
|
|
1336
|
+
}), "dsh-any-background: wallpaper upload route");
|
|
1004
1337
|
});
|
|
1005
1338
|
}
|
|
1006
1339
|
//#endregion
|