targets->list() as $t) { $targets[] = ['id' => $t->getId(), 'name' => $t->getName(), 'backend' => $t->getBackend(), 'path' => $t->getBasePath(), 'repositoryId' => $t->getRepositoryId(), 'createdAt' => $t->getCreatedAt()]; } $runs = array_map(fn (Run $r) => $this->runJson($r), $this->runs->findRecent(15)); return new JSONResponse([ 'key' => ['initialized' => $this->keys->isInitialized(), 'fingerprint' => $this->keys->fingerprint(), 'confirmation' => $this->keys->recoveryKitConfirmation()], 'statement' => KeyConfirm::STATEMENT, 'confirmationPhrase' => KeyConfirm::CONFIRMATION, 'targets' => $targets, 'runs' => $runs, 'schedule' => $this->appConfig->getValueString(Application::APP_ID, 'schedule_time', ''), ]); } public function initKey(string $passphrase): JSONResponse { try { $this->keys->initialize($passphrase); } catch (\Throwable $e) { return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } return new JSONResponse(['fingerprint' => $this->keys->fingerprint()]); } public function downloadKit(): DataDownloadResponse|JSONResponse { if (!$this->keys->isInitialized()) { return new JSONResponse(['error' => 'No backup key yet'], Http::STATUS_BAD_REQUEST); } $json = json_encode($this->keys->recoveryKit(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); return new DataDownloadResponse($json, 'ng_backup-recovery-kit-' . $this->keys->fingerprint() . '.json', 'application/json'); } /** The admin must tick the statement and send the exact confirmation phrase. */ public function confirmKit(bool $accepted, string $phrase): JSONResponse { if (!$accepted || $phrase !== KeyConfirm::CONFIRMATION) { return new JSONResponse(['error' => 'Not confirmed'], Http::STATUS_BAD_REQUEST); } $this->keys->confirmRecoveryKit($this->session->getUser()?->getUID() ?? 'unknown'); return new JSONResponse(['confirmation' => $this->keys->recoveryKitConfirmation()]); } public function backends(): JSONResponse { return new JSONResponse($this->targets->availableBackends()); } public function addTarget(string $name, string $backend, string $auth, array $options = [], string $path = 'ng_backup'): JSONResponse { try { $r = $this->targets->add($name, $backend, $auth, $options, $path); } catch (\Throwable $e) { return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } return new JSONResponse(['id' => $r['target']->getId(), 'created' => $r['created']]); } public function removeTarget(int $id): JSONResponse { $this->targets->remove($this->targets->get((string)$id)); return new JSONResponse([]); } public function testTarget(int $id): JSONResponse { try { $repo = $this->targets->repository($this->targets->get((string)$id)); return new JSONResponse(['ok' => true, 'snapshots' => count($repo->listSnapshots())]); } catch (\Throwable $e) { return new JSONResponse(['ok' => false, 'error' => $e->getMessage()]); } } public function startBackup(int $targetId, string $label = ''): JSONResponse { try { $run = $this->backups->start($this->targets->get((string)$targetId), $label); } catch (\Throwable $e) { return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } return new JSONResponse($this->runJson($run)); } public function setSchedule(string $time): JSONResponse { if ($time !== '' && !preg_match('/^([01]\d|2[0-3]):[0-5]\d$/', $time)) { return new JSONResponse(['error' => 'Use HH:MM or empty'], Http::STATUS_BAD_REQUEST); } $this->appConfig->setValueString(Application::APP_ID, 'schedule_time', $time); return new JSONResponse(['schedule' => $time]); } public function snapshots(int $targetId): JSONResponse { $out = []; foreach ($this->snapshots->findForTarget($targetId) as $s) { $out[] = ['id' => $s->getSnapshotId(), 'kind' => $s->getKind(), 'label' => $s->getLabel(), 'createdAt' => $s->getCreatedAt(), 'files' => $s->getFiles(), 'bytes' => $s->getBytes()]; } return new JSONResponse($out); } public function browse(int $targetId, string $snapshotId, string $path = ''): JSONResponse { try { return new JSONResponse($this->restore->browse($this->targets->get((string)$targetId), $snapshotId, $path)); } catch (\Throwable $e) { return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } } /** Queue a restore of user files; it runs as a background job and shows up in the runs. */ public function startRestore(int $targetId, string $snapshotId, string $path, string $mode = RestoreService::MODE_NEW_FOLDER): JSONResponse { if (!in_array($mode, [RestoreService::MODE_NEW_FOLDER, RestoreService::MODE_MERGE, RestoreService::MODE_REPLACE], true) || !preg_match('#^data/[^/]+/files(/.*)?$#', $path)) { return new JSONResponse(['error' => 'Invalid path or mode'], Http::STATUS_BAD_REQUEST); } $run = new Run(); $run->setTargetId($targetId); $run->setKind('restore'); $run->setStatus(Run::RUNNING); $run->setPhase('queued'); $run->setState(json_encode(['snapshot' => $snapshotId, 'path' => $path, 'mode' => $mode, 'by' => $this->session->getUser()?->getUID()], JSON_THROW_ON_ERROR)); $run->setSnapshotId($snapshotId); $run->setStartedAt(time()); $run->setUpdatedAt(time()); $run = $this->runs->insert($run); $this->jobs->add(RestoreJob::class, ['run' => $run->getId()]); return new JSONResponse($this->runJson($run)); } private function runJson(Run $r): array { return ['id' => $r->getId(), 'targetId' => $r->getTargetId(), 'kind' => $r->getKind(), 'status' => $r->getStatus(), 'phase' => $r->getPhase(), 'snapshotId' => $r->getSnapshotId(), 'startedAt' => $r->getStartedAt(), 'updatedAt' => $r->getUpdatedAt(), 'finishedAt' => $r->getFinishedAt(), 'error' => $r->getError(), 'stats' => json_decode($r->getStats() ?? 'null', true)]; } }