Skip to content
Snippets Groups Projects
Commit e9bc3ec3 authored by Adam G-H's avatar Adam G-H
Browse files

Issue #3231997 by phenaproxima: Ensure there is enough disk space to do the update

parent cb675d3a
No related branches found
No related tags found
No related merge requests found
......@@ -34,6 +34,12 @@ services:
arguments: ['@package_manager.composer_runner']
tags:
- { name: event_subscriber }
automatic_updates.disk_space_validator:
class: Drupal\automatic_updates\Validator\DiskSpaceValidator
arguments:
- '@automatic_updates.path_locator'
tags:
- { name: event_subscriber }
automatic_updates.path_locator:
class: Drupal\automatic_updates\PathLocator
arguments:
......
<?php
namespace Drupal\automatic_updates\Validator;
use Drupal\automatic_updates\AutomaticUpdatesEvents;
use Drupal\automatic_updates\Event\UpdateEvent;
use Drupal\automatic_updates\PathLocator;
use Drupal\automatic_updates\Validation\ValidationResult;
use Drupal\Component\FileSystem\FileSystem;
use Drupal\Component\Utility\Bytes;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Validates that there is enough free disk space to do automatic updates.
*/
class DiskSpaceValidator implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* The path locator service.
*
* @var \Drupal\automatic_updates\PathLocator
*/
protected $pathLocator;
/**
* Constructs a DiskSpaceValidator object.
*
* @param \Drupal\automatic_updates\PathLocator $path_locator
* The path locator service.
*/
public function __construct(PathLocator $path_locator) {
$this->pathLocator = $path_locator;
}
/**
* Wrapper around the disk_free_space() function.
*
* @param string $path
* The path for which to retrieve the amount of free disk space.
*
* @return float
* The number of bytes of free space on the disk.
*
* @throws \RuntimeException
* If the amount of free space could not be determined.
*/
protected function freeSpace(string $path): float {
$free_space = disk_free_space($path);
if ($free_space === FALSE) {
throw new \RuntimeException("Cannot get disk information for $path.");
}
return $free_space;
}
/**
* Wrapper around the stat() function.
*
* @param string $path
* The path to check.
*
* @return array
* The statistics for the path.
*
* @throws \RuntimeException
* If the statistics could not be determined.
*/
protected function stat(string $path): array {
$stat = stat($path);
if ($stat === FALSE) {
throw new \RuntimeException("Cannot get information for $path.");
}
return $stat;
}
/**
* Checks if two paths are located on the same logical disk.
*
* @param string $root
* The path of the project root.
* @param string $vendor
* The path of the vendor directory.
*
* @return bool
* TRUE if the project root and vendor directory are on the same logical
* disk, FALSE otherwise.
*/
protected function areSameLogicalDisk(string $root, string $vendor): bool {
$root_statistics = $this->stat($root);
$vendor_statistics = $this->stat($vendor);
return $root_statistics['dev'] === $vendor_statistics['dev'];
}
/**
* Checks that there is enough free space to perform automatic updates.
*
* @param \Drupal\automatic_updates\Event\UpdateEvent $event
* The update event object.
*/
public function checkDiskSpace(UpdateEvent $event): void {
$root_path = $this->pathLocator->getProjectRoot();
$vendor_path = $this->pathLocator->getVendorDirectory();
$messages = [];
// @todo Make this configurable.
$minimum_mb = 1024;
$minimum_bytes = Bytes::toNumber($minimum_mb . 'M');
if (!$this->areSameLogicalDisk($root_path, $vendor_path)) {
if ($this->freeSpace($root_path) < $minimum_bytes) {
$messages[] = $this->t('Drupal root filesystem "@root" has insufficient space. There must be at least @space megabytes free.', [
'@root' => $root_path,
'@space' => $minimum_mb,
]);
}
if (is_dir($vendor_path) && $this->freeSpace($vendor_path) < $minimum_bytes) {
$messages[] = $this->t('Vendor filesystem "@vendor" has insufficient space. There must be at least @space megabytes free.', [
'@vendor' => $vendor_path,
'@space' => $minimum_mb,
]);
}
}
elseif ($this->freeSpace($root_path) < $minimum_bytes) {
$messages[] = $this->t('Drupal root filesystem "@root" has insufficient space. There must be at least @space megabytes free.', [
'@root' => $root_path,
'@space' => $minimum_mb,
]);
}
$temp = $this->temporaryDirectory();
if ($this->freeSpace($temp) < $minimum_bytes) {
$messages[] = $this->t('Directory "@temp" has insufficient space. There must be at least @space megabytes free.', [
'@temp' => $temp,
'@space' => $minimum_mb,
]);
}
if ($messages) {
$summary = count($messages) > 1
? $this->t("There is not enough disk space to perform an automatic update.")
: NULL;
$error = ValidationResult::createError($messages, $summary);
$event->addValidationResult($error);
}
}
/**
* Returns the path of the system temporary directory.
*
* @return string
* The absolute path of the system temporary directory.
*/
protected function temporaryDirectory(): string {
return FileSystem::getOsTemporaryDirectory();
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
AutomaticUpdatesEvents::READINESS_CHECK => 'checkDiskSpace',
];
}
}
<?php
namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation;
use Drupal\automatic_updates\Event\UpdateEvent;
use Drupal\automatic_updates\Validation\ValidationResult;
use Drupal\automatic_updates\Validator\DiskSpaceValidator;
use Drupal\Component\Utility\Bytes;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\automatic_updates\Traits\ValidationTestTrait;
/**
* @covers \Drupal\automatic_updates\Validator\DiskSpaceValidator
*
* @group automatic_updates
*/
class DiskSpaceValidatorTest extends KernelTestBase {
use ValidationTestTrait;
/**
* The validator under test.
*
* @var \Drupal\automatic_updates\Validator\DiskSpaceValidator
*/
private $validator;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$path_locator = $this->prophesize('\Drupal\automatic_updates\PathLocator');
$path_locator->getProjectRoot()->willReturn('root');
$path_locator->getVendorDirectory()->willReturn('vendor');
// Create a mocked version of the validator which can be rigged up to return
// specific values for various filesystem checks.
$this->validator = new class ($path_locator->reveal()) extends DiskSpaceValidator {
/**
* Whether the root and vendor directories are on the same logical disk.
*
* @var bool
*/
public $sharedDisk;
/**
* The amount of free space, keyed by location.
*
* @var float[]
*/
public $freeSpace = [];
/**
* {@inheritdoc}
*/
protected function stat(string $path): array {
return [
'dev' => $this->sharedDisk ? 'disk' : uniqid(),
];
}
/**
* {@inheritdoc}
*/
protected function freeSpace(string $path): float {
return $this->freeSpace[$path];
}
/**
* {@inheritdoc}
*/
protected function temporaryDirectory(): string {
return 'temp';
}
};
}
/**
* Data provider for ::testDiskSpaceValidation().
*
* @return mixed[][]
* Sets of arguments to pass to the test method.
*/
public function providerDiskSpaceValidation(): array {
$root_insufficient = t('Drupal root filesystem "root" has insufficient space. There must be at least 1024 megabytes free.');
$vendor_insufficient = t('Vendor filesystem "vendor" has insufficient space. There must be at least 1024 megabytes free.');
$temp_insufficient = t('Directory "temp" has insufficient space. There must be at least 1024 megabytes free.');
$summary = t("There is not enough disk space to perform an automatic update.");
return [
'shared, vendor and temp sufficient, root insufficient' => [
TRUE,
[
'root' => '1M',
'vendor' => '2G',
'temp' => '4G',
],
[
ValidationResult::createError([$root_insufficient]),
],
],
'shared, root and vendor insufficient, temp sufficient' => [
TRUE,
[
'root' => '1M',
'vendor' => '2M',
'temp' => '2G',
],
[
ValidationResult::createError([$root_insufficient]),
],
],
'shared, vendor and root sufficient, temp insufficient' => [
TRUE,
[
'root' => '2G',
'vendor' => '4G',
'temp' => '1M',
],
[
ValidationResult::createError([$temp_insufficient]),
],
],
'shared, root and temp insufficient, vendor sufficient' => [
TRUE,
[
'root' => '1M',
'vendor' => '2G',
'temp' => '2M',
],
[
ValidationResult::createError([
$root_insufficient,
$temp_insufficient,
], $summary),
],
],
'not shared, root insufficient, vendor and temp sufficient' => [
FALSE,
[
'root' => '5M',
'vendor' => '1G',
'temp' => '4G',
],
[
ValidationResult::createError([$root_insufficient]),
],
],
'not shared, vendor insufficient, root and temp sufficient' => [
FALSE,
[
'root' => '2G',
'vendor' => '10M',
'temp' => '4G',
],
[
ValidationResult::createError([$vendor_insufficient]),
],
],
'not shared, root and vendor sufficient, temp insufficient' => [
FALSE,
[
'root' => '1G',
'vendor' => '2G',
'temp' => '3M',
],
[
ValidationResult::createError([$temp_insufficient]),
],
],
'not shared, root and vendor insufficient, temp sufficient' => [
FALSE,
[
'root' => '500M',
'vendor' => '75M',
'temp' => '2G',
],
[
ValidationResult::createError([
$root_insufficient,
$vendor_insufficient,
], $summary),
],
],
];
}
/**
* Tests disk space validation.
*
* @param bool $shared_disk
* Whether the root and vendor directories are on the same logical disk.
* @param array $free_space
* The free space that should be reported for various locations. The keys
* are the locations (only 'root', 'vendor', and 'temp' are supported), and
* the values are the space that should be reported, in a format that can be
* parsed by \Drupal\Component\Utility\Bytes::toNumber().
* @param \Drupal\automatic_updates\Validation\ValidationResult[] $expected_results
* The expected validation results.
*
* @dataProvider providerDiskSpaceValidation
*/
public function testDiskSpaceValidation(bool $shared_disk, array $free_space, array $expected_results): void {
$this->validator->sharedDisk = $shared_disk;
$this->validator->freeSpace = array_map([Bytes::class, 'toNumber'], $free_space);
$event = new UpdateEvent();
$this->validator->checkDiskSpace($event);
$this->assertValidationResultsEqual($expected_results, $event->getResults());
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment