M4: admin settings page and JSON API

- AdminApiController (admin only, CSRF-protected): status, create key,
  download recovery kit, confirm (statement accepted + exact phrase),
  backends with their usable auth mechanisms, add/test/remove locations,
  start backups, schedule, snapshots, browse, queue restores
- RestoreJob runs web-initiated restores in the background; backup and
  restore runs are kept apart in the run table
- admin section 'NG Backup' with a Vue 3 / @nextcloud/vue 9 page: key and
  recovery-kit flow (download, tick statement, 'Yes, I confirm'), location
  form built from files_external's parameter definitions, schedule, run
  list with polling, snapshot browser and restore with the three modes
- auth mechanisms that depend on a user session or on files_external's
  own stored credentials are not offered

API tested with curl on NC34 (status, backends, schedule validation,
confirmation refusal, browse, queued restore and backup completed by
cron); non-admins get 403, anonymous 401.
This commit is contained in:
2026-10-02 01:09:47 +02:00
parent ebd9737f51
commit 0309f2e44e
29 changed files with 9333 additions and 13 deletions
+2 -2
View File
@@ -40,7 +40,7 @@ class BackupJob extends TimedJob {
$budget = PHP_SAPI === 'cli' ? 240.0 : 20.0;
$deadline = microtime(true) + $budget;
$this->startScheduled();
foreach ($this->runs->findRunning() as $run) {
foreach ($this->runs->findRunning(null, BackupService::KIND_FULL) as $run) {
while (microtime(true) < $deadline && $run->getStatus() === 'running') {
$run = $this->backups->step($run, min($deadline, microtime(true) + $budget));
}
@@ -59,7 +59,7 @@ class BackupJob extends TimedJob {
}
foreach ($this->targets->list() as $t) {
$last = $this->runs->lastFinished($t->getId(), BackupService::KIND_FULL);
if ($this->runs->findRunning($t->getId()) !== [] || ($last !== null && $last->getStartedAt() >= $todayAt)) {
if ($this->runs->findRunning($t->getId(), BackupService::KIND_FULL) !== [] || ($last !== null && $last->getStartedAt() >= $todayAt)) {
continue;
}
try {
+54
View File
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: 2026 Dillard Blom
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\NgBackup\BackgroundJob;
use OCA\NgBackup\Db\Run;
use OCA\NgBackup\Db\RunMapper;
use OCA\NgBackup\Service\RestoreService;
use OCA\NgBackup\Service\TargetService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\QueuedJob;
use Psr\Log\LoggerInterface;
/** Runs a restore requested from the web interface, outside the web request. */
class RestoreJob extends QueuedJob {
public function __construct(
ITimeFactory $time,
private RunMapper $runs,
private TargetService $targets,
private RestoreService $restore,
private LoggerInterface $logger,
) {
parent::__construct($time);
}
protected function run($argument): void {
$run = $this->runs->find((int)$argument['run']);
if ($run->getStatus() !== Run::RUNNING) {
return;
}
$req = json_decode($run->getState(), true, 512, JSON_THROW_ON_ERROR);
$run->setPhase('restoring');
$run->setUpdatedAt(time());
$this->runs->update($run);
@set_time_limit(0);
try {
$result = $this->restore->restoreUserFiles($this->targets->get((string)$run->getTargetId()), $req['snapshot'], $req['path'], $req['mode']);
$run->setStatus(Run::DONE);
$run->setPhase('done');
$run->setStats(json_encode($result, JSON_THROW_ON_ERROR));
} catch (\Throwable $e) {
$this->logger->error('NG Backup restore ' . $run->getId() . ' failed: ' . $e->getMessage(), ['exception' => $e]);
$run->setStatus(Run::FAILED);
$run->setError($e->getMessage());
}
$run->setFinishedAt(time());
$run->setUpdatedAt(time());
$this->runs->update($run);
}
}