@tequila99/githuman 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -3
- package/dist/server/app.js +1 -0
- package/dist/server/db/migrations.js +6 -0
- package/dist/server/errors/note-assets.js +3 -0
- package/dist/server/repositories/notes-catalog.repo.js +4 -3
- package/dist/server/repositories/notes.repo.js +69 -27
- package/dist/server/routes/agent.js +1 -0
- package/dist/server/routes/notes.js +48 -2
- package/dist/server/services/agent/context-builder.js +13 -0
- package/dist/server/services/notes-external-assets.service.js +177 -0
- package/dist/server/services/notes-identity.service.js +112 -27
- package/dist/server/services/notes-operations.service.js +3 -5
- package/dist/server/services/notes.service.js +29 -4
- package/dist/server/utils/note-asset-format.js +1 -1
- package/dist/server/utils/remote-asset-request.js +193 -0
- package/dist/server/utils/request-abort.js +20 -0
- package/dist/shared/agents/schemas.js +14 -0
- package/dist/shared/notes/asset-formats.js +19 -1
- package/dist/shared/notes/constants.js +5 -0
- package/dist/shared/notes/external-assets.js +153 -0
- package/dist/shared/notes/note-path.js +17 -0
- package/dist/shared/notes/reference-path.js +2 -1
- package/dist/shared/notes/references.js +1 -1
- package/dist/shared/notes/schemas.js +60 -2
- package/dist/shared/utils/web-url.js +15 -0
- package/dist/web/assets/AppPage-DdmqiV2R.js +1 -0
- package/dist/web/assets/{ClosePopup-D2_HzWXW.js → ClosePopup-lpH_1Ju2.js} +1 -1
- package/dist/web/assets/{CreateButton-pmDRGwna.js → CreateButton-Dnpwobd9.js} +2 -2
- package/dist/web/assets/{CreateReviewFabButton-CcXWnmSN.js → CreateReviewFabButton-KvKPEJzI.js} +1 -1
- package/dist/web/assets/IndexPage-DE-IkAhK.js +1 -0
- package/dist/web/assets/IndexPage-F8MMAAkn.css +1 -0
- package/dist/web/assets/{MainLayout-DOGoQ-1m.js → MainLayout-wWA8ckBg.js} +4 -4
- package/dist/web/assets/NoteVisualEditor-Bj13_oEl.js +1054 -0
- package/dist/web/assets/{NoteVisualEditor-CClIKW0X.css → NoteVisualEditor-IH3s3d3W.css} +1 -1
- package/dist/web/assets/NotesPage-FidBg83M.js +4 -0
- package/dist/web/assets/NotesPage-KaCEnfdz.css +1 -0
- package/dist/web/assets/PanelResize-C51WwwLD.css +1 -0
- package/dist/web/assets/PanelResize-DY0Nzmql.js +1 -0
- package/dist/web/assets/{PdfPreview-CdgeqqEv.js → PdfPreview-C7HomxEg.js} +1 -1
- package/dist/web/assets/{QFab-DyUFgj0W.js → QFab-C-YZWgmW.js} +1 -1
- package/dist/web/assets/{QForm-BiRkV9BY.js → QForm-DpEhYVCa.js} +1 -1
- package/dist/web/assets/QResizeObserver-DeRFR_WF.js +1 -0
- package/dist/web/assets/{QScrollArea-DfPW35JQ.js → QScrollArea-BSSZoaV5.js} +1 -1
- package/dist/web/assets/{QSelect-BpZ5KfWP.js → QSelect-2iOBkp1K.js} +1 -1
- package/dist/web/assets/QSpace-B5NneHAd.js +1 -0
- package/dist/web/assets/QVirtualScroll-CZJMBpmR.js +1 -0
- package/dist/web/assets/ReviewDetailPage-BI1iAhSf.js +1 -0
- package/dist/web/assets/{ReviewsPage-BaxUZzUZ.js → ReviewsPage-RyST0FlH.js} +1 -1
- package/dist/web/assets/StatusSwitcher-DAeNzNP7.js +1 -0
- package/dist/web/assets/{TerminalView-DMJTXE-G.js → TerminalView-BAzJgUTq.js} +2 -2
- package/dist/web/assets/{abnfDiagram-O67JEVCF-CiOkMSRz.js → abnfDiagram-O67JEVCF-BNZQ7duT.js} +1 -1
- package/dist/web/assets/agent-store-DP6Vrw6L.js +113 -0
- package/dist/web/assets/app-info-store-u6ZVFjv5.js +1 -0
- package/dist/web/assets/architecture-7GRP2DOG-BSdnjp9Z.js +1 -0
- package/dist/web/assets/{architectureDiagram-NJMV4G6O-BexHJffj.js → architectureDiagram-NJMV4G6O-Bkj0kfFn.js} +1 -1
- package/dist/web/assets/{blockDiagram-BEXU5L5S-CEyB_5a1.js → blockDiagram-BEXU5L5S-CXO3zN2c.js} +1 -1
- package/dist/web/assets/{c4Diagram-YGBWAQC7-BRNWA7xI.js → c4Diagram-YGBWAQC7-BHfMeZNf.js} +1 -1
- package/dist/web/assets/channel-DmoOM6AB.js +1 -0
- package/dist/web/assets/{chunk-3FUC2YCW-BcQR1JDF.js → chunk-3FUC2YCW-z1WbH7Gi.js} +1 -1
- package/dist/web/assets/{chunk-5DYCD2WN-D6aQ1hFt.js → chunk-5DYCD2WN-K9HPch97.js} +1 -1
- package/dist/web/assets/{chunk-742MDFTN-BRgrFtkf.js → chunk-742MDFTN-Bhjp9fC-.js} +1 -1
- package/dist/web/assets/{chunk-7INBJB4K-3RBjXeq6.js → chunk-7INBJB4K-Do0EmU14.js} +1 -1
- package/dist/web/assets/{chunk-7M6MHVWA-Malugc_N.js → chunk-7M6MHVWA-veeEjsOh.js} +1 -1
- package/dist/web/assets/{chunk-7PRAP22T-Bb3gaISA.js → chunk-7PRAP22T-CPqB5tCK.js} +1 -1
- package/dist/web/assets/{chunk-GTNCS2PH-Cps7XhUC.js → chunk-GTNCS2PH-Civ1V52P.js} +1 -1
- package/dist/web/assets/{chunk-GWA4HPMP-Bfp2Tg3_.js → chunk-GWA4HPMP-C7LzAlyZ.js} +1 -1
- package/dist/web/assets/{chunk-MBY4JIJT-IfXbX1Ur.js → chunk-MBY4JIJT-DfmJy0o_.js} +1 -1
- package/dist/web/assets/{chunk-NETBCI7D-r2fmahwk.js → chunk-NETBCI7D-BZIKmMvL.js} +1 -1
- package/dist/web/assets/{chunk-O7XYJQB3-sadsiWmA.js → chunk-O7XYJQB3-DcHeu6b7.js} +1 -1
- package/dist/web/assets/{chunk-UA2S7LBM-DibfwtDn.js → chunk-UA2S7LBM-0SR2og6x.js} +1 -1
- package/dist/web/assets/{chunk-WEXAMYUT-DBouMgEO.js → chunk-WEXAMYUT-CJMSoM-Z.js} +1 -1
- package/dist/web/assets/{chunk-Z7XXMR3K-fAHToON5.js → chunk-Z7XXMR3K-q6fce66x.js} +1 -1
- package/dist/web/assets/{chunk-ZIGJFQKS-CJG1UIHO.js → chunk-ZIGJFQKS-DsPzen4Y.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-NBCMYWYE-CPHP5ynj.js → classDiagram-v2-NBCMYWYE-Cpm_bvzb.js} +1 -1
- package/dist/web/assets/{cynefin-OW5HDTMX-BU7_LYGR.js → cynefin-OW5HDTMX-ZTr5XZMW.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-VND7K2PF-BXFEy9Io.js → cynefinDiagram-VND7K2PF-ByUmUx58.js} +1 -1
- package/dist/web/assets/{dagre-6A5THRUB-aKpdpQ18.js → dagre-6A5THRUB-mQOy0E3r.js} +1 -1
- package/dist/web/assets/{diagram-22UHCM2B-ddbRgIQ9.js → diagram-22UHCM2B-BW0LPtyk.js} +1 -1
- package/dist/web/assets/{diagram-3UASUU5V-vNYy6WP_.js → diagram-3UASUU5V-jo148ec5.js} +1 -1
- package/dist/web/assets/{diagram-ATOU4E4O-B9Z3zFfp.js → diagram-ATOU4E4O-CJt4F0i4.js} +1 -1
- package/dist/web/assets/{diagram-CDSNMT55-D3TaDPXU.js → diagram-CDSNMT55-DEShJv56.js} +1 -1
- package/dist/web/assets/{diagram-MLGK6HIB-DOXcXm2Y.js → diagram-MLGK6HIB-DOFOADfs.js} +1 -1
- package/dist/web/assets/{diagram-MPIPVDR6-B5E2J8pO.js → diagram-MPIPVDR6-ux537BMl.js} +1 -1
- package/dist/web/assets/{dist-DSPzgfNo.js → dist-7xWvOKEv.js} +1 -1
- package/dist/web/assets/{dist-C9cJQvJH.js → dist-BThMRW5U.js} +1 -1
- package/dist/web/assets/{dist-BXUopzfH.js → dist-BWa3ecCV.js} +1 -1
- package/dist/web/assets/{dist-BX8UA0fl.js → dist-BbdrgL5V.js} +1 -1
- package/dist/web/assets/{dist-Kd0W7Y_b.js → dist-Bo7anpPj.js} +1 -1
- package/dist/web/assets/{dist-BFbA030x.js → dist-Bu9yRqTU.js} +1 -1
- package/dist/web/assets/{dist-Cxd9qKh7.js → dist-Bvnostf5.js} +1 -1
- package/dist/web/assets/{dist-C9STut2J.js → dist-CK1va5C_.js} +1 -1
- package/dist/web/assets/{dist-BGzvKGEu.js → dist-CLv-grWO.js} +1 -1
- package/dist/web/assets/{dist-CRoJr3t6.js → dist-D1EUrS6y.js} +1 -1
- package/dist/web/assets/{dist-PDsGXJi3.js → dist-DL0dBIkt.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-ZINNZB2B-D-OCB_ud.js → ebnfDiagram-ZINNZB2B-DEybuZle.js} +1 -1
- package/dist/web/assets/{elk-276RUBZZ-CzjP8RHy.js → elk-276RUBZZ-CZXZRpGb.js} +1 -1
- package/dist/web/assets/{erDiagram-OPXOYQCR-Clohps7p.js → erDiagram-OPXOYQCR-BgRqGAt5.js} +1 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-ZuRq6Sae.js +1 -0
- package/dist/web/assets/flowDiagram-KWPJA3E3-DRyHAdLb.js +1 -0
- package/dist/web/assets/{ganttDiagram-FUAMR5RP-ZWH4HYq5.js → ganttDiagram-FUAMR5RP-CBJwX1gk.js} +1 -1
- package/dist/web/assets/{gitGraph-4MIJSDKK-C3AfRnC8.js → gitGraph-4MIJSDKK-Dw-XA3G1.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-X574FWY7-BobkQARX.js → gitGraphDiagram-X574FWY7-BpGccoWp.js} +1 -1
- package/dist/web/assets/i18n-sfJvmpVb.js +1 -0
- package/dist/web/assets/index-B2CzafCr.js +2 -0
- package/dist/web/assets/{info-A6RAGUB7-Dh_SAe09.js → info-A6RAGUB7-Dj5RfJsg.js} +1 -1
- package/dist/web/assets/{infoDiagram-VRGFBTTK-izDlhNTw.js → infoDiagram-VRGFBTTK-DyVSH991.js} +1 -1
- package/dist/web/assets/{init-app-C1a6BWWN.js → init-app-DSS44aRI.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-BJ89XzVJ.js → ishikawaDiagram-OU5B5YK6-Cu0n2oqT.js} +1 -1
- package/dist/web/assets/{journeyDiagram-ZHPQQLJL-poV-f08K.js → journeyDiagram-ZHPQQLJL-Bz1Ot6Zg.js} +1 -1
- package/dist/web/assets/{kanban-definition-PNTS6WVX-Cn3FEnH7.js → kanban-definition-PNTS6WVX-C42vuY7H.js} +1 -1
- package/dist/web/assets/{line-bE9Mqcqy.js → line-C5WKurbS.js} +1 -1
- package/dist/web/assets/markdown-DBTEhbhQ.js +15 -0
- package/dist/web/assets/{mermaid-parser.core-Cua7H5pF.js → mermaid-parser.core-B7l8qh8B.js} +3 -3
- package/dist/web/assets/{mermaid.core-BMPOA8T-.js → mermaid.core-Drs2mjGg.js} +4 -4
- package/dist/web/assets/{mindmap-definition-NLK3R4M7-DaTGr8Fs.js → mindmap-definition-NLK3R4M7-BV1FNwCn.js} +1 -1
- package/dist/web/assets/{packet-AYTQ26CC-DmLfghd9.js → packet-AYTQ26CC-BPmUY1Ig.js} +1 -1
- package/dist/web/assets/{pegDiagram-GJSIUBJH-CTlxXcbf.js → pegDiagram-GJSIUBJH-D6NPxPdr.js} +1 -1
- package/dist/web/assets/{pie-WAS4IAKB-52oNsEtc.js → pie-WAS4IAKB-CTp2jNAc.js} +1 -1
- package/dist/web/assets/{pieDiagram-5QR66LMP-CbJtB1te.js → pieDiagram-5QR66LMP-BVkABrg7.js} +1 -1
- package/dist/web/assets/pinia-BVWaJ8NX.js +1 -0
- package/dist/web/assets/{quadrantDiagram-O4NWA36T-DCg-uD08.js → quadrantDiagram-O4NWA36T-LZCjVd08.js} +1 -1
- package/dist/web/assets/{radar-RG4KPBEZ-9uA3AOZR.js → radar-RG4KPBEZ-CCiUrmsg.js} +1 -1
- package/dist/web/assets/{railroad-74A4TZTK-BPxQfpxv.js → railroad-74A4TZTK-CZn8VJbK.js} +1 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-B-9tG9pW.js +1 -0
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-D7lUyGOq.js +1 -0
- package/dist/web/assets/railroad-peg-WCYAUIDC-ja94t32K.js +1 -0
- package/dist/web/assets/{railroadDiagram-XR7U4H2S-CwBNr6fr.js → railroadDiagram-XR7U4H2S-nopy-mdw.js} +1 -1
- package/dist/web/assets/{requirementDiagram-PLB6GJNP-BbtZt0St.js → requirementDiagram-PLB6GJNP-BOeMzf-x.js} +1 -1
- package/dist/web/assets/{safe-storage-Bn-sHAYw.js → safe-storage-aRoyeFxv.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-IPEJSGJF-DRWdMnFl.js → sankeyDiagram-IPEJSGJF-Bbk0AQUe.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-PO4LG4MO-C4tWeWpt.js → sequenceDiagram-PO4LG4MO-ZpubjKFc.js} +1 -1
- package/dist/web/assets/{stateDiagram-v2-GCMORJYK-pE0T1U5E.js → stateDiagram-v2-GCMORJYK-DNjDh9yZ.js} +1 -1
- package/dist/web/assets/{swimlanes-2SLR337P-DKky6E3p.js → swimlanes-2SLR337P-C-_UzZF_.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-CVSZNvS2.js +8 -0
- package/dist/web/assets/theme-CeQhGDjh.js +1 -0
- package/dist/web/assets/{timeline-definition-EJHVYXUP-CsRzcMLr.js → timeline-definition-EJHVYXUP-BiaJyHdJ.js} +1 -1
- package/dist/web/assets/{treeView-Q6P3EWNA-CQYqoPG3.js → treeView-Q6P3EWNA-CENjlerX.js} +1 -1
- package/dist/web/assets/{treemap-WGGIJYW6-BzFw4gFs.js → treemap-WGGIJYW6-CBpdf3L2.js} +1 -1
- package/dist/web/assets/use-add-agent-context-SApxn9fW.js +1 -0
- package/dist/web/assets/{use-app-theme-9qFZUBqz.js → use-app-theme-DP8Ymwwb.js} +1 -1
- package/dist/web/assets/use-panel-width-CTXD_csP.js +1 -0
- package/dist/web/assets/use-server-events-C13Sw-rU.js +1 -0
- package/dist/web/assets/{usecaseDiagram-POWQR4AR-IMfiBw6h.js → usecaseDiagram-POWQR4AR-CcJ83_Ve.js} +1 -1
- package/dist/web/assets/{vennDiagram-UO4OBE2U-JKdzpNy2.js → vennDiagram-UO4OBE2U-DBjzpbFr.js} +1 -1
- package/dist/web/assets/{wardley-WFR3VGLG-B9Yiwlp8.js → wardley-WFR3VGLG-DmlbUWUM.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-VNRHLVJA-BVoc2hGS.js → wardleyDiagram-VNRHLVJA-CWSZCqf4.js} +1 -1
- package/dist/web/assets/window-store-DNnKUhxs.js +1 -0
- package/dist/web/assets/{xychartDiagram-PMCCYNJV-B_XV1aim.js → xychartDiagram-PMCCYNJV-CMnNSaZv.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +3 -1
- package/dist/web/assets/AppPage-cl-3VfAl.js +0 -1
- package/dist/web/assets/IndexPage-CYFIGWy9.css +0 -1
- package/dist/web/assets/IndexPage-CmBmgu4A.js +0 -1
- package/dist/web/assets/NoteVisualEditor-zjZr9aod.js +0 -1056
- package/dist/web/assets/NotesPage-C5VXA9Bb.css +0 -1
- package/dist/web/assets/NotesPage-Jeva3Jta.js +0 -114
- package/dist/web/assets/QSpace-BRLIXkHB.js +0 -1
- package/dist/web/assets/ReviewDetailPage-CpJPrSKO.js +0 -1
- package/dist/web/assets/StatusSwitcher-D5UG_YWP.js +0 -1
- package/dist/web/assets/agent-icon-Dx8WBYX_.js +0 -1
- package/dist/web/assets/agent-store-B1VK6S0D.js +0 -1
- package/dist/web/assets/app-info-store-D76h_OdJ.js +0 -1
- package/dist/web/assets/architecture-7GRP2DOG-Byc4Oz95.js +0 -1
- package/dist/web/assets/channel-DbRAFIx1.js +0 -1
- package/dist/web/assets/equal-arrays-GA_7ZCAb.js +0 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-FWK4tnN_.js +0 -1
- package/dist/web/assets/flowDiagram-KWPJA3E3-C10B5_6J.js +0 -1
- package/dist/web/assets/i18n-CC_rvfan.js +0 -1
- package/dist/web/assets/index-Ic8yFbJg.js +0 -2
- package/dist/web/assets/markdown-D85iiL_9.js +0 -15
- package/dist/web/assets/pinia-BlCzIRX-.js +0 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-CPVHNg05.js +0 -1
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-DAjTaOUg.js +0 -1
- package/dist/web/assets/railroad-peg-WCYAUIDC-DkxyKsj4.js +0 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-CPhibEqM.js +0 -8
- package/dist/web/assets/theme-B-g7wQEs.js +0 -1
- package/dist/web/assets/use-panel-width-Dyh49bod.js +0 -1
- package/dist/web/assets/use-server-events-24_soFuj.js +0 -1
package/README.md
CHANGED
|
@@ -210,12 +210,25 @@ MIT, см. [LICENSE](LICENSE).
|
|
|
210
210
|
каталога запуска приложения. Поддерживаются папки, визуальный редактор, полный
|
|
211
211
|
исходник Markdown, редактируемые свойства YAML frontmatter и обычные относительные ссылки.
|
|
212
212
|
Внутренние ссылки активируют заметку в текущем окне; внешние открываются в новой вкладке.
|
|
213
|
+
**Add to agent** в контекстном меню заметки и панели редактора добавляет её в чат
|
|
214
|
+
как удаляемый бейдж. При отправке агент получает актуальный Markdown вместе
|
|
215
|
+
с YAML-свойствами; связанные заметки и изображения не включаются.
|
|
216
|
+
Несохранённые изменения сохраняются перед отправкой, ошибки сохранения её блокируют.
|
|
213
217
|
Кнопка **Link to note** помогает выбрать заметку для ссылки; теги со счётчиками
|
|
214
218
|
в дереве фильтруют список. Изменения автоматически сохраняются после паузы ввода.
|
|
215
219
|
|
|
216
220
|
Другой каталог задаётся через `serve --notes-dir ./notes` или
|
|
217
221
|
`GITHUMAN_NOTES_DIR`; относительный путь считается от каталога запуска.
|
|
218
|
-
Такой каталог можно версионировать.
|
|
222
|
+
Такой каталог можно версионировать.
|
|
223
|
+
При создании вводится короткое имя без пути: файл создаётся в выбранном каталоге,
|
|
224
|
+
расширение `.md` добавляется автоматически. Заметки можно перетаскивать между
|
|
225
|
+
каталогами в дереве слева и в обзоре каталога справа, в том числе между панелями.
|
|
226
|
+
Кнопка корня слева и кнопка родительского каталога справа также принимают заметки.
|
|
227
|
+
Перенос сохраняет UUID и обновляет относительные ссылки; совпадающий файл не перезаписывается.
|
|
228
|
+
**Delete** в контекстном меню скрывает заметку в приложении: отметка хранится в БД,
|
|
229
|
+
сам Markdown остаётся на диске. Сканирование и перезапуск не возвращают её в список;
|
|
230
|
+
восстановление через интерфейс пока не предусмотрено.
|
|
231
|
+
Личное хранилище по умолчанию
|
|
219
232
|
исключается локально через Git `info/exclude`; уже отслеживаемые файлы
|
|
220
233
|
автоматически из индекса не удаляются.
|
|
221
234
|
|
|
@@ -226,16 +239,39 @@ MIT, см. [LICENSE](LICENSE).
|
|
|
226
239
|
в дереве. При альтернативном Git-хранилище исключите `.history/` из Git,
|
|
227
240
|
если резервные копии не нужно публиковать.
|
|
228
241
|
|
|
242
|
+
Полный Markdown с YAML-шапкой можно вставить в пустое тело визуального редактора
|
|
243
|
+
или вместо всего выделенного тела: свойства импортируются в Properties, UUID
|
|
244
|
+
заметки сохраняется. Некорректный YAML отклоняется с сообщением об ошибке.
|
|
245
|
+
При неподдерживаемой разметке сохраняется полный исходник
|
|
246
|
+
и включается режим Markdown.
|
|
247
|
+
|
|
229
248
|
Визуальное редактирование может нормализовать оформление списков, таблиц
|
|
230
249
|
и reference-ссылок. Открытие и переключение режимов без правок файл не
|
|
231
250
|
перезаписывают. Если при открытии визуальный редактор теряет конструкции,
|
|
232
251
|
приложение сохраняет исходный текст и переключается в Markdown. Вставленные PNG, JPEG, GIF и WebP (до 10 МБ) сохраняются в `assets/` внутри
|
|
233
252
|
хранилища заметок и доступны в визуальном редакторе после перезапуска. Ссылки
|
|
234
|
-
на вложения не зависят от хоста или порта сервиса. Внешние
|
|
235
|
-
остаются в исходном режиме. Размер заметки ограничен 1 МБ; символические ссылки в
|
|
253
|
+
на вложения не зависят от хоста или порта сервиса. Внешние HTTP/HTTPS-изображения доступны в визуальном редакторе. Размер заметки ограничен 1 МБ; символические ссылки в
|
|
236
254
|
пути хранилища не поддерживаются. Черновик после аварии браузера пока
|
|
237
255
|
не восстанавливается.
|
|
238
256
|
|
|
257
|
+
Команда **Download external attachments** в панели заметки или **Ctrl+Shift+D**, пока заметка открыта на экране, независимо от фокуса,
|
|
258
|
+
открывает список внешних вложений.
|
|
259
|
+
Клавиша определяется по физическому коду, независимо от раскладки.
|
|
260
|
+
Картинки и ссылки на PNG/JPEG/GIF/WebP/PDF можно выбрать перед загрузкой;
|
|
261
|
+
одинаковые адреса скачиваются один раз. Размер картинок ограничен 10 МБ,
|
|
262
|
+
PDF до 50 МБ загружаются без дополнительного вопроса, от 50 до 100 МБ требуют
|
|
263
|
+
подтверждения, свыше 100 МБ отклоняются. При неизвестном размере подтверждение
|
|
264
|
+
запрашивается после остановки на пороге 50 МБ; согласие запускает загрузку заново.
|
|
265
|
+
Лимит одной операции — 200 МБ сохранённых файлов. Проверка размера действует
|
|
266
|
+
и во время скачивания, не только по заголовкам сервера.
|
|
267
|
+
|
|
268
|
+
Успешные загрузки заменяют адреса в текущем Markdown, ошибки оставляют внешние
|
|
269
|
+
ссылки. PDF остаются обычными ссылками, свойства и подписи сохраняются.
|
|
270
|
+
Скачивание работает с публичными HTTP/HTTPS-адресами без браузерных cookies;
|
|
271
|
+
обычные ссылки определяются по расширению в пути URL. **Restore external links**
|
|
272
|
+
возвращает последнюю замену, пока текст не изменён. Обновление визуального
|
|
273
|
+
редактора после импорта сбрасывает его прежнюю историю Ctrl+Z.
|
|
274
|
+
|
|
239
275
|
Заметки имеют UUID в SQLite-каталоге; необязательный `githuman_id` в YAML помогает
|
|
240
276
|
распознавать внешние перемещения. Back/Forward и история браузера возвращают между
|
|
241
277
|
заметками. Rename в контекстном меню сохраняет ID и исправляет относительные ссылки;
|
package/dist/server/app.js
CHANGED
|
@@ -88,6 +88,12 @@ export function applyMigrations(db) {
|
|
|
88
88
|
CREATE INDEX IF NOT EXISTS review_files_review_id_idx
|
|
89
89
|
ON review_files(review_id);
|
|
90
90
|
`);
|
|
91
|
+
const noteColumns = db
|
|
92
|
+
.prepare('PRAGMA table_info(note_catalog)')
|
|
93
|
+
.all();
|
|
94
|
+
if (!noteColumns.some(column => column.name === 'deleted_at')) {
|
|
95
|
+
db.exec('ALTER TABLE note_catalog ADD COLUMN deleted_at TEXT');
|
|
96
|
+
}
|
|
91
97
|
const commentColumns = db
|
|
92
98
|
.prepare('PRAGMA table_info(comments)')
|
|
93
99
|
.all();
|
|
@@ -7,6 +7,7 @@ function toNote(row) {
|
|
|
7
7
|
version: row.version,
|
|
8
8
|
createdAt: row.created_at,
|
|
9
9
|
updatedAt: row.updated_at,
|
|
10
|
+
deletedAt: row.deleted_at,
|
|
10
11
|
redirectId: row.redirect_id
|
|
11
12
|
};
|
|
12
13
|
}
|
|
@@ -48,9 +49,9 @@ export class NotesCatalogRepository {
|
|
|
48
49
|
}
|
|
49
50
|
put(note) {
|
|
50
51
|
this.db
|
|
51
|
-
.prepare(`INSERT INTO note_catalog(storage_id,id,path,presence,version,created_at,updated_at,path_changed_at,redirect_id) VALUES(
|
|
52
|
-
ON CONFLICT(storage_id,id) DO UPDATE SET path=excluded.path,presence=excluded.presence,version=excluded.version,updated_at=excluded.updated_at,path_changed_at=CASE WHEN note_catalog.path != excluded.path THEN strftime('%Y-%m-%dT%H:%M:%fZ','now') ELSE note_catalog.path_changed_at END,redirect_id=excluded.redirect_id`)
|
|
53
|
-
.run(this.storageId, note.id, note.path, note.presence, note.version, note.createdAt, note.updatedAt, note.updatedAt, note.redirectId);
|
|
52
|
+
.prepare(`INSERT INTO note_catalog(storage_id,id,path,presence,version,created_at,updated_at,path_changed_at,redirect_id,deleted_at) VALUES(?,?,?,?,?,?,?,?,?,?)
|
|
53
|
+
ON CONFLICT(storage_id,id) DO UPDATE SET path=excluded.path,presence=excluded.presence,version=excluded.version,updated_at=excluded.updated_at,path_changed_at=CASE WHEN note_catalog.path != excluded.path THEN strftime('%Y-%m-%dT%H:%M:%fZ','now') ELSE note_catalog.path_changed_at END,redirect_id=excluded.redirect_id,deleted_at=excluded.deleted_at`)
|
|
54
|
+
.run(this.storageId, note.id, note.path, note.presence, note.version, note.createdAt, note.updatedAt, note.updatedAt, note.redirectId, note.deletedAt);
|
|
54
55
|
}
|
|
55
56
|
links(id) {
|
|
56
57
|
const rows = this.db
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { noteChildPath, noteParentPath } from "../../shared/notes/note-path.js";
|
|
1
2
|
import { constants } from 'node:fs';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
4
|
import { lstat, link, mkdir, open, readdir, realpath, rename, unlink, writeFile } from 'node:fs/promises';
|
|
4
5
|
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
5
6
|
import { BadRequestError, NotFoundError } from "../errors/http.js";
|
|
6
7
|
import { isSystemError } from "../utils/system-error.js";
|
|
7
8
|
import { sha256Hex } from "../utils/hash.js";
|
|
8
9
|
import { NoteConflictError, NoteExistsError } from "../errors/notes.js";
|
|
9
|
-
import {
|
|
10
|
+
import { noteAssetFormat } from "../../shared/notes/asset-formats.js";
|
|
10
11
|
// Bound file reads before allocating document buffers.
|
|
11
12
|
const MAX_BYTES = 1000000;
|
|
12
13
|
export class NotesRepository {
|
|
@@ -213,36 +214,72 @@ export class NotesRepository {
|
|
|
213
214
|
});
|
|
214
215
|
}
|
|
215
216
|
async saveAsset(bytes, extension) {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
217
|
+
const staged = await this.stageAsset((async function* () {
|
|
218
|
+
yield bytes;
|
|
219
|
+
})(), extension, new AbortController().signal);
|
|
220
|
+
try {
|
|
221
|
+
return await staged.commit();
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
await staged.dispose();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async stageAsset(chunks, extension, signal) {
|
|
228
|
+
const format = noteAssetFormat(extension);
|
|
229
|
+
if (!format)
|
|
230
|
+
throw new BadRequestError('Unsupported attachment format');
|
|
231
|
+
const probe = await this.path('assets/probe.png', false);
|
|
232
|
+
await mkdir(dirname(probe), { recursive: true });
|
|
233
|
+
await this.path('assets/probe.png', false);
|
|
234
|
+
const temporary = join(dirname(probe), '.asset-' + randomUUID());
|
|
235
|
+
const dispose = async () => {
|
|
236
|
+
await unlink(temporary).catch(error => {
|
|
237
|
+
if (!isSystemError(error, 'ENOENT'))
|
|
238
|
+
throw error;
|
|
239
|
+
});
|
|
240
|
+
};
|
|
221
241
|
try {
|
|
222
242
|
const file = await open(temporary, 'wx', 0o600);
|
|
243
|
+
const hash = createHash('sha256');
|
|
244
|
+
let bytes = 0;
|
|
223
245
|
try {
|
|
224
|
-
await
|
|
246
|
+
for await (const chunk of chunks) {
|
|
247
|
+
signal.throwIfAborted();
|
|
248
|
+
bytes += chunk.length;
|
|
249
|
+
if (bytes > format.maxBytes) {
|
|
250
|
+
throw new BadRequestError('Attachment exceeds its size limit');
|
|
251
|
+
}
|
|
252
|
+
hash.update(chunk);
|
|
253
|
+
await file.writeFile(chunk);
|
|
254
|
+
}
|
|
255
|
+
if (!bytes)
|
|
256
|
+
throw new BadRequestError('Empty attachment');
|
|
225
257
|
await file.sync();
|
|
226
258
|
}
|
|
227
259
|
finally {
|
|
228
260
|
await file.close();
|
|
229
261
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
262
|
+
const id = hash.digest('hex') + '.' + extension;
|
|
263
|
+
return {
|
|
264
|
+
dispose,
|
|
265
|
+
commit: async () => {
|
|
266
|
+
signal.throwIfAborted();
|
|
267
|
+
const target = await this.path('assets/' + id, false);
|
|
268
|
+
try {
|
|
269
|
+
await link(temporary, target);
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
if (!isSystemError(error, 'EEXIST'))
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
return id;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
238
278
|
}
|
|
239
|
-
|
|
240
|
-
await
|
|
241
|
-
|
|
242
|
-
throw error;
|
|
243
|
-
});
|
|
279
|
+
catch (error) {
|
|
280
|
+
await dispose();
|
|
281
|
+
throw error;
|
|
244
282
|
}
|
|
245
|
-
return id;
|
|
246
283
|
}
|
|
247
284
|
async readAsset(id) {
|
|
248
285
|
const target = await this.path('assets/' + id, false);
|
|
@@ -250,13 +287,18 @@ export class NotesRepository {
|
|
|
250
287
|
const file = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
251
288
|
try {
|
|
252
289
|
const stat = await file.stat();
|
|
253
|
-
|
|
254
|
-
|
|
290
|
+
const format = noteAssetFormat(id.split('.').at(-1));
|
|
291
|
+
if (!format ||
|
|
292
|
+
!stat.isFile() ||
|
|
293
|
+
!stat.size ||
|
|
294
|
+
stat.size > format.maxBytes) {
|
|
295
|
+
throw new BadRequestError('Invalid attachment');
|
|
255
296
|
}
|
|
256
|
-
return
|
|
297
|
+
return file.createReadStream({ autoClose: true, end: stat.size - 1 });
|
|
257
298
|
}
|
|
258
|
-
|
|
299
|
+
catch (error) {
|
|
259
300
|
await file.close();
|
|
301
|
+
throw error;
|
|
260
302
|
}
|
|
261
303
|
}
|
|
262
304
|
catch (error) {
|
|
@@ -340,7 +382,7 @@ export class NotesRepository {
|
|
|
340
382
|
if (path.split('/')[0] === 'assets') {
|
|
341
383
|
throw new BadRequestError('Cannot rename attachments');
|
|
342
384
|
}
|
|
343
|
-
const targetPath =
|
|
385
|
+
const targetPath = noteChildPath(noteParentPath(path), name);
|
|
344
386
|
if (targetPath.split('/')[0] === 'assets') {
|
|
345
387
|
throw new BadRequestError('Reserved folder name');
|
|
346
388
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { NotesExternalAssetsService } from "../services/notes-external-assets.service.js";
|
|
2
|
+
import { InspectExternalAssetBody, PreviewExternalAssetBody, ImportExternalAssetBody, ExternalAssetInspectionSchema, ExternalAssetResultSchema } from "../../shared/notes/schemas.js";
|
|
1
3
|
import multipart from '@fastify/multipart';
|
|
2
4
|
import { MAX_NOTE_ASSET_BYTES } from "../../shared/notes/constants.js";
|
|
3
5
|
import { noteAssetFormat } from "../../shared/notes/asset-formats.js";
|
|
4
6
|
import { NoteRecordSchema, NoteIdQuery, NoteDescriptorSchema, NoteResolveBody, NoteResolutionSchema, NoteGraphSchema, ResolveIdentityBody, NoteMutationSchema, UploadNoteAssetBody, NoteAssetSchema, NoteAssetContentSchema, NoteAssetParams, NoteQuery, NoteSchema, NotesIndexSchema, CreateNoteBody, SaveNoteBody, RenameNoteBody, CreateFolderBody } from "../../shared/notes/schemas.js";
|
|
5
7
|
import { ERROR_RESPONSES, NoContentSchema } from "../../shared/http/schemas.js";
|
|
6
8
|
import { BadRequestError } from "../errors/http.js";
|
|
9
|
+
import { withRequestSignal } from "../utils/request-abort.js";
|
|
7
10
|
import { createNotesOriginHook } from "../hooks/notes/same-origin.js";
|
|
8
|
-
export async function notesRoutes(app, { service, loopbackBind = false }) {
|
|
11
|
+
export async function notesRoutes(app, { service, loopbackBind = false, externalAssets = new NotesExternalAssetsService() }) {
|
|
9
12
|
await app.register(multipart, {
|
|
10
13
|
attachFieldsToBody: true,
|
|
11
14
|
limits: { fileSize: MAX_NOTE_ASSET_BYTES, files: 1, fields: 0, parts: 1 }
|
|
@@ -31,10 +34,42 @@ export async function notesRoutes(app, { service, loopbackBind = false }) {
|
|
|
31
34
|
reply.code(201);
|
|
32
35
|
return asset;
|
|
33
36
|
});
|
|
37
|
+
typed.post('/api/notes/assets/inspect', {
|
|
38
|
+
schema: {
|
|
39
|
+
tags: ['notes'],
|
|
40
|
+
summary: 'Inspect an external attachment',
|
|
41
|
+
body: InspectExternalAssetBody,
|
|
42
|
+
response: { 200: ExternalAssetInspectionSchema, ...ERROR_RESPONSES }
|
|
43
|
+
}
|
|
44
|
+
}, (request, reply) => withRequestSignal(reply, signal => externalAssets.inspect(request.body.url, signal)));
|
|
45
|
+
typed.post('/api/notes/assets/import', {
|
|
46
|
+
schema: {
|
|
47
|
+
tags: ['notes'],
|
|
48
|
+
summary: 'Import an external attachment',
|
|
49
|
+
body: ImportExternalAssetBody,
|
|
50
|
+
response: { 200: ExternalAssetResultSchema, ...ERROR_RESPONSES }
|
|
51
|
+
}
|
|
52
|
+
}, (request, reply) => withRequestSignal(reply, signal => externalAssets.importAsset(request.body, signal, (chunks, extension, saveSignal) => service.importAsset(chunks, extension, saveSignal))));
|
|
53
|
+
// The typed provider rejects a binary payload for the content schema.
|
|
54
|
+
app.post('/api/notes/assets/preview', {
|
|
55
|
+
schema: {
|
|
56
|
+
tags: ['notes'],
|
|
57
|
+
summary: 'Preview an external image',
|
|
58
|
+
body: PreviewExternalAssetBody,
|
|
59
|
+
response: { 200: NoteAssetContentSchema, ...ERROR_RESPONSES }
|
|
60
|
+
}
|
|
61
|
+
}, async (request, reply) => {
|
|
62
|
+
const preview = await withRequestSignal(reply, signal => externalAssets.preview(request.body.url, signal));
|
|
63
|
+
reply
|
|
64
|
+
.header('Cache-Control', 'no-store')
|
|
65
|
+
.header('X-Content-Type-Options', 'nosniff')
|
|
66
|
+
.type(preview.mime);
|
|
67
|
+
return reply.send(preview.body);
|
|
68
|
+
});
|
|
34
69
|
app.get('/api/notes/assets/:id', {
|
|
35
70
|
schema: {
|
|
36
71
|
tags: ['notes'],
|
|
37
|
-
summary: 'Read a stored note
|
|
72
|
+
summary: 'Read a stored note attachment',
|
|
38
73
|
params: NoteAssetParams,
|
|
39
74
|
response: { 200: NoteAssetContentSchema, ...ERROR_RESPONSES }
|
|
40
75
|
}
|
|
@@ -143,6 +178,17 @@ export async function notesRoutes(app, { service, loopbackBind = false }) {
|
|
|
143
178
|
response: { 200: NoteSchema, ...ERROR_RESPONSES }
|
|
144
179
|
}
|
|
145
180
|
}, request => service.save(request.body.path, request.body.text, request.body.version, request.body.id, request.body.storageId));
|
|
181
|
+
typed.post('/api/notes/delete', {
|
|
182
|
+
schema: {
|
|
183
|
+
tags: ['notes'],
|
|
184
|
+
summary: 'Hide a note without removing its file',
|
|
185
|
+
body: NoteIdQuery,
|
|
186
|
+
response: { 204: NoContentSchema, ...ERROR_RESPONSES }
|
|
187
|
+
}
|
|
188
|
+
}, async (request, reply) => {
|
|
189
|
+
await service.delete(request.body.id, request.body.storageId);
|
|
190
|
+
reply.code(204).send(null);
|
|
191
|
+
});
|
|
146
192
|
typed.post('/api/notes/rename', {
|
|
147
193
|
schema: {
|
|
148
194
|
tags: ['notes'],
|
|
@@ -162,7 +162,20 @@ async function buildAttachmentItem(item, deps) {
|
|
|
162
162
|
...(item.mimeType === '' ? {} : { mimeType: item.mimeType })
|
|
163
163
|
};
|
|
164
164
|
}
|
|
165
|
+
async function buildNoteItem(item, deps) {
|
|
166
|
+
try {
|
|
167
|
+
if (!deps.notes)
|
|
168
|
+
throw new AgentContextError('Notes are unavailable');
|
|
169
|
+
const note = await deps.notes.identity.readId(item.noteId, item.storageId);
|
|
170
|
+
return embed(deps, `githuman://note/${encodeURIComponent(item.storageId)}/${encodeURIComponent(item.noteId)}`, `Note ${note.path}:`, 'text/markdown', `Note: ${note.path}\n\n${note.text}`);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
throw asContextError(error);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
165
176
|
function buildItem(item, deps) {
|
|
177
|
+
if (item.kind === 'note')
|
|
178
|
+
return buildNoteItem(item, deps);
|
|
166
179
|
if (item.kind === 'diff') {
|
|
167
180
|
return buildDiffItem(item, deps);
|
|
168
181
|
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import PQueue from 'p-queue';
|
|
2
|
+
import { BadRequestError } from "../errors/http.js";
|
|
3
|
+
import { NoteAssetLimitError, NoteAssetQueueError } from "../errors/note-assets.js";
|
|
4
|
+
import { remoteAssetRequest, translateRemoteAssetError } from "../utils/remote-asset-request.js";
|
|
5
|
+
import { detectNoteAssetFormat } from "../utils/note-asset-format.js";
|
|
6
|
+
import { MAX_NOTE_ASSET_BYTES, NOTE_PDF_WARNING_BYTES } from "../../shared/notes/constants.js";
|
|
7
|
+
import { maxNoteAssetBytes } from "../../shared/notes/asset-formats.js";
|
|
8
|
+
// Limit disk and memory pressure across imports and background previews.
|
|
9
|
+
const ACTIVE_LIMIT = 3;
|
|
10
|
+
const QUEUE_LIMIT = 30;
|
|
11
|
+
// Bound active operations so stalled imports cannot occupy queue slots indefinitely.
|
|
12
|
+
const OPERATION_TIMEOUT_MS = 120000;
|
|
13
|
+
// A small prefix is sufficient for supported image and PDF signatures.
|
|
14
|
+
const FORMAT_PREFIX_BYTES = 8192;
|
|
15
|
+
// Explain the preview limit to the client in megabytes.
|
|
16
|
+
const PREVIEW_LIMIT_MESSAGE = `Image exceeds ${MAX_NOTE_ASSET_BYTES / 1_000_000} MB`;
|
|
17
|
+
// Yields Buffer chunks and fails once the total passes the limit.
|
|
18
|
+
async function* boundedChunks(source, limit, counter, signal, message) {
|
|
19
|
+
for await (const value of source) {
|
|
20
|
+
signal.throwIfAborted();
|
|
21
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
22
|
+
counter.bytes += chunk.length;
|
|
23
|
+
if (counter.bytes > limit)
|
|
24
|
+
throw new NoteAssetLimitError(message);
|
|
25
|
+
yield chunk;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function isSuccess(response) {
|
|
29
|
+
return (!!response.statusCode &&
|
|
30
|
+
response.statusCode >= 200 &&
|
|
31
|
+
response.statusCode < 300);
|
|
32
|
+
}
|
|
33
|
+
export class NotesExternalAssetsService {
|
|
34
|
+
queue = new PQueue({ concurrency: ACTIVE_LIMIT });
|
|
35
|
+
request;
|
|
36
|
+
constructor(request = remoteAssetRequest) {
|
|
37
|
+
this.request = request;
|
|
38
|
+
}
|
|
39
|
+
run(signal, priority, work) {
|
|
40
|
+
signal.throwIfAborted();
|
|
41
|
+
if (this.queue.pending >= ACTIVE_LIMIT && this.queue.size >= QUEUE_LIMIT) {
|
|
42
|
+
throw new NoteAssetQueueError();
|
|
43
|
+
}
|
|
44
|
+
// The queue removes a task that waits when its signal aborts. A running
|
|
45
|
+
// task stops through the signal that it receives.
|
|
46
|
+
return this.queue.add(async () => {
|
|
47
|
+
try {
|
|
48
|
+
return await work(AbortSignal.any([signal, AbortSignal.timeout(OPERATION_TIMEOUT_MS)]));
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throw translateRemoteAssetError(error);
|
|
52
|
+
}
|
|
53
|
+
}, { priority: priority ? 1 : 0, signal });
|
|
54
|
+
}
|
|
55
|
+
async inspect(url, signal) {
|
|
56
|
+
return this.run(signal, false, async (abort) => {
|
|
57
|
+
const response = await this.request(url, 'HEAD', abort);
|
|
58
|
+
try {
|
|
59
|
+
return {
|
|
60
|
+
size: isSuccess(response) ? this.length(response) : null
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
response.destroy();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
length(response) {
|
|
69
|
+
const value = response.headers['content-length'];
|
|
70
|
+
if (!value || !/^\d+$/.test(value))
|
|
71
|
+
return null;
|
|
72
|
+
const size = Number(value);
|
|
73
|
+
return Number.isSafeInteger(size) ? size : null;
|
|
74
|
+
}
|
|
75
|
+
async importAsset(input, signal, save) {
|
|
76
|
+
return this.run(signal, true, async (abort) => {
|
|
77
|
+
const response = await this.request(input.url, 'GET', abort);
|
|
78
|
+
try {
|
|
79
|
+
this.checkResponse(response);
|
|
80
|
+
const fileLimit = maxNoteAssetBytes(input.kind);
|
|
81
|
+
const warningLimit = input.kind === 'pdf' && !input.allowLargePdf
|
|
82
|
+
? NOTE_PDF_WARNING_BYTES
|
|
83
|
+
: fileLimit;
|
|
84
|
+
const limit = Math.min(input.remainingBytes, fileLimit, warningLimit);
|
|
85
|
+
const over = (size) => {
|
|
86
|
+
if (size <= limit)
|
|
87
|
+
return undefined;
|
|
88
|
+
if (size > input.remainingBytes) {
|
|
89
|
+
return { status: 'rejected', reason: 'operation-limit' };
|
|
90
|
+
}
|
|
91
|
+
if (size > fileLimit) {
|
|
92
|
+
return { status: 'rejected', reason: 'file-too-large' };
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
status: 'confirmation-required',
|
|
96
|
+
size: this.length(response)
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
const known = this.length(response);
|
|
100
|
+
if (known !== null) {
|
|
101
|
+
const result = over(known);
|
|
102
|
+
if (result)
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
const counter = { bytes: 0 };
|
|
106
|
+
const iterator = boundedChunks(response, limit, counter, abort, 'Attachment download limit reached');
|
|
107
|
+
const prefix = [];
|
|
108
|
+
let prefixBytes = 0;
|
|
109
|
+
try {
|
|
110
|
+
while (prefixBytes < FORMAT_PREFIX_BYTES) {
|
|
111
|
+
const next = await iterator.next();
|
|
112
|
+
if (next.done)
|
|
113
|
+
break;
|
|
114
|
+
prefix.push(next.value);
|
|
115
|
+
prefixBytes += next.value.length;
|
|
116
|
+
}
|
|
117
|
+
const format = await detectNoteAssetFormat(Buffer.concat(prefix));
|
|
118
|
+
if ((format.display === 'link' ? 'pdf' : 'image') !== input.kind) {
|
|
119
|
+
throw new BadRequestError('Attachment type does not match the selected resource');
|
|
120
|
+
}
|
|
121
|
+
const body = async function* () {
|
|
122
|
+
yield* prefix;
|
|
123
|
+
yield* iterator;
|
|
124
|
+
};
|
|
125
|
+
const url = await save(body(), format.extension, abort);
|
|
126
|
+
return {
|
|
127
|
+
status: 'stored',
|
|
128
|
+
url,
|
|
129
|
+
bytes: counter.bytes,
|
|
130
|
+
format: format.extension
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error instanceof NoteAssetLimitError)
|
|
135
|
+
return over(counter.bytes);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
response.destroy();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async preview(url, signal) {
|
|
145
|
+
return this.run(signal, false, async (abort) => {
|
|
146
|
+
const response = await this.request(url, 'GET', abort);
|
|
147
|
+
try {
|
|
148
|
+
this.checkResponse(response);
|
|
149
|
+
if ((this.length(response) ?? 0) > MAX_NOTE_ASSET_BYTES) {
|
|
150
|
+
throw new NoteAssetLimitError(PREVIEW_LIMIT_MESSAGE);
|
|
151
|
+
}
|
|
152
|
+
const chunks = [];
|
|
153
|
+
for await (const chunk of boundedChunks(response, MAX_NOTE_ASSET_BYTES, { bytes: 0 }, abort, PREVIEW_LIMIT_MESSAGE)) {
|
|
154
|
+
chunks.push(chunk);
|
|
155
|
+
}
|
|
156
|
+
const body = Buffer.concat(chunks);
|
|
157
|
+
const format = await detectNoteAssetFormat(body);
|
|
158
|
+
if (format.display !== 'image') {
|
|
159
|
+
throw new BadRequestError('Preview is not an image');
|
|
160
|
+
}
|
|
161
|
+
return { body, mime: format.mime };
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
response.destroy();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
checkResponse(response) {
|
|
169
|
+
if (!isSuccess(response)) {
|
|
170
|
+
throw new BadRequestError('Attachment server returned HTTP ' + response.statusCode);
|
|
171
|
+
}
|
|
172
|
+
if (response.headers['content-encoding'] &&
|
|
173
|
+
response.headers['content-encoding'] !== 'identity') {
|
|
174
|
+
throw new BadRequestError('Compressed attachment responses are not supported');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|