setName('backup:status')->setDescription('Show the backup key, locations and recent runs') ->addOption('all', null, InputOption::VALUE_NONE, 'List all runs, not only those of the last ' . self::RECENT_DAYS . ' days'); } protected function execute(InputInterface $input, OutputInterface $output): int { $zone = $this->schedule->get()['timezone']; $cronProblem = CronHealth::describe($this->cron->check()); if ($cronProblem !== null) { $output->writeln('' . $cronProblem . ''); } if (!$this->keys->isInitialized()) { $output->writeln('Key: not created (occ backup:key:init)'); } else { $c = $this->keys->recoveryKitConfirmation(); $output->writeln('Key: ' . $this->keys->fingerprint() . ', recovery kit ' . ($c ? 'confirmed by ' . $c['uid'] . ' on ' . Format::time($c['time'], $zone) . " ($zone)" : 'not confirmed (occ backup:key:confirm)')); } $names = []; foreach ($this->targets->list() as $t) { $names[$t->getId()] = $t->getName(); } $output->writeln('Locations: ' . ($names === [] ? 'none' : implode(', ', $names))); $rows = []; $all = (bool)$input->getOption('all'); $shown = $this->runs->findShown($all ? null : time() - self::RECENT_DAYS * 86400); $queued = array_map(static fn (\OCA\NgBackup\Db\Run $r): int => $r->getId(), array_slice($this->backups->runsInTurn(), 1)); foreach ($shown as $r) { $s = json_decode($r->getStats() ?? '{}', true)['files'] ?? []; $rows[] = [$r->getId(), $names[$r->getTargetId()] ?? '#' . $r->getTargetId(), $r->getKind(), in_array($r->getId(), $queued, true) ? 'queued' : ($r->getStatus() === 'running' ? self::runningStatus($r) : $r->getStatus()), Format::time($r->getStartedAt(), $zone), $r->getFinishedAt() ? Format::duration($r->getFinishedAt() - $r->getStartedAt()) : '', $s['files'] ?? '', isset($s['bytes']) ? self::size((int)$s['bytes']) : '', isset($s['uploaded']) ? self::size((int)$s['uploaded']) : '', mb_strimwidth((string)$r->getError(), 0, 50, '…')]; } if ($rows !== []) { (new Table($output))->setHeaders(['run', 'location', 'kind', 'status', "started ($zone)", 'took', 'files', 'size', 'uploaded', 'error'])->setRows($rows)->render(); } $older = $all ? 0 : $this->runs->countAll() - count($shown); if ($older > 0) { $output->writeln(sprintf('%d older run%s not shown (--all)', $older, $older === 1 ? '' : 's')); } foreach ($this->verify->progress() as $targetId => $state) { $output->writeln(sprintf('Verify of %s on %s: %s', mb_strimwidth((string)($state['what'] ?? ''), 0, 40, '…'), $names[$targetId] ?? '#' . $targetId, VerifyService::describe($state))); } return 0; } /** " 68%", or " ~68%" when estimated from the file cache; empty without an estimate */ private static function size(int $bytes): string { $b = (float)$bytes; foreach (['B', 'KiB', 'MiB', 'GiB'] as $u) { if ($b < 1024) { return $u === 'B' ? "$bytes B" : sprintf('%.1f %s', $b, $u); } $b /= 1024.0; } return sprintf('%.1f TiB', $b); } /** " 47/183 tables" in the database phase, " 68%" or " ~68%" in the files phase; empty without a figure */ private static function percent(\OCA\NgBackup\Db\Run $r): string { $p = \OCA\NgBackup\Job\BackupRun::progressOf($r); if ($p === null) { return ''; } if (isset($p['tables'], $p['totalTables'])) { return sprintf(': %d/%d tables', $p['tables'], $p['totalTables']); } return sprintf(' %s%d%%', $p['approximate'] ? '~' : '', (int)floor($p['fraction'] * 100.0)); } /** "running (db: 47/183 tables)", or "waiting for background jobs (last step 12 min ago)" */ private static function runningStatus(\OCA\NgBackup\Db\Run $r): string { $waiting = $r->getKind() === \OCA\NgBackup\Service\BackupService::KIND_FULL ? CronHealth::waitingFor($r) : null; if ($waiting !== null) { return 'waiting for background jobs (' . $r->getPhase() . self::percent($r) . ', last step ' . CronHealth::ago($waiting) . ')'; } return 'running (' . $r->getPhase() . self::percent($r) . ')'; } }