[path, offset, length] */ private array $ranges = []; /** @var \Generator|null */ private ?\Generator $streams = null; private int $current = -1; /** @var resource|null */ private $stream = null; /** @param string $spool local folder for the ranges read ahead (see IReadsAhead::readAhead()) */ public function __construct( public readonly string $spool, ) { } public function add(string $path, int $offset, int $length): void { $last = array_pop($this->ranges); if ($last !== null) { [$p, $start, $len] = $last; if ($p === $path && $offset >= $start && $offset <= $start + $len + self::GAP && $offset + $length - $start <= self::MAX_RANGE) { $this->ranges[] = [$p, $start, max($len, $offset + $length - $start)]; return; } $this->ranges[] = $last; } $this->ranges[] = [$path, $offset, $length]; } /** @return list */ public function ranges(): array { return $this->ranges; } /** @param \Generator $streams what IReadsAhead::readAhead() gave for ranges() */ public function readFrom(\Generator $streams): void { $this->streams = $streams; } /** The bytes of a blob, or null when its range could not be read ahead (read it the usual way). */ public function read(string $path, int $offset, int $length): ?string { if ($this->streams === null) { return null; } if (!$this->contains($this->current, $path, $offset, $length)) { // Normally the next range. Further on when blobs before it were read elsewhere: the // index was reloaded after another server's cleanup moved them to new packs. $to = null; for ($i = $this->current + 1; $i <= $this->current + self::SEARCH && $to === null; $i++) { if ($this->contains($i, $path, $offset, $length)) { $to = $i; } } if ($to === null) { return null; // not where the plan has it } if ($this->current < 0) { $this->streams->current(); $this->current = 0; } while ($this->current < $to) { $this->streams->next(); $this->current++; } $stream = $this->streams->valid() ? $this->streams->current() : null; $this->stream = is_resource($stream) ? $stream : null; } $fh = $this->stream; if ($fh === null || fseek($fh, $offset - $this->ranges[$this->current][1]) !== 0) { return null; } $data = ''; while (strlen($data) < $length) { $chunk = fread($fh, $length - strlen($data)); if ($chunk === false || $chunk === '') { return null; } $data .= $chunk; } return $data; } /** Ends the reads ahead, also halfway: closes their streams and connections. */ public function close(): void { $this->stream = null; $streams = $this->streams; $this->streams = null; unset($streams); // the generator's finally blocks run when it is destroyed } private function contains(int $i, string $path, int $offset, int $length): bool { if ($i < 0 || !isset($this->ranges[$i])) { return false; } [$p, $start, $len] = $this->ranges[$i]; return $p === $path && $offset >= $start && $offset + $length <= $start + $len; } }