- tests/integration/roundtrip.sh: key, kit code and confirmation,
location, full and unchanged incremental backup, restore new-folder and
replace (files identical, extra file in the trash), database dump and
restore, retention, trash and restore from trash, no files_external
errors in the log
- tests/integration/matrix.sh: fresh Nextcloud per version and database
(SQLite, MariaDB, PostgreSQL), removes only its own containers and
their volumes; locally green on NC32 and NC35 with all three databases
- GitHub Actions: unit tests on PHP 8.2-8.4, round trip on NC32-35 x
SQLite/MariaDB/PostgreSQL
Fixes found by the matrix:
- files_external is no longer booted when it is disabled: only its
classes are loaded and a private BackendService gets its backends from
its Application class (booting registered its mount provider, which
failed on installations where its tables were never created)
- SQLite: the database file is excluded from the file backup (it is
covered by the logical dump and copying the live file is inconsistent)
- occ backup:key:confirm accepts --code and --confirm for scripts
- DbDumper::dumpStep(): tables are dumped until the step's deadline in one
REPEATABLE READ transaction and resumed at the last primary key in the
next step; the running table checksum is carried over. A dump that fits
in one step is fully consistent, otherwise the snapshot records
dbConsistent=false. BackupService uses it, so the database phase no
longer needs one long request (hosted Nextcloud, web cron)
- restore inserts 100 rows per statement and leaves tables whose checksum
already matches the dump untouched
- spike C: 30-step dump gives the same checksums as the one-shot dump;
restore rewrote only the 3 damaged tables of 147; 300k-row table
restored in 14.6 s instead of 46 s
- detect files changing during reading or between resumable steps and
re-read them (size/mtime/ctime/inode), spike F overwrites mid-run
- drop pending blob ids when a pack or index upload fails
- dump and restore PostgreSQL sequence state exactly, bound parameters,
validated catalog names; spike C checks all sequences exactly
- keep invalid UTF-8 byte-exact (tagged base64)
- refuse large tables without a primary key
- StorageBackend: check write/flush/close results, validate base path
- user_migration import/export: root paths, intermediate folders,
partial writes, read errors
- spikes: never touch an existing account, own fixtures, 512M note
BackupRun is a resumable state machine: each step works until a deadline,
uploads its pack and tree segment, and returns the state; large files are
resumed at the next blob. Snapshot is written last.
Spike F: 200 MiB file resumed across steps in separate processes; with 6
random kill -9 the run completes and restores bit-identically.
spikes/RESULTS.md summarises phase 0 and the findings for the plan.
RepositoryExportDestination / RepositoryImportSource implement the
public OCP\UserMigration interfaces on top of the repository, so
user_migration's migrators (calendar, contacts, trashbin, account,
files) export straight into the encrypted repository.
Spike E (NC34): export 0.8s, unchanged second export stores nothing;
after deleting the user, import restores account, files (bit-identical),
calendar event and contact. Findings: calendars/address books come back
as 'migrated-*' with new object URIs; importing over an existing user
duplicates contacts and adds a migrated birthday calendar; creating the
user on import needs >128 MB (password_policy word list).
ExternalStorageFactory builds storages with files_external's own
backends and auth mechanisms from an unsaved StorageConfig (as its
ConfigAdapter does), and StorageBackend adapts any IStorage to IBackend
(writeStream, seekable range reads, temp name + rename except on S3).
IBackend::put() now owns and always closes the stream, because
files_external's S3 writeStream closes it.
Spike D (NC34): S3 (SeaweedFS) and SFTP: 120 MiB backup ~6s, unchanged
run uploads nothing, restore bit-identical, no files_external mount
created, no temp files (max 12 KiB in /tmp). Works with files_external
disabled when its classes are loaded. Backup peak memory 85-89 MiB on
these targets (to investigate).
DbDumper reads every prefixed table through the Nextcloud connection in
one REPEATABLE READ (read-only) transaction, with keyset pagination on
the primary key, and streams each table as JSON lines into its own blob
stream (BlobWriter), so unchanged tables are deduplicated. Restore
replaces table contents in one transaction and resets PostgreSQL
sequences.
Spike C (NC34, PostgreSQL, standard image without pg_dump/pgsql ext):
300k-row table with bytea: dump 6.3s / 50 MiB peak, 94 MiB JSON -> 20 MiB
stored; unchanged second dump stores nothing; restore makes every table
checksum-identical and all 110 sequences continue above the maximum.
Content-addressed repository on an IBackend (local implementation):
4 MiB blobs, deflate when it helps, each blob encrypted with its id as
associated data, ~32 MiB packs with an encrypted header, one index file
per pack, snapshots as encrypted JSON lines written last.
Spike B: 390 MiB tree, peak 54 MiB; unchanged run reads nothing; a 1 MiB
change in a 300 MiB file uploads one blob; full and partial restore are
bit-identical; tampering is detected.