Skip to content

Commit

Permalink
WIP: Robocopy File Syncer.
Browse files Browse the repository at this point in the history
  • Loading branch information
TravisCarden committed Apr 16, 2024
1 parent b1e1a9e commit bb05e0a
Showing 1 changed file with 179 additions and 0 deletions.
179 changes: 179 additions & 0 deletions src/Internal/FileSyncer/Service/RobocopyFileSyncer.php
@@ -0,0 +1,179 @@
<?php declare(strict_types=1);

namespace PhpTuf\ComposerStager\Internal\FileSyncer\Service;

use PhpTuf\ComposerStager\API\Environment\Service\EnvironmentInterface;
use PhpTuf\ComposerStager\API\Exception\LogicException;
use PhpTuf\ComposerStager\API\FileSyncer\Service\FileSyncerInterface;
use PhpTuf\ComposerStager\API\Filesystem\Service\FilesystemInterface;
use PhpTuf\ComposerStager\API\Finder\Service\ExecutableFinderInterface;
use PhpTuf\ComposerStager\API\Path\Value\PathInterface;
use PhpTuf\ComposerStager\API\Path\Value\PathListInterface;
use PhpTuf\ComposerStager\API\Process\Service\OutputCallbackInterface;
use PhpTuf\ComposerStager\API\Process\Service\ProcessInterface;
use PhpTuf\ComposerStager\API\Process\Service\RsyncProcessRunnerInterface;
use PhpTuf\ComposerStager\API\Translation\Factory\TranslatableFactoryInterface;
use PhpTuf\ComposerStager\Internal\Helper\PathHelper;
use PhpTuf\ComposerStager\Internal\Path\Factory\PathFactory;
use PhpTuf\ComposerStager\Internal\Path\Value\PathList;
use PhpTuf\ComposerStager\Internal\Translation\Factory\TranslatableAwareTrait;
use Symfony\Component\Process\Process;

/**
* @package FileSyncer
*
* @internal Don't depend directly on this class. It may be changed or removed at any time without notice.
*/
final class RobocopyFileSyncer

Check failure on line 27 in src/Internal/FileSyncer/Service/RobocopyFileSyncer.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Concrete class must implement an interface
{
use TranslatableAwareTrait;

public function __construct(
private readonly EnvironmentInterface $environment,
private readonly ExecutableFinderInterface $executableFinder,
private readonly FilesystemInterface $filesystem,
private readonly RsyncProcessRunnerInterface $rsync,

Check failure on line 35 in src/Internal/FileSyncer/Service/RobocopyFileSyncer.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Property PhpTuf\ComposerStager\Internal\FileSyncer\Service\RobocopyFileSyncer::$rsync is never read, only written.
TranslatableFactoryInterface $translatableFactory,
) {
$this->setTranslatableFactory($translatableFactory);
}

public function sync(

Check failure on line 41 in src/Internal/FileSyncer/Service/RobocopyFileSyncer.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Method sync() should not be public because it doesn't implement a method on an interface.
PathInterface $source,
PathInterface $destination,
?PathListInterface $exclusions = null,
?OutputCallbackInterface $callback = null,
int $timeout = ProcessInterface::DEFAULT_TIMEOUT,
): void {
$this->environment->setTimeLimit($timeout);

$exclusions ??= new PathList();

Check failure on line 50 in src/Internal/FileSyncer/Service/RobocopyFileSyncer.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Class PhpTuf\ComposerStager\Internal\Path\Value\PathList constructor invoked with 0 parameters, at least 1 required.

$this->assertRobocopyIsAvailable();

Check failure on line 52 in src/Internal/FileSyncer/Service/RobocopyFileSyncer.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Method PhpTuf\ComposerStager\Internal\FileSyncer\Service\RobocopyFileSyncer::sync() throws checked exception PhpTuf\ComposerStager\API\Exception\LogicException but it's missing from the PHPDoc @throws tag.
$this->assertSourceAndDestinationAreDifferent($source, $destination);

Check failure on line 53 in src/Internal/FileSyncer/Service/RobocopyFileSyncer.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Method PhpTuf\ComposerStager\Internal\FileSyncer\Service\RobocopyFileSyncer::sync() throws checked exception PhpTuf\ComposerStager\API\Exception\LogicException but it's missing from the PHPDoc @throws tag.
$this->assertSourceExists($source);

Check failure on line 54 in src/Internal/FileSyncer/Service/RobocopyFileSyncer.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Method PhpTuf\ComposerStager\Internal\FileSyncer\Service\RobocopyFileSyncer::sync() throws checked exception PhpTuf\ComposerStager\API\Exception\LogicException but it's missing from the PHPDoc @throws tag.

$this->runCommand($exclusions, $source->absolute(), $destination->absolute(), $destination, $callback);

Check failure on line 56 in src/Internal/FileSyncer/Service/RobocopyFileSyncer.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Method PhpTuf\ComposerStager\Internal\FileSyncer\Service\RobocopyFileSyncer::sync() throws checked exception PhpTuf\ComposerStager\API\Exception\IOException but it's missing from the PHPDoc @throws tag.
}

/** @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);

Check failure on line 107 in src/Internal/FileSyncer/Service/RobocopyFileSyncer.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Call to static method canonicalize() on an unknown class PhpTuf\ComposerStager\Internal\Helper\PathHelper.
$destinationAbsolute = PathHelper::canonicalize($destinationAbsolute);

Check failure on line 108 in src/Internal/FileSyncer/Service/RobocopyFileSyncer.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Call to static method canonicalize() on an unknown class PhpTuf\ComposerStager\Internal\Helper\PathHelper.

$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<string> */
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;
}
}

0 comments on commit bb05e0a

Please sign in to comment.