$roots group => folder on this site * @param int[] $want file-list numbers, in order * @param int $off where to continue inside the first one * @return array{payload: string, sent: int, off: int} sent = how many of $want were finished; off = progress inside the next one */ public static function pack(FileList $list, array $roots, array $want, int $off, int $max): array { $payload = ''; $sent = 0; // The list is read once for the whole batch, and each folder resolved once (a server's disk is slow per call). $entries = $list->entries(array_map('intval', array_values($want))); $real = []; foreach (array_values($want) as $k => $i) { $start = $k === 0 ? max(0, $off) : 0; $space = $max - strlen($payload); if ($payload !== '' && $space < self::MIN_PIECE) { break; } $e = $entries[(int) $i] ?? null; $path = $e !== null && isset($roots[$e['g']]) ? self::path($roots[$e['g']], $e['r'], $real) : null; clearstatcache(true, (string) $path); if ($e === null || $path === null || !is_file($path) || filesize($path) !== $e['s'] || $start > $e['s']) { $payload .= self::header(['i' => (int) $i, 'gone' => true]); $sent++; $off = 0; continue; } $len = min($e['s'] - $start, max($payload === '' ? 4096 : 0, $space - 256)); $data = ''; if ($len > 0) { $h = Fs::open($path, 'rb'); fseek($h, $start); $data = (string) Fs::read($h, $len); Fs::close($h); } $payload .= self::header(['i' => (int) $i, 'off' => $start, 'len' => strlen($data), 'size' => $e['s'], 'h' => $e['h']]) . $data; if ($start + strlen($data) >= $e['s']) { $sent++; $off = 0; continue; } return ['payload' => $payload, 'sent' => $sent, 'off' => $start + strlen($data)]; } return ['payload' => $payload, 'sent' => $sent, 'off' => 0]; } /** * The file at $rel under $root, or null when it isn't a plain file inside $root (symlinks are never followed). * $real keeps folders already resolved, for many files at once. * * @param array $real */ public static function path(string $root, string $rel, array &$real = []): ?string { if (!RelPath::valid($rel)) { return null; } $path = rtrim($root, '/') . '/' . $rel; $real_root = $real[$root] ?? ($real[$root] = realpath($root)); $real_dir = $real[dirname($path)] ?? ($real[dirname($path)] = realpath(dirname($path))); if ($real_root === false || $real_dir === false || is_link($path) || strpos($real_dir . '/', rtrim($real_root, '/') . '/') !== 0) { return null; } return $path; } private static function header(array $h): string { return json_encode($h, JSON_UNESCAPED_SLASHES) . "\n"; } }