bin2hex(random_bytes(16)), 'source' => rtrim($source, '/'), 'parent' => $parent, 'label' => $label, 'started' => gmdate('c'), 'phase' => 'scan', 'files' => [], 'pos' => 0, 'cur' => null, 'segments' => [], 'steps' => 0, 'stats' => ['files' => 0, 'reused' => 0, 'read' => 0, 'newBlobs' => 0, 'dupBlobs' => 0, 'uploaded' => 0], ]; } /** Run one step until $deadline (microtime(true)); returns the new state. */ public static function step(Repository $repo, array $state, float $deadline): array { $state['steps']++; if ($state['phase'] === 'scan') { $state['files'] = self::scan($state['source']); $state['phase'] = 'files'; // Scanning can take a while on big trees; continue in the same step if time is left. } if ($state['phase'] === 'files') { $previous = $state['parent'] !== null ? $repo->snapshotEntries($state['parent']) : []; $stats = $state['stats']; $lines = []; $total = count($state['files']); while ($state['pos'] < $total) { $relative = $state['files'][$state['pos']]; $path = $state['source'] . '/' . $relative; clearstatcache(true, $path); $size = @filesize($path); $mtime = @filemtime($path); if ($size === false) { // Deleted since the scan: skip. $state['pos']++; $state['cur'] = null; continue; } $prev = $previous[$relative] ?? null; if ($state['cur'] === null && $prev !== null && $prev['s'] === $size && $prev['m'] === $mtime) { $lines[] = self::line($relative, $size, $mtime, $prev['b']); $stats['files']++; $stats['reused']++; $state['pos']++; continue; } $cur = $state['cur'] ?? ['path' => $relative, 'offset' => 0, 'blobs' => []]; $fh = fopen($path, 'rb'); fseek($fh, $cur['offset']); $finished = true; while (($data = self::readFull($fh, Repository::BLOB_SIZE)) !== '') { $cur['blobs'][] = $repo->storeData($data, $stats); $cur['offset'] += strlen($data); if (microtime(true) >= $deadline && !feof($fh)) { $finished = false; break; } } fclose($fh); if (!$finished) { $state['cur'] = $cur; break; } $lines[] = self::line($relative, $size, $mtime, $cur['blobs']); $stats['files']++; $stats['read']++; $state['cur'] = null; $state['pos']++; if (microtime(true) >= $deadline) { break; } } // Persist this step's tree lines as a blob stream segment and upload the open pack. if ($lines !== []) { $writer = $repo->blobWriter($stats); $writer->write(implode("\n", $lines) . "\n"); $state['segments'][] = $writer->finish(); } $repo->flushPacks(); $state['stats'] = $stats; if ($state['pos'] >= $total) { $state['phase'] = 'finish'; } } if ($state['phase'] === 'finish' && microtime(true) < $deadline + 5) { $segments = $state['segments']; $repo->writeSnapshot($state['snapshot'], ['time' => $state['started'], 'parent' => $state['parent'], 'label' => $state['label'], 'source' => $state['source']], (function () use ($repo, $segments) { foreach ($segments as $blobs) { yield from $repo->readLines($blobs); } })()); $state['phase'] = 'done'; $state['files'] = []; } return $state; } /** @return list */ private static function scan(string $root): array { $files = []; $it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root, \FilesystemIterator::SKIP_DOTS)); foreach ($it as $file) { if ($file->isFile()) { $files[] = substr($file->getPathname(), strlen($root) + 1); } } sort($files, SORT_STRING); return $files; } private static function line(string $path, int $size, int $mtime, array $blobs): string { return json_encode(['p' => $path, 's' => $size, 'm' => $mtime, 'b' => $blobs], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); } private static function readFull($fh, int $length): string { $buffer = ''; while (strlen($buffer) < $length && !feof($fh)) { $buffer .= (string)fread($fh, $length - strlen($buffer)); } return $buffer; } }