*/ private array $roots; private string $staging; private FileSwap $swap; private string $aside; /** @var array */ private array $policies; /** @param array $policies group => SubsiteFiles policy (a subsite only adds missing code, never other content) */ public function __construct(FileList $list, IndexList $needed, string $extras, array $roots, string $staging, FileSwap $swap, string $aside, array $policies = []) { $this->policies = $policies; $this->list = $list; $this->needed = $needed; $this->extras = $extras; $this->roots = $roots; $this->staging = rtrim($staging, '/'); $this->swap = $swap; $this->aside = rtrim($aside, '/'); } public static function start(): array { return ['pos' => 0, 'extra' => 0, 'placed' => 0, 'aside' => 0, 'done' => false]; } /** @param bool $low_disk uploads were written in place already: nothing to do */ public function uploads(array $cursor, Budget $budget, bool $low_disk): array { if ($low_disk) { return ['done' => true] + $cursor; } while (!$budget->exhausted()) { $slice = $this->needed->slice((int) $cursor['pos'], 200); if ($slice === []) { break; } foreach ($slice as $i) { $cursor['pos']++; $e = $this->list->get($i); if ($e === null || $e['g'] !== 'uploads') { continue; } $staged = $this->staging . '/uploads/' . $e['r']; if (!is_file($staged) || is_link($staged)) { continue; // gone at the source, or placed by a request that died before saving its progress } $dir = Fs::ensure_dir_within($this->roots['uploads'], dirname($e['r']) === '.' ? '' : dirname($e['r'])); $this->swap->replace('uploads', $e['r'], $staged, $dir . '/' . basename($e['r']), $this->aside . '/uploads/' . $e['r']); $cursor['placed']++; } } if ($this->needed->slice((int) $cursor['pos'], 1) !== []) { return $cursor; } foreach ($this->extras_from((int) $cursor['extra']) as $offset => $x) { if ($budget->exhausted()) { return $cursor; } $cursor['extra'] = $offset; if ($x['g'] !== 'uploads') { continue; } $live = Packer::path($this->roots['uploads'], $x['r']); if ($live !== null && is_file($live)) { $this->swap->move_aside('uploads', $x['r'], $live, $this->aside . '/uploads/' . $x['r']); $cursor['aside']++; } } $cursor['done'] = true; return $cursor; } /** @return int how many items were replaced or moved aside */ public function code(): int { $moved = 0; $protected = Groups::protected_items($this->roots); foreach (['themes', 'plugins', 'mu-plugins', 'other'] as $group) { $staged_root = $this->staging . '/' . $group; $policy = $this->policies[$group] ?? SubsiteFiles::REPLACE; foreach (is_dir($staged_root) && $policy !== SubsiteFiles::SKIP ? (scandir($staged_root) ?: []) : [] as $item) { if ($item === '.' || $item === '..' || !RelPath::valid($item) || RelPath::is_protected($group, $item, $protected)) { continue; } if ($policy === SubsiteFiles::ADD_MISSING && file_exists(rtrim($this->roots[$group], '/') . '/' . $item)) { continue; // the network's copy stays: other subsites use it } $this->swap->replace($group, $item, $staged_root . '/' . $item, rtrim($this->roots[$group], '/') . '/' . $item, $this->aside . '/' . $group . '/' . $item); $moved++; } } foreach ($this->extras_from(0) as $x) { if (!in_array($x['g'], ['themes', 'plugins'], true) || strpos($x['r'], '/') !== false || RelPath::is_protected($x['g'], $x['r'], $protected)) { continue; } $live = rtrim($this->roots[$x['g']], '/') . '/' . $x['r']; if ((file_exists($live) || is_link($live)) && !file_exists($this->aside . '/' . $x['g'] . '/' . $x['r'])) { $this->swap->move_aside($x['g'], $x['r'], $live, $this->aside . '/' . $x['g'] . '/' . $x['r']); $moved++; } } return $moved; } /** @return \Generator byte offset after the line => extra */ private function extras_from(int $offset): \Generator { if (!is_file($this->extras)) { return; } $h = Fs::open($this->extras, 'rb'); fseek($h, $offset); try { while (($raw = fgets($h)) !== false) { $offset += strlen($raw); $x = json_decode($raw, true); if (is_array($x) && is_string($x['g'] ?? null) && is_string($x['r'] ?? null) && RelPath::valid($x['r'])) { yield $offset => $x; } } } finally { Fs::close($h); } } }