diff --git a/lib/Service/UserRestoreService.php b/lib/Service/UserRestoreService.php index d4877e1..eff3621 100644 --- a/lib/Service/UserRestoreService.php +++ b/lib/Service/UserRestoreService.php @@ -53,7 +53,8 @@ final class UserRestoreService { if (!$repo->isRecordedUserExport($manifestPath)) { throw new \InvalidArgumentException("$manifestPath is not a recorded user export for this location"); } - $source = new RepositoryImportSource($repo, $manifestPath); + $beat = LeaseService::throttled($refresh); + $source = new RepositoryImportSource($repo, $manifestPath, $beat); $originalUid = $source->getOriginalUid(); if ($mode === self::MODE_AS_BACKUP) { @@ -80,7 +81,7 @@ final class UserRestoreService { if ($existing !== null) { // Never import over a live account: export its current state first, so nothing // is lost if the restore turns out to be the wrong choice. - $safety = new RepositoryExportDestination($repo, $originalUid, LeaseService::throttled($refresh)); + $safety = new RepositoryExportDestination($repo, $originalUid, $beat); $service->export($safety, $existing); $safetyManifestPath = $safety->manifestPath(); // Same race as UserBackupService::backupUser(): reopen the repository inside the @@ -119,7 +120,7 @@ final class UserRestoreService { private function withSharedLock(Target $target, callable $fn): mixed { try { - // Refreshed while the safety export writes data (see UserBackupService::withSharedLock()). + // Refreshed while the safety export writes and the import reads (see UserBackupService::withSharedLock()). return $this->leases->with(PruneService::lockKey($target), LeaseService::SHARED, 3600, $fn); } catch (LockedException) { throw new \RuntimeException('A cleanup of ' . $target->getName() . ' is in progress; try again later'); diff --git a/lib/UserMigration/RepositoryImportSource.php b/lib/UserMigration/RepositoryImportSource.php index 84a6acc..8ba5a07 100644 --- a/lib/UserMigration/RepositoryImportSource.php +++ b/lib/UserMigration/RepositoryImportSource.php @@ -20,6 +20,8 @@ final class RepositoryImportSource implements IImportSource { public function __construct( private Repository $repo, string $manifestPath, + /** Called while blobs are read, to keep the location lease alive. */ + private ?\Closure $heartbeat = null, ) { $this->manifest = json_decode($repo->getObject($manifestPath), true, 512, JSON_THROW_ON_ERROR); } @@ -40,6 +42,9 @@ final class RepositoryImportSource implements IImportSource { throw new UserMigrationException('Cannot buffer ' . $path); } foreach ($this->repo->readBlobs($entry['b']) as $data) { + if ($this->heartbeat !== null) { + ($this->heartbeat)(); + } for ($done = 0, $len = strlen($data); $done < $len; $done += $n) { $n = fwrite($stream, $done === 0 ? $data : substr($data, $done)); if ($n === false || $n === 0) {