@dotobokuri/fleet-console 1.44.0 → 1.45.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/dist/cli.d.ts +1 -1
- package/dist/cli.mjs +3088 -3139
- package/dist/client/assets/{_baseUniq-DfRTNtWd.js → _baseUniq-DtJH2bh8.js} +1 -1
- package/dist/client/assets/{arc-Ds9vyD8n.js → arc-DHCKMqqF.js} +1 -1
- package/dist/client/assets/{architectureDiagram-Q4EWVU46-pZ_1tCpx.js → architectureDiagram-Q4EWVU46-BOuyINL8.js} +1 -1
- package/dist/client/assets/{blockDiagram-DXYQGD6D-nntXWaQ9.js → blockDiagram-DXYQGD6D-cIre3_5n.js} +1 -1
- package/dist/client/assets/{c4Diagram-AHTNJAMY-AHAd4NjZ.js → c4Diagram-AHTNJAMY-DTX-qjSW.js} +1 -1
- package/dist/client/assets/channel-BhciS8jR.js +1 -0
- package/dist/client/assets/{chunk-4BX2VUAB-BNfWXMb-.js → chunk-4BX2VUAB-DiBwpozK.js} +1 -1
- package/dist/client/assets/{chunk-4TB4RGXK-B5MiBj0N.js → chunk-4TB4RGXK-uCZ-B3-y.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-9vJ9AfWn.js → chunk-55IACEB6-CmBu2sf0.js} +1 -1
- package/dist/client/assets/{chunk-EDXVE4YY-CpxmITfr.js → chunk-EDXVE4YY-BXpw2VyB.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-BoUi1EcN.js → chunk-FMBD7UC4-Cn9sKTn5.js} +1 -1
- package/dist/client/assets/{chunk-OYMX7WX6-s7qMNTfq.js → chunk-OYMX7WX6-BDgH1zlW.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-Bdgo-JE9.js → chunk-QZHKN3VN-BOdvRibu.js} +1 -1
- package/dist/client/assets/{chunk-YZCP3GAM-C9RCb0eu.js → chunk-YZCP3GAM-BDIEapcU.js} +1 -1
- package/dist/client/assets/classDiagram-6PBFFD2Q-BjRUnHce.js +1 -0
- package/dist/client/assets/classDiagram-v2-HSJHXN6E-BjRUnHce.js +1 -0
- package/dist/client/assets/clone-DygBt67n.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-Mr2xbCPD.js → cose-bilkent-S5V4N54A-BVlc3Dad.js} +1 -1
- package/dist/client/assets/{dagre-KV5264BT-xIfrxsrb.js → dagre-KV5264BT-raV4BK6w.js} +1 -1
- package/dist/client/assets/{diagram-5BDNPKRD-BM49JRbq.js → diagram-5BDNPKRD-BMAh1LJt.js} +1 -1
- package/dist/client/assets/{diagram-G4DWMVQ6-uMduxuqy.js → diagram-G4DWMVQ6-AM5t2f3j.js} +1 -1
- package/dist/client/assets/{diagram-MMDJMWI5-BfCnnu9a.js → diagram-MMDJMWI5-DMw_UwWR.js} +1 -1
- package/dist/client/assets/{diagram-TYMM5635-DTVL_WEx.js → diagram-TYMM5635-CiDAD3e9.js} +1 -1
- package/dist/client/assets/{erDiagram-SMLLAGMA-BUQDjGU2.js → erDiagram-SMLLAGMA-D1IXSwFZ.js} +1 -1
- package/dist/client/assets/{flowDiagram-DWJPFMVM-RZ85tRp5.js → flowDiagram-DWJPFMVM-pEq1L6HP.js} +1 -1
- package/dist/client/assets/{ganttDiagram-T4ZO3ILL-CenzFyys.js → ganttDiagram-T4ZO3ILL-BAXTU16v.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-UUTBAWPF-BpkFTm7f.js → gitGraphDiagram-UUTBAWPF-PkEvpSio.js} +1 -1
- package/dist/client/assets/{graph-BlyDgw2r.js → graph-OOLam9NE.js} +1 -1
- package/dist/client/assets/{index-BmGY-BNs.css → index-BgHFwZy9.css} +1 -1
- package/dist/client/assets/{index-BAMerP8r.js → index-CvXOp1di.js} +68 -68
- package/dist/client/assets/{infoDiagram-42DDH7IO-ZB4pKXQT.js → infoDiagram-42DDH7IO-CcLveXnw.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-UXIWVN3A-BfXl0ksQ.js → ishikawaDiagram-UXIWVN3A-D-p_eX0W.js} +1 -1
- package/dist/client/assets/{journeyDiagram-VCZTEJTY-DCSluqxm.js → journeyDiagram-VCZTEJTY-D81ht6ZY.js} +1 -1
- package/dist/client/assets/{kanban-definition-6JOO6SKY-5X3OxziR.js → kanban-definition-6JOO6SKY-D8SIIBWy.js} +1 -1
- package/dist/client/assets/{layout-C2Jxamqz.js → layout-CeMy87tl.js} +1 -1
- package/dist/client/assets/{linear-DgO5otCG.js → linear-C0Txkuhf.js} +1 -1
- package/dist/client/assets/{mermaid.core-CqKupZd5.js → mermaid.core-zdTcCzmD.js} +4 -4
- package/dist/client/assets/{min-B9jNVA7s.js → min-BPp_9Vnv.js} +1 -1
- package/dist/client/assets/{mindmap-definition-QFDTVHPH-B_Clh0rq.js → mindmap-definition-QFDTVHPH-DRJbMf8W.js} +1 -1
- package/dist/client/assets/{pieDiagram-DEJITSTG-D1rR7mwo.js → pieDiagram-DEJITSTG-CqPISwE-.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-34T5L4WZ-CGnU5k3k.js → quadrantDiagram-34T5L4WZ-BGLaujDD.js} +1 -1
- package/dist/client/assets/{requirementDiagram-MS252O5E-BDFOsklw.js → requirementDiagram-MS252O5E-DWF6sFQ_.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-XADWPNL6-DT-8dpb6.js → sankeyDiagram-XADWPNL6-Dv2O23aj.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-FGHM5R23-3IY8NxLM.js → sequenceDiagram-FGHM5R23-BXgYtweg.js} +1 -1
- package/dist/client/assets/{stateDiagram-FHFEXIEX-DqaGlW3D.js → stateDiagram-FHFEXIEX-COtDK2gC.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-BzCFtEfV.js +1 -0
- package/dist/client/assets/{timeline-definition-GMOUNBTQ-DmOlxq0r.js → timeline-definition-GMOUNBTQ-B_-QYLHK.js} +1 -1
- package/dist/client/assets/{vennDiagram-DHZGUBPP-UsUm6iWZ.js → vennDiagram-DHZGUBPP-BNVDRgAa.js} +1 -1
- package/dist/client/assets/{wardley-RL74JXVD-C0JK7zQx.js → wardley-RL74JXVD-B3XwoW1A.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-NUSXRM2D-pFHz6DzF.js → wardleyDiagram-NUSXRM2D-B6-OKLa-.js} +1 -1
- package/dist/client/assets/{xychartDiagram-5P7HB3ND-Cgy9Igjb.js → xychartDiagram-5P7HB3ND-DepfDqrZ.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/fleet-plugins/file-explorer/routes.mjs +647 -648
- package/dist/fleet-plugins/ledger/routes.mjs +1 -5
- package/dist/fleet-plugins/quota/routes.mjs +169 -157
- package/dist/fleet-plugins/repository/routes.mjs +0 -2
- package/dist/fleet-plugins/scuttlebutt/routes.mjs +1 -5
- package/dist/fleet-plugins/skills/routes.mjs +1 -5
- package/dist/fleet-plugins/terminal/routes.mjs +5865 -6684
- package/package.json +1 -1
- package/dist/client/assets/channel-D_VyV3Fr.js +0 -1
- package/dist/client/assets/classDiagram-6PBFFD2Q-TBLL_03g.js +0 -1
- package/dist/client/assets/classDiagram-v2-HSJHXN6E-TBLL_03g.js +0 -1
- package/dist/client/assets/clone-CYDDwEhh.js +0 -1
- package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-2tlPZvTx.js +0 -1
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFile, spawn } from 'child_process';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
3
|
+
import fs2, { constants } from 'fs';
|
|
4
|
+
import fsp from 'fs/promises';
|
|
5
|
+
import path5 from 'path';
|
|
6
6
|
|
|
7
7
|
// sdk/plugin/node.ts
|
|
8
8
|
function definePlugin(definition) {
|
|
9
9
|
return definition;
|
|
10
10
|
}
|
|
11
|
-
function registerRouter(ctx,
|
|
12
|
-
ctx.registerRouter(normalizePath(
|
|
11
|
+
function registerRouter(ctx, path6, handler) {
|
|
12
|
+
ctx.registerRouter(normalizePath(path6), handler);
|
|
13
13
|
}
|
|
14
|
-
function normalizePath(
|
|
15
|
-
if (
|
|
16
|
-
return
|
|
14
|
+
function normalizePath(path6) {
|
|
15
|
+
if (path6.length === 0 || path6 === "/") return "/";
|
|
16
|
+
return path6;
|
|
17
17
|
}
|
|
18
18
|
var PathActionError = class extends Error {
|
|
19
19
|
code;
|
|
@@ -24,16 +24,16 @@ var PathActionError = class extends Error {
|
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
26
|
async function resolveContainedActionPath(theaterPath, relativePath) {
|
|
27
|
-
if (
|
|
28
|
-
const rootPath =
|
|
29
|
-
const candidatePath =
|
|
27
|
+
if (path5.isAbsolute(relativePath)) throw new PathActionError("path_outside_theater");
|
|
28
|
+
const rootPath = path5.resolve(theaterPath);
|
|
29
|
+
const candidatePath = path5.resolve(rootPath, relativePath);
|
|
30
30
|
if (!isPathContained(rootPath, candidatePath)) throw new PathActionError("path_outside_theater");
|
|
31
31
|
let realRootPath;
|
|
32
32
|
let realCandidatePath;
|
|
33
33
|
try {
|
|
34
34
|
[realRootPath, realCandidatePath] = await Promise.all([
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
fsp.realpath(rootPath),
|
|
36
|
+
fsp.realpath(candidatePath)
|
|
37
37
|
]);
|
|
38
38
|
} catch (error) {
|
|
39
39
|
const code = error.code;
|
|
@@ -51,11 +51,61 @@ async function resolveContainedActionPath(theaterPath, relativePath) {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
function isPathContained(rootPath, candidatePath) {
|
|
54
|
-
const relative =
|
|
55
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${
|
|
54
|
+
const relative = path5.relative(rootPath, candidatePath);
|
|
55
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path5.sep}`) && !path5.isAbsolute(relative);
|
|
56
|
+
}
|
|
57
|
+
var FileActionUnavailableError = class extends Error {
|
|
58
|
+
constructor() {
|
|
59
|
+
super("action_unavailable");
|
|
60
|
+
this.name = "FileActionUnavailableError";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var DEFAULT_REVEAL_DEPENDENCIES = {
|
|
64
|
+
platform: process.platform,
|
|
65
|
+
launch: spawnDetached
|
|
66
|
+
};
|
|
67
|
+
async function revealPath(theaterPath, relativePath, mode, dependencies = {}) {
|
|
68
|
+
const deps = { ...DEFAULT_REVEAL_DEPENDENCIES, ...dependencies };
|
|
69
|
+
const { realPath } = await resolveContainedActionPath(theaterPath, relativePath);
|
|
70
|
+
const command = resolveRevealCommand(deps.platform, mode, realPath);
|
|
71
|
+
if (!command) throw new FileActionUnavailableError();
|
|
72
|
+
try {
|
|
73
|
+
await deps.launch(command.file, command.args);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error.code === "ENOENT") throw new FileActionUnavailableError();
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function resolveRevealCommand(platform, mode, absolutePath) {
|
|
80
|
+
if (platform === "darwin") {
|
|
81
|
+
return mode === "reveal" ? { file: "open", args: ["-R", absolutePath] } : { file: "open", args: [absolutePath] };
|
|
82
|
+
}
|
|
83
|
+
if (platform === "linux") {
|
|
84
|
+
return {
|
|
85
|
+
file: "xdg-open",
|
|
86
|
+
args: [mode === "reveal" ? path5.dirname(absolutePath) : absolutePath]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (platform === "win32") {
|
|
90
|
+
return mode === "reveal" ? { file: "explorer", args: [`/select,${absolutePath}`] } : { file: "explorer", args: [absolutePath] };
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
function spawnDetached(file, args) {
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const child = spawn(file, [...args], {
|
|
97
|
+
detached: true,
|
|
98
|
+
shell: false,
|
|
99
|
+
stdio: "ignore",
|
|
100
|
+
windowsHide: true
|
|
101
|
+
});
|
|
102
|
+
child.once("error", reject);
|
|
103
|
+
child.once("spawn", () => {
|
|
104
|
+
child.unref();
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
56
108
|
}
|
|
57
|
-
|
|
58
|
-
// ../fleet-plugins/file-explorer/server/clipboard.ts
|
|
59
109
|
var ClipboardUnavailableError = class extends Error {
|
|
60
110
|
constructor() {
|
|
61
111
|
super("clipboard_unavailable");
|
|
@@ -67,13 +117,13 @@ var LINUX_CLIPBOARD_TOOLS = [
|
|
|
67
117
|
{ name: "xclip", args: ["-selection", "clipboard"] },
|
|
68
118
|
{ name: "xsel", args: ["--clipboard", "--input"] }
|
|
69
119
|
];
|
|
70
|
-
var
|
|
120
|
+
var DEFAULT_CLIPBOARD_ACTION_DEPENDENCIES = {
|
|
71
121
|
platform: process.platform,
|
|
72
122
|
findExecutable: findExecutableOnPath,
|
|
73
123
|
runWithInput: execFileWithInput
|
|
74
124
|
};
|
|
75
125
|
async function copyPathToClipboard(theaterPath, relativePath, dependencies = {}) {
|
|
76
|
-
const deps = { ...
|
|
126
|
+
const deps = { ...DEFAULT_CLIPBOARD_ACTION_DEPENDENCIES, ...dependencies };
|
|
77
127
|
const { logicalPath } = await resolveContainedActionPath(theaterPath, relativePath);
|
|
78
128
|
const command = await resolveClipboardCommand(deps.platform, deps.findExecutable);
|
|
79
129
|
if (!command) throw new ClipboardUnavailableError();
|
|
@@ -96,11 +146,11 @@ async function resolveClipboardCommand(platform, findExecutable = findExecutable
|
|
|
96
146
|
}
|
|
97
147
|
async function findExecutableOnPath(name) {
|
|
98
148
|
const rawPath = process.env.PATH ?? "";
|
|
99
|
-
for (const directory of rawPath.split(
|
|
149
|
+
for (const directory of rawPath.split(path5.delimiter)) {
|
|
100
150
|
if (!directory) continue;
|
|
101
|
-
const candidate =
|
|
151
|
+
const candidate = path5.join(directory, name);
|
|
102
152
|
try {
|
|
103
|
-
await
|
|
153
|
+
await fsp.access(candidate, constants.X_OK);
|
|
104
154
|
return candidate;
|
|
105
155
|
} catch {
|
|
106
156
|
}
|
|
@@ -186,14 +236,14 @@ var EXT_LANG_MAP = {
|
|
|
186
236
|
".txt": "plaintext"
|
|
187
237
|
};
|
|
188
238
|
async function readFileForTheater(theaterPath, relativePath) {
|
|
189
|
-
const resolved =
|
|
239
|
+
const resolved = path5.resolve(theaterPath, relativePath);
|
|
190
240
|
if (!isWithinRoot(resolved, theaterPath)) throw new FileReadError("path_outside_theater");
|
|
191
241
|
let realResolved;
|
|
192
242
|
let realRoot;
|
|
193
243
|
try {
|
|
194
244
|
[realResolved, realRoot] = await Promise.all([
|
|
195
|
-
|
|
196
|
-
|
|
245
|
+
fs2.promises.realpath(resolved),
|
|
246
|
+
fs2.promises.realpath(theaterPath)
|
|
197
247
|
]);
|
|
198
248
|
} catch (error) {
|
|
199
249
|
const code = error.code;
|
|
@@ -203,7 +253,7 @@ async function readFileForTheater(theaterPath, relativePath) {
|
|
|
203
253
|
if (!isWithinRoot(realResolved, realRoot)) throw new FileReadError("path_outside_theater");
|
|
204
254
|
let stat;
|
|
205
255
|
try {
|
|
206
|
-
stat = await
|
|
256
|
+
stat = await fs2.promises.stat(realResolved);
|
|
207
257
|
} catch (error) {
|
|
208
258
|
const code = error.code;
|
|
209
259
|
if (code === "EACCES" || code === "EPERM") throw new FileReadError("forbidden");
|
|
@@ -211,29 +261,29 @@ async function readFileForTheater(theaterPath, relativePath) {
|
|
|
211
261
|
}
|
|
212
262
|
if (!stat.isFile()) throw new FileReadError("not_a_file");
|
|
213
263
|
if (stat.size > FILE_SIZE_CAP) {
|
|
214
|
-
const fd = await
|
|
264
|
+
const fd = await fs2.promises.open(realResolved, "r");
|
|
215
265
|
const buffer2 = Buffer.alloc(FILE_SIZE_CAP);
|
|
216
266
|
const { bytesRead } = await fd.read(buffer2, 0, FILE_SIZE_CAP, 0);
|
|
217
267
|
await fd.close();
|
|
218
268
|
const chunk = buffer2.subarray(0, bytesRead);
|
|
219
269
|
if (isBinaryBuffer(chunk)) throw new FileReadError("binary_file");
|
|
220
270
|
return {
|
|
221
|
-
relativePath:
|
|
271
|
+
relativePath: path5.relative(realRoot, realResolved),
|
|
222
272
|
content: chunk.toString("utf8"),
|
|
223
273
|
lang: detectLang(realResolved),
|
|
224
274
|
truncated: true
|
|
225
275
|
};
|
|
226
276
|
}
|
|
227
|
-
const buffer = await
|
|
277
|
+
const buffer = await fs2.promises.readFile(realResolved);
|
|
228
278
|
if (isBinaryBuffer(buffer)) throw new FileReadError("binary_file");
|
|
229
279
|
return {
|
|
230
|
-
relativePath:
|
|
280
|
+
relativePath: path5.relative(realRoot, realResolved),
|
|
231
281
|
content: buffer.toString("utf8"),
|
|
232
282
|
lang: detectLang(realResolved)
|
|
233
283
|
};
|
|
234
284
|
}
|
|
235
285
|
function isWithinRoot(resolved, root) {
|
|
236
|
-
const normalizedRoot = root.endsWith(
|
|
286
|
+
const normalizedRoot = root.endsWith(path5.sep) ? root : root + path5.sep;
|
|
237
287
|
return resolved === root || resolved.startsWith(normalizedRoot);
|
|
238
288
|
}
|
|
239
289
|
function isBinaryBuffer(buffer) {
|
|
@@ -246,605 +296,646 @@ function isBinaryBuffer(buffer) {
|
|
|
246
296
|
return nulCount / checkLen > BINARY_NUL_THRESHOLD;
|
|
247
297
|
}
|
|
248
298
|
function detectLang(filePath) {
|
|
249
|
-
const ext =
|
|
299
|
+
const ext = path5.extname(filePath).toLowerCase();
|
|
250
300
|
return EXT_LANG_MAP[ext] ?? "plaintext";
|
|
251
301
|
}
|
|
252
|
-
var
|
|
302
|
+
var ImageServeError = class extends Error {
|
|
253
303
|
code;
|
|
254
304
|
constructor(code) {
|
|
255
305
|
super(code);
|
|
256
|
-
this.name = "
|
|
306
|
+
this.name = "ImageServeError";
|
|
257
307
|
this.code = code;
|
|
258
308
|
}
|
|
259
309
|
};
|
|
260
|
-
var
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
310
|
+
var IMAGE_SIZE_CAP = 16 * 1024 * 1024;
|
|
311
|
+
var MIME_ALLOWLIST = {
|
|
312
|
+
".png": "image/png",
|
|
313
|
+
".jpg": "image/jpeg",
|
|
314
|
+
".jpeg": "image/jpeg",
|
|
315
|
+
".webp": "image/webp",
|
|
316
|
+
".gif": "image/gif"
|
|
317
|
+
};
|
|
318
|
+
var IMAGE_SECURITY_HEADERS = {
|
|
319
|
+
"X-Content-Type-Options": "nosniff",
|
|
320
|
+
"Referrer-Policy": "no-referrer",
|
|
321
|
+
"Cache-Control": "private, no-store"
|
|
322
|
+
};
|
|
323
|
+
async function readImageForTheater(theaterPath, relativePath) {
|
|
324
|
+
const resolved = path5.resolve(theaterPath, relativePath);
|
|
325
|
+
if (!isWithinRoot2(resolved, theaterPath)) throw new ImageServeError("path_outside_theater");
|
|
326
|
+
const ext = path5.extname(resolved).toLowerCase();
|
|
327
|
+
const mimeType = MIME_ALLOWLIST[ext];
|
|
328
|
+
if (!mimeType) throw new ImageServeError("mime_not_allowed");
|
|
329
|
+
let realResolved;
|
|
269
330
|
let realRoot;
|
|
270
|
-
let realTargetAbs;
|
|
271
331
|
try {
|
|
272
|
-
[
|
|
273
|
-
|
|
274
|
-
|
|
332
|
+
[realResolved, realRoot] = await Promise.all([
|
|
333
|
+
fs2.promises.realpath(resolved),
|
|
334
|
+
fs2.promises.realpath(theaterPath)
|
|
275
335
|
]);
|
|
276
336
|
} catch (error) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (realTargetAbs !== realRoot && !realTargetAbs.startsWith(realNormalizedRoot)) {
|
|
281
|
-
throw new FolderBrowserError("forbidden");
|
|
337
|
+
const code = error.code;
|
|
338
|
+
if (code === "EACCES" || code === "EPERM") throw new ImageServeError("forbidden");
|
|
339
|
+
throw new ImageServeError("not_found");
|
|
282
340
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const entries = [];
|
|
286
|
-
const truncated = await collectContentsEntries(realTargetAbs, realRoot, opendir, stat, entries);
|
|
287
|
-
entries.sort((a, b) => {
|
|
288
|
-
if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1;
|
|
289
|
-
return a.name.localeCompare(b.name);
|
|
290
|
-
});
|
|
291
|
-
const rel = path4.relative(realRoot, realTargetAbs);
|
|
292
|
-
const parentRel = rel === "" ? null : path4.relative(realRoot, path4.dirname(realTargetAbs));
|
|
293
|
-
return {
|
|
294
|
-
relativePath: rel,
|
|
295
|
-
parentRelativePath: parentRel === "" ? null : parentRel ?? null,
|
|
296
|
-
entries,
|
|
297
|
-
...truncated ? { truncated: true } : {}
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
async function statDirectory(targetPath, stat) {
|
|
341
|
+
if (!isWithinRoot2(realResolved, realRoot)) throw new ImageServeError("path_outside_theater");
|
|
342
|
+
let stat;
|
|
301
343
|
try {
|
|
302
|
-
|
|
344
|
+
stat = await fs2.promises.stat(realResolved);
|
|
303
345
|
} catch (error) {
|
|
304
|
-
|
|
346
|
+
const code = error.code;
|
|
347
|
+
if (code === "EACCES" || code === "EPERM") throw new ImageServeError("forbidden");
|
|
348
|
+
throw new ImageServeError("not_found");
|
|
305
349
|
}
|
|
350
|
+
if (!stat.isFile()) throw new ImageServeError("not_a_file");
|
|
351
|
+
if (stat.size > IMAGE_SIZE_CAP) throw new ImageServeError("size_exceeded");
|
|
352
|
+
const buffer = await fs2.promises.readFile(realResolved);
|
|
353
|
+
return { mimeType, buffer };
|
|
306
354
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (entry !== null) entries.push(entry);
|
|
315
|
-
dirent = await directory.read();
|
|
316
|
-
}
|
|
317
|
-
return false;
|
|
318
|
-
} catch (error) {
|
|
319
|
-
throw mapFsError(error);
|
|
320
|
-
} finally {
|
|
321
|
-
await directory.close();
|
|
322
|
-
}
|
|
355
|
+
function writeImageResponse(res, result) {
|
|
356
|
+
res.writeHead(200, {
|
|
357
|
+
...IMAGE_SECURITY_HEADERS,
|
|
358
|
+
"Content-Type": result.mimeType,
|
|
359
|
+
"Content-Length": result.buffer.length
|
|
360
|
+
});
|
|
361
|
+
res.end(result.buffer);
|
|
323
362
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
} catch (error) {
|
|
328
|
-
throw mapFsError(error);
|
|
329
|
-
}
|
|
363
|
+
function isWithinRoot2(resolved, root) {
|
|
364
|
+
const normalizedRoot = root.endsWith(path5.sep) ? root : root + path5.sep;
|
|
365
|
+
return resolved === root || resolved.startsWith(normalizedRoot);
|
|
330
366
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
367
|
+
var DEBOUNCE_MS = 200;
|
|
368
|
+
var LINUX_DIRECTORY_WATCH_CAP = 1024;
|
|
369
|
+
var watcherRegistry = createWatcherRegistry();
|
|
370
|
+
function createWatcherRegistry(watcherFactory = (p, opts, cb) => fs2.watch(p, opts, (ev, fn) => cb(ev, typeof fn === "string" ? fn : null)), debounceMs = DEBOUNCE_MS, platform = process.platform) {
|
|
371
|
+
const entries = /* @__PURE__ */ new Map();
|
|
372
|
+
function scheduleChange(entry, relDir) {
|
|
373
|
+
const pending = entry.debounceTimers.get(relDir);
|
|
374
|
+
if (pending) clearTimeout(pending);
|
|
375
|
+
entry.debounceTimers.set(
|
|
376
|
+
relDir,
|
|
377
|
+
setTimeout(() => {
|
|
378
|
+
entry.debounceTimers.delete(relDir);
|
|
379
|
+
for (const sub of entry.subscribers) sub(relDir);
|
|
380
|
+
}, debounceMs)
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
function closeEntry(entry) {
|
|
384
|
+
for (const timer of entry.debounceTimers.values()) clearTimeout(timer);
|
|
385
|
+
entry.debounceTimers.clear();
|
|
386
|
+
for (const watcher of entry.watchers.values()) {
|
|
387
|
+
try {
|
|
388
|
+
watcher.close();
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
346
391
|
}
|
|
392
|
+
entry.watchers.clear();
|
|
393
|
+
entry.identityChecks.clear();
|
|
394
|
+
entry.pendingDirectories.clear();
|
|
347
395
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
396
|
+
async function rearmPendingDirectories(theaterId, entry) {
|
|
397
|
+
entry.rearmRequested = true;
|
|
398
|
+
if (entry.rearmRunning) return;
|
|
399
|
+
entry.rearmRunning = true;
|
|
400
|
+
try {
|
|
401
|
+
while (entry.rearmRequested) {
|
|
402
|
+
entry.rearmRequested = false;
|
|
403
|
+
for (const relativePath of entry.pendingDirectories) {
|
|
404
|
+
const target = await resolveTrackedDirectory(entry.theaterPath, relativePath);
|
|
405
|
+
if (!target) continue;
|
|
406
|
+
if (entries.get(theaterId) !== entry || !entry.pendingDirectories.has(relativePath) || entry.watchers.has(relativePath) || entry.watchers.size >= LINUX_DIRECTORY_WATCH_CAP) continue;
|
|
407
|
+
try {
|
|
408
|
+
startWatcher(theaterId, entry, relativePath, target.path, target.identity);
|
|
409
|
+
entry.pendingDirectories.delete(relativePath);
|
|
410
|
+
scheduleChange(entry, relativePath);
|
|
411
|
+
} catch {
|
|
412
|
+
for (const notify of entry.stateSubscribers) notify("degraded");
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
} finally {
|
|
417
|
+
entry.rearmRunning = false;
|
|
418
|
+
}
|
|
362
419
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
throw mapFsError2(error);
|
|
420
|
+
function invalidateWatcherTree(entry, watchKey) {
|
|
421
|
+
const descendantPrefix = watchKey + path5.sep;
|
|
422
|
+
for (const [candidateKey, candidateWatcher] of entry.watchers) {
|
|
423
|
+
if (candidateKey !== watchKey && !candidateKey.startsWith(descendantPrefix)) continue;
|
|
424
|
+
entry.watchers.delete(candidateKey);
|
|
425
|
+
entry.identityChecks.delete(candidateKey);
|
|
426
|
+
entry.pendingDirectories.add(candidateKey);
|
|
427
|
+
try {
|
|
428
|
+
candidateWatcher.close();
|
|
429
|
+
} catch {
|
|
430
|
+
}
|
|
431
|
+
}
|
|
376
432
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
433
|
+
function startWatcher(theaterId, entry, watchKey, watchPath, watchIdentity) {
|
|
434
|
+
let identityCheckRunning = false;
|
|
435
|
+
let identityCheckRequested = false;
|
|
436
|
+
let activeWatcher = null;
|
|
437
|
+
const lexicalWatchPath = path5.resolve(entry.theaterPath, watchKey);
|
|
438
|
+
const checkDirectoryIdentity = async (expectedWatcher) => {
|
|
439
|
+
identityCheckRequested = true;
|
|
440
|
+
if (identityCheckRunning || !watchIdentity) return;
|
|
441
|
+
identityCheckRunning = true;
|
|
442
|
+
try {
|
|
443
|
+
while (identityCheckRequested) {
|
|
444
|
+
identityCheckRequested = false;
|
|
445
|
+
let replaced = false;
|
|
446
|
+
try {
|
|
447
|
+
const stats = await fs2.promises.stat(lexicalWatchPath);
|
|
448
|
+
replaced = stats.dev !== watchIdentity.device || stats.ino !== watchIdentity.inode;
|
|
449
|
+
} catch (error) {
|
|
450
|
+
replaced = error.code === "ENOENT";
|
|
451
|
+
}
|
|
452
|
+
if (entry.watchers.get(watchKey) !== expectedWatcher) return;
|
|
453
|
+
if (!replaced) continue;
|
|
454
|
+
invalidateWatcherTree(entry, watchKey);
|
|
455
|
+
await rearmPendingDirectories(theaterId, entry);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
} finally {
|
|
459
|
+
identityCheckRunning = false;
|
|
460
|
+
}
|
|
404
461
|
};
|
|
405
|
-
|
|
406
|
-
|
|
462
|
+
const watcher = watcherFactory(watchPath, { recursive: entry.recursive }, (event, filename) => {
|
|
463
|
+
const currentWatcher = activeWatcher;
|
|
464
|
+
if (!currentWatcher || entry.watchers.get(watchKey) !== currentWatcher) return;
|
|
465
|
+
scheduleChange(entry, entry.recursive ? computeRelDir(filename) : watchKey);
|
|
466
|
+
if (!entry.recursive && event === "rename") {
|
|
467
|
+
if (watchKey !== "" && watchIdentity) void checkDirectoryIdentity(currentWatcher);
|
|
468
|
+
if (filename) {
|
|
469
|
+
const changedChildKey = path5.join(watchKey, filename);
|
|
470
|
+
entry.identityChecks.get(changedChildKey)?.();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (!entry.recursive && entry.pendingDirectories.size > 0) {
|
|
474
|
+
void rearmPendingDirectories(theaterId, entry);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
activeWatcher = watcher;
|
|
478
|
+
entry.watchers.set(watchKey, watcher);
|
|
479
|
+
if (watchIdentity) {
|
|
480
|
+
entry.identityChecks.set(watchKey, () => {
|
|
481
|
+
const currentWatcher = activeWatcher;
|
|
482
|
+
if (currentWatcher) void checkDirectoryIdentity(currentWatcher);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
watcher.on("error", () => {
|
|
486
|
+
if (entry.watchers.get(watchKey) !== watcher) return;
|
|
487
|
+
try {
|
|
488
|
+
watcher.close();
|
|
489
|
+
} catch {
|
|
490
|
+
}
|
|
491
|
+
entry.watchers.delete(watchKey);
|
|
492
|
+
entry.identityChecks.delete(watchKey);
|
|
493
|
+
if (watchKey === "") {
|
|
494
|
+
closeEntry(entry);
|
|
495
|
+
if (entries.get(theaterId) === entry) entries.delete(theaterId);
|
|
496
|
+
} else {
|
|
497
|
+
const pending = entry.debounceTimers.get(watchKey);
|
|
498
|
+
if (pending) clearTimeout(pending);
|
|
499
|
+
entry.debounceTimers.delete(watchKey);
|
|
500
|
+
}
|
|
501
|
+
for (const notify of entry.stateSubscribers) notify("degraded");
|
|
502
|
+
});
|
|
407
503
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (status && gitPath) statuses.push({ gitPath, status });
|
|
422
|
-
if (isRenameOrCopy && index + 1 < records.length) index += 1;
|
|
504
|
+
function makeUnsubscribe(theaterId, onChange, onState, entry) {
|
|
505
|
+
let called = false;
|
|
506
|
+
return () => {
|
|
507
|
+
if (called) return;
|
|
508
|
+
called = true;
|
|
509
|
+
entry.subscribers.delete(onChange);
|
|
510
|
+
entry.stateSubscribers.delete(onState);
|
|
511
|
+
entry.subscriberCount--;
|
|
512
|
+
if (entry.subscriberCount <= 0) {
|
|
513
|
+
closeEntry(entry);
|
|
514
|
+
if (entries.get(theaterId) === entry) entries.delete(theaterId);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
423
517
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
518
|
+
function subscribe(theaterId, theaterPath, onChange, onState) {
|
|
519
|
+
const existing = entries.get(theaterId);
|
|
520
|
+
if (existing) {
|
|
521
|
+
existing.subscribers.add(onChange);
|
|
522
|
+
existing.stateSubscribers.add(onState);
|
|
523
|
+
existing.subscriberCount++;
|
|
524
|
+
onState("watching");
|
|
525
|
+
return makeUnsubscribe(theaterId, onChange, onState, existing);
|
|
526
|
+
}
|
|
527
|
+
const entry = {
|
|
528
|
+
// Linux의 recursive fs.watch fallback은 구독 시 전체 트리를 동기 순회한다.
|
|
529
|
+
// 대신 root와 실제로 조회된 디렉터리만 개별 비재귀 watcher로 추적한다.
|
|
530
|
+
theaterPath,
|
|
531
|
+
recursive: platform !== "linux",
|
|
532
|
+
watchers: /* @__PURE__ */ new Map(),
|
|
533
|
+
identityChecks: /* @__PURE__ */ new Map(),
|
|
534
|
+
pendingDirectories: /* @__PURE__ */ new Set(),
|
|
535
|
+
subscribers: /* @__PURE__ */ new Set([onChange]),
|
|
536
|
+
stateSubscribers: /* @__PURE__ */ new Set([onState]),
|
|
537
|
+
subscriberCount: 1,
|
|
538
|
+
debounceTimers: /* @__PURE__ */ new Map(),
|
|
539
|
+
rearmRunning: false,
|
|
540
|
+
rearmRequested: false
|
|
541
|
+
};
|
|
542
|
+
try {
|
|
543
|
+
startWatcher(theaterId, entry, "", theaterPath);
|
|
544
|
+
} catch {
|
|
545
|
+
onState("degraded");
|
|
546
|
+
return () => {
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
entries.set(theaterId, entry);
|
|
550
|
+
onState("watching");
|
|
551
|
+
return makeUnsubscribe(theaterId, onChange, onState, entry);
|
|
436
552
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
553
|
+
async function trackDirectory(theaterId, theaterPath, relativePath) {
|
|
554
|
+
const entry = entries.get(theaterId);
|
|
555
|
+
if (!entry || entry.recursive || relativePath === "" || entry.watchers.has(relativePath)) return;
|
|
556
|
+
if (entry.watchers.size >= LINUX_DIRECTORY_WATCH_CAP) {
|
|
557
|
+
for (const notify of entry.stateSubscribers) notify("degraded");
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const target = await resolveTrackedDirectory(theaterPath, relativePath);
|
|
561
|
+
if (!target) return;
|
|
562
|
+
if (entries.get(theaterId) !== entry || entry.watchers.has(relativePath) || entry.watchers.size >= LINUX_DIRECTORY_WATCH_CAP) return;
|
|
563
|
+
try {
|
|
564
|
+
startWatcher(theaterId, entry, relativePath, target.path, target.identity);
|
|
565
|
+
entry.pendingDirectories.delete(relativePath);
|
|
566
|
+
} catch {
|
|
567
|
+
for (const notify of entry.stateSubscribers) notify("degraded");
|
|
568
|
+
}
|
|
445
569
|
}
|
|
446
|
-
return
|
|
447
|
-
}
|
|
448
|
-
function executeGit(args, options) {
|
|
449
|
-
return new Promise((resolve, reject) => {
|
|
450
|
-
execFile(
|
|
451
|
-
"git",
|
|
452
|
-
[...args],
|
|
453
|
-
{
|
|
454
|
-
encoding: "utf8",
|
|
455
|
-
env: options.env,
|
|
456
|
-
killSignal: options.killSignal,
|
|
457
|
-
maxBuffer: options.maxBuffer,
|
|
458
|
-
shell: false,
|
|
459
|
-
timeout: options.timeout,
|
|
460
|
-
windowsHide: true
|
|
461
|
-
},
|
|
462
|
-
(error, stdout) => {
|
|
463
|
-
if (error) {
|
|
464
|
-
reject(error);
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
resolve(stdout);
|
|
468
|
-
}
|
|
469
|
-
);
|
|
470
|
-
});
|
|
570
|
+
return { subscribe, trackDirectory };
|
|
471
571
|
}
|
|
472
|
-
function
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
572
|
+
async function resolveTrackedDirectory(theaterPath, relativePath) {
|
|
573
|
+
const resolvedRoot = path5.resolve(theaterPath);
|
|
574
|
+
const targetPath = path5.resolve(resolvedRoot, relativePath);
|
|
575
|
+
if (!isWithinRoot3(targetPath, resolvedRoot)) return null;
|
|
576
|
+
try {
|
|
577
|
+
const [realRoot, realTarget] = await Promise.all([
|
|
578
|
+
fs2.promises.realpath(theaterPath),
|
|
579
|
+
fs2.promises.realpath(targetPath)
|
|
580
|
+
]);
|
|
581
|
+
if (!isWithinRoot3(realTarget, realRoot)) return null;
|
|
582
|
+
const stats = await fs2.promises.stat(realTarget);
|
|
583
|
+
if (!stats.isDirectory()) return null;
|
|
584
|
+
return {
|
|
585
|
+
path: realTarget,
|
|
586
|
+
identity: { device: stats.dev, inode: stats.ino }
|
|
587
|
+
};
|
|
588
|
+
} catch {
|
|
589
|
+
return null;
|
|
477
590
|
}
|
|
478
|
-
sanitized.GIT_OPTIONAL_LOCKS = "0";
|
|
479
|
-
return sanitized;
|
|
480
591
|
}
|
|
481
|
-
function
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
return withoutLineFeed.endsWith("\r") ? withoutLineFeed.slice(0, -1) : withoutLineFeed;
|
|
592
|
+
function isWithinRoot3(candidate, root) {
|
|
593
|
+
const normalizedRoot = root.endsWith(path5.sep) ? root : root + path5.sep;
|
|
594
|
+
return candidate === root || candidate.startsWith(normalizedRoot);
|
|
485
595
|
}
|
|
486
|
-
function
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (
|
|
490
|
-
return
|
|
596
|
+
function computeRelDir(filename) {
|
|
597
|
+
if (!filename) return "";
|
|
598
|
+
const lastSep = Math.max(filename.lastIndexOf("/"), filename.lastIndexOf("\\"));
|
|
599
|
+
if (lastSep <= 0) return "";
|
|
600
|
+
return filename.slice(0, lastSep);
|
|
491
601
|
}
|
|
492
|
-
|
|
602
|
+
|
|
603
|
+
// ../fleet-plugins/file-explorer/server/tree-services.ts
|
|
604
|
+
var FolderBrowserError = class extends Error {
|
|
493
605
|
code;
|
|
494
606
|
constructor(code) {
|
|
495
607
|
super(code);
|
|
496
|
-
this.name = "
|
|
608
|
+
this.name = "FolderBrowserError";
|
|
497
609
|
this.code = code;
|
|
498
610
|
}
|
|
499
611
|
};
|
|
500
|
-
var
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
var IMAGE_SECURITY_HEADERS = {
|
|
509
|
-
"X-Content-Type-Options": "nosniff",
|
|
510
|
-
"Referrer-Policy": "no-referrer",
|
|
511
|
-
"Cache-Control": "private, no-store"
|
|
512
|
-
};
|
|
513
|
-
async function readImageForTheater(theaterPath, relativePath) {
|
|
514
|
-
const resolved = path4.resolve(theaterPath, relativePath);
|
|
515
|
-
if (!isWithinRoot2(resolved, theaterPath)) throw new ImageServeError("path_outside_theater");
|
|
516
|
-
const ext = path4.extname(resolved).toLowerCase();
|
|
517
|
-
const mimeType = MIME_ALLOWLIST[ext];
|
|
518
|
-
if (!mimeType) throw new ImageServeError("mime_not_allowed");
|
|
519
|
-
let realResolved;
|
|
520
|
-
let realRoot;
|
|
521
|
-
try {
|
|
522
|
-
[realResolved, realRoot] = await Promise.all([
|
|
523
|
-
fs3.promises.realpath(resolved),
|
|
524
|
-
fs3.promises.realpath(theaterPath)
|
|
525
|
-
]);
|
|
526
|
-
} catch (error) {
|
|
527
|
-
const code = error.code;
|
|
528
|
-
if (code === "EACCES" || code === "EPERM") throw new ImageServeError("forbidden");
|
|
529
|
-
throw new ImageServeError("not_found");
|
|
612
|
+
var DIRECTORY_ENTRY_CAP = 500;
|
|
613
|
+
async function listTheaterContents(theaterPath, relativePath, deps = {}) {
|
|
614
|
+
const opendir = deps.opendir ?? fs2.promises.opendir;
|
|
615
|
+
const stat = deps.stat ?? fs2.promises.stat;
|
|
616
|
+
const targetAbs = path5.resolve(theaterPath, relativePath);
|
|
617
|
+
const normalizedRoot = theaterPath.endsWith(path5.sep) ? theaterPath : theaterPath + path5.sep;
|
|
618
|
+
if (targetAbs !== theaterPath && !targetAbs.startsWith(normalizedRoot)) {
|
|
619
|
+
throw new FolderBrowserError("forbidden");
|
|
530
620
|
}
|
|
531
|
-
|
|
532
|
-
let
|
|
621
|
+
let realRoot;
|
|
622
|
+
let realTargetAbs;
|
|
533
623
|
try {
|
|
534
|
-
|
|
624
|
+
[realRoot, realTargetAbs] = await Promise.all([
|
|
625
|
+
fs2.promises.realpath(theaterPath),
|
|
626
|
+
fs2.promises.realpath(targetAbs)
|
|
627
|
+
]);
|
|
535
628
|
} catch (error) {
|
|
536
|
-
|
|
537
|
-
if (code === "EACCES" || code === "EPERM") throw new ImageServeError("forbidden");
|
|
538
|
-
throw new ImageServeError("not_found");
|
|
629
|
+
throw mapFolderBrowserFsError(error);
|
|
539
630
|
}
|
|
540
|
-
|
|
541
|
-
if (
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
"
|
|
631
|
+
const realNormalizedRoot = realRoot.endsWith(path5.sep) ? realRoot : realRoot + path5.sep;
|
|
632
|
+
if (realTargetAbs !== realRoot && !realTargetAbs.startsWith(realNormalizedRoot)) {
|
|
633
|
+
throw new FolderBrowserError("forbidden");
|
|
634
|
+
}
|
|
635
|
+
const targetStat = await statDirectory(realTargetAbs, stat);
|
|
636
|
+
if (!targetStat.isDirectory()) throw new FolderBrowserError("invalid_path");
|
|
637
|
+
const entries = [];
|
|
638
|
+
const truncated = await collectContentsEntries(realTargetAbs, realRoot, opendir, stat, entries);
|
|
639
|
+
entries.sort((a, b) => {
|
|
640
|
+
if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1;
|
|
641
|
+
return a.name.localeCompare(b.name);
|
|
550
642
|
});
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
643
|
+
const rel = path5.relative(realRoot, realTargetAbs);
|
|
644
|
+
const parentRel = rel === "" ? null : path5.relative(realRoot, path5.dirname(realTargetAbs));
|
|
645
|
+
return {
|
|
646
|
+
relativePath: rel,
|
|
647
|
+
parentRelativePath: parentRel === "" ? null : parentRel ?? null,
|
|
648
|
+
entries,
|
|
649
|
+
...truncated ? { truncated: true } : {}
|
|
650
|
+
};
|
|
556
651
|
}
|
|
557
|
-
|
|
558
|
-
constructor() {
|
|
559
|
-
super("action_unavailable");
|
|
560
|
-
this.name = "FileActionUnavailableError";
|
|
561
|
-
}
|
|
562
|
-
};
|
|
563
|
-
var DEFAULT_DEPENDENCIES2 = {
|
|
564
|
-
platform: process.platform,
|
|
565
|
-
launch: spawnDetached
|
|
566
|
-
};
|
|
567
|
-
async function revealPath(theaterPath, relativePath, mode, dependencies = {}) {
|
|
568
|
-
const deps = { ...DEFAULT_DEPENDENCIES2, ...dependencies };
|
|
569
|
-
const { realPath } = await resolveContainedActionPath(theaterPath, relativePath);
|
|
570
|
-
const command = resolveRevealCommand(deps.platform, mode, realPath);
|
|
571
|
-
if (!command) throw new FileActionUnavailableError();
|
|
652
|
+
async function statDirectory(targetPath, stat) {
|
|
572
653
|
try {
|
|
573
|
-
await
|
|
654
|
+
return await stat(targetPath);
|
|
574
655
|
} catch (error) {
|
|
575
|
-
|
|
576
|
-
throw error;
|
|
656
|
+
throw mapFolderBrowserFsError(error);
|
|
577
657
|
}
|
|
578
658
|
}
|
|
579
|
-
function
|
|
580
|
-
|
|
581
|
-
|
|
659
|
+
async function collectContentsEntries(targetPath, theaterPath, opendir, stat, entries) {
|
|
660
|
+
const directory = await openDirectory(targetPath, opendir);
|
|
661
|
+
try {
|
|
662
|
+
let dirent = await directory.read();
|
|
663
|
+
while (dirent !== null) {
|
|
664
|
+
if (entries.length >= DIRECTORY_ENTRY_CAP) return true;
|
|
665
|
+
const entry = await toContentsEntry(targetPath, theaterPath, dirent, stat);
|
|
666
|
+
if (entry !== null) entries.push(entry);
|
|
667
|
+
dirent = await directory.read();
|
|
668
|
+
}
|
|
669
|
+
return false;
|
|
670
|
+
} catch (error) {
|
|
671
|
+
throw mapFolderBrowserFsError(error);
|
|
672
|
+
} finally {
|
|
673
|
+
await directory.close();
|
|
582
674
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
675
|
+
}
|
|
676
|
+
async function openDirectory(targetPath, opendir) {
|
|
677
|
+
try {
|
|
678
|
+
return await opendir(targetPath, { bufferSize: DIRECTORY_ENTRY_CAP + 1 });
|
|
679
|
+
} catch (error) {
|
|
680
|
+
throw mapFolderBrowserFsError(error);
|
|
588
681
|
}
|
|
589
|
-
|
|
590
|
-
|
|
682
|
+
}
|
|
683
|
+
async function toContentsEntry(targetPath, theaterPath, dirent, stat) {
|
|
684
|
+
const entryPath = path5.join(targetPath, dirent.name);
|
|
685
|
+
const rel = path5.relative(theaterPath, entryPath);
|
|
686
|
+
if (dirent.isDirectory()) return { name: dirent.name, relativePath: rel, kind: "dir" };
|
|
687
|
+
if (dirent.isFile()) return { name: dirent.name, relativePath: rel, kind: "file" };
|
|
688
|
+
if (dirent.isSymbolicLink()) {
|
|
689
|
+
try {
|
|
690
|
+
const realEntryPath = await fs2.promises.realpath(entryPath);
|
|
691
|
+
const realNormalizedRoot = theaterPath.endsWith(path5.sep) ? theaterPath : theaterPath + path5.sep;
|
|
692
|
+
if (realEntryPath !== theaterPath && !realEntryPath.startsWith(realNormalizedRoot)) return null;
|
|
693
|
+
const s = await stat(realEntryPath);
|
|
694
|
+
if (s.isDirectory()) return { name: dirent.name, relativePath: rel, kind: "dir" };
|
|
695
|
+
if (s.isFile()) return { name: dirent.name, relativePath: rel, kind: "file" };
|
|
696
|
+
} catch {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
591
699
|
}
|
|
592
700
|
return null;
|
|
593
701
|
}
|
|
594
|
-
function
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
stdio: "ignore",
|
|
600
|
-
windowsHide: true
|
|
601
|
-
});
|
|
602
|
-
child.once("error", reject);
|
|
603
|
-
child.once("spawn", () => {
|
|
604
|
-
child.unref();
|
|
605
|
-
resolve();
|
|
606
|
-
});
|
|
607
|
-
});
|
|
702
|
+
function mapFolderBrowserFsError(error) {
|
|
703
|
+
const code = error.code;
|
|
704
|
+
if (code === "EACCES" || code === "EPERM") return new FolderBrowserError("forbidden");
|
|
705
|
+
if (code === "ENOENT" || code === "ENOTDIR") return new FolderBrowserError("not_found");
|
|
706
|
+
return new FolderBrowserError("invalid_path");
|
|
608
707
|
}
|
|
609
|
-
var
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const pending = entry.debounceTimers.get(relDir);
|
|
616
|
-
if (pending) clearTimeout(pending);
|
|
617
|
-
entry.debounceTimers.set(
|
|
618
|
-
relDir,
|
|
619
|
-
setTimeout(() => {
|
|
620
|
-
entry.debounceTimers.delete(relDir);
|
|
621
|
-
for (const sub of entry.subscribers) sub(relDir);
|
|
622
|
-
}, debounceMs)
|
|
623
|
-
);
|
|
708
|
+
var GitStatusPathError = class extends Error {
|
|
709
|
+
code;
|
|
710
|
+
constructor(code) {
|
|
711
|
+
super(code);
|
|
712
|
+
this.name = "GitStatusPathError";
|
|
713
|
+
this.code = code;
|
|
624
714
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
715
|
+
};
|
|
716
|
+
var GIT_TIMEOUT_MS = 5e3;
|
|
717
|
+
var GIT_MAX_BUFFER = 8 * 1024 * 1024;
|
|
718
|
+
var GIT_STATUS_CAP = 1e4;
|
|
719
|
+
var UNMERGED_STATUS_PAIRS = /* @__PURE__ */ new Set(["UU", "UD", "DU", "AA", "AU", "UA", "DD"]);
|
|
720
|
+
async function readTheaterGitStatus(theaterPath, deps = {}) {
|
|
721
|
+
const realpath = deps.realpath ?? ((target) => fsp.realpath(target));
|
|
722
|
+
const execGit = deps.execGit ?? executeGit;
|
|
723
|
+
let theaterRootAbs;
|
|
724
|
+
try {
|
|
725
|
+
theaterRootAbs = await realpath(path5.resolve(theaterPath));
|
|
726
|
+
} catch (error) {
|
|
727
|
+
throw mapGitStatusFsError(error);
|
|
637
728
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
729
|
+
try {
|
|
730
|
+
const options = {
|
|
731
|
+
env: sanitizeGitEnvironment(deps.environment ?? process.env),
|
|
732
|
+
killSignal: "SIGKILL",
|
|
733
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
734
|
+
timeout: GIT_TIMEOUT_MS
|
|
735
|
+
};
|
|
736
|
+
const gitArgs = (args) => [
|
|
737
|
+
"-c",
|
|
738
|
+
"core.fsmonitor=false",
|
|
739
|
+
"--no-optional-locks",
|
|
740
|
+
"-C",
|
|
741
|
+
theaterRootAbs,
|
|
742
|
+
...args
|
|
743
|
+
];
|
|
744
|
+
const [prefixOutput, statusOutput] = await Promise.all([
|
|
745
|
+
execGit(gitArgs(["rev-parse", "--show-prefix"]), options),
|
|
746
|
+
execGit(gitArgs(["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", "."]), options)
|
|
747
|
+
]);
|
|
748
|
+
const prefix = stripTrailingLineBreak(prefixOutput);
|
|
749
|
+
const statuses = scopeGitStatusesToTheater(parseGitStatusPorcelainV1Z(statusOutput), prefix);
|
|
750
|
+
const truncated = statuses.length > GIT_STATUS_CAP;
|
|
751
|
+
return {
|
|
752
|
+
ok: true,
|
|
753
|
+
gitAvailable: true,
|
|
754
|
+
statuses: statuses.slice(0, GIT_STATUS_CAP),
|
|
755
|
+
...truncated ? { truncated: true } : {}
|
|
756
|
+
};
|
|
757
|
+
} catch {
|
|
758
|
+
return { ok: true, gitAvailable: false, statuses: [] };
|
|
661
759
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
760
|
+
}
|
|
761
|
+
function parseGitStatusPorcelainV1Z(output) {
|
|
762
|
+
const records = output.split("\0");
|
|
763
|
+
const statuses = [];
|
|
764
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
765
|
+
const record = records[index];
|
|
766
|
+
if (!record || record.length < 4 || record[2] !== " ") continue;
|
|
767
|
+
const indexStatus = record[0] ?? " ";
|
|
768
|
+
const worktreeStatus = record[1] ?? " ";
|
|
769
|
+
const statusPair = `${indexStatus}${worktreeStatus}`;
|
|
770
|
+
const gitPath = record.slice(3);
|
|
771
|
+
const isRenameOrCopy = statusPair.includes("R") || statusPair.includes("C");
|
|
772
|
+
const status = classifyGitStatus(statusPair);
|
|
773
|
+
if (status && gitPath) statuses.push({ gitPath, status });
|
|
774
|
+
if (isRenameOrCopy && index + 1 < records.length) index += 1;
|
|
674
775
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
let replaced = false;
|
|
688
|
-
try {
|
|
689
|
-
const stats = await fs3.promises.stat(lexicalWatchPath);
|
|
690
|
-
replaced = stats.dev !== watchIdentity.device || stats.ino !== watchIdentity.inode;
|
|
691
|
-
} catch (error) {
|
|
692
|
-
replaced = error.code === "ENOENT";
|
|
693
|
-
}
|
|
694
|
-
if (entry.watchers.get(watchKey) !== expectedWatcher) return;
|
|
695
|
-
if (!replaced) continue;
|
|
696
|
-
invalidateWatcherTree(entry, watchKey);
|
|
697
|
-
await rearmPendingDirectories(theaterId, entry);
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
} finally {
|
|
701
|
-
identityCheckRunning = false;
|
|
702
|
-
}
|
|
703
|
-
};
|
|
704
|
-
const watcher = watcherFactory(watchPath, { recursive: entry.recursive }, (event, filename) => {
|
|
705
|
-
const currentWatcher = activeWatcher;
|
|
706
|
-
if (!currentWatcher || entry.watchers.get(watchKey) !== currentWatcher) return;
|
|
707
|
-
scheduleChange(entry, entry.recursive ? computeRelDir(filename) : watchKey);
|
|
708
|
-
if (!entry.recursive && event === "rename") {
|
|
709
|
-
if (watchKey !== "" && watchIdentity) void checkDirectoryIdentity(currentWatcher);
|
|
710
|
-
if (filename) {
|
|
711
|
-
const changedChildKey = path4.join(watchKey, filename);
|
|
712
|
-
entry.identityChecks.get(changedChildKey)?.();
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
if (!entry.recursive && entry.pendingDirectories.size > 0) {
|
|
716
|
-
void rearmPendingDirectories(theaterId, entry);
|
|
717
|
-
}
|
|
718
|
-
});
|
|
719
|
-
activeWatcher = watcher;
|
|
720
|
-
entry.watchers.set(watchKey, watcher);
|
|
721
|
-
if (watchIdentity) {
|
|
722
|
-
entry.identityChecks.set(watchKey, () => {
|
|
723
|
-
const currentWatcher = activeWatcher;
|
|
724
|
-
if (currentWatcher) void checkDirectoryIdentity(currentWatcher);
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
watcher.on("error", () => {
|
|
728
|
-
if (entry.watchers.get(watchKey) !== watcher) return;
|
|
729
|
-
try {
|
|
730
|
-
watcher.close();
|
|
731
|
-
} catch {
|
|
732
|
-
}
|
|
733
|
-
entry.watchers.delete(watchKey);
|
|
734
|
-
entry.identityChecks.delete(watchKey);
|
|
735
|
-
if (watchKey === "") {
|
|
736
|
-
closeEntry(entry);
|
|
737
|
-
if (entries.get(theaterId) === entry) entries.delete(theaterId);
|
|
738
|
-
} else {
|
|
739
|
-
const pending = entry.debounceTimers.get(watchKey);
|
|
740
|
-
if (pending) clearTimeout(pending);
|
|
741
|
-
entry.debounceTimers.delete(watchKey);
|
|
742
|
-
}
|
|
743
|
-
for (const notify of entry.stateSubscribers) notify("degraded");
|
|
744
|
-
});
|
|
776
|
+
return statuses;
|
|
777
|
+
}
|
|
778
|
+
function scopeGitStatusesToTheater(entries, prefix, separator = path5.sep) {
|
|
779
|
+
const statuses = [];
|
|
780
|
+
for (const entry of entries) {
|
|
781
|
+
if (prefix && !entry.gitPath.startsWith(prefix)) continue;
|
|
782
|
+
const relativeGitPath = prefix ? entry.gitPath.slice(prefix.length) : entry.gitPath;
|
|
783
|
+
const segments = relativeGitPath.split("/");
|
|
784
|
+
if (relativeGitPath === "" || relativeGitPath.startsWith("/") || segments.some((segment) => segment === "" || segment === "." || segment === "..")) continue;
|
|
785
|
+
const relativePath = segments.join(separator);
|
|
786
|
+
if (path5.isAbsolute(relativePath)) continue;
|
|
787
|
+
statuses.push({ path: relativePath, status: entry.status });
|
|
745
788
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
789
|
+
return statuses;
|
|
790
|
+
}
|
|
791
|
+
function classifyGitStatus(statusPair) {
|
|
792
|
+
if (statusPair === "??") return "untracked";
|
|
793
|
+
if (UNMERGED_STATUS_PAIRS.has(statusPair)) return "modified";
|
|
794
|
+
if (statusPair.includes("D")) return "deleted";
|
|
795
|
+
if ([...statusPair].some((status) => status === "A" || status === "M" || status === "R" || status === "C" || status === "T")) {
|
|
796
|
+
return "modified";
|
|
797
|
+
}
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
function executeGit(args, options) {
|
|
801
|
+
return new Promise((resolve, reject) => {
|
|
802
|
+
execFile(
|
|
803
|
+
"git",
|
|
804
|
+
[...args],
|
|
805
|
+
{
|
|
806
|
+
encoding: "utf8",
|
|
807
|
+
env: options.env,
|
|
808
|
+
killSignal: options.killSignal,
|
|
809
|
+
maxBuffer: options.maxBuffer,
|
|
810
|
+
shell: false,
|
|
811
|
+
timeout: options.timeout,
|
|
812
|
+
windowsHide: true
|
|
813
|
+
},
|
|
814
|
+
(error, stdout) => {
|
|
815
|
+
if (error) {
|
|
816
|
+
reject(error);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
resolve(stdout);
|
|
757
820
|
}
|
|
758
|
-
|
|
821
|
+
);
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
function sanitizeGitEnvironment(environment) {
|
|
825
|
+
const sanitized = {};
|
|
826
|
+
for (const [key, value] of Object.entries(environment)) {
|
|
827
|
+
const normalizedKey = key.toUpperCase();
|
|
828
|
+
if (normalizedKey.startsWith("GIT_") || normalizedKey === "LC_ALL" || normalizedKey === "SSH_ASKPASS" || normalizedKey === "SSH_ASKPASS_REQUIRE") continue;
|
|
829
|
+
if (value !== void 0) sanitized[key] = value;
|
|
759
830
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
831
|
+
sanitized.GIT_OPTIONAL_LOCKS = "0";
|
|
832
|
+
sanitized.GIT_TERMINAL_PROMPT = "0";
|
|
833
|
+
sanitized.GIT_ALLOW_PROTOCOL = "ssh:git:http:https:file";
|
|
834
|
+
sanitized.LC_ALL = "C";
|
|
835
|
+
return sanitized;
|
|
836
|
+
}
|
|
837
|
+
function stripTrailingLineBreak(output) {
|
|
838
|
+
if (!output.endsWith("\n")) return output;
|
|
839
|
+
const withoutLineFeed = output.slice(0, -1);
|
|
840
|
+
return withoutLineFeed.endsWith("\r") ? withoutLineFeed.slice(0, -1) : withoutLineFeed;
|
|
841
|
+
}
|
|
842
|
+
function mapGitStatusFsError(error) {
|
|
843
|
+
const code = error.code;
|
|
844
|
+
if (code === "EACCES" || code === "EPERM") return new GitStatusPathError("forbidden");
|
|
845
|
+
if (code === "ENOENT" || code === "ENOTDIR") return new GitStatusPathError("not_found");
|
|
846
|
+
return new GitStatusPathError("invalid_path");
|
|
847
|
+
}
|
|
848
|
+
var SEARCH_DIRECTORY_CAP = 500;
|
|
849
|
+
var SEARCH_ENTRY_CAP = 25e3;
|
|
850
|
+
async function searchTheaterFiles(theaterPath, query, limit) {
|
|
851
|
+
const realRoot = await fsp.realpath(theaterPath);
|
|
852
|
+
const tokens = query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
|
|
853
|
+
const pending = [{ absolutePath: realRoot, relativePath: "" }];
|
|
854
|
+
const visited = /* @__PURE__ */ new Set();
|
|
855
|
+
const matches = [];
|
|
856
|
+
let directoryCount = 0;
|
|
857
|
+
let entryCount = 0;
|
|
858
|
+
while (pending.length > 0 && directoryCount < SEARCH_DIRECTORY_CAP && entryCount < SEARCH_ENTRY_CAP) {
|
|
859
|
+
const directory = pending.shift();
|
|
860
|
+
if (!directory || visited.has(directory.absolutePath)) continue;
|
|
861
|
+
visited.add(directory.absolutePath);
|
|
862
|
+
directoryCount += 1;
|
|
863
|
+
let entries;
|
|
784
864
|
try {
|
|
785
|
-
|
|
865
|
+
entries = await fsp.readdir(directory.absolutePath, { withFileTypes: true });
|
|
786
866
|
} catch {
|
|
787
|
-
|
|
788
|
-
return () => {
|
|
789
|
-
};
|
|
790
|
-
}
|
|
791
|
-
entries.set(theaterId, entry);
|
|
792
|
-
onState("watching");
|
|
793
|
-
return makeUnsubscribe(theaterId, onChange, onState, entry);
|
|
794
|
-
}
|
|
795
|
-
async function trackDirectory(theaterId, theaterPath, relativePath) {
|
|
796
|
-
const entry = entries.get(theaterId);
|
|
797
|
-
if (!entry || entry.recursive || relativePath === "" || entry.watchers.has(relativePath)) return;
|
|
798
|
-
if (entry.watchers.size >= LINUX_DIRECTORY_WATCH_CAP) {
|
|
799
|
-
for (const notify of entry.stateSubscribers) notify("degraded");
|
|
800
|
-
return;
|
|
867
|
+
continue;
|
|
801
868
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
entry.
|
|
808
|
-
|
|
809
|
-
|
|
869
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
870
|
+
for (const entry of entries) {
|
|
871
|
+
if (entryCount >= SEARCH_ENTRY_CAP) break;
|
|
872
|
+
entryCount += 1;
|
|
873
|
+
const relativePath = directory.relativePath ? `${directory.relativePath}/${entry.name}` : entry.name;
|
|
874
|
+
const absolutePath = path5.join(directory.absolutePath, entry.name);
|
|
875
|
+
let kind = entry.isDirectory() ? "dir" : entry.isFile() ? "file" : null;
|
|
876
|
+
let realPath = absolutePath;
|
|
877
|
+
if (entry.isSymbolicLink()) {
|
|
878
|
+
try {
|
|
879
|
+
realPath = await fsp.realpath(absolutePath);
|
|
880
|
+
if (!isContained(realRoot, realPath)) continue;
|
|
881
|
+
const stat = await fsp.stat(realPath);
|
|
882
|
+
kind = stat.isDirectory() ? "dir" : stat.isFile() ? "file" : null;
|
|
883
|
+
} catch {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (kind === "dir") {
|
|
888
|
+
if (!visited.has(realPath)) pending.push({ absolutePath: realPath, relativePath });
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
if (kind !== "file") continue;
|
|
892
|
+
const low = relativePath.toLocaleLowerCase();
|
|
893
|
+
if (tokens.every((token) => low.includes(token))) matches.push({ relativePath });
|
|
810
894
|
}
|
|
811
895
|
}
|
|
812
|
-
return
|
|
896
|
+
return matches.sort((left, right) => compareFileSearchItem(left, right, query)).slice(0, limit);
|
|
813
897
|
}
|
|
814
|
-
async function
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
898
|
+
async function handleFilesSearch(req, res, ctx) {
|
|
899
|
+
if (req.method !== "POST") {
|
|
900
|
+
ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (!ctx.host.security.isTerminalAuthorized(req)) {
|
|
904
|
+
ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const body = await ctx.host.http.readJsonBody(req);
|
|
908
|
+
if (!isPlainObject(body) || typeof body.theaterId !== "string" || typeof body.query !== "string" || body.query.trim() === "" || !Number.isInteger(body.limit) || body.limit < 1 || body.limit > 8) {
|
|
909
|
+
ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
|
|
913
|
+
if (!theaterPath) {
|
|
914
|
+
ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
818
917
|
try {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
]);
|
|
823
|
-
if (!isWithinRoot3(realTarget, realRoot)) return null;
|
|
824
|
-
const stats = await fs3.promises.stat(realTarget);
|
|
825
|
-
if (!stats.isDirectory()) return null;
|
|
826
|
-
return {
|
|
827
|
-
path: realTarget,
|
|
828
|
-
identity: { device: stats.dev, inode: stats.ino }
|
|
829
|
-
};
|
|
918
|
+
ctx.host.http.writeJson(res, 200, {
|
|
919
|
+
files: await searchTheaterFiles(theaterPath, body.query, body.limit)
|
|
920
|
+
});
|
|
830
921
|
} catch {
|
|
831
|
-
|
|
922
|
+
ctx.host.http.writeJson(res, 500, { error: "search_failed" });
|
|
832
923
|
}
|
|
833
924
|
}
|
|
834
|
-
function
|
|
835
|
-
const
|
|
836
|
-
|
|
925
|
+
function compareFileSearchItem(left, right, query) {
|
|
926
|
+
const lowQuery = query.trim().toLocaleLowerCase();
|
|
927
|
+
const leftName = left.relativePath.split("/").at(-1)?.toLocaleLowerCase() ?? "";
|
|
928
|
+
const rightName = right.relativePath.split("/").at(-1)?.toLocaleLowerCase() ?? "";
|
|
929
|
+
const leftRank = leftName === lowQuery ? 0 : leftName.startsWith(lowQuery) ? 1 : 2;
|
|
930
|
+
const rightRank = rightName === lowQuery ? 0 : rightName.startsWith(lowQuery) ? 1 : 2;
|
|
931
|
+
return leftRank - rightRank || left.relativePath.localeCompare(right.relativePath);
|
|
837
932
|
}
|
|
838
|
-
function
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
if (lastSep <= 0) return "";
|
|
842
|
-
return filename.slice(0, lastSep);
|
|
933
|
+
function isContained(root, candidate) {
|
|
934
|
+
const relative = path5.relative(root, candidate);
|
|
935
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path5.sep}`) && !path5.isAbsolute(relative);
|
|
843
936
|
}
|
|
844
|
-
|
|
845
|
-
// ../fleet-plugins/file-explorer/server/handlers.ts
|
|
846
937
|
var DEFAULT_CLIPBOARD_DEPENDENCIES = { copyPath: copyPathToClipboard };
|
|
847
|
-
var
|
|
938
|
+
var DEFAULT_REVEAL_DEPENDENCIES2 = { revealPath };
|
|
848
939
|
function isPlainObject(value) {
|
|
849
940
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
850
941
|
}
|
|
@@ -1031,7 +1122,7 @@ async function handleFilesClipboard(req, res, ctx, dependencies = DEFAULT_CLIPBO
|
|
|
1031
1122
|
ctx.host.http.writeJson(res, 500, { error: "clipboard_failed" });
|
|
1032
1123
|
}
|
|
1033
1124
|
}
|
|
1034
|
-
async function handleFilesReveal(req, res, ctx, dependencies =
|
|
1125
|
+
async function handleFilesReveal(req, res, ctx, dependencies = DEFAULT_REVEAL_DEPENDENCIES2) {
|
|
1035
1126
|
if (req.method !== "POST") {
|
|
1036
1127
|
ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
|
|
1037
1128
|
return;
|
|
@@ -1127,98 +1218,6 @@ function writePathActionError(res, ctx, error) {
|
|
|
1127
1218
|
const status = error.code === "not_found" ? 404 : 403;
|
|
1128
1219
|
ctx.host.http.writeJson(res, status, { error: error.code });
|
|
1129
1220
|
}
|
|
1130
|
-
var SEARCH_DIRECTORY_CAP = 500;
|
|
1131
|
-
var SEARCH_ENTRY_CAP = 25e3;
|
|
1132
|
-
async function searchTheaterFiles(theaterPath, query, limit) {
|
|
1133
|
-
const realRoot = await fs8.realpath(theaterPath);
|
|
1134
|
-
const tokens = query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
|
|
1135
|
-
const pending = [{ absolutePath: realRoot, relativePath: "" }];
|
|
1136
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1137
|
-
const matches = [];
|
|
1138
|
-
let directoryCount = 0;
|
|
1139
|
-
let entryCount = 0;
|
|
1140
|
-
while (pending.length > 0 && directoryCount < SEARCH_DIRECTORY_CAP && entryCount < SEARCH_ENTRY_CAP) {
|
|
1141
|
-
const directory = pending.shift();
|
|
1142
|
-
if (!directory || visited.has(directory.absolutePath)) continue;
|
|
1143
|
-
visited.add(directory.absolutePath);
|
|
1144
|
-
directoryCount += 1;
|
|
1145
|
-
let entries;
|
|
1146
|
-
try {
|
|
1147
|
-
entries = await fs8.readdir(directory.absolutePath, { withFileTypes: true });
|
|
1148
|
-
} catch {
|
|
1149
|
-
continue;
|
|
1150
|
-
}
|
|
1151
|
-
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
1152
|
-
for (const entry of entries) {
|
|
1153
|
-
if (entryCount >= SEARCH_ENTRY_CAP) break;
|
|
1154
|
-
entryCount += 1;
|
|
1155
|
-
const relativePath = directory.relativePath ? `${directory.relativePath}/${entry.name}` : entry.name;
|
|
1156
|
-
const absolutePath = path4.join(directory.absolutePath, entry.name);
|
|
1157
|
-
let kind = entry.isDirectory() ? "dir" : entry.isFile() ? "file" : null;
|
|
1158
|
-
let realPath = absolutePath;
|
|
1159
|
-
if (entry.isSymbolicLink()) {
|
|
1160
|
-
try {
|
|
1161
|
-
realPath = await fs8.realpath(absolutePath);
|
|
1162
|
-
if (!isContained(realRoot, realPath)) continue;
|
|
1163
|
-
const stat = await fs8.stat(realPath);
|
|
1164
|
-
kind = stat.isDirectory() ? "dir" : stat.isFile() ? "file" : null;
|
|
1165
|
-
} catch {
|
|
1166
|
-
continue;
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
if (kind === "dir") {
|
|
1170
|
-
if (!visited.has(realPath)) pending.push({ absolutePath: realPath, relativePath });
|
|
1171
|
-
continue;
|
|
1172
|
-
}
|
|
1173
|
-
if (kind !== "file") continue;
|
|
1174
|
-
const low = relativePath.toLocaleLowerCase();
|
|
1175
|
-
if (tokens.every((token) => low.includes(token))) matches.push({ relativePath });
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
return matches.sort((left, right) => compareFileSearchItem(left, right, query)).slice(0, limit);
|
|
1179
|
-
}
|
|
1180
|
-
async function handleFilesSearch(req, res, ctx) {
|
|
1181
|
-
if (req.method !== "POST") {
|
|
1182
|
-
ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
|
|
1183
|
-
return;
|
|
1184
|
-
}
|
|
1185
|
-
if (!ctx.host.security.isTerminalAuthorized(req)) {
|
|
1186
|
-
ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
|
-
const body = await ctx.host.http.readJsonBody(req);
|
|
1190
|
-
if (!isPlainObject2(body) || typeof body.theaterId !== "string" || typeof body.query !== "string" || body.query.trim() === "" || !Number.isInteger(body.limit) || body.limit < 1 || body.limit > 8) {
|
|
1191
|
-
ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
|
|
1195
|
-
if (!theaterPath) {
|
|
1196
|
-
ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
|
-
try {
|
|
1200
|
-
ctx.host.http.writeJson(res, 200, {
|
|
1201
|
-
files: await searchTheaterFiles(theaterPath, body.query, body.limit)
|
|
1202
|
-
});
|
|
1203
|
-
} catch {
|
|
1204
|
-
ctx.host.http.writeJson(res, 500, { error: "search_failed" });
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
function compareFileSearchItem(left, right, query) {
|
|
1208
|
-
const lowQuery = query.trim().toLocaleLowerCase();
|
|
1209
|
-
const leftName = left.relativePath.split("/").at(-1)?.toLocaleLowerCase() ?? "";
|
|
1210
|
-
const rightName = right.relativePath.split("/").at(-1)?.toLocaleLowerCase() ?? "";
|
|
1211
|
-
const leftRank = leftName === lowQuery ? 0 : leftName.startsWith(lowQuery) ? 1 : 2;
|
|
1212
|
-
const rightRank = rightName === lowQuery ? 0 : rightName.startsWith(lowQuery) ? 1 : 2;
|
|
1213
|
-
return leftRank - rightRank || left.relativePath.localeCompare(right.relativePath);
|
|
1214
|
-
}
|
|
1215
|
-
function isContained(root, candidate) {
|
|
1216
|
-
const relative = path4.relative(root, candidate);
|
|
1217
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${path4.sep}`) && !path4.isAbsolute(relative);
|
|
1218
|
-
}
|
|
1219
|
-
function isPlainObject2(value) {
|
|
1220
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1221
|
-
}
|
|
1222
1221
|
|
|
1223
1222
|
// ../fleet-plugins/file-explorer/routes.ts
|
|
1224
1223
|
var routes_default = definePlugin({
|