InitCommand::maybeSetMediaPath()   A
last analyzed

Complexity

Conditions 6
Paths 6

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
eloc 14
c 0
b 0
f 0
dl 0
loc 28
ccs 0
cts 15
cp 0
rs 9.2222
cc 6
nc 6
nop 0
crap 42
1
<?php
2
3
namespace App\Console\Commands;
4
5
use App\Console\Commands\Traits\AskForPassword;
6
use App\Exceptions\InstallationFailedException;
7
use App\Models\Setting;
8
use App\Models\User;
9
use App\Repositories\SettingRepository;
10
use App\Services\MediaCacheService;
11
use Exception;
12
use Illuminate\Console\Command;
13
use Illuminate\Contracts\Console\Kernel as Artisan;
14
use Illuminate\Contracts\Hashing\Hasher	as Hash;
15
use Illuminate\Database\DatabaseManager as DB;
16
use Jackiedo\DotenvEditor\DotenvEditor;
17
18
class InitCommand extends Command
19
{
20
    use AskForPassword;
21
22
    protected $signature = 'koel:init';
23
    protected $description = 'Install or upgrade Koel';
24
25
    private $mediaCacheService;
26
    private $artisan;
27
    private $dotenvEditor;
28
    private $hash;
29
    private $db;
30
    private $settingRepository;
31
32 132
    public function __construct(
33
        MediaCacheService $mediaCacheService,
34
        SettingRepository $settingRepository,
35
        Artisan $artisan,
36
        Hash $hash,
37
        DotenvEditor $dotenvEditor,
38
        DB $db
39
    ) {
40 132
        parent::__construct();
41
42 132
        $this->mediaCacheService = $mediaCacheService;
43 132
        $this->artisan = $artisan;
44 132
        $this->dotenvEditor = $dotenvEditor;
45 132
        $this->hash = $hash;
46 132
        $this->db = $db;
47 132
        $this->settingRepository = $settingRepository;
48 132
    }
49
50
    public function handle(): void
51
    {
52
        $this->comment('Attempting to install or upgrade Koel.');
53
        $this->comment('Remember, you can always install/upgrade manually following the guide here:');
54
        $this->info('📙  '.config('koel.misc.docs_url').PHP_EOL);
55
56
        if ($this->inNoInteractionMode()) {
57
            $this->info('Running in no-interaction mode');
58
        }
59
60
        try {
61
            $this->maybeGenerateAppKey();
62
            $this->maybeGenerateJwtSecret();
63
            $this->maybeSetUpDatabase();
64
            $this->migrateDatabase();
65
            $this->maybeSeedDatabase();
66
            $this->maybeSetMediaPath();
67
            $this->compileFrontEndAssets();
68
        } catch (Exception $e) {
69
            $this->error("Oops! Koel installation or upgrade didn't finish successfully.");
70
            $this->error('Please try again, or visit '.config('koel.misc.docs_url').' for manual installation.');
71
            $this->error('😥 Sorry for this. You deserve better.');
72
73
            return;
74
        }
75
76
        $this->comment(PHP_EOL.'🎆  Success! Koel can now be run from localhost with `php artisan serve`.');
77
78
        if (Setting::get('media_path')) {
79
            $this->comment('You can also scan for media with `php artisan koel:sync`.');
80
        }
81
82
        $this->comment('Again, visit 📙 '.config('koel.misc.docs_url').' for the official documentation.');
83
        $this->comment(
84
            "Feeling generous and want to support Koel's development? Check out "
85
            .config('koel.misc.sponsor_github_url')
86
            .' 🤗'
87
        );
88
        $this->comment('Thanks for using Koel. You rock! 🤘');
89
    }
90
91
    /**
92
     * Prompt user for valid database credentials and set up the database.
93
     */
94
    private function setUpDatabase(): void
95
    {
96
        $config = [
97
            'DB_CONNECTION' => '',
98
            'DB_HOST' => '',
99
            'DB_PORT' => '',
100
            'DB_DATABASE' => '',
101
            'DB_USERNAME' => '',
102
            'DB_PASSWORD' => '',
103
        ];
104
105
        $config['DB_CONNECTION'] = $this->choice(
106
            'Your DB driver of choice',
107
            [
108
                'mysql' => 'MySQL/MariaDB',
109
                'pgsql' => 'PostgreSQL',
110
                'sqlsrv' => 'SQL Server',
111
                'sqlite-e2e' => 'SQLite',
112
            ],
113
            'mysql'
114
        );
115
116
        if ($config['DB_CONNECTION'] === 'sqlite-e2e') {
117
            $config['DB_DATABASE'] = $this->ask('Absolute path to the DB file');
118
        } else {
119
            $config['DB_HOST'] = $this->anticipate('DB host', ['127.0.0.1', 'localhost']);
120
            $config['DB_PORT'] = (string) $this->ask('DB port (leave empty for default)');
121
            $config['DB_DATABASE'] = $this->anticipate('DB name', ['koel']);
122
            $config['DB_USERNAME'] = $this->anticipate('DB user', ['koel']);
123
            $config['DB_PASSWORD'] = (string) $this->ask('DB password');
124
        }
125
126
        foreach ($config as $key => $value) {
127
            $this->dotenvEditor->setKey($key, $value);
128
        }
129
130
        $this->dotenvEditor->save();
131
132
        // Set the config so that the next DB attempt uses refreshed credentials
133
        config([
134
            'database.default' => $config['DB_CONNECTION'],
135
            "database.connections.{$config['DB_CONNECTION']}.host" => $config['DB_HOST'],
136
            "database.connections.{$config['DB_CONNECTION']}.port" => $config['DB_PORT'],
137
            "database.connections.{$config['DB_CONNECTION']}.database" => $config['DB_DATABASE'],
138
            "database.connections.{$config['DB_CONNECTION']}.username" => $config['DB_USERNAME'],
139
            "database.connections.{$config['DB_CONNECTION']}.password" => $config['DB_PASSWORD'],
140
        ]);
141
    }
142
143
    private function inNoInteractionMode(): bool
144
    {
145
        return (bool) $this->option('no-interaction');
146
    }
147
148
    private function setUpAdminAccount(): void
149
    {
150
        $this->info("Let's create the admin account.");
151
152
        [$name, $email, $password] = $this->gatherAdminAccountCredentials();
153
154
        User::create([
155
            'name' => $name,
156
            'email' => $email,
157
            'password' => $this->hash->make($password),
158
            'is_admin' => true,
159
        ]);
160
    }
161
162
    private function maybeSetMediaPath(): void
163
    {
164
        if (Setting::get('media_path')) {
165
            return;
166
        }
167
168
        if ($this->inNoInteractionMode()) {
169
            $this->setMediaPathFromEnvFile();
170
171
            return;
172
        }
173
174
        $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.');
175
176
        while (true) {
177
            $path = $this->ask('Media path', config('koel.media_path'));
178
179
            if (!$path) {
180
                return;
181
            }
182
183
            if ($this->isValidMediaPath($path)) {
184
                Setting::set('media_path', $path);
185
186
                return;
187
            }
188
189
            $this->error('The path does not exist or not readable. Try again.');
190
        }
191
    }
192
193
    private function maybeGenerateAppKey(): void
194
    {
195
        if (!config('app.key')) {
196
            $this->info('Generating app key');
197
            $this->artisan->call('key:generate');
198
        } else {
199
            $this->comment('App key exists -- skipping');
200
        }
201
    }
202
203
    private function maybeGenerateJwtSecret(): void
204
    {
205
        if (!config('jwt.secret')) {
206
            $this->info('Generating JWT secret');
207
            $this->artisan->call('koel:generate-jwt-secret');
208
        } else {
209
            $this->comment('JWT secret exists -- skipping');
210
        }
211
    }
212
213
    private function maybeSeedDatabase(): void
214
    {
215
        if (!User::count()) {
216
            $this->setUpAdminAccount();
217
            $this->info('Seeding initial data');
218
            $this->artisan->call('db:seed', ['--force' => true]);
219
        } else {
220
            $this->comment('Data seeded -- skipping');
221
        }
222
    }
223
224
    private function maybeSetUpDatabase(): void
225
    {
226
        while (true) {
227
            try {
228
                // Make sure the config cache is cleared before another attempt.
229
                $this->artisan->call('config:clear');
230
                $this->db->reconnect()->getPdo();
231
232
                break;
233
            } catch (Exception $e) {
234
                $this->error($e->getMessage());
235
                $this->warn(PHP_EOL."Koel cannot connect to the database. Let's set it up.");
236
                $this->setUpDatabase();
237
            }
238
        }
239
    }
240
241
    private function migrateDatabase(): void
242
    {
243
        $this->info('Migrating database');
244
        $this->artisan->call('migrate', ['--force' => true]);
245
246
        // Clear the media cache, just in case we did any media-related migration
247
        $this->mediaCacheService->clear();
248
    }
249
250
    private function compileFrontEndAssets(): void
251
    {
252
        $this->info('Now to front-end stuff');
253
254
        // We need to run several yarn commands:
255
        // - The first to install node_modules in the resources/assets submodule
256
        // - The second and third for the root folder, to build Koel's front-end assets with Mix.
257
258
        chdir('./resources/assets');
259
        $this->info('├── Installing Node modules in resources/assets directory');
260
261
        $runOkOrThrow = static function (string $command): void {
262
            passthru($command, $status);
263
            throw_if((bool) $status, InstallationFailedException::class);
264
        };
265
266
        $runOkOrThrow('yarn install --colors');
267
268
        chdir('../..');
269
        $this->info('└── Compiling assets');
270
271
        $runOkOrThrow('yarn install --colors');
272
        $runOkOrThrow('yarn production --colors');
273
    }
274
275
    /** @return array<string> */
276
    private function gatherAdminAccountCredentials(): array
277
    {
278
        if ($this->inNoInteractionMode()) {
279
            return [config('koel.admin.name'), config('koel.admin.email'), config('koel.admin.password')];
280
        }
281
282
        $name = $this->ask('Your name', config('koel.admin.name'));
283
        $email = $this->ask('Your email address', config('koel.admin.email'));
284
        $password = $this->askForPassword();
285
286
        return [$name, $email, $password];
287
    }
288
289
    private function isValidMediaPath(string $path): bool
290
    {
291
        return is_dir($path) && is_readable($path);
292
    }
293
294
    private function setMediaPathFromEnvFile(): void
295
    {
296
        with(config('koel.media_path'), function (?string $path): void {
297
            if (!$path) {
298
                return;
299
            }
300
301
            if ($this->isValidMediaPath($path)) {
302
                Setting::set('media_path', $path);
303
            } else {
304
                $this->warn(sprintf('The path %s does not exist or not readable. Skipping.', $path));
305
            }
306
        });
307
    }
308
}
309