mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-09-01 12:51:00 +00:00
fix(updater): never sweep the live sqlite database as a stale file
The cleanup step deletes any file the shipped manifest does not list outside the protected prefixes. A SQLite database kept under database/ (a path the installer accepts) is user state that can never appear in a manifest, so an update destroyed it. The keep-list now always includes the resolved database file of every in-installation SQLite connection, plus its -wal/-shm/-journal side files. Found by the 3.0.0-alpha.2 -> alpha.3 upgrade rehearsal.
This commit is contained in:
@@ -152,6 +152,48 @@ class Updater
|
||||
* are never touched. Directories left empty by the sweep are removed in a
|
||||
* second walk, so a pruned subtree disappears entirely.
|
||||
*/
|
||||
/**
|
||||
* The paths the cleanup sweep must never touch: the configured protected
|
||||
* prefixes, plus the live database files of every SQLite connection whose
|
||||
* file sits inside the installation. The database is user state, not
|
||||
* shipped code, so it can never appear in a release manifest; without this
|
||||
* a database kept outside `storage` (for example `database/database.sqlite`,
|
||||
* which the installer accepts) would be swept as stale on every update.
|
||||
* SQLite side files (`-wal`, `-shm`, `-journal`) ride along with their
|
||||
* database.
|
||||
*/
|
||||
public static function protectedPaths(): array
|
||||
{
|
||||
$keep = config('invoiceshelf.update_protected_paths', []);
|
||||
|
||||
foreach (config('database.connections', []) as $connection) {
|
||||
if (($connection['driver'] ?? null) !== 'sqlite') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$database = $connection['database'] ?? null;
|
||||
|
||||
if (! is_string($database) || $database === '' || $database === ':memory:') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$root = rtrim(base_path(), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
|
||||
$absolute = str_starts_with($database, DIRECTORY_SEPARATOR) ? $database : $root.$database;
|
||||
|
||||
if (! str_starts_with($absolute, $root)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relative = substr($absolute, strlen($root));
|
||||
|
||||
foreach (['', '-wal', '-shm', '-journal'] as $suffix) {
|
||||
$keep[] = $relative.$suffix;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($keep));
|
||||
}
|
||||
|
||||
public static function cleanStaleFiles(): array
|
||||
{
|
||||
$manifestPath = base_path('manifest.json');
|
||||
@@ -167,7 +209,7 @@ class Updater
|
||||
}
|
||||
|
||||
$shipped = array_flip($manifest);
|
||||
$keep = config('invoiceshelf.update_protected_paths', []);
|
||||
$keep = static::protectedPaths();
|
||||
$cleaned = 0;
|
||||
|
||||
foreach (static::walkInstallation() as $entry) {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use App\Platform\Operations\Update\Updater;
|
||||
|
||||
/*
|
||||
* The cleanup sweep's keep-list. The static config prefixes are the baseline;
|
||||
* on top of them the live SQLite database file (and its side files) must be
|
||||
* protected wherever the installer put it, because user state can never appear
|
||||
* in a release manifest and would otherwise be swept as stale.
|
||||
*/
|
||||
|
||||
test('the configured protected prefixes are always kept', function (): void {
|
||||
$paths = Updater::protectedPaths();
|
||||
|
||||
foreach (config('invoiceshelf.update_protected_paths') as $prefix) {
|
||||
expect($paths)->toContain($prefix);
|
||||
}
|
||||
});
|
||||
|
||||
test('a sqlite database inside the installation is protected with its side files', function (): void {
|
||||
config()->set('database.connections.sqlite.database', base_path('database/database.sqlite'));
|
||||
|
||||
$paths = Updater::protectedPaths();
|
||||
|
||||
expect($paths)
|
||||
->toContain('database/database.sqlite')
|
||||
->toContain('database/database.sqlite-wal')
|
||||
->toContain('database/database.sqlite-shm')
|
||||
->toContain('database/database.sqlite-journal');
|
||||
});
|
||||
|
||||
test('a relative sqlite path resolves against the installation root', function (): void {
|
||||
config()->set('database.connections.sqlite.database', 'database/relative.sqlite');
|
||||
|
||||
expect(Updater::protectedPaths())->toContain('database/relative.sqlite');
|
||||
});
|
||||
|
||||
test('an in-memory database adds nothing to the keep list', function (): void {
|
||||
config()->set('database.connections.sqlite.database', ':memory:');
|
||||
|
||||
$baseline = count(config('invoiceshelf.update_protected_paths'));
|
||||
|
||||
expect(Updater::protectedPaths())->toHaveCount($baseline);
|
||||
});
|
||||
|
||||
test('a sqlite database outside the installation adds nothing to the keep list', function (): void {
|
||||
config()->set('database.connections.sqlite.database', '/var/lib/invoiceshelf/external.sqlite');
|
||||
|
||||
$baseline = count(config('invoiceshelf.update_protected_paths'));
|
||||
|
||||
expect(Updater::protectedPaths())->toHaveCount($baseline);
|
||||
});
|
||||
Reference in New Issue
Block a user