collabmd 0.1.45 → 0.1.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -19
- package/bin/collabmd.js +27 -1
- package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CYM5nxUi.js +1 -0
- package/dist/client/assets/browser-utils--H_DkyEF.js +1 -0
- package/dist/client/assets/chunk-ERSWUTIN-hHdvnhaY.js +41 -0
- package/dist/client/assets/comment-thread-store-CdcwrObc.js +1 -0
- package/dist/client/assets/comment-threads-9J6Em-_y.js +4 -0
- package/dist/client/assets/comment-ui-shared-BC4pt4w8.js +7 -0
- package/dist/client/assets/dist-B5HTH2BO.js +1 -0
- package/dist/client/assets/{dist-DhLvC8DS.js → dist-B5Jf1SLZ.js} +2 -2
- package/dist/client/assets/{dist-CnL7Y2sM.js → dist-BDt3rz7l.js} +1 -1
- package/dist/client/assets/dist-BEwK4csK.js +1 -0
- package/dist/client/assets/dist-BKW4PALF.js +1 -0
- package/dist/client/assets/dist-BWcHHOx-.js +1 -0
- package/dist/client/assets/{dist-CYZmbokb.js → dist-BalScOwZ.js} +9 -9
- package/dist/client/assets/dist-Bf8f4HmM.js +6 -0
- package/dist/client/assets/dist-BjHJDtyU.js +1 -0
- package/dist/client/assets/dist-Bm6pf9Bl.js +1 -0
- package/dist/client/assets/dist-BoVBdSjk.js +1 -0
- package/dist/client/assets/dist-CC5IrQMG.js +1 -0
- package/dist/client/assets/{dist-C11hHqv_.js → dist-CCP_AKPG.js} +12 -12
- package/dist/client/assets/dist-CZgOmxK6.js +1 -0
- package/dist/client/assets/{dist-D2AQ-76g.js → dist-Cqhv5LAk.js} +1 -1
- package/dist/client/assets/{dist-MvcV4190.js → dist-D-Z6FLem.js} +1 -1
- package/dist/client/assets/dist-DLMiIsNC.js +11 -0
- package/dist/client/assets/{dist-3bgCp7iY.js → dist-DZKu9Cs8.js} +1 -1
- package/dist/client/assets/dist-DaYdhcVs.js +1 -0
- package/dist/client/assets/dist-FWJsgGYI.js +1 -0
- package/dist/client/assets/dist-Kh2COra_.js +1 -0
- package/dist/client/assets/{dist-JYEDRrPT.js → dist-PDZ3bSqm.js} +1 -1
- package/dist/client/assets/{dist-Drp8uu5J.js → dist-TjMh8n1u.js} +1 -1
- package/dist/client/assets/{dist-DGb27Dl6.js → dist-dtAdgL39.js} +1 -1
- package/dist/client/assets/dist-jv4WdPnK.js +9 -0
- package/dist/client/assets/{dist-DK5Gs2wa.js → dist-rGBmqxC7.js} +1 -1
- package/dist/client/assets/{dist-DCzuX1AF.js → dist-shrl6K_0.js} +1 -1
- package/dist/client/assets/dist-tOaegDYS2.js +1 -0
- package/dist/client/assets/dist-ule2n6ak.js +1 -0
- package/dist/client/assets/dist-xElaxbia.js +1 -0
- package/dist/client/assets/{drawio-editor-ClE8jpoG.js → drawio-editor-FzY1cVw8.js} +1 -1
- package/dist/client/assets/drawioEditor-BjiKJqlc.js +2 -0
- package/dist/client/assets/editor-session-Cm9ekeNo.js +54 -0
- package/dist/client/assets/embedded-editor-base-CwTkk911.css +1 -0
- package/dist/client/assets/{preview-render-profile-BZzGSg6K.js → es-DXGV3RHd.js} +13 -13
- package/dist/client/assets/excalidraw-editor-B46zsTFI.js +9 -0
- package/dist/client/assets/{excalidraw-editor-CyGwOhLz.css → excalidraw-editor-Bt7gMSoh.css} +1 -1
- package/dist/client/assets/excalidraw-scene-BOben2gr.js +1 -0
- package/dist/client/assets/excalidrawEditor-CT2pHq7e.js +2 -0
- package/dist/client/assets/{export-page-CUfws2T-.js → export-page-A7NHawJY.js} +3 -3
- package/dist/client/assets/exportDocument-B2L8fe34.css +1 -0
- package/dist/client/assets/exportDocument-BaqcOAAW.js +2 -0
- package/dist/client/assets/file-icon-svg-CQAjlWjK.js +1 -0
- package/dist/client/assets/file-kind-BbqgGzpc.js +1 -0
- package/dist/client/assets/image-QXRYHUNP-SWEV8H0J.js +1 -0
- package/dist/client/assets/index-ClbjMBVt.js +2 -0
- package/dist/client/assets/index-Pum8ET-5.css +1 -0
- package/dist/client/assets/main-B3uPyqic.js +893 -0
- package/dist/client/assets/pdf-BL9Pr2D5.js +54 -0
- package/dist/client/assets/pdf.worker-CLesOks4.mjs +64857 -0
- package/dist/client/assets/pdf_viewer-FVkkThnz.js +551 -0
- package/dist/client/assets/{preview-render-compiler-BSaQO_p3.js → preview-render-compiler-BD-XhkS2.js} +2 -2
- package/dist/client/assets/preview-render-compiler-BKedaMxx.js +1 -0
- package/dist/client/assets/preview-render-profile-CE-5rVTC.js +1 -0
- package/dist/client/assets/{preview-render-worker-B42LWvrD.js → preview-render-worker-CiPqc9n6.js} +20 -20
- package/dist/client/assets/prod-Dy20X7eT.js +1 -0
- package/dist/client/assets/prod-hnwh7wRM.js +177 -0
- package/dist/client/assets/quick-switcher-controller-fqOYR7qR.js +12 -0
- package/dist/client/assets/runtime-config-BLrEKVO1.js +1 -0
- package/dist/client/assets/vault-api-client-_LpV1aIn.js +6 -0
- package/dist/client/drawio-editor.html +1 -1
- package/dist/client/excalidraw-editor.html +1 -1
- package/dist/client/export-document.html +2 -2
- package/dist/client/index.html +15 -3
- package/docker-compose.yml +42 -0
- package/package.json +17 -4
- package/scripts/local-structurizr-compose.mjs +101 -0
- package/src/client/app/index.html +13 -1
- package/src/client/application/app-shell/comments-feature.js +19 -10
- package/src/client/application/app-shell/git-feature.js +12 -0
- package/src/client/application/app-shell/presence-feature.js +2 -1
- package/src/client/application/app-shell/ui-feature-shell.js +45 -2
- package/src/client/application/app-shell/ui-feature-toolbar.js +2 -52
- package/src/client/application/app-shell/workspace-feature.js +5 -1
- package/src/client/application/app-shell-elements.js +2 -0
- package/src/client/application/pdf-preview-controller.js +992 -0
- package/src/client/application/structurizr-preview-controller.js +395 -0
- package/src/client/application/workspace-coordinator.js +25 -3
- package/src/client/application/workspace-preview-controller.js +91 -2
- package/src/client/bootstrap/collabmd-app-shell.js +31 -5
- package/src/client/browser-utils.js +44 -0
- package/src/client/domain/runtime-paths.js +26 -5
- package/src/client/domain/structurizr-language.js +99 -0
- package/src/client/domain/vault-paths.js +48 -0
- package/src/client/excalidraw-editor.js +833 -51
- package/src/client/infrastructure/browser-preferences-port.js +10 -0
- package/src/client/infrastructure/comment-thread-store.js +34 -13
- package/src/client/infrastructure/editor-session.js +10 -0
- package/src/client/infrastructure/editor-view-adapter.js +46 -2
- package/src/client/infrastructure/excalidraw-room-client.js +103 -14
- package/src/client/infrastructure/git-api-client.js +5 -0
- package/src/client/infrastructure/vault-api-client.js +26 -0
- package/src/client/presentation/comment-ui/comment-ui-shared.js +4 -0
- package/src/client/presentation/drawio-embed-controller.js +9 -102
- package/src/client/presentation/drawio-viewer.js +118 -0
- package/src/client/presentation/excalidraw-embed-controller.js +74 -8
- package/src/client/presentation/file-action-controller.js +102 -0
- package/src/client/presentation/file-explorer-view.js +10 -29
- package/src/client/presentation/file-icon-svg.js +40 -0
- package/src/client/presentation/file-tree-state.js +2 -0
- package/src/client/presentation/git-diff-view-controller.js +731 -3
- package/src/client/presentation/outline-controller.js +86 -0
- package/src/client/presentation/quick-switcher-controller.js +25 -13
- package/src/client/presentation/quick-switcher-text-search.js +1 -1
- package/src/client/styles/components/navigation.css +11 -1
- package/src/client/styles/features/diagram-preview.css +76 -0
- package/src/client/styles/features/diff-special.css +309 -0
- package/src/client/styles/foundation/themes.css +6 -0
- package/src/client/styles/layout/shell.css +358 -1
- package/src/client/styles/layout/view-modes.css +1 -0
- package/src/client/styles/style.css +1 -0
- package/src/client/styles/surfaces/excalidraw-editor.css +294 -0
- package/src/domain/comment-threads.js +63 -5
- package/src/domain/excalidraw-room-codec.js +6 -4
- package/src/domain/file-kind.js +35 -3
- package/src/server/config/env.js +79 -0
- package/src/server/create-app-server.js +12 -0
- package/src/server/domain/bases/base-definition.js +1 -1
- package/src/server/domain/bases/base-expression-runtime.js +1 -4
- package/src/server/domain/bases/base-index-snapshot-store.js +1 -4
- package/src/server/domain/bases/base-transform.js +1 -4
- package/src/server/domain/collaboration/collaboration-room.js +55 -21
- package/src/server/domain/comment-overview.js +15 -5
- package/src/server/domain/ripgrep-search-service.js +4 -0
- package/src/server/infrastructure/git/diff-service.js +5 -0
- package/src/server/infrastructure/git/git-service.js +21 -0
- package/src/server/infrastructure/git/history-service.js +54 -0
- package/src/server/infrastructure/git/image-mime.js +14 -0
- package/src/server/infrastructure/git/untracked-files.js +28 -1
- package/src/server/infrastructure/http/create-git-api-query-handler.js +31 -1
- package/src/server/infrastructure/http/create-request-handler.js +16 -23
- package/src/server/infrastructure/http/create-static-handler.js +5 -2
- package/src/server/infrastructure/http/create-structurizr-api-handler.js +145 -0
- package/src/server/infrastructure/http/create-vault-api-command-handler.js +66 -4
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +51 -3
- package/src/server/infrastructure/http/http-request-helpers.js +22 -0
- package/src/server/infrastructure/persistence/path-utils.js +1 -1
- package/src/server/infrastructure/persistence/vault-file-store.js +3 -0
- package/src/server/infrastructure/structurizr/structurizr-embed.css +43 -0
- package/src/server/infrastructure/structurizr/structurizr-workspace-service.js +374 -0
- package/src/server/infrastructure/websocket/attach-collaboration-gateway.js +4 -22
- package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CbSISC1w.js +0 -1
- package/dist/client/assets/browser-utils-CKtI_rZl.js +0 -1
- package/dist/client/assets/chunk-ERSWUTIN-ME9RVNel.js +0 -41
- package/dist/client/assets/comment-threads-DDegmaFO.js +0 -4
- package/dist/client/assets/dist-BH87JoHx.js +0 -11
- package/dist/client/assets/dist-BIkPRZO7.js +0 -6
- package/dist/client/assets/dist-BqCsFpx4.js +0 -1
- package/dist/client/assets/dist-C2JHCt8U.js +0 -1
- package/dist/client/assets/dist-CZjTtmTu.js +0 -1
- package/dist/client/assets/dist-Cig4Ov_X.js +0 -1
- package/dist/client/assets/dist-CqQEC8gR.js +0 -1
- package/dist/client/assets/dist-D4Rzqod1.js +0 -1
- package/dist/client/assets/dist-D5sn-mwk.js +0 -9
- package/dist/client/assets/dist-D7G-n87v.js +0 -1
- package/dist/client/assets/dist-DRqOrNgN.js +0 -1
- package/dist/client/assets/dist-DetkMS_4.js +0 -1
- package/dist/client/assets/dist-Dp6FVkQ1.js +0 -1
- package/dist/client/assets/dist-DtbjNB28.js +0 -1
- package/dist/client/assets/dist-DyrYcvpo2.js +0 -1
- package/dist/client/assets/dist-GrygD2zE.js +0 -1
- package/dist/client/assets/dist-HmEpWUow.js +0 -1
- package/dist/client/assets/dist-RTa81Km7.js +0 -1
- package/dist/client/assets/drawioEditor-CXaTM9Ds.js +0 -2
- package/dist/client/assets/editor-session-GTwZS6Df.js +0 -16
- package/dist/client/assets/embedded-editor-base-CiNEc8vy.css +0 -1
- package/dist/client/assets/excalidraw-editor-BZVM81BO.js +0 -9
- package/dist/client/assets/excalidraw-scene-C8n9CHsK.js +0 -176
- package/dist/client/assets/excalidrawEditor-DxJanCze.js +0 -2
- package/dist/client/assets/exportDocument-BCuxqXDY.js +0 -2
- package/dist/client/assets/exportDocument-DV-OX74E.css +0 -1
- package/dist/client/assets/file-kind-BKPOIRPE.js +0 -1
- package/dist/client/assets/image-QXRYHUNP-lfTHE3bb.js +0 -1
- package/dist/client/assets/index-C3YnUgMD.css +0 -1
- package/dist/client/assets/index-D2wXraJ1.js +0 -2
- package/dist/client/assets/main-DtiGz7uI.js +0 -795
- package/dist/client/assets/preview-render-compiler-csGM5ZFz.js +0 -1
- package/dist/client/assets/quick-switcher-controller-shW_6vPY.js +0 -12
- package/dist/client/assets/room-CQLoqtMo.js +0 -1
- package/dist/client/assets/runtime-config-DSGmGE9V.js +0 -1
- package/dist/client/assets/vault-api-client-C6bcGwy-.js +0 -5
- package/src/server/domain/collaboration/room-persistence-controller.js +0 -80
|
@@ -70,6 +70,23 @@ export class GitService {
|
|
|
70
70
|
|
|
71
71
|
return String(result.stdout ?? '');
|
|
72
72
|
},
|
|
73
|
+
async execGitBuffer(args, { env = null } = {}) {
|
|
74
|
+
const result = await gitExecFile('git', ['-c', 'core.quotepath=false', ...args], {
|
|
75
|
+
cwd: vaultDir,
|
|
76
|
+
encoding: null,
|
|
77
|
+
env: {
|
|
78
|
+
...process.env,
|
|
79
|
+
...(commandEnv ?? {}),
|
|
80
|
+
...(env ?? {}),
|
|
81
|
+
},
|
|
82
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
83
|
+
timeout: 10_000,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return Buffer.isBuffer(result.stdout)
|
|
87
|
+
? result.stdout
|
|
88
|
+
: Buffer.from(result.stdout ?? '');
|
|
89
|
+
},
|
|
73
90
|
};
|
|
74
91
|
this.untrackedFileService = new GitUntrackedFileService({ vaultDir });
|
|
75
92
|
this.statusService = new GitStatusService({
|
|
@@ -293,6 +310,10 @@ export class GitService {
|
|
|
293
310
|
return this.historyService.getFileSnapshot({ hash, path });
|
|
294
311
|
}
|
|
295
312
|
|
|
313
|
+
async getFileAttachment({ hash, path } = {}) {
|
|
314
|
+
return this.historyService.getFileAttachment({ hash, path });
|
|
315
|
+
}
|
|
316
|
+
|
|
296
317
|
async pathExistsAtRef(ref, path) {
|
|
297
318
|
try {
|
|
298
319
|
await this.commandRunner.execGit(['cat-file', '-e', `${ref}:${path}`]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRequestError as createGitRequestError } from '../http/http-errors.js';
|
|
2
2
|
import { getVaultFileKind, isImageAttachmentFilePath } from '../../../domain/file-kind.js';
|
|
3
3
|
import { normalizeRelativeGitPath } from './path-utils.js';
|
|
4
|
+
import { getImageMimeType } from './image-mime.js';
|
|
4
5
|
import {
|
|
5
6
|
countPatchLines,
|
|
6
7
|
parseGitLogHeader,
|
|
@@ -308,6 +309,7 @@ export class GitHistoryService {
|
|
|
308
309
|
|
|
309
310
|
if (metaOnly) {
|
|
310
311
|
const metaResponse = {
|
|
312
|
+
baseRef: meta.baseRef,
|
|
311
313
|
commit: meta.commit,
|
|
312
314
|
files: meta.files,
|
|
313
315
|
isGitRepo: true,
|
|
@@ -335,6 +337,7 @@ export class GitHistoryService {
|
|
|
335
337
|
&& (fileSummary.additions + fileSummary.deletions) > this.maxInitialPatchLines
|
|
336
338
|
) {
|
|
337
339
|
const guardedResponse = {
|
|
340
|
+
baseRef: meta.baseRef,
|
|
338
341
|
commit: meta.commit,
|
|
339
342
|
files: [{
|
|
340
343
|
...baseFile,
|
|
@@ -396,6 +399,7 @@ export class GitHistoryService {
|
|
|
396
399
|
};
|
|
397
400
|
|
|
398
401
|
const response = {
|
|
402
|
+
baseRef: meta.baseRef,
|
|
399
403
|
commit: meta.commit,
|
|
400
404
|
files: [detailedFile],
|
|
401
405
|
isGitRepo: true,
|
|
@@ -460,6 +464,46 @@ export class GitHistoryService {
|
|
|
460
464
|
});
|
|
461
465
|
}
|
|
462
466
|
|
|
467
|
+
async getFileAttachment({ hash, path } = {}) {
|
|
468
|
+
const isGitRepo = await this.commandRunner.isGitRepo();
|
|
469
|
+
const normalizedHash = this.normalizeAttachmentRef(hash);
|
|
470
|
+
const normalizedPath = normalizeRelativeGitPath(path);
|
|
471
|
+
|
|
472
|
+
if (!isGitRepo) {
|
|
473
|
+
throw createGitRequestError(404, 'Git repository not found');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!isImageAttachmentFilePath(normalizedPath)) {
|
|
477
|
+
throw createGitRequestError(400, 'Unsupported image attachment path');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const cacheKey = JSON.stringify({
|
|
481
|
+
hash: normalizedHash,
|
|
482
|
+
path: normalizedPath,
|
|
483
|
+
type: 'file-attachment',
|
|
484
|
+
});
|
|
485
|
+
const cached = getCachedValue(this.commitCache, cacheKey);
|
|
486
|
+
if (cached) {
|
|
487
|
+
return cached;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return this.runRequest(cacheKey, async () => {
|
|
491
|
+
let content;
|
|
492
|
+
try {
|
|
493
|
+
content = await this.commandRunner.execGitBuffer(['cat-file', 'blob', `${normalizedHash}:${normalizedPath}`]);
|
|
494
|
+
} catch {
|
|
495
|
+
throw createGitRequestError(404, 'Commit image not found');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return setCachedValue(this.commitCache, cacheKey, {
|
|
499
|
+
content,
|
|
500
|
+
hash: normalizedHash,
|
|
501
|
+
mimeType: getImageMimeType(normalizedPath),
|
|
502
|
+
path: normalizedPath,
|
|
503
|
+
}, this.responseCacheTtlMs);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
463
507
|
async runRequest(key, callback) {
|
|
464
508
|
if (this.pendingRequests.has(key)) {
|
|
465
509
|
return this.pendingRequests.get(key);
|
|
@@ -494,6 +538,14 @@ export class GitHistoryService {
|
|
|
494
538
|
return normalizedHash;
|
|
495
539
|
}
|
|
496
540
|
|
|
541
|
+
normalizeAttachmentRef(hash) {
|
|
542
|
+
const normalizedHash = String(hash ?? '').trim();
|
|
543
|
+
if (normalizedHash === 'HEAD') {
|
|
544
|
+
return normalizedHash;
|
|
545
|
+
}
|
|
546
|
+
return this.normalizeCommitHash(normalizedHash);
|
|
547
|
+
}
|
|
548
|
+
|
|
497
549
|
parseHistoryChunk(chunk) {
|
|
498
550
|
const [headerLine = '', ...statLines] = String(chunk ?? '')
|
|
499
551
|
.split(/\r?\n/u)
|
|
@@ -588,6 +640,7 @@ export class GitHistoryService {
|
|
|
588
640
|
authoredAt: chunk.authoredAt,
|
|
589
641
|
deletions: Number(numstatEntry?.deletions || 0),
|
|
590
642
|
filesChanged: 1,
|
|
643
|
+
fileKind: getVaultFileKind(nameStatusEntry?.path),
|
|
591
644
|
hash: chunk.hash,
|
|
592
645
|
isMergeCommit: chunk.isMergeCommit,
|
|
593
646
|
oldPath: nameStatusEntry?.oldPath ?? null,
|
|
@@ -679,6 +732,7 @@ export class GitHistoryService {
|
|
|
679
732
|
const numstatEntry = numstatEntries[index];
|
|
680
733
|
return {
|
|
681
734
|
...entry,
|
|
735
|
+
fileKind: getVaultFileKind(entry.path),
|
|
682
736
|
stats: numstatEntry
|
|
683
737
|
? {
|
|
684
738
|
additions: numstatEntry.additions,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const IMAGE_MIME_TYPES = Object.freeze({
|
|
2
|
+
'.gif': 'image/gif',
|
|
3
|
+
'.jpeg': 'image/jpeg',
|
|
4
|
+
'.jpg': 'image/jpeg',
|
|
5
|
+
'.png': 'image/png',
|
|
6
|
+
'.svg': 'image/svg+xml',
|
|
7
|
+
'.webp': 'image/webp',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export function getImageMimeType(filePath) {
|
|
11
|
+
const normalizedPath = String(filePath ?? '').toLowerCase();
|
|
12
|
+
const extension = Object.keys(IMAGE_MIME_TYPES).find((candidate) => normalizedPath.endsWith(candidate));
|
|
13
|
+
return extension ? IMAGE_MIME_TYPES[extension] : 'application/octet-stream';
|
|
14
|
+
}
|
|
@@ -2,11 +2,17 @@ import { createReadStream } from 'node:fs';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
|
|
5
|
+
import { getVaultFileKind, isImageAttachmentFilePath } from '../../../domain/file-kind.js';
|
|
5
6
|
import { mapWithConcurrency } from '../../shared/async-utils.js';
|
|
6
7
|
import { createEmptyStats } from './responses.js';
|
|
7
8
|
import { splitContentLines } from './parsers.js';
|
|
9
|
+
import { getImageMimeType } from './image-mime.js';
|
|
8
10
|
|
|
9
11
|
async function countFileLines(filePath) {
|
|
12
|
+
if (isImageAttachmentFilePath(filePath)) {
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
return new Promise((resolve, reject) => {
|
|
11
17
|
const stream = createReadStream(filePath, { encoding: 'utf8' });
|
|
12
18
|
let lineCount = 0;
|
|
@@ -36,6 +42,7 @@ function buildSyntheticAddedFileDiff(pathValue, content) {
|
|
|
36
42
|
const normalizedContent = String(content ?? '').replace(/\r\n/g, '\n');
|
|
37
43
|
const file = {
|
|
38
44
|
code: 'U',
|
|
45
|
+
fileKind: getVaultFileKind(pathValue),
|
|
39
46
|
hunks: [],
|
|
40
47
|
isBinary: false,
|
|
41
48
|
oldPath: null,
|
|
@@ -89,7 +96,27 @@ export class GitUntrackedFileService {
|
|
|
89
96
|
async buildSyntheticDiffs(files = []) {
|
|
90
97
|
const results = await mapWithConcurrency(files, 2, async (file) => {
|
|
91
98
|
try {
|
|
92
|
-
const
|
|
99
|
+
const absolutePath = join(this.vaultDir, file.path);
|
|
100
|
+
if (isImageAttachmentFilePath(file.path)) {
|
|
101
|
+
const content = await readFile(absolutePath);
|
|
102
|
+
return {
|
|
103
|
+
...file,
|
|
104
|
+
binaryMessage: 'Binary image file',
|
|
105
|
+
byteLength: content.byteLength,
|
|
106
|
+
code: file.code || 'U',
|
|
107
|
+
fileKind: 'image',
|
|
108
|
+
hunks: [],
|
|
109
|
+
isBinary: true,
|
|
110
|
+
mimeType: getImageMimeType(file.path),
|
|
111
|
+
oldPath: file.oldPath ?? null,
|
|
112
|
+
path: file.path,
|
|
113
|
+
stats: createEmptyStats(),
|
|
114
|
+
status: file.status || 'untracked',
|
|
115
|
+
synthetic: true,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const content = await readFile(absolutePath, 'utf8');
|
|
93
120
|
return buildSyntheticAddedFileDiff(file.path, content);
|
|
94
121
|
} catch {
|
|
95
122
|
return null;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
|
|
1
3
|
import { handleApiError } from './http-request-helpers.js';
|
|
2
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createSafeAsciiFilename,
|
|
6
|
+
encodeContentDispositionFilename,
|
|
7
|
+
jsonResponse,
|
|
8
|
+
sendResponse,
|
|
9
|
+
} from './http-response.js';
|
|
3
10
|
|
|
4
11
|
function resolveDiffScope(requestUrl) {
|
|
5
12
|
const scope = String(requestUrl.searchParams.get('scope') || '').trim().toLowerCase();
|
|
@@ -106,6 +113,29 @@ export function createGitApiQueryHandler({ gitService }) {
|
|
|
106
113
|
return true;
|
|
107
114
|
}
|
|
108
115
|
|
|
116
|
+
if (requestUrl.pathname === '/api/git/file-attachment' && req.method === 'GET') {
|
|
117
|
+
try {
|
|
118
|
+
const attachment = await gitService.getFileAttachment({
|
|
119
|
+
hash: requestUrl.searchParams.get('hash'),
|
|
120
|
+
path: requestUrl.searchParams.get('path'),
|
|
121
|
+
});
|
|
122
|
+
const fileName = basename(String(attachment.path || 'image'));
|
|
123
|
+
sendResponse(req, res, {
|
|
124
|
+
body: attachment.content,
|
|
125
|
+
headers: {
|
|
126
|
+
'Cache-Control': 'private, max-age=300, stale-while-revalidate=3600',
|
|
127
|
+
'Content-Disposition': `inline; filename="${createSafeAsciiFilename(fileName)}"; filename*=UTF-8''${encodeContentDispositionFilename(fileName)}`,
|
|
128
|
+
'Content-Type': attachment.mimeType || 'application/octet-stream',
|
|
129
|
+
'X-Content-Type-Options': 'nosniff',
|
|
130
|
+
},
|
|
131
|
+
statusCode: 200,
|
|
132
|
+
});
|
|
133
|
+
} catch (error) {
|
|
134
|
+
handleApiError(req, res, error, '[api] Failed to read git file attachment:', 'Failed to read git file attachment');
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
109
139
|
if (requestUrl.pathname === '/api/git/pull-backups' && req.method === 'GET') {
|
|
110
140
|
try {
|
|
111
141
|
jsonResponse(req, res, 200, {
|
|
@@ -4,9 +4,14 @@ import { createGitApiQueryHandler } from './create-git-api-query-handler.js';
|
|
|
4
4
|
import { createHostedApiHandler } from './create-hosted-api-handler.js';
|
|
5
5
|
import { createPlantUmlApiHandler } from './create-plantuml-api-handler.js';
|
|
6
6
|
import { createStaticHandler } from './create-static-handler.js';
|
|
7
|
+
import { createStructurizrApiHandler } from './create-structurizr-api-handler.js';
|
|
7
8
|
import { createVaultApiCommandHandler } from './create-vault-api-command-handler.js';
|
|
8
9
|
import { createVaultApiQueryHandler } from './create-vault-api-query-handler.js';
|
|
9
10
|
import { parseJsonBody } from './request-body.js';
|
|
11
|
+
import {
|
|
12
|
+
createRequestUrlWithPathname,
|
|
13
|
+
stripBasePath,
|
|
14
|
+
} from './http-request-helpers.js';
|
|
10
15
|
import {
|
|
11
16
|
applyCorsHeaders,
|
|
12
17
|
jsonResponse,
|
|
@@ -16,28 +21,6 @@ import {
|
|
|
16
21
|
WRITE_METHODS,
|
|
17
22
|
} from './http-response.js';
|
|
18
23
|
|
|
19
|
-
function stripBasePath(pathname, basePath) {
|
|
20
|
-
if (!basePath) {
|
|
21
|
-
return pathname;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (pathname === basePath) {
|
|
25
|
-
return '/';
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (pathname.startsWith(`${basePath}/`)) {
|
|
29
|
-
return pathname.slice(basePath.length) || '/';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return pathname;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function createRequestUrlWithPathname(requestUrl, pathname) {
|
|
36
|
-
const nextUrl = new URL(requestUrl.toString());
|
|
37
|
-
nextUrl.pathname = pathname || '/';
|
|
38
|
-
return nextUrl;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
24
|
export function createRequestHandler(
|
|
42
25
|
config,
|
|
43
26
|
authService,
|
|
@@ -54,6 +37,7 @@ export function createRequestHandler(
|
|
|
54
37
|
fileSystemSyncService = null,
|
|
55
38
|
hostedWorkspaceService = null,
|
|
56
39
|
githubSetupFlow = null,
|
|
40
|
+
structurizrWorkspaceService = null,
|
|
57
41
|
) {
|
|
58
42
|
const handleStaticRequest = createStaticHandler(config, authService, searchService);
|
|
59
43
|
const handleAuthApi = createAuthApiHandler({ authService });
|
|
@@ -78,12 +62,17 @@ export function createRequestHandler(
|
|
|
78
62
|
});
|
|
79
63
|
const handleVaultApiCommand = createVaultApiCommandHandler({
|
|
80
64
|
backlinkIndex,
|
|
65
|
+
maxPdfUploadBytes: config.maxPdfUploadBytes,
|
|
81
66
|
renderDocx,
|
|
82
67
|
roomRegistry,
|
|
83
68
|
vaultFileStore,
|
|
84
69
|
workspaceMutationCoordinator,
|
|
85
70
|
});
|
|
86
71
|
const handlePlantUmlApi = createPlantUmlApiHandler({ plantUmlRenderer });
|
|
72
|
+
const handleStructurizrApi = createStructurizrApiHandler({
|
|
73
|
+
basePath: config.basePath,
|
|
74
|
+
service: structurizrWorkspaceService,
|
|
75
|
+
});
|
|
87
76
|
|
|
88
77
|
function handleBasePathRedirect(req, res, originalRequestUrl) {
|
|
89
78
|
if (
|
|
@@ -193,7 +182,7 @@ export function createRequestHandler(
|
|
|
193
182
|
return;
|
|
194
183
|
}
|
|
195
184
|
|
|
196
|
-
if (requestUrl.pathname.startsWith('/api/')) {
|
|
185
|
+
if (requestUrl.pathname.startsWith('/api/') || handleStructurizrApi.requiresAuthorization(requestUrl)) {
|
|
197
186
|
const authorization = authService.authorizeApiRequest(req);
|
|
198
187
|
if (!authorization.ok) {
|
|
199
188
|
jsonResponse(req, res, authorization.statusCode, authorization.body);
|
|
@@ -209,6 +198,10 @@ export function createRequestHandler(
|
|
|
209
198
|
}
|
|
210
199
|
}
|
|
211
200
|
|
|
201
|
+
if (await handleStructurizrApi(req, res, requestUrl)) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
212
205
|
if (requestUrl.pathname.startsWith('/api/')) {
|
|
213
206
|
if (await handleVaultApiQuery(req, res, requestUrl)) {
|
|
214
207
|
return;
|
|
@@ -7,12 +7,13 @@ const CONTENT_TYPES = {
|
|
|
7
7
|
'.css': 'text/css; charset=utf-8',
|
|
8
8
|
'.html': 'text/html; charset=utf-8',
|
|
9
9
|
'.js': 'text/javascript; charset=utf-8',
|
|
10
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
10
11
|
'.json': 'application/json; charset=utf-8',
|
|
11
12
|
'.svg': 'image/svg+xml',
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
function isVersionedAssetPath(pathname = '') {
|
|
15
|
-
return /-[A-Za-z0-9_-]{8,}\.(?:css|js|woff2?|ttf|svg)$/u.test(pathname);
|
|
16
|
+
return /-[A-Za-z0-9_-]{8,}\.(?:css|js|mjs|woff2?|ttf|svg)$/u.test(pathname);
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
function getStaticCacheControl(pathname = '', extension = '') {
|
|
@@ -33,7 +34,7 @@ function getStaticCacheControl(pathname = '', extension = '') {
|
|
|
33
34
|
return 'public, max-age=31536000, immutable';
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
if (extension === '.css' || extension === '.js') {
|
|
37
|
+
if (extension === '.css' || extension === '.js' || extension === '.mjs') {
|
|
37
38
|
return 'public, max-age=0, must-revalidate';
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -53,6 +54,7 @@ function buildRuntimeConfig({
|
|
|
53
54
|
nodeEnv,
|
|
54
55
|
publicWsBaseUrl,
|
|
55
56
|
search,
|
|
57
|
+
structurizr,
|
|
56
58
|
wikiLinkAutoCreate,
|
|
57
59
|
wsBasePath,
|
|
58
60
|
}) {
|
|
@@ -68,6 +70,7 @@ function buildRuntimeConfig({
|
|
|
68
70
|
},
|
|
69
71
|
publicWsBaseUrl,
|
|
70
72
|
search,
|
|
73
|
+
structurizrEnabled: Boolean(structurizr?.enabled ?? structurizr?.serverUrl),
|
|
71
74
|
wikiLinkAutoCreate,
|
|
72
75
|
wsBasePath,
|
|
73
76
|
})};\n`;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
import { getRequestErrorStatusCode } from './http-errors.js';
|
|
4
|
+
import { jsonResponse, sendResponse, textResponse } from './http-response.js';
|
|
5
|
+
import { parseJsonBody } from './request-body.js';
|
|
6
|
+
|
|
7
|
+
const EMBED_STYLESHEET_PATH = new URL('../structurizr/structurizr-embed.css', import.meta.url);
|
|
8
|
+
|
|
9
|
+
function isSyncRequest(requestUrl, req) {
|
|
10
|
+
return requestUrl.pathname === '/api/structurizr/sync' && req.method === 'POST';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isEmbedStylesheetRequest(requestUrl) {
|
|
14
|
+
return requestUrl.pathname === '/api/structurizr/embed.css';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function rewriteBasePath(body, contentType, basePath, { injectEmbedStylesheet = false } = {}) {
|
|
18
|
+
if (!/(?:text\/(?:html|css)|javascript|ecmascript)/iu.test(contentType)) {
|
|
19
|
+
return body;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let source = body.toString('utf8');
|
|
23
|
+
if (basePath) {
|
|
24
|
+
source = source.replace(
|
|
25
|
+
/(["'`(=])\/(static|workspace|api\/workspace)(?=[/"'`?#)])/gu,
|
|
26
|
+
`$1${basePath}/$2`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (injectEmbedStylesheet && /text\/html/iu.test(contentType)) {
|
|
31
|
+
const normalizedBasePath = String(basePath || '').replace(/\/+$/u, '');
|
|
32
|
+
const stylesheetPath = `${normalizedBasePath}/api/structurizr/embed.css`;
|
|
33
|
+
source = source.replace(
|
|
34
|
+
/<head(?:\s[^>]*)?>/iu,
|
|
35
|
+
(head) => `${head}\n<link rel="stylesheet" href="${stylesheetPath}">`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return Buffer.from(source);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createStructurizrApiHandler({ basePath = '', service = null } = {}) {
|
|
43
|
+
async function handleRequest(req, res, requestUrl) {
|
|
44
|
+
if (isSyncRequest(requestUrl, req)) {
|
|
45
|
+
try {
|
|
46
|
+
const body = await parseJsonBody(req);
|
|
47
|
+
if (!body || typeof body.path !== 'string') {
|
|
48
|
+
jsonResponse(req, res, 400, { error: 'Missing Structurizr workspace path' });
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!service?.isEnabled()) {
|
|
53
|
+
jsonResponse(req, res, 503, { error: 'Structurizr renderer is not configured' });
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
jsonResponse(req, res, 200, await service.sync({
|
|
58
|
+
content: body.source,
|
|
59
|
+
rootPath: body.path,
|
|
60
|
+
}));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const statusCode = getRequestErrorStatusCode(error) || Number(error?.statusCode) || 502;
|
|
63
|
+
if (statusCode >= 500) {
|
|
64
|
+
console.error('[api] Failed to sync Structurizr workspace:', error.message);
|
|
65
|
+
}
|
|
66
|
+
jsonResponse(req, res, statusCode, {
|
|
67
|
+
code: error?.requestCode,
|
|
68
|
+
error: error instanceof Error ? error.message : 'Failed to sync Structurizr workspace',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isEmbedStylesheetRequest(requestUrl)) {
|
|
75
|
+
if (!service?.isEnabled()) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
80
|
+
textResponse(req, res, 405, 'Method Not Allowed', { Allow: 'GET, HEAD' });
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
sendResponse(req, res, {
|
|
86
|
+
body: await readFile(EMBED_STYLESHEET_PATH),
|
|
87
|
+
headers: {
|
|
88
|
+
'Cache-Control': 'no-store',
|
|
89
|
+
'Content-Type': 'text/css; charset=utf-8',
|
|
90
|
+
},
|
|
91
|
+
statusCode: 200,
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('[http] Failed to serve Structurizr embed stylesheet:', error.message);
|
|
95
|
+
textResponse(req, res, 500, 'Internal Server Error');
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!service?.isProxyPath(requestUrl.pathname)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
105
|
+
textResponse(req, res, 405, 'Method Not Allowed', { Allow: 'GET, HEAD' });
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const response = await service.proxy(requestUrl, {
|
|
111
|
+
headers: req.headers,
|
|
112
|
+
method: req.method,
|
|
113
|
+
});
|
|
114
|
+
if (!response) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
sendResponse(req, res, {
|
|
119
|
+
body: rewriteBasePath(response.body, response.contentType, basePath, {
|
|
120
|
+
injectEmbedStylesheet: requestUrl.pathname === '/workspace/1/diagrams',
|
|
121
|
+
}),
|
|
122
|
+
headers: {
|
|
123
|
+
'Cache-Control': requestUrl.pathname.startsWith('/static/')
|
|
124
|
+
? 'public, max-age=31536000, immutable'
|
|
125
|
+
: 'no-store',
|
|
126
|
+
'Content-Type': response.contentType,
|
|
127
|
+
},
|
|
128
|
+
statusCode: response.statusCode,
|
|
129
|
+
});
|
|
130
|
+
} catch (error) {
|
|
131
|
+
const statusCode = getRequestErrorStatusCode(error) || Number(error?.statusCode) || 502;
|
|
132
|
+
console.error('[http] Failed to proxy Structurizr request:', error.message);
|
|
133
|
+
textResponse(req, res, statusCode, error.message || 'Structurizr renderer unavailable');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
handleRequest.requiresAuthorization = (requestUrl) => (
|
|
140
|
+
Boolean(service?.isProxyPath(requestUrl.pathname))
|
|
141
|
+
&& !requestUrl.pathname.startsWith('/api/')
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return handleRequest;
|
|
145
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
|
+
import { isPdfFilePath } from '../../../domain/file-kind.js';
|
|
2
3
|
import { createRequestError } from './http-errors.js';
|
|
3
4
|
import { handleApiError, readRequestId } from './http-request-helpers.js';
|
|
4
5
|
import {
|
|
@@ -7,10 +8,15 @@ import {
|
|
|
7
8
|
jsonResponse,
|
|
8
9
|
sendResponse,
|
|
9
10
|
} from './http-response.js';
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
parseJsonBody,
|
|
13
|
+
readBinaryRequestBody,
|
|
14
|
+
REQUEST_BODY_LIMIT_BYTES,
|
|
15
|
+
} from './request-body.js';
|
|
11
16
|
|
|
12
17
|
const DOCX_EXPORT_REQUEST_LIMIT_BYTES = 33_554_432;
|
|
13
18
|
const DOCX_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
|
19
|
+
const PDF_SIGNATURE = Buffer.from('%PDF-');
|
|
14
20
|
|
|
15
21
|
function createDocxDownloadHeaders(filePath) {
|
|
16
22
|
const fileName = basename(String(filePath ?? 'document')).replace(/\.[^.]+$/u, '') || 'document';
|
|
@@ -23,7 +29,7 @@ function createDocxDownloadHeaders(filePath) {
|
|
|
23
29
|
};
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
function decodeHeaderMetadata(value) {
|
|
32
|
+
function decodeHeaderMetadata(value, label = 'attachment') {
|
|
27
33
|
const normalized = String(value ?? '').trim();
|
|
28
34
|
if (!normalized) {
|
|
29
35
|
return '';
|
|
@@ -32,7 +38,7 @@ function decodeHeaderMetadata(value) {
|
|
|
32
38
|
try {
|
|
33
39
|
return decodeURIComponent(normalized);
|
|
34
40
|
} catch {
|
|
35
|
-
throw createRequestError(400,
|
|
41
|
+
throw createRequestError(400, `Invalid ${label} metadata header encoding`);
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
44
|
|
|
@@ -129,6 +135,55 @@ async function handleUploadAttachment({ workspaceMutationCoordinator }, req, res
|
|
|
129
135
|
return true;
|
|
130
136
|
}
|
|
131
137
|
|
|
138
|
+
async function handleUploadFile({ maxPdfUploadBytes, workspaceMutationCoordinator }, req, res) {
|
|
139
|
+
try {
|
|
140
|
+
const filePath = decodeHeaderMetadata(req.headers['x-collabmd-file-path'], 'file upload');
|
|
141
|
+
if (!filePath) {
|
|
142
|
+
jsonResponse(req, res, 400, { error: 'Missing file path' });
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (typeof workspaceMutationCoordinator?.createFile !== 'function') {
|
|
147
|
+
jsonResponse(req, res, 503, { error: 'File upload is unavailable' });
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const isPdfUpload = isPdfFilePath(filePath);
|
|
152
|
+
if (isPdfUpload) {
|
|
153
|
+
const contentType = String(req.headers['content-type'] || '')
|
|
154
|
+
.split(';', 1)[0]
|
|
155
|
+
.trim()
|
|
156
|
+
.toLowerCase();
|
|
157
|
+
if (contentType !== 'application/pdf') {
|
|
158
|
+
jsonResponse(req, res, 400, { error: 'PDF uploads must use application/pdf' });
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const maxUploadBytes = isPdfUpload ? maxPdfUploadBytes : REQUEST_BODY_LIMIT_BYTES;
|
|
164
|
+
const content = await readBinaryRequestBody(req, maxUploadBytes);
|
|
165
|
+
if (isPdfUpload && !content.subarray(0, PDF_SIGNATURE.length).equals(PDF_SIGNATURE)) {
|
|
166
|
+
jsonResponse(req, res, 400, { error: 'Invalid PDF file' });
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const result = await workspaceMutationCoordinator.createFile({
|
|
171
|
+
content,
|
|
172
|
+
path: filePath,
|
|
173
|
+
requestId: readRequestId(req),
|
|
174
|
+
});
|
|
175
|
+
if (!result.ok) {
|
|
176
|
+
jsonResponse(req, res, result.error === 'File already exists' ? 409 : 400, { error: result.error });
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
jsonResponse(req, res, 201, { ok: true, path: filePath });
|
|
181
|
+
} catch (error) {
|
|
182
|
+
handleApiError(req, res, error, '[api] Failed to upload file:', 'Failed to upload file');
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
132
187
|
async function handleCreateFile({ workspaceMutationCoordinator }, req, res) {
|
|
133
188
|
try {
|
|
134
189
|
const body = await parseJsonBody(req);
|
|
@@ -280,6 +335,7 @@ const ROUTE_TABLE = [
|
|
|
280
335
|
{ method: 'POST', path: '/api/export/docx', handler: handleExportDocx },
|
|
281
336
|
{ method: 'PUT', path: '/api/file', handler: handleWriteFile },
|
|
282
337
|
{ method: 'POST', path: '/api/attachments', handler: handleUploadAttachment },
|
|
338
|
+
{ method: 'POST', path: '/api/file/upload', handler: handleUploadFile },
|
|
283
339
|
{ method: 'POST', path: '/api/file', handler: handleCreateFile },
|
|
284
340
|
{ method: 'DELETE', path: '/api/file', handler: handleDeleteFile },
|
|
285
341
|
{ method: 'PATCH', path: '/api/file', handler: handleRenameFile },
|
|
@@ -289,11 +345,17 @@ const ROUTE_TABLE = [
|
|
|
289
345
|
];
|
|
290
346
|
|
|
291
347
|
export function createVaultApiCommandHandler({
|
|
348
|
+
maxPdfUploadBytes = REQUEST_BODY_LIMIT_BYTES,
|
|
292
349
|
renderDocx = null,
|
|
293
350
|
vaultFileStore,
|
|
294
351
|
workspaceMutationCoordinator = null,
|
|
295
352
|
}) {
|
|
296
|
-
const context = {
|
|
353
|
+
const context = {
|
|
354
|
+
maxPdfUploadBytes,
|
|
355
|
+
renderDocx,
|
|
356
|
+
vaultFileStore,
|
|
357
|
+
workspaceMutationCoordinator,
|
|
358
|
+
};
|
|
297
359
|
|
|
298
360
|
return async function handleVaultApiCommand(req, res, requestUrl) {
|
|
299
361
|
for (const route of ROUTE_TABLE) {
|