Close remaining review gaps

- restore runs from the web are created under the location's exclusive
  lock, like backup runs
- raw restore writes to an unpredictable temporary name opened with
  exclusive create (never follows a planted symlink) and validates the
  path prefix; the restore service rejects non-canonical paths instead
  of trimming them (occ normalises user input before calling it)
- restore jobs report progress every minute, so a long restore is not
  taken for a dead one
- README: Object Lock turns deletions into delete markers
This commit is contained in:
2026-10-02 11:25:22 +02:00
parent 7bde1cfdf0
commit 2c10dd2227
8 changed files with 57 additions and 8 deletions
+11 -2
View File
@@ -234,8 +234,13 @@ class AdminApiController extends Controller {
|| !\OCA\NgBackup\Repository\Repository::isSafePath($path) || !preg_match('#^data/[^/]+/files(/.*)?$#', $path)) {
return new JSONResponse(['error' => 'Invalid path or mode'], Http::STATUS_BAD_REQUEST);
}
try {
$target = $this->targets->get((string)$targetId);
} catch (\Throwable) {
return new JSONResponse(['error' => 'No such location'], Http::STATUS_NOT_FOUND);
}
$run = new Run();
$run->setTargetId($targetId);
$run->setTargetId($target->getId());
$run->setKind('restore');
$run->setStatus(Run::RUNNING);
$run->setPhase('queued');
@@ -244,7 +249,11 @@ class AdminApiController extends Controller {
$run->setSnapshotId($snapshotId);
$run->setStartedAt(time());
$run->setUpdatedAt(time());
$run = $this->runs->insert($run);
try {
$run = $this->prune->underStartLock($target, fn () => $this->runs->insert($run));
} catch (\Throwable $e) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_CONFLICT);
}
$this->jobs->add(RestoreJob::class, ['run' => $run->getId()]);
return new JSONResponse($this->runJson($run));
}