From c12408d2e33ff49fb76e434e2308e4190bda02de Mon Sep 17 00:00:00 2001 From: doyaGu Date: Sat, 3 Oct 2026 08:12:53 -0400 Subject: [PATCH] fix(storage): refuse to open a workspace whose database file is missing With .ykanban present but ykanban.db gone, opening used to create an empty database and show an empty board without any hint. It now throws WorkspaceDatabaseMissingException and shows the open-failed page, telling the user to restore a backup or delete .ykanban and initialize again. --- USAGE.md | 2 +- .../Workspace/WorkspaceSessionTests.cs | 15 +++++++++++++ .../OpenFailedPageViewModelTests.cs | 3 +++ YKanBan/Assets/Locales/Resources.resx | 3 +++ YKanBan/Assets/Locales/Resources.zh-Hans.resx | 3 +++ YKanBan/Storage/StorageExceptions.cs | 22 +++++++++++++++++++ YKanBan/Storage/Workspace/WorkspaceSession.cs | 16 +++++++++++++- .../ErrorPages/OpenFailedPageViewModel.cs | 3 ++- docs/ACCEPTANCE.md | 1 + 9 files changed, 65 insertions(+), 3 deletions(-) diff --git a/USAGE.md b/USAGE.md index 83af356..69141cb 100644 --- a/USAGE.md +++ b/USAGE.md @@ -62,7 +62,7 @@ instead: | Folder not found | `` does not exist or is not a folder. | | No workspace here | `` exists but has no `.ykanban`. The **Initialize workspace** button creates one, with the columns To Do / In Progress / Done (named in the current interface language). | | Workspace locked | Another YKanBan window already has this workspace open. Close that window first. Also shown when another window takes the lock while you initialize. | -| Cannot open workspace | The database was created by a newer YKanBan, is damaged or cannot be opened, or a file-system or permission error occurred (also when initializing fails for a reason other than the lock). The English error details can be selected and copied. | +| Cannot open workspace | `.ykanban` exists but `ykanban.db` in it is missing (restore it from a backup, or delete `.ykanban` and initialize again; YKanBan never silently creates an empty board), the database was created by a newer YKanBan, is damaged or cannot be opened, or a file-system or permission error occurred (also when initializing fails for a reason other than the lock). The English error details can be selected and copied. | The window title is ` - YKanBan` while a workspace is open and just `YKanBan` on these pages. diff --git a/YKanBan.Tests/Storage/Workspace/WorkspaceSessionTests.cs b/YKanBan.Tests/Storage/Workspace/WorkspaceSessionTests.cs index 8401c72..e49d495 100644 --- a/YKanBan.Tests/Storage/Workspace/WorkspaceSessionTests.cs +++ b/YKanBan.Tests/Storage/Workspace/WorkspaceSessionTests.cs @@ -89,4 +89,19 @@ public class WorkspaceSessionTests Assert.ThrowsExactly(() => WorkspaceSession.Initialize(directory.FullPath)); using WorkspaceSession reopened = WorkspaceSession.Open(directory.FullPath); } + + [TestMethod] + public void OpenWithMissingDatabaseThrowsAndCreatesNothing() + { + using var directory = new TempDirectory(); + WorkspaceInitializer.Initialize(directory.FullPath); + string databasePath = WorkspacePaths.Database(directory.FullPath); + File.Delete(databasePath); + + Assert.ThrowsExactly(() => WorkspaceSession.Open(directory.FullPath)); + Assert.IsFalse(File.Exists(databasePath)); + + // The lock was released again: the next attempt fails the same way, not with a lock conflict. + Assert.ThrowsExactly(() => WorkspaceSession.Open(directory.FullPath)); + } } diff --git a/YKanBan.Tests/ViewModels/OpenFailedPageViewModelTests.cs b/YKanBan.Tests/ViewModels/OpenFailedPageViewModelTests.cs index cdd2744..ac1e546 100644 --- a/YKanBan.Tests/ViewModels/OpenFailedPageViewModelTests.cs +++ b/YKanBan.Tests/ViewModels/OpenFailedPageViewModelTests.cs @@ -43,6 +43,9 @@ public class OpenFailedPageViewModelTests Assert.AreEqual( Expected(Resources.Error_OpenFailed_SchemaTooNew, 3L, 1), OpenFailedPageViewModel.BuildMessage(Folder, new SchemaVersionException(3, 1))); + Assert.AreEqual( + Expected(Resources.Error_OpenFailed_DatabaseMissing), + OpenFailedPageViewModel.BuildMessage(Folder, new WorkspaceDatabaseMissingException("/work/repo/.ykanban/ykanban.db"))); Assert.AreEqual( Expected(Resources.Error_OpenFailed_Database), OpenFailedPageViewModel.BuildMessage(Folder, new SqliteException("file is not a database", 26))); diff --git a/YKanBan/Assets/Locales/Resources.resx b/YKanBan/Assets/Locales/Resources.resx index 8dca7d8..edbca1b 100644 --- a/YKanBan/Assets/Locales/Resources.resx +++ b/YKanBan/Assets/Locales/Resources.resx @@ -79,6 +79,9 @@ The workspace database cannot be opened or is damaged: {0} + + The workspace database (.ykanban/ykanban.db) is missing. Restore it from a backup together with its -wal and -shm files, or delete the .ykanban folder to initialize the workspace again: {0} + Access to the workspace was denied: {0} diff --git a/YKanBan/Assets/Locales/Resources.zh-Hans.resx b/YKanBan/Assets/Locales/Resources.zh-Hans.resx index 691ca62..78af73d 100644 --- a/YKanBan/Assets/Locales/Resources.zh-Hans.resx +++ b/YKanBan/Assets/Locales/Resources.zh-Hans.resx @@ -79,6 +79,9 @@ 工作区数据库无法打开或已损坏:{0} + + 工作区数据库(.ykanban/ykanban.db)不存在。请从备份恢复(连同 -wal、-shm 文件),或删除 .ykanban 文件夹后重新初始化:{0} + 没有访问该工作区的权限:{0} diff --git a/YKanBan/Storage/StorageExceptions.cs b/YKanBan/Storage/StorageExceptions.cs index de0b738..ffa37ad 100644 --- a/YKanBan/Storage/StorageExceptions.cs +++ b/YKanBan/Storage/StorageExceptions.cs @@ -61,6 +61,28 @@ public sealed class WorkspaceNotInitializedException : YKanBanException } } +/// +/// Signals that .ykanban exists but its database file does not, so the +/// workspace data is missing; a new empty database is never created silently. +/// +public sealed class WorkspaceDatabaseMissingException : YKanBanException +{ + /// + /// Gets the database file that was expected to exist. + /// + public string DatabasePath { get; } + + /// + /// Initializes the exception for a missing workspace database. + /// + /// The missing database file path. + public WorkspaceDatabaseMissingException(string databasePath) + : base($"The workspace database is missing: {databasePath}") + { + DatabasePath = databasePath; + } +} + /// /// Signals that another instance already holds the workspace lock, so this /// process must not touch the workspace. diff --git a/YKanBan/Storage/Workspace/WorkspaceSession.cs b/YKanBan/Storage/Workspace/WorkspaceSession.cs index c39f44f..0d8ab63 100644 --- a/YKanBan/Storage/Workspace/WorkspaceSession.cs +++ b/YKanBan/Storage/Workspace/WorkspaceSession.cs @@ -41,7 +41,21 @@ public sealed class WorkspaceSession : IDisposable /// The opened session; dispose it to release the lock and connection. /// The folder has no .ykanban structure. /// Another instance already holds the lock. - public static WorkspaceSession Open(string folderPath) => OpenLocked(folderPath, WorkspaceLock.Acquire(folderPath)); + /// .ykanban exists but its database file does not. + public static WorkspaceSession Open(string folderPath) + { + WorkspaceLock workspaceLock = WorkspaceLock.Acquire(folderPath); + + // Opening would otherwise create an empty database and show an empty board as if nothing was lost. + string databasePath = WorkspacePaths.Database(folderPath); + if (!File.Exists(databasePath)) + { + workspaceLock.Dispose(); + throw new WorkspaceDatabaseMissingException(databasePath); + } + + return OpenLocked(folderPath, workspaceLock); + } /// /// Initializes a workspace with its preset columns and opens it. The lock diff --git a/YKanBan/ViewModels/ErrorPages/OpenFailedPageViewModel.cs b/YKanBan/ViewModels/ErrorPages/OpenFailedPageViewModel.cs index d4f9978..8af39db 100644 --- a/YKanBan/ViewModels/ErrorPages/OpenFailedPageViewModel.cs +++ b/YKanBan/ViewModels/ErrorPages/OpenFailedPageViewModel.cs @@ -6,7 +6,7 @@ namespace YKanBan.ViewModels.ErrorPages; /// /// The full-window page shown when a workspace cannot be opened or initialized: -/// a schema newer than this build, an unopenable or damaged database, an I/O or +/// a schema newer than this build, a missing, unopenable or damaged database, an I/O or /// permission error, or any unrecognized failure. It shows a localized message /// chosen by exception type plus the read-only, copyable English exception /// details, and has no operations. @@ -55,6 +55,7 @@ public sealed class OpenFailedPageViewModel : ViewModelBase { SchemaVersionException schema => string.Format( culture, Resources.Error_OpenFailed_SchemaTooNew, folderPath, schema.StoredVersion, schema.SupportedVersion), + WorkspaceDatabaseMissingException => string.Format(culture, Resources.Error_OpenFailed_DatabaseMissing, folderPath), SqliteException => string.Format(culture, Resources.Error_OpenFailed_Database, folderPath), UnauthorizedAccessException => string.Format(culture, Resources.Error_OpenFailed_AccessDenied, folderPath), IOException => string.Format(culture, Resources.Error_OpenFailed_Io, folderPath), diff --git a/docs/ACCEPTANCE.md b/docs/ACCEPTANCE.md index 33ed957..d8e1ce2 100644 --- a/docs/ACCEPTANCE.md +++ b/docs/ACCEPTANCE.md @@ -54,6 +54,7 @@ - [ ] 同一工作区开第二个实例 → 锁冲突页,无任何操作按钮;关闭第一个实例后可正常打开 - [ ] 工作区数据库 `user_version` 改为更高值 → 打开失败页,提示版本过新,英文详情可选中复制 - [ ] 把 `ykanban.db` 换成垃圾内容 → 打开失败页(数据库无法打开/损坏) +- [ ] 关闭程序后删除 `.ykanban/ykanban.db`(及 -wal / -shm)→ 打开失败页,提示数据库缺失;`.ykanban` 中**不会**出现新建的空库 - [ ] 窗口标题:正常时 `工作区名 - YKanBan`,错误页时仅 `YKanBan` - [ ] 顶部区:工作区名 + 小一号完整路径;右侧 导出 / 设置 / 关于 同排