fix: koel:init

This commit is contained in:
Phan An 2022-08-02 10:03:19 +02:00
parent 971a3c2629
commit 9d3011fe2c
No known key found for this signature in database
GPG key ID: A81E4477F0BB6FDC
5 changed files with 281 additions and 176 deletions

View file

@ -11,9 +11,10 @@ use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel as Artisan;
use Illuminate\Contracts\Hashing\Hasher as Hash;
use Illuminate\Database\DatabaseManager as DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Encryption\Encrypter;
use Illuminate\Support\Str;
use Jackiedo\DotenvEditor\DotenvEditor;
use Psr\Log\LoggerInterface;
use Throwable;
class InitCommand extends Command
@ -23,7 +24,7 @@ class InitCommand extends Command
private const DEFAULT_ADMIN_NAME = 'Koel';
private const DEFAULT_ADMIN_EMAIL = 'admin@koel.dev';
private const DEFAULT_ADMIN_PASSWORD = 'KoelIsCool';
private const NON_INTERACTION_MAX_ATTEMPT_COUNT = 10;
private const NON_INTERACTION_MAX_DATABASE_ATTEMPT_COUNT = 10;
protected $signature = 'koel:init {--no-assets}';
protected $description = 'Install or upgrade Koel';
@ -35,16 +36,20 @@ class InitCommand extends Command
private Artisan $artisan,
private Hash $hash,
private DotenvEditor $dotenvEditor,
private DB $db
private DB $db,
private LoggerInterface $logger
) {
parent::__construct();
}
public function handle(): int
{
$this->comment('Attempting to install or upgrade Koel.');
$this->comment('Remember, you can always install/upgrade manually following the guide here:');
$this->info('📙 ' . config('koel.misc.docs_url') . PHP_EOL);
$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
);
if ($this->inNoInteractionMode()) {
$this->components->info('Running in no-interaction mode');
@ -52,7 +57,7 @@ class InitCommand extends Command
try {
$this->clearCaches();
$this->maybeCopyEnvFile();
$this->loadEnvFile();
$this->maybeGenerateAppKey();
$this->maybeSetUpDatabase();
$this->migrateDatabase();
@ -60,71 +65,79 @@ class InitCommand extends Command
$this->maybeSetMediaPath();
$this->maybeCompileFrontEndAssets();
} catch (Throwable $e) {
Log::error($e);
$this->logger->error($e);
$this->components->error("Oops! Koel installation or upgrade didn't finish successfully.");
$this->error('Please try again, or visit '
. config('koel.misc.docs_url')
. ' for other options.');
$this->error('😥 Sorry for this. You deserve better.');
$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.');
return self::FAILURE;
}
$this->comment(PHP_EOL . '🎆 Success! Koel can now be run from localhost with `php artisan serve`.');
$this->newLine();
$this->output->success('All done!');
$this->info('Koel can now be run from localhost with `php artisan serve`.');
if ($this->adminSeeded) {
$this->comment(
$this->info(
sprintf('Log in with email %s and password %s', self::DEFAULT_ADMIN_EMAIL, self::DEFAULT_ADMIN_PASSWORD)
);
}
if (Setting::get('media_path')) {
$this->comment('You can also scan for media with `php artisan koel:sync`.');
$this->info('You can also scan for media now with `php artisan koel:sync`.');
}
$this->comment('Again, visit 📙 ' . config('koel.misc.docs_url') . ' for the official documentation.');
$this->info('Again, visit 📙 ' . config('koel.misc.docs_url') . ' for more tips and tweaks.');
$this->comment(
$this->info(
"Feeling generous and want to support Koel's development? Check out "
. config('koel.misc.sponsor_github_url')
. ' 🤗'
);
$this->comment('Thanks for using Koel. You rock! 🤘');
$this->info('Thanks for using Koel. You rock! 🤘');
return self::SUCCESS;
}
private function clearCaches(): void
{
$this->components->info('Clearing caches');
$this->artisan->call('config:clear');
$this->artisan->call('cache:clear');
$this->components->task('Clearing caches', function (): void {
$this->artisan->call('config:clear');
$this->artisan->call('cache:clear');
});
}
private function maybeCopyEnvFile(): void
private function loadEnvFile(): void
{
if (!file_exists(base_path('.env'))) {
$this->components->info('Copying .env file');
copy(base_path('.env.example'), base_path('.env'));
$this->components->task('Copying .env file', static function (): void {
copy(base_path('.env.example'), base_path('.env'));
});
} else {
$this->components->info('.env file exists -- skipping');
}
$this->dotenvEditor->load(base_path('.env'));
}
private function maybeGenerateAppKey(): void
{
if (!config('app.key')) {
$this->components->info('Generating app key');
$this->artisan->call('key:generate');
} else {
$this->components->info('App key exists -- skipping');
}
$key = $this->laravel['config']['app.key'];
$this->components->info('Using app key: ' . Str::limit(config('app.key'), 16));
$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;
}
});
$this->newLine();
$this->components->info('Using app key: ' . Str::limit($key, 16));
}
/**
@ -189,16 +202,83 @@ class InitCommand extends Command
private function setUpAdminAccount(): void
{
$this->info("Creating default admin account");
$this->components->task('Creating default admin account', function (): void {
User::create([
'name' => self::DEFAULT_ADMIN_NAME,
'email' => self::DEFAULT_ADMIN_EMAIL,
'password' => $this->hash->make(self::DEFAULT_ADMIN_PASSWORD),
'is_admin' => true,
]);
User::create([
'name' => self::DEFAULT_ADMIN_NAME,
'email' => self::DEFAULT_ADMIN_EMAIL,
'password' => $this->hash->make(self::DEFAULT_ADMIN_PASSWORD),
'is_admin' => true,
]);
$this->adminSeeded = true;
});
}
$this->adminSeeded = true;
private function maybeSeedDatabase(): void
{
if (!User::count()) {
$this->setUpAdminAccount();
$this->components->task('Seeding data', function (): void {
$this->artisan->call('db:seed', ['--force' => true]);
});
} else {
$this->newLine();
$this->components->info('Data already seeded -- skipping');
}
}
private function maybeSetUpDatabase(): void
{
$attempt = 0;
while (true) {
// 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() && $attempt >= self::NON_INTERACTION_MAX_DATABASE_ATTEMPT_COUNT) {
$this->components->error('Maximum database connection attempts reached. Giving up.');
break;
}
$attempt++;
try {
// Make sure the config cache is cleared before another attempt.
$this->artisan->call('config:clear');
$this->db->reconnect()->getPdo();
break;
} catch (Throwable $e) {
$this->logger->error($e);
// 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(
"Cannot connect to the database. Attempt: %d/%d",
$attempt,
self::NON_INTERACTION_MAX_DATABASE_ATTEMPT_COUNT
);
$this->components->warn($warning);
} else {
$this->components->warn("Cannot connect to the database. Let's set it up.");
$this->setUpDatabase();
}
}
}
}
private function migrateDatabase(): void
{
$this->components->task('Migrating database', function (): void {
$this->artisan->call('migrate', ['--force' => true]);
});
// Clear the media cache, just in case we did any media-related migration
$this->mediaCacheService->clear();
}
private function maybeSetMediaPath(): void
@ -213,6 +293,7 @@ class InitCommand extends Command
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) {
@ -228,76 +309,10 @@ class InitCommand extends Command
return;
}
$this->components->error('The path does not exist or not readable. Try again.');
$this->components->error('The path does not exist or not readable. Try again?');
}
}
private function maybeSeedDatabase(): void
{
if (!User::count()) {
$this->setUpAdminAccount();
$this->components->info('Seeding initial data');
$this->artisan->call('db:seed', ['--force' => true]);
} else {
$this->components->info('Data seeded -- skipping');
}
}
private function maybeSetUpDatabase(): void
{
$attemptCount = 0;
while (true) {
// 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->components->warn("Maximum database connection attempts reached. Giving up.");
break;
}
$attemptCount++;
try {
// Make sure the config cache is cleared before another attempt.
$this->artisan->call('config:clear');
$this->db->reconnect()->getPdo();
break;
} catch (Throwable $e) {
$this->error($e->getMessage());
// 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->components->warn($warning);
} else {
$this->components->warn(
sprintf("%sKoel cannot connect to the database. Let's set it up.", PHP_EOL)
);
$this->setUpDatabase();
}
}
}
}
private function migrateDatabase(): void
{
$this->components->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();
}
private function maybeCompileFrontEndAssets(): void
{
if ($this->inNoAssetsMode()) {
@ -318,21 +333,29 @@ class InitCommand extends Command
private function setMediaPathFromEnvFile(): void
{
with(config('koel.media_path'), function (?string $path): void {
if (!$path) {
return;
}
$path = config('koel.media_path');
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));
}
});
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));
}
}
private static function isValidMediaPath(string $path): bool
{
return is_dir($path) && is_readable($path);
}
/**
* Generate a random key for the application.
*/
private function generateRandomKey(): string
{
return 'base64:' . base64_encode(Encrypter::generateKey($this->laravel['config']['app.cipher']));
}
}

View file

@ -28,7 +28,6 @@ use Laravel\Scout\Searchable;
* @property string $id
* @property int $artist_id
* @property int $mtime
* @property int $contributing_artist_id
* @property ?bool $liked Whether the song is liked by the current user (dynamically calculated)
* @property ?int $play_count The number of times the song has been played by the current user (dynamically calculated)
* @property Carbon $created_at

View file

@ -16,7 +16,7 @@
"aws/aws-sdk-php-laravel": "^3.1",
"pusher/pusher-php-server": "^4.0",
"predis/predis": "~1.0",
"jackiedo/dotenv-editor": "^1.0",
"jackiedo/dotenv-editor": "^2.0",
"ext-exif": "*",
"ext-gd": "*",
"ext-fileinfo": "*",
@ -25,7 +25,7 @@
"daverandom/resume": "^0.0.3",
"laravel/helpers": "^1.0",
"intervention/image": "^2.5",
"doctrine/dbal": "^2.10",
"doctrine/dbal": "^3.0",
"lstrojny/functional-php": "^1.14",
"teamtnt/laravel-scout-tntsearch-driver": "^11.1",
"algolia/algoliasearch-client-php": "^3.3",

168
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "cdcb59dddec768e6cc821e1f6dffb72f",
"content-hash": "310243edd4bc8d4f2184ff38520a52dd",
"packages": [
{
"name": "algolia/algoliasearch-client-php",
@ -639,35 +639,38 @@
},
{
"name": "doctrine/dbal",
"version": "2.13.9",
"version": "3.3.7",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "c480849ca3ad6706a39c970cdfe6888fa8a058b8"
"reference": "9f79d4650430b582f4598fe0954ef4d52fbc0a8a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/c480849ca3ad6706a39c970cdfe6888fa8a058b8",
"reference": "c480849ca3ad6706a39c970cdfe6888fa8a058b8",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/9f79d4650430b582f4598fe0954ef4d52fbc0a8a",
"reference": "9f79d4650430b582f4598fe0954ef4d52fbc0a8a",
"shasum": ""
},
"require": {
"doctrine/cache": "^1.0|^2.0",
"composer-runtime-api": "^2",
"doctrine/cache": "^1.11|^2.0",
"doctrine/deprecations": "^0.5.3|^1",
"doctrine/event-manager": "^1.0",
"ext-pdo": "*",
"php": "^7.1 || ^8"
"php": "^7.3 || ^8.0",
"psr/cache": "^1|^2|^3",
"psr/log": "^1|^2|^3"
},
"require-dev": {
"doctrine/coding-standard": "9.0.0",
"jetbrains/phpstorm-stubs": "2021.1",
"phpstan/phpstan": "1.4.6",
"phpunit/phpunit": "^7.5.20|^8.5|9.5.16",
"jetbrains/phpstorm-stubs": "2022.1",
"phpstan/phpstan": "1.7.13",
"phpstan/phpstan-strict-rules": "^1.2",
"phpunit/phpunit": "9.5.20",
"psalm/plugin-phpunit": "0.16.1",
"squizlabs/php_codesniffer": "3.6.2",
"symfony/cache": "^4.4",
"symfony/console": "^2.0.5|^3.0|^4.0|^5.0",
"vimeo/psalm": "4.22.0"
"squizlabs/php_codesniffer": "3.7.0",
"symfony/cache": "^5.2|^6.0",
"symfony/console": "^2.7|^3.0|^4.0|^5.0|^6.0",
"vimeo/psalm": "4.23.0"
},
"suggest": {
"symfony/console": "For helpful console commands such as SQL execution and import of files."
@ -678,7 +681,7 @@
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\DBAL\\": "lib/Doctrine/DBAL"
"Doctrine\\DBAL\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@ -721,14 +724,13 @@
"queryobject",
"sasql",
"sql",
"sqlanywhere",
"sqlite",
"sqlserver",
"sqlsrv"
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/2.13.9"
"source": "https://github.com/doctrine/dbal/tree/3.3.7"
},
"funding": [
{
@ -744,7 +746,7 @@
"type": "tidelift"
}
],
"time": "2022-05-02T20:28:55+00:00"
"time": "2022-06-13T21:43:03+00:00"
},
{
"name": "doctrine/deprecations",
@ -1900,26 +1902,29 @@
},
{
"name": "jackiedo/dotenv-editor",
"version": "1.1.2",
"version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/JackieDo/Laravel-Dotenv-Editor.git",
"reference": "44062c804e6fa777c01d9fba3bd8fe9a57ec25f0"
"reference": "0971d876567b4bcf96078f8e55700b4726431704"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/JackieDo/Laravel-Dotenv-Editor/zipball/44062c804e6fa777c01d9fba3bd8fe9a57ec25f0",
"reference": "44062c804e6fa777c01d9fba3bd8fe9a57ec25f0",
"url": "https://api.github.com/repos/JackieDo/Laravel-Dotenv-Editor/zipball/0971d876567b4bcf96078f8e55700b4726431704",
"reference": "0971d876567b4bcf96078f8e55700b4726431704",
"shasum": ""
},
"require": {
"illuminate/config": ">=5.0",
"illuminate/container": ">=5.0",
"illuminate/support": ">=5.0",
"php": ">=5.4.0"
"illuminate/console": "^9.0|^8.0|7.0|^6.0|^5.8",
"illuminate/contracts": "^9.0|^8.0|7.0|^6.0|^5.8",
"illuminate/support": "^9.0|^8.0|7.0|^6.0|^5.8",
"jackiedo/path-helper": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
},
"laravel": {
"providers": [
"Jackiedo\\DotenvEditor\\DotenvEditorServiceProvider"
@ -1931,7 +1936,7 @@
},
"autoload": {
"psr-4": {
"Jackiedo\\DotenvEditor\\": "src/Jackiedo/DotenvEditor"
"Jackiedo\\DotenvEditor\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@ -1944,7 +1949,7 @@
"email": "anhvudo@gmail.com"
}
],
"description": "The .env file editor tool for Laravel 5+",
"description": "The .env file editor tool for Laravel 5.8+",
"keywords": [
"dotenv",
"dotenv-editor",
@ -1952,9 +1957,61 @@
],
"support": {
"issues": "https://github.com/JackieDo/Laravel-Dotenv-Editor/issues",
"source": "https://github.com/JackieDo/Laravel-Dotenv-Editor/tree/master"
"source": "https://github.com/JackieDo/Laravel-Dotenv-Editor/tree/2.0.1"
},
"time": "2020-06-07T00:55:45+00:00"
"time": "2022-03-10T17:04:52+00:00"
},
{
"name": "jackiedo/path-helper",
"version": "v1.0.0",
"source": {
"type": "git",
"url": "https://github.com/JackieDo/Path-Helper.git",
"reference": "43179cfa17d01f94f4889f286430c2cec1071fb2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/JackieDo/Path-Helper/zipball/43179cfa17d01f94f4889f286430c2cec1071fb2",
"reference": "43179cfa17d01f94f4889f286430c2cec1071fb2",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"type": "library",
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Jackiedo\\PathHelper\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jackie Do",
"email": "anhvudo@gmail.com"
}
],
"description": "Helper class for working with local paths in PHP",
"homepage": "https://github.com/JackieDo/path-helper",
"keywords": [
"helper",
"helpers",
"library",
"path",
"paths",
"php"
],
"support": {
"issues": "https://github.com/JackieDo/Path-Helper/issues",
"source": "https://github.com/JackieDo/Path-Helper/tree/v1.0.0"
},
"time": "2022-03-07T20:28:08+00:00"
},
{
"name": "james-heinrich/getid3",
@ -4369,6 +4426,55 @@
],
"time": "2022-01-05T17:46:08+00:00"
},
{
"name": "psr/cache",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/cache.git",
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Cache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for caching libraries",
"keywords": [
"cache",
"psr",
"psr-6"
],
"support": {
"source": "https://github.com/php-fig/cache/tree/3.0.0"
},
"time": "2021-02-03T23:26:27+00:00"
},
{
"name": "psr/container",
"version": "2.0.2",

View file

@ -2,7 +2,6 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class RenameContributingArtistId extends Migration
@ -10,29 +9,7 @@ class RenameContributingArtistId extends Migration
public function up(): void
{
Schema::table('songs', static function (Blueprint $table): void {
if (DB::getDriverName() !== 'sqlite') { // @phpstan-ignore-line
$table->dropForeign(['contributing_artist_id']);
}
});
Schema::table('songs', static function (Blueprint $table): void {
$table->renameColumn('contributing_artist_id', 'artist_id');
$table->foreign('artist_id')->references('id')->on('artists')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
*/
public function down(): void
{
Schema::table('songs', static function (Blueprint $table): void {
if (DB::getDriverName() !== 'sqlite') { // @phpstan-ignore-line
$table->dropForeign(['contributing_artist_id']);
}
$table->renameColumn('artist_id', 'contributing_artist_id');
$table->integer('artist_id')->unsigned()->nullable();
$table->foreign('artist_id')->references('id')->on('artists')->onDelete('cascade');
});
}