webworld888/app/Console/Commands/XeUpdate.php
2021-10-26 19:14:12 +09:00

423 lines
14 KiB
PHP

<?php
/**
* XeUpdate.php
*
* PHP version 7
*
* @category Commands
* @package App\Console\Commands
* @author XE Team (developers) <developers@xpressengine.com>
* @copyright 2020 Copyright XEHub Corp. <https://www.xehub.io>
* @license http://www.gnu.org/licenses/lgpl-3.0-standalone.html LGPL
* @link http://www.xpressengine.com
*/
namespace App\Console\Commands;
use FilesystemIterator;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use Xpressengine\Foundation\Operator;
use Xpressengine\Foundation\ReleaseProvider;
use Xpressengine\Plugin\Composer\ComposerFileWriter;
use Xpressengine\Support\Migration;
/**
* Class XeUpdate
*
* @category Commands
* @package App\Console\Commands
* @author XE Team (developers) <developers@xpressengine.com>
* @copyright 2020 Copyright XEHub Corp. <https://www.xehub.io>
* @license http://www.gnu.org/licenses/lgpl-3.0-standalone.html LGPL
* @link http://www.xpressengine.com
*/
class XeUpdate extends ShouldOperation
{
/**
* The console command name.
*
* @var string
*/
protected $signature = 'xe:update
{version? : The version of xpressengine for install}
{--skip-composer : skip running composer update.}
{--skip-download : skip downloading update file.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update the XpressEngine';
/**
* Filesystem instance
*
* @var Filesystem
*/
protected $filesystem;
/**
* ReleaseProvider instance
*
* @var ReleaseProvider
*/
protected $releaseProvider;
/**
* ComposerFileWriter instance
*
* @var ComposerFileWriter
*/
protected $writer;
/**
* Create a new controller creator command instance.
*
* @param Operator $operator Operator instance
* @param ReleaseProvider $releaseProvider ReleaseProvider instance
* @param Filesystem $filesystem Filesystem instance
* @param ComposerFileWriter $writer ComposerFileWriter instance
*/
public function __construct(
Operator $operator,
ReleaseProvider $releaseProvider,
Filesystem $filesystem,
ComposerFileWriter $writer
) {
parent::__construct($operator);
$this->filesystem = $filesystem;
$this->releaseProvider = $releaseProvider;
$this->writer = $writer;
}
/**
* Execute the console command.
*
* @return void
* @throws \Throwable|\GuzzleHttp\Exception\GuzzleException
*/
public function handle()
{
$this->output->title('Updating Xpressengine.');
$updateVersion = $this->argument('version') ?: $this->releaseProvider->getLatestCoreVersion();
$skipDownload = $this->option('skip-download');
$skipComposer = $this->option('skip-composer');
if (!in_array($updateVersion, $this->releaseProvider->coreVersions())) {
throw new \RuntimeException("Unknown version [$updateVersion]");
}
// 현재 파일이 업데이트할 버전보다 낮지 않다면 다운로드 하지 않음.
if (version_compare(__XE_VERSION__, $updateVersion) !== -1) {
$skipDownload = true;
}
if ($skipDownload) {
$updateVersion = __XE_VERSION__;
}
// version 안내
$installedVersion = app()->getInstalledVersion();
// 업데이트 버전 정보:
$this->warn('Version information:');
$this->line(" $installedVersion -> $updateVersion");
if (version_compare($installedVersion, $updateVersion) !== -1) {
throw new \RuntimeException("Version [$updateVersion] is already installed.");
}
// confirm
if ($this->input->isInteractive() && $this->confirm(
// Xpressengine ver.".$updateVersion."을 업데이트합니다. 최대 수분이 소요될 수 있습니다.\r\n 업데이트 하시겠습니까?
"The Xpressengine ver.".$updateVersion." will be updated. It may take up to a few minutes. \r\nDo you want to update?"
) === false
) {
return;
}
$this->startCore(function ($operator) use ($updateVersion, $skipDownload, $skipComposer, $installedVersion) {
$operator->version($updateVersion);
$operator->save();
$this->writer->reset()->write(true);
$tempPath = storage_path('app/temp');
// download
if (!$skipDownload) {
if (!$this->filesystem->isWritable(base_path())) {
throw new \RuntimeException('Could not write to project root.');
}
$this->output->section('Clone project.');
$this->cloneProject($tempPath);
$this->output->section('Download.');
$this->download($updateVersion, $tempPath);
// download 를 수행하는 경우 반드시 composer 를 실행함.
$skipComposer = false;
$workDir = $tempPath;
} else {
$workDir = base_path();
}
// composer update실행(composer update --no-dev)
if (!$skipComposer) {
$this->output->section('Checking environment for Composer.');
$this->prepareComposer();
$this->output->section('Composer update command is running.. It may take up to a few minutes.');
$this->line('> composer update');
$result = $this->runComposer([
'command' => 'update',
'--working-dir' => $workDir,
'--no-autoloader' => true,
'--no-suggest' => true,
], true, $this->output);
if (0 !== $result) {
throw new \RuntimeException('Composer update failed..', $result);
}
}
if (!$skipDownload) {
$this->output->section('Merging to project.');
$this->mergeToProject($tempPath);
}
if (!$skipComposer) {
$this->output->section('Dump autoload.');
$this->line('> composer dump-autoload');
$result = $this->runComposer([
'command' => 'dump-autoload',
'--working-dir' => base_path(),
], false, $this->output);
if (0 !== $result) {
throw new \RuntimeException('Failed to composer dump.');
}
}
// migration
\Artisan::call('cache:clear');
$this->output->section('Running migration..');
$this->migrateCore($installedVersion);
});
// mark installed
$this->markInstalled($updateVersion);
$this->output->success("Update the Xpressengine to ver.{$updateVersion}.");
}
/**
* Clone project for update
*
* @param string $destination destination dir
* @return void
* @throws \Exception
*/
private function cloneProject($destination)
{
if ($this->filesystem->isDirectory($destination)) {
if (!$this->filesystem->deleteDirectory($destination)) {
throw new \RuntimeException('Failed to empty directory.');
}
}
$sources = ['composer.json', 'composer.lock', 'storage/app/composer.plugins.json', 'vendor', 'plugins'];
foreach (['composer.user.json', 'privates'] as $item) {
if (file_exists(base_path($item))) {
$sources[] = $item;
}
}
foreach ($sources as $item) {
$this->output->write(" Cloning <info>{$item}</info>: ");
$source = base_path($item);
$target = $destination.'/'.$item;
if (is_dir($source)) {
$this->filesystem->copyDirectory($source, $target);
} else {
if (!$this->filesystem->isDirectory($dir = dirname($target))) {
$this->filesystem->makeDirectory($dir, 0777, true);
}
$this->filesystem->copy($source, $target);
}
$this->output->writeln('done');
}
}
/**
* Copy a directory from one location to another.
*
* @param string $directory
* @param string $destination
* @param array $excepts
* @param int|null $options
* @return bool
*
* @see \Illuminate\Filesystem\Filesystem::copyDirectory
*/
protected function copyDirectory($directory, $destination, $excepts = [], $options = null)
{
if (! $this->filesystem->isDirectory($directory)) {
return false;
}
$options = $options ?: FilesystemIterator::SKIP_DOTS;
if (! $this->filesystem->isDirectory($destination)) {
$this->filesystem->makeDirectory($destination, 0777, true);
}
$items = new FilesystemIterator($directory, $options);
foreach ($items as $item) {
$target = $destination.'/'.$item->getBasename();
if ($item->isDir()) {
$path = $item->getPathname();
if (in_array($item->getBasename(), $excepts) || Str::startsWith($item->getBasename(), '.')) {
continue;
}
$ignores = collect($excepts)->filter(function ($except) use ($item) {
return Str::startsWith($except, $item->getBasename().'/');
})->map(function ($except) use ($item) {
return substr($except, strlen($item->getBasename().'/'));
})->all();
if (!$this->copyDirectory($path, $target, $ignores, $options)) {
return false;
}
} else {
if (in_array($item->getBasename(), $excepts) || Str::startsWith($item->getBasename(), '.')) {
continue;
}
if (!$this->filesystem->copy($item->getPathname(), $target)) {
return false;
}
}
}
return true;
}
/**
* Download release file
*
* @param string $ver version
* @param string $destination destination dir
* @return void
* @throws \Exception|\GuzzleHttp\Exception\GuzzleException
*/
private function download($ver, $destination)
{
$updatesPath = storage_path('app/updates');
$versions = $this->releaseProvider->getUpdatableVersions();
foreach ($versions as $version) {
if (version_compare($ver, $version) === -1) {
break;
}
$this->output->write(" Downloading <info>v{$version}</info>: ");
$filepath = $this->releaseProvider->download($version, $updatesPath);
$zip = new \ZipArchive;
if ($zip->open($filepath) !== true) {
throw new \RuntimeException("fail to open zip file [$filepath]");
}
$zip->extractTo($destination);
$zip->close();
$this->filesystem->delete($filepath);
$this->output->writeln('done');
}
$this->filesystem->deleteDirectory($updatesPath);
}
/**
* Copy updated files to the project.
*
* @param string $source the path for updated
* @return void
*/
private function mergeToProject($source)
{
$this->output->write(' - Copying <info>vendor</info>: ');
if ($this->filesystem->isDirectory($newVendorPath = base_path('vendor-new'))) {
if (!$this->filesystem->deleteDirectory($newVendorPath)) {
throw new \RuntimeException("Failed to empty directory [$newVendorPath].");
}
}
$this->filesystem->moveDirectory($source.'/vendor', $newVendorPath);
$this->output->writeln('done');
$this->output->write(' - Copying <info>core</info>: ');
$this->copyDirectory($source, base_path(), ['vendor', 'plugins', 'privates', 'storage']);
$this->output->writeln('done');
$this->output->write(' - Changing <info>vendor</info>: ');
$this->filesystem->moveDirectory(base_path('vendor'), base_path('vendor-old'), true);
$this->filesystem->moveDirectory(base_path('vendor-new'), base_path('vendor'), true);
$this->output->writeln('done');
$this->output->write(' - Deleting Unnecessary files: ');
$this->filesystem->deleteDirectory($source);
$this->filesystem->deleteDirectory(base_path('vendor-old'));
$this->output->writeln('done');
$items = new FilesystemIterator(base_path('vendor/bin'), FilesystemIterator::SKIP_DOTS);
foreach ($items as $item) {
if ($item->isFile()) {
@chmod($item->getPathname(), 0755);
}
}
}
/**
* Execute migrations for XE core.
*
* @param string $installedVersion installed version
* @return void
*/
private function migrateCore($installedVersion)
{
$files = $this->filesystem->files(base_path('migrations'));
foreach ($files as $file) {
$name = lcfirst(str_replace('Migration', '', basename($file, '.php')));
$class = "\\Xpressengine\\Migrations\\".basename($file, '.php');
$migration = new $class();
/** @var Migration $migration */
$this->output->write(" updating $name.. ");
if($migration->checkUpdated($installedVersion) === false) {
$migration->update($installedVersion);
$this->info("[success]");
} else {
$this->warn("[skipped]");
}
}
}
/**
* Mark installed.
*
* @param string $ver version
* @return void
*/
private function markInstalled($ver)
{
file_put_contents(app()->getInstalledPath(), $ver);
}
}