beauticode-dsh 1.0.0
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.zh-CN.md +48 -0
- package/agent.mjs +595 -0
- package/atmosphere.js +306 -0
- package/bin/beauticode-dsh +219 -0
- package/cli.js +219 -0
- package/client.js +370 -0
- package/console.js +303 -0
- package/control-client.mjs +445 -0
- package/cordis.patch.yml +4 -0
- package/host-apply.mjs +163 -0
- package/index.mjs +570 -0
- package/package.json +56 -0
- package/presets.mjs +73 -0
- package/themes/internal-beyond/NOTICE.md +9 -0
- package/themes/internal-beyond/bg-canvas-4k.png +0 -0
- package/themes/internal-beyond/bg-canvas.png +0 -0
- package/themes/internal-beyond/bg-infernal.jpg +0 -0
- package/themes/internal-beyond/bg-internal.jpg +0 -0
- package/ui-host.mjs +292 -0
- package/vendor/adapter-dsh/bridge.js +215 -0
- package/vendor/adapter-dsh/host-descriptor.js +15 -0
- package/vendor/adapter-dsh/index.js +6 -0
- package/vendor/adapter-dsh/injector-lock.js +27 -0
- package/vendor/adapter-dsh/package.json +5 -0
- package/vendor/adapter-dsh/session.js +527 -0
- package/vendor/adapter-dsh/token.js +46 -0
- package/vendor/adapter-dsh/tray-handoff.js +54 -0
- package/vendor/core/apply-transaction.js +287 -0
- package/vendor/core/background-store.js +1116 -0
- package/vendor/core/bundled-gallery.js +53 -0
- package/vendor/core/constants.js +47 -0
- package/vendor/core/error-message.js +208 -0
- package/vendor/core/file-lock.js +151 -0
- package/vendor/core/host-session.js +2 -0
- package/vendor/core/index.js +12 -0
- package/vendor/core/media-server.js +544 -0
- package/vendor/core/media-validation.js +244 -0
- package/vendor/core/package.json +5 -0
- package/vendor/core/paths.js +168 -0
- package/vendor/core/types.js +15 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { IMAGE_EXTENSIONS, MAX_IMAGE_BYTES, MAX_VIDEO_BYTES, VIDEO_EXTENSION, } from "./constants.js";
|
|
5
|
+
const JPEG_MAGIC = Buffer.from([0xff, 0xd8, 0xff]);
|
|
6
|
+
const PNG_MAGIC = Buffer.from([
|
|
7
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
8
|
+
]);
|
|
9
|
+
// WEBP: RIFF....WEBP
|
|
10
|
+
function isWebp(buf) {
|
|
11
|
+
return (buf.length >= 12 &&
|
|
12
|
+
buf.subarray(0, 4).toString("ascii") === "RIFF" &&
|
|
13
|
+
buf.subarray(8, 12).toString("ascii") === "WEBP");
|
|
14
|
+
}
|
|
15
|
+
/** AVIF is an ISO-BMFF image. Some downloaders save it with a .png suffix. */
|
|
16
|
+
export function isAvifContainer(bytes, totalSize = bytes?.byteLength ?? 0) {
|
|
17
|
+
if (!(bytes instanceof Uint8Array) || bytes.length < 16)
|
|
18
|
+
return false;
|
|
19
|
+
const view = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
20
|
+
const firstBoxSize = view.readUInt32BE(0);
|
|
21
|
+
if (firstBoxSize < 16 ||
|
|
22
|
+
firstBoxSize > totalSize ||
|
|
23
|
+
view.subarray(4, 8).toString("ascii") !== "ftyp") {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const brands = [view.subarray(8, 12)];
|
|
27
|
+
for (let offset = 16; offset + 4 <= Math.min(firstBoxSize, view.length); offset += 4) {
|
|
28
|
+
brands.push(view.subarray(offset, offset + 4));
|
|
29
|
+
}
|
|
30
|
+
return brands.some((brand) => brand.toString("ascii") === "avif" || brand.toString("ascii") === "avis");
|
|
31
|
+
}
|
|
32
|
+
export class MediaValidationError extends Error {
|
|
33
|
+
constructor(message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "MediaValidationError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function assertNoSymbolicLinkSegments(filePath) {
|
|
39
|
+
const root = path.parse(filePath).root;
|
|
40
|
+
const relative = path.relative(root, filePath);
|
|
41
|
+
let current = root;
|
|
42
|
+
for (const segment of relative.split(path.sep)) {
|
|
43
|
+
if (!segment)
|
|
44
|
+
continue;
|
|
45
|
+
current = path.join(current, segment);
|
|
46
|
+
const segmentStat = await fs.lstat(current);
|
|
47
|
+
if (segmentStat.isSymbolicLink()) {
|
|
48
|
+
throw new MediaValidationError("Media path must not traverse a symbolic link or reparse point.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function isMp4Container(bytes, totalSize = bytes?.byteLength ?? 0) {
|
|
53
|
+
if (!(bytes instanceof Uint8Array) || bytes.length < 16)
|
|
54
|
+
return false;
|
|
55
|
+
const view = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
56
|
+
const firstBoxSize = view.readUInt32BE(0);
|
|
57
|
+
return (firstBoxSize >= 16 &&
|
|
58
|
+
firstBoxSize <= totalSize &&
|
|
59
|
+
view.subarray(4, 8).toString("ascii") === "ftyp");
|
|
60
|
+
}
|
|
61
|
+
async function assertRegularFile(filePath) {
|
|
62
|
+
const resolvedInput = path.resolve(filePath);
|
|
63
|
+
let lstat;
|
|
64
|
+
try {
|
|
65
|
+
lstat = await fs.lstat(resolvedInput);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new MediaValidationError(`Media file not found: ${resolvedInput}`);
|
|
69
|
+
}
|
|
70
|
+
if (lstat.isSymbolicLink()) {
|
|
71
|
+
throw new MediaValidationError("Media file must not be a symbolic link.");
|
|
72
|
+
}
|
|
73
|
+
// On Windows, reject reparse points that are not ordinary files.
|
|
74
|
+
// lstat.isSymbolicLink covers symlinks; directory junctions won't be files.
|
|
75
|
+
const realPath = await fs.realpath(resolvedInput);
|
|
76
|
+
const normalizeForCompare = (value) => {
|
|
77
|
+
const resolved = path.resolve(value);
|
|
78
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
79
|
+
};
|
|
80
|
+
if (normalizeForCompare(realPath) !== normalizeForCompare(resolvedInput)) {
|
|
81
|
+
// realpath can also expand a harmless Windows 8.3 path alias. Inspect each
|
|
82
|
+
// segment so only an actual symlink or junction is rejected.
|
|
83
|
+
await assertNoSymbolicLinkSegments(resolvedInput);
|
|
84
|
+
}
|
|
85
|
+
const stat = await fs.stat(realPath);
|
|
86
|
+
if (!stat.isFile()) {
|
|
87
|
+
throw new MediaValidationError("Media path must be a regular file.");
|
|
88
|
+
}
|
|
89
|
+
return { resolved: realPath, size: stat.size };
|
|
90
|
+
}
|
|
91
|
+
async function inspectAndHash(filePath, expectedSize) {
|
|
92
|
+
const handle = await fs.open(filePath, "r");
|
|
93
|
+
try {
|
|
94
|
+
const before = await handle.stat();
|
|
95
|
+
if (!before.isFile() || before.size !== expectedSize) {
|
|
96
|
+
throw new MediaValidationError("Media file changed during validation.");
|
|
97
|
+
}
|
|
98
|
+
const head = Buffer.alloc(Math.min(64, expectedSize));
|
|
99
|
+
const hash = createHash("sha256");
|
|
100
|
+
const chunk = Buffer.allocUnsafe(1024 * 1024);
|
|
101
|
+
let position = 0;
|
|
102
|
+
let headOffset = 0;
|
|
103
|
+
while (position < expectedSize) {
|
|
104
|
+
const length = Math.min(chunk.byteLength, expectedSize - position);
|
|
105
|
+
const { bytesRead } = await handle.read(chunk, 0, length, position);
|
|
106
|
+
if (bytesRead <= 0) {
|
|
107
|
+
throw new MediaValidationError("Media file ended during validation.");
|
|
108
|
+
}
|
|
109
|
+
const bytes = chunk.subarray(0, bytesRead);
|
|
110
|
+
hash.update(bytes);
|
|
111
|
+
if (headOffset < head.byteLength) {
|
|
112
|
+
const take = Math.min(bytesRead, head.byteLength - headOffset);
|
|
113
|
+
bytes.copy(head, headOffset, 0, take);
|
|
114
|
+
headOffset += take;
|
|
115
|
+
}
|
|
116
|
+
position += bytesRead;
|
|
117
|
+
}
|
|
118
|
+
const after = await handle.stat();
|
|
119
|
+
if (after.size !== before.size ||
|
|
120
|
+
after.mtimeMs !== before.mtimeMs ||
|
|
121
|
+
after.ctimeMs !== before.ctimeMs) {
|
|
122
|
+
throw new MediaValidationError("Media file changed during validation.");
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
head,
|
|
126
|
+
identity: hash.digest("hex"),
|
|
127
|
+
device: after.dev,
|
|
128
|
+
inode: after.ino,
|
|
129
|
+
mtimeMs: after.mtimeMs,
|
|
130
|
+
ctimeMs: after.ctimeMs,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await handle.close();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export function detectImageMime(buf, extension, totalSize = buf.byteLength) {
|
|
138
|
+
if (buf.length >= 3 && buf.subarray(0, 3).equals(JPEG_MAGIC)) {
|
|
139
|
+
if (extension === ".jpg" || extension === ".jpeg") {
|
|
140
|
+
return { mime: "image/jpeg" };
|
|
141
|
+
}
|
|
142
|
+
// Allow jpeg magic even if extension is jpeg family only
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
if (buf.length >= 8 && buf.subarray(0, 8).equals(PNG_MAGIC)) {
|
|
146
|
+
if (extension === ".png")
|
|
147
|
+
return { mime: "image/png" };
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
if (isWebp(buf)) {
|
|
151
|
+
if (extension === ".webp")
|
|
152
|
+
return { mime: "image/webp" };
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
// Keep compatibility with files such as the supplied "*.png" whose
|
|
156
|
+
// contents are AVIF. Do not make unrelated JPEG/PNG/WebP mismatches valid.
|
|
157
|
+
if (isAvifContainer(buf, totalSize)) {
|
|
158
|
+
if (extension === ".avif" || extension === ".png") {
|
|
159
|
+
return { mime: "image/avif" };
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
export async function validateImageFile(filePath, opts = {}) {
|
|
166
|
+
const maxBytes = opts.maxBytes ?? MAX_IMAGE_BYTES;
|
|
167
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
168
|
+
if (!IMAGE_EXTENSIONS.includes(ext)) {
|
|
169
|
+
throw new MediaValidationError(`Image must use one of: ${IMAGE_EXTENSIONS.join(", ")}`);
|
|
170
|
+
}
|
|
171
|
+
const { resolved, size } = await assertRegularFile(filePath);
|
|
172
|
+
if (size < 1 || size > maxBytes) {
|
|
173
|
+
throw new MediaValidationError(`Image must be a non-empty file no larger than ${maxBytes} bytes.`);
|
|
174
|
+
}
|
|
175
|
+
const inspected = await inspectAndHash(resolved, size);
|
|
176
|
+
const detected = detectImageMime(inspected.head, ext, size);
|
|
177
|
+
if (!detected) {
|
|
178
|
+
throw new MediaValidationError("Image content does not match a supported JPEG/PNG/WEBP/AVIF signature.");
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
kind: "image",
|
|
182
|
+
filePath: resolved,
|
|
183
|
+
size,
|
|
184
|
+
extension: ext,
|
|
185
|
+
mime: detected.mime,
|
|
186
|
+
identity: inspected.identity,
|
|
187
|
+
device: inspected.device,
|
|
188
|
+
inode: inspected.inode,
|
|
189
|
+
mtimeMs: inspected.mtimeMs,
|
|
190
|
+
ctimeMs: inspected.ctimeMs,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
export async function validateVideoFile(filePath, opts = {}) {
|
|
194
|
+
const maxBytes = opts.maxBytes ?? MAX_VIDEO_BYTES;
|
|
195
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
196
|
+
if (ext !== VIDEO_EXTENSION) {
|
|
197
|
+
throw new MediaValidationError("Video backgrounds must use an MP4 file.");
|
|
198
|
+
}
|
|
199
|
+
const { resolved, size } = await assertRegularFile(filePath);
|
|
200
|
+
if (size < 1 || size > maxBytes) {
|
|
201
|
+
throw new MediaValidationError(`Video background must be a non-empty MP4 no larger than ${maxBytes} bytes.`);
|
|
202
|
+
}
|
|
203
|
+
const inspected = await inspectAndHash(resolved, size);
|
|
204
|
+
if (!isMp4Container(inspected.head, size)) {
|
|
205
|
+
throw new MediaValidationError("Video background is not a valid MP4 container.");
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
kind: "video",
|
|
209
|
+
filePath: resolved,
|
|
210
|
+
size,
|
|
211
|
+
identity: inspected.identity,
|
|
212
|
+
device: inspected.device,
|
|
213
|
+
inode: inspected.inode,
|
|
214
|
+
mtimeMs: inspected.mtimeMs,
|
|
215
|
+
ctimeMs: inspected.ctimeMs,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/** Basename-only guard for names stored inside active/staging trees. */
|
|
219
|
+
export function assertSafeBasename(name, label) {
|
|
220
|
+
if (typeof name !== "string" || name.length < 1 || name.length > 128) {
|
|
221
|
+
throw new MediaValidationError(`${label} must be a basename string.`);
|
|
222
|
+
}
|
|
223
|
+
if (name !== path.basename(name) || name === "." || name === "..") {
|
|
224
|
+
throw new MediaValidationError(`${label} must be a basename only.`);
|
|
225
|
+
}
|
|
226
|
+
if (name.includes("/") || name.includes("\\") || name.includes("\0")) {
|
|
227
|
+
throw new MediaValidationError(`${label} contains illegal path characters.`);
|
|
228
|
+
}
|
|
229
|
+
if (/[<>:"|?*]/.test(name) || /[. ]$/.test(name)) {
|
|
230
|
+
throw new MediaValidationError(`${label} contains characters invalid in Windows basenames.`);
|
|
231
|
+
}
|
|
232
|
+
const stem = name.split(".", 1)[0] ?? "";
|
|
233
|
+
if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(stem)) {
|
|
234
|
+
throw new MediaValidationError(`${label} uses a reserved device name.`);
|
|
235
|
+
}
|
|
236
|
+
for (let i = 0; i < name.length; i += 1) {
|
|
237
|
+
const code = name.charCodeAt(i);
|
|
238
|
+
if (code < 32 || code === 127) {
|
|
239
|
+
throw new MediaValidationError(`${label} contains control characters.`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return name;
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=media-validation.js.map
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import { ACTIVE_DIR_NAME, MANIFEST_NAME, RUNTIME_MEDIA_DIR_NAME, SAVED_DIR_NAME, SCHEMA_ID, SNAPSHOTS_DIR_NAME, STAGING_DIR_NAME, } from "./constants.js";
|
|
6
|
+
const DATA_ROOT_MARKER_NAME = ".beauticode-root.json";
|
|
7
|
+
const DATA_ROOT_SCHEMA = "beauticode.data-root/v1";
|
|
8
|
+
export function defaultDataRoot() {
|
|
9
|
+
if (process.platform === "win32") {
|
|
10
|
+
const base = process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local");
|
|
11
|
+
return path.join(base, "beautiCode");
|
|
12
|
+
}
|
|
13
|
+
if (process.platform === "darwin") {
|
|
14
|
+
return path.join(os.homedir(), "Library", "Application Support", "beautiCode");
|
|
15
|
+
}
|
|
16
|
+
const xdg = process.env.XDG_DATA_HOME ?? path.join(os.homedir(), ".local", "share");
|
|
17
|
+
return path.join(xdg, "beautiCode");
|
|
18
|
+
}
|
|
19
|
+
export function resolveDataPaths(root = defaultDataRoot()) {
|
|
20
|
+
const resolved = path.resolve(root);
|
|
21
|
+
return {
|
|
22
|
+
root: resolved,
|
|
23
|
+
activeDir: path.join(resolved, ACTIVE_DIR_NAME),
|
|
24
|
+
stagingDir: path.join(resolved, STAGING_DIR_NAME),
|
|
25
|
+
snapshotsDir: path.join(resolved, SNAPSHOTS_DIR_NAME),
|
|
26
|
+
savedDir: path.join(resolved, SAVED_DIR_NAME),
|
|
27
|
+
runtimeMediaDir: path.join(resolved, RUNTIME_MEDIA_DIR_NAME),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function ensureDataLayout(paths) {
|
|
31
|
+
let rootExists = true;
|
|
32
|
+
try {
|
|
33
|
+
const stat = await fs.lstat(paths.root);
|
|
34
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
35
|
+
throw new Error("beautiCode data root must be a real directory.");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const code = error && typeof error === "object" && "code" in error
|
|
40
|
+
? String(error.code)
|
|
41
|
+
: "";
|
|
42
|
+
if (code !== "ENOENT")
|
|
43
|
+
throw error;
|
|
44
|
+
rootExists = false;
|
|
45
|
+
await fs.mkdir(paths.root, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
const markerPath = path.join(paths.root, DATA_ROOT_MARKER_NAME);
|
|
48
|
+
let markerValid = false;
|
|
49
|
+
try {
|
|
50
|
+
const raw = await fs.readFile(markerPath, "utf8");
|
|
51
|
+
const marker = JSON.parse(raw);
|
|
52
|
+
markerValid = marker?.schema === DATA_ROOT_SCHEMA;
|
|
53
|
+
if (!markerValid) {
|
|
54
|
+
throw new Error("beautiCode data-root ownership marker is invalid.");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const code = error && typeof error === "object" && "code" in error
|
|
59
|
+
? String(error.code)
|
|
60
|
+
: "";
|
|
61
|
+
if (code !== "ENOENT")
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
if (!markerValid) {
|
|
65
|
+
const entries = rootExists ? await fs.readdir(paths.root) : [];
|
|
66
|
+
const allowedLegacyEntry = (entry) => entry === ACTIVE_DIR_NAME ||
|
|
67
|
+
entry === STAGING_DIR_NAME ||
|
|
68
|
+
entry === SNAPSHOTS_DIR_NAME ||
|
|
69
|
+
entry === SAVED_DIR_NAME ||
|
|
70
|
+
entry === RUNTIME_MEDIA_DIR_NAME ||
|
|
71
|
+
entry === "logs" ||
|
|
72
|
+
entry === "engine-launcher.log" ||
|
|
73
|
+
entry === "injector.lock" ||
|
|
74
|
+
entry === "store.lock" ||
|
|
75
|
+
entry === "dsh-control.json" ||
|
|
76
|
+
entry === "session-host.json" ||
|
|
77
|
+
entry === "tray-claim.json" ||
|
|
78
|
+
entry === "dsh-bridge.token" ||
|
|
79
|
+
entry === ".beauticode-commit.json" ||
|
|
80
|
+
entry === ".beauticode-commit-in-progress" ||
|
|
81
|
+
entry.startsWith("active-backup-");
|
|
82
|
+
let hasLegacyManifest = false;
|
|
83
|
+
try {
|
|
84
|
+
const raw = await fs.readFile(path.join(paths.activeDir, MANIFEST_NAME), "utf8");
|
|
85
|
+
const manifest = JSON.parse(raw);
|
|
86
|
+
hasLegacyManifest = manifest?.schema === SCHEMA_ID;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
hasLegacyManifest = false;
|
|
90
|
+
}
|
|
91
|
+
if (entries.length > 0 &&
|
|
92
|
+
!hasLegacyManifest &&
|
|
93
|
+
!entries.every(allowedLegacyEntry)) {
|
|
94
|
+
throw new Error("Refusing to adopt a non-empty directory without beautiCode data. Choose an empty --data-root.");
|
|
95
|
+
}
|
|
96
|
+
const marker = `${JSON.stringify({
|
|
97
|
+
schema: DATA_ROOT_SCHEMA,
|
|
98
|
+
createdAt: new Date().toISOString(),
|
|
99
|
+
}, null, 2)}\n`;
|
|
100
|
+
try {
|
|
101
|
+
const handle = await fs.open(markerPath, "wx", 0o600);
|
|
102
|
+
try {
|
|
103
|
+
await handle.writeFile(marker, "utf8");
|
|
104
|
+
await handle.sync();
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await handle.close();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
const code = error && typeof error === "object" && "code" in error
|
|
112
|
+
? String(error.code)
|
|
113
|
+
: "";
|
|
114
|
+
if (code !== "EEXIST")
|
|
115
|
+
throw error;
|
|
116
|
+
const raw = await fs.readFile(markerPath, "utf8");
|
|
117
|
+
const existing = JSON.parse(raw);
|
|
118
|
+
if (existing?.schema !== DATA_ROOT_SCHEMA) {
|
|
119
|
+
throw new Error("beautiCode data-root ownership marker is invalid.");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
await fs.mkdir(paths.stagingDir, { recursive: true });
|
|
124
|
+
await fs.mkdir(paths.snapshotsDir, { recursive: true });
|
|
125
|
+
await fs.mkdir(paths.savedDir, { recursive: true });
|
|
126
|
+
await fs.mkdir(paths.runtimeMediaDir, { recursive: true });
|
|
127
|
+
}
|
|
128
|
+
/** True iff `candidate` is equal to or strictly inside `root`. */
|
|
129
|
+
export function isPathInsideRoot(root, candidate) {
|
|
130
|
+
const rootResolved = path.resolve(root);
|
|
131
|
+
const candResolved = path.resolve(candidate);
|
|
132
|
+
if (rootResolved === candResolved)
|
|
133
|
+
return true;
|
|
134
|
+
const rel = path.relative(rootResolved, candResolved);
|
|
135
|
+
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
136
|
+
}
|
|
137
|
+
export async function rmrf(target) {
|
|
138
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
export async function emptyDir(target) {
|
|
141
|
+
await rmrf(target);
|
|
142
|
+
await fs.mkdir(target, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
export async function copyFileAtomic(source, destination) {
|
|
145
|
+
const dir = path.dirname(destination);
|
|
146
|
+
await fs.mkdir(dir, { recursive: true });
|
|
147
|
+
const tmp = `${destination}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
148
|
+
await fs.copyFile(source, tmp);
|
|
149
|
+
await fs.rename(tmp, destination);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Snapshot helper: use a same-volume hard link to avoid copying large media,
|
|
153
|
+
* with an atomic-copy fallback for filesystems that do not support links.
|
|
154
|
+
*/
|
|
155
|
+
export async function linkOrCopyFileAtomic(source, destination) {
|
|
156
|
+
const dir = path.dirname(destination);
|
|
157
|
+
await fs.mkdir(dir, { recursive: true });
|
|
158
|
+
const tmp = `${destination}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
159
|
+
try {
|
|
160
|
+
await fs.link(source, tmp);
|
|
161
|
+
await fs.rename(tmp, destination);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
await fs.rm(tmp, { force: true }).catch(() => { });
|
|
165
|
+
await copyFileAtomic(source, destination);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function normalizeBackgroundEffects(value) {
|
|
2
|
+
if (!value || typeof value !== "object")
|
|
3
|
+
return null;
|
|
4
|
+
const preset = value.preset;
|
|
5
|
+
if (preset !== "internal" &&
|
|
6
|
+
preset !== "infernal" &&
|
|
7
|
+
preset !== "gallery") {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return { preset, rain: true, overlay: true, water: true };
|
|
11
|
+
}
|
|
12
|
+
export function effectsForPreset(preset) {
|
|
13
|
+
return { preset, rain: true, overlay: true, water: true };
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=types.js.map
|