@karmaniverous/jeeves-server 3.0.0-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.local +13 -0
- package/.env.local.template +13 -0
- package/.tsbuildinfo +1 -0
- package/CHANGELOG.md +450 -0
- package/about.md +82 -0
- package/client/README.md +73 -0
- package/client/eslint.config.js +23 -0
- package/client/index.html +14 -0
- package/client/package-lock.json +5181 -0
- package/client/package.json +60 -0
- package/client/public/vite.svg +1 -0
- package/client/src/App.tsx +22 -0
- package/client/src/components/AccountMenu.tsx +167 -0
- package/client/src/components/ActionDropdown.tsx +120 -0
- package/client/src/components/CodeEditor.tsx +143 -0
- package/client/src/components/CodeViewer.tsx +113 -0
- package/client/src/components/ConfirmDialog.tsx +32 -0
- package/client/src/components/DirectoryRow.tsx +62 -0
- package/client/src/components/DirectoryTable.tsx +42 -0
- package/client/src/components/DownloadDropdown.tsx +116 -0
- package/client/src/components/DriveList.tsx +54 -0
- package/client/src/components/EmbeddedDiagramPanzoom.ts +28 -0
- package/client/src/components/FileContentView.tsx +155 -0
- package/client/src/components/InlineSvgPanzoom.ts +60 -0
- package/client/src/components/LazyDiagram.ts +93 -0
- package/client/src/components/LinkDropdown.tsx +134 -0
- package/client/src/components/MarkdownView.tsx +115 -0
- package/client/src/components/MermaidViewer.tsx +21 -0
- package/client/src/components/PlantUmlViewer.tsx +21 -0
- package/client/src/components/SearchModal.tsx +424 -0
- package/client/src/components/SvgViewer.tsx +107 -0
- package/client/src/components/TabBar.tsx +96 -0
- package/client/src/components/layout/Header.tsx +270 -0
- package/client/src/components/panzoom.ts +203 -0
- package/client/src/components/renderableUtils.ts +15 -0
- package/client/src/components/runner/JobTable.tsx +153 -0
- package/client/src/components/runner/RunHistory.tsx +140 -0
- package/client/src/components/runner/StatsBar.tsx +43 -0
- package/client/src/components/runner/StatusPill.tsx +27 -0
- package/client/src/components/runner/jobTableUtils.ts +65 -0
- package/client/src/components/scrollUtils.ts +39 -0
- package/client/src/components/ui/alert-dialog.tsx +107 -0
- package/client/src/components/ui/button.tsx +40 -0
- package/client/src/components/ui/dropdown-menu.tsx +79 -0
- package/client/src/components/ui/input.tsx +26 -0
- package/client/src/components/useActionState.ts +43 -0
- package/client/src/hooks/useFileBrowser.ts +102 -0
- package/client/src/hooks/useFileData.ts +78 -0
- package/client/src/hooks/useScrollAnchor.ts +70 -0
- package/client/src/hooks/useShareSettings.ts +22 -0
- package/client/src/hooks/useTopBar.ts +27 -0
- package/client/src/index.css +281 -0
- package/client/src/lib/AuthContext.ts +27 -0
- package/client/src/lib/api.ts +239 -0
- package/client/src/lib/auth.tsx +50 -0
- package/client/src/lib/codeBlockCm6.ts +129 -0
- package/client/src/lib/codeBlockCopy.ts +43 -0
- package/client/src/lib/codemirror.ts +77 -0
- package/client/src/lib/runner-api.ts +172 -0
- package/client/src/lib/svg.ts +50 -0
- package/client/src/lib/theme.ts +34 -0
- package/client/src/lib/utils.ts +6 -0
- package/client/src/main.tsx +11 -0
- package/client/src/pages/FileBrowser.tsx +135 -0
- package/client/src/pages/Home.tsx +46 -0
- package/client/src/pages/Runner.tsx +151 -0
- package/client/src/pages/RunnerJob.tsx +170 -0
- package/client/tsconfig.app.json +32 -0
- package/client/tsconfig.json +7 -0
- package/client/tsconfig.node.json +26 -0
- package/client/vite.config.ts +35 -0
- package/content/privacy.md +61 -0
- package/content/terms.md +41 -0
- package/dist/client/assets/CodeEditor-0XHVI8Nu.js +1 -0
- package/dist/client/assets/CodeViewer-CykMVsfX.js +1 -0
- package/dist/client/assets/index--MBieNJA.js +1 -0
- package/dist/client/assets/index-BENeXQI_.js +1 -0
- package/dist/client/assets/index-BbBpoOxz.js +1 -0
- package/dist/client/assets/index-BdV9g5AM.js +6 -0
- package/dist/client/assets/index-BjAilRri.js +2 -0
- package/dist/client/assets/index-BqbhWo2I.js +3 -0
- package/dist/client/assets/index-CVbycZ0H.js +1 -0
- package/dist/client/assets/index-Cs5oz2oJ.js +5 -0
- package/dist/client/assets/index-D8KZVveX.js +1 -0
- package/dist/client/assets/index-DC4HMHxY.js +13 -0
- package/dist/client/assets/index-DbMebkkd.css +1 -0
- package/dist/client/assets/index-DcY2RXqX.js +1 -0
- package/dist/client/assets/index-Duy-tZYV.js +1 -0
- package/dist/client/assets/index-Dw7rDFmE.js +7 -0
- package/dist/client/assets/index-FlCUvrjv.js +2 -0
- package/dist/client/assets/index-K6OVmfhg.js +1 -0
- package/dist/client/assets/index-LjwgzZ7F.js +62 -0
- package/dist/client/assets/index-MLwyFRN0.js +1 -0
- package/dist/client/assets/index-OpqBpSjn.js +1 -0
- package/dist/client/assets/index-SsHei0HE.js +1 -0
- package/dist/client/assets/index-uQa2yckk.js +1 -0
- package/dist/client/assets/index-udkXoIER.js +1 -0
- package/dist/client/index.html +15 -0
- package/dist/client/vite.svg +1 -0
- package/dist/src/auth/google.js +57 -0
- package/dist/src/auth/keys.js +185 -0
- package/dist/src/auth/resolve.js +102 -0
- package/dist/src/auth/session.js +57 -0
- package/dist/src/cli/commands/config.js +100 -0
- package/dist/src/cli/commands/config.test.js +84 -0
- package/dist/src/cli/commands/service.js +93 -0
- package/dist/src/cli/commands/start.js +24 -0
- package/dist/src/cli/index.js +20 -0
- package/dist/src/config/index.js +90 -0
- package/dist/src/config/loadConfig.test.js +127 -0
- package/dist/src/config/resolve.js +134 -0
- package/dist/src/config/resolve.test.js +148 -0
- package/dist/src/config/schema.js +159 -0
- package/dist/src/config/substituteEnvVars.js +45 -0
- package/dist/src/config/substituteEnvVars.test.js +51 -0
- package/dist/src/config/types.js +5 -0
- package/dist/src/routes/api/auth-status.js +56 -0
- package/dist/src/routes/api/diagrams.js +35 -0
- package/dist/src/routes/api/directory.js +93 -0
- package/dist/src/routes/api/drives.js +15 -0
- package/dist/src/routes/api/export.js +218 -0
- package/dist/src/routes/api/fileContent.js +286 -0
- package/dist/src/routes/api/index.js +33 -0
- package/dist/src/routes/api/linkInfo.js +71 -0
- package/dist/src/routes/api/linkInfo.test.js +104 -0
- package/dist/src/routes/api/middleware.js +117 -0
- package/dist/src/routes/api/raw.js +38 -0
- package/dist/src/routes/api/runner.js +59 -0
- package/dist/src/routes/api/search.js +236 -0
- package/dist/src/routes/api/sharing.js +203 -0
- package/dist/src/routes/api/status.js +68 -0
- package/dist/src/routes/api/status.test.js +62 -0
- package/dist/src/routes/auth.js +99 -0
- package/dist/src/routes/event.js +77 -0
- package/dist/src/routes/event.test.js +206 -0
- package/dist/src/routes/health.js +10 -0
- package/dist/src/routes/keys.js +129 -0
- package/dist/src/routes/path/index.js +17 -0
- package/dist/src/routes/static.js +30 -0
- package/dist/src/server.js +90 -0
- package/dist/src/services/deepShareLinks.js +163 -0
- package/dist/src/services/diagramCache.js +104 -0
- package/dist/src/services/embeddedDiagrams.js +136 -0
- package/dist/src/services/eventLog.js +55 -0
- package/dist/src/services/eventLog.test.js +113 -0
- package/dist/src/services/eventQueue.js +154 -0
- package/dist/src/services/eventQueue.test.js +104 -0
- package/dist/src/services/export.js +220 -0
- package/dist/src/services/exportCache.js +196 -0
- package/dist/src/services/markdown.js +147 -0
- package/dist/src/services/mermaid.js +97 -0
- package/dist/src/services/plantuml.js +145 -0
- package/dist/src/services/puppeteer.js +156 -0
- package/dist/src/util/breadcrumbs.js +22 -0
- package/dist/src/util/crypto.js +56 -0
- package/dist/src/util/crypto.test.js +99 -0
- package/dist/src/util/fileDetection.js +66 -0
- package/dist/src/util/fileDetection.test.js +89 -0
- package/dist/src/util/formatters.js +43 -0
- package/dist/src/util/formatters.test.js +83 -0
- package/dist/src/util/packageVersion.js +25 -0
- package/dist/src/util/platform.js +148 -0
- package/dist/src/util/state.js +46 -0
- package/dist/vitest.config.js +12 -0
- package/favicon.svg +3 -0
- package/guides/access-decision-flow.mmd +24 -0
- package/guides/access-decision-flow.svg +1 -0
- package/guides/api-integration.md +236 -0
- package/guides/deployment.md +287 -0
- package/guides/event-gateway.md +204 -0
- package/guides/event-gateway.mmd +17 -0
- package/guides/event-gateway.svg +1 -0
- package/guides/exports.md +239 -0
- package/guides/setup.md +313 -0
- package/guides/sharing.md +204 -0
- package/jeeves-server.config.template.json +25 -0
- package/package.json +124 -0
- package/scripts/download-plantuml.js +70 -0
- package/src/auth/google.ts +93 -0
- package/src/auth/keys.ts +252 -0
- package/src/auth/resolve.ts +157 -0
- package/src/auth/session.ts +77 -0
- package/src/cli/commands/config.test.ts +107 -0
- package/src/cli/commands/config.ts +113 -0
- package/src/cli/commands/service.ts +129 -0
- package/src/cli/commands/start.ts +27 -0
- package/src/cli/index.ts +25 -0
- package/src/config/index.ts +113 -0
- package/src/config/loadConfig.test.ts +155 -0
- package/src/config/resolve.test.ts +192 -0
- package/src/config/resolve.ts +173 -0
- package/src/config/schema.ts +179 -0
- package/src/config/substituteEnvVars.test.ts +64 -0
- package/src/config/substituteEnvVars.ts +52 -0
- package/src/config/types.ts +129 -0
- package/src/routes/api/auth-status.ts +85 -0
- package/src/routes/api/diagrams.ts +53 -0
- package/src/routes/api/directory.ts +123 -0
- package/src/routes/api/drives.ts +23 -0
- package/src/routes/api/export.ts +314 -0
- package/src/routes/api/fileContent.ts +414 -0
- package/src/routes/api/index.ts +37 -0
- package/src/routes/api/linkInfo.test.ts +132 -0
- package/src/routes/api/linkInfo.ts +83 -0
- package/src/routes/api/middleware.ts +156 -0
- package/src/routes/api/raw.ts +54 -0
- package/src/routes/api/runner.ts +107 -0
- package/src/routes/api/search.ts +321 -0
- package/src/routes/api/sharing.ts +259 -0
- package/src/routes/api/status.test.ts +72 -0
- package/src/routes/api/status.ts +82 -0
- package/src/routes/auth.ts +143 -0
- package/src/routes/event.test.ts +248 -0
- package/src/routes/event.ts +109 -0
- package/src/routes/health.ts +13 -0
- package/src/routes/keys.ts +192 -0
- package/src/routes/path/index.ts +24 -0
- package/src/routes/static.ts +54 -0
- package/src/server.ts +104 -0
- package/src/services/deepShareLinks.ts +203 -0
- package/src/services/diagramCache.ts +128 -0
- package/src/services/embeddedDiagrams.ts +168 -0
- package/src/services/eventLog.test.ts +144 -0
- package/src/services/eventLog.ts +68 -0
- package/src/services/eventQueue.test.ts +127 -0
- package/src/services/eventQueue.ts +196 -0
- package/src/services/export.ts +267 -0
- package/src/services/exportCache.ts +216 -0
- package/src/services/markdown.ts +189 -0
- package/src/services/mermaid.ts +113 -0
- package/src/services/plantuml.ts +172 -0
- package/src/services/puppeteer.ts +188 -0
- package/src/types/fastify.d.ts +13 -0
- package/src/types/jsonmap.d.ts +10 -0
- package/src/types/plantuml-encoder.d.ts +4 -0
- package/src/util/breadcrumbs.ts +33 -0
- package/src/util/crypto.test.ts +132 -0
- package/src/util/crypto.ts +79 -0
- package/src/util/fileDetection.test.ts +115 -0
- package/src/util/fileDetection.ts +70 -0
- package/src/util/formatters.test.ts +105 -0
- package/src/util/formatters.ts +44 -0
- package/src/util/packageVersion.ts +30 -0
- package/src/util/platform.ts +178 -0
- package/src/util/state.ts +55 -0
- package/test-docs/diagram-retry-test.md +18 -0
- package/test-docs/embedded-diagrams.md +52 -0
- package/test-docs/lazy-diagrams-test.md +333 -0
- package/test-docs/page-a.md +7 -0
- package/test-docs/page-b.md +7 -0
- package/test-docs/page-c.md +7 -0
- package/test-docs/sub/page-d.md +7 -0
- package/test-docs/test-diagram.puml +13 -0
- package/test-docs/validate-deep-share.js +318 -0
- package/tsconfig.json +37 -0
- package/tsdoc.json +13 -0
- package/vendor/.plantuml-version +1 -0
- package/vendor/plantuml.jar +0 -0
- package/vitest.config.js +12 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Directory listing API route.
|
|
3
|
+
*
|
|
4
|
+
* Handles: GET /api/path/*
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { _directoryVisibleUnderScopes, _pathMatchesPatterns, _pathMatchesScopes, } from '../../auth/keys.js';
|
|
9
|
+
import { getConfig } from '../../config/index.js';
|
|
10
|
+
import { filterBreadcrumbsForOutsider } from '../../util/breadcrumbs.js';
|
|
11
|
+
import { breadcrumbParts, fsPathToUrl, getRoots, urlPathToFs, } from '../../util/platform.js';
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
13
|
+
export const directoryRoutes = async (fastify) => {
|
|
14
|
+
const roots = getRoots(getConfig().roots);
|
|
15
|
+
fastify.get('/api/path/*', async (request, reply) => {
|
|
16
|
+
const reqPath = request.params['*'];
|
|
17
|
+
if (!reqPath)
|
|
18
|
+
return reply.redirect('/api/drives');
|
|
19
|
+
const fsPath = urlPathToFs(reqPath, roots);
|
|
20
|
+
if (!fsPath)
|
|
21
|
+
return reply.code(404).send({ error: 'Invalid path' });
|
|
22
|
+
const resolved = path.resolve(fsPath);
|
|
23
|
+
if (!fs.existsSync(resolved)) {
|
|
24
|
+
return reply.code(404).send({ error: 'Not found', path: resolved });
|
|
25
|
+
}
|
|
26
|
+
const stats = fs.statSync(resolved);
|
|
27
|
+
if (!stats.isDirectory()) {
|
|
28
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
29
|
+
return reply.send({
|
|
30
|
+
type: 'file',
|
|
31
|
+
path: reqPath,
|
|
32
|
+
ext,
|
|
33
|
+
size: stats.size,
|
|
34
|
+
mtime: stats.mtime.toISOString().split('T')[0],
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const isInsider = request.accessMode === 'insider';
|
|
38
|
+
const insiderScopes = request.insiderScopes ?? null;
|
|
39
|
+
const allEntries = fs.readdirSync(resolved, { withFileTypes: true });
|
|
40
|
+
const entries = insiderScopes
|
|
41
|
+
? allEntries.filter((entry) => {
|
|
42
|
+
const entryPath = path.join(resolved, entry.name);
|
|
43
|
+
const entryUrlPath = fsPathToUrl(entryPath, roots);
|
|
44
|
+
if (insiderScopes.deny.length > 0) {
|
|
45
|
+
if (_pathMatchesPatterns(entryUrlPath, insiderScopes.deny))
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (entry.isDirectory()) {
|
|
49
|
+
return _directoryVisibleUnderScopes(entryUrlPath, insiderScopes.allow);
|
|
50
|
+
}
|
|
51
|
+
return _pathMatchesScopes(entryUrlPath, insiderScopes);
|
|
52
|
+
})
|
|
53
|
+
: allEntries;
|
|
54
|
+
const sorted = entries.sort((a, b) => {
|
|
55
|
+
if (a.isDirectory() && !b.isDirectory())
|
|
56
|
+
return -1;
|
|
57
|
+
if (!a.isDirectory() && b.isDirectory())
|
|
58
|
+
return 1;
|
|
59
|
+
return a.name.localeCompare(b.name);
|
|
60
|
+
});
|
|
61
|
+
const result = sorted.map((entry) => {
|
|
62
|
+
const entryPath = path.join(resolved, entry.name);
|
|
63
|
+
let size = null;
|
|
64
|
+
let mtime = null;
|
|
65
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
66
|
+
try {
|
|
67
|
+
const entryStats = fs.statSync(entryPath);
|
|
68
|
+
mtime = entryStats.mtime.toISOString().split('T')[0];
|
|
69
|
+
if (!entry.isDirectory())
|
|
70
|
+
size = entryStats.size;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* ignore */
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
name: entry.name,
|
|
77
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
78
|
+
ext,
|
|
79
|
+
size,
|
|
80
|
+
mtime,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
const breadcrumbs = breadcrumbParts(resolved, roots);
|
|
84
|
+
const matchedPath = request.authMatchedPath ?? null;
|
|
85
|
+
const filteredBreadcrumbs = filterBreadcrumbsForOutsider(breadcrumbs, isInsider, matchedPath, true);
|
|
86
|
+
return reply.send({
|
|
87
|
+
path: reqPath,
|
|
88
|
+
entries: result,
|
|
89
|
+
breadcrumbs: filteredBreadcrumbs,
|
|
90
|
+
isInsider,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drive listing API route.
|
|
3
|
+
*
|
|
4
|
+
* Handles: GET /api/drives
|
|
5
|
+
*/
|
|
6
|
+
import { getConfig } from '../../config/index.js';
|
|
7
|
+
import { getRoots } from '../../util/platform.js';
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
9
|
+
export const drivesRoutes = async (fastify) => {
|
|
10
|
+
const roots = getRoots(getConfig().roots);
|
|
11
|
+
fastify.get('/api/drives', async (_request, reply) => {
|
|
12
|
+
const drives = roots.map((r) => ({ letter: r.id, label: r.label }));
|
|
13
|
+
return reply.send(drives);
|
|
14
|
+
});
|
|
15
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export API routes.
|
|
3
|
+
*
|
|
4
|
+
* Handles: /api/export/*, /api/mermaid-export/*, /api/plantuml-export/*
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import archiver from 'archiver';
|
|
9
|
+
import { getConfig } from '../../config/index.js';
|
|
10
|
+
import { cacheDiagramBuffer, getCachedDiagramBuffer, } from '../../services/diagramCache.js';
|
|
11
|
+
import { appendEvent } from '../../services/eventQueue.js';
|
|
12
|
+
import { exportPage } from '../../services/export.js';
|
|
13
|
+
import { cacheExport, clearDiagramCacheForFile, clearExportCache, clearStandaloneDiagramCache, getCachedExport, } from '../../services/exportCache.js';
|
|
14
|
+
import { renderMermaidToFile } from '../../services/mermaid.js';
|
|
15
|
+
import { getPlantUmlFormats, renderPlantUmlToBuffer, } from '../../services/plantuml.js';
|
|
16
|
+
import { DIAGRAM_CONTENT_TYPES } from '../../util/fileDetection.js';
|
|
17
|
+
import { getDirSize, getRoots, urlPathToFs } from '../../util/platform.js';
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
19
|
+
export const exportRoutes = async (fastify) => {
|
|
20
|
+
const roots = getRoots(getConfig().roots);
|
|
21
|
+
// GET /api/export/*
|
|
22
|
+
fastify.get('/api/export/*', async (request, reply) => {
|
|
23
|
+
const reqPath = request.params['*'];
|
|
24
|
+
if (!reqPath)
|
|
25
|
+
return reply.code(400).send({ error: 'Path required' });
|
|
26
|
+
const exportFsPath = urlPathToFs(reqPath, roots);
|
|
27
|
+
if (!exportFsPath)
|
|
28
|
+
return reply.code(404).send({ error: 'Invalid path' });
|
|
29
|
+
const resolved = path.resolve(exportFsPath);
|
|
30
|
+
if (!fs.existsSync(resolved))
|
|
31
|
+
return reply.code(404).send({ error: 'Not found', path: resolved });
|
|
32
|
+
const format = request.query.format ?? 'pdf';
|
|
33
|
+
const stats = fs.statSync(resolved);
|
|
34
|
+
// ZIP export for directories
|
|
35
|
+
if (stats.isDirectory()) {
|
|
36
|
+
if (format !== 'zip')
|
|
37
|
+
return reply
|
|
38
|
+
.code(400)
|
|
39
|
+
.send({ error: 'Directories only support ZIP export' });
|
|
40
|
+
const isInsider = request.accessMode === 'insider';
|
|
41
|
+
if (!isInsider)
|
|
42
|
+
return reply
|
|
43
|
+
.code(403)
|
|
44
|
+
.send({ error: 'ZIP export requires insider access' });
|
|
45
|
+
const config = getConfig();
|
|
46
|
+
const totalSize = getDirSize(resolved);
|
|
47
|
+
const maxSizeBytes = config.maxZipSizeMb * 1024 * 1024;
|
|
48
|
+
if (totalSize > maxSizeBytes) {
|
|
49
|
+
return reply.code(413).send({
|
|
50
|
+
error: `Directory too large for ZIP export (${String(Math.round(totalSize / 1024 / 1024))}MB, max ${String(config.maxZipSizeMb)}MB)`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const dirName = path.basename(resolved);
|
|
54
|
+
const archive = archiver('zip', { zlib: { level: 6 } });
|
|
55
|
+
reply.header('Content-Type', 'application/zip');
|
|
56
|
+
reply.header('Content-Disposition', `attachment; filename="${dirName}.zip"`);
|
|
57
|
+
reply.send(archive);
|
|
58
|
+
archive.directory(resolved, dirName);
|
|
59
|
+
void archive.finalize();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// PDF/DOCX export for files
|
|
63
|
+
if (format !== 'pdf' && format !== 'docx') {
|
|
64
|
+
return reply
|
|
65
|
+
.code(400)
|
|
66
|
+
.send({ error: 'Files support pdf or docx export' });
|
|
67
|
+
}
|
|
68
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
69
|
+
if (ext !== '.md')
|
|
70
|
+
return reply
|
|
71
|
+
.code(400)
|
|
72
|
+
.send({ error: 'Only markdown files support PDF/DOCX export' });
|
|
73
|
+
const config = getConfig();
|
|
74
|
+
const internalKey = config.internalInsiderKey;
|
|
75
|
+
const { port } = config;
|
|
76
|
+
const exportKey = request.query.key || internalKey;
|
|
77
|
+
if (!exportKey)
|
|
78
|
+
return reply
|
|
79
|
+
.code(500)
|
|
80
|
+
.send({ error: 'Export unavailable — no internal key configured' });
|
|
81
|
+
const exportUrl = `http://localhost:${String(port)}/browse/${reqPath}?key=${exportKey}&render_diagrams=1&plain_code=1`;
|
|
82
|
+
const fileName = path.basename(resolved);
|
|
83
|
+
const baseName = fileName.replace(/\.md$/i, '');
|
|
84
|
+
try {
|
|
85
|
+
const contentType = format === 'pdf'
|
|
86
|
+
? 'application/pdf'
|
|
87
|
+
: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
|
88
|
+
const fileExt = format === 'pdf' ? 'pdf' : 'docx';
|
|
89
|
+
// Check export cache
|
|
90
|
+
const cached = getCachedExport(resolved, format);
|
|
91
|
+
if (cached) {
|
|
92
|
+
return await reply
|
|
93
|
+
.header('Content-Type', contentType)
|
|
94
|
+
.header('Content-Disposition', `attachment; filename="${baseName}.${fileExt}"`)
|
|
95
|
+
.header('Content-Length', cached.length)
|
|
96
|
+
.send(cached);
|
|
97
|
+
}
|
|
98
|
+
const buffer = await exportPage({
|
|
99
|
+
url: exportUrl,
|
|
100
|
+
fileName,
|
|
101
|
+
format: format,
|
|
102
|
+
});
|
|
103
|
+
// Cache the result
|
|
104
|
+
cacheExport(resolved, format, buffer);
|
|
105
|
+
return await reply
|
|
106
|
+
.header('Content-Type', contentType)
|
|
107
|
+
.header('Content-Disposition', `attachment; filename="${baseName}.${fileExt}"`)
|
|
108
|
+
.header('Content-Length', buffer.length)
|
|
109
|
+
.send(buffer);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
appendEvent({ kind: `${format}_export_error`, error: String(err) });
|
|
113
|
+
return reply.code(500).send({
|
|
114
|
+
error: `${format.toUpperCase()} export failed`,
|
|
115
|
+
details: String(err),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// DELETE /api/export-cache/* — clear all caches for a file (insider-only)
|
|
120
|
+
fastify.delete('/api/export-cache/*', async (request, reply) => {
|
|
121
|
+
if (request.accessMode !== 'insider') {
|
|
122
|
+
return reply.code(403).send({ error: 'Insider access required' });
|
|
123
|
+
}
|
|
124
|
+
const reqPath = request.params['*'];
|
|
125
|
+
if (!reqPath)
|
|
126
|
+
return reply.code(400).send({ error: 'Path required' });
|
|
127
|
+
const fsPath = urlPathToFs(reqPath, roots);
|
|
128
|
+
if (!fsPath)
|
|
129
|
+
return reply.code(404).send({ error: 'Invalid path' });
|
|
130
|
+
const resolved = path.resolve(fsPath);
|
|
131
|
+
const { getDiagramCacheDir } = await import('../../services/diagramCache.js');
|
|
132
|
+
const diagCacheDir = getDiagramCacheDir();
|
|
133
|
+
const exportCount = clearExportCache(resolved);
|
|
134
|
+
const embeddedCount = clearDiagramCacheForFile(resolved, diagCacheDir);
|
|
135
|
+
const standaloneCount = clearStandaloneDiagramCache(resolved, diagCacheDir);
|
|
136
|
+
return reply.send({
|
|
137
|
+
cleared: {
|
|
138
|
+
exports: exportCount,
|
|
139
|
+
diagrams: embeddedCount + standaloneCount,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
// GET /api/mermaid-export/*
|
|
144
|
+
fastify.get('/api/mermaid-export/*', async (request, reply) => {
|
|
145
|
+
const reqPath = request.params['*'];
|
|
146
|
+
if (!reqPath)
|
|
147
|
+
return reply.code(400).send({ error: 'Path required' });
|
|
148
|
+
const mmdFsPath = urlPathToFs(reqPath, roots);
|
|
149
|
+
if (!mmdFsPath)
|
|
150
|
+
return reply.code(404).send({ error: 'Invalid path' });
|
|
151
|
+
const resolved = path.resolve(mmdFsPath);
|
|
152
|
+
if (!fs.existsSync(resolved) ||
|
|
153
|
+
!resolved.toLowerCase().endsWith('.mmd')) {
|
|
154
|
+
return reply.code(404).send({ error: 'Mermaid file not found' });
|
|
155
|
+
}
|
|
156
|
+
const mermaidFormats = ['svg', 'png', 'pdf'];
|
|
157
|
+
const format = mermaidFormats.includes(request.query.format ?? '')
|
|
158
|
+
? request.query.format
|
|
159
|
+
: 'svg';
|
|
160
|
+
const source = fs.readFileSync(resolved, 'utf8');
|
|
161
|
+
// Check cache
|
|
162
|
+
const cachedBuffer = getCachedDiagramBuffer('mermaid', source, format);
|
|
163
|
+
if (cachedBuffer) {
|
|
164
|
+
const downloadName = `${path.basename(resolved, '.mmd')}.${format}`;
|
|
165
|
+
return reply
|
|
166
|
+
.header('Content-Type', DIAGRAM_CONTENT_TYPES[format] ?? 'application/octet-stream')
|
|
167
|
+
.header('Content-Disposition', `attachment; filename="${downloadName}"`)
|
|
168
|
+
.send(cachedBuffer);
|
|
169
|
+
}
|
|
170
|
+
const outFile = await renderMermaidToFile(resolved, format);
|
|
171
|
+
if (!outFile)
|
|
172
|
+
return reply.code(500).send({ error: 'Mermaid render failed' });
|
|
173
|
+
const content = fs.readFileSync(outFile);
|
|
174
|
+
cacheDiagramBuffer('mermaid', source, content, format);
|
|
175
|
+
const downloadName = path.basename(outFile);
|
|
176
|
+
return reply
|
|
177
|
+
.header('Content-Type', DIAGRAM_CONTENT_TYPES[format] ?? 'application/octet-stream')
|
|
178
|
+
.header('Content-Disposition', `attachment; filename="${downloadName}"`)
|
|
179
|
+
.send(content);
|
|
180
|
+
});
|
|
181
|
+
// GET /api/plantuml-export/*
|
|
182
|
+
fastify.get('/api/plantuml-export/*', async (request, reply) => {
|
|
183
|
+
const reqPath = request.params['*'];
|
|
184
|
+
if (!reqPath)
|
|
185
|
+
return reply.code(400).send({ error: 'Path required' });
|
|
186
|
+
const pumlFsPath = urlPathToFs(reqPath, roots);
|
|
187
|
+
if (!pumlFsPath)
|
|
188
|
+
return reply.code(404).send({ error: 'Invalid path' });
|
|
189
|
+
const resolved = path.resolve(pumlFsPath);
|
|
190
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
191
|
+
if (!fs.existsSync(resolved) ||
|
|
192
|
+
!['.puml', '.plantuml', '.pu'].includes(ext)) {
|
|
193
|
+
return reply.code(404).send({ error: 'PlantUML file not found' });
|
|
194
|
+
}
|
|
195
|
+
const supported = getPlantUmlFormats();
|
|
196
|
+
const format = supported.includes(request.query.format ?? '')
|
|
197
|
+
? request.query.format
|
|
198
|
+
: 'svg';
|
|
199
|
+
const source = fs.readFileSync(resolved, 'utf8');
|
|
200
|
+
const cachedBuffer = getCachedDiagramBuffer('plantuml', source, format);
|
|
201
|
+
if (cachedBuffer) {
|
|
202
|
+
const baseName = path.basename(resolved, ext);
|
|
203
|
+
return reply
|
|
204
|
+
.header('Content-Type', DIAGRAM_CONTENT_TYPES[format] ?? 'application/octet-stream')
|
|
205
|
+
.header('Content-Disposition', `attachment; filename="${baseName}.${format}"`)
|
|
206
|
+
.send(cachedBuffer);
|
|
207
|
+
}
|
|
208
|
+
const buffer = await renderPlantUmlToBuffer(resolved, format);
|
|
209
|
+
if (!buffer)
|
|
210
|
+
return reply.code(500).send({ error: 'PlantUML render failed' });
|
|
211
|
+
cacheDiagramBuffer('plantuml', source, buffer, format);
|
|
212
|
+
const baseName = path.basename(resolved, ext);
|
|
213
|
+
return reply
|
|
214
|
+
.header('Content-Type', DIAGRAM_CONTENT_TYPES[format] ?? 'application/octet-stream')
|
|
215
|
+
.header('Content-Disposition', `attachment; filename="${baseName}.${format}"`)
|
|
216
|
+
.send(buffer);
|
|
217
|
+
});
|
|
218
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File content API routes.
|
|
3
|
+
*
|
|
4
|
+
* Handles: GET /api/file/*, PUT /api/file/*
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { getConfig } from '../../config/index.js';
|
|
9
|
+
import { rewriteLinksForDeepShare } from '../../services/deepShareLinks.js';
|
|
10
|
+
import { getOrRenderDiagram } from '../../services/diagramCache.js';
|
|
11
|
+
import { renderEmbeddedDiagrams, setDiagramContext, } from '../../services/embeddedDiagrams.js';
|
|
12
|
+
import { registerDiagramHashes } from '../../services/exportCache.js';
|
|
13
|
+
import { parseMarkdown } from '../../services/markdown.js';
|
|
14
|
+
import { renderMermaidSvg } from '../../services/mermaid.js';
|
|
15
|
+
import { renderPlantUmlSvg } from '../../services/plantuml.js';
|
|
16
|
+
import { filterBreadcrumbsForOutsider } from '../../util/breadcrumbs.js';
|
|
17
|
+
import { looksLikeText } from '../../util/fileDetection.js';
|
|
18
|
+
import { breadcrumbParts, getRoots, urlPathToFs } from '../../util/platform.js';
|
|
19
|
+
/** Image extensions recognized for type detection. */
|
|
20
|
+
const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico'];
|
|
21
|
+
/** PlantUML file extensions. */
|
|
22
|
+
const PLANTUML_EXTS = ['.puml', '.plantuml', '.pu'];
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
24
|
+
export const fileContentRoutes = async (fastify) => {
|
|
25
|
+
const roots = getRoots(getConfig().roots);
|
|
26
|
+
// GET /api/file/*
|
|
27
|
+
fastify.get('/api/file/*', async (request, reply) => {
|
|
28
|
+
const rawOnly = request.query.raw === '1';
|
|
29
|
+
const reqPath = request.params['*'];
|
|
30
|
+
if (!reqPath)
|
|
31
|
+
return reply.code(400).send({ error: 'Path required' });
|
|
32
|
+
const fsFilePath = urlPathToFs(reqPath, roots);
|
|
33
|
+
if (!fsFilePath)
|
|
34
|
+
return reply.code(404).send({ error: 'Invalid path' });
|
|
35
|
+
const resolved = path.resolve(fsFilePath);
|
|
36
|
+
if (!fs.existsSync(resolved))
|
|
37
|
+
return reply.code(404).send({ error: 'Not found' });
|
|
38
|
+
const stats = fs.statSync(resolved);
|
|
39
|
+
if (stats.isDirectory())
|
|
40
|
+
return reply
|
|
41
|
+
.code(400)
|
|
42
|
+
.send({ error: 'Use /api/path/ for directories' });
|
|
43
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
44
|
+
const fileName = path.basename(resolved);
|
|
45
|
+
const isInsider = request.accessMode === 'insider';
|
|
46
|
+
const matchedPath = request.authMatchedPath ?? null;
|
|
47
|
+
const breadcrumbs = filterBreadcrumbsForOutsider(breadcrumbParts(resolved, roots), isInsider, matchedPath, false);
|
|
48
|
+
// Markdown
|
|
49
|
+
if (ext === '.md') {
|
|
50
|
+
return handleMarkdown(request, reply, resolved, reqPath, rawOnly, fileName, breadcrumbs, isInsider);
|
|
51
|
+
}
|
|
52
|
+
// Mermaid
|
|
53
|
+
if (ext === '.mmd') {
|
|
54
|
+
const content = fs.readFileSync(resolved, 'utf8');
|
|
55
|
+
if (rawOnly)
|
|
56
|
+
return reply.send({
|
|
57
|
+
type: 'mermaid',
|
|
58
|
+
content,
|
|
59
|
+
fileName,
|
|
60
|
+
breadcrumbs,
|
|
61
|
+
isInsider,
|
|
62
|
+
});
|
|
63
|
+
const renderedMermaid = await getOrRenderDiagram('mermaid', content, () => renderMermaidSvg(resolved));
|
|
64
|
+
return reply.send({
|
|
65
|
+
type: 'mermaid',
|
|
66
|
+
content,
|
|
67
|
+
html: renderedMermaid,
|
|
68
|
+
fileName,
|
|
69
|
+
breadcrumbs,
|
|
70
|
+
isInsider,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// PlantUML
|
|
74
|
+
if (PLANTUML_EXTS.includes(ext)) {
|
|
75
|
+
const content = fs.readFileSync(resolved, 'utf8');
|
|
76
|
+
if (rawOnly)
|
|
77
|
+
return reply.send({
|
|
78
|
+
type: 'plantuml',
|
|
79
|
+
content,
|
|
80
|
+
fileName,
|
|
81
|
+
breadcrumbs,
|
|
82
|
+
isInsider,
|
|
83
|
+
});
|
|
84
|
+
const renderedPuml = await getOrRenderDiagram('plantuml', content, () => renderPlantUmlSvg(resolved));
|
|
85
|
+
return reply.send({
|
|
86
|
+
type: 'plantuml',
|
|
87
|
+
content,
|
|
88
|
+
html: renderedPuml,
|
|
89
|
+
fileName,
|
|
90
|
+
breadcrumbs,
|
|
91
|
+
isInsider,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// SVG
|
|
95
|
+
if (ext === '.svg') {
|
|
96
|
+
const content = fs.readFileSync(resolved, 'utf8');
|
|
97
|
+
return reply.send({
|
|
98
|
+
type: 'svg',
|
|
99
|
+
content,
|
|
100
|
+
fileName,
|
|
101
|
+
breadcrumbs,
|
|
102
|
+
isInsider,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// Text files
|
|
106
|
+
const buffer = fs.readFileSync(resolved);
|
|
107
|
+
if (looksLikeText(buffer)) {
|
|
108
|
+
// Try watcher render for non-natively-renderable text files
|
|
109
|
+
if (!rawOnly) {
|
|
110
|
+
const renderResult = await tryWatcherRender(resolved);
|
|
111
|
+
if (renderResult && renderResult.renderAs === 'md') {
|
|
112
|
+
const { html, headings } = await renderMarkdownContent(renderResult.content, request, resolved, reqPath, isInsider);
|
|
113
|
+
return await reply.send({
|
|
114
|
+
type: 'markdown',
|
|
115
|
+
content: buffer.toString('utf8'),
|
|
116
|
+
html,
|
|
117
|
+
headings,
|
|
118
|
+
fileName,
|
|
119
|
+
breadcrumbs,
|
|
120
|
+
isInsider,
|
|
121
|
+
renderAs: renderResult.renderAs,
|
|
122
|
+
matchedRules: renderResult.rules,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return handleText(reply, buffer, ext, rawOnly, fileName, breadcrumbs, isInsider);
|
|
127
|
+
}
|
|
128
|
+
// Images
|
|
129
|
+
if (IMAGE_EXTS.includes(ext)) {
|
|
130
|
+
return reply.send({ type: 'image', fileName, breadcrumbs, isInsider });
|
|
131
|
+
}
|
|
132
|
+
// Binary
|
|
133
|
+
return reply.send({
|
|
134
|
+
type: 'binary',
|
|
135
|
+
fileName,
|
|
136
|
+
size: stats.size,
|
|
137
|
+
breadcrumbs,
|
|
138
|
+
isInsider,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
// PUT /api/file/*
|
|
142
|
+
fastify.put('/api/file/*', async (request, reply) => {
|
|
143
|
+
if (request.accessMode !== 'insider') {
|
|
144
|
+
return reply.code(403).send({ error: 'Insider access required' });
|
|
145
|
+
}
|
|
146
|
+
const reqPath = request.params['*'];
|
|
147
|
+
const fsPath = urlPathToFs(reqPath, roots);
|
|
148
|
+
if (!fsPath)
|
|
149
|
+
return reply.code(404).send({ error: 'Invalid path' });
|
|
150
|
+
const resolved = path.resolve(fsPath);
|
|
151
|
+
try {
|
|
152
|
+
const stat = await fs.promises.stat(resolved);
|
|
153
|
+
if (!stat.isFile())
|
|
154
|
+
return await reply
|
|
155
|
+
.code(400)
|
|
156
|
+
.send({ error: 'Can only write to files' });
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return reply.code(404).send({ error: 'File not found' });
|
|
160
|
+
}
|
|
161
|
+
const body = request.body;
|
|
162
|
+
if (!body || typeof body.content !== 'string') {
|
|
163
|
+
return reply
|
|
164
|
+
.code(400)
|
|
165
|
+
.send({ error: 'Request body must include "content" string' });
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
await fs.promises.writeFile(resolved, body.content, 'utf8');
|
|
169
|
+
return await reply.send({
|
|
170
|
+
ok: true,
|
|
171
|
+
path: resolved,
|
|
172
|
+
size: Buffer.byteLength(body.content, 'utf8'),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
return reply
|
|
177
|
+
.code(500)
|
|
178
|
+
.send({ error: `Write failed: ${err.message}` });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Try to render a file via the watcher's render endpoint.
|
|
184
|
+
* Returns null if watcher is not configured, unreachable, or no rules match.
|
|
185
|
+
*/
|
|
186
|
+
async function tryWatcherRender(fsPath) {
|
|
187
|
+
const config = getConfig();
|
|
188
|
+
if (!config.watcherUrl)
|
|
189
|
+
return null;
|
|
190
|
+
try {
|
|
191
|
+
const res = await fetch(`${config.watcherUrl}/render`, {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: { 'Content-Type': 'application/json' },
|
|
194
|
+
body: JSON.stringify({ path: fsPath }),
|
|
195
|
+
signal: AbortSignal.timeout(5000),
|
|
196
|
+
});
|
|
197
|
+
if (!res.ok)
|
|
198
|
+
return null;
|
|
199
|
+
const data = (await res.json());
|
|
200
|
+
if (data.rules.length === 0)
|
|
201
|
+
return null;
|
|
202
|
+
return data;
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
console.warn('Watcher render failed for ' + fsPath + ':', err);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Shared markdown rendering pipeline: parse → diagram hashes → optional
|
|
211
|
+
* diagram rendering → optional deep share link rewriting.
|
|
212
|
+
*/
|
|
213
|
+
async function renderMarkdownContent(markdownSource, request, resolved, reqPath, isInsider) {
|
|
214
|
+
const urlDir = reqPath.includes('/')
|
|
215
|
+
? reqPath.substring(0, reqPath.lastIndexOf('/'))
|
|
216
|
+
: '';
|
|
217
|
+
const fsDir = path.dirname(resolved);
|
|
218
|
+
setDiagramContext(fsDir);
|
|
219
|
+
const { headings, html: parsedHtml } = parseMarkdown(markdownSource, {
|
|
220
|
+
linkWindowsPaths: true,
|
|
221
|
+
basePath: urlDir,
|
|
222
|
+
});
|
|
223
|
+
let html = parsedHtml;
|
|
224
|
+
// Register diagram hashes for cache-clear reverse index
|
|
225
|
+
const diagramHashMatches = [
|
|
226
|
+
...html.matchAll(/data-diagram-hash="([a-f0-9]{64})"/g),
|
|
227
|
+
];
|
|
228
|
+
if (diagramHashMatches.length > 0) {
|
|
229
|
+
registerDiagramHashes(resolved, diagramHashMatches.map((m) => m[1]));
|
|
230
|
+
}
|
|
231
|
+
if (request.query.render_diagrams === '1') {
|
|
232
|
+
html = await renderEmbeddedDiagrams(html, fsDir);
|
|
233
|
+
}
|
|
234
|
+
const deepShare = request.deepShareParams;
|
|
235
|
+
const seed = request.authSeed;
|
|
236
|
+
if (!isInsider && deepShare && seed) {
|
|
237
|
+
const maxDepth = parseInt(deepShare.d, 10);
|
|
238
|
+
const dirs = deepShare.dirs === '1';
|
|
239
|
+
const currentPath = `/${reqPath}`;
|
|
240
|
+
if (!isNaN(maxDepth) && maxDepth > 0) {
|
|
241
|
+
html = rewriteLinksForDeepShare(html, seed, currentPath, maxDepth, dirs, deepShare.s, request.query.exp);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { html, headings };
|
|
245
|
+
}
|
|
246
|
+
/** Handle markdown file content. */
|
|
247
|
+
async function handleMarkdown(request, reply, resolved, reqPath, rawOnly, fileName, breadcrumbs, isInsider) {
|
|
248
|
+
const markdown = fs.readFileSync(resolved, 'utf8');
|
|
249
|
+
if (rawOnly)
|
|
250
|
+
return reply.send({
|
|
251
|
+
type: 'markdown',
|
|
252
|
+
content: markdown,
|
|
253
|
+
fileName,
|
|
254
|
+
breadcrumbs,
|
|
255
|
+
isInsider,
|
|
256
|
+
});
|
|
257
|
+
const { html, headings } = await renderMarkdownContent(markdown, request, resolved, reqPath, isInsider);
|
|
258
|
+
return reply.send({
|
|
259
|
+
type: 'markdown',
|
|
260
|
+
content: markdown,
|
|
261
|
+
html,
|
|
262
|
+
headings,
|
|
263
|
+
fileName,
|
|
264
|
+
breadcrumbs,
|
|
265
|
+
isInsider,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
/** Handle text file content with optional syntax highlighting. */
|
|
269
|
+
function handleText(reply, buffer, ext, rawOnly, fileName, breadcrumbs, isInsider) {
|
|
270
|
+
const textContent = buffer.toString('utf8');
|
|
271
|
+
if (rawOnly)
|
|
272
|
+
return reply.send({
|
|
273
|
+
type: 'text',
|
|
274
|
+
content: textContent,
|
|
275
|
+
fileName,
|
|
276
|
+
breadcrumbs,
|
|
277
|
+
isInsider,
|
|
278
|
+
});
|
|
279
|
+
return reply.send({
|
|
280
|
+
type: 'text',
|
|
281
|
+
content: textContent,
|
|
282
|
+
fileName,
|
|
283
|
+
breadcrumbs,
|
|
284
|
+
isInsider,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API route registrar — composes all API sub-plugins.
|
|
3
|
+
*/
|
|
4
|
+
import { authStatusRoutes } from './auth-status.js';
|
|
5
|
+
import { diagramsRoutes } from './diagrams.js';
|
|
6
|
+
import { directoryRoutes } from './directory.js';
|
|
7
|
+
import { drivesRoutes } from './drives.js';
|
|
8
|
+
import { exportRoutes } from './export.js';
|
|
9
|
+
import { fileContentRoutes } from './fileContent.js';
|
|
10
|
+
import { linkInfoRoutes } from './linkInfo.js';
|
|
11
|
+
import { addAuthMiddleware } from './middleware.js';
|
|
12
|
+
import { rawRoutes } from './raw.js';
|
|
13
|
+
import { runnerRoutes } from './runner.js';
|
|
14
|
+
import { searchRoutes } from './search.js';
|
|
15
|
+
import { sharingRoutes } from './sharing.js';
|
|
16
|
+
import { statusRoutes } from './status.js';
|
|
17
|
+
export const apiRoute = async (fastify) => {
|
|
18
|
+
// Add auth hook directly to this context (not as a child plugin)
|
|
19
|
+
// so it applies to all routes registered below.
|
|
20
|
+
addAuthMiddleware(fastify);
|
|
21
|
+
await fastify.register(drivesRoutes);
|
|
22
|
+
await fastify.register(directoryRoutes);
|
|
23
|
+
await fastify.register(fileContentRoutes);
|
|
24
|
+
await fastify.register(linkInfoRoutes);
|
|
25
|
+
await fastify.register(rawRoutes);
|
|
26
|
+
await fastify.register(exportRoutes);
|
|
27
|
+
await fastify.register(diagramsRoutes);
|
|
28
|
+
await fastify.register(runnerRoutes);
|
|
29
|
+
await fastify.register(searchRoutes);
|
|
30
|
+
await fastify.register(sharingRoutes);
|
|
31
|
+
await fastify.register(authStatusRoutes);
|
|
32
|
+
await fastify.register(statusRoutes);
|
|
33
|
+
};
|