jishushell 0.4.24 → 0.5.15
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/INSTALL-NOTICE +11 -0
- package/apps/anythingllm-container.yaml +287 -0
- package/apps/browserless-chromium-container.yaml +90 -0
- package/apps/filebrowser-container.yaml +163 -0
- package/apps/hermes-container.yaml +36 -2
- package/apps/ollama-binary.yaml +91 -90
- package/apps/ollama-cpu-container.yaml +8 -1
- package/apps/ollama-with-hollama-binary.yaml +91 -90
- package/apps/openclaw-binary.yaml +38 -1
- package/apps/openclaw-container.yaml +45 -2
- package/apps/openclaw-with-ollama-container.yaml +11 -2
- package/apps/openclaw-with-searxng-container.yaml +26 -2
- package/apps/openwebui-container.yaml +45 -1
- package/apps/playwright-container.yaml +7 -1
- package/apps/searxng-container.yaml +58 -7
- package/apps/weknora-container.yaml +471 -0
- package/dist/cli/app.js +79 -9
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/doctor.d.ts +12 -12
- package/dist/cli/doctor.js +242 -55
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/llm.d.ts +4 -3
- package/dist/cli/llm.js +4 -3
- package/dist/cli/llm.js.map +1 -1
- package/dist/cli/panel.d.ts +6 -5
- package/dist/cli/panel.js +10 -9
- package/dist/cli/panel.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.js +99 -1
- package/dist/config.js.map +1 -1
- package/dist/control.d.ts +7 -6
- package/dist/control.js +7 -6
- package/dist/control.js.map +1 -1
- package/dist/install.js +3 -3
- package/dist/install.js.map +1 -1
- package/dist/routes/agent-apps.d.ts +1 -1
- package/dist/routes/agent-apps.js +1 -1
- package/dist/routes/apps.js +44 -11
- package/dist/routes/apps.js.map +1 -1
- package/dist/routes/auth.js +5 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/backup.js +64 -11
- package/dist/routes/backup.js.map +1 -1
- package/dist/routes/external-mounts.d.ts +17 -0
- package/dist/routes/external-mounts.js +73 -0
- package/dist/routes/external-mounts.js.map +1 -0
- package/dist/routes/file-mounts.d.ts +13 -0
- package/dist/routes/file-mounts.js +90 -0
- package/dist/routes/file-mounts.js.map +1 -0
- package/dist/routes/files-organize.d.ts +28 -0
- package/dist/routes/files-organize.js +167 -0
- package/dist/routes/files-organize.js.map +1 -0
- package/dist/routes/files.d.ts +31 -0
- package/dist/routes/files.js +321 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/instances.js +826 -17
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/internal.d.ts +2 -0
- package/dist/routes/internal.js +59 -0
- package/dist/routes/internal.js.map +1 -0
- package/dist/routes/llm.js +24 -35
- package/dist/routes/llm.js.map +1 -1
- package/dist/routes/setup.js +10 -10
- package/dist/routes/setup.js.map +1 -1
- package/dist/routes/system.js +1 -1
- package/dist/routes/system.js.map +1 -1
- package/dist/routes/webdav.d.ts +17 -0
- package/dist/routes/webdav.js +114 -0
- package/dist/routes/webdav.js.map +1 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +751 -20
- package/dist/server.js.map +1 -1
- package/dist/services/agent-apps/catalog.js +4 -3
- package/dist/services/agent-apps/catalog.js.map +1 -1
- package/dist/services/agent-apps/index.d.ts +1 -1
- package/dist/services/agent-apps/index.js +1 -1
- package/dist/services/agent-apps/installers/adapter.d.ts +1 -1
- package/dist/services/agent-apps/installers/adapter.js +1 -1
- package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
- package/dist/services/agent-apps/installers/shell-script.js +3 -3
- package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
- package/dist/services/agent-apps/types.d.ts +2 -2
- package/dist/services/agent-apps/types.js +1 -1
- package/dist/services/app/app-compiler.d.ts +1 -1
- package/dist/services/app/app-compiler.js +5 -5
- package/dist/services/app/app-compiler.js.map +1 -1
- package/dist/services/app/app-manager.d.ts +25 -1
- package/dist/services/app/app-manager.js +829 -150
- package/dist/services/app/app-manager.js.map +1 -1
- package/dist/services/app/custom-manager.js.map +1 -1
- package/dist/services/app/hermes-agent-manager.js +7 -4
- package/dist/services/app/hermes-agent-manager.js.map +1 -1
- package/dist/services/app/ollama-manager.js +1 -1
- package/dist/services/app/ollama-manager.js.map +1 -1
- package/dist/services/app/openclaw-manager.js +20 -3
- package/dist/services/app/openclaw-manager.js.map +1 -1
- package/dist/services/app/platform-transform.d.ts +32 -0
- package/dist/services/app/platform-transform.js +65 -0
- package/dist/services/app/platform-transform.js.map +1 -0
- package/dist/services/app/provide-resolver.d.ts +29 -0
- package/dist/services/app/provide-resolver.js +112 -0
- package/dist/services/app/provide-resolver.js.map +1 -0
- package/dist/services/app-passwords.d.ts +61 -0
- package/dist/services/app-passwords.js +173 -0
- package/dist/services/app-passwords.js.map +1 -0
- package/dist/services/backup-manager.d.ts +11 -0
- package/dist/services/backup-manager.js +177 -4
- package/dist/services/backup-manager.js.map +1 -1
- package/dist/services/capability-endpoint-validator.d.ts +41 -0
- package/dist/services/capability-endpoint-validator.js +104 -0
- package/dist/services/capability-endpoint-validator.js.map +1 -0
- package/dist/services/capability-health.d.ts +16 -0
- package/dist/services/capability-health.js +121 -0
- package/dist/services/capability-health.js.map +1 -0
- package/dist/services/capability-registry.d.ts +106 -0
- package/dist/services/capability-registry.js +313 -0
- package/dist/services/capability-registry.js.map +1 -0
- package/dist/services/connection-apply.d.ts +91 -0
- package/dist/services/connection-apply.js +475 -0
- package/dist/services/connection-apply.js.map +1 -0
- package/dist/services/connection-resolver.d.ts +65 -0
- package/dist/services/connection-resolver.js +281 -0
- package/dist/services/connection-resolver.js.map +1 -0
- package/dist/services/connection-transactor.d.ts +39 -0
- package/dist/services/connection-transactor.js +351 -0
- package/dist/services/connection-transactor.js.map +1 -0
- package/dist/services/external-mounts.d.ts +40 -0
- package/dist/services/external-mounts.js +187 -0
- package/dist/services/external-mounts.js.map +1 -0
- package/dist/services/files-manager.d.ts +252 -0
- package/dist/services/files-manager.js +1075 -0
- package/dist/services/files-manager.js.map +1 -0
- package/dist/services/files-mounts.d.ts +42 -0
- package/dist/services/files-mounts.js +207 -0
- package/dist/services/files-mounts.js.map +1 -0
- package/dist/services/instance-manager.d.ts +13 -0
- package/dist/services/instance-manager.js +138 -46
- package/dist/services/instance-manager.js.map +1 -1
- package/dist/services/llm-proxy/index.d.ts +16 -2
- package/dist/services/llm-proxy/index.js +48 -44
- package/dist/services/llm-proxy/index.js.map +1 -1
- package/dist/services/llm-proxy/probe.d.ts +6 -0
- package/dist/services/llm-proxy/probe.js +85 -0
- package/dist/services/llm-proxy/probe.js.map +1 -0
- package/dist/services/llm-proxy/ssrf.d.ts +1 -0
- package/dist/services/llm-proxy/ssrf.js +24 -9
- package/dist/services/llm-proxy/ssrf.js.map +1 -1
- package/dist/services/nomad-manager.d.ts +4 -0
- package/dist/services/nomad-manager.js +428 -35
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/organize/applier.d.ts +46 -0
- package/dist/services/organize/applier.js +218 -0
- package/dist/services/organize/applier.js.map +1 -0
- package/dist/services/organize/rules.d.ts +57 -0
- package/dist/services/organize/rules.js +286 -0
- package/dist/services/organize/rules.js.map +1 -0
- package/dist/services/organize/scanner.d.ts +50 -0
- package/dist/services/organize/scanner.js +366 -0
- package/dist/services/organize/scanner.js.map +1 -0
- package/dist/services/organize/store.d.ts +14 -0
- package/dist/services/organize/store.js +82 -0
- package/dist/services/organize/store.js.map +1 -0
- package/dist/services/panel-manager.js +20 -1
- package/dist/services/panel-manager.js.map +1 -1
- package/dist/services/process-manager.js +4 -3
- package/dist/services/process-manager.js.map +1 -1
- package/dist/services/runtime/adapters/hermes.d.ts +30 -1
- package/dist/services/runtime/adapters/hermes.js +219 -6
- package/dist/services/runtime/adapters/hermes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw-mcporter.d.ts +45 -0
- package/dist/services/runtime/adapters/openclaw-mcporter.js +108 -0
- package/dist/services/runtime/adapters/openclaw-mcporter.js.map +1 -0
- package/dist/services/runtime/adapters/openclaw-routes.d.ts +8 -2
- package/dist/services/runtime/adapters/openclaw-routes.js +68 -0
- package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw.d.ts +177 -0
- package/dist/services/runtime/adapters/openclaw.js +1171 -11
- package/dist/services/runtime/adapters/openclaw.js.map +1 -1
- package/dist/services/runtime/instance.d.ts +1 -1
- package/dist/services/runtime/instance.js +1 -1
- package/dist/services/runtime/instance.js.map +1 -1
- package/dist/services/runtime/mcp-shims/anythingllm-shim.d.ts +46 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js +281 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/drive-shim.d.ts +54 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js +489 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/firewall.d.ts +26 -0
- package/dist/services/runtime/mcp-shims/firewall.js +129 -0
- package/dist/services/runtime/mcp-shims/firewall.js.map +1 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.d.ts +27 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.js +125 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.d.ts +83 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.js +127 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.js.map +1 -0
- package/dist/services/runtime/migrations.d.ts +8 -0
- package/dist/services/runtime/migrations.js +100 -0
- package/dist/services/runtime/migrations.js.map +1 -1
- package/dist/services/runtime/types.d.ts +46 -0
- package/dist/services/setup-manager.js +99 -24
- package/dist/services/setup-manager.js.map +1 -1
- package/dist/services/suggestions.d.ts +27 -0
- package/dist/services/suggestions.js +133 -0
- package/dist/services/suggestions.js.map +1 -0
- package/dist/services/task-registry.js +4 -2
- package/dist/services/task-registry.js.map +1 -1
- package/dist/services/telemetry/device-fingerprint.d.ts +1 -1
- package/dist/services/telemetry/device-fingerprint.js +1 -1
- package/dist/services/types-shim.d.ts +16 -0
- package/dist/services/types-shim.js +2 -0
- package/dist/services/types-shim.js.map +1 -0
- package/dist/services/webdav/server.d.ts +24 -0
- package/dist/services/webdav/server.js +420 -0
- package/dist/services/webdav/server.js.map +1 -0
- package/dist/services/webdav/xml-builder.d.ts +73 -0
- package/dist/services/webdav/xml-builder.js +156 -0
- package/dist/services/webdav/xml-builder.js.map +1 -0
- package/dist/services/workspace-builder.d.ts +29 -0
- package/dist/services/workspace-builder.js +188 -0
- package/dist/services/workspace-builder.js.map +1 -0
- package/dist/types.d.ts +231 -1
- package/dist/utils/instance-lock.d.ts +22 -0
- package/dist/utils/instance-lock.js +48 -0
- package/dist/utils/instance-lock.js.map +1 -0
- package/dist/utils/path-locks.d.ts +30 -0
- package/dist/utils/path-locks.js +63 -0
- package/dist/utils/path-locks.js.map +1 -0
- package/dist/utils/path-safety.d.ts +41 -0
- package/dist/utils/path-safety.js +119 -0
- package/dist/utils/path-safety.js.map +1 -0
- package/dist/utils/safe-json.js +55 -22
- package/dist/utils/safe-json.js.map +1 -1
- package/dist/utils/safe-write.d.ts +24 -0
- package/dist/utils/safe-write.js +82 -0
- package/dist/utils/safe-write.js.map +1 -0
- package/install/jishu-install.sh +323 -27
- package/install/jishu-uninstall.sh +353 -20
- package/package.json +18 -1
- package/public/assets/Dashboard-BdWPtroF.js +1 -0
- package/public/assets/{HermesChatPanel-mFSureyc.js → HermesChatPanel-B_2HlVBQ.js} +1 -1
- package/public/assets/HermesConfigForm-DVlhg3WV.js +4 -0
- package/public/assets/{InitPassword-CVA8wQA6.js → InitPassword-D7glTExX.js} +1 -1
- package/public/assets/InstanceDetail-CxSy2cpe.js +92 -0
- package/public/assets/{Login-BWsZH2mu.js → Login-Cfr5c2sv.js} +1 -1
- package/public/assets/NewInstance-BIYDmJis.js +1 -0
- package/public/assets/ProviderRecommendations-BuRnvRcI.js +1 -0
- package/public/assets/Settings-Cc-tYBil.js +1 -0
- package/public/assets/Setup-lGZEk5jq.js +1 -0
- package/public/assets/{WeixinLoginPanel-CnjR8xMu.js → WeixinLoginPanel-CoGqzxeV.js} +2 -2
- package/public/assets/index-87IJXG-w.css +1 -0
- package/public/assets/index-BZc5zH7u.js +19 -0
- package/public/assets/providers-DtNXh9JD.js +1 -0
- package/public/assets/registry-BWnkJgZ1.js +2 -0
- package/public/assets/{usePolling-Do5Erqm_.js → usePolling-CwwT9KrC.js} +1 -1
- package/public/assets/{vendor-i18n-ucpM0OR0.js → vendor-i18n-y9V7Sfuu.js} +1 -1
- package/public/assets/{vendor-react-Bk1hRGiY.js → vendor-react-BWrEVJVb.js} +6 -6
- package/public/index.html +4 -4
- package/scripts/check-app-spec.mjs +457 -0
- package/scripts/check-i18n.mjs +154 -0
- package/scripts/check-new-file-tests.mjs +230 -0
- package/scripts/check-quarantine-expiry.mjs +105 -0
- package/scripts/perf/README.md +49 -0
- package/scripts/perf/auth.js +99 -0
- package/scripts/perf/config.js +63 -0
- package/scripts/perf/instances.js +143 -0
- package/scripts/perf/proxy.js +96 -0
- package/scripts/run.sh +4 -4
- package/scripts/smoke/files-w1.sh +142 -0
- package/scripts/smoke-backend.mjs +122 -0
- package/scripts/smoke-post-publish.mjs +346 -0
- package/public/assets/Dashboard-B-JoOjBQ.js +0 -1
- package/public/assets/HermesConfigForm-DvR05LK1.js +0 -4
- package/public/assets/InstanceDetail-DcZW2QGO.js +0 -91
- package/public/assets/NewInstance-BCIrAd86.js +0 -1
- package/public/assets/Settings-xkDcduFz.js +0 -1
- package/public/assets/Setup-Cfuwj4gV.js +0 -1
- package/public/assets/index-CPhVFEsx.css +0 -1
- package/public/assets/index-DQsM6Joa.js +0 -19
- package/public/assets/providers-V-vwrExZ.js +0 -1
- package/public/assets/registry-B4UFJdpA.js +0 -2
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suggestions engine — scans every app instance for unbound `requires`
|
|
3
|
+
* slots that *could* be filled from the current capability registry, and
|
|
4
|
+
* emits one suggestion per (consumer, slot, candidate). The Dashboard
|
|
5
|
+
* polls this so the user gets a low-friction "you can connect X to Y"
|
|
6
|
+
* prompt without having to dig into each instance.
|
|
7
|
+
*
|
|
8
|
+
* Dismissals are stored on the consumer's `instance.json.dismissedSuggestions`
|
|
9
|
+
* (per-slot, 30-day TTL).
|
|
10
|
+
*/
|
|
11
|
+
import * as capabilityRegistry from "./capability-registry.js";
|
|
12
|
+
import * as instanceManager from "./instance-manager.js";
|
|
13
|
+
import { getApp } from "./app/app-manager.js";
|
|
14
|
+
import { resolveConnections } from "./connection-resolver.js";
|
|
15
|
+
import { loadCapabilitySpecForLegacyInstance } from "./runtime/migrations.js";
|
|
16
|
+
import { safeReadJson, safeWriteJson } from "../utils/safe-json.js";
|
|
17
|
+
const DISMISS_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
18
|
+
export function computeSuggestions() {
|
|
19
|
+
const out = [];
|
|
20
|
+
const allInstances = instanceManager.listInstances();
|
|
21
|
+
for (const inst of allInstances) {
|
|
22
|
+
// App-installed instances (yaml apps from the Apps page) carry an
|
|
23
|
+
// `app_id` and resolve their spec via the registry. Legacy instances
|
|
24
|
+
// (hermes / openclaw created via the classic NewInstance flow) have
|
|
25
|
+
// no `app_id`; their `requires` come from the matching yaml template
|
|
26
|
+
// synthesized in-memory by `loadCapabilitySpecForLegacyInstance`.
|
|
27
|
+
// Without this branch, brand-new hermes / openclaw instances never
|
|
28
|
+
// surface suggestions even when running candidates exist.
|
|
29
|
+
const appId = inst.app_id;
|
|
30
|
+
let spec = null;
|
|
31
|
+
if (appId) {
|
|
32
|
+
const appData = getApp(appId);
|
|
33
|
+
if (!appData)
|
|
34
|
+
continue;
|
|
35
|
+
spec = appData.spec;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
spec = loadCapabilitySpecForLegacyInstance(inst);
|
|
39
|
+
if (!spec)
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const persisted = (inst.connections ?? {});
|
|
43
|
+
const dismissed = (inst.dismissedSuggestions ?? []);
|
|
44
|
+
const dismissedSlots = new Set(dismissed
|
|
45
|
+
.filter((d) => new Date(d.until).getTime() > Date.now())
|
|
46
|
+
.map((d) => d.slot));
|
|
47
|
+
let pending = [];
|
|
48
|
+
try {
|
|
49
|
+
const r = resolveConnections(spec, { connections: persisted }, "preCreate");
|
|
50
|
+
pending = r.pending;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
for (const p of pending) {
|
|
56
|
+
if (dismissedSlots.has(p.slot))
|
|
57
|
+
continue;
|
|
58
|
+
const candidates = enumerateCandidates(p.capability);
|
|
59
|
+
// Same agent-on-agent guard as the Connections-tab dropdown: hermes
|
|
60
|
+
// and openclaw consuming `llm` should never be offered another
|
|
61
|
+
// agent's `llm-agent` (would chain agent → agent → real model).
|
|
62
|
+
const consumerAgentType = String(inst?.agentType ?? "");
|
|
63
|
+
const consumerIsAgent = consumerAgentType === "hermes" || consumerAgentType === "openclaw";
|
|
64
|
+
const running = candidates.filter((e) => {
|
|
65
|
+
if (e.status !== "running")
|
|
66
|
+
return false;
|
|
67
|
+
if (e.instanceId === inst.id)
|
|
68
|
+
return false;
|
|
69
|
+
if (consumerIsAgent && p.capability === "llm" && e.capability === "llm-agent")
|
|
70
|
+
return false;
|
|
71
|
+
return true;
|
|
72
|
+
});
|
|
73
|
+
if (running.length === 0)
|
|
74
|
+
continue;
|
|
75
|
+
// Single candidate → suggest it; multiple → suggest the running ones.
|
|
76
|
+
const top = running[0];
|
|
77
|
+
out.push({
|
|
78
|
+
id: `${inst.id}::${p.slot}::${top.instanceId}::${top.capability}`,
|
|
79
|
+
consumerInstanceId: inst.id,
|
|
80
|
+
consumerName: inst.name ?? inst.id,
|
|
81
|
+
appId: appId ?? "",
|
|
82
|
+
slot: p.slot,
|
|
83
|
+
capability: p.capability,
|
|
84
|
+
candidate: {
|
|
85
|
+
providerId: top.instanceId,
|
|
86
|
+
capability: top.capability,
|
|
87
|
+
name: top.name,
|
|
88
|
+
protocol: top.protocol,
|
|
89
|
+
status: top.status,
|
|
90
|
+
hostPort: top.hostPort,
|
|
91
|
+
path: top.path,
|
|
92
|
+
},
|
|
93
|
+
reason: p.reason,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
function enumerateCandidates(capability) {
|
|
100
|
+
const isCategoryToken = ["llm", "search", "browser", "mcp"].includes(capability);
|
|
101
|
+
if (!isCategoryToken) {
|
|
102
|
+
return capabilityRegistry.listProviders(capability);
|
|
103
|
+
}
|
|
104
|
+
const file = capabilityRegistry.snapshot();
|
|
105
|
+
const out = [];
|
|
106
|
+
for (const [cap, list] of Object.entries(file.providersByCapability ?? {})) {
|
|
107
|
+
if (cap.startsWith(capability + "-") || cap === capability) {
|
|
108
|
+
out.push(...list);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Mark a suggestion as dismissed. Persisted on the consumer instance so
|
|
115
|
+
* subsequent computeSuggestions() invocations skip it for the TTL window.
|
|
116
|
+
*
|
|
117
|
+
* `id` follows the format `${consumerInstanceId}::${slot}::...` — the
|
|
118
|
+
* leading two segments are enough to identify the slot to dismiss.
|
|
119
|
+
*/
|
|
120
|
+
export async function dismissSuggestion(id) {
|
|
121
|
+
const parts = id.split("::");
|
|
122
|
+
if (parts.length < 2)
|
|
123
|
+
throw new Error(`Invalid suggestion id: ${id}`);
|
|
124
|
+
const [consumerInstanceId, slot] = parts;
|
|
125
|
+
const path = instanceManager.instanceMetaPath(consumerInstanceId);
|
|
126
|
+
const cur = (safeReadJson(path, `instance:${consumerInstanceId}`) ?? {});
|
|
127
|
+
const list = Array.isArray(cur.dismissedSuggestions) ? cur.dismissedSuggestions : [];
|
|
128
|
+
const filtered = list.filter((d) => d.slot !== slot);
|
|
129
|
+
filtered.push({ slot, until: new Date(Date.now() + DISMISS_TTL_MS).toISOString() });
|
|
130
|
+
cur.dismissedSuggestions = filtered;
|
|
131
|
+
safeWriteJson(path, cur);
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=suggestions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suggestions.js","sourceRoot":"","sources":["../../src/services/suggestions.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,13 +15,15 @@ const MAX_TASK_EVENTS = 500;
|
|
|
15
15
|
setInterval(() => {
|
|
16
16
|
const now = Date.now();
|
|
17
17
|
for (const [id, task] of tasks) {
|
|
18
|
-
|
|
18
|
+
const parts = id.split("-");
|
|
19
|
+
const createdAt = Number(parts[parts.length - 2] || 0);
|
|
20
|
+
if (task.status !== "running" && Number.isFinite(createdAt) && now - createdAt > TASK_MAX_AGE) {
|
|
19
21
|
tasks.delete(id);
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
}, 60_000).unref();
|
|
23
25
|
export function createTask(name) {
|
|
24
|
-
const id = `${name}-${Date.now()}`;
|
|
26
|
+
const id = `${name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
25
27
|
const task = { id, name, status: "running", events: [], listeners: new Set() };
|
|
26
28
|
tasks.set(id, task);
|
|
27
29
|
return task;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-registry.js","sourceRoot":"","sources":["../../src/services/task-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AA4BH,MAAM,KAAK,GAAG,IAAI,GAAG,EAAgB,CAAC;AACtC,MAAM,YAAY,GAAG,OAAO,CAAC;AAC7B,MAAM,eAAe,GAAG,GAAG,CAAC;AAE5B,WAAW,CAAC,GAAG,EAAE;IACf,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC;QAC/B,
|
|
1
|
+
{"version":3,"file":"task-registry.js","sourceRoot":"","sources":["../../src/services/task-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AA4BH,MAAM,KAAK,GAAG,IAAI,GAAG,EAAgB,CAAC;AACtC,MAAM,YAAY,GAAG,OAAO,CAAC;AAC7B,MAAM,eAAe,GAAG,GAAG,CAAC;AAE5B,WAAW,CAAC,GAAG,EAAE;IACf,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC5B,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACvD,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,GAAG,GAAG,SAAS,GAAG,YAAY,EAAE,CAAC;YAC9F,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;AACH,CAAC,EAAE,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC;AAEnB,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,EAAE,GAAG,GAAG,IAAI,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;IAC7E,MAAM,IAAI,GAAS,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC;IACrF,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IACpB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAU,EAAE,KAAgB;IACnD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,eAAe,EAAE,CAAC;QACzC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,eAAe,CAAC,CAAC;IAC9D,CAAC;IACD,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QACtC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,EAAU;IAChC,OAAO,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,EAAU;IACxC,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC3B,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;KACzB,CAAC;AACJ,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,eAAe,CAAC,UAAmB;IACjD,MAAM,MAAM,GAAwC,EAAE,CAAC;IACvD,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC;QAC/B,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS;YAAE,SAAS;QACxC,IAAI,UAAU,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;YAAE,SAAS;QAC9D,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IACvC,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAAU,EAAE,QAAoC;IAC5E,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC3B,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC7B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC/C,CAAC"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Cross-platform device fingerprint generation.
|
|
3
3
|
*
|
|
4
4
|
* Supported platforms:
|
|
5
|
-
* - Linux: Jetson Orin/Thor, RK3588, CIX P1,
|
|
5
|
+
* - Linux: Jetson Orin/Thor, RK3588, CIX P1, Huixi R1, Raspberry Pi
|
|
6
6
|
* - macOS: Mac Mini, Mac Studio
|
|
7
7
|
*
|
|
8
8
|
* Fingerprint = SHA-256(machine_id_or_uuid + mac)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Cross-platform device fingerprint generation.
|
|
3
3
|
*
|
|
4
4
|
* Supported platforms:
|
|
5
|
-
* - Linux: Jetson Orin/Thor, RK3588, CIX P1,
|
|
5
|
+
* - Linux: Jetson Orin/Thor, RK3588, CIX P1, Huixi R1, Raspberry Pi
|
|
6
6
|
* - macOS: Mac Mini, Mac Studio
|
|
7
7
|
*
|
|
8
8
|
* Fingerprint = SHA-256(machine_id_or_uuid + mac)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight `AppInstance` shim exported from a stable module path so PR 4
|
|
3
|
+
* apply hooks don't need to import from app-manager.ts (which would create
|
|
4
|
+
* a circular dependency: app-manager → connection-apply → app-manager).
|
|
5
|
+
*/
|
|
6
|
+
import type { InstanceConnections } from "../types.js";
|
|
7
|
+
export interface AppInstance {
|
|
8
|
+
id: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
app_id?: string;
|
|
12
|
+
connections?: InstanceConnections;
|
|
13
|
+
"connections-env"?: Record<string, string>;
|
|
14
|
+
schemaVersion?: number;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-shim.js","sourceRoot":"","sources":["../../src/services/types-shim.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { FastifyRequest, FastifyReply } from "fastify";
|
|
2
|
+
import { FilesManager } from "../files-manager.js";
|
|
3
|
+
import { type AppPassword } from "../app-passwords.js";
|
|
4
|
+
export interface WebdavContext {
|
|
5
|
+
filesManager: FilesManager;
|
|
6
|
+
/** WebDAV root URL prefix, e.g. "/webdav" — no trailing slash. */
|
|
7
|
+
mountPrefix: string;
|
|
8
|
+
/** Authenticated app password (already verified by auth middleware). */
|
|
9
|
+
authedAs: AppPassword;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Strip the mount prefix from req.url and decode each segment, then
|
|
13
|
+
* normalize: leading / trailing slash removed, "." collapsed. Returns
|
|
14
|
+
* the files/-relative path the rest of the WebDAV server uses.
|
|
15
|
+
*/
|
|
16
|
+
export declare function extractRel(reqUrl: string, mountPrefix: string): string;
|
|
17
|
+
export declare function handleOptions(_ctx: WebdavContext, _req: FastifyRequest, reply: FastifyReply): Promise<void>;
|
|
18
|
+
export declare function handlePropfind(ctx: WebdavContext, req: FastifyRequest, reply: FastifyReply): Promise<void>;
|
|
19
|
+
export declare function handleGet(ctx: WebdavContext, req: FastifyRequest, reply: FastifyReply, bodyless: boolean): Promise<void>;
|
|
20
|
+
export declare function handlePut(ctx: WebdavContext, req: FastifyRequest, reply: FastifyReply): Promise<void>;
|
|
21
|
+
export declare function handleDelete(ctx: WebdavContext, req: FastifyRequest, reply: FastifyReply): Promise<void>;
|
|
22
|
+
export declare function handleMkcol(ctx: WebdavContext, req: FastifyRequest, reply: FastifyReply): Promise<void>;
|
|
23
|
+
export declare function handleMove(ctx: WebdavContext, req: FastifyRequest, reply: FastifyReply): Promise<void>;
|
|
24
|
+
export declare function handleCopy(ctx: WebdavContext, req: FastifyRequest, reply: FastifyReply): Promise<void>;
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebDAV server logic for the AI NAS (M3 W4 PR-3).
|
|
3
|
+
*
|
|
4
|
+
* Implements a focused subset of RFC 4918 — the methods iOS Files,
|
|
5
|
+
* macOS Finder, Windows Explorer, and FX (Android) actually issue:
|
|
6
|
+
*
|
|
7
|
+
* OPTIONS capability advertisement
|
|
8
|
+
* PROPFIND list (Depth: 0 / 1)
|
|
9
|
+
* GET stream a file
|
|
10
|
+
* HEAD same headers as GET, no body
|
|
11
|
+
* PUT upload (octet-stream pass-through; overwrites are normal)
|
|
12
|
+
* DELETE soft-delete to .trash via FilesManager
|
|
13
|
+
* MKCOL create directory
|
|
14
|
+
* MOVE rename / move within the WebDAV root
|
|
15
|
+
* COPY shallow file copy (no recursive copy in this PR)
|
|
16
|
+
*
|
|
17
|
+
* Locking (LOCK / UNLOCK / PROPPATCH) is intentionally NOT implemented;
|
|
18
|
+
* we return 501 — clients fall back to optimistic concurrency.
|
|
19
|
+
*
|
|
20
|
+
* All request paths are validated through the FilesManager surface so
|
|
21
|
+
* we share path-safety, symlink defense, and the audit log with the
|
|
22
|
+
* /api/files routes. Authentication runs upstream (see routes/webdav.ts).
|
|
23
|
+
*/
|
|
24
|
+
import * as fs from "node:fs";
|
|
25
|
+
import * as path from "node:path";
|
|
26
|
+
import { FilesError, } from "../files-manager.js";
|
|
27
|
+
import { buildMultistatus, parseDepth, } from "./xml-builder.js";
|
|
28
|
+
import { isAllowed } from "../app-passwords.js";
|
|
29
|
+
import { resolveSafe } from "../../utils/path-safety.js";
|
|
30
|
+
import { atomicWriteStream } from "../../utils/safe-write.js";
|
|
31
|
+
import { withPathLock } from "../../utils/path-locks.js";
|
|
32
|
+
import { FILES_ROOT } from "../../config.js";
|
|
33
|
+
// ── Path extraction ────────────────────────────────
|
|
34
|
+
/**
|
|
35
|
+
* Strip the mount prefix from req.url and decode each segment, then
|
|
36
|
+
* normalize: leading / trailing slash removed, "." collapsed. Returns
|
|
37
|
+
* the files/-relative path the rest of the WebDAV server uses.
|
|
38
|
+
*/
|
|
39
|
+
export function extractRel(reqUrl, mountPrefix) {
|
|
40
|
+
const cleanPrefix = mountPrefix.replace(/\/+$/, "");
|
|
41
|
+
let urlPath = reqUrl;
|
|
42
|
+
// Strip query string — WebDAV doesn't use it
|
|
43
|
+
const q = urlPath.indexOf("?");
|
|
44
|
+
if (q >= 0)
|
|
45
|
+
urlPath = urlPath.slice(0, q);
|
|
46
|
+
if (urlPath.startsWith(cleanPrefix))
|
|
47
|
+
urlPath = urlPath.slice(cleanPrefix.length);
|
|
48
|
+
if (urlPath.startsWith("/"))
|
|
49
|
+
urlPath = urlPath.slice(1);
|
|
50
|
+
if (urlPath.endsWith("/"))
|
|
51
|
+
urlPath = urlPath.slice(0, -1);
|
|
52
|
+
// Decode each segment so `%E7%AC%94%E8%AE%B0` becomes `笔记`
|
|
53
|
+
return urlPath
|
|
54
|
+
.split("/")
|
|
55
|
+
.map((s) => {
|
|
56
|
+
try {
|
|
57
|
+
return decodeURIComponent(s);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return s;
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
.filter((s) => s !== "")
|
|
64
|
+
.join("/");
|
|
65
|
+
}
|
|
66
|
+
// ── Auth scope check ───────────────────────────────
|
|
67
|
+
function checkScope(ctx, rel, needsWrite) {
|
|
68
|
+
return isAllowed(ctx.authedAs, rel, needsWrite);
|
|
69
|
+
}
|
|
70
|
+
function send403(reply, reason) {
|
|
71
|
+
reply
|
|
72
|
+
.code(403)
|
|
73
|
+
.header("Content-Type", "text/plain; charset=utf-8")
|
|
74
|
+
.send(`forbidden: ${reason ?? "scope mismatch"}`);
|
|
75
|
+
}
|
|
76
|
+
// ── OPTIONS ────────────────────────────────────────
|
|
77
|
+
export async function handleOptions(_ctx, _req, reply) {
|
|
78
|
+
reply
|
|
79
|
+
.code(200)
|
|
80
|
+
.header("Allow", "OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, MKCOL, MOVE, COPY")
|
|
81
|
+
.header("DAV", "1")
|
|
82
|
+
.header("MS-Author-Via", "DAV") // Windows Explorer compatibility
|
|
83
|
+
.send();
|
|
84
|
+
}
|
|
85
|
+
// ── PROPFIND ───────────────────────────────────────
|
|
86
|
+
export async function handlePropfind(ctx, req, reply) {
|
|
87
|
+
const rel = extractRel(req.url, ctx.mountPrefix);
|
|
88
|
+
const scope = checkScope(ctx, rel, false);
|
|
89
|
+
if (!scope.allowed)
|
|
90
|
+
return send403(reply, scope.reason);
|
|
91
|
+
const depth = parseDepth(req.headers.depth);
|
|
92
|
+
const targetAbs = resolveSafe(FILES_ROOT, rel);
|
|
93
|
+
let stat;
|
|
94
|
+
try {
|
|
95
|
+
const ls = fs.lstatSync(targetAbs);
|
|
96
|
+
if (ls.isSymbolicLink()) {
|
|
97
|
+
reply.code(403).send("symlinks not exposed via WebDAV");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
stat = fs.statSync(targetAbs);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
if (e?.code === "ENOENT") {
|
|
104
|
+
reply.code(404).send();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
throw e;
|
|
108
|
+
}
|
|
109
|
+
const resources = [
|
|
110
|
+
statToResource(rel, stat, basename(rel) || "files"),
|
|
111
|
+
];
|
|
112
|
+
if (depth >= 1 && stat.isDirectory()) {
|
|
113
|
+
let entries;
|
|
114
|
+
try {
|
|
115
|
+
entries = fs.readdirSync(targetAbs);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
entries = [];
|
|
119
|
+
}
|
|
120
|
+
for (const name of entries) {
|
|
121
|
+
if (name.startsWith("."))
|
|
122
|
+
continue; // hidden / .trash never exposed
|
|
123
|
+
const childAbs = path.join(targetAbs, name);
|
|
124
|
+
let childStat;
|
|
125
|
+
try {
|
|
126
|
+
const ls = fs.lstatSync(childAbs);
|
|
127
|
+
if (ls.isSymbolicLink())
|
|
128
|
+
continue;
|
|
129
|
+
childStat = fs.statSync(childAbs);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const childRel = rel ? `${rel}/${name}` : name;
|
|
135
|
+
const childScope = checkScope(ctx, childRel, false);
|
|
136
|
+
if (!childScope.allowed)
|
|
137
|
+
continue; // hide outside-scope children
|
|
138
|
+
resources.push(statToResource(childRel, childStat, name));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const xml = buildMultistatus(ctx.mountPrefix, resources);
|
|
142
|
+
reply
|
|
143
|
+
.code(207)
|
|
144
|
+
.header("Content-Type", 'application/xml; charset="utf-8"')
|
|
145
|
+
.header("DAV", "1")
|
|
146
|
+
.send(xml);
|
|
147
|
+
}
|
|
148
|
+
function statToResource(rel, stat, displayName) {
|
|
149
|
+
const isDir = stat.isDirectory();
|
|
150
|
+
return {
|
|
151
|
+
pathRel: rel,
|
|
152
|
+
displayName,
|
|
153
|
+
isDir,
|
|
154
|
+
size: isDir ? 0 : stat.size,
|
|
155
|
+
mtime: Math.floor(stat.mtimeMs / 1000),
|
|
156
|
+
etag: `W/"${stat.size}-${Math.floor(stat.mtimeMs)}"`,
|
|
157
|
+
mime: isDir ? undefined : guessMime(displayName),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function basename(rel) {
|
|
161
|
+
if (!rel)
|
|
162
|
+
return "";
|
|
163
|
+
const idx = rel.lastIndexOf("/");
|
|
164
|
+
return idx === -1 ? rel : rel.slice(idx + 1);
|
|
165
|
+
}
|
|
166
|
+
const MIME_BY_EXT = {
|
|
167
|
+
".txt": "text/plain; charset=utf-8",
|
|
168
|
+
".md": "text/markdown; charset=utf-8",
|
|
169
|
+
".json": "application/json",
|
|
170
|
+
".pdf": "application/pdf",
|
|
171
|
+
".jpg": "image/jpeg",
|
|
172
|
+
".jpeg": "image/jpeg",
|
|
173
|
+
".png": "image/png",
|
|
174
|
+
".webp": "image/webp",
|
|
175
|
+
".gif": "image/gif",
|
|
176
|
+
".mp4": "video/mp4",
|
|
177
|
+
".mov": "video/quicktime",
|
|
178
|
+
".mp3": "audio/mpeg",
|
|
179
|
+
".zip": "application/zip",
|
|
180
|
+
};
|
|
181
|
+
function guessMime(name) {
|
|
182
|
+
const ext = path.extname(name).toLowerCase();
|
|
183
|
+
return MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
184
|
+
}
|
|
185
|
+
// ── GET / HEAD ─────────────────────────────────────
|
|
186
|
+
export async function handleGet(ctx, req, reply, bodyless) {
|
|
187
|
+
const rel = extractRel(req.url, ctx.mountPrefix);
|
|
188
|
+
const scope = checkScope(ctx, rel, false);
|
|
189
|
+
if (!scope.allowed)
|
|
190
|
+
return send403(reply, scope.reason);
|
|
191
|
+
if (rel === "") {
|
|
192
|
+
reply.code(403).send("cannot GET the WebDAV root");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
const r = await ctx.filesManager.readStream(rel);
|
|
197
|
+
reply
|
|
198
|
+
.header("Content-Type", r.mime)
|
|
199
|
+
.header("Content-Length", String(r.size))
|
|
200
|
+
.header("ETag", r.etag)
|
|
201
|
+
.header("Last-Modified", new Date(r.mtime * 1000).toUTCString())
|
|
202
|
+
.header("Accept-Ranges", "bytes");
|
|
203
|
+
if (bodyless) {
|
|
204
|
+
reply.code(200).send();
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
reply.code(200).send(r.openStream());
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
sendFilesError(reply, e);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// ── PUT ────────────────────────────────────────────
|
|
215
|
+
export async function handlePut(ctx, req, reply) {
|
|
216
|
+
const rel = extractRel(req.url, ctx.mountPrefix);
|
|
217
|
+
const scope = checkScope(ctx, rel, true);
|
|
218
|
+
if (!scope.allowed)
|
|
219
|
+
return send403(reply, scope.reason);
|
|
220
|
+
if (rel === "") {
|
|
221
|
+
reply.code(405).send("cannot PUT the WebDAV root");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
// Content-Length is mandatory for the same reason as /api/files.
|
|
225
|
+
const cl = req.headers["content-length"];
|
|
226
|
+
const clNum = typeof cl === "string" ? Number.parseInt(cl, 10) : Number.NaN;
|
|
227
|
+
if (!Number.isFinite(clNum) || clNum < 0) {
|
|
228
|
+
reply.code(411).send("Content-Length required");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const body = req.raw;
|
|
232
|
+
if (!body) {
|
|
233
|
+
reply.code(400).send("missing body");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
// WebDAV PUT default-overwrites; matches RFC 4918 §9.7.
|
|
238
|
+
const result = await ctx.filesManager.writeStream(rel, body, {
|
|
239
|
+
overwrite: true,
|
|
240
|
+
expectedSize: clNum,
|
|
241
|
+
});
|
|
242
|
+
// 201 if newly created, 204 if overwritten — FilesManager doesn't
|
|
243
|
+
// currently distinguish; default to 201 (Finder accepts both).
|
|
244
|
+
reply
|
|
245
|
+
.code(201)
|
|
246
|
+
.header("ETag", result.etag)
|
|
247
|
+
.header("Content-Length", "0")
|
|
248
|
+
.send();
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
sendFilesError(reply, e);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// ── DELETE ─────────────────────────────────────────
|
|
255
|
+
export async function handleDelete(ctx, req, reply) {
|
|
256
|
+
const rel = extractRel(req.url, ctx.mountPrefix);
|
|
257
|
+
const scope = checkScope(ctx, rel, true);
|
|
258
|
+
if (!scope.allowed)
|
|
259
|
+
return send403(reply, scope.reason);
|
|
260
|
+
if (rel === "") {
|
|
261
|
+
reply.code(405).send("cannot DELETE the WebDAV root");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
await ctx.filesManager.remove(rel);
|
|
266
|
+
reply.code(204).send();
|
|
267
|
+
}
|
|
268
|
+
catch (e) {
|
|
269
|
+
sendFilesError(reply, e);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// ── MKCOL ──────────────────────────────────────────
|
|
273
|
+
export async function handleMkcol(ctx, req, reply) {
|
|
274
|
+
const rel = extractRel(req.url, ctx.mountPrefix);
|
|
275
|
+
const scope = checkScope(ctx, rel, true);
|
|
276
|
+
if (!scope.allowed)
|
|
277
|
+
return send403(reply, scope.reason);
|
|
278
|
+
if (rel === "") {
|
|
279
|
+
reply.code(405).send("cannot MKCOL the WebDAV root");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
// RFC 4918 §9.3: MKCOL must reject a request with a body.
|
|
283
|
+
const cl = Number.parseInt(req.headers["content-length"], 10);
|
|
284
|
+
if (Number.isFinite(cl) && cl > 0) {
|
|
285
|
+
reply.code(415).send("MKCOL must not include a body");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
await ctx.filesManager.mkdir(rel);
|
|
290
|
+
reply.code(201).send();
|
|
291
|
+
}
|
|
292
|
+
catch (e) {
|
|
293
|
+
sendFilesError(reply, e);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// ── MOVE ───────────────────────────────────────────
|
|
297
|
+
export async function handleMove(ctx, req, reply) {
|
|
298
|
+
const fromRel = extractRel(req.url, ctx.mountPrefix);
|
|
299
|
+
const toRel = parseDestinationHeader(req, ctx.mountPrefix);
|
|
300
|
+
if (toRel === null) {
|
|
301
|
+
reply.code(400).send("invalid Destination header");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const fromScope = checkScope(ctx, fromRel, true);
|
|
305
|
+
if (!fromScope.allowed)
|
|
306
|
+
return send403(reply, fromScope.reason);
|
|
307
|
+
const toScope = checkScope(ctx, toRel, true);
|
|
308
|
+
if (!toScope.allowed)
|
|
309
|
+
return send403(reply, toScope.reason);
|
|
310
|
+
const overwrite = String(req.headers.overwrite ?? "T")
|
|
311
|
+
.trim()
|
|
312
|
+
.toUpperCase() !== "F";
|
|
313
|
+
try {
|
|
314
|
+
await ctx.filesManager.move(fromRel, toRel, overwrite);
|
|
315
|
+
// 201 if the destination was newly created, 204 if overwritten.
|
|
316
|
+
// Without knowing pre-state we default to 201 (clients accept it).
|
|
317
|
+
reply.code(201).header("Content-Length", "0").send();
|
|
318
|
+
}
|
|
319
|
+
catch (e) {
|
|
320
|
+
sendFilesError(reply, e);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// ── COPY ───────────────────────────────────────────
|
|
324
|
+
export async function handleCopy(ctx, req, reply) {
|
|
325
|
+
const fromRel = extractRel(req.url, ctx.mountPrefix);
|
|
326
|
+
const toRel = parseDestinationHeader(req, ctx.mountPrefix);
|
|
327
|
+
if (toRel === null) {
|
|
328
|
+
reply.code(400).send("invalid Destination header");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const fromScope = checkScope(ctx, fromRel, false);
|
|
332
|
+
if (!fromScope.allowed)
|
|
333
|
+
return send403(reply, fromScope.reason);
|
|
334
|
+
const toScope = checkScope(ctx, toRel, true);
|
|
335
|
+
if (!toScope.allowed)
|
|
336
|
+
return send403(reply, toScope.reason);
|
|
337
|
+
const overwrite = String(req.headers.overwrite ?? "T")
|
|
338
|
+
.trim()
|
|
339
|
+
.toUpperCase() !== "F";
|
|
340
|
+
// For W4 we only support file copy (not recursive directory copy).
|
|
341
|
+
// Finder relies primarily on MOVE; recursive COPY is rare. Returning
|
|
342
|
+
// 502 Bad Gateway with a clear body lets clients fall back.
|
|
343
|
+
const fromAbs = resolveSafe(FILES_ROOT, fromRel);
|
|
344
|
+
let stat;
|
|
345
|
+
try {
|
|
346
|
+
stat = fs.statSync(fromAbs);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
reply.code(404).send();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (stat.isDirectory()) {
|
|
353
|
+
reply
|
|
354
|
+
.code(502)
|
|
355
|
+
.send("recursive directory COPY is not supported in this build");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const toAbs = resolveSafe(FILES_ROOT, toRel);
|
|
359
|
+
try {
|
|
360
|
+
if (!overwrite && fs.existsSync(toAbs)) {
|
|
361
|
+
reply.code(412).send();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
fs.mkdirSync(path.dirname(toAbs), { recursive: true });
|
|
365
|
+
// Stream copy via atomicWriteStream so we get tmp+rename atomicity
|
|
366
|
+
await withPathLock(toAbs, async () => {
|
|
367
|
+
await atomicWriteStream(toAbs, fs.createReadStream(fromAbs));
|
|
368
|
+
});
|
|
369
|
+
reply.code(201).header("Content-Length", "0").send();
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
sendFilesError(reply, e);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function parseDestinationHeader(req, mountPrefix) {
|
|
376
|
+
const raw = req.headers.destination;
|
|
377
|
+
if (typeof raw !== "string" || !raw)
|
|
378
|
+
return null;
|
|
379
|
+
// Destination may be a full URL (https://host/webdav/x) or a path-only
|
|
380
|
+
// value. We strip everything up to and including the mount prefix.
|
|
381
|
+
let dest = raw;
|
|
382
|
+
try {
|
|
383
|
+
const u = new URL(raw);
|
|
384
|
+
dest = u.pathname;
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// Not a full URL — assume path-only
|
|
388
|
+
}
|
|
389
|
+
const cleanPrefix = mountPrefix.replace(/\/+$/, "");
|
|
390
|
+
if (!dest.startsWith(cleanPrefix))
|
|
391
|
+
return null;
|
|
392
|
+
let rest = dest.slice(cleanPrefix.length);
|
|
393
|
+
if (rest.startsWith("/"))
|
|
394
|
+
rest = rest.slice(1);
|
|
395
|
+
if (rest.endsWith("/"))
|
|
396
|
+
rest = rest.slice(0, -1);
|
|
397
|
+
return rest
|
|
398
|
+
.split("/")
|
|
399
|
+
.map((s) => {
|
|
400
|
+
try {
|
|
401
|
+
return decodeURIComponent(s);
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
return s;
|
|
405
|
+
}
|
|
406
|
+
})
|
|
407
|
+
.filter((s) => s !== "")
|
|
408
|
+
.join("/");
|
|
409
|
+
}
|
|
410
|
+
// ── Error mapping ──────────────────────────────────
|
|
411
|
+
function sendFilesError(reply, e) {
|
|
412
|
+
if (e instanceof FilesError) {
|
|
413
|
+
reply.code(e.httpStatus).send(e.message);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const err = e;
|
|
417
|
+
console.error("[webdav] unexpected:", err.message);
|
|
418
|
+
reply.code(500).send(err.message ?? "internal error");
|
|
419
|
+
}
|
|
420
|
+
//# sourceMappingURL=server.js.map
|