diff --git a/app/Facades/Plugins.php b/app/Facades/Plugins.php index 673a5fad7..43f11ca3a 100644 --- a/app/Facades/Plugins.php +++ b/app/Facades/Plugins.php @@ -14,6 +14,7 @@ use Filament\Panel; * @method static void requireComposerPackages(Plugin $plugin) * @method static void runPluginMigrations(Plugin $plugin) * @method static void installPlugin(Plugin $plugin) + * @method static void updatePlugin(Plugin $plugin) * @method static void downloadPluginFromFile(UploadedFile $file) * @method static void downloadPluginFromUrl(string $url) * @method static void enablePlugin(string|Plugin $plugin) diff --git a/app/Filament/Admin/Resources/PluginResource.php b/app/Filament/Admin/Resources/PluginResource.php index bd70339a3..b76d5bf5e 100644 --- a/app/Filament/Admin/Resources/PluginResource.php +++ b/app/Filament/Admin/Resources/PluginResource.php @@ -84,7 +84,21 @@ class PluginResource extends Resource ->title('Plugin installed') ->send(); }), - // TODO: "update" button + Action::make('update') + ->authorize(fn (Plugin $plugin) => auth()->user()->can('update', $plugin)) + ->icon('tabler-download') + ->color('success') + ->visible(fn (Plugin $plugin) => $plugin->isUpdateAvailable()) + ->action(function (Plugin $plugin) { + Plugins::updatePlugin($plugin); + + redirect(ListPlugins::getUrl()); + + Notification::make() + ->success() + ->title('Plugin updated') + ->send(); + }), Action::make('enable') ->authorize(fn (Plugin $plugin) => auth()->user()->can('update', $plugin)) ->icon('tabler-check') diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 5ac50cc0f..dfbc830a0 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -243,36 +243,66 @@ class Plugin extends Model implements HasPluginSettings return !str($this->panel_version)->startsWith('^'); } - public function isUpdateAvailable(): bool + /** @return null|array */ + private function getUpdateData(): ?array { if ($this->update_url === null) { - return false; + return null; } + return cache()->remember("plugins.$this->id.update", now()->addMinutes(10), function () { + try { + return json_decode(file_get_contents($this->update_url), true, 512, JSON_THROW_ON_ERROR); + } catch (Exception $exception) { + report($exception); + } + + return null; + }); + } + + public function isUpdateAvailable(): bool + { $panelVersion = config('app.version', 'canary'); if ($panelVersion === 'canary') { return false; } - return cache()->remember("plugins.$this->id.update", now()->addHour(), function () use ($panelVersion) { - try { - /** @var array */ - $updateData = json_decode(file_get_contents($this->update_url), true, 512, JSON_THROW_ON_ERROR); - - if (array_key_exists('*', $updateData)) { - return version_compare($updateData['*']['version'], $this->version, '>'); - } - - if (array_key_exists($panelVersion, $updateData)) { - return version_compare($updateData[$panelVersion]['version'], $this->version, '>'); - } - } catch (Exception $exception) { - report($exception); + $updateData = $this->getUpdateData(); + if ($updateData) { + if (array_key_exists($panelVersion, $updateData)) { + return version_compare($updateData[$panelVersion]['version'], $this->version, '>'); } - return false; - }); + if (array_key_exists('*', $updateData)) { + return version_compare($updateData['*']['version'], $this->version, '>'); + } + } + + return false; + } + + public function getDownloadUrlForUpdate(): ?string + { + $panelVersion = config('app.version', 'canary'); + + if ($panelVersion === 'canary') { + return null; + } + + $updateData = $this->getUpdateData(); + if ($updateData) { + if (array_key_exists($panelVersion, $updateData)) { + return $updateData['panelVersion']['download_url']; + } + + if (array_key_exists('*', $updateData)) { + return $updateData['*']['download_url']; + } + } + + return null; } public function hasSettings(): bool diff --git a/app/Services/Helpers/PluginService.php b/app/Services/Helpers/PluginService.php index 1e9456a0e..027d173de 100644 --- a/app/Services/Helpers/PluginService.php +++ b/app/Services/Helpers/PluginService.php @@ -176,7 +176,24 @@ class PluginService } } - public function downloadPluginFromFile(UploadedFile $file): void + public function updatePlugin(Plugin $plugin): void + { + try { + $this->downloadPluginFromUrl($plugin->getDownloadUrlForUpdate(), true); + + $this->requireComposerPackages($plugin); + + $this->runPluginMigrations($plugin); + + cache()->forget("plugins.$plugin->id.update"); + } catch (Exception $exception) { + report($exception); + + $this->setStatus($plugin, PluginStatus::Errored, $exception->getMessage()); + } + } + + public function downloadPluginFromFile(UploadedFile $file, bool $cleanDownload = false): void { $zip = new ZipArchive(); @@ -185,6 +202,11 @@ class PluginService } $pluginName = str($file->getClientOriginalName())->before('.zip')->toString(); + + if ($cleanDownload) { + File::deleteDirectory(plugin_path($pluginName)); + } + $extractPath = $zip->locateName($pluginName) ? base_path('plugins') : plugin_path($pluginName); if (!$zip->extractTo($extractPath)) { @@ -194,7 +216,7 @@ class PluginService $zip->close(); } - public function downloadPluginFromUrl(string $url): void + public function downloadPluginFromUrl(string $url, bool $cleanDownload = false): void { $info = pathinfo($url); $tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed(); @@ -204,7 +226,7 @@ class PluginService throw new InvalidFileUploadException('Could not write temporary file.'); } - $this->downloadPluginFromFile(new UploadedFile($tmpPath, $info['basename'], 'application/zip')); + $this->downloadPluginFromFile(new UploadedFile($tmpPath, $info['basename'], 'application/zip'), $cleanDownload); } public function enablePlugin(string|Plugin $plugin): void