.
This commit is contained in:
commit
bbb491d3d0
5
.env
Executable file
5
.env
Executable file
@ -0,0 +1,5 @@
|
||||
APP_ENV=dev
|
||||
APP_VERSION='1.0.0'
|
||||
APP_SECRET=d7e71b25a69f640224cfb1df1328454f
|
||||
APP_DEV_GIT_REPOSITORY='https://github.com/swieczorek/tmp-delete-me.git'
|
||||
APP_DEV_DEPLOY_DIRECTORY='/home/moby/htdocs/www.moby.io/'
|
9
.gitignore
vendored
Executable file
9
.gitignore
vendored
Executable file
@ -0,0 +1,9 @@
|
||||
/.env.local
|
||||
/.env.local.php
|
||||
/.env.*.local
|
||||
/bin/compiled/
|
||||
/data/keys/private.key
|
||||
/config/secrets/prod/prod.decrypt.private.php
|
||||
/public/bundles/
|
||||
/var/
|
||||
/vendor/
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright © 2013—2023 Anton Medvedev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
17
bin/console
Executable file
17
bin/console
Executable file
@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env php8.2
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
|
||||
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
return function (array $context) {
|
||||
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
|
||||
return new Application($kernel);
|
||||
};
|
26
bin/create-phar
Executable file
26
bin/create-phar
Executable file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env php8.2
|
||||
<?php
|
||||
|
||||
$cwd = getcwd();
|
||||
assert(is_string($cwd));
|
||||
|
||||
require __DIR__.'/../src/Compiler/bootstrap.php';
|
||||
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
use App\Compiler\Compiler;
|
||||
|
||||
$dotenv = new Dotenv();
|
||||
$dotenv->load(__DIR__.'/../.env');
|
||||
|
||||
error_reporting(-1);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
try {
|
||||
$privateKey = realpath(__DIR__.'/../data/keys/private.key');
|
||||
$compiler = new Compiler();
|
||||
$compiler->setPrivateKey($privateKey);
|
||||
$compiler->compile($_ENV['APP_VERSION']);
|
||||
} catch (\Exception $e) {
|
||||
echo 'Failed to compile phar: ['.get_class($e).'] '.$e->getMessage().' at '.$e->getFile().':'.$e->getLine().PHP_EOL;
|
||||
exit(1);
|
||||
}
|
46
bin/dploy
Executable file
46
bin/dploy
Executable file
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env php8.2
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
use App\Console\Application;
|
||||
|
||||
set_time_limit(0);
|
||||
|
||||
if (false === defined('IS_PHAR')) {
|
||||
define('IS_PHAR', (strlen(Phar::running()) > 0));
|
||||
}
|
||||
|
||||
if (function_exists('ini_set')) {
|
||||
@ini_set('display_errors', '1');
|
||||
if ($memoryLimit = getenv('DPLOY_MEMORY_LIMIT')) {
|
||||
@ini_set('memory_limit', $memoryLimit);
|
||||
} else {
|
||||
$memoryInBytes = function ($value) {
|
||||
$unit = strtolower(substr($value, -1, 1));
|
||||
$value = (int) $value;
|
||||
switch($unit) {
|
||||
case 'g':
|
||||
$value *= 1024;
|
||||
case 'm':
|
||||
$value *= 1024;
|
||||
case 'k':
|
||||
$value *= 1024;
|
||||
}
|
||||
|
||||
return $value;
|
||||
};
|
||||
$memoryLimit = trim(ini_get('memory_limit'));
|
||||
if ($memoryLimit != -1 && $memoryInBytes($memoryLimit) < 1024 * 1024 * 1536) {
|
||||
@ini_set('memory_limit', '1536M');
|
||||
}
|
||||
unset($memoryInBytes);
|
||||
}
|
||||
unset($memoryLimit);
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
return function (array $context) {
|
||||
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
return new Application($kernel);
|
||||
};
|
2
changelog
Executable file
2
changelog
Executable file
@ -0,0 +1,2 @@
|
||||
1.0.0 Stefan Wieczorek <stefan.wieczorek@cloudpanel.io> Thu, 01 Jun 2023 08:08:08
|
||||
- Initial Release
|
69
composer.json
Executable file
69
composer.json
Executable file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"type": "project",
|
||||
"license": "proprietary",
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"ext-ctype": "*",
|
||||
"ext-iconv": "*",
|
||||
"composer/pcre": "^2.1 || ^3.1",
|
||||
"symfony/console": "6.2.*",
|
||||
"symfony/dotenv": "6.2.*",
|
||||
"symfony/filesystem": "6.2.*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/framework-bundle": "6.2.*",
|
||||
"symfony/http-client": "6.2.*",
|
||||
"symfony/process": "6.2.*",
|
||||
"symfony/runtime": "6.2.*",
|
||||
"symfony/yaml": "6.2.*"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true,
|
||||
"symfony/flex": true,
|
||||
"symfony/runtime": true
|
||||
},
|
||||
"sort-packages": true
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"App\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"replace": {
|
||||
"symfony/polyfill-ctype": "*",
|
||||
"symfony/polyfill-iconv": "*",
|
||||
"symfony/polyfill-php72": "*",
|
||||
"symfony/polyfill-php73": "*",
|
||||
"symfony/polyfill-php74": "*",
|
||||
"symfony/polyfill-php80": "*",
|
||||
"symfony/polyfill-php81": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"auto-scripts": {
|
||||
"cache:clear": "symfony-cmd",
|
||||
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||
},
|
||||
"post-install-cmd": [
|
||||
"@auto-scripts"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@auto-scripts"
|
||||
]
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
},
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "6.2.*"
|
||||
}
|
||||
}
|
||||
}
|
2689
composer.lock
generated
Executable file
2689
composer.lock
generated
Executable file
File diff suppressed because it is too large
Load Diff
5
config/bundles.php
Executable file
5
config/bundles.php
Executable file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||
];
|
19
config/packages/cache.yaml
Executable file
19
config/packages/cache.yaml
Executable file
@ -0,0 +1,19 @@
|
||||
framework:
|
||||
cache:
|
||||
# Unique name of your app: used to compute stable namespaces for cache keys.
|
||||
#prefix_seed: your_vendor_name/app_name
|
||||
|
||||
# The "app" cache stores to the filesystem by default.
|
||||
# The data in this cache should persist between deploys.
|
||||
# Other options include:
|
||||
|
||||
# Redis
|
||||
#app: cache.adapter.redis
|
||||
#default_redis_provider: redis://localhost
|
||||
|
||||
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
|
||||
#app: cache.adapter.apcu
|
||||
|
||||
# Namespaced pools use the above "app" backend by default
|
||||
#pools:
|
||||
#my.dedicated.cache: null
|
29
config/packages/framework.yaml
Executable file
29
config/packages/framework.yaml
Executable file
@ -0,0 +1,29 @@
|
||||
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
#csrf_protection: true
|
||||
http_method_override: false
|
||||
handle_all_throwables: true
|
||||
|
||||
# Enables session support. Note that the session will ONLY be started if you read or write from it.
|
||||
# Remove or comment this section to explicitly disable session support.
|
||||
session:
|
||||
handler_id: null
|
||||
cookie_secure: auto
|
||||
cookie_samesite: lax
|
||||
storage_factory_id: session.storage.factory.native
|
||||
|
||||
#esi: true
|
||||
#fragments: true
|
||||
php_errors:
|
||||
log: true
|
||||
|
||||
http_client:
|
||||
default_options:
|
||||
http_version: '2.0'
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
test: true
|
||||
session:
|
||||
storage_factory_id: session.storage.factory.mock_file
|
12
config/packages/routing.yaml
Executable file
12
config/packages/routing.yaml
Executable file
@ -0,0 +1,12 @@
|
||||
framework:
|
||||
router:
|
||||
utf8: true
|
||||
|
||||
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||
#default_uri: http://localhost
|
||||
|
||||
when@prod:
|
||||
framework:
|
||||
router:
|
||||
strict_requirements: null
|
5
config/preload.php
Executable file
5
config/preload.php
Executable file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
|
||||
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
|
||||
}
|
5
config/routes.yaml
Executable file
5
config/routes.yaml
Executable file
@ -0,0 +1,5 @@
|
||||
controllers:
|
||||
resource:
|
||||
path: ../src/Controller/
|
||||
namespace: App\Controller
|
||||
type: attribute
|
4
config/routes/framework.yaml
Executable file
4
config/routes/framework.yaml
Executable file
@ -0,0 +1,4 @@
|
||||
when@dev:
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /_error
|
40
config/services.yaml
Executable file
40
config/services.yaml
Executable file
@ -0,0 +1,40 @@
|
||||
# This file is the entry point to configure your own services.
|
||||
# Files in the packages/ subdirectory configure your dependencies.
|
||||
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
_defaults:
|
||||
autowire: true # Automatically injects dependencies in your services.
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/'
|
||||
exclude:
|
||||
- '../src/DependencyInjection/'
|
||||
- '../src/Entity/'
|
||||
- '../src/Kernel.php'
|
||||
|
||||
App\Dploy:
|
||||
arguments: ['@http_client']
|
||||
public: true
|
||||
|
||||
App\Command\SetupCommand:
|
||||
public: true
|
||||
|
||||
App\Command\DeployCommand:
|
||||
public: true
|
||||
|
||||
App\Command\SelfUpdateCommand:
|
||||
public: true
|
||||
|
||||
App\Command\TestCommand:
|
||||
public: true
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
9
data/keys/public.key
Executable file
9
data/keys/public.key
Executable file
@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAycvbAEuoQ8Qw8VCQgmIL
|
||||
xccc4P5fKKyrnCXUve9cWbVuXaRgRVC9Mnn3bOy78wXhdQwcptjjTavRngFHO3hM
|
||||
8zZqnUGotmOid0z/CLxLFNvQ7crUODFgiY8o3xydW3uS8IUGvnLlsqFxTHcBmcQR
|
||||
H+hT8ONTgTWX3om/dXClSjwvSXXzUl1ps+Be1UQeiYWo9yW+RmWXMy7eZ/Y7ca8k
|
||||
IObvyGc4bjMtqWH+L9HXPmkoZhl1XQuFJ/m4DdEHlCEbjjwjHC9FeTc1AoXPp47i
|
||||
wyL736NYBs7dU+E6QXDk+tX82OTHVBOSaj2jcpmoyq5zyqyQ2pJp6cbnf8rma3ah
|
||||
0wIDAQAB
|
||||
-----END PUBLIC KEY-----
|
9
public/index.php
Executable file
9
public/index.php
Executable file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Kernel;
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
return function (array $context) {
|
||||
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
};
|
32
src/Command/Command.php
Executable file
32
src/Command/Command.php
Executable file
@ -0,0 +1,32 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command as BaseCommand;
|
||||
|
||||
abstract class Command extends BaseCommand
|
||||
{
|
||||
public const TEMPLATES_GITHUB_REPOSITORY = 'https://github.com/cloudpanel-io/dploy-application-templates';
|
||||
|
||||
protected function get(string $id)
|
||||
{
|
||||
return $this->getContainer()->get($id);
|
||||
}
|
||||
|
||||
protected function getContainer()
|
||||
{
|
||||
return $this->getApplication()->getContainer();
|
||||
}
|
||||
|
||||
protected function getKernel()
|
||||
{
|
||||
$container = $this->getContainer();
|
||||
return $container->get('kernel');
|
||||
}
|
||||
|
||||
protected function getProjectDirectory(): string
|
||||
{
|
||||
$kernel = $this->getKernel();
|
||||
return $kernel->getProjectDir();
|
||||
}
|
||||
}
|
53
src/Command/DeployCommand.php
Executable file
53
src/Command/DeployCommand.php
Executable file
@ -0,0 +1,53 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Deployment\Deployment;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use App\Deployment\Config as DeploymentConfig;
|
||||
|
||||
class DeployCommand extends Command
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('deploy');
|
||||
$this->setHidden(false);
|
||||
$this->setDescription('Deploys a branch or tag.');
|
||||
$this->addArgument('version', InputArgument::REQUIRED, 'version');
|
||||
$this->setHelp(
|
||||
<<<EOT
|
||||
<info>Examples:</info>
|
||||
|
||||
Deploying a branch: <comment>dploy deploy main</comment>
|
||||
Deploying a tag: <comment>dploy deploy v1.0.0</comment>
|
||||
EOT);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
try {
|
||||
$version = trim($input->getArgument('version'));
|
||||
$config = new DeploymentConfig();
|
||||
$systemUserId = $config->getSystemUserId();
|
||||
if (0 == $systemUserId) {
|
||||
throw new \Exception('Not allowed to run the command as root, use the site user.');
|
||||
}
|
||||
$filesystem = new Filesystem();
|
||||
$configFile = $config->getConfigFile();
|
||||
if (true === $filesystem->exists($configFile)) {
|
||||
$deployment = new Deployment($output, $config, $version);
|
||||
$deployment->deploy();
|
||||
$output->writeln('<info>Deployment has been completed!</info>');
|
||||
} else {
|
||||
throw new \Exception(sprintf('Config file "%s" does not exist. Did you run dploy setup $template?', $configFile));
|
||||
}
|
||||
return Command::SUCCESS;
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
107
src/Command/InitCommand.php
Executable file
107
src/Command/InitCommand.php
Executable file
@ -0,0 +1,107 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Symfony\Component\Process\Process;
|
||||
use App\Deployment\Setup as DeploymentSetup;
|
||||
use App\Deployment\Config as DeploymentConfig;
|
||||
|
||||
class InitCommand extends Command
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('init');
|
||||
$this->setDescription('Setups the project directory structure.');
|
||||
$this->addArgument('application', InputArgument::OPTIONAL, 'Downloads a config for the application.', 'generic');
|
||||
$this->setHelp(
|
||||
<<<EOT
|
||||
The <info>init</info> command downloads a pre-configured <info>config.yml</info> from
|
||||
|
||||
<comment>https://github.com/cloudpanel-io/dploy-application-templates</comment>
|
||||
|
||||
<info>dploy init laravel</info>
|
||||
EOT);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
try {
|
||||
$applicationName = trim($input->getArgument('application'));
|
||||
$filesystem = new Filesystem();
|
||||
$config = new DeploymentConfig();
|
||||
$systemUserId = $config->getSystemUserId();
|
||||
if (0 == $systemUserId) {
|
||||
throw new \Exception('Not allowed to run the command as root, use the site user.');
|
||||
}
|
||||
$configFile = $config->getConfigFile();
|
||||
if (false === $filesystem->exists($configFile)) {
|
||||
$templates = $this->getTemplates();
|
||||
if (false === isset($templates[$applicationName])) {
|
||||
throw new \Exception(sprintf('Template %s does not exist, available templates: %s', $applicationName, implode(',', array_keys($templates))));
|
||||
}
|
||||
if (true === function_exists('xdebug_is_debugger_active') && true === xdebug_is_debugger_active()) {
|
||||
$gitRepository = $_ENV['APP_DEV_GIT_REPOSITORY'];
|
||||
$deployDirectory = $_ENV['APP_DEV_DEPLOY_DIRECTORY'];
|
||||
} else {
|
||||
$helper = $this->getHelper('question');
|
||||
$gitRepositoryQuestion = new Question('Git Repository: ');
|
||||
$gitRepository = $helper->ask($input, $output, $gitRepositoryQuestion);
|
||||
$output->writeln(sprintf('<info>Deploy directory like: /home/%s/htdocs/www.domain.com</info>', $config->getSystemUserName()));
|
||||
$deployDirectoryQuestion = new Question('Deploy Directory: ');
|
||||
$deployDirectory = $helper->ask($input, $output, $deployDirectoryQuestion);
|
||||
}
|
||||
$deployDirectory = rtrim($deployDirectory, '/');
|
||||
$template = str_replace(['{git_repository}', '{deploy_directory}'], [$gitRepository, $deployDirectory], $templates[$applicationName]);
|
||||
$tmpFile = tmpfile();
|
||||
$tmpFilePath = stream_get_meta_data($tmpFile)['uri'];
|
||||
file_put_contents($tmpFilePath, $template);
|
||||
$configDirectory = $config->getConfigDirectory();
|
||||
$filesystem = new Filesystem();
|
||||
$filesystem->mkdir($configDirectory, 0770);
|
||||
$filesystem->copy($tmpFilePath, $configFile);
|
||||
$setup = new DeploymentSetup($config);
|
||||
$setup->create();
|
||||
$output->writeln(sprintf('<comment>%s</comment>', sprintf('The config "%s" has been created.', $configFile)));
|
||||
} else {
|
||||
throw new \Exception(sprintf('Config file %s already exists, nothing to do.', $configFile));
|
||||
}
|
||||
return Command::SUCCESS;
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
private function getTemplates(): array
|
||||
{
|
||||
$templates = [];
|
||||
try {
|
||||
$filesystem = new Filesystem();
|
||||
$tmpDirectory = sprintf('%s/%s', rtrim(sys_get_temp_dir(), '/'), uniqid());
|
||||
$gitCloneCommand = sprintf('/usr/bin/git clone %s %s', self::TEMPLATES_GITHUB_REPOSITORY, $tmpDirectory);
|
||||
$process = Process::fromShellCommandline($gitCloneCommand);
|
||||
$process->setTimeout(600);
|
||||
$process->run();
|
||||
$directoryIterator = new \DirectoryIterator($tmpDirectory);
|
||||
foreach ($directoryIterator as $fileInfo) {
|
||||
$name = $fileInfo->getFilename();
|
||||
$filePath = $fileInfo->getPathname();
|
||||
if (false === empty($name) && true === is_file($filePath) && true === file_exists($filePath)) {
|
||||
$templates[$name] = file_get_contents($filePath);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
throw $e;
|
||||
} finally {
|
||||
if (true === isset($tmpDirectory) && true === is_dir($tmpDirectory)) {
|
||||
$filesystem->remove($tmpDirectory);
|
||||
}
|
||||
}
|
||||
return $templates;
|
||||
}
|
||||
}
|
94
src/Command/ListCommand.php
Executable file
94
src/Command/ListCommand.php
Executable file
@ -0,0 +1,94 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Descriptor\ApplicationDescription;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Helper\DescriptorHelper;
|
||||
use Symfony\Component\Console\Helper\Helper;
|
||||
use App\Command\Command as BaseCommand;
|
||||
|
||||
class ListCommand extends BaseCommand
|
||||
{
|
||||
protected OutputInterface $output;
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('list')
|
||||
->setDefinition([
|
||||
new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name', null, function () {
|
||||
return array_keys((new ApplicationDescription($this->getApplication()))->getNamespaces());
|
||||
}),
|
||||
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'),
|
||||
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', function () {
|
||||
return (new DescriptorHelper())->getFormats();
|
||||
}),
|
||||
new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'),
|
||||
])
|
||||
->setDescription('List commands')
|
||||
->setHelp(<<<'EOF'
|
||||
The <info>%command.name%</info> command lists all commands:
|
||||
|
||||
<info>%command.full_name%</info>
|
||||
|
||||
You can also display the commands for a specific namespace:
|
||||
|
||||
<info>%command.full_name% test</info>
|
||||
|
||||
You can also output the information in other formats by using the <comment>--format</comment> option:
|
||||
|
||||
<info>%command.full_name% --format=xml</info>
|
||||
|
||||
It's also possible to get raw list of commands (useful for embedding command runner):
|
||||
|
||||
<info>%command.full_name% --raw</info>
|
||||
EOF
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$application = $this->getApplication();
|
||||
if ('' != $help = $application->getHelp()) {
|
||||
$output->writeln($help.PHP_EOL);
|
||||
}
|
||||
$output->writeln('<comment>Usage:</comment>');
|
||||
$output->writeln(' command [options] [arguments]'.PHP_EOL);
|
||||
$output->writeln('<comment>Available commands:</comment>');
|
||||
$commands = $application->getCommands();
|
||||
$width = $this->getColumnWidth($commands);
|
||||
foreach ($commands as $command) {
|
||||
if (false === ($command instanceof BaseCommand) || (true === $command->isHidden())) {
|
||||
continue;
|
||||
}
|
||||
$name = $command->getName();
|
||||
$spacingWidth = $width - Helper::width($name);
|
||||
$output->writeln(sprintf(' <info>%s</info>%s %s', $name, str_repeat(' ', $spacingWidth), $command->getDescription()));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function getColumnWidth(array $commands): int
|
||||
{
|
||||
$widths = [];
|
||||
foreach ($commands as $command) {
|
||||
if (false === ($command instanceof BaseCommand)) {
|
||||
continue;
|
||||
}
|
||||
if ($command instanceof BaseCommand) {
|
||||
$widths[] = Helper::width($command->getName());
|
||||
foreach ($command->getAliases() as $alias) {
|
||||
$widths[] = Helper::width($alias);
|
||||
}
|
||||
} else {
|
||||
$widths[] = Helper::width($command);
|
||||
}
|
||||
}
|
||||
return $widths ? max($widths) + 2 : 0;
|
||||
}
|
||||
}
|
84
src/Command/SelfUpdateCommand.php
Executable file
84
src/Command/SelfUpdateCommand.php
Executable file
@ -0,0 +1,84 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use App\SelfUpdate\Config;
|
||||
use App\Dploy;
|
||||
|
||||
class SelfUpdateCommand extends Command
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('self-update');
|
||||
$this->setDescription('Updates dploy to the latest version.');
|
||||
$this->addOption('channel', null, InputOption::VALUE_OPTIONAL, sprintf('Sets the channel to update dploy from, available channels: %s', implode(', ', Dploy::CHANNELS)));
|
||||
$this->addOption('setVersion', null, InputOption::VALUE_OPTIONAL, 'Sets the specific version to update.');
|
||||
$this->setHelp(
|
||||
<<<EOT
|
||||
The <info>self-update</info> command checks dploy.cloudpanel.io for newer
|
||||
versions of dploy and if found, installs the latest.
|
||||
|
||||
<info>dploy self-update</info>
|
||||
EOT);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
try {
|
||||
$container = $this->getContainer();
|
||||
$dploy = $container->get('App\Dploy');
|
||||
$config = new Config();
|
||||
$channel = (string)$input->getOption('channel');
|
||||
if (true === empty($channel)) {
|
||||
$channel = $config->get('channel');
|
||||
$channel = (false === empty($channel) ? $channel : Dploy::CHANNEL_STABLE);
|
||||
}
|
||||
$channels = $dploy->getChannels();
|
||||
if (false === in_array($channel, $channels)) {
|
||||
throw new \Exception(sprintf('%s is not a valid channel, available channels: %s', $channel, implode(', ', Dploy::CHANNELS)));
|
||||
}
|
||||
$config->set('channel', $channel);
|
||||
$localFilename = realpath($_SERVER['argv'][0]);
|
||||
if (false === $localFilename) {
|
||||
$localFilename = $_SERVER['argv'][0];
|
||||
}
|
||||
if (false === file_exists($localFilename)) {
|
||||
throw new \Exception(sprintf('Dploy update failed: the %s is not accessible.', $localFilename));
|
||||
}
|
||||
$latest = $dploy->getLatest($channel);
|
||||
$latestVersion = $latest['version'] ?? '';
|
||||
$currentVersion = $dploy->getVersion();
|
||||
$updateVersion = $input->getOption('setVersion');
|
||||
$updateVersion = (false === empty($updateVersion) ? $updateVersion : $latestVersion);
|
||||
if ($currentVersion == $updateVersion) {
|
||||
$output->writeln(sprintf('<info>You already use the latest dploy version %s (%s channel).</info>', $updateVersion, $channel));
|
||||
} else {
|
||||
if ($currentVersion < $updateVersion) {
|
||||
$output->writeln(sprintf('Upgrading from <info>%s</info> to <info>%s</info> (%s channel).', $currentVersion, $updateVersion, $channel));
|
||||
$output->writeln('');
|
||||
$downloadedFile = $dploy->downloadVersion($updateVersion, $output);
|
||||
$publicKey = sprintf('%s/data/keys/public.key', $this->getProjectDirectory());
|
||||
$opensslPublicKey = openssl_pkey_get_public(file_get_contents($publicKey));
|
||||
if (false === $opensslPublicKey) {
|
||||
throw new \RuntimeException(sprintf('Failed loading the public key from: %s', $publicKey));
|
||||
}
|
||||
$signature = $dploy->getSignatureForVersion($updateVersion);
|
||||
$verified = 1 === openssl_verify((string) file_get_contents($downloadedFile), $signature, $opensslPublicKey, OPENSSL_ALGO_SHA384);
|
||||
if (false === $verified) {
|
||||
throw new \RuntimeException('The phar signature did not match the file you downloaded, this means your public keys are outdated or that the phar file is corrupt/has been modified.');
|
||||
}
|
||||
@copy($downloadedFile, $localFilename);
|
||||
$output->writeln(str_repeat(PHP_EOL, 1));
|
||||
exit(Command::SUCCESS);
|
||||
}
|
||||
}
|
||||
return Command::SUCCESS;
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln(PHP_EOL.PHP_EOL.sprintf('<error>%s</error>', $e->getMessage()));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
46
src/Command/TestCommand.php
Executable file
46
src/Command/TestCommand.php
Executable file
@ -0,0 +1,46 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
class TestCommand extends Command
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('test');
|
||||
$this->setHidden(false);
|
||||
$this->setDescription('dploy test');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
try {
|
||||
|
||||
|
||||
|
||||
/*
|
||||
$process = Process::fromShellCommandline($command, '/tmp/');
|
||||
$process->setTimeout(3600);
|
||||
$process->run(function ($type, $buffer) use ($output) {
|
||||
if (false === empty($buffer)) {
|
||||
$output->write($buffer);
|
||||
}
|
||||
});
|
||||
if (false === $process->isSuccessful()) {
|
||||
throw new \RuntimeException($process->getErrorOutput());
|
||||
}
|
||||
*/
|
||||
|
||||
//$output->writeln(sprintf('Muha: %s', rand(1,1000)));
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (\Exception $e) {
|
||||
$errorMessage = $e->getMessage();
|
||||
$output->writeln(sprintf('<error>An error has occurred: "%s"</error>', $errorMessage));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
227
src/Compiler/Compiler.php
Executable file
227
src/Compiler/Compiler.php
Executable file
@ -0,0 +1,227 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Compiler;
|
||||
|
||||
use Composer\Pcre\Preg;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
class Compiler
|
||||
{
|
||||
private $version;
|
||||
|
||||
private $branchAliasVersion = '';
|
||||
|
||||
private $versionDate;
|
||||
|
||||
private $privateKey;
|
||||
|
||||
public function compile(string $version): void
|
||||
{
|
||||
$filesystem = new Filesystem();
|
||||
$compiledDirectory = realpath(__DIR__.'/../../bin/compiled');
|
||||
$filesystem->remove($compiledDirectory);
|
||||
$filesystem->mkdir($compiledDirectory);
|
||||
$pharFile = sprintf('%s/dploy.phar', $compiledDirectory);
|
||||
$finalFile = sprintf('%s/dploy', $compiledDirectory);
|
||||
$signatureFile = sprintf('%s/dploy.sig', $compiledDirectory);
|
||||
if (file_exists($pharFile)) {
|
||||
unlink($pharFile);
|
||||
}
|
||||
$phar = new \Phar($pharFile, 0, 'dploy.phar');
|
||||
$phar->setSignatureAlgorithm(\Phar::SHA512);
|
||||
$phar->startBuffering();
|
||||
$finderSort = static function ($a, $b): int {
|
||||
return strcmp(strtr($a->getRealPath(), '\\', '/'), strtr($b->getRealPath(), '\\', '/'));
|
||||
};
|
||||
$finder = new Finder();
|
||||
$finder->files()
|
||||
->ignoreVCS(true)
|
||||
->name('*.php')
|
||||
->notName('Compiler.php')
|
||||
->notName('ClassLoader.php')
|
||||
->notName('InstalledVersions.php')
|
||||
->in(__DIR__.'/..')
|
||||
->sort($finderSort)
|
||||
;
|
||||
foreach ($finder as $file) {
|
||||
$this->addFile($phar, $file);
|
||||
}
|
||||
$envFile = new \SplFileInfo(__DIR__.'/../../.env');
|
||||
$this->addFile($phar, $envFile);
|
||||
// Add vendor files
|
||||
$finder = new Finder();
|
||||
$finder->files()
|
||||
->ignoreVCS(true)
|
||||
->notPath('/\/(composer\.(json|lock)|[A-Z]+\.md(?:own)?|\.gitignore|appveyor.yml|phpunit\.xml\.dist|phpstan\.neon\.dist|phpstan-config\.neon|phpstan-baseline\.neon)$/')
|
||||
->notPath('/bin\/(jsonlint|validate-json|simple-phpunit|phpstan|phpstan\.phar)(\.bat)?$/')
|
||||
->notPath('justinrainbow/json-schema/demo/')
|
||||
->notPath('justinrainbow/json-schema/dist/')
|
||||
->notPath('composer/installed.json')
|
||||
->notPath('composer/LICENSE')
|
||||
->notPath('keys/private.key')
|
||||
->notPath('bin/patch-type-declarations')
|
||||
->notPath('bin/var-dump-server')
|
||||
->notPath('bin/yaml-lint')
|
||||
->notPath('psr/cache/LICENSE.txt')
|
||||
->notPath('symfony/console/Resources/bin/hiddeninput.exe')
|
||||
->notPath('symfony/console/Resources/completion.bash')
|
||||
->notPath('symfony/console/Resources/completion.fish')
|
||||
->notPath('symfony/console/Resources/completion.zsh')
|
||||
->notPath('symfony/dependency-injection/Loader/schema/dic/services/services-1.0.xsd')
|
||||
->notPath('symfony/error-handler/Resources/assets/')
|
||||
->notPath('symfony/var-dumper/Resources/css/htmlDescriptor.css')
|
||||
->notPath('symfony/var-dumper/Resources/js/htmlDescriptor.js')
|
||||
->notPath('symfony/runtime/Internal/autoload_runtime.template')
|
||||
->notPath('symfony/routing/Loader/schema/routing/routing-1.0.xsd')
|
||||
->notPath('symfony/framework-bundle/Resources/config/schema/symfony-1.0.xsd')
|
||||
->notPath('symfony/framework-bundle/Resources/config/routing/errors.xml')
|
||||
->exclude('Tests')
|
||||
->exclude('tests')
|
||||
->exclude('docs')
|
||||
->in(__DIR__.'/../../vendor/')
|
||||
->in(__DIR__.'/../../config/')
|
||||
->in(__DIR__.'/../../data/')
|
||||
->in(__DIR__.'/../../var/')
|
||||
->sort($finderSort)
|
||||
;
|
||||
$extraFiles = [];
|
||||
$unexpectedFiles = [];
|
||||
foreach ($finder as $file) {
|
||||
if (false !== ($index = array_search($file->getRealPath(), $extraFiles, true))) {
|
||||
unset($extraFiles[$index]);
|
||||
} elseif (!Preg::isMatch('{(^LICENSE$|\.php$)}', $file->getFilename())) {
|
||||
//$unexpectedFiles[] = (string) $file;
|
||||
}
|
||||
if (Preg::isMatch('{\.php[\d.]*$}', $file->getFilename())) {
|
||||
$this->addFile($phar, $file);
|
||||
} else {
|
||||
$this->addFile($phar, $file, false);
|
||||
}
|
||||
}
|
||||
if (count($extraFiles) > 0) {
|
||||
throw new \RuntimeException('These files were expected but not added to the phar, they might be excluded or gone from the source package:'.PHP_EOL.var_export($extraFiles, true));
|
||||
}
|
||||
if (count($unexpectedFiles) > 0) {
|
||||
throw new \RuntimeException('These files were unexpectedly added to the phar, make sure they are excluded or listed in $extraFiles:'.PHP_EOL.var_export($unexpectedFiles, true));
|
||||
}
|
||||
$envFile = dirname(__FILE__).'/../../.env';
|
||||
$envFileContent = file_get_contents($envFile);
|
||||
$this->addDployBin($phar);
|
||||
$phar->addFromString('composer.json', '');
|
||||
$phar->addFromString('.env', $envFileContent);
|
||||
$phar['.env']->chmod(0777);
|
||||
$stub = $this->getStub();
|
||||
$phar->setStub($stub);
|
||||
//$phar->compressFiles(\Phar::GZ);
|
||||
$phar->stopBuffering();
|
||||
$privateKey = $this->getPrivateKey();
|
||||
$private = openssl_get_privatekey(file_get_contents($privateKey));
|
||||
openssl_sign((string)file_get_contents($pharFile), $signature, $private, OPENSSL_ALGO_SHA384);
|
||||
file_put_contents($signatureFile, base64_encode($signature));
|
||||
rename($pharFile, $finalFile);
|
||||
unset($phar);
|
||||
}
|
||||
|
||||
public function setPrivateKey(string $privateKey)
|
||||
{
|
||||
$this->privateKey = $privateKey;
|
||||
}
|
||||
|
||||
public function getPrivateKey()
|
||||
{
|
||||
return $this->privateKey;
|
||||
}
|
||||
|
||||
private function getRelativeFilePath(\SplFileInfo $file): string
|
||||
{
|
||||
$realPath = (string)$file->getRealPath();
|
||||
$pathPrefix = dirname(__DIR__, 2).DIRECTORY_SEPARATOR;
|
||||
|
||||
$pos = strpos($realPath, $pathPrefix);
|
||||
$relativePath = ($pos !== false) ? substr_replace($realPath, '', $pos, strlen($pathPrefix)) : $realPath;
|
||||
|
||||
return strtr($relativePath, '\\', '/');
|
||||
}
|
||||
|
||||
private function addFile(\Phar $phar, \SplFileInfo $file, bool $strip = true): void
|
||||
{
|
||||
$path = $this->getRelativeFilePath($file);
|
||||
$content = file_get_contents((string) $file);
|
||||
if ($strip) {
|
||||
//$content = $this->stripWhitespace($content);
|
||||
} elseif ('LICENSE' === $file->getFilename()) {
|
||||
$content = "\n".$content."\n";
|
||||
}
|
||||
if ($path === 'src/Composer/Composer.php') {
|
||||
$content = strtr(
|
||||
$content,
|
||||
[
|
||||
'@package_version@' => $this->version,
|
||||
'@package_branch_alias_version@' => $this->branchAliasVersion,
|
||||
'@release_date@' => $this->versionDate->format('Y-m-d H:i:s'),
|
||||
]
|
||||
);
|
||||
$content = Preg::replace('{SOURCE_VERSION = \'[^\']+\';}', 'SOURCE_VERSION = \'\';', $content);
|
||||
}
|
||||
$phar->addFromString($path, $content);
|
||||
$phar[$path]->chmod(0777);
|
||||
}
|
||||
|
||||
private function addDployBin(\Phar $phar): void
|
||||
{
|
||||
$content = file_get_contents(__DIR__.'/../../bin/dploy');
|
||||
//$content = Preg::replace('{^#!/usr/bin/env php8.2\s*}', '', $content);
|
||||
$phar->addFromString('bin/dploy', $content);
|
||||
}
|
||||
|
||||
private function stripWhitespace(string $source): string
|
||||
{
|
||||
if (!function_exists('token_get_all')) {
|
||||
return $source;
|
||||
}
|
||||
|
||||
$output = '';
|
||||
foreach (token_get_all($source) as $token) {
|
||||
if (is_string($token)) {
|
||||
$output .= $token;
|
||||
} elseif (in_array($token[0], [T_COMMENT, T_DOC_COMMENT])) {
|
||||
$output .= str_repeat("\n", substr_count($token[1], "\n"));
|
||||
} elseif (T_WHITESPACE === $token[0]) {
|
||||
// reduce wide spaces
|
||||
$whitespace = Preg::replace('{[ \t]+}', ' ', $token[1]);
|
||||
// normalize newlines to \n
|
||||
$whitespace = Preg::replace('{(?:\r\n|\r|\n)}', "\n", $whitespace);
|
||||
// trim leading spaces
|
||||
$whitespace = Preg::replace('{\n +}', "\n", $whitespace);
|
||||
$output .= $whitespace;
|
||||
} else {
|
||||
$output .= $token[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function getStub(): string
|
||||
{
|
||||
$stub = <<<'EOF'
|
||||
#!/usr/bin/env php8.2
|
||||
<?php
|
||||
|
||||
if (!class_exists('Phar')) {
|
||||
echo 'PHP\'s phar extension is missing.' . PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
Phar::mapPhar('dploy.phar');
|
||||
|
||||
EOF;
|
||||
|
||||
return $stub . <<<'EOF'
|
||||
require 'phar://dploy.phar/bin/dploy';
|
||||
|
||||
__HALT_COMPILER();
|
||||
EOF;
|
||||
}
|
||||
}
|
26
src/Compiler/bootstrap.php
Executable file
26
src/Compiler/bootstrap.php
Executable file
@ -0,0 +1,26 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
|
||||
function includeIfExists(string $file): ?ClassLoader
|
||||
{
|
||||
return file_exists($file) ? include $file : null;
|
||||
}
|
||||
|
||||
if ((!$loader = includeIfExists(__DIR__.'/../../vendor/autoload.php')) && (!$loader = includeIfExists(__DIR__.'/../../../../autoload.php'))) {
|
||||
echo 'You must set up the project dependencies using `composer install`'.PHP_EOL.
|
||||
'See https://getcomposer.org/download/ for instructions on installing Composer'.PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
return $loader;
|
96
src/Console/Application.php
Executable file
96
src/Console/Application.php
Executable file
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application as BaseApplication;
|
||||
use Symfony\Component\Console\Command\HelpCommand;
|
||||
use Symfony\Component\Console\Command\CompleteCommand;
|
||||
use Symfony\Component\Console\Command\DumpCompletionCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use App\Dploy;
|
||||
use App\Command\InitCommand;
|
||||
use App\Command\ListCommand;
|
||||
use App\Command\DeployCommand;
|
||||
use App\Command\SelfUpdateCommand;
|
||||
class Application extends BaseApplication
|
||||
{
|
||||
const APPLICATION_NAME = 'Dploy';
|
||||
const APPLICATION_LOGO = '
|
||||
_____ _____ _ ______ __
|
||||
| __ \| __ \| | / __ \ \ / /
|
||||
| | | | |__) | | | | | \ \_/ /
|
||||
| | | | ___/| | | | | |\ /
|
||||
| |__| | | | |___| |__| | | |
|
||||
|_____/|_| |______\____/ |_|
|
||||
|
||||
|
||||
';
|
||||
private KernelInterface $kernel;
|
||||
public function __construct(KernelInterface $kernel)
|
||||
{
|
||||
$this->kernel = $kernel;
|
||||
parent::__construct($this->kernel);
|
||||
}
|
||||
public function doRun(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->setApplicationNameAndVersion();
|
||||
return parent::doRun($input, $output);
|
||||
}
|
||||
private function setApplicationNameAndVersion(): void
|
||||
{
|
||||
$version = Dploy::getVersion();
|
||||
$this->setName(self::APPLICATION_NAME);
|
||||
$this->setVersion($version);
|
||||
}
|
||||
private function init()
|
||||
{
|
||||
if ($this->initialized) {
|
||||
return;
|
||||
}
|
||||
$this->initialized = true;
|
||||
|
||||
foreach ($this->getDefaultCommands() as $command) {
|
||||
$this->add($command);
|
||||
}
|
||||
}
|
||||
public function getHelp(): string
|
||||
{
|
||||
return self::APPLICATION_LOGO . parent::getHelp();
|
||||
}
|
||||
public function getLongVersion(): string
|
||||
{
|
||||
$name = $this->getName();
|
||||
$version = $this->getVersion();
|
||||
$longVersion = sprintf('<info>%s</info> version <comment>%s</comment>', $name, $version);
|
||||
return $longVersion;
|
||||
|
||||
}
|
||||
protected function getDefaultCommands(): array
|
||||
{
|
||||
$listCommand = new ListCommand();
|
||||
$listCommand->setHidden(true);
|
||||
$commands = [
|
||||
new HelpCommand(),
|
||||
$listCommand,
|
||||
new CompleteCommand(),
|
||||
new DumpCompletionCommand(),
|
||||
new InitCommand(),
|
||||
new DeployCommand(),
|
||||
];
|
||||
if (true === IS_PHAR) {
|
||||
$commands[] = new SelfUpdateCommand();
|
||||
}
|
||||
return $commands;
|
||||
}
|
||||
public function getCommands(): array
|
||||
{
|
||||
return $this->getDefaultCommands();
|
||||
}
|
||||
public function getContainer(): ContainerInterface
|
||||
{
|
||||
return $this->kernel->getContainer();
|
||||
}
|
||||
}
|
0
src/Controller/.gitignore
vendored
Executable file
0
src/Controller/.gitignore
vendored
Executable file
31
src/Deployment/Command/CommandExecutor.php
Executable file
31
src/Deployment/Command/CommandExecutor.php
Executable file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Deployment\Command;
|
||||
|
||||
use App\System\Command;
|
||||
use App\System\Process;
|
||||
|
||||
class CommandExecutor
|
||||
{
|
||||
public function execute(Command $command, $timeout = 30): void
|
||||
{
|
||||
try {
|
||||
$runInBackground = $command->runInBackground();
|
||||
$process = Process::fromShellCommandline($command->getCommand(), '/tmp/');
|
||||
$process->setCommand($command);
|
||||
if (true === $runInBackground) {
|
||||
$process->start();
|
||||
} else {
|
||||
$process->setTimeout($timeout);
|
||||
$process->run();
|
||||
if (false === $process->isSuccessful()) {
|
||||
throw new \RuntimeException($process->getErrorOutput());
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$fullCommand = $command->getCommand();
|
||||
$errorMessage = sprintf('Command "%s : %s" failed, error message: %s', $command->getName(), $fullCommand, $e->getMessage());
|
||||
throw new \Exception($errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
117
src/Deployment/Config.php
Executable file
117
src/Deployment/Config.php
Executable file
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Deployment;
|
||||
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class Config
|
||||
{
|
||||
private array $data = [];
|
||||
private ?bool $configParsed = null;
|
||||
|
||||
public function getGitRepository(): ?string
|
||||
{
|
||||
return $this->getValue('git_repository');
|
||||
}
|
||||
|
||||
public function getDeployDirectory(): ?string
|
||||
{
|
||||
$deployConfig = $this->getValue('deploy');
|
||||
$deployDirectory = $deployConfig['directory'] ?? '';
|
||||
return $deployDirectory;
|
||||
}
|
||||
|
||||
public function getOverlaysDirectory(): string
|
||||
{
|
||||
$deployConfigDirectory = $this->getConfigDirectory();
|
||||
$overlaysDirectory = sprintf('%s/overlays', $deployConfigDirectory);
|
||||
return $overlaysDirectory;
|
||||
}
|
||||
|
||||
public function getConfigDirectory(): string
|
||||
{
|
||||
$systemUserName = $this->getSystemUserName();
|
||||
$configDirectory = sprintf('/home/%s/.dploy', $systemUserName);
|
||||
return $configDirectory;
|
||||
}
|
||||
|
||||
public function getHomeDirectory()
|
||||
{
|
||||
$homeDirectory = $_SERVER['HOME'] ?? '';
|
||||
return $homeDirectory;
|
||||
}
|
||||
|
||||
public function getConfigFile(): string
|
||||
{
|
||||
$configDirectory = $this->getConfigDirectory();
|
||||
$configFile = sprintf('%s/config.yml', $configDirectory);
|
||||
return $configFile;
|
||||
}
|
||||
|
||||
public function getReleasesDirectory(): string
|
||||
{
|
||||
$deployDirectory = $this->getDeployDirectory();
|
||||
$releasesDirectory = sprintf('%s/releases', $deployDirectory);
|
||||
return $releasesDirectory;
|
||||
}
|
||||
|
||||
public function getSharedDirectory(): string
|
||||
{
|
||||
$deployDirectory = $this->getDeployDirectory();
|
||||
$sharedDirectory = sprintf('%s/shared', $deployDirectory);
|
||||
return $sharedDirectory;
|
||||
}
|
||||
|
||||
public function getSharedDirectories(): array
|
||||
{
|
||||
$deployConfig = $this->getValue('deploy');
|
||||
$sharedDirectories = (true == isset($deployConfig['shared_directories']) ? (array)$deployConfig['shared_directories'] : []);
|
||||
return $sharedDirectories;
|
||||
}
|
||||
|
||||
public function getBeforeDeployCommands(): array
|
||||
{
|
||||
$deployConfig = $this->getValue('deploy');
|
||||
$beforeDeployCommands = (true == isset($deployConfig['before_commands']) ? (array)$deployConfig['before_commands'] : []);
|
||||
return $beforeDeployCommands;
|
||||
}
|
||||
|
||||
public function getAfterDeployCommands(): array
|
||||
{
|
||||
$deployConfig = $this->getValue('deploy');
|
||||
$afterDeployCommands = (true == isset($deployConfig['after_commands']) ? (array)$deployConfig['after_commands'] : []);
|
||||
return $afterDeployCommands;
|
||||
}
|
||||
|
||||
private function parseConfigFile(): void
|
||||
{
|
||||
if (true === is_null($this->configParsed)) {
|
||||
$configFile = $this->getConfigFile();
|
||||
$data = Yaml::parseFile($configFile);
|
||||
if (true === isset($data['project'])) {
|
||||
$this->data = $data['project'];
|
||||
}
|
||||
$this->configParsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private function getValue(string $key): mixed
|
||||
{
|
||||
$this->parseConfigFile();
|
||||
$value = $this->data[$key] ?? '';
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function getSystemUserName(): string
|
||||
{
|
||||
$systemUserId = $this->getSystemUserId();
|
||||
$processUser = posix_getpwuid($systemUserId);
|
||||
$systemUserName = $processUser['name'];
|
||||
return $systemUserName;
|
||||
}
|
||||
public function getSystemUserId(): int
|
||||
{
|
||||
$systemUserId = posix_geteuid();
|
||||
return $systemUserId;
|
||||
}
|
||||
}
|
131
src/Deployment/Deployment.php
Executable file
131
src/Deployment/Deployment.php
Executable file
@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace App\Deployment;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use App\Deployment\Task\GitCloneRepository as GitCloneRepositoryTask;
|
||||
use App\Deployment\Task\CopyOverlayFiles as CopyOverlayFilesTask;
|
||||
use App\Deployment\Task\SymlinkSharedDirectories as SymlinkSharedDirectoriesTask;
|
||||
use App\Deployment\Task\ExecuteCommand as ExecuteCommandTask;
|
||||
use App\Deployment\Task\SetPermissions as SetPermissionsTask;
|
||||
use App\Deployment\Task\ReleaseSwitch as ReleaseSwitchTask;
|
||||
use App\Deployment\Task\CleanUpReleases as CleanUpReleasesTask;
|
||||
|
||||
class Deployment
|
||||
{
|
||||
private ?string $releaseName = null;
|
||||
private array $tasks = [];
|
||||
|
||||
public function __construct(
|
||||
private OutputInterface $output,
|
||||
private readonly Config $config,
|
||||
private readonly string $version
|
||||
) {
|
||||
}
|
||||
|
||||
public function deploy(): void
|
||||
{
|
||||
$this->validate();
|
||||
$this->executeTasks();
|
||||
}
|
||||
|
||||
private function executeTasks(): void
|
||||
{
|
||||
$this->addTasks();
|
||||
foreach ($this->tasks as $task) {
|
||||
$this->output->writeln(sprintf('<comment>%s ...</comment>', $task->getDescription()));
|
||||
$task->run();
|
||||
}
|
||||
}
|
||||
|
||||
private function addTasks(): void
|
||||
{
|
||||
$gitCloneRepositoryTask = new GitCloneRepositoryTask($this);
|
||||
$copyOverlayFilesTask = new CopyOverlayFilesTask($this);
|
||||
$symlinkSharedDirectoriesTask = new SymlinkSharedDirectoriesTask($this);
|
||||
$this->tasks = array_merge([
|
||||
$gitCloneRepositoryTask,
|
||||
$copyOverlayFilesTask,
|
||||
$symlinkSharedDirectoriesTask
|
||||
], $this->tasks);
|
||||
$beforeCommands = $this->config->getBeforeDeployCommands();
|
||||
if (false === empty($beforeCommands)) {
|
||||
foreach ($beforeCommands as $command) {
|
||||
if (false === empty($command)) {
|
||||
$executeCommandTask = new ExecuteCommandTask($this);
|
||||
$executeCommandTask->setCommand($command);
|
||||
$this->tasks[] = $executeCommandTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
//$setPermissionsTask = new SetPermissionsTask($this);
|
||||
$releaseSwitchTask = new ReleaseSwitchTask($this);
|
||||
$this->tasks = array_merge($this->tasks, [$releaseSwitchTask]);
|
||||
$afterCommands = $this->config->getAfterDeployCommands();
|
||||
if (false === empty($afterCommands)) {
|
||||
foreach ($afterCommands as $command) {
|
||||
if (false === empty($command)) {
|
||||
$executeCommandTask = new ExecuteCommandTask($this);
|
||||
$executeCommandTask->setCommand($command);
|
||||
$this->tasks[] = $executeCommandTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
$cleanUpReleasesTask = new CleanUpReleasesTask($this);
|
||||
$this->tasks[] = $cleanUpReleasesTask;
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
public function getOutput(): OutputInterface
|
||||
{
|
||||
return $this->output;
|
||||
}
|
||||
|
||||
public function getReleaseName(): string
|
||||
{
|
||||
if (true === is_null($this->releaseName)) {
|
||||
$dateTime = new \DateTime('now');
|
||||
$this->releaseName = sprintf('%s-%s', $dateTime->format('Y-m-d-H-i-s'), $this->version);
|
||||
}
|
||||
return $this->releaseName;
|
||||
}
|
||||
|
||||
public function getReleaseDirectory(): string
|
||||
{
|
||||
$releaseName = $this->getReleaseName();
|
||||
$releasesDirectory = $this->config->getReleasesDirectory();
|
||||
$releaseDirectory = sprintf('%s/%s', $releasesDirectory, $releaseName);
|
||||
return $releaseDirectory;
|
||||
}
|
||||
|
||||
public function getConfig(): Config
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
private function validate(): void
|
||||
{
|
||||
$filesystem = new Filesystem();
|
||||
$deployDirectory = $this->config->getDeployDirectory();
|
||||
$systemUserName = $this->config->getSystemUserName();
|
||||
if (false === $filesystem->exists($deployDirectory)) {
|
||||
throw new \Exception(sprintf('Deploy directory "%s" does not exist.', $deployDirectory));
|
||||
}
|
||||
if (false === str_starts_with($deployDirectory, sprintf('/home/%s/htdocs', $systemUserName))) {
|
||||
throw new \Exception(sprintf('System User "%s" is not part of the deploy directory: "%s"', $systemUserName, $deployDirectory));
|
||||
}
|
||||
$overlaysDirectory = $this->config->getOverlaysDirectory();
|
||||
if (false === $filesystem->exists($overlaysDirectory)) {
|
||||
throw new \Exception(sprintf('Overlays directory "%s" does not exist.', $overlaysDirectory));
|
||||
}
|
||||
$gitRepository = $this->config->getGitRepository();
|
||||
if (true === empty($gitRepository)) {
|
||||
throw new \Exception('Git repository cannot be empty.');
|
||||
}
|
||||
}
|
||||
}
|
53
src/Deployment/Setup.php
Executable file
53
src/Deployment/Setup.php
Executable file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Deployment;
|
||||
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
class Setup
|
||||
{
|
||||
private const DIRECTORY_CHMOD = 0760;
|
||||
|
||||
public function __construct(
|
||||
private readonly Config $config
|
||||
) {
|
||||
}
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
$gitRepository = $this->config->getGitRepository();
|
||||
$deployDirectory = $this->config->getDeployDirectory();
|
||||
if (true === empty($gitRepository)) {
|
||||
throw new \Exception('git repository cannot be empty.');
|
||||
}
|
||||
if (true === empty($deployDirectory)) {
|
||||
throw new \Exception('deploy directory cannot be empty.');
|
||||
}
|
||||
$filesystem = new Filesystem();
|
||||
$configDirectory = $this->config->getConfigDirectory();
|
||||
if (false === $filesystem->exists($configDirectory)) {
|
||||
$filesystem->mkdir($configDirectory, self::DIRECTORY_CHMOD);
|
||||
}
|
||||
$releasesDirectory = $this->config->getReleasesDirectory();
|
||||
if (false === $filesystem->exists($releasesDirectory)) {
|
||||
$filesystem->mkdir($releasesDirectory, self::DIRECTORY_CHMOD);
|
||||
}
|
||||
$overlaysDirectory = $this->config->getOverlaysDirectory();
|
||||
if (false === $filesystem->exists($overlaysDirectory)) {
|
||||
$filesystem->mkdir($overlaysDirectory, self::DIRECTORY_CHMOD);
|
||||
}
|
||||
$sharedDirectory = $this->config->getSharedDirectory();
|
||||
if (false === $filesystem->exists($sharedDirectory)) {
|
||||
$filesystem->mkdir($sharedDirectory, self::DIRECTORY_CHMOD);
|
||||
}
|
||||
$sharedDirectories = $this->config->getSharedDirectories();
|
||||
if (false === empty($sharedDirectories)) {
|
||||
foreach ($sharedDirectories as $directory) {
|
||||
$directory = sprintf('%s/%s', $sharedDirectory, $directory);
|
||||
if (false === $filesystem->exists($directory)) {
|
||||
$filesystem->mkdir($directory, self::DIRECTORY_CHMOD);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
src/Deployment/Task/CleanUpReleases.php
Executable file
19
src/Deployment/Task/CleanUpReleases.php
Executable file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Deployment\Task;
|
||||
|
||||
use App\Deployment\Task\Task as BaseTask;
|
||||
|
||||
class CleanUpReleases extends BaseTask
|
||||
{
|
||||
protected string $description = 'Clean Up Releases';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$deployment = $this->getDeployment();
|
||||
$config = $deployment->getConfig();
|
||||
$releasesDirectory = $config->getReleasesDirectory();
|
||||
$command = sprintf('cd %s && ls -t | tail -n +4 | xargs rm -rf', $releasesDirectory);
|
||||
$this->runCommand($command);
|
||||
}
|
||||
}
|
28
src/Deployment/Task/CopyOverlayFiles.php
Executable file
28
src/Deployment/Task/CopyOverlayFiles.php
Executable file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Deployment\Task;
|
||||
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use App\Deployment\Task\Task as BaseTask;
|
||||
|
||||
class CopyOverlayFiles extends BaseTask
|
||||
{
|
||||
protected string $description = 'Copying Overlay Files';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$deployment = $this->getDeployment();
|
||||
$config = $deployment->getConfig();
|
||||
$releaseDirectory = $deployment->getReleaseDirectory();
|
||||
$overlaysDirectory = $config->getOverlaysDirectory();
|
||||
$filesystem = new Filesystem();
|
||||
$objects = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($overlaysDirectory), \RecursiveIteratorIterator::SELF_FIRST);
|
||||
foreach($objects as $object){
|
||||
if (true === $object->isFile()) {
|
||||
$originFile = $object->getRealPath();
|
||||
$targetFile = str_replace($overlaysDirectory, $releaseDirectory, $originFile);
|
||||
$filesystem->copy($originFile, $targetFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
51
src/Deployment/Task/ExecuteCommand.php
Executable file
51
src/Deployment/Task/ExecuteCommand.php
Executable file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Deployment\Task;
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
use App\Deployment\Task\Task as BaseTask;
|
||||
|
||||
class ExecuteCommand extends BaseTask
|
||||
{
|
||||
protected string $description = 'Executing Command';
|
||||
private string $command;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$deployment = $this->getDeployment();
|
||||
$output = $deployment->getOutput();
|
||||
$command = $this->getCommand();
|
||||
$process = Process::fromShellCommandline($command, '/tmp/');
|
||||
$process->setTimeout(3600);
|
||||
$process->run(function ($type, $buffer) use ($output) {
|
||||
if (false === empty($buffer)) {
|
||||
$output->write($buffer);
|
||||
}
|
||||
});
|
||||
if (false === $process->isSuccessful()) {
|
||||
throw new \RuntimeException($process->getErrorOutput());
|
||||
}
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
$command = $this->getCommand();
|
||||
$this->description = sprintf('%s: %s', $this->description, $command);
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setCommand(string $command): void
|
||||
{
|
||||
$this->command = $command;
|
||||
}
|
||||
|
||||
public function getCommand(): string
|
||||
{
|
||||
$deployment = $this->getDeployment();
|
||||
$releaseDirectory = $deployment->getReleaseDirectory();
|
||||
if (true === str_contains($this->command, '{release_directory}')) {
|
||||
$this->command = str_replace('{release_directory}', $releaseDirectory, $this->command);
|
||||
}
|
||||
return $this->command;
|
||||
}
|
||||
}
|
38
src/Deployment/Task/GitCloneRepository.php
Executable file
38
src/Deployment/Task/GitCloneRepository.php
Executable file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Deployment\Task;
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use App\Deployment\Task\Task as BaseTask;
|
||||
|
||||
class GitCloneRepository extends BaseTask
|
||||
{
|
||||
protected string $description = 'Cloning Git Repository';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$deployment = $this->getDeployment();
|
||||
$config = $deployment->getConfig();
|
||||
$gitRepository = $config->getGitRepository();
|
||||
$version = $deployment->getVersion();
|
||||
$releaseDirectory = $deployment->getReleaseDirectory();
|
||||
$command = sprintf('/usr/bin/git clone -c advice.detachedHead=false --progress --depth 1 --branch %s %s %s', escapeshellarg($version), escapeshellarg($gitRepository), escapeshellarg($releaseDirectory));
|
||||
$output = $deployment->getOutput();
|
||||
$process = Process::fromShellCommandline($command, '/tmp/');
|
||||
$process->setTimeout(600);
|
||||
$process->run(function ($type, $buffer) use ($output) {
|
||||
if (false === empty($buffer)) {
|
||||
$output->write($buffer);
|
||||
}
|
||||
});
|
||||
if (false === $process->isSuccessful()) {
|
||||
throw new \RuntimeException($process->getErrorOutput());
|
||||
}
|
||||
$gitDirectory = sprintf('%s/.git', $releaseDirectory);
|
||||
$filesystem = new Filesystem();
|
||||
if (true === $filesystem->exists($gitDirectory)) {
|
||||
$filesystem->remove($gitDirectory);
|
||||
}
|
||||
}
|
||||
}
|
21
src/Deployment/Task/ReleaseSwitch.php
Executable file
21
src/Deployment/Task/ReleaseSwitch.php
Executable file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Deployment\Task;
|
||||
|
||||
use App\Deployment\Task\Task as BaseTask;
|
||||
|
||||
class ReleaseSwitch extends BaseTask
|
||||
{
|
||||
protected string $description = 'Switching Release';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$deployment = $this->getDeployment();
|
||||
$config = $deployment->getConfig();
|
||||
$deployDirectory = $config->getDeployDirectory();
|
||||
$releaseDirectory = $deployment->getReleaseDirectory();
|
||||
$currentDirectory = sprintf('%s/current', $deployDirectory);
|
||||
$command = sprintf('ln -sfn %s %s', $releaseDirectory, $currentDirectory);
|
||||
$this->runCommand($command);
|
||||
}
|
||||
}
|
26
src/Deployment/Task/SetPermissions.php
Executable file
26
src/Deployment/Task/SetPermissions.php
Executable file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Deployment\Task;
|
||||
|
||||
use App\Deployment\Task\Task as BaseTask;
|
||||
|
||||
class SetPermissions extends BaseTask
|
||||
{
|
||||
private const DIRECTORY_CHMOD = 770;
|
||||
private const FILE_CHMOD = 660;
|
||||
|
||||
protected string $description = 'Setting Permissions';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$deployment = $this->getDeployment();
|
||||
$releaseDirectory = $deployment->getReleaseDirectory();
|
||||
$command = sprintf('/usr/bin/find %s -type d -exec chmod %s {} \; && /usr/bin/find %s -type f -exec chmod %s {} \;',
|
||||
escapeshellarg($releaseDirectory),
|
||||
self::DIRECTORY_CHMOD,
|
||||
escapeshellarg($releaseDirectory),
|
||||
self::FILE_CHMOD
|
||||
);
|
||||
$this->runCommand($command);
|
||||
}
|
||||
}
|
28
src/Deployment/Task/SymlinkSharedDirectories.php
Executable file
28
src/Deployment/Task/SymlinkSharedDirectories.php
Executable file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Deployment\Task;
|
||||
|
||||
use App\Deployment\Task\Task as BaseTask;
|
||||
|
||||
class SymlinkSharedDirectories extends BaseTask
|
||||
{
|
||||
protected string $description = 'Symlink Shared Directories';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$deployment = $this->getDeployment();
|
||||
$config = $deployment->getConfig();
|
||||
$releaseDirectory = $deployment->getReleaseDirectory();
|
||||
$sharedDirectories = $config->getSharedDirectories();
|
||||
if (false === empty($sharedDirectories)) {
|
||||
foreach ($sharedDirectories as $sharedDirectory) {
|
||||
$sharedDirectory = rtrim(ltrim($sharedDirectory, '/'), '/');
|
||||
$sharedDirectoryPath = sprintf('%s/%s', rtrim($releaseDirectory, '/'), $sharedDirectory);
|
||||
$deleteDestinationCommand = sprintf('rm -rf %s', $sharedDirectoryPath);
|
||||
$this->runCommand($deleteDestinationCommand);
|
||||
$symlinkCommand = sprintf('cd %s && ln -sfrn ../../shared/%s %s', $releaseDirectory, $sharedDirectory, $sharedDirectory);
|
||||
$this->runCommand($symlinkCommand);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
41
src/Deployment/Task/Task.php
Executable file
41
src/Deployment/Task/Task.php
Executable file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Deployment\Task;
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
use App\Deployment\Deployment;
|
||||
|
||||
abstract class Task
|
||||
{
|
||||
protected string $description;
|
||||
|
||||
public function __construct(
|
||||
private Deployment $deployment
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function run()
|
||||
{
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function getDeployment(): Deployment
|
||||
{
|
||||
return $this->deployment;
|
||||
}
|
||||
|
||||
protected function runCommand(string $command, $timeout = 900)
|
||||
{
|
||||
$process = Process::fromShellCommandline($command, '/tmp/');
|
||||
$process->setTimeout($timeout);
|
||||
$process->run();
|
||||
if (false === $process->isSuccessful()) {
|
||||
throw new \RuntimeException($process->getErrorOutput());
|
||||
}
|
||||
}
|
||||
}
|
111
src/Dploy.php
Executable file
111
src/Dploy.php
Executable file
@ -0,0 +1,111 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
|
||||
class Dploy
|
||||
{
|
||||
private const BASE_URL = 'https://dploy.cloudpanel.io';
|
||||
private const REQUEST_TIMEOUT = '30';
|
||||
public const CHANNEL_STABLE = 'stable';
|
||||
public const CHANNELS = ['preview', 'stable'];
|
||||
private float $percentageDownloaded = 0;
|
||||
private ?string $tmpFile;
|
||||
public function __construct(
|
||||
private HttpClientInterface $httpClient,
|
||||
)
|
||||
{
|
||||
}
|
||||
public function downloadVersion(string $version, OutputInterface $output): string
|
||||
{
|
||||
$downloadedFile = '';
|
||||
$remoteFile = sprintf('%s/download/%s/dploy', self::BASE_URL, $version);
|
||||
$progressBar = new ProgressBar($output, 100);
|
||||
$progressBar->setFormat('verbose');
|
||||
$this->percentageDownloaded = 0;
|
||||
$response = $this->httpClient->request('GET', $remoteFile, [
|
||||
'on_progress' => function (int $downloadedInBytes, int $downloadFilesizeInBytes, array $info) use ($progressBar): void {
|
||||
if ($downloadedInBytes > 0) {
|
||||
$downloadedInMegabyte = round($downloadedInBytes/1000000);
|
||||
$downloadFilesizeInMegabyte = round($downloadFilesizeInBytes/1000000);
|
||||
$percentageDownloaded = (int)round(($downloadedInMegabyte/$downloadFilesizeInMegabyte)*100);
|
||||
if ($percentageDownloaded > 0 && $percentageDownloaded > $this->percentageDownloaded) {
|
||||
$this->percentageDownloaded = $percentageDownloaded;
|
||||
$progressBar->setProgress($percentageDownloaded);
|
||||
}
|
||||
}
|
||||
},
|
||||
]);
|
||||
if (200 == $response->getStatusCode()) {
|
||||
$this->tmpFile = tempnam(sys_get_temp_dir(), '');
|
||||
$tmpFile = sprintf('%s.phar', $this->tmpFile);
|
||||
rename($this->tmpFile, $tmpFile);
|
||||
$this->tmpFile = $tmpFile;
|
||||
$body = $response->getContent();
|
||||
file_put_contents($this->tmpFile, $body);
|
||||
$downloadedFile = $this->tmpFile;
|
||||
} else {
|
||||
throw new \Exception(sprintf('Cannot download file %s, status code: %s', $remoteFile, $response->getStatusCode()));
|
||||
}
|
||||
return $downloadedFile;
|
||||
}
|
||||
|
||||
public function getSignatureForVersion(string $version): string
|
||||
{
|
||||
$signature = '';
|
||||
$signatureFile = sprintf('%s/download/%s/dploy.sig', self::BASE_URL, $version);
|
||||
$response = $this->httpClient->request('GET', $signatureFile, ['timeout' => self::REQUEST_TIMEOUT]);
|
||||
$statusCode = $response->getStatusCode();
|
||||
if (200 == $statusCode) {
|
||||
$signature = (string)$response->getContent();
|
||||
$signature = base64_decode($signature);
|
||||
} else {
|
||||
throw new \Exception(sprintf('Signature file %s not available.', $signatureFile));
|
||||
}
|
||||
return $signature;
|
||||
}
|
||||
public function getLatest(string $channel)
|
||||
{
|
||||
$latest = $this->getVersionsData($channel);
|
||||
return $latest;
|
||||
}
|
||||
|
||||
private function getVersionsData(string $channel): array
|
||||
{
|
||||
$requestUrl = sprintf('%s/versions.json', self::BASE_URL);
|
||||
$response = $this->httpClient->request('GET', $requestUrl, ['timeout' => self::REQUEST_TIMEOUT]);
|
||||
$statusCode = $response->getStatusCode();
|
||||
if (200 == $statusCode) {
|
||||
$versionsData = (array)$response->toArray();
|
||||
if (true === isset($versionsData[$channel]) && false === empty($versionsData[$channel])) {
|
||||
$versionsData = $versionsData[$channel];
|
||||
return $versionsData;
|
||||
} else {
|
||||
throw new \Exception(sprintf('No versions data available for channel %s.', $channel));
|
||||
}
|
||||
} else {
|
||||
throw new \Exception(sprintf('Versions file %s not available.', $requestUrl));
|
||||
}
|
||||
}
|
||||
|
||||
static public function getChannels(): array
|
||||
{
|
||||
return self::CHANNELS;
|
||||
}
|
||||
|
||||
static public function getVersion(): string
|
||||
{
|
||||
$version = $_ENV['APP_VERSION'] ?? '';
|
||||
return $version;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
if (true === isset($this->tmpFile) && true === file_exists($this->tmpFile)) {
|
||||
@unlink($this->tmpFile);
|
||||
}
|
||||
}
|
||||
}
|
38
src/Kernel.php
Executable file
38
src/Kernel.php
Executable file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||
|
||||
class Kernel extends BaseKernel
|
||||
{
|
||||
use MicroKernelTrait;
|
||||
|
||||
public function boot()
|
||||
{
|
||||
date_default_timezone_set('UTC');
|
||||
setlocale(LC_CTYPE, "en_US.UTF-8");
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
private function getHomeDirectory()
|
||||
{
|
||||
$homeDirectory = $_SERVER['HOME'];
|
||||
return $homeDirectory;
|
||||
}
|
||||
|
||||
public function getCacheDir(): string
|
||||
{
|
||||
$homeDirectory = $this->getHomeDirectory();
|
||||
$cacheDir = sprintf('%s/.dploy/.app/cache', $homeDirectory);
|
||||
return $cacheDir;
|
||||
}
|
||||
|
||||
public function getLogDir(): string
|
||||
{
|
||||
$homeDirectory = $this->getHomeDirectory();
|
||||
$logDir = sprintf('%s/.dploy/.app/logs', $homeDirectory);
|
||||
return $logDir;
|
||||
}
|
||||
}
|
60
src/SelfUpdate/Config.php
Executable file
60
src/SelfUpdate/Config.php
Executable file
@ -0,0 +1,60 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\SelfUpdate;
|
||||
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
class Config
|
||||
{
|
||||
public function get(string $key): mixed
|
||||
{
|
||||
switch ($key) {
|
||||
case 'dploy-directory':
|
||||
$homeDirectory = $this->get('home-directory');
|
||||
$dployDirectory = sprintf('%s/.dploy', $homeDirectory);
|
||||
return $dployDirectory;
|
||||
case 'cache-directory':
|
||||
$dployDirectory = $this->get('dploy-directory');
|
||||
$cacheDirectory = sprintf('/home/%s/.cache', $dployDirectory);
|
||||
return $cacheDirectory;
|
||||
case 'home-directory':
|
||||
$homeDirectory = $_SERVER['HOME'] ?? '';
|
||||
return $homeDirectory;
|
||||
case 'channel':
|
||||
$channel = '';
|
||||
$dployDirectory = $this->get('dploy-directory');
|
||||
$channelFile = sprintf('%s/.channel', $dployDirectory);
|
||||
if (true === file_exists($channelFile)) {
|
||||
$channel = trim(file_get_contents($channelFile));
|
||||
}
|
||||
return $channel;
|
||||
case 'channel-file':
|
||||
$dployDirectory = $this->get('dploy-directory');
|
||||
$channelFile = sprintf('%s/.channel', $dployDirectory);
|
||||
return $channelFile;
|
||||
}
|
||||
}
|
||||
|
||||
public function set(string $key, string $value): void
|
||||
{
|
||||
$filesystem = new Filesystem();
|
||||
$dployDirectory = $this->get('dploy-directory');
|
||||
if (false === file_exists($dployDirectory)) {
|
||||
$filesystem->mkdir($dployDirectory, 0770);
|
||||
}
|
||||
switch ($key) {
|
||||
case 'channel':
|
||||
$channelFile = $this->get('channel-file');
|
||||
file_put_contents($channelFile, $value);
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
static public function getSystemUserName(): string
|
||||
{
|
||||
$processUser = posix_getpwuid(posix_geteuid());
|
||||
$systemUserName = $processUser['name'];
|
||||
return $systemUserName;
|
||||
}
|
||||
}
|
23
src/Util/Retry.php
Executable file
23
src/Util/Retry.php
Executable file
@ -0,0 +1,23 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace App\Util;
|
||||
|
||||
class Retry
|
||||
{
|
||||
static public function retry(callable $fn, $retries = 2, $delay = 5): mixed
|
||||
{
|
||||
beginning:
|
||||
try {
|
||||
return $fn();
|
||||
} catch (\Exception $e) {
|
||||
if (!$retries) {
|
||||
throw $e;
|
||||
}
|
||||
$retries--;
|
||||
if ($delay) {
|
||||
sleep($delay);
|
||||
}
|
||||
goto beginning;
|
||||
}
|
||||
}
|
||||
}
|
58
symfony.lock
Executable file
58
symfony.lock
Executable file
@ -0,0 +1,58 @@
|
||||
{
|
||||
"symfony/console": {
|
||||
"version": "6.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "5.3",
|
||||
"ref": "da0c8be8157600ad34f10ff0c9cc91232522e047"
|
||||
},
|
||||
"files": [
|
||||
"bin/console"
|
||||
]
|
||||
},
|
||||
"symfony/flex": {
|
||||
"version": "2.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "146251ae39e06a95be0fe3d13c807bcf3938b172"
|
||||
},
|
||||
"files": [
|
||||
".env"
|
||||
]
|
||||
},
|
||||
"symfony/framework-bundle": {
|
||||
"version": "6.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.2",
|
||||
"ref": "af47254c5e4cd543e6af3e4508298ffebbdaddd3"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/cache.yaml",
|
||||
"config/packages/framework.yaml",
|
||||
"config/preload.php",
|
||||
"config/routes/framework.yaml",
|
||||
"config/services.yaml",
|
||||
"public/index.php",
|
||||
"src/Controller/.gitignore",
|
||||
"src/Kernel.php"
|
||||
]
|
||||
},
|
||||
"symfony/routing": {
|
||||
"version": "6.2",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.2",
|
||||
"ref": "e0a11b4ccb8c9e70b574ff5ad3dfdcd41dec5aa6"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/routing.yaml",
|
||||
"config/routes.yaml"
|
||||
]
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user