Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,31 @@
<?php
namespace Robo\Task\Filesystem;
use Robo\Task\BaseTask;
use Symfony\Component\Filesystem\Filesystem as sfFilesystem;
abstract class BaseDir extends BaseTask
{
/**
* @var string[]
*/
protected $dirs = [];
/**
* @var \Symfony\Component\Filesystem\Filesystem
*/
protected $fs;
/**
* @param string|string[] $dirs
*/
public function __construct($dirs)
{
is_array($dirs)
? $this->dirs = $dirs
: $this->dirs[] = $dirs;
$this->fs = new sfFilesystem();
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Robo\Task\Filesystem;
use Robo\Common\ResourceExistenceChecker;
use Robo\Result;
/**
* Deletes all files from specified dir, ignoring git files.
*
* ``` php
* <?php
* $this->taskCleanDir(['tmp','logs'])->run();
* // as shortcut
* $this->_cleanDir('app/cache');
* ?>
* ```
*/
class CleanDir extends BaseDir
{
use ResourceExistenceChecker;
/**
* {@inheritdoc}
*/
public function run()
{
if (!$this->checkResources($this->dirs, 'dir')) {
return Result::error($this, 'Source directories are missing!');
}
foreach ($this->dirs as $dir) {
$this->emptyDir($dir);
$this->printTaskInfo("Cleaned {dir}", ['dir' => $dir]);
}
return Result::success($this);
}
/**
* @param string $path
*/
protected function emptyDir($path)
{
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($path),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $path) {
if ($path->isDir()) {
$dir = (string)$path;
if (basename($dir) === '.' || basename($dir) === '..') {
continue;
}
$this->fs->remove($dir);
} else {
$file = (string)$path;
if (basename($file) === '.gitignore' || basename($file) === '.gitkeep') {
continue;
}
$this->fs->remove($file);
}
}
}
}

View File

@ -0,0 +1,175 @@
<?php
namespace Robo\Task\Filesystem;
use Robo\Common\ResourceExistenceChecker;
use Robo\Result;
use Robo\Exception\TaskException;
/**
* Copies one dir into another
*
* ``` php
* <?php
* $this->taskCopyDir(['dist/config' => 'config'])->run();
* // as shortcut
* $this->_copyDir('dist/config', 'config');
* ?>
* ```
*/
class CopyDir extends BaseDir
{
use ResourceExistenceChecker;
/**
* Explicitly declare our consturctor, so that
* our copyDir() method does not look like a php4 constructor.
*
* @param string|string[] $dirs
*/
public function __construct($dirs)
{
parent::__construct($dirs);
}
/**
* @var int
*/
protected $chmod = 0755;
/**
* Files to exclude on copying.
*
* @var string[]
*/
protected $exclude = [];
/**
* Overwrite destination files newer than source files.
*/
protected $overwrite = true;
/**
* {@inheritdoc}
*/
public function run()
{
if (!$this->checkResources($this->dirs, 'dir')) {
return Result::error($this, 'Source directories are missing!');
}
foreach ($this->dirs as $src => $dst) {
$this->copyDir($src, $dst);
$this->printTaskInfo('Copied from {source} to {destination}', ['source' => $src, 'destination' => $dst]);
}
return Result::success($this);
}
/**
* Sets the default folder permissions for the destination if it doesn't exist
*
* @link https://en.wikipedia.org/wiki/Chmod
* @link https://php.net/manual/en/function.mkdir.php
* @link https://php.net/manual/en/function.chmod.php
*
* @param int $value
*
* @return $this
*/
public function dirPermissions($value)
{
$this->chmod = (int)$value;
return $this;
}
/**
* List files to exclude.
*
* @param string[] $exclude
*
* @return $this
*/
public function exclude($exclude = [])
{
$this->exclude = $this->simplifyForCompare($exclude);
return $this;
}
/**
* Destination files newer than source files are overwritten.
*
* @param bool $overwrite
*
* @return $this
*/
public function overwrite($overwrite)
{
$this->overwrite = $overwrite;
return $this;
}
/**
* Copies a directory to another location.
*
* @param string $src Source directory
* @param string $dst Destination directory
* @param string $parent Parent directory
*
* @throws \Robo\Exception\TaskException
*/
protected function copyDir($src, $dst, $parent = '')
{
$dir = @opendir($src);
if (false === $dir) {
throw new TaskException($this, "Cannot open source directory '" . $src . "'");
}
if (!is_dir($dst)) {
mkdir($dst, $this->chmod, true);
}
while (false !== ($file = readdir($dir))) {
// Support basename and full path exclusion.
if ($this->excluded($file, $src, $parent)) {
continue;
}
$srcFile = $src . '/' . $file;
$destFile = $dst . '/' . $file;
if (is_dir($srcFile)) {
$this->copyDir($srcFile, $destFile, $parent . $file . DIRECTORY_SEPARATOR);
} else {
$this->fs->copy($srcFile, $destFile, $this->overwrite);
}
}
closedir($dir);
}
/**
* Check to see if the current item is excluded.
*
* @param string $file
* @param string $src
* @param string $parent
*
* @return bool
*/
protected function excluded($file, $src, $parent)
{
return
($file == '.') ||
($file == '..') ||
in_array($file, $this->exclude) ||
in_array($this->simplifyForCompare($parent . $file), $this->exclude) ||
in_array($this->simplifyForCompare($src . DIRECTORY_SEPARATOR . $file), $this->exclude);
}
/**
* Avoid problems comparing paths on Windows that may have a
* combination of DIRECTORY_SEPARATOR and /.
*
* @param string$item
*
* @return string
*/
protected function simplifyForCompare($item)
{
return str_replace(DIRECTORY_SEPARATOR, '/', $item);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Robo\Task\Filesystem;
use Robo\Common\ResourceExistenceChecker;
use Robo\Result;
/**
* Deletes dir
*
* ``` php
* <?php
* $this->taskDeleteDir('tmp')->run();
* // as shortcut
* $this->_deleteDir(['tmp', 'log']);
* ?>
* ```
*/
class DeleteDir extends BaseDir
{
use ResourceExistenceChecker;
/**
* {@inheritdoc}
*/
public function run()
{
if (!$this->checkResources($this->dirs, 'dir')) {
return Result::error($this, 'Source directories are missing!');
}
foreach ($this->dirs as $dir) {
$this->fs->remove($dir);
$this->printTaskInfo("Deleted {dir}...", ['dir' => $dir]);
}
return Result::success($this);
}
}

View File

@ -0,0 +1,155 @@
<?php
namespace Robo\Task\Filesystem;
use Robo\Task\StackBasedTask;
use Symfony\Component\Filesystem\Filesystem as sfFilesystem;
use Symfony\Component\Filesystem\Exception\IOException;
use Robo\Contract\BuilderAwareInterface;
use Robo\Common\BuilderAwareTrait;
/**
* Wrapper for [Symfony Filesystem](https://symfony.com/doc/current/components/filesystem.html) Component.
* Comands are executed in stack and can be stopped on first fail with `stopOnFail` option.
*
* ``` php
* <?php
* $this->taskFilesystemStack()
* ->mkdir('logs')
* ->touch('logs/.gitignore')
* ->chgrp('www', 'www-data')
* ->symlink('/var/log/nginx/error.log', 'logs/error.log')
* ->run();
*
* // one line
* $this->_touch('.gitignore');
* $this->_mkdir('logs');
*
* ?>
* ```
*
* @method $this mkdir(string|array|\Traversable $dir, int $mode = 0777)
* @method $this touch(string|array|\Traversable $file, int $time = null, int $atime = null)
* @method $this copy(string $from, string $to, bool $force = false)
* @method $this chmod(string|array|\Traversable $file, int $permissions, int $umask = 0000, bool $recursive = false)
* @method $this chgrp(string|array|\Traversable $file, string $group, bool $recursive = false)
* @method $this chown(string|array|\Traversable $file, string $user, bool $recursive = false)
* @method $this remove(string|array|\Traversable $file)
* @method $this rename(string $from, string $to, bool $force = false)
* @method $this symlink(string $from, string $to, bool $copyOnWindows = false)
* @method $this mirror(string $from, string $to, \Traversable $iterator = null, array $options = [])
*/
class FilesystemStack extends StackBasedTask implements BuilderAwareInterface
{
use BuilderAwareTrait;
/**
* @var \Symfony\Component\Filesystem\Filesystem
*/
protected $fs;
public function __construct()
{
$this->fs = new sfFilesystem();
}
/**
* @return \Symfony\Component\Filesystem\Filesystem
*/
protected function getDelegate()
{
return $this->fs;
}
/**
* @param string $from
* @param string $to
* @param bool $force
*/
protected function _copy($from, $to, $force = false)
{
$this->fs->copy($from, $to, $force);
}
/**
* @param string|string[]|\Traversable $file
* @param int $permissions
* @param int $umask
* @param bool $recursive
*/
protected function _chmod($file, $permissions, $umask = 0000, $recursive = false)
{
$this->fs->chmod($file, $permissions, $umask, $recursive);
}
/**
* @param string|string[]|\Traversable $file
* @param string $group
* @param bool $recursive
*/
protected function _chgrp($file, $group, $recursive = null)
{
$this->fs->chgrp($file, $group, $recursive);
}
/**
* @param string|string[]|\Traversable $file
* @param string $user
* @param bool $recursive
*/
protected function _chown($file, $user, $recursive = null)
{
$this->fs->chown($file, $user, $recursive);
}
/**
* @param string $origin
* @param string $target
* @param bool $overwrite
*
* @return null|true|\Robo\Result
*/
protected function _rename($origin, $target, $overwrite = false)
{
// we check that target does not exist
if ((!$overwrite && is_readable($target)) || (file_exists($target) && !is_writable($target))) {
throw new IOException(sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target);
}
// Due to a bug (limitation) in PHP, cross-volume renames do not work.
// See: https://bugs.php.net/bug.php?id=54097
if (true !== @rename($origin, $target)) {
return $this->crossVolumeRename($origin, $target);
}
return true;
}
/**
* @param string $origin
* @param string $target
*
* @return null|\Robo\Result
*/
protected function crossVolumeRename($origin, $target)
{
// First step is to try to get rid of the target. If there
// is a single, deletable file, then we will just unlink it.
if (is_file($target)) {
unlink($target);
}
// If the target still exists, we will try to delete it.
// TODO: Note that if this fails partway through, then we cannot
// adequately rollback. Perhaps we need to preflight the operation
// and determine if everything inside of $target is writable.
if (file_exists($target)) {
$this->fs->remove($target);
}
/** @var \Robo\Result $result */
$result = $this->collectionBuilder()->taskCopyDir([$origin => $target])->run();
if (!$result->wasSuccessful()) {
return $result;
}
$this->fs->remove($origin);
}
}

View File

@ -0,0 +1,294 @@
<?php
namespace Robo\Task\Filesystem;
use Robo\Result;
use Robo\Exception\TaskException;
use Symfony\Component\Finder\Finder;
/**
* Searches for files in a nested directory structure and copies them to
* a target directory with or without the parent directories. The task was
* inspired by [gulp-flatten](https://www.npmjs.com/package/gulp-flatten).
*
* Example directory structure:
*
* ```
* └── assets
* ├── asset-library1
* │ ├── README.md
* │ └── asset-library1.min.js
* └── asset-library2
* ├── README.md
* └── asset-library2.min.js
* ```
*
* The following code will search the `*.min.js` files and copy them
* inside a new `dist` folder:
*
* ``` php
* <?php
* $this->taskFlattenDir(['assets/*.min.js' => 'dist'])->run();
* // or use shortcut
* $this->_flattenDir('assets/*.min.js', 'dist');
* ?>
* ```
*
* You can also define the target directory with an additional method, instead of
* key/value pairs. More similar to the gulp-flatten syntax:
*
* ``` php
* <?php
* $this->taskFlattenDir(['assets/*.min.js'])
* ->to('dist')
* ->run();
* ?>
* ```
*
* You can also append parts of the parent directories to the target path. If you give
* the value `1` to the `includeParents()` method, then the top parent will be appended
* to the target directory resulting in a path such as `dist/assets/asset-library1.min.js`.
*
* If you give a negative number, such as `-1` (the same as specifying `array(0, 1)` then
* the bottom parent will be appended, resulting in a path such as
* `dist/asset-library1/asset-library1.min.js`.
*
* The top parent directory will always be starting from the relative path to the current
* directory. You can override that with the `parentDir()` method. If in the above example
* you would specify `assets`, then the top parent directory would be `asset-library1`.
*
* ``` php
* <?php
* $this->taskFlattenDir(['assets/*.min.js' => 'dist'])
* ->parentDir('assets')
* ->includeParents(1)
* ->run();
* ?>
* ```
*/
class FlattenDir extends BaseDir
{
/**
* @var int
*/
protected $chmod = 0755;
/**
* @var int[]
*/
protected $parents = array(0, 0);
/**
* @var string
*/
protected $parentDir = '';
/**
* @var string
*/
protected $to;
/**
* {@inheritdoc}
*/
public function __construct($dirs)
{
parent::__construct($dirs);
$this->parentDir = getcwd();
}
/**
* {@inheritdoc}
*/
public function run()
{
// find the files
$files = $this->findFiles($this->dirs);
// copy the files
$this->copyFiles($files);
$fileNoun = count($files) == 1 ? ' file' : ' files';
$this->printTaskSuccess("Copied {count} $fileNoun to {destination}", ['count' => count($files), 'destination' => $this->to]);
return Result::success($this);
}
/**
* Sets the default folder permissions for the destination if it does not exist.
*
* @link https://en.wikipedia.org/wiki/Chmod
* @link https://php.net/manual/en/function.mkdir.php
* @link https://php.net/manual/en/function.chmod.php
*
* @param int $permission
*
* @return $this
*/
public function dirPermissions($permission)
{
$this->chmod = (int) $permission;
return $this;
}
/**
* Sets the value from which direction and how much parent dirs should be included.
* Accepts a positive or negative integer or an array with two integer values.
*
* @param int|int[] $parents
*
* @return $this
*
* @throws TaskException
*/
public function includeParents($parents)
{
if (is_int($parents)) {
// if an integer is given check whether it is for top or bottom parent
if ($parents >= 0) {
$this->parents[0] = $parents;
return $this;
}
$this->parents[1] = 0 - $parents;
return $this;
}
if (is_array($parents)) {
// check if the array has two values no more, no less
if (count($parents) == 2) {
$this->parents = $parents;
return $this;
}
}
throw new TaskException($this, 'includeParents expects an integer or an array with two values');
}
/**
* Sets the parent directory from which the relative parent directories will be calculated.
*
* @param string $dir
*
* @return $this
*/
public function parentDir($dir)
{
if (!$this->fs->isAbsolutePath($dir)) {
// attach the relative path to current working directory
$dir = getcwd() . '/' . $dir;
}
$this->parentDir = $dir;
return $this;
}
/**
* Sets the target directory where the files will be copied to.
*
* @param string $target
*
* @return $this
*/
public function to($target)
{
$this->to = rtrim($target, '/');
return $this;
}
/**
* @param array $dirs
*
* @return array|\Robo\Result
*
* @throws \Robo\Exception\TaskException
*/
protected function findFiles($dirs)
{
$files = array();
// find the files
foreach ($dirs as $k => $v) {
// reset finder
$finder = new Finder();
$dir = $k;
$to = $v;
// check if target was given with the to() method instead of key/value pairs
if (is_int($k)) {
$dir = $v;
if (isset($this->to)) {
$to = $this->to;
} else {
throw new TaskException($this, 'target directory is not defined');
}
}
try {
$finder->files()->in($dir);
} catch (\InvalidArgumentException $e) {
// if finder cannot handle it, try with in()->name()
if (strpos($dir, '/') === false) {
$dir = './' . $dir;
}
$parts = explode('/', $dir);
$new_dir = implode('/', array_slice($parts, 0, -1));
try {
$finder->files()->in($new_dir)->name(array_pop($parts));
} catch (\InvalidArgumentException $e) {
return Result::fromException($this, $e);
}
}
foreach ($finder as $file) {
// store the absolute path as key and target as value in the files array
$files[$file->getRealpath()] = $this->getTarget($file->getRealPath(), $to);
}
$fileNoun = count($files) == 1 ? ' file' : ' files';
$this->printTaskInfo("Found {count} $fileNoun in {dir}", ['count' => count($files), 'dir' => $dir]);
}
return $files;
}
/**
* @param string $file
* @param string $to
*
* @return string
*/
protected function getTarget($file, $to)
{
$target = $to . '/' . basename($file);
if ($this->parents !== array(0, 0)) {
// if the parent is set, create additional directories inside target
// get relative path to parentDir
$rel_path = $this->fs->makePathRelative(dirname($file), $this->parentDir);
// get top parents and bottom parents
$parts = explode('/', rtrim($rel_path, '/'));
$prefix_dir = '';
$prefix_dir .= ($this->parents[0] > 0 ? implode('/', array_slice($parts, 0, $this->parents[0])) . '/' : '');
$prefix_dir .= ($this->parents[1] > 0 ? implode('/', array_slice($parts, (0 - $this->parents[1]), $this->parents[1])) : '');
$prefix_dir = rtrim($prefix_dir, '/');
$target = $to . '/' . $prefix_dir . '/' . basename($file);
}
return $target;
}
/**
* @param array $files
*/
protected function copyFiles($files)
{
// copy the files
foreach ($files as $from => $to) {
// check if target dir exists
if (!is_dir(dirname($to))) {
$this->fs->mkdir(dirname($to), $this->chmod);
}
$this->fs->copy($from, $to);
}
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Robo\Task\Filesystem;
use Robo\Result;
/**
* Mirrors a directory to another
*
* ``` php
* <?php
* $this->taskMirrorDir(['dist/config/' => 'config/'])->run();
* // or use shortcut
* $this->_mirrorDir('dist/config/', 'config/');
*
* ?>
* ```
*/
class MirrorDir extends BaseDir
{
/**
* {@inheritdoc}
*/
public function run()
{
foreach ($this->dirs as $src => $dst) {
$this->fs->mirror(
$src,
$dst,
null,
[
'override' => true,
'copy_on_windows' => true,
'delete' => true
]
);
$this->printTaskInfo("Mirrored from {source} to {destination}", ['source' => $src, 'destination' => $dst]);
}
return Result::success($this);
}
}

View File

@ -0,0 +1,160 @@
<?php
namespace Robo\Task\Filesystem;
trait Shortcuts
{
/**
* @param string $src
* @param string $dst
*
* @return \Robo\Result
*/
protected function _copyDir($src, $dst)
{
return $this->taskCopyDir([$src => $dst])->run();
}
/**
* @param string $src
* @param string $dst
*
* @return \Robo\Result
*/
protected function _mirrorDir($src, $dst)
{
return $this->taskMirrorDir([$src => $dst])->run();
}
/**
* @param string|string[] $dir
*
* @return \Robo\Result
*/
protected function _deleteDir($dir)
{
return $this->taskDeleteDir($dir)->run();
}
/**
* @param string|string[] $dir
*
* @return \Robo\Result
*/
protected function _cleanDir($dir)
{
return $this->taskCleanDir($dir)->run();
}
/**
* @param string $from
* @param string $to
* @param bool $overwrite
*
* @return \Robo\Result
*/
protected function _rename($from, $to, $overwrite = false)
{
return $this->taskFilesystemStack()->rename($from, $to, $overwrite)->run();
}
/**
* @param string|string[] $dir
*
* @return \Robo\Result
*/
protected function _mkdir($dir)
{
return $this->taskFilesystemStack()->mkdir($dir)->run();
}
/**
* @param string $prefix
* @param string $base
* @param bool $includeRandomPart
*
* @return string
*/
protected function _tmpDir($prefix = 'tmp', $base = '', $includeRandomPart = true)
{
$result = $this->taskTmpDir($prefix, $base, $includeRandomPart)->run();
return isset($result['path']) ? $result['path'] : '';
}
/**
* @param string $file
*
* @return \Robo\Result
*/
protected function _touch($file)
{
return $this->taskFilesystemStack()->touch($file)->run();
}
/**
* @param string|string[] $file
*
* @return \Robo\Result
*/
protected function _remove($file)
{
return $this->taskFilesystemStack()->remove($file)->run();
}
/**
* @param string|string[] $file
* @param string $group
*
* @return \Robo\Result
*/
protected function _chgrp($file, $group)
{
return $this->taskFilesystemStack()->chgrp($file, $group)->run();
}
/**
* @param string|string[] $file
* @param int $permissions
* @param int $umask
* @param bool $recursive
*
* @return \Robo\Result
*/
protected function _chmod($file, $permissions, $umask = 0000, $recursive = false)
{
return $this->taskFilesystemStack()->chmod($file, $permissions, $umask, $recursive)->run();
}
/**
* @param string $from
* @param string $to
*
* @return \Robo\Result
*/
protected function _symlink($from, $to)
{
return $this->taskFilesystemStack()->symlink($from, $to)->run();
}
/**
* @param string $from
* @param string $to
*
* @return \Robo\Result
*/
protected function _copy($from, $to)
{
return $this->taskFilesystemStack()->copy($from, $to)->run();
}
/**
* @param string $from
* @param string $to
*
* @return \Robo\Result
*/
protected function _flattenDir($from, $to)
{
return $this->taskFlattenDir([$from => $to])->run();
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace Robo\Task\Filesystem;
trait Tasks
{
/**
* @param string|string[] $dirs
*
* @return \Robo\Task\Filesystem\CleanDir|\Robo\Collection\CollectionBuilder
*/
protected function taskCleanDir($dirs)
{
return $this->task(CleanDir::class, $dirs);
}
/**
* @param string|string[] $dirs
*
* @return \Robo\Task\Filesystem\DeleteDir|\Robo\Collection\CollectionBuilder
*/
protected function taskDeleteDir($dirs)
{
return $this->task(DeleteDir::class, $dirs);
}
/**
* @param string $prefix
* @param string $base
* @param bool $includeRandomPart
*
* @return \Robo\Task\Filesystem\WorkDir|\Robo\Collection\CollectionBuilder
*/
protected function taskTmpDir($prefix = 'tmp', $base = '', $includeRandomPart = true)
{
return $this->task(TmpDir::class, $prefix, $base, $includeRandomPart);
}
/**
* @param string $finalDestination
*
* @return \Robo\Task\Filesystem\TmpDir|\Robo\Collection\CollectionBuilder
*/
protected function taskWorkDir($finalDestination)
{
return $this->task(WorkDir::class, $finalDestination);
}
/**
* @param string|string[] $dirs
*
* @return \Robo\Task\Filesystem\CopyDir|\Robo\Collection\CollectionBuilder
*/
protected function taskCopyDir($dirs)
{
return $this->task(CopyDir::class, $dirs);
}
/**
* @param string|string[] $dirs
*
* @return \Robo\Task\Filesystem\MirrorDir|\Robo\Collection\CollectionBuilder
*/
protected function taskMirrorDir($dirs)
{
return $this->task(MirrorDir::class, $dirs);
}
/**
* @param string|string[] $dirs
*
* @return \Robo\Task\Filesystem\FlattenDir|\Robo\Collection\CollectionBuilder
*/
protected function taskFlattenDir($dirs)
{
return $this->task(FlattenDir::class, $dirs);
}
/**
* @return \Robo\Task\Filesystem\FilesystemStack|\Robo\Collection\CollectionBuilder
*/
protected function taskFilesystemStack()
{
return $this->task(FilesystemStack::class);
}
}

View File

@ -0,0 +1,173 @@
<?php
namespace Robo\Task\Filesystem;
use Robo\Result;
use Robo\Contract\CompletionInterface;
/**
* Create a temporary directory that is automatically cleaned up
* once the task collection is is part of completes.
*
* Use WorkDir if you do not want the directory to be deleted.
*
* ``` php
* <?php
* // Delete on rollback or on successful completion.
* // Note that in this example, everything is deleted at
* // the end of $collection->run().
* $collection = $this->collectionBuilder();
* $tmpPath = $collection->tmpDir()->getPath();
* $collection->taskFilesystemStack()
* ->mkdir("$tmpPath/log")
* ->touch("$tmpPath/log/error.txt");
* $collection->run();
* // as shortcut (deleted when program exits)
* $tmpPath = $this->_tmpDir();
* ?>
* ```
*/
class TmpDir extends BaseDir implements CompletionInterface
{
/**
* @var string
*/
protected $base;
/**
* @var string
*/
protected $prefix;
/**
* @var bool
*/
protected $cwd;
/**
* @var string
*/
protected $savedWorkingDirectory;
/**
* @param string $prefix
* @param string $base
* @param bool $includeRandomPart
*/
public function __construct($prefix = 'tmp', $base = '', $includeRandomPart = true)
{
if (empty($base)) {
$base = sys_get_temp_dir();
}
$path = "{$base}/{$prefix}";
if ($includeRandomPart) {
$path = static::randomLocation($path);
}
parent::__construct(["$path"]);
}
/**
* Add a random part to a path, ensuring that the directory does
* not (currently) exist.
*
* @param string $path The base/prefix path to add a random component to
* @param int $length Number of digits in the random part
*
* @return string
*/
protected static function randomLocation($path, $length = 12)
{
$random = static::randomString($length);
while (is_dir("{$path}_{$random}")) {
$random = static::randomString($length);
}
return "{$path}_{$random}";
}
/**
* Generate a suitably random string to use as the suffix for our
* temporary directory.
*
* @param int $length
*
* @return string
*/
protected static function randomString($length = 12)
{
return substr(str_shuffle('23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'), 0, max($length, 3));
}
/**
* Flag that we should cwd to the temporary directory when it is
* created, and restore the old working directory when it is deleted.
*
* @param bool $shouldChangeWorkingDirectory
*
* @return $this
*/
public function cwd($shouldChangeWorkingDirectory = true)
{
$this->cwd = $shouldChangeWorkingDirectory;
return $this;
}
/**
* {@inheritdoc}
*/
public function run()
{
// Save the current working directory
$this->savedWorkingDirectory = getcwd();
foreach ($this->dirs as $dir) {
$this->fs->mkdir($dir);
$this->printTaskInfo("Created {dir}...", ['dir' => $dir]);
// Change the current working directory, if requested
if ($this->cwd) {
chdir($dir);
}
}
return Result::success($this, '', ['path' => $this->getPath()]);
}
protected function restoreWorkingDirectory()
{
// Restore the current working directory, if we redirected it.
if ($this->cwd) {
chdir($this->savedWorkingDirectory);
}
}
protected function deleteTmpDir()
{
foreach ($this->dirs as $dir) {
$this->fs->remove($dir);
}
}
/**
* Delete this directory when our collection completes.
* If this temporary directory is not part of a collection,
* then it will be deleted when the program terminates,
* presuming that it was created by taskTmpDir() or _tmpDir().
*/
public function complete()
{
$this->restoreWorkingDirectory();
$this->deleteTmpDir();
}
/**
* Get a reference to the path to the temporary directory, so that
* it may be used to create other tasks. Note that the directory
* is not actually created until the task runs.
*
* @return string
*/
public function getPath()
{
return $this->dirs[0];
}
}

View File

@ -0,0 +1,126 @@
<?php
namespace Robo\Task\Filesystem;
use Robo\Result;
use Robo\Contract\RollbackInterface;
use Robo\Contract\BuilderAwareInterface;
use Robo\Common\BuilderAwareTrait;
/**
* Create a temporary working directory that is automatically renamed to its
* final desired location if all of the tasks in the collection succeed. If
* there is a rollback, then the working directory is deleted.
*
* ``` php
* <?php
* $collection = $this->collectionBuilder();
* $workingPath = $collection->workDir("build")->getPath();
* $collection->taskFilesystemStack()
* ->mkdir("$workingPath/log")
* ->touch("$workingPath/log/error.txt");
* $collection->run();
* ?>
* ```
*/
class WorkDir extends TmpDir implements RollbackInterface, BuilderAwareInterface
{
use BuilderAwareTrait;
/**
* @var string
*/
protected $finalDestination;
/**
* @param string $finalDestination
*/
public function __construct($finalDestination)
{
$this->finalDestination = $finalDestination;
// Create a temporary directory to work in. We will place our
// temporary directory in the same location as the final destination
// directory, so that the work directory can be moved into place
// without having to be copied, e.g. in a cross-volume rename scenario.
parent::__construct(basename($finalDestination), dirname($finalDestination));
}
/**
* Create our working directory.
*
* @return \Robo\Result
*/
public function run()
{
// Destination cannot be empty
if (empty($this->finalDestination)) {
return Result::error($this, "Destination directory not specified.");
}
// Before we do anything else, ensure that any directory in the
// final destination is writable, so that we can at a minimum
// move it out of the way before placing our results there.
if (is_dir($this->finalDestination)) {
if (!is_writable($this->finalDestination)) {
return Result::error($this, "Destination directory {dir} exists and cannot be overwritten.", ['dir' => $this->finalDestination]);
}
}
return parent::run();
}
/**
* Move our working directory into its final destination once the
* collection it belongs to completes.
*/
public function complete()
{
$this->restoreWorkingDirectory();
// Delete the final destination, if it exists.
// Move it out of the way first, in case it cannot
// be completely deleted.
if (file_exists($this->finalDestination)) {
$temporaryLocation = static::randomLocation($this->finalDestination . '_TO_DELETE_');
// This should always work, because we already created a temporary
// folder in the parent directory of the final destination, and we
// have already checked to confirm that the final destination is
// writable.
rename($this->finalDestination, $temporaryLocation);
// This may silently fail, leaving artifacts behind, if there
// are permissions problems with some items somewhere inside
// the folder being deleted.
$this->fs->remove($temporaryLocation);
}
// Move our working directory over the final destination.
// This should never be a cross-volume rename, so this should
// always succeed.
$workDir = reset($this->dirs);
if (file_exists($workDir)) {
rename($workDir, $this->finalDestination);
}
}
/**
* Delete our working directory
*/
public function rollback()
{
$this->restoreWorkingDirectory();
$this->deleteTmpDir();
}
/**
* Get a reference to the path to the temporary directory, so that
* it may be used to create other tasks. Note that the directory
* is not actually created until the task runs.
*
* @return string
*/
public function getPath()
{
return $this->dirs[0];
}
}