$files rutas relativas al paquete * @param array $expectedFiles rel -> hash previo (lock) * @param array|null $destinations * @param array{force?:bool, backup?:bool, keepLocal?:bool, dryRun?:bool} $options * @return array{files: array, actions: array, aborted: list} */ public function install( array $files, string $sourceDir, string $rootDir, string $rootCore, ?array $destinations, string $mode, array $options = [], array $expectedFiles = [] ): array { $installed = []; $actions = []; $aborted = []; foreach ($files as $file) { $relative = Destination::relative($file, $rootDir, $rootCore, $destinations); $source = rtrim($sourceDir, '/') . '/' . ltrim($file, '/'); $target = rtrim($rootDir, '/') . '/' . $relative; $expected = $expectedFiles[$relative] ?? null; if (($options['dryRun'] ?? false)) { $actions[$relative] = 'planned'; continue; } if ($mode === 'link') { $action = $this->link($source, $target, $options); } else { $contents = file_get_contents($source); if ($contents === false) { throw new ToolboxException("No se pudo leer el archivo de origen: {$source}"); } $action = $this->sync->write($target, $contents, $expected, $options); } $actions[$relative] = $action; if ($action === FileSync::ABORTED) { $aborted[] = $relative; continue; } $installed[$relative] = is_file($source) ? Drift::hash($source) : ''; } return ['files' => $installed, 'actions' => $actions, 'aborted' => $aborted]; } /** * @param array{force?:bool, keepLocal?:bool} $options */ private function link(string $source, string $target, array $options): string { if (is_link($target)) { if (readlink($target) === $source) { return FileSync::WRITTEN; } unlink($target); } elseif (is_file($target)) { if ($options['keepLocal'] ?? false) { return FileSync::SKIPPED; } if (!($options['force'] ?? false)) { return FileSync::ABORTED; } unlink($target); } if (!is_dir(dirname($target))) { mkdir(dirname($target), 0777, true); } if (!symlink($source, $target)) { throw new ToolboxException("No se pudo enlazar {$target} -> {$source}"); } return FileSync::WRITTEN; } }