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` - [ ] 顶部区:工作区名 + 小一号完整路径;右侧 导出 / 设置 / 关于 同排