6cbc5d79d4
CI / unit (8.2) (push) Has been cancelled
CI / unit (8.3) (push) Has been cancelled
CI / unit (8.4) (push) Has been cancelled
CI / psalm (push) Has been cancelled
CI / integration (mariadb, 32) (push) Has been cancelled
CI / integration (mariadb, 33) (push) Has been cancelled
CI / integration (mariadb, 34) (push) Has been cancelled
CI / integration (mariadb, 35) (push) Has been cancelled
CI / integration (pgsql, 32) (push) Has been cancelled
CI / integration (pgsql, 33) (push) Has been cancelled
CI / integration (pgsql, 34) (push) Has been cancelled
CI / integration (pgsql, 35) (push) Has been cancelled
CI / integration (sqlite, 32) (push) Has been cancelled
CI / integration (sqlite, 33) (push) Has been cancelled
CI / integration (sqlite, 34) (push) Has been cancelled
CI / integration (sqlite, 35) (push) Has been cancelled
CI / integration-targets (push) Has been cancelled
CI / integration-user (push) Has been cancelled
Wires the existing RepositoryExportDestination/RepositoryImportSource (see lib/UserMigration/) into user_migration's own UserMigrationService, resolved on demand through a new UserMigrationServiceLocator since the app is an optional dependency (PLAN.md 3.8) -- without it, only this feature is unavailable, the full backup still works. New: UserBackupService::backupUser() (occ backup:backup:user) streams a user_migration export straight into the repository, under the same shared location lock as a regular backup. UserRestoreService::restoreUser() (occ backup:restore:user) never imports over a live account: --mode=replace safety-exports the current state first, then deletes and recreates the account from the backup under the same uid; --mode=as-backup restores into a new "<uid>-bak" account instead, since Nextcloud cannot rename a uid. The catalog now also tracks user export manifests (Repository::recordUserExport/ listUserExports/forgetUserExport/isRecordedUserExport), rollback-protected like a snapshot: listUserExports() verifies each one is still present on the location, and restore only ever reads a manifest the verified catalog actually recorded, never an arbitrary caller-supplied path. prune()'s existing raw users/ scan for blob retention is unchanged. A new short exclusive "catalog" lock (PruneService::catalogLockKey()) serializes the catalog write itself, so two concurrent per-user backups (or one alongside a full backup) can no longer race to write the same next generation and silently drop one of them -- the broader per-target shared lock only ever protected against a concurrent cleanup, not against other shared holders. Two real bugs found by testing this against a live Nextcloud instance, not by unit tests (RepositoryExportDestination/RepositoryImportSource implement OCP interfaces that plain PHPUnit can't load, see tests/Unit/Repository/UserExportTest.php's docblock; the new tests/integration/user-roundtrip.sh covers the real round trip instead): - RepositoryImportSource::copyToFolder() double-closed the stream it handed to putContent()/newFile() (both already close it themselves), which throws a TypeError on PHP 8 and broke every restored file with content. - A replace-mode restore failed on NotPermittedException: IRootFolder::getUserFolder() caches a Folder per uid for the process lifetime, so after delete+recreate of the same uid the import still got the deleted account's (now storage-less) folder. Fixed by dispatching FilesystemTornDownEvent, which Root already listens for to clear exactly that cache -- no internal OC\ classes needed. Local review (CodeRabbit + Codex) against this diff found three more real issues, all fixed: the catalog write race above, a failed as-backup import leaving a broken stub account behind (now deleted on failure), and an export stream leak on a read/backend failure in RepositoryExportDestination::copyFolder() (now closed in a finally). Deliberately left out: wiring per-user export into the automatic backup schedule (on-demand via occ only, for now) and retention/pruning of old user exports -- both open design questions, not yet decided. The lease covering a single export/import still has a fixed TTL (3600s) with no mid-call refresh, since UserMigrationService exposes no progress; a resumable, step-based per-user backup (like the main BackupService run) would remove that cap but is a bigger change than this pass.
55 lines
2.4 KiB
XML
55 lines
2.4 KiB
XML
<?xml version="1.0"?>
|
|
<!-- SPDX-FileCopyrightText: 2026 Dillard Blom -->
|
|
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
<psalm
|
|
errorLevel="2"
|
|
resolveFromConfigFile="true"
|
|
findUnusedBaselineEntry="true"
|
|
findUnusedCode="false"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns="https://getpsalm.org/schema/config"
|
|
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
|
errorBaseline="psalm-baseline.xml"
|
|
>
|
|
<projectFiles>
|
|
<directory name="lib" />
|
|
<ignoreFiles>
|
|
<directory name="vendor" />
|
|
</ignoreFiles>
|
|
</projectFiles>
|
|
|
|
<!-- Interface-only stubs for Nextcloud's public API, not autoloaded at runtime: scanned for
|
|
type resolution only, never reported on. lib/ must only use OCP\, never internal OC\
|
|
classes, since those differ between the supported Nextcloud versions (PLAN.md section 6). -->
|
|
<extraFiles>
|
|
<directory name="vendor/nextcloud/ocp/OCP" />
|
|
<directory name="vendor/nextcloud/ocp/NCU" />
|
|
</extraFiles>
|
|
|
|
<issueHandlers>
|
|
<UndefinedClass>
|
|
<errorLevel type="suppress">
|
|
<!-- Provided by files_external at runtime; not a composer dependency (PLAN.md 3.5). -->
|
|
<referencedClass name="OCA\Files_External\**" />
|
|
<!-- Provided by user_migration at runtime, an optional dependency (PLAN.md 3.8). -->
|
|
<referencedClass name="OCA\UserMigration\**" />
|
|
</errorLevel>
|
|
</UndefinedClass>
|
|
<UndefinedDocblockClass>
|
|
<errorLevel type="suppress">
|
|
<!-- Server::get()'s templated return type resolving the users_migration class-string
|
|
constant below; same reason as the UndefinedClass suppression above. -->
|
|
<referencedClass name="OCA\UserMigration\**" />
|
|
</errorLevel>
|
|
</UndefinedDocblockClass>
|
|
<!-- The #[\Override] attribute convention (PHP 8.3+) adds no behaviour; this app supports
|
|
PHP 8.1+, and applying it to every interface-implementing method project-wide is pure
|
|
ceremony with no bug-catching value, so it's suppressed rather than papered over. -->
|
|
<MissingOverrideAttribute>
|
|
<errorLevel type="suppress">
|
|
<directory name="lib" />
|
|
</errorLevel>
|
|
</MissingOverrideAttribute>
|
|
</issueHandlers>
|
|
</psalm>
|