2015-12-14 22:37:46 +00:00
< ? php
namespace App\Console\Commands ;
2019-11-29 20:28:34 +00:00
use App\Console\Commands\Traits\AskForPassword ;
2019-09-10 18:30:38 +00:00
use App\Exceptions\InstallationFailedException ;
2017-12-03 16:54:11 +00:00
use App\Models\Setting ;
2016-01-15 02:16:58 +00:00
use App\Models\User ;
2018-08-29 09:41:24 +00:00
use App\Repositories\SettingRepository ;
2018-08-19 14:56:56 +00:00
use App\Services\MediaCacheService ;
2015-12-14 22:37:46 +00:00
use Illuminate\Console\Command ;
2018-08-19 21:01:01 +00:00
use Illuminate\Contracts\Console\Kernel as Artisan ;
2020-12-22 20:11:22 +00:00
use Illuminate\Contracts\Hashing\Hasher as Hash ;
2018-08-19 21:01:01 +00:00
use Illuminate\Database\DatabaseManager as DB ;
use Jackiedo\DotenvEditor\DotenvEditor ;
2020-12-22 20:11:22 +00:00
use Throwable ;
2015-12-14 22:37:46 +00:00
2018-08-19 21:01:01 +00:00
class InitCommand extends Command
2015-12-14 22:37:46 +00:00
{
2019-11-29 20:28:34 +00:00
use AskForPassword ;
2021-01-23 18:58:08 +00:00
private const DEFAULT_ADMIN_NAME = 'Koel' ;
private const DEFAULT_ADMIN_EMAIL = 'admin@koel.dev' ;
private const DEFAULT_ADMIN_PASSWORD = 'KoelIsCool' ;
2021-04-16 14:15:21 +00:00
private const NON_INTERACTION_MAX_ATTEMPT_COUNT = 10 ;
2021-01-23 18:58:08 +00:00
2020-07-15 13:20:02 +00:00
protected $signature = 'koel:init {--no-assets}' ;
2016-01-15 02:16:58 +00:00
protected $description = 'Install or upgrade Koel' ;
2018-08-19 14:56:56 +00:00
2021-06-05 10:47:56 +00:00
private MediaCacheService $mediaCacheService ;
private Artisan $artisan ;
private DotenvEditor $dotenvEditor ;
private Hash $hash ;
private DB $db ;
private SettingRepository $settingRepository ;
private bool $adminSeeded = false ;
2018-08-19 21:01:01 +00:00
public function __construct (
MediaCacheService $mediaCacheService ,
2018-08-29 09:41:24 +00:00
SettingRepository $settingRepository ,
2018-08-19 21:01:01 +00:00
Artisan $artisan ,
Hash $hash ,
DotenvEditor $dotenvEditor ,
DB $db
2018-08-19 21:03:21 +00:00
) {
2018-08-19 14:56:56 +00:00
parent :: __construct ();
$this -> mediaCacheService = $mediaCacheService ;
2018-08-19 21:01:01 +00:00
$this -> artisan = $artisan ;
$this -> dotenvEditor = $dotenvEditor ;
$this -> hash = $hash ;
$this -> db = $db ;
2018-08-29 09:41:24 +00:00
$this -> settingRepository = $settingRepository ;
2018-08-19 14:56:56 +00:00
}
2015-12-14 22:37:46 +00:00
2018-08-24 15:27:19 +00:00
public function handle () : void
2015-12-14 22:37:46 +00:00
{
2016-01-15 02:16:58 +00:00
$this -> comment ( 'Attempting to install or upgrade Koel.' );
$this -> comment ( 'Remember, you can always install/upgrade manually following the guide here:' );
2020-12-22 20:11:22 +00:00
$this -> info ( '📙 ' . config ( 'koel.misc.docs_url' ) . PHP_EOL );
2015-12-14 22:37:46 +00:00
2019-01-01 11:53:20 +00:00
if ( $this -> inNoInteractionMode ()) {
$this -> info ( 'Running in no-interaction mode' );
}
2019-09-10 18:30:38 +00:00
try {
$this -> maybeGenerateAppKey ();
$this -> maybeSetUpDatabase ();
$this -> migrateDatabase ();
$this -> maybeSeedDatabase ();
$this -> maybeSetMediaPath ();
2020-07-15 13:20:02 +00:00
$this -> maybeCompileFrontEndAssets ();
2020-12-22 20:11:22 +00:00
} catch ( Throwable $e ) {
2019-09-10 18:30:38 +00:00
$this -> error ( " Oops! Koel installation or upgrade didn't finish successfully. " );
2020-12-22 20:11:22 +00:00
$this -> error ( 'Please try again, or visit ' . config ( 'koel.misc.docs_url' ) . ' for manual installation.' );
2019-09-10 18:30:38 +00:00
$this -> error ( '😥 Sorry for this. You deserve better.' );
return ;
}
2015-12-14 22:37:46 +00:00
2020-12-22 20:11:22 +00:00
$this -> comment ( PHP_EOL . '🎆 Success! Koel can now be run from localhost with `php artisan serve`.' );
2018-08-19 21:01:01 +00:00
2021-01-23 18:58:08 +00:00
if ( $this -> adminSeeded ) {
$this -> comment (
sprintf ( 'Log in with email %s and password %s' , self :: DEFAULT_ADMIN_EMAIL , self :: DEFAULT_ADMIN_PASSWORD )
);
}
2018-08-31 13:47:15 +00:00
if ( Setting :: get ( 'media_path' )) {
2017-12-03 16:54:11 +00:00
$this -> comment ( 'You can also scan for media with `php artisan koel:sync`.' );
}
2018-08-19 21:01:01 +00:00
2020-12-22 20:11:22 +00:00
$this -> comment ( 'Again, visit 📙 ' . config ( 'koel.misc.docs_url' ) . ' for the official documentation.' );
2019-09-10 18:30:38 +00:00
$this -> comment (
" Feeling generous and want to support Koel's development? Check out "
2020-12-22 20:11:22 +00:00
. config ( 'koel.misc.sponsor_github_url' )
. ' 🤗'
2019-09-10 18:30:38 +00:00
);
$this -> comment ( 'Thanks for using Koel. You rock! 🤘' );
2015-12-14 22:37:46 +00:00
}
2017-12-03 16:54:11 +00:00
/**
* Prompt user for valid database credentials and set up the database .
*/
2018-08-24 15:27:19 +00:00
private function setUpDatabase () : void
2017-12-03 16:54:11 +00:00
{
$config = [
'DB_CONNECTION' => '' ,
'DB_HOST' => '' ,
'DB_PORT' => '' ,
'DB_DATABASE' => '' ,
'DB_USERNAME' => '' ,
2017-12-03 16:54:34 +00:00
'DB_PASSWORD' => '' ,
2017-12-03 16:54:11 +00:00
];
$config [ 'DB_CONNECTION' ] = $this -> choice (
'Your DB driver of choice' ,
[
'mysql' => 'MySQL/MariaDB' ,
2017-12-11 20:50:32 +00:00
'pgsql' => 'PostgreSQL' ,
2017-12-03 16:54:11 +00:00
'sqlsrv' => 'SQL Server' ,
2017-12-03 16:54:34 +00:00
'sqlite-e2e' => 'SQLite' ,
2017-12-03 16:54:11 +00:00
],
'mysql'
);
2018-08-19 21:03:21 +00:00
2017-12-03 16:54:11 +00:00
if ( $config [ 'DB_CONNECTION' ] === 'sqlite-e2e' ) {
$config [ 'DB_DATABASE' ] = $this -> ask ( 'Absolute path to the DB file' );
} else {
$config [ 'DB_HOST' ] = $this -> anticipate ( 'DB host' , [ '127.0.0.1' , 'localhost' ]);
2019-06-30 14:22:53 +00:00
$config [ 'DB_PORT' ] = ( string ) $this -> ask ( 'DB port (leave empty for default)' );
2017-12-03 16:54:11 +00:00
$config [ 'DB_DATABASE' ] = $this -> anticipate ( 'DB name' , [ 'koel' ]);
$config [ 'DB_USERNAME' ] = $this -> anticipate ( 'DB user' , [ 'koel' ]);
2019-06-30 14:22:53 +00:00
$config [ 'DB_PASSWORD' ] = ( string ) $this -> ask ( 'DB password' );
2017-12-03 16:54:11 +00:00
}
foreach ( $config as $key => $value ) {
2018-08-19 21:01:01 +00:00
$this -> dotenvEditor -> setKey ( $key , $value );
2017-12-03 16:54:11 +00:00
}
2018-08-19 21:01:01 +00:00
$this -> dotenvEditor -> save ();
2017-12-03 16:54:11 +00:00
// Set the config so that the next DB attempt uses refreshed credentials
config ([
'database.default' => $config [ 'DB_CONNECTION' ],
" database.connections. { $config [ 'DB_CONNECTION' ] } .host " => $config [ 'DB_HOST' ],
" database.connections. { $config [ 'DB_CONNECTION' ] } .port " => $config [ 'DB_PORT' ],
" database.connections. { $config [ 'DB_CONNECTION' ] } .database " => $config [ 'DB_DATABASE' ],
" database.connections. { $config [ 'DB_CONNECTION' ] } .username " => $config [ 'DB_USERNAME' ],
" database.connections. { $config [ 'DB_CONNECTION' ] } .password " => $config [ 'DB_PASSWORD' ],
]);
}
2019-01-01 11:53:20 +00:00
private function inNoInteractionMode () : bool
{
return ( bool ) $this -> option ( 'no-interaction' );
}
2020-07-15 13:20:02 +00:00
private function inNoAssetsMode () : bool
{
return ( bool ) $this -> option ( 'no-assets' );
}
2018-08-24 15:27:19 +00:00
private function setUpAdminAccount () : void
2017-12-03 16:54:11 +00:00
{
2021-01-23 18:58:08 +00:00
$this -> info ( " Creating default admin account " );
2017-12-03 16:54:11 +00:00
User :: create ([
2021-01-23 18:58:08 +00:00
'name' => self :: DEFAULT_ADMIN_NAME ,
'email' => self :: DEFAULT_ADMIN_EMAIL ,
'password' => $this -> hash -> make ( self :: DEFAULT_ADMIN_PASSWORD ),
2017-12-03 16:54:11 +00:00
'is_admin' => true ,
]);
2021-01-23 18:58:08 +00:00
$this -> adminSeeded = true ;
2017-12-03 16:54:11 +00:00
}
2018-08-24 15:27:19 +00:00
private function maybeSetMediaPath () : void
2017-12-03 16:54:11 +00:00
{
2018-08-31 13:47:15 +00:00
if ( Setting :: get ( 'media_path' )) {
2018-08-23 06:51:16 +00:00
return ;
}
2019-01-01 11:53:20 +00:00
if ( $this -> inNoInteractionMode ()) {
$this -> setMediaPathFromEnvFile ();
return ;
}
2020-12-22 20:11:22 +00:00
$this -> info ( 'The absolute path to your media directory. If this is skipped (left blank) now, you can set it later via the web interface.' ); // @phpcs-ignore-line
2017-12-03 16:54:11 +00:00
while ( true ) {
2019-01-01 11:53:20 +00:00
$path = $this -> ask ( 'Media path' , config ( 'koel.media_path' ));
2018-08-19 21:01:01 +00:00
2019-01-01 11:53:20 +00:00
if ( ! $path ) {
2017-12-03 16:54:11 +00:00
return ;
}
2019-01-01 11:53:20 +00:00
if ( $this -> isValidMediaPath ( $path )) {
2017-12-03 16:54:11 +00:00
Setting :: set ( 'media_path' , $path );
2017-12-03 16:54:34 +00:00
2017-12-03 16:54:11 +00:00
return ;
}
$this -> error ( 'The path does not exist or not readable. Try again.' );
}
}
2018-08-23 06:51:16 +00:00
2018-08-24 15:27:19 +00:00
private function maybeGenerateAppKey () : void
2018-08-23 06:51:16 +00:00
{
if ( ! config ( 'app.key' )) {
$this -> info ( 'Generating app key' );
$this -> artisan -> call ( 'key:generate' );
} else {
$this -> comment ( 'App key exists -- skipping' );
}
}
2018-08-24 15:27:19 +00:00
private function maybeSeedDatabase () : void
2018-08-23 06:51:16 +00:00
{
if ( ! User :: count ()) {
$this -> setUpAdminAccount ();
$this -> info ( 'Seeding initial data' );
$this -> artisan -> call ( 'db:seed' , [ '--force' => true ]);
} else {
$this -> comment ( 'Data seeded -- skipping' );
}
}
2018-08-24 15:27:19 +00:00
private function maybeSetUpDatabase () : void
2018-08-23 06:51:16 +00:00
{
2021-04-16 14:15:21 +00:00
$attemptCount = 0 ;
2019-11-29 20:28:34 +00:00
while ( true ) {
2021-04-16 14:15:21 +00:00
// In non-interactive mode, we must not endlessly attempt to connect.
// Doing so will just end up with a huge amount of "failed to connect" logs.
// We do retry a little, though, just in case there's some kind of temporary failure.
if ( $this -> inNoInteractionMode () && $attemptCount >= self :: NON_INTERACTION_MAX_ATTEMPT_COUNT ) {
$this -> warn ( " Maximum database connection attempts reached. Giving up. " );
break ;
}
$attemptCount ++ ;
2018-08-23 06:51:16 +00:00
try {
// Make sure the config cache is cleared before another attempt.
$this -> artisan -> call ( 'config:clear' );
$this -> db -> reconnect () -> getPdo ();
2019-11-29 20:28:34 +00:00
break ;
2020-12-22 20:11:22 +00:00
} catch ( Throwable $e ) {
2018-08-23 06:51:16 +00:00
$this -> error ( $e -> getMessage ());
2021-04-16 14:15:21 +00:00
// We only try to update credentials if running in interactive mode.
// Otherwise, we require admin intervention to fix them.
// This avoids inadvertently wiping credentials if there's a connection failure.
if ( $this -> inNoInteractionMode ()) {
$warning = sprintf (
" %sKoel cannot connect to the database. Attempt: %d/%d " ,
PHP_EOL ,
$attemptCount ,
self :: NON_INTERACTION_MAX_ATTEMPT_COUNT
);
$this -> warn ( $warning );
} else {
$this -> warn ( sprintf ( " %sKoel cannot connect to the database. Let's set it up. " , PHP_EOL ));
$this -> setUpDatabase ();
}
2018-08-23 06:51:16 +00:00
}
}
}
2018-08-24 15:27:19 +00:00
private function migrateDatabase () : void
2018-08-23 06:51:16 +00:00
{
$this -> info ( 'Migrating database' );
$this -> artisan -> call ( 'migrate' , [ '--force' => true ]);
// Clear the media cache, just in case we did any media-related migration
$this -> mediaCacheService -> clear ();
}
2020-07-15 13:20:02 +00:00
private function maybeCompileFrontEndAssets () : void
2018-08-23 06:51:16 +00:00
{
2020-07-15 13:20:02 +00:00
if ( $this -> inNoAssetsMode ()) {
return ;
}
2019-08-28 21:55:53 +00:00
$this -> info ( 'Now to front-end stuff' );
2019-09-10 18:30:38 +00:00
// We need to run several yarn commands:
2019-08-28 21:55:53 +00:00
// - The first to install node_modules in the resources/assets submodule
2019-09-10 18:30:38 +00:00
// - The second and third for the root folder, to build Koel's front-end assets with Mix.
2019-08-28 21:55:53 +00:00
chdir ( './resources/assets' );
$this -> info ( '├── Installing Node modules in resources/assets directory' );
2019-09-10 18:30:38 +00:00
$runOkOrThrow = static function ( string $command ) : void {
passthru ( $command , $status );
throw_if (( bool ) $status , InstallationFailedException :: class );
};
$runOkOrThrow ( 'yarn install --colors' );
2019-08-28 21:55:53 +00:00
chdir ( '../..' );
$this -> info ( '└── Compiling assets' );
2019-09-10 18:30:38 +00:00
$runOkOrThrow ( 'yarn install --colors' );
2021-01-01 15:22:39 +00:00
$runOkOrThrow ( 'yarn build --colors' );
2018-08-23 06:51:16 +00:00
}
2019-01-01 11:53:20 +00:00
private function isValidMediaPath ( string $path ) : bool
{
return is_dir ( $path ) && is_readable ( $path );
}
private function setMediaPathFromEnvFile () : void
{
with ( config ( 'koel.media_path' ), function ( ? string $path ) : void {
if ( ! $path ) {
return ;
}
if ( $this -> isValidMediaPath ( $path )) {
Setting :: set ( 'media_path' , $path );
} else {
$this -> warn ( sprintf ( 'The path %s does not exist or not readable. Skipping.' , $path ));
}
});
}
2015-12-14 22:37:46 +00:00
}