Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/Filesystem/Filesystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

namespace Meteor\Filesystem;

use FilesystemIterator;
use Meteor\Filesystem\Finder\FinderFactory;
use Meteor\IO\IOInterface;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RuntimeException;
use Symfony\Component\Filesystem\Filesystem as BaseFilesystem;

Expand Down Expand Up @@ -219,4 +222,26 @@ public function replaceDirectory(string $sourceDir, string $targetDir, string $r
$this->io->debug(sprintf('Removing %s', $old));
$this->remove($old);
}

/**
* @param string $directory
*
* @return int
*/
public function getDirectorySize($directory)
{
$totalBytes = 0;

$path = realpath($directory);

if ($path !== false && $path != '' && is_dir($path)) {
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS)) as $object) {
if (!$object->isLink()) {
$totalBytes += $object->getSize();
}
}
}

return $totalBytes;
}
}
13 changes: 13 additions & 0 deletions src/IO/ConsoleIO.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ public function isInteractive()
return $this->input->isInteractive();
}

/**
* {@inheritdoc}
*/
public function formatFileSize($bytes, $dec = 2)
{
$suffix = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];

$base = 1024;
$class = min((int) log($bytes, $base), count($suffix) - 1);

return sprintf("%1.{$dec}f", $bytes / pow($base, $class)) . ' ' . $suffix[$class];
}

/**
* {@inheritdoc}
*/
Expand Down
8 changes: 8 additions & 0 deletions src/IO/IOInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ interface IOInterface
*/
public function isInteractive();

/**
* @param int $bytes
* @param int $dec
*
* @return string
*/
public function formatFileSize($bytes, $dec = 2);

/**
* Gets argument by name.
*
Expand Down
8 changes: 8 additions & 0 deletions src/IO/NullIO.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ public function isInteractive()
return false;
}

/**
* {@inheritdoc}
*/
public function formatFileSize($bytes, $dec = 2)
{
return '';
}

/**
* {@inheritdoc}
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Patch/Strategy/Overwrite/OverwritePatchStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public function apply($patchDir, $installDir, array $options)
$tasks[] = new LimitBackups($backupsDir, $installDir, $options['limit-backups']);
}

$tasks[] = new CheckDiskSpace($installDir, $backupsDir);
$tasks[] = new CheckDiskSpace($installDir, $backupsDir, $patchFilesDir);

if (!$options['skip-backup']) {
$tasks[] = new BackupFiles($backupDir, $patchDir, $installDir);
Expand Down
8 changes: 7 additions & 1 deletion src/Patch/Task/CheckDiskSpace.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ class CheckDiskSpace
*/
public $backupsDir;

/**
* @var string
*/
public $patchFilesDir;

/**
* @param string $installDir
* @param string $backupsDir
*/
public function __construct($installDir, $backupsDir)
public function __construct($installDir, $backupsDir, $patchFilesDir)
{
$this->installDir = $installDir;
$this->backupsDir = $backupsDir;
$this->patchFilesDir = $patchFilesDir;
}
}
51 changes: 35 additions & 16 deletions src/Patch/Task/CheckDiskSpaceHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,11 @@ class CheckDiskSpaceHandler
use BackupHandlerTrait;

/**
* Assuming required space is 300MB for backup and new files. Not checking the real package
* size to avoid performance issues when checking the size of thousands of files.
* Free space must be at least patch size multiplied by this number. Should
* ensure we set this large enough to make backup copies of everything in
* the patch.
*/
public const REQUIRED_BYTES = 314572800;

/**
* The required free space as a percentage.
*/
public const REQUIRED_FREE_SPACE_PERCENT = 10;
public const PATCH_SIZE_MULTIPLIER = 2.5;

/**
* The maximum number of backups to keep when running low on disk space.
Expand Down Expand Up @@ -62,18 +58,24 @@ public function __construct(BackupFinder $backupFinder, Filesystem $filesystem,
*/
public function handle(CheckDiskSpace $task, array $config)
{
if ($this->hasFreeSpace($task->installDir)) {
$spaceRequired = $this->calculateRequiredDiskSpace($task->patchFilesDir);

if ($this->hasFreeSpace($task->installDir, $spaceRequired)) {
// Plenty of space available
return true;
}

$this->io->warning('Patching will reduce free disk space to less than ' . self::REQUIRED_FREE_SPACE_PERCENT . '%');
$this->io->warning(sprintf(
'There is not enough free disk space to apply this patch. Space required: %s, Space available: %s',
$this->io->formatFileSize($spaceRequired),
$this->io->formatFileSize(disk_free_space($task->installDir))
));

// Try removing old backups
$this->removeOldBackups($task->backupsDir, $task->installDir, $config);

// Check disk space again
if ($this->hasFreeSpace($task->installDir)) {
if ($this->hasFreeSpace($task->installDir, $spaceRequired)) {
return true;
}

Expand All @@ -85,19 +87,36 @@ public function handle(CheckDiskSpace $task, array $config)
return true;
}

private function calculateRequiredDiskSpace($patchDirectory)
{
$patchSize = $this->filesystem->getDirectorySize($patchDirectory);

return $patchSize * static::PATCH_SIZE_MULTIPLIER;
}

/**
* @param string $installDir
* @param int $spaceRequired
*
* @return bool
*/
private function hasFreeSpace($installDir)
private function hasFreeSpace($installDir, $spaceRequired)
{
$totalSpace = disk_total_space($installDir);
$freeSpace = disk_free_space($installDir) - self::REQUIRED_BYTES;
$freeSpace = disk_free_space($installDir);

$this->io->debug(sprintf(
'Available disk space: %s',
$this->io->formatFileSize($freeSpace)
));

$this->io->debug(sprintf(
'Disk space required: %s',
$this->io->formatFileSize($spaceRequired)
));

$freeSpacePercent = ($freeSpace / $totalSpace) * 100;
$resultingSpace = $freeSpace - $spaceRequired;

return $freeSpacePercent > self::REQUIRED_FREE_SPACE_PERCENT;
return $resultingSpace > 0;
}

/**
Expand Down
22 changes: 22 additions & 0 deletions tests/IO/ConsoleIOTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,26 @@ public function testNewLine()

static::assertSame("\n", $this->getOutput());
}

/**
* @dataProvider formatFileSizeProvider
*/
public function testFormatFileSize($bytes, $dec, $expected)
{
$actual = $this->io->formatFileSize($bytes, $dec);

static::assertEquals($expected, $actual);
}

public function formatFileSizeProvider()
{
return [
['1024', 2, '1.00 KiB'],
['1234', 2, '1.21 KiB'],
['123456789', 2, '117.74 MiB'],
['1048576', 2, '1.00 MiB'],
['1048576', 0, '1 MiB'],
['100456789012', 2, '93.56 GiB'],
];
}
}
83 changes: 65 additions & 18 deletions tests/Patch/Task/CheckDiskSpaceHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

class CheckDiskSpaceHandlerTest extends TestCase
{
private const PATCH_SIZE_BYTES = 346030080;

private $backupFinder;
private $filesystem;
private $io;
Expand All @@ -18,23 +20,32 @@ protected function setUp(): void
{
$this->backupFinder = Mockery::mock('Meteor\Patch\Backup\BackupFinder');
$this->filesystem = Mockery::mock('Meteor\Filesystem\Filesystem');

$this->filesystem->shouldReceive('getDirectorySize')
->with('/path/to/patch')
->andReturn(static::PATCH_SIZE_BYTES);

$this->io = new NullIO();

$this->handler = new CheckDiskSpaceHandler($this->backupFinder, $this->filesystem, $this->io);
}

public function testPlentyOfSpace()
{
$GLOBALS['disk_total_space'] = 1048576000;
$GLOBALS['disk_free_space'] = 1048576000;

$config = ['name' => 'test'];
static::assertTrue($this->handler->handle(new CheckDiskSpace('install', 'install/backups'), $config));

$this->filesystem->shouldReceive('getDirectorySize')
->with('/path/to/patch')
->once()
->andReturn(static::PATCH_SIZE_BYTES);

static::assertTrue($this->handler->handle(new CheckDiskSpace('install', 'install/backups', '/path/to/patch'), $config));
}

public function testWhenRunningLowOnSpace()
{
$GLOBALS['disk_total_space'] = 1048576000;
$GLOBALS['disk_free_space'] = 419430400;

$config = ['name' => 'test'];
Expand All @@ -43,13 +54,17 @@ public function testWhenRunningLowOnSpace()
->with('install/backups', 'install', $config)
->andReturn([]);

static::assertFalse($this->handler->handle(new CheckDiskSpace('install', 'install/backups'), $config));
$this->filesystem->shouldReceive('getDirectorySize')
->with('/path/to/patch')
->once()
->andReturn(static::PATCH_SIZE_BYTES);

static::assertFalse($this->handler->handle(new CheckDiskSpace('install', 'install/backups', '/path/to/patch'), $config));
}

public function testRemovesOldBackupsWhenRunningLowOnSpace()
{
$GLOBALS['disk_total_space'] = 1048576000;
$GLOBALS['disk_free_space'] = 419430400;
$GLOBALS['disk_free_space'] = static::PATCH_SIZE_BYTES;

$config = ['name' => 'test'];

Expand All @@ -70,7 +85,7 @@ public function testRemovesOldBackupsWhenRunningLowOnSpace()
->with('backups/3')
->andReturnUsing(function () {
// Free up some space
$GLOBALS['disk_free_space'] += 104857600;
$GLOBALS['disk_free_space'] += static::PATCH_SIZE_BYTES;
})
->once();

Expand All @@ -90,13 +105,12 @@ public function testRemovesOldBackupsWhenRunningLowOnSpace()
})
->once();

static::assertTrue($this->handler->handle(new CheckDiskSpace('install', 'install/backups'), $config));
static::assertTrue($this->handler->handle(new CheckDiskSpace('install', 'install/backups', '/path/to/patch'), $config));
}

public function testDoesNotRemoveMostRecentBackups()
{
$GLOBALS['disk_total_space'] = 1048576000;
$GLOBALS['disk_free_space'] = 419430400;
$GLOBALS['disk_free_space'] = static::PATCH_SIZE_BYTES;

$config = ['name' => 'test'];

Expand All @@ -113,13 +127,12 @@ public function testDoesNotRemoveMostRecentBackups()
$this->filesystem->shouldReceive('remove')
->never();

static::assertFalse($this->handler->handle(new CheckDiskSpace('install', 'install/backups'), $config));
static::assertFalse($this->handler->handle(new CheckDiskSpace('install', 'install/backups', '/path/to/patch'), $config));
}

public function testRemovesOldBackupsWhenRunningLowOnSpaceButNotEnoughIsFreedUp()
{
$GLOBALS['disk_total_space'] = 1048576000;
$GLOBALS['disk_free_space'] = 104857600;
$GLOBALS['disk_free_space'] = 2000;

$config = ['name' => 'test'];

Expand Down Expand Up @@ -160,13 +173,47 @@ public function testRemovesOldBackupsWhenRunningLowOnSpaceButNotEnoughIsFreedUp(
})
->once();

static::assertFalse($this->handler->handle(new CheckDiskSpace('install', 'install/backups'), $config));
static::assertFalse($this->handler->handle(new CheckDiskSpace('install', 'install/backups', '/path/to/patch'), $config));
}
}

function disk_total_space($directory)
{
return $GLOBALS['disk_total_space'] ?? 1048576000;
public function testWarningOutputWhenNotEnoughSpace()
{
$GLOBALS['disk_free_space'] = 2000000;

$config = ['name' => 'test'];

$backups = [
new Backup('backups/2', []),
new Backup('backups/1', []),
];

$this->backupFinder->shouldReceive('find')
->with('install/backups', 'install', $config)
->andReturn($backups)
->once();

$io = Mockery::mock(\Meteor\IO\IOInterface::class, [
'askConfirmation' => null,
'debug' => null,
'formatFileSize' => '',
]);

$this->handler = new CheckDiskSpaceHandler($this->backupFinder, $this->filesystem, $io);

$io->shouldReceive('warning')
->once()
->with('There is not enough free disk space to apply this patch. Space required: 825.00 MiB, Space available: 1.91 MiB');

$io->shouldReceive('formatFileSize')
->with(static::PATCH_SIZE_BYTES * CheckDiskSpaceHandler::PATCH_SIZE_MULTIPLIER)
->andReturn('825.00 MiB');

$io->shouldReceive('formatFileSize')
->with($GLOBALS['disk_free_space'])
->andReturn('1.91 MiB');

static::assertFalse($this->handler->handle(new CheckDiskSpace('install', 'install/backups', '/path/to/patch'), $config));
}
}

function disk_free_space($directory)
Expand Down