Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions .github/workflows/flatpak.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,26 @@ jobs:
- name: Run targeted regressions inside SDK
run: |
repository_test=$(find "$PWD/tmp/state/build" -type f -name notes-repository-test -print -quit)
controller_test=$(find "$PWD/tmp/state/build" -type f -name notes-controller-test -print -quit)
test -n "$repository_test"
test -n "$controller_test"
flatpak-builder --run tmp/build tmp/org.nextks.notes.json "$repository_test" \
managedSandboxLockRecoversAndRejectsConcurrentWriters \
managedFolderSummariesTrackNotesAndPreserveCreation managedLegacyFoldersGainOrderWithoutLosingIcons \
managedFolderMoveKeepsContentsAndRejectsInvalidDrops managedFolderOrderPersistsAndReplays \
managedDirectNoteDeletionIsPermanentAndScoped managedDirectNoteDeletionPreservesFailedItems \
managedDirectFolderDeletionIsPermanentAndScoped managedDirectFolderDeletionProtectsRecoveryDrafts \
2>&1 | tee tmp/evidence/repository-tests.log
flatpak-builder --run tmp/build tmp/org.nextks.notes.json env QT_QPA_PLATFORM=offscreen "$controller_test" \
directNoteDeletionSavesBeforeRemovingCurrentDocument failedDirectNoteDeletionKeepsCurrentDocument \
directFolderDeletionDrainsAndClearsTheOpenDocument failedDirectFolderDeletionKeepsTheOpenDocument \
2>&1 | tee tmp/evidence/controller-tests.log
for suite in managed-assets managed-recovery managed-portability notes-controller notes-chrome; do
binary=$(find "$PWD/tmp/state/build" -type f -name "$suite-test" -print -quit)
test -n "$binary"
arguments=()
if [ "$suite" = notes-chrome ]; then
qml_tests=$(find "$PWD/tmp/state/build" -type d -path '*/tests/chrome' -print -quit)
test -n "$qml_tests"
arguments=(-input "$qml_tests")
fi
flatpak-builder --run tmp/build tmp/org.nextks.notes.json env QT_QPA_PLATFORM=offscreen QT_QUICK_BACKEND=software "$binary" "${arguments[@]}" \
2>&1 | tee "tmp/evidence/$suite-tests.log"
done
- name: Export bundle and exact release plan
run: |
/usr/bin/python3 packaging/flatpak/export-release.py --repo tmp/repo --output dist --revision "$(git rev-parse HEAD)"
Expand Down
18 changes: 14 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ add_subdirectory("${KOS_UI_SOURCE_DIR}/shared/qml" kos-ui)
qt_add_library(nextnotes-storage STATIC
src/storage/MarkdownFile.cpp src/storage/MarkdownFile.h
src/storage/NotesRepository.cpp src/storage/NotesRepository.h
src/storage/ManagedStore.cpp src/storage/ManagedStore.h)
src/storage/ManagedStore.cpp src/storage/ManagedStore.h
src/storage/ManagedLibrary.cpp src/storage/ManagedLibrary.h src/storage/ManagedAssets.h)
target_include_directories(nextnotes-storage PUBLIC src/storage)
target_link_libraries(nextnotes-storage PUBLIC Qt6::Core SQLite::SQLite3)
qt_add_executable(nextnotes src/main.cpp src/EditorSession.cpp src/EditorSessionWeb.cpp src/EditorSession.h src/NotesController.cpp src/NotesLibrary.cpp src/EditorExport.cpp src/PdfExport.cpp src/NotesOrganizer.cpp src/NotesKnowledge.cpp src/NotesController.h)
target_sources(nextnotes PRIVATE src/EditorRenderLifecycle.cpp src/EditorRenderLifecycle.h)
qt_add_qml_module(nextnotes URI NextNotes VERSION 1.0 RESOURCE_PREFIX /qt/qml
QML_FILES qml/FolderBrowser.qml qml/NotesBrowser.qml qml/NotesSelectionBox.qml qml/FolderIcons.js qml/PopupPlacement.js qml/NotesPopover.qml qml/NotesContentPopover.qml qml/NotesSettings.qml qml/LibraryActions.qml qml/NotesWindowControls.qml qml/KnowledgePanel.qml qml/RecycleBin.qml qml/Main.qml qml/NoteNameEditor.qml qml/NotesOrganizer.qml qml/SourceEditor.qml qml/EditorTools.qml qml/CommandRegistry.qml qml/NotesTokens.qml qml/NoteReadingView.qml)
QML_FILES qml/FolderBrowser.qml qml/NotesBrowser.qml qml/NotesSelectionBox.qml qml/FolderIcons.js qml/PopupPlacement.js qml/NotesPopover.qml qml/NotesContentPopover.qml qml/NotesSettings.qml qml/NotesFilePicker.qml qml/LibraryActions.qml qml/NotesWindowControls.qml qml/KnowledgePanel.qml qml/RecycleBin.qml qml/Main.qml qml/NoteNameEditor.qml qml/NotesOrganizer.qml qml/SourceEditor.qml qml/EditorTools.qml qml/CommandRegistry.qml qml/NotesTokens.qml qml/NoteReadingView.qml)
qt_add_resources(nextnotes window-controls PREFIX /nextnotes/platform BASE qml FILES qml/NativeWindowControls.qml)
qt_add_resources(nextnotes app-icon PREFIX /nextnotes/icons BASE packaging FILES packaging/org.nextks.notes.svg)
file(GLOB_RECURSE EDITOR_ASSETS CONFIGURE_DEPENDS web/editor/dist/* web/editor/vendor/*)
Expand All @@ -33,11 +34,11 @@ if(NEXTNOTES_BUILD_TESTS)
enable_testing()
qt_add_executable(notes-chrome-test tests/chrome/main.cpp)
qt_add_qml_module(notes-chrome-test URI NextNotes.ChromeTests VERSION 1.0 RESOURCE_PREFIX /qt/qml
QML_FILES qml/NotesWindowControls.qml qml/NotesOrganizer.qml qml/FolderIcons.js qml/NotesTokens.qml qml/FolderBrowser.qml qml/NotesBrowser.qml qml/NotesSettings.qml
QML_FILES qml/NotesFilePicker.qml qml/NotesWindowControls.qml qml/NotesOrganizer.qml qml/FolderIcons.js qml/NotesTokens.qml qml/FolderBrowser.qml qml/NotesBrowser.qml qml/NotesSettings.qml
qml/NotesPopover.qml qml/NotesContentPopover.qml qml/PopupPlacement.js)
qt_add_resources(notes-chrome-test test-window-controls PREFIX /nextnotes/platform BASE qml FILES qml/NativeWindowControls.qml)
target_compile_definitions(notes-chrome-test PRIVATE QUICK_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/tests/chrome")
target_link_libraries(notes-chrome-test PRIVATE Kos::Ui Kos::UiPlugin Qt6::QuickTest Qt6::QuickControls2)
target_link_libraries(notes-chrome-test PRIVATE Kos::Ui Kos::UiPlugin Qt6::QuickTest Qt6::QuickControls2 Qt6::QuickDialogs2)
add_test(NAME notes-chrome COMMAND notes-chrome-test)
set_tests_properties(notes-chrome PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen;QT_QUICK_BACKEND=software")
qt_add_executable(editor-render-test tests/render/main.cpp src/EditorRenderLifecycle.cpp src/EditorRenderLifecycle.h)
Expand All @@ -50,6 +51,15 @@ if(NEXTNOTES_BUILD_TESTS)
qt_add_executable(notes-repository-test tests/NotesRepositoryTest.cpp)
target_link_libraries(notes-repository-test PRIVATE nextnotes-storage Qt6::Test)
add_test(NAME notes-repository COMMAND notes-repository-test)
qt_add_executable(managed-assets-test tests/ManagedAssetsTest.cpp)
target_link_libraries(managed-assets-test PRIVATE nextnotes-storage Qt6::Test)
add_test(NAME managed-assets COMMAND managed-assets-test)
qt_add_executable(managed-recovery-test tests/ManagedRecoveryTest.cpp)
target_link_libraries(managed-recovery-test PRIVATE nextnotes-storage Qt6::Test)
add_test(NAME managed-recovery COMMAND managed-recovery-test)
qt_add_executable(managed-portability-test tests/ManagedPortabilityTest.cpp)
target_link_libraries(managed-portability-test PRIVATE nextnotes-storage Qt6::Test)
add_test(NAME managed-portability COMMAND managed-portability-test)
qt_add_executable(notes-controller-test tests/NotesControllerTest.cpp src/NotesController.cpp src/NotesLibrary.cpp src/EditorExport.cpp src/PdfExport.cpp src/NotesOrganizer.cpp src/NotesKnowledge.cpp src/NotesController.h src/EditorSession.cpp src/EditorSessionWeb.cpp src/EditorSession.h)
target_include_directories(notes-controller-test PRIVATE src)
target_link_libraries(notes-controller-test PRIVATE nextnotes-storage Qt6::Test Qt6::Quick Qt6::Gui Qt6::WebEngineCore)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

面向 NextKde 的原生 Qt Quick 笔记应用:SQLite 管理内部 Markdown 笔记、三栏导航、所见即所得编辑、700ms 自动保存、中文搜索、收藏、虚拟文件夹和最近删除。首次打开创建空的内部笔记空间,不扫描个人目录、不创建演示数据。正文支持附件导入与可恢复清理;支持安全移动/重命名、非空文件夹整理和应用内 Markdown 链接导航;现支持笔记历史、搜索索引、标签/属性、双链补全与反链、智能文件夹、模板和每日笔记。

当前存储与导入方向以 [SQLite 管理说明](docs/managed-notes.md) 为准;下文旧文件仓库说明保留为历史实现记录。
当前存储与导入方向以 [SQLite 管理说明](docs/managed-notes.md) 为准;文件授权、附件跨位置恢复及数据库故障恢复见 [恢复说明与验证状态](docs/portable-recovery.md)。下文旧文件仓库说明保留为历史实现记录。

产品依据:[产品规格](docs/PRODUCT_SPEC.zh-CN.md)。正文功能、存储边界和 VM 验证记录见[正文编辑说明](docs/body-editor.md),笔记整理与批量接口见[整理操作说明](docs/organizer.md)。实现边界、编辑器比较、命令表、最小文件约定和组件差距:[架构记录](docs/architecture.md)。本次编辑器验证范围见下文,不代表全部产品验收。

Expand Down
2 changes: 2 additions & 0 deletions docs/backup-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

技术依据:[SQLite Backup API](https://www.sqlite.org/backup.html)。格式目前为应用标识与版本受校验的 SQLite 文件;不是 Markdown 导出的替代文件格式。

2026-11-13 开发更新:当前数据库无法初始化时,启动页可从完整备份恢复到同级新空间,保留原目录与数据库侧车文件。Flatpak 的文件选择、旧绝对附件链接兼容和外部文件失败草稿机制,见 [恢复说明与验证状态](portable-recovery.md);下方人工运行记录仍对应 2026-11-06,不代表这些新增路径已通过 GUI 验收。

## 批量导入

导入仍需先预览来源、目标、数量、目录结构和重复策略。扫描、内容检查、准备和写入显示阶段及已处理数量。读取阶段可停止;确认导入后也可停止。操作在存储工作线程执行,取消使用线程间原子标记,不等待工作线程处理另一条排队命令。
Expand Down
47 changes: 47 additions & 0 deletions docs/portable-recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 文件授权、附件迁移与故障恢复

2026-11-13。适用于本次开发分支;验证状态见末尾。

## 文件选择与 Flatpak

Flatpak 内的导入文件、导入文件夹、外部 Markdown、附件、Markdown 导出目录、完整备份及 HTML/PDF 输出,统一使用 Qt Quick Dialogs 的系统文件对话框。原生安装继续使用应用内选择器。正式包沿用 `QT_QPA_PLATFORMTHEME=xdgdesktopportal`,不增加 home/host 文件系统权限。

保存时在建议文件名中提供扩展名,使用系统对话框返回的原始 URL。若最终名称缺少要求的扩展名,提示重新选择,不在授权后追加扩展名写到另一个位置。取消不会发起读写,过期对话框回调不完成新的请求。写文件仍使用 QSaveFile,禁止退化为直接覆盖。备份先在私有位置生成一致 SQLite 快照,再写入所选位置;恢复先复制所选文件到私有位置进行校验。

单个 Markdown 文件的授权不包含同目录的图片。收集带相对附件的笔记时,应选择“从文件夹收集”;无法读取的资源保留原链接并报告。Qt 可能在系统对话框服务不可用时回退到内部对话框,因此组件测试不能证明 portal 已工作,必须在正式运行时观察实际 FileChooser 请求。Qt 6.11 的 portal 实现还存在程序主动关闭不发送 Request.Close 的限制;应用会取消本地请求并忽略迟到结果,但系统窗口可能仍需用户关闭。

## 跨位置恢复附件

新附件使用相对、百分号编码的 URL。数据库中 `assets/<SHA-256>.<扩展名>` 对应的 BLOB 是权威内容;磁盘 assets 目录是可再生缓存。

旧正文中的绝对 `file:` / `nkasset:` 链接只提供附件身份:必须精确匹配当前数据库拥有的路径与摘要,才能读取当前空间的附件。旧磁盘位置不会被读取。当前缓存需通过目录范围、符号链接、大小及实际内容摘要检查;编辑器获得的是已校验字节。外部 Markdown 只使用自身目录范围,不沿用内部空间的旧路径映射。普通文件/目录链接与图片读取分开校验,不把图片的 32 MiB 限制套用到普通外部链接。Linux 目录遍历使用 O_PATH,允许读取经过可搜索但不可列出的父目录下的已知文件。

恢复不批量改写正文、历史、草稿及其摘要。历史正文保留的 `../assets/...` 在移动后按数据库身份定位;它不授予跨目录读取权限。创建副本、应用模板和 Markdown 导出时,按目标位置重写受支持的附件链接并保留 URL 查询与片段。Markdown 导出直接读取 BLOB,即使缓存缺失或被更改也可生成原附件。HTML 资源链接及未知附件保留原文并在导出完成窗口列出警告。

附件清理同时检查当前笔记、最近删除、历史、恢复草稿、模板及内存草稿中的引用,防止当前正文不再引用后提前回收仍可恢复的附件。

## 当前数据库无法打开时恢复

初始化失败后,启动页提供“从完整备份恢复”。在私有副本中检查现有数据库及侧车文件,避免校验过程修改故障现场。所选备份需通过应用标识、版本、完整性、外键、受控结构、路径与附件摘要校验。

预检查还会拒绝被普通文件占用的缓存目录及缺少 rowid 的不兼容历史表。正常打开时,附件缓存写入失败作为警告报告,不把已经打开的数据库误报为初始化失败;故障恢复的新空间则必须完整生成缓存后才能发布。

恢复先在同级 `Library-restored-<UUID>` 准备完整可打开空间及附件缓存,再通过稳定入口目录中的 `active-library.json` 发布新位置。原数据库、WAL、SHM 等文件保留,原锁文件 inode 不替换。下次启动仍从原入口读取活动位置,并同时保护入口与活动空间的实例锁。

发布前失败保留原活动位置;发布后若目录同步无法确认,会明确提示关闭后重开,并阻止继续写入,不声称切换未发生。若当前空间本来可正常打开,则继续使用已有恢复机制:先生成保护备份,再恢复所选内容。

## 外部文件保存失败

保存外部 Markdown 前,先在内部空间独立提交恢复草稿。外部文件发生冲突、删除或写入失败时,本地输入仍可从草稿恢复;冲突后的继续输入也会自动更新恢复草稿,不覆盖外部版本。

草稿提交失败时,界面明确显示仅在内存中保留,关闭仍受保护。迟到的旧修订回包不会把最新输入标记为已持久保存。外部写入成功但草稿清理失败时,保留新文件摘要并报告清理警告,避免下一次保存误判为外部冲突。

## 验证状态

测试均使用合成数据和临时空间,不使用个人笔记库。新增 `managed-assets`、`managed-recovery`、`managed-portability`,并扩展 controller 与 QML 文件选择器回归;已有 Flatpak CI 已登记这些套件及完整 controller/chrome 回归,尚未触发新 CI 运行。覆盖跨根目录恢复、旧路径不可用、附件 BLOB 导出、历史/草稿引用、损坏数据库原字节保留、锁 inode、恢复发布中断、外部冲突及草稿持久性。

此前候选已通过原生 Qt 6.10.2 与 KDE 6.11 SDK 编译,以 `org.nextks.notes.RecoveryTest` 安装于隔离 VM;运行权限没有 home/host 授权。该快照的 repository、managed-assets、managed-recovery、editor-session 四组通过;notes-chrome 出现 5 个失败,notes-controller 因旧测试假定而崩溃。随后修正了 URL 比较、对话框取消竞态及过时测试;这些修正与新 portability 测试尚未重跑,不能视为通过。

源码快照已传至 VM 的 `/home/amao/nknotes-recovery-20261113/source-final.tar.gz`,SHA-256 为 `3f4b36acca3c9bae78915b5391c97589436106f0108572956024b7b837bed52e`。自动审批拒绝解压及构建,理由是缺少用户对该主机写入与构建的直接授权。后续静态复核修正、对应回归、文档及 CI 路径调整不包含在该归档中;重跑前应重新冻结最终源码。

实际 Flatpak 图形验收也尚未执行:自动审批拒绝了远程测试夹具写入与应用启动。尚无 FileChooser D-Bus 请求、实际点击、截图或 portal 保存结果证据,组件测试与 SDK 编译不替代这些验收。本次仅提交源码供审查,尚未发布新版本或更新用户安装。
Loading
Loading