# ng_backup — plan Een webgebaseerde, versleutelde backup- en restore-app voor Nextcloud 32–35, bedoeld voor MKB zonder eigen IT-afdeling en voor gehoste Nextcloud zonder SSH. occ-commando's onder `backup:`. Opgesteld 2026-10-01, na de evaluatie van nextcloud/backup op NC34 (zie onderaan). ## 1. Uitgangspunten 1. **Werkt zonder shell.** Geen `pg_dump`, `mysqldump`, rclone, rsync, tar of extra PHP-extensies. Alleen wat Nextcloud zelf meelevert (files_external-opslagklassen, AWS-SDK, phpseclib, sabre, Doctrine/DBAL, sodium). 2. **Werkt op elke variant:** AIO, losse installatie (Docker, bare metal, snap) én gehoste Nextcloud zonder SSH. Juist gehoste gebruikers hebben er het meeste aan; alles wat alleen met shell kan, is hooguit een optionele versnelling. 3. **Backuplocaties zijn tussentijds niet gekoppeld.** Geen mount in Files, geen open verbinding buiten een run; de locatie is voor gebruikers onzichtbaar. 4. **Restore is een hoofdfunctie, geen bijzaak.** Alles wat via occ kan, kan ook via de webinterface. 5. **Nooit een instantie achterlaten in onderhoudsmodus.** Backups draaien zonder onderhoudsmodus; waar die toch nodig is (volledige restore), altijd met gegarandeerd herstel. 6. **Hervatbaar.** Gehoste omgevingen hebben korte PHP-tijdslimieten en 512 MB geheugen: elk werk gebeurt in kleine, hervatbare stappen met checkpoints. Geheugengebruik onafhankelijk van datagrootte. 7. **Streamend van bron naar doel, zonder lokale tussenkopie.** Lezen → opdelen → comprimeren → versleutelen → uploaden gebeurt als één doorlopende stroom; restore andersom (downloaden → ontsleutelen → uitpakken → wegschrijven). Er is nooit een lokaal backupbestand of een tweede kopie van de data nodig; de lokale schijf hoeft hooguit ruimte te hebben voor één pack in aanbouw (~32 MB, instelbaar), en ook dat bij voorkeur in het geheugen. 8. **Alleen wat nodig is.** Data, database en config, geen Nextcloud-code en geen apps uit de store (die zijn opnieuw te installeren; de lijst met apps en versies gaat wel mee). 9. **Aantoonbaar betrouwbaar.** Elke release draait in CI een volledige backup→restore-rondgang op NC32–35 × SQLite/MySQL/PostgreSQL. ## 2. Wat de app doet (functioneel) | Functie | Fase | |---|---| | Geplande backups (dagelijks/wekelijks, tijdvenster) en handmatig starten | 1 | | Versleuteling, altijd aan, met herstelsleutel om te downloaden | 1 | | Opslag via files_external (S3, SFTP, WebDAV, lokaal pad), aangestuurd door ng_backup, zonder koppeling of share | 1 | | FTP(S), SMB, Swift; NFS/rclone via gateway (zie 3.5) | 2 | | Append-only-modus en advies voor object lock (bescherming tegen ransomware) | 1 | | Restore van één bestand of map, per gebruiker, naar elk herstelpunt | 1 | | Backup en restore per gebruiker via user_migration (bestanden, agenda, contacten, Deck, Mail-instellingen, profiel, prullenbak, ...) | 2 | | Volledige restore (disaster recovery) op een verse installatie | 2 | | Volledige restore in-place op de bestaande instantie | 3 (onderzoeken) | | Bewaarbeleid (bijv. 7 dagelijks, 4 wekelijks, 6 maandelijks) | 1 | | Verificatie: periodiek checksums controleren, steekproef-restore | 2 | | Meldingen bij mislukken of te oude backup (Nextcloud-notificatie + e-mail), setup-check in Beheer | 1 | | Differentiële/incrementele backups met deduplicatie | 1 (zit in het formaat) | ## 3. Architectuur ### 3.1 Backupformaat Content-addressed opslag, vergelijkbaar met restic/borg, zodat incrementeel en deduplicatie vanzelf gaan: - **Blobs:** bestandsinhoud in stukken van max. 4–8 MB, geïdentificeerd met een hash (BLAKE2b via sodium) van de *onversleutelde* inhoud met een geheime sleutel (keyed hash, zodat de hash niets verraadt). Een blob die al op de opslag staat, wordt niet opnieuw geüpload. - **Packs:** blobs worden gebundeld tot packbestanden van ~32 MB (minder kleine objecten op S3/SFTP). - **Snapshot-manifest:** per herstelpunt een versleuteld manifest: tijdstip, NC-versie, app-lijst met versies, databasetype, en per gebruiker/map de boom met bestanden → blobs, plus de database-dump. - **Index:** welke blob in welk pack zit, zodat restore van één bestand alleen de nodige packs ophaalt. **Datastroom zonder tussenopslag:** - *Backup:* bestand lezen als stream → blobs van 4–8 MB → elke blob gecomprimeerd (zstd als beschikbaar, anders gzip via zlib) en versleuteld in het geheugen → toegevoegd aan het pack in aanbouw. Een vol pack (~32 MB) wordt direct geüpload (bij S3 als multipart-onderdeel, bij SFTP/WebDAV als stream) en daarna uit het geheugen verwijderd. Piekgebruik: één blob + één pack, ongeacht de grootte van de backup. - *Database:* de dump gaat tabel voor tabel als stream door dezelfde pijplijn; er komt nooit een dumpbestand op schijf. - *Restore:* alleen de benodigde packs worden opgehaald, per blob ontsleuteld en gecontroleerd (AEAD) en direct naar het doelbestand geschreven. Een groot bestand terugzetten kost dus geen dubbele ruimte; een half teruggezet bestand wordt eerst als tijdelijk bestand naast het doel geschreven en pas na volledige controle op zijn plek gezet (alleen dát ene bestand dubbel). - *Volledige restore van de database:* rijen worden in batches uit de stroom geïmporteerd. - Als het geheugen te krap is voor een pack in aanbouw, kan dat naar een tijdelijk bestand (instelbaar), maar nooit meer dan één pack tegelijk. Waarom niet zip/tar per backup: daarmee is één bestand of één gebruiker terugzetten traag (alles uitpakken) en kost elke backup weer de volle ruimte. Dat was precies het probleem bij nextcloud/backup. ### 3.2 Versleuteling - libsodium **secretstream (XChaCha20-Poly1305)**: streamend, dus het geheugen blijft klein, ongeacht de grootte. - Een willekeurige **hoofdsleutel** per backup-repository; die wordt versleuteld opgeslagen met een sleutel afgeleid van een **wachtwoordzin** (Argon2id, `sodium_crypto_pwhash`). - **Herstelkit** (wachtwoordzin-hint, versleutelde hoofdsleutel, repository-ID; als tekst/PDF + QR, te downloaden bij inrichten). Zonder die kit is een backup na verlies van de server onbruikbaar. - **Verplichte bevestiging door de beheerder** voordat de eerste backup draait (en opnieuw bij een nieuwe sleutel): 1. de kit downloaden (de knop verder blijft uit tot de download is gestart); 2. een vinkje: *"Ik heb de herstelkit gedownload en veilig opgeslagen, buiten deze server. Ik begrijp dat het zorgvuldig bewaren van deze sleutels mijn eigen verantwoordelijkheid is en dat backups zonder deze sleutels niet te herstellen zijn."*; 3. pas dan wordt de knop *"Yes, I confirm"* actief; klikken bevestigt en start de inrichting. Wie, wanneer en welke sleutelversie bevestigd is, wordt vastgelegd (audit log + instellingenpagina). - De herstelkit kan op verzoek ook versleuteld naar het e-mailadres van de beheerder worden gestuurd, als extra kopie; dit vervangt de bevestiging niet. - De sleutel staat **niet onversleuteld in de database**; de database gaat zelf ook mee in de backup. - Integriteit: elk object is geauthenticeerd (AEAD); manifesten verwijzen met hashes naar hun inhoud. ### 3.3 Database-dump zonder externe tools - **Schema:** via Nextcloud's eigen `SchemaEncoder`/Doctrine-schema-export (zoals `occ db:export-schema`). - **Data:** per tabel in batches (bijv. 5.000 rijen, gesorteerd op primaire sleutel) via `IDBConnection`, weggeschreven als regels JSON met type-informatie, gestreamd door de versleuteling heen. Dit is hetzelfde principe als `occ db:convert-type`, dat ook rijen tussen databases kopieert. - **Consistentie:** de dump draait binnen één transactie met `REPEATABLE READ` (PostgreSQL/MySQL), zodat de snapshot consistent is zonder onderhoudsmodus. Voor SQLite: een kopie van het bestand. - **Restore:** werkt ook naar een ander databasetype (bijv. van MySQL naar PostgreSQL bij verhuizen). - Grote tabellen die opnieuw op te bouwen zijn (bijv. `filecache` van externe opslag, previews, zoekindexen) zijn instelbaar uit te sluiten. ### 3.4 Bestanden - Gelezen via de Nextcloud Files-API / opslaglaag (zodat ook object storage als primaire opslag werkt), niet rechtstreeks uit de datamap. - Bron van wijzigingen: `filecache` (mtime, etag, size), zodat een incrementele run niet alle bestanden opnieuw hoeft te lezen. - Externe opslag gaat standaard **niet** mee (instelbaar per koppeling). - Server-side encryptie van Nextcloud: bestanden worden ontsleuteld gelezen en in de backup opnieuw versleuteld; documenteren dat de master key dan ook in de herstelkit moet. ### 3.5 Opslagdoelen **files_external is een vereiste; ng_backup stuurt het aan, zonder koppeling.** De meegeleverde app files_external heeft alle verbindingscode al: backends voor S3, SFTP, FTP, SMB, Swift en WebDAV, met hun inlogmethoden (wachtwoord, SFTP-sleutel, enz.) en de beschrijving van hun instellingenvelden. ng_backup bouwt daar niets van na, maar gebruikt files_external zoals het zichzelf gebruikt (in NC34 nagekeken): 1. `BackendService::getAvailableBackends()` levert de beschikbare backends en hun parameters; het instellingenformulier van ng_backup wordt daaruit **automatisch opgebouwd** (zelfde velden en validatie als in files_external, ook voor backends die later bijkomen). 2. Bij een run maakt ng_backup een **niet-opgeslagen** `StorageConfig`, laat backend en inlogmethode hun `manipulateStorageConfig()` doen, en maakt de opslag aan met `new $storageClass($options)`, precies zoals `ConfigAdapter::constructStorage()` dat doet. 3. Na de run wordt het object vrijgegeven; er blijft geen verbinding open. **Waarom geen (verborgen) koppeling in files_external:** een globale koppeling zonder toegewezen gebruikers of groepen geldt in files_external juist voor **alle** gebruikers. Een verborgen koppeling zou een aparte groep of systeemgebruiker vereisen; één verkeerde groepswijziging en de backups zijn zichtbaar en wijzigbaar in Files. Met de werkwijze hierboven bestaat de backuplocatie nergens als koppeling of share: - niet zichtbaar in Files, niet gescand, niet deelbaar, ook niet voor beheerders; - geen verbinding buiten een run ("tussentijds niet gemount"); - instellingen en inloggegevens in de eigen tabellen van ng_backup, versleuteld met `ICrypto`. **Installatie en instellingen van files_external:** - files_external is een meegeleverde app (`core/shipped.json`): aanwezig in elke reguliere installatie, standaard **uitgeschakeld**. ng_backup **verandert de status niet**: staat het uit, dan blijft het uit en laadt ng_backup alleen de klassen (in fase 0 getest: werkt); staat het aan omdat de beheerder het zelf gebruikt, dan blijft het aan. Uit is hier juist goed: onzichtbaarheid van de backuplocatie beperkt wat ransomware of een overgenomen account kan zien. Laden van een uitgeschakelde app gaat via interne API → in de adapter, met CI per NC-versie. Ontbreekt files_external helemaal (aangepaste installatie), dan alleen de eigen doelen. - Gebruikers hoeven files_external niet te zien: "gebruikers mogen externe opslag koppelen" kan uit blijven en er hoeven geen koppelingen te bestaan. ng_backup controleert dat en waarschuwt als gebruikers zelf externe opslag mogen koppelen (geen vereiste, wel een tip). - Dunne eigen adapter (`IBackend`: put/get/list/delete/stat met streams) bovenop de opslagobjecten van files_external, omdat dit interne klassen zijn: wijzigingen tussen NC-versies op één plek opvangen, met CI op elke versie. Een eigen implementatie alleen waar files_external tekortschiet (bijv. S3 multipart met object lock). | Doel | Via | Opmerking | |---|---|---| | S3 en compatibel (incl. MinIO, Wasabi, Hetzner Object Storage) | files_external `AmazonS3` of eigen adapter op `aws/aws-sdk-php` (core) | voorkeursdoel, object lock mogelijk | | SFTP | files_external `SFTP` (phpseclib) | sleutel of wachtwoord | | FTP(S) | files_external `FTP` | | | SMB/CIFS | files_external `SMB` (icewind/smb) | vereist libsmbclient of `smbclient` op de server; op hosted vaak niet beschikbaar → melden bij instellen | | WebDAV (ook een andere Nextcloud) | core DAV-opslag | | | Swift | files_external `Swift` | | | NFS | alleen als lokaal pad | NFS kan PHP niet zelf aanspreken; vereist een koppeling op OS-niveau, wat botst met "tussentijds niet gemount". Advies: NFS-doel via een SFTP/S3-gateway, of een systemd-automount die de beheerder buiten de app regelt. Op hosted niet mogelijk. | | rclone | niet rechtstreeks | rclone is een los programma (shell nodig). Alternatief: `rclone serve sftp/webdav/s3` op de doelmachine en ng_backup daarheen laten schrijven; daarmee komen alle rclone-doelen beschikbaar zonder binary op de Nextcloud-server. | **Bescherming van de backups zelf (ransomware, gehackte server):** de Nextcloud-server heeft noodzakelijk schrijfrechten op het doel; wie de server overneemt, kan dus ook backups wissen. Daarom: - aanbevolen inrichting per doel documenteren: S3 met **versioning + object lock** en een sleutel zonder delete-recht; SFTP met een chroot-gebruiker zonder shell; bij voorkeur een doel dat niet op hetzelfde netwerk staat; - **append-only-modus** als optie: ng_backup verwijdert dan zelf niets; opruimen (bewaarbeleid) gebeurt via een levenscyclusregel op het doel of via een aparte, handmatig gestarte run met andere inloggegevens; - meerdere doelen tegelijk (3-2-1), elk met eigen inloggegevens. ### 3.6 Uitvoering: hervatbare jobs - Eén `TimedJob` start een run binnen het ingestelde tijdvenster; het werk zelf is een **toestandsmachine** (dump DB → verzamel bestanden → upload → manifest → opruimen) waarvan elke stap in delen van ~20–30 seconden draait en zijn voortgang opslaat (eigen tabel `ng_backup_runs`). - Werkt met system cron, webcron én AJAX-cron (dan trager, wel betrouwbaar). Bij system cron kan `occ backup:run` ook in één keer doorlopen. - **Lock** per repository (`ILockingProvider` + heartbeat), zodat twee runs nooit tegelijk schrijven; een verlopen lock (crash) wordt na een time-out netjes vrijgegeven. - Een afgebroken run wordt de volgende keer **hervat**, niet opnieuw begonnen. ### 3.7 Onderhoudsmodus en volledige restore - **Backup:** nooit onderhoudsmodus (consistentie via DB-transactie, zie 3.3). - **Volledige restore, fase 2 (disaster recovery):** op een **verse** Nextcloud met dezelfde hoofdversie: ng_backup installeren → herstelkit invoeren → opslagdoel koppelen → herstelpunt kiezen → restore. Database wordt vervangen, bestanden teruggezet, de app-lijst wordt getoond om apps opnieuw te installeren. Hier is onderhoudsmodus niet nodig omdat er nog niets in gebruik is. - **Volledige restore in-place, fase 3:** lastig, omdat de app dan de database vervangt waarop hij zelf draait en de webinterface in onderhoudsmodus niet bereikbaar is. Te onderzoeken: een eigen "restore-modus" (middleware die alleen de beheerder en de restore-endpoints doorlaat) in plaats van onderhoudsmodus. Altijd met `try/finally` en een watchdog die de modus na een time-out opheft. ### 3.8 Backup en restore per gebruiker: via user_migration In plaats van zelf te bepalen welke gegevens van een gebruiker meegaan, hergebruiken we **user_migration** (officiële Nextcloud-app, actief onderhouden, NC29–36; draait ook op onze eigen productie-instantie). Die app werkt met *migrators* die andere apps zelf aanleveren. Op onze productie-instantie zijn dat er al: calendar, dav (agenda's en contactpersonen), deck, files_trashbin, mail, settings (profiel, account, app-instellingen) en de bestanden zelf. Elke app die later een migrator toevoegt, gaat automatisch mee. **Koppeling:** user_migration schrijft normaal een zip naar de map van de gebruiker, via de publieke interface `OCP\UserMigration\IExportDestination`. ng_backup levert een **eigen `IExportDestination`** die alles direct in de versleutelde, ontdubbelde backupstroom zet (geen zip, geen kopie in de map van de gebruiker, geen dubbele ruimte), en een **eigen `IImportSource`** die bij restore uit een herstelpunt leest. Het aansturen gaat via `UserMigrationService::export()` / `import()` van user_migration. - `copyFolder()` krijgt een `Folder` mee: daar gebruiken we etag/mtime uit de filecache, zodat ongewijzigde bestanden niet opnieuw gelezen of gehasht worden (incrementeel blijft goedkoop). - Dezelfde migrators zorgen dat een gebruiker ook naar **een andere Nextcloud** kan worden teruggezet (verhuizen), omdat het formaat app-onafhankelijk is. - Een gebruikersbackup en de volledige backup delen dezelfde blobs; dubbel opslaan kost niets. **Afhankelijkheid en risico's (uitzoeken in fase 0):** - `UserMigrationService` is een interne klasse van user_migration (geen publieke API); net als bij files_external via een eigen adapter, met CI op elke NC-versie. Zonder user_migration valt alleen de gebruikersbackup weg; de volledige backup werkt dan nog. - **Restore over een bestaande gebruiker** is in fase 0 getest en niet schoon (contacten dubbel, extra `migrated-*`-agenda's). Daarom twee keuzes in de restore-wizard, nooit importeren over een bestaand account heen: 1. **Vervangen:** eerst automatisch een veiligheidsexport van de huidige stand naar de repository, dan het account verwijderen, dan uit de backup opnieuw aanmaken. 2. **Naast het bestaande account:** de backup terugzetten in een nieuw account `-bak` (of `-`), zodat de beheerder kan vergelijken en gericht overzetten. (Een gebruikers-id hernoemen kan Nextcloud niet; daarom krijgt de *backup* de nieuwe naam, niet het bestaande account.) - Agenda's en adresboeken komen terug als `migrated-*` met nieuwe URI's: clients synchroniseren opnieuw, delen van die agenda's moeten opnieuw. In de wizard vermelden. - **Versies en prullenbak gaan mee:** de FilesMigrator exporteert ook `files_versions` van de gebruiker, de trashbin-migrator de prullenbak. Alleen bestanden in de eigen opslag van de gebruiker; externe opslag en groepsmappen niet (die gaan wel mee in de volledige backup). - Een terugzetting per bestand of map blijft via de eigen bestandsboom gaan (sneller, geen migrators nodig). Niet alle apps hebben een migrator (bijv. Talk-gesprekken); in de restore-wizard tonen welke onderdelen wel en niet terugkomen, op basis van de geregistreerde migrators. ## 4. Webinterface - **Beheer → Backup:** status (laatste geslaagde backup, volgende run, opslaggebruik), opslagdoelen, schema, bewaarbeleid, uitsluitingen, sleutelbeheer (herstelkit opnieuw downloaden na wachtwoordcontrole). - **Herstelpunten:** lijst met datum, grootte, status van verificatie; per herstelpunt bladeren door gebruikers en mappen, bestanden of mappen terugzetten (naar de oorspronkelijke plek of naar een map "Teruggezet "). - **Restore-wizard** voor één gebruiker en (fase 2) voor een volledige restore op een verse installatie. - **Voortgang** van lopende runs, met logboek; bij fouten een duidelijke melding en wat te doen. - **Gebruikers zelf** (optioneel, fase 3): eigen bestanden terugzetten vanuit de Files-app, zoals bij versies. ## 5. occ-commando's (`backup:`) | Commando | Doel | |---|---| | `backup:status` | laatste runs, status per opslagdoel | | `backup:run [--target=] [--full]` | backup nu draaien (doorlopend, voor system cron/handmatig) | | `backup:list [--target=]` | herstelpunten | | `backup:verify [--deep]` | checksums controleren, optioneel alles ontsleutelen | | `backup:restore:file [--to=]` | bestand/map terugzetten | | `backup:user:restore [--to-user=]` | gebruiker terugzetten via user_migration (fase 2) | | `backup:restore:full ` | volledige restore (fase 2, verse installatie) | | `backup:target:add/list/remove/test` | opslagdoelen beheren en testen | | `backup:key:export / import` | herstelkit | | `backup:prune [--dry-run]` | bewaarbeleid toepassen | **Let op:** nextcloud/backup gebruikt ook het voorvoegsel `backup:` (o.a. `backup:point:*`). Als beide apps tegelijk aan staan, botsen commando's met dezelfde naam. Onze namen overlappen niet met de hunne, maar bij installatie waarschuwen als `backup` ook actief is. ## 6. Kwaliteit en tests - PHPUnit-unittests voor formaat, versleuteling, chunking, toestandsmachine, bewaarbeleid. - Integratietests met echte databases: dump→restore→vergelijk (rij-voor-rij) op SQLite, MySQL/MariaDB, PostgreSQL. - **End-to-end in CI:** NC32–35 opzetten, testdata (gebruikers, bestanden, agenda, contacten), backup naar MinIO (S3) en SFTP-container, data beschadigen, restore, alles vergelijken. - Foutinjectie: crash halverwege een run/restore → instantie nooit in onderhoudsmodus, run hervat. - Geheugen- en schijftest: backup én restore van 20 GB data binnen `memory_limit=256M` op een machine met minder dan 1 GB vrije tijdelijke ruimte (tmpfs met limiet); meten dat er nooit meer dan één pack lokaal staat. - Psalm (niveau streng) tegen `nextcloud/ocp` van elke ondersteunde versie; geen private `OC\`-klassen. - Code review per PR, zoals bij occweb. ## 7. Fasering en inschatting | Fase | Inhoud | Indicatie | |---|---|---| | 0 | Onderzoek/prototype: formaat, secretstream, DB-dump via DBAL, S3/SFTP via files_external-klassen, eigen IExportDestination voor user_migration; meten op hosted-achtige limieten | 1–2 weken | | 1 (MVP) | Geplande, versleutelde, incrementele backups naar lokaal/S3/SFTP/WebDAV; restore van bestanden/mappen via web en occ; bewaarbeleid; meldingen; CI-rondgang | 4–6 weken | | 2 | Backup/restore per gebruiker via user_migration, volledige restore op verse installatie, FTP/SMB, verificatie | 3–4 weken | | 3 | In-place volledige restore (restore-modus), zelfbediening voor gebruikers, extra doelen | open | Release in de app store pas na fase 1 en een periode van eigen gebruik op onze productie-instantie (net als occweb): een backup-app die niet betrouwbaar is, is erger dan geen backup-app. ## 8. Besluiten (2026-10-01) 1. **Doelgroep:** alle varianten: AIO, losse installaties én gehoste Nextcloud zonder SSH. 2. **Licentie:** AGPL-3.0-or-later. 3. **Repository:** eigen Gitea + GitHub (dillardblom), zoals occweb. 4. **Naam:** "NG Backup", app-id `ng_backup`, occ-voorvoegsel `backup:`. 5. **Herstelkit:** verplicht downloaden + expliciete bevestiging door de beheerder (vinkje + "Yes, I confirm"), zie 3.2; versleuteld per e-mail als optionele extra kopie. 6. **Opslag:** files_external wordt door ng_backup aangestuurd zonder koppeling of (verborgen) share; de app-status blijft zoals de beheerder hem had (uit blijft uit), zie 3.5. 7. **Restore per gebruiker:** nooit over een bestaand account heen; óf vervangen (veiligheids- export, verwijderen, opnieuw aanmaken), óf terugzetten als `-bak`, zie 3.8. 8. **Fase 0 afgerond (2026-10-01),** resultaten in `spikes/RESULTS.md`. ## 9. Releases en versies (besluit 2026-10-02) | Versie | Betekenis | |---|---| | 0.1.0-alpha.N | publiek; meetesten gewaardeerd op een kopie van productie (dev, test, staging) | | 0.9.0-beta.N | mag meedraaien op productie, naast de reguliere backup; volledige restore aanwezig en getest | | 1.0.0-rc.N | releasekandidaten, alleen bugfixes | | 1.0.0 | stabiel: vervanging van de reguliere backup, noodherstel bewezen | | 1.2.0 e.v. | functies van fase 2 (restore per gebruiker, FTP/SMB, verificatie, ...) | Volgorde: fase 1 grondig testen, reviewen en debuggen; dan nadenken over 0.1.0-alpha.1. Een minimale volledige restore (`occ backup:restore:full` op een verse installatie) komt vóór de beta. **Roadmap-indeling fase 2 (besluit 2026-10-05):** naar v1.2.0 toewerken met in fase 2: 1. `backup:restore:full` (disaster recovery, verse installatie) — **klaar** (2026-10-05) 2. FTP/SMB-ondersteuning — **klaar** (2026-10-06): beide liepen al via files_external's generieke adapter zonder enige ng_backup-code; alleen testdekking toegevoegd (`tests/integration/targets-matrix.sh`) + bevindingen in README ("Storage backend experiences"). SMB vereist de `smbclient` PECL-extensie (niet standaard in de Nextcloud-image, in CI nu on-the-fly gebouwd). NFS is geen eigen backend: lokaal mounten + `local`-backend erop richten. 3. Verificatie/checksum-audit-commando — **klaar** (2026-10-05) 4. Retentie/prune voor user-exports — **klaar** (2026-10-06): `occ backup:retention --user-last=N` (standaard 3, per installatie, net als de bestaande snapshot-policy), toegepast door `occ backup:prune` (geen prullenbak voor user-exports, in tegenstelling tot snapshots — on-demand, niet het primaire vangnet). 5. Automatische user-export in het reguliere schema — **naar backlog** (niet voor deze release; on-demand blijft) 9. SMB-roundtrip (protocol, niet een specifieke server) — **klaar** (2026-10-06): CI-matrix, 2 GB-roundtrip en user-export via SMB; NFS via de OMV-share getest (`local`-backend op een gemounte share, 2026-10-06) 10. `backup:user:restore` zonder user_migration: vraagt om toestemming om de app te installeren en te activeren (of `--install-user-migration`) — **klaar** (2026-10-06); waarschuwing over de settings-allowlist — **klaar**; agenda/contacten bij migratie naar een server zonder die gebruiker — **getest** (2026-10-06, naar `migrated-personal`/`migrated-contacts`) Backlog: (a) de originele weergavenaam herstellen (het label uit de export is niet getest als weergavenaam); (b) de interne `migrated-`-naam hernoemen (vereist directe databasewijziging, niet voor de beta). **Backlog uit de pre-beta review (2026-10-07)** — geen belemmering voor 0.9.0-beta of 1.0.0, staat in README "Known limitations": 1. Geheugen bij grote instanties (>1 TB, miljoenen bestanden): blob-index, prune-set, browse van een snapshot in de web-UI, `trashExtras` bij restore en het manifest van een user-export staan volledig in het geheugen; streaming/paginering nodig. 2. Twee servers op één locatie: catalogus kan breken (generatie overschreven, of anchor >200 generaties achter). Docs-waarschuwing staat er; mogelijk later een echte put-if-absent per backend. 3. Sleutelrotatie: een slot verwijderen trekt geen toegang in (master-key blijft gelijk). 4. Rollback vóór DR niet detecteerbaar: referentie-generatie en -hash in de kit opnemen of tonen. 5. Kit-velden buiten `wrapped_key` (o.a. `delete_delay_days`) zijn niet geauthenticeerd; nu alleen getoond. 6. Repository-id in de AD van objecten (formaatwijziging). 7. Wachtwoorden van locaties via `--option-file` of omgevingsvariabele i.p.v. de commandoregel. 8. Tijdelijke restore-bestanden niet via de Files-API verwijderen (komen nu in de prullenbak). 9. DB-restore in foreign-key-volgorde (cascades). 10. Packs zonder index (index-upload mislukt) opruimen. 11. `backup:restore:full` in onderhoudsmodus en hervatbaar maken; nu: opnieuw draaien met `--force`. 12. Config/rename op FTP/SMB-servers die geen rename over een bestaand bestand toestaan. **Plan richting 1.0.0 en 1.1.0 (besluit 2026-10-15/16):** - **1.0.0-rc.1:** wat nu op `polish` staat (snelle S3-verify en snapshot-commit, kortere runlijst, duur met dagen, job bij elke cron-run, deflate-test, feedbackformulier, datums in de locale van de gebruiker, wachtwoordzin min. 16 tekens). Geen nieuwe functies meer. - **Vóór 1.0.0 (verplicht):** volledige restore met kit op een tweede server (PVE02), alleen lezend of op een kopie van de locatie; plus een user-restore. Zonder geslaagde test geen 1.0.0. - **1.0.0 (besluit 2026-10-18):** alleen betrouwbaarheid. rc2 uitgebracht; slaagt de grote DR-test op VM 980 (snapshot van 12-10, met `--skip-missing-tables`), dan 1.0.0. Zo niet: fix, rc3, en rc3 pas uitbrengen na een geslaagde volledige DR-test. - **rc4 (besluit 2026-10-19):** rc3 is niet uitgebracht. De DR-test met rc3 liep ~2,8 MiB/s (~3,5 dag); parallel lezen over SFTP haalde vanaf VM 980 ~7,6 MiB/s. Daarom rc4 = rc3 + vooruitlezen over SFTP (4 eigen verbindingen). Volgorde: volledige DR-test op VM 980 volgens de gids (SFTP, Storage Box) → rc4 uitbrengen en om tests vragen → zelf een volledige DR van een productie-instantie van ~250 GB via S3 en via SMB. Drie volledige DR-restores vóór 1.0.0. Problemen of meldingen van gebruikers: rc5. - **1.0.1:** 1. (Naar rc4: herstelsnelheid over SFTP.) S3 leest al 16 tegelijk bij index en tree, maar een restore leest de packs nog één voor één: ook vooruitlezen via de S3-client. 2. Achtergrondjobs zonder systeem-cron (issue #13; besluit 2026-10-19). De PR die dit oplost zegt "Fixes #13" (bevestigd: #13 was de cron; besluit 2026-10-21): a. **Waarschuwen** in de beheerpagina en `backup:status`: modus uit `core backgroundjobs_mode` (`ajax`/`webcron`/`cron`) en de laatste run uit `core lastcron`. Niet op `cron`, of de laatste run te lang geleden: melding met de cron-regel erbij ("Backups only move while someone has Nextcloud open"), en "waiting for background jobs" als een lopende backup lang geen stap zette. b. **De pagina laat de backup lopen** zolang ze openstaat: in `ajax`/`webcron` voert de statusaanroep die de pagina toch al herhaalt ook een korte, begrensde stap uit (zoals de korte stappen die AJAX-cron nu al krijgt). "Back up now" werkt dan ook zonder cron zolang het tabblad openstaat; de melding zegt dat geplande backups echte cron nodig hebben. De melding zegt (zonder werkende systeem-cron): backups hebben systeem-cron nodig, ook de gewone nachtelijke; zo stel je cron in (link naar de Nextcloud-documentatie over achtergrondjobs voor de versie van de server, nu https://docs.nextcloud.com/server/34/admin_manual/configuration_server/background_jobs_configuration.html, versienummer uit de server, niet vast); zolang er geen cron is, loopt een backup die je met de knop start alleen door terwijl de NG Backup-pagina openstaat. c. **Voortgang in de databasefase** (issue #13): nu staat er alleen "running (db)", wat er hetzelfde uitziet als vastzitten. Toon "running (db: 47/183 tables)" zoals de bestandsfase een percentage toont, en "waiting for background jobs (last ran 12 min ago)" als een run een tijd geen stap zette, ook bij een cron die is blijven staan. Niet doen: de cron-modus vanuit de app omzetten (zonder echte cron-regel stopt alles, en `cron.php` zet de modus zelf op `cron`), en geen `exec php occ backup:run &` vanuit de knop (vaak uitgeschakeld, niet op hosted, buiten Nextcloud om lastig te volgen en te stoppen). 3. Volledige DR-test met een backup die al de tabelstructuur bevat (gemaakt met rc2 of later), zonder `--skip-missing-tables`: dat pad is op 1 TB nog niet getest. 4. **Label bij "Back up now"** (besluit 2026-10-19): een optioneel tekstveld naast de knop; de tekst gaat als label van de snapshot mee (nu kan dat alleen met `occ backup:run -l`; de knop stuurt een leeg label, geplande runs krijgen "scheduled"). Raakt alleen de UI en een bestaande parameter. 5. **`--salvage`** voor `backup:restore` en `backup:restore:full` (besluit 2026-10-22): terughalen wat kan, een bestand dat niet te lezen of te ontsleutelen is overslaan en aan het eind opsommen, in plaats van stoppen. 6. **Tijdelijke bestanden van een afgebroken restore opruimen** (gevonden in de SMB-DR van rc6, 2026-10-24): de rc5-poging die stopte liet `VID_0009.mp4.ngb-restore-` staan; de geslaagde restore daarna haalde het niet weg, en `files:scan` maakte het zichtbaar in Files. Een restore ruimt `*.ngb-restore-*` in de mappen die hij terugzet op. 7. **Eén bestand terugzetten met `backup:restore`**: een pad naar een bestand zet 0 bestanden terug (er komt een `/` achter); nu werkt alleen een map. 8. **Uitleg (i) bij de prullenbak van herstelpunten** (besluit 2026-10-25): bij "removed by retention policy" een infoknop met wat het betekent: het bewaarbeleid houdt voor oudere dagen één herstelpunt per dag (het laatste), weken en maanden; de rest gaat naar de prullenbak en wordt pas na de ingestelde wachttijd (`delete_delay_days`, standaard 30) echt verwijderd, en is tot die datum terug te zetten. Aanleiding: de beheerder zag "19-10 13:12 – after rc4 upgrade removed by retention policy" en dacht dat er iets was weggegooid. 9. **Tijdzone in `occ backup:status`**: de tabel toont de servertijd (UTC), de webinterface de tijd van de beheerder; dat leest als een verkeerde starttijd. Zet de zone in de kolomkop ("started (UTC)") of toon de tijden in de zone van het schema. 10. **User-restore op gebruikersnaam** (issue #22, 2026-10-27): `backup:user:restore New_User` gaf alleen "is not a recorded user export"; het argument moet het manifestpad zijn, dat alleen `backup:user:backup` eenmalig toonde. Nu: `backup:user:list` toont de exports, een kale uid werkt bij precies één export, anders een lijst om uit te kiezen — **klaar** (2026-10-27). 11. **Tabblad "Docs" in de beheerpagina** (besluit 2026-10-27): een derde knop naast "Backup and restore" en "Settings", met links naar de HOWTO (`docs/HOWTO.md`), de DR-gids (`docs/DISASTER-RECOVERY.md`) en de roadmap (README, `#roadmap`) op GitHub, elk met een regel wat erin staat. Nu zijn ze alleen via de README te vinden. Zichtbaar ook zonder sleutel, zodat de gids vindbaar is vóór de eerste installatie-stap. "NG Backup is in beta" gaat uit de feedbackregel — **klaar** (2026-10-27). - **DR-tests over databasegrenzen** (besluit 2026-10-20; productie nc01 draait PostgreSQL): na de MariaDB→PostgreSQL-fout (tellers, rc5) elke overstap in de integratietest (`to-pgsql`, `to-mysql`, `to-sqlite`, ook in CI). Volle DR-tests met echte data: vóór 1.0.0 de S3-simulatie (MariaDB-bron, 138 GB) nogmaals naar een MariaDB-doel en de SMB-simulatie naar een SQLite-doel; **na 1.0.0** de 1 TB van nc01 (PostgreSQL) naar een MariaDB-doel. - **Polish:** `target:add` toonde bij het eerste indexbestand al "about 642 min left" (S3, 4.568 bestanden, daarna binnen een minuut klaar): een schatting pas na ~5% of na meer dan een paar bestanden. - **1.1.0:** 1. `backup:key:import` zonder kit: de sleutel-slots van de locatie (`config`) lezen, met de kit als reserve als `config` ontbreekt of niet opent. Vergt een andere volgorde van `target:add` en sleutelimport (het gevoeligste pad), daarom niet in rc1. Daarna de DR-test op PVE02 herhalen zonder kit. 2. Instelling "zware achtergrondjobs pauzeren tijdens een backup" (standaard aan, uit te zetten): Memories, files_antivirus BackgroundScanner, Recognize, Preview Generator, fulltextsearch. Via de reservering/last_run van die jobs (geen apps uitzetten), crashbestendig, maximaal ~6 uur achter elkaar, zichtbaar in UI en log. Op nc01 met de hand getest tijdens SMB-run 10. 3. Snelheid: lezen en uploaden laten overlappen, meerdere bestanden tegelijk lezen, eventueel een eigen worker (`occ background-job:worker`). Gemeten op nc01: 55% wachten op de schijf, 33% op de SMB-upload, ~7% CPU. Vergt extra PHP-processen; botst mogelijk met uitgangspunt 1. 4. **Schema per locatie** (besluit 2026-10-19): in de instellingen van een locatie "uit", "dagelijks om HH:MM" of "elke N uur (vanaf HH:MM)". Nu is er één `schedule_time` voor alle locaties, één keer per dag. Bij de update wordt de huidige tijd het schema van elke bestaande locatie (voor bestaande gebruikers verandert er niets). 5. **Locaties na elkaar laten starten** in plaats van tegelijk: twee runs die dezelfde schijven lezen zijn samen niet sneller (gezien op nc01); wachtende runs zichtbaar als "queued". 7. **SMB sneller** (gemeten 2026-10-21): lokaal netwerk zonder vertraging (Samba-container op de laptop, 1,8 GB) back-up 24,9 MB/s, restore 27,6 MiB/s; vanaf Canada naar de Storage Box (130 ms) 2,6 MiB/s, omdat SMB per blok op het antwoord wacht. Grotere schrijfblokken, en vooruitlezen bij een restore zoals SFTP (IReadsAhead) of meerdere packs tegelijk. Een NAS met 1 Gbit haalt ~110 MB/s: daar is winst. Ook de index: `target:add` over SMB vanaf Canada haalt ~1 indexbestand per seconde (4.927 bestanden ~85 min), want SMB krijgt geen `getMany()` met verzoeken tegelijk zoals S3 en SFTP. 6. **Notitie achteraf bij een restorepunt**, te wijzigen in de lijst. Het label in de snapshot is versleuteld en ligt vast; de notitie apart op de locatie bewaren (bijv. in de catalogus), zodat een nieuwe server na een DR hem ook ziet. 8. **Herstelpunt vastpinnen** (besluit 2026-10-25): een herstelpunt dat het bewaarbeleid nooit opruimt (bijv. "vóór de upgrade naar NC 35"), in de lijst en met `occ backup:pin ` / `--unpin`. Op de locatie bewaren (catalogus), zodat de opschoning het ziet, ook na een DR; vastgepinde punten tellen niet mee in last/daily/weekly. 9. **Feedbackvraag weghalen** in 1.1.0 of 1.2.0 (besluit 2026-10-27): de regel "We would like to hear how NG Backup works for you ... Share your experience" in de beheerpagina blijft tot dan staan (de vermelding "in beta" is er in 1.0.1 al af). Zijn er tegen die tijd geen noemenswaardige meldingen, dan werkt het: mensen melden eerder wat niet werkt dan wat gewoon werkt. Het formulier (`experience.yml`) mag blijven voor wie het zelf opzoekt. - **Bevindingen DR-test op PVE02 (2026-10-17, snapshot van nc01, 1 TB, AIO → gewone NC 34 met PostgreSQL 18)** — voor **1.0.0-rc.2**: 1. Tabellen van apps die bij de backup **niet aanstonden** (restanten: Deck, ONLYOFFICE, AppAPI, een archief-app) blokkeren de restore met één tabelnaam per poging. Standaard overslaan met één waarschuwing per app; optie om ze toch terug te zetten. 2. Alle ontbrekende apps en tabellen **vooraf in één lijst**, per app benoemd (nu één per poging, ook voor uitgeschakelde standaard-apps zoals `admin_audit`). 3. `--skip-missing-apps`; `nextcloud-aio` standaard overslaan (bestaat buiten AIO niet), zodat AIO → gewone Nextcloud werkt. 4. `--install-apps`: ontbrekende apps uit de app store installeren. 5. Apps buiten de app store (eigen builds, `side_menu`, `nextcloud-aio`): documenteren; later eventueel `custom_apps` meenemen in de backup. 6. Tabellen van een app die wél aanstond maar die hun tabel later zelf maakt (Memories: `memories_planet_geometry`) netjes afhandelen. 7. `backup:target:add` zonder sleutel verwijst naar `key:init`; als er al een repository staat, moet het naar `key:import ` wijzen. 8. `backup:target:add` toont geen voortgang bij het ophalen van de index (27.582 bestanden). 9. `backup:target:remove` laat de index-cache van die locatie in de database staan; opruimen, ook bestaande wezen. 10. Snapshot-meta uitbreiden: ook geïnstalleerde maar uitgeschakelde apps, met per app of hij uit de app store komt; `occ backup:apps ` toont de lijst leesbaar (geen platte tekst op de locatie: dat lekt welke apps/versies er draaien). 11. **Tabelstructuur meedumpen** (kolommen, typen, indexen, primary key), zodat de restore een ontbrekende tabel zelf aanmaakt met de gegevens erin. Lost restanten van oude apps op zonder ze over te slaan; een later opnieuw geïnstalleerde app vindt zijn oude gegevens terug. 12. **Foreign keys**: de restore zet tabellen op alfabet terug; `oc_mail_accounts` verwijst naar `oc_mail_mailboxes` en PostgreSQL weigert (fk_61a7e87f59a77cdc). Iedereen met Mail loopt hier vast. Oplossing: in de juiste volgorde terugzetten (afhankelijkheden eerst), of de controle tijdelijk uitzetten en na afloop terugzetten en controleren. In de test: 15 FK's (alle van Mail) tijdelijk verwijderd en na de restore teruggezet: alle 15 geldig op de teruggezette data, dus "uitzetten, terugzetten, achteraf controleren" werkt. 13. **SFTP-restore over een lange lijn is traag**: 0,75 MiB/s vanaf Hetzner naar Canada (RTT 122 ms), terwijl OpenSSH `sftp` 16,6 MiB/s haalt. De SFTP-stream van files_external (phpseclib) wacht na elk klein blok op antwoord. Oplossing: pack-ranges bij SFTP met phpseclib's eigen `get()` (veel verzoeken tegelijk) lezen in plaats van via de stream, zoals bij S3. 1 TB duurt nu ~2 weken. Na meten: de grootste oorzaak was het netwerk (cubic, zie 14): met BBR gaat de restore van 0,75 naar 2,3 MiB/s, een hele pack lezen 4-5 MiB/s. Meer verzoeken in één stream (DEPTH 16/32) gaf geen duidelijke winst. Verder komen vraagt meerdere verbindingen tegelijk: naar **1.1.0**. 14. **Uploaden over een lange lijn**: backup naar OVH S3 Warschau vanaf Canada (RTT 114 ms) haalt ~1 MiB/s met één pack tegelijk; `aws s3 cp` (multipart, parallel) 6 MiB/s. Meerdere packs tegelijk uploaden of S3 multipart. Minder dringend dan 13 (restore). Oorzaak netwerk (met Sysop): pakketverlies trans-Atlantisch + cubic in de VM; met BBR gaat één PUT van 2,4 naar 7,5 MiB/s, ng_backup van ~1 naar ~1,6 MiB/s (rest = serieel lezen/uploaden). Tip over BBR in de README gezet. Let op: PVE02 staat in Canada; een restore in de EU gaat sneller. 15. DR-document: in het Docker-voorbeeld "je containernaam" (`docker ps`) i.p.v. `nextcloud`. 16. Grote restore (VM 980, snapshot aa7cce8d van de Storage Box) stopte na 22 uur met `Not found: packs/26/2642ef39…`. Oorzaak: de wekelijkse opruiming van nc01 (zo 18-10) heeft dat pakket tijdens de restore omgepakt (blob staat nu in `389567b7…`); de herstelserver had de index van vóór de opruiming. Productie heel: `backup:verify` op nc01 OK (150.639 bestanden, 316.830 blobs). Opgelost in rc2: bij een mislukte pakketlezing de index herladen en de blob lezen waar hij nu staat (test `testRestoreFollowsPacksRepackedByAnotherServer`). **Kleine DR-test met rc2 (2026-10-18), volautomatisch geslaagd:** bron VM 981 (NC 34, PostgreSQL, 51 GB, Mail met gesynct account, Deck uitgeschakeld, Calendar/Contacts, admin_audit) → OVH S3 → verse VM 982 met alleen de DR-gids en `--install-apps`: apps vanzelf geïnstalleerd, 10 Deck-tabellen aangemaakt, 15 FK's in orde, 23.519/23.519 checksums gelijk, agenda/contact/mail/bord terug. Restore 1 u 47 vanuit Warschau naar Canada (met BBR). Lokaal slaagt de test ook op SQLite en MariaDB. **Daarna verplicht:** een tweede volledige restore op een verse VM, alleen met de HOWTO en de nieuwe opties, zonder handwerk. Pas als die in één keer slaagt: 1.0.0. - **Later (geen versie):** optie om de sleutel niet op de locatie op te slaan (kit is dan de enige kopie; uitdrukkelijke waarschuwing). Primaire object storage en External-storage-koppelingen meenemen via de opslaglaag (§3.4; nu niet ondersteund, zie README "Known limitations"). - **Open:** of de bevestigingscode van de kit dubbelop is (nog niet over besloten). Doorgeschoven naar **v1.3.0** (fase 3, niet meer fase 2): in-place volledige restore (eigen restore-modus i.p.v. onderhoudsmodus) en zelfbediening voor gebruikers (eigen bestanden terugzetten vanuit de Files-app). **Let op (ontdekt 2026-10-05):** Nextcloud's eigen `info.xsd` staat voor `` alleen `[0-9]+(\.[0-9]+){0,2}` toe — geen `-alpha`/`-beta`/`-rc`-achtervoegsel. De stabiliteitsaanduiding uit deze tabel gaat dus niet in `info.xml` (dat blijft gewoon oplopend, bijv. `0.1.0`), maar in de git-tag (`v0.1.0-alpha.1`), de GitHub/Gitea release (gemarkeerd als pre-release) en de CHANGELOG. ## Bijlage: lessen uit nextcloud/backup (test NC34, 2026-10-01) - Externe programma's (`pg_dump`) en extensies (`pgsql`, `mysqli`) → onbruikbaar op standaard/hosted. - Crash tijdens backup/restore laat de instantie in onderhoudsmodus; occ-commando's van de app zijn dan niet beschikbaar. - Hele chunks in het geheugen (`ZipArchive::getFromName()` als bestaan-check) → OOM bij restore. - Bij een fout eindeloos "opnieuw proberen?" zonder invoer → hangende restore. - Backupt de hele webroot + apps → groot en traag; restore alleen via occ in meerdere stappen. - Herbruikbaar als idee: checksums + metadata per herstelpunt, AES-GCM/sodium, differentiële backups.