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.
This commit is contained in:
1 parent
22102cd1ea
commit
c12408d2e3
9 files changed
+65
-3
No files matched your search
@@ -62,7 +62,7 @@ instead:
|
|||||||
| Folder not found | `<path>` does not exist or is not a folder. |
|
| Folder not found | `<path>` does not exist or is not a folder. |
|
||||||
| No workspace here | `<path>` 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). |
|
| No workspace here | `<path>` 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. |
|
| 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 `<workspace name> - YKanBan` while a workspace is open and
|
The window title is `<workspace name> - YKanBan` while a workspace is open and
|
||||||
just `YKanBan` on these pages.
|
just `YKanBan` on these pages.
|
||||||
|
|||||||
@@ -89,4 +89,19 @@ public class WorkspaceSessionTests
|
|||||||
Assert.ThrowsExactly<InvalidOperationException>(() => WorkspaceSession.Initialize(directory.FullPath));
|
Assert.ThrowsExactly<InvalidOperationException>(() => WorkspaceSession.Initialize(directory.FullPath));
|
||||||
using WorkspaceSession reopened = WorkspaceSession.Open(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<WorkspaceDatabaseMissingException>(() => 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<WorkspaceDatabaseMissingException>(() => WorkspaceSession.Open(directory.FullPath));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -43,6 +43,9 @@ public class OpenFailedPageViewModelTests
|
|||||||
Assert.AreEqual(
|
Assert.AreEqual(
|
||||||
Expected(Resources.Error_OpenFailed_SchemaTooNew, 3L, 1),
|
Expected(Resources.Error_OpenFailed_SchemaTooNew, 3L, 1),
|
||||||
OpenFailedPageViewModel.BuildMessage(Folder, new SchemaVersionException(3, 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(
|
Assert.AreEqual(
|
||||||
Expected(Resources.Error_OpenFailed_Database),
|
Expected(Resources.Error_OpenFailed_Database),
|
||||||
OpenFailedPageViewModel.BuildMessage(Folder, new SqliteException("file is not a database", 26)));
|
OpenFailedPageViewModel.BuildMessage(Folder, new SqliteException("file is not a database", 26)));
|
||||||
|
|||||||
@@ -79,6 +79,9 @@
|
|||||||
<data name="Error_OpenFailed_Database" xml:space="preserve">
|
<data name="Error_OpenFailed_Database" xml:space="preserve">
|
||||||
<value>The workspace database cannot be opened or is damaged: {0}</value>
|
<value>The workspace database cannot be opened or is damaged: {0}</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Error_OpenFailed_DatabaseMissing" xml:space="preserve">
|
||||||
|
<value>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}</value>
|
||||||
|
</data>
|
||||||
<data name="Error_OpenFailed_AccessDenied" xml:space="preserve">
|
<data name="Error_OpenFailed_AccessDenied" xml:space="preserve">
|
||||||
<value>Access to the workspace was denied: {0}</value>
|
<value>Access to the workspace was denied: {0}</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -79,6 +79,9 @@
|
|||||||
<data name="Error_OpenFailed_Database" xml:space="preserve">
|
<data name="Error_OpenFailed_Database" xml:space="preserve">
|
||||||
<value>工作区数据库无法打开或已损坏:{0}</value>
|
<value>工作区数据库无法打开或已损坏:{0}</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Error_OpenFailed_DatabaseMissing" xml:space="preserve">
|
||||||
|
<value>工作区数据库(.ykanban/ykanban.db)不存在。请从备份恢复(连同 -wal、-shm 文件),或删除 .ykanban 文件夹后重新初始化:{0}</value>
|
||||||
|
</data>
|
||||||
<data name="Error_OpenFailed_AccessDenied" xml:space="preserve">
|
<data name="Error_OpenFailed_AccessDenied" xml:space="preserve">
|
||||||
<value>没有访问该工作区的权限:{0}</value>
|
<value>没有访问该工作区的权限:{0}</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -61,6 +61,28 @@ public sealed class WorkspaceNotInitializedException : YKanBanException
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signals that .ykanban exists but its database file does not, so the
|
||||||
|
/// workspace data is missing; a new empty database is never created silently.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WorkspaceDatabaseMissingException : YKanBanException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the database file that was expected to exist.
|
||||||
|
/// </summary>
|
||||||
|
public string DatabasePath { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the exception for a missing workspace database.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="databasePath">The missing database file path.</param>
|
||||||
|
public WorkspaceDatabaseMissingException(string databasePath)
|
||||||
|
: base($"The workspace database is missing: {databasePath}")
|
||||||
|
{
|
||||||
|
DatabasePath = databasePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Signals that another instance already holds the workspace lock, so this
|
/// Signals that another instance already holds the workspace lock, so this
|
||||||
/// process must not touch the workspace.
|
/// process must not touch the workspace.
|
||||||
|
|||||||
@@ -41,7 +41,21 @@ public sealed class WorkspaceSession : IDisposable
|
|||||||
/// <returns>The opened session; dispose it to release the lock and connection.</returns>
|
/// <returns>The opened session; dispose it to release the lock and connection.</returns>
|
||||||
/// <exception cref="WorkspaceNotInitializedException">The folder has no .ykanban structure.</exception>
|
/// <exception cref="WorkspaceNotInitializedException">The folder has no .ykanban structure.</exception>
|
||||||
/// <exception cref="WorkspaceLockException">Another instance already holds the lock.</exception>
|
/// <exception cref="WorkspaceLockException">Another instance already holds the lock.</exception>
|
||||||
public static WorkspaceSession Open(string folderPath) => OpenLocked(folderPath, WorkspaceLock.Acquire(folderPath));
|
/// <exception cref="WorkspaceDatabaseMissingException">.ykanban exists but its database file does not.</exception>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a workspace with its preset columns and opens it. The lock
|
/// Initializes a workspace with its preset columns and opens it. The lock
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace YKanBan.ViewModels.ErrorPages;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The full-window page shown when a workspace cannot be opened or initialized:
|
/// 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
|
/// permission error, or any unrecognized failure. It shows a localized message
|
||||||
/// chosen by exception type plus the read-only, copyable English exception
|
/// chosen by exception type plus the read-only, copyable English exception
|
||||||
/// details, and has no operations.
|
/// details, and has no operations.
|
||||||
@@ -55,6 +55,7 @@ public sealed class OpenFailedPageViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
SchemaVersionException schema => string.Format(
|
SchemaVersionException schema => string.Format(
|
||||||
culture, Resources.Error_OpenFailed_SchemaTooNew, folderPath, schema.StoredVersion, schema.SupportedVersion),
|
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),
|
SqliteException => string.Format(culture, Resources.Error_OpenFailed_Database, folderPath),
|
||||||
UnauthorizedAccessException => string.Format(culture, Resources.Error_OpenFailed_AccessDenied, folderPath),
|
UnauthorizedAccessException => string.Format(culture, Resources.Error_OpenFailed_AccessDenied, folderPath),
|
||||||
IOException => string.Format(culture, Resources.Error_OpenFailed_Io, folderPath),
|
IOException => string.Format(culture, Resources.Error_OpenFailed_Io, folderPath),
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
- [ ] 同一工作区开第二个实例 → 锁冲突页,无任何操作按钮;关闭第一个实例后可正常打开
|
- [ ] 同一工作区开第二个实例 → 锁冲突页,无任何操作按钮;关闭第一个实例后可正常打开
|
||||||
- [ ] 工作区数据库 `user_version` 改为更高值 → 打开失败页,提示版本过新,英文详情可选中复制
|
- [ ] 工作区数据库 `user_version` 改为更高值 → 打开失败页,提示版本过新,英文详情可选中复制
|
||||||
- [ ] 把 `ykanban.db` 换成垃圾内容 → 打开失败页(数据库无法打开/损坏)
|
- [ ] 把 `ykanban.db` 换成垃圾内容 → 打开失败页(数据库无法打开/损坏)
|
||||||
|
- [ ] 关闭程序后删除 `.ykanban/ykanban.db`(及 -wal / -shm)→ 打开失败页,提示数据库缺失;`.ykanban` 中**不会**出现新建的空库
|
||||||
- [ ] 窗口标题:正常时 `工作区名 - YKanBan`,错误页时仅 `YKanBan`
|
- [ ] 窗口标题:正常时 `工作区名 - YKanBan`,错误页时仅 `YKanBan`
|
||||||
- [ ] 顶部区:工作区名 + 小一号完整路径;右侧 导出 / 设置 / 关于 同排
|
- [ ] 顶部区:工作区名 + 小一号完整路径;右侧 导出 / 设置 / 关于 同排
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user