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 ;
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 ;
2022-08-02 08:03:19 +00:00
use Illuminate\Encryption\Encrypter ;
2024-01-04 21:51:32 +00:00
use Illuminate\Support\Facades\File ;
2022-08-02 06:22:08 +00:00
use Illuminate\Support\Str ;
2018-08-19 21:01:01 +00:00
use Jackiedo\DotenvEditor\DotenvEditor ;
2022-08-02 08:03:19 +00:00
use Psr\Log\LoggerInterface ;
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' ;
2022-08-02 08:03:19 +00:00
private const NON_INTERACTION_MAX_DATABASE_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 bool $adminSeeded = false ;
2018-08-19 21:01:01 +00:00
public function __construct (
2022-07-27 15:32:36 +00:00
private Artisan $artisan ,
private Hash $hash ,
private DotenvEditor $dotenvEditor ,
2022-08-02 08:03:19 +00:00
private DB $db ,
private LoggerInterface $logger
2018-08-19 21:03:21 +00:00
) {
2018-08-19 14:56:56 +00:00
parent :: __construct ();
}
2015-12-14 22:37:46 +00:00
2022-07-29 06:47:10 +00:00
public function handle () : int
2015-12-14 22:37:46 +00:00
{
2022-08-02 08:03:19 +00:00
$this -> alert ( 'KOEL INSTALLATION WIZARD' );
$this -> info (
'As a reminder, you can always install/upgrade manually following the guide at '
. 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 ()) {
2022-08-02 06:22:08 +00:00
$this -> components -> info ( 'Running in no-interaction mode' );
2019-01-01 11:53:20 +00:00
}
2019-09-10 18:30:38 +00:00
try {
2022-08-02 06:22:08 +00:00
$this -> clearCaches ();
2022-08-02 08:03:19 +00:00
$this -> loadEnvFile ();
2019-09-10 18:30:38 +00:00
$this -> maybeGenerateAppKey ();
$this -> maybeSetUpDatabase ();
$this -> migrateDatabase ();
$this -> maybeSeedDatabase ();
$this -> maybeSetMediaPath ();
2020-07-15 13:20:02 +00:00
$this -> maybeCompileFrontEndAssets ();
2022-08-03 10:01:32 +00:00
$this -> dotenvEditor -> save ();
2020-12-22 20:11:22 +00:00
} catch ( Throwable $e ) {
2022-08-02 08:03:19 +00:00
$this -> logger -> error ( $e );
2022-07-29 10:53:07 +00:00
2022-08-02 06:22:08 +00:00
$this -> components -> error ( " Oops! Koel installation or upgrade didn't finish successfully. " );
2022-08-02 08:03:19 +00:00
$this -> components -> error ( 'Please check the error log at storage/logs/laravel.log and try again.' );
$this -> components -> error ( 'You can also visit ' . config ( 'koel.misc.docs_url' ) . ' for other options.' );
$this -> components -> error ( '😥 Sorry for this. You deserve better.' );
2019-09-10 18:30:38 +00:00
2022-07-29 06:47:10 +00:00
return self :: FAILURE ;
2019-09-10 18:30:38 +00:00
}
2015-12-14 22:37:46 +00:00
2022-08-02 08:03:19 +00:00
$this -> newLine ();
$this -> output -> success ( 'All done!' );
$this -> info ( '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 ) {
2022-08-02 08:03:19 +00:00
$this -> info (
2021-01-23 18:58:08 +00:00
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' )) {
2022-08-02 08:03:19 +00:00
$this -> info ( 'You can also scan for media now with `php artisan koel:sync`.' );
2017-12-03 16:54:11 +00:00
}
2018-08-19 21:01:01 +00:00
2022-08-02 08:03:19 +00:00
$this -> info ( 'Again, visit 📙 ' . config ( 'koel.misc.docs_url' ) . ' for more tips and tweaks.' );
2022-07-29 06:47:10 +00:00
2022-08-02 08:03:19 +00:00
$this -> info (
2019-09-10 18:30:38 +00:00
" 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
);
2022-07-29 06:47:10 +00:00
2022-08-02 08:03:19 +00:00
$this -> info ( 'Thanks for using Koel. You rock! 🤘' );
2022-07-29 06:47:10 +00:00
return self :: SUCCESS ;
2015-12-14 22:37:46 +00:00
}
2017-12-03 16:54:11 +00:00
2022-08-02 06:22:08 +00:00
private function clearCaches () : void
{
2022-08-02 08:03:19 +00:00
$this -> components -> task ( 'Clearing caches' , function () : void {
$this -> artisan -> call ( 'config:clear' );
$this -> artisan -> call ( 'cache:clear' );
});
2022-08-02 06:22:08 +00:00
}
2022-08-02 08:03:19 +00:00
private function loadEnvFile () : void
2022-08-02 06:22:08 +00:00
{
if ( ! file_exists ( base_path ( '.env' ))) {
2022-08-02 08:03:19 +00:00
$this -> components -> task ( 'Copying .env file' , static function () : void {
copy ( base_path ( '.env.example' ), base_path ( '.env' ));
});
2022-08-02 06:22:08 +00:00
} else {
$this -> components -> info ( '.env file exists -- skipping' );
}
2022-08-02 08:03:19 +00:00
$this -> dotenvEditor -> load ( base_path ( '.env' ));
2022-08-02 06:22:08 +00:00
}
private function maybeGenerateAppKey () : void
{
2022-08-02 08:03:19 +00:00
$key = $this -> laravel [ 'config' ][ 'app.key' ];
$this -> components -> task ( $key ? 'Retrieving app key' : 'Generating app key' , function () use ( & $key ) : void {
if ( ! $key ) {
// Generate the key manually to prevent some clashes with `php artisan key:generate`
$key = $this -> generateRandomKey ();
$this -> dotenvEditor -> setKey ( 'APP_KEY' , $key );
$this -> laravel [ 'config' ][ 'app.key' ] = $key ;
}
});
2022-08-02 06:22:08 +00:00
2022-08-02 08:03:19 +00:00
$this -> newLine ();
$this -> components -> info ( 'Using app key: ' . Str :: limit ( $key , 16 ));
2022-08-02 06:22:08 +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_HOST' => '' ,
'DB_PORT' => '' ,
'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
{
2022-08-02 08:03:19 +00:00
$this -> components -> task ( 'Creating default admin account' , function () : void {
2022-08-09 18:45:11 +00:00
User :: query () -> create ([
2022-08-02 08:03:19 +00:00
'name' => self :: DEFAULT_ADMIN_NAME ,
'email' => self :: DEFAULT_ADMIN_EMAIL ,
'password' => $this -> hash -> make ( self :: DEFAULT_ADMIN_PASSWORD ),
'is_admin' => true ,
]);
$this -> adminSeeded = true ;
});
2018-08-23 06:51:16 +00:00
}
2018-08-24 15:27:19 +00:00
private function maybeSeedDatabase () : void
2018-08-23 06:51:16 +00:00
{
2022-08-09 18:45:11 +00:00
if ( ! User :: query () -> count ()) {
2018-08-23 06:51:16 +00:00
$this -> setUpAdminAccount ();
2022-08-02 08:03:19 +00:00
$this -> components -> task ( 'Seeding data' , function () : void {
$this -> artisan -> call ( 'db:seed' , [ '--force' => true ]);
});
2018-08-23 06:51:16 +00:00
} else {
2022-08-02 08:03:19 +00:00
$this -> newLine ();
$this -> components -> info ( 'Data already seeded -- skipping' );
2018-08-23 06:51:16 +00:00
}
}
2018-08-24 15:27:19 +00:00
private function maybeSetUpDatabase () : void
2018-08-23 06:51:16 +00:00
{
2022-08-02 08:03:19 +00:00
$attempt = 0 ;
2021-04-16 14:15:21 +00:00
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.
2022-08-02 08:03:19 +00:00
if ( $this -> inNoInteractionMode () && $attempt >= self :: NON_INTERACTION_MAX_DATABASE_ATTEMPT_COUNT ) {
$this -> components -> error ( 'Maximum database connection attempts reached. Giving up.' );
2021-04-16 14:15:21 +00:00
break ;
}
2022-08-02 08:03:19 +00:00
$attempt ++ ;
2021-04-16 14:15:21 +00:00
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 ) {
2022-08-02 08:03:19 +00:00
$this -> logger -> error ( $e );
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 (
2022-08-02 08:03:19 +00:00
" Cannot connect to the database. Attempt: %d/%d " ,
$attempt ,
self :: NON_INTERACTION_MAX_DATABASE_ATTEMPT_COUNT
2021-04-16 14:15:21 +00:00
);
2022-08-02 08:03:19 +00:00
2022-08-02 06:22:08 +00:00
$this -> components -> warn ( $warning );
2021-04-16 14:15:21 +00:00
} else {
2022-08-02 08:03:19 +00:00
$this -> components -> warn ( " Cannot connect to the database. Let's set it up. " );
2021-04-16 14:15:21 +00:00
$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
{
2022-08-02 08:03:19 +00:00
$this -> components -> task ( 'Migrating database' , function () : void {
$this -> artisan -> call ( 'migrate' , [ '--force' => true ]);
});
2018-08-23 06:51:16 +00:00
}
2022-08-02 08:03:19 +00:00
private function maybeSetMediaPath () : void
{
if ( Setting :: get ( 'media_path' )) {
return ;
}
if ( $this -> inNoInteractionMode ()) {
$this -> setMediaPathFromEnvFile ();
return ;
}
$this -> newLine ();
$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
while ( true ) {
$path = $this -> ask ( 'Media path' , config ( 'koel.media_path' ));
if ( ! $path ) {
return ;
}
if ( self :: isValidMediaPath ( $path )) {
Setting :: set ( 'media_path' , $path );
return ;
}
$this -> components -> error ( 'The path does not exist or not readable. Try again?' );
}
}
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 ;
}
2022-08-02 06:22:08 +00:00
$this -> components -> info ( 'Now to front-end stuff' );
2019-08-28 21:55:53 +00:00
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' );
2022-08-02 06:22:08 +00:00
$this -> components -> info ( 'Compiling assets' );
$runOkOrThrow ( 'yarn build' );
2018-08-23 06:51:16 +00:00
}
2019-01-01 11:53:20 +00:00
private function setMediaPathFromEnvFile () : void
{
2022-08-02 08:03:19 +00:00
$path = config ( 'koel.media_path' );
2019-01-01 11:53:20 +00:00
2022-08-02 08:03:19 +00:00
if ( ! $path ) {
return ;
}
if ( self :: isValidMediaPath ( $path )) {
Setting :: set ( 'media_path' , $path );
} else {
$this -> components -> warn ( sprintf ( 'The path %s does not exist or not readable. Skipping.' , $path ));
}
2019-01-01 11:53:20 +00:00
}
2022-07-29 10:49:55 +00:00
private static function isValidMediaPath ( string $path ) : bool
{
2024-01-04 21:51:32 +00:00
return File :: isDirectory ( $path ) && File :: isReadable ( $path );
2022-07-29 10:49:55 +00:00
}
2022-08-02 08:03:19 +00:00
/**
* Generate a random key for the application .
*/
private function generateRandomKey () : string
{
return 'base64:' . base64_encode ( Encrypter :: generateKey ( $this -> laravel [ 'config' ][ 'app.cipher' ]));
}
2015-12-14 22:37:46 +00:00
}