From bb05e0a0b4f1219cf1c280e8407e4a12d5bb6c2e Mon Sep 17 00:00:00 2001 From: Travis Carden Date: Fri, 16 Feb 2024 10:30:55 -0500 Subject: [PATCH] WIP: Robocopy File Syncer. --- .../FileSyncer/Service/RobocopyFileSyncer.php | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 src/Internal/FileSyncer/Service/RobocopyFileSyncer.php diff --git a/src/Internal/FileSyncer/Service/RobocopyFileSyncer.php b/src/Internal/FileSyncer/Service/RobocopyFileSyncer.php new file mode 100644 index 00000000..eec19e88 --- /dev/null +++ b/src/Internal/FileSyncer/Service/RobocopyFileSyncer.php @@ -0,0 +1,179 @@ +setTranslatableFactory($translatableFactory); + } + + public function sync( + PathInterface $source, + PathInterface $destination, + ?PathListInterface $exclusions = null, + ?OutputCallbackInterface $callback = null, + int $timeout = ProcessInterface::DEFAULT_TIMEOUT, + ): void { + $this->environment->setTimeLimit($timeout); + + $exclusions ??= new PathList(); + + $this->assertRobocopyIsAvailable(); + $this->assertSourceAndDestinationAreDifferent($source, $destination); + $this->assertSourceExists($source); + + $this->runCommand($exclusions, $source->absolute(), $destination->absolute(), $destination, $callback); + } + + /** @throws \PhpTuf\ComposerStager\API\Exception\LogicException */ + private function assertRobocopyIsAvailable(): void + { + $this->executableFinder->find('robocopy'); + } + + /** @throws \PhpTuf\ComposerStager\API\Exception\LogicException */ + private function assertSourceAndDestinationAreDifferent(PathInterface $source, PathInterface $destination): void + { + if ($source->absolute() === $destination->absolute()) { + throw new LogicException( + $this->t( + 'The source and destination directories cannot be the same at %path', + $this->p(['%path' => $source->absolute()]), + $this->d()->exceptions(), + ), + ); + } + } + + /** @throws \PhpTuf\ComposerStager\API\Exception\LogicException */ + private function assertSourceExists(PathInterface $source): void + { + if (!$this->filesystem->fileExists($source)) { + throw new LogicException($this->t( + 'The source directory does not exist at %path', + $this->p(['%path' => $source->absolute()]), + $this->d()->exceptions(), + )); + } + + if (!$this->filesystem->isDir($source)) { + throw new LogicException($this->t( + 'The source directory is not actually a directory at %path', + $this->p(['%path' => $source->absolute()]), + $this->d()->exceptions(), + )); + } + } + + /** @throws \PhpTuf\ComposerStager\API\Exception\IOException */ + private function runCommand( + ?PathListInterface $exclusions, + string $sourceAbsolute, + string $destinationAbsolute, + PathInterface $destination, + ?OutputCallbackInterface $callback, + ): void { + $sourceAbsolute = PathHelper::canonicalize($sourceAbsolute); + $destinationAbsolute = PathHelper::canonicalize($destinationAbsolute); + + $this->ensureDestinationDirectoryExists($destination); + $command = $this->buildCommand($exclusions, $sourceAbsolute, $destinationAbsolute); + + $process = new Process($command); + + $process->run(); + } + + /** + * Ensures that the destination directory exists. This has no effect if it already does. + * + * @throws \PhpTuf\ComposerStager\API\Exception\IOException + */ + private function ensureDestinationDirectoryExists(PathInterface $destination): void + { + $this->filesystem->mkdir($destination); + } + + /** @return array */ + private function buildCommand( + ?PathListInterface $exclusions, + string $sourceAbsolute, + string $destinationAbsolute, + ): array { + $exclusions ??= new PathList(); + /** @noinspection CallableParameterUseCaseInTypeContextInspection */ + $exclusions = $exclusions->getAll(); + + $command = [ + 'robocopy', + $sourceAbsolute, + $destinationAbsolute, + '/MIR', + ]; + + // Prevent infinite recursion if the source is inside the destination. + if ($this->isDescendant($sourceAbsolute, $destinationAbsolute)) { + $exclusions[] = self::getRelativePath($destinationAbsolute, $sourceAbsolute); + } + + $pathFactory = new PathFactory(); + + if ($exclusions !== []) { + $command[] = '/XD'; + $command[] = implode(' ', $exclusions); + $command[] = '/XF'; + $command[] = implode(' ', $exclusions); + } + + return $command; + } + + private function isDescendant(string $descendant, string $ancestor): bool + { + $ancestor .= DIRECTORY_SEPARATOR; + + return str_starts_with($descendant, $ancestor); + } + + private static function getRelativePath(string $ancestor, string $path): string + { + $ancestor .= DIRECTORY_SEPARATOR; + + if (str_starts_with($path, $ancestor)) { + return substr($path, strlen($ancestor)); + } + + return $path; + } +}