diff --git a/.gitignore b/.gitignore index 55db7121..61cb7163 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /installed.lock +/upgrading.lock +/*.zip /node_modules /public/hot /public/storage diff --git a/app/Console/Commands/Install.php b/app/Console/Commands/Install.php index 0b74c5b6..32854bb0 100644 --- a/app/Console/Commands/Install.php +++ b/app/Console/Commands/Install.php @@ -5,7 +5,6 @@ namespace App\Console\Commands; use Illuminate\Console\Command; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Config; -use Illuminate\Support\Facades\DB; class Install extends Command { @@ -21,7 +20,7 @@ class Install extends Command * * @var string */ - protected $description = 'Install Lsky Pro'; + protected $description = 'Install Lsky Pro.'; /** * Create a new command instance. diff --git a/app/Console/Commands/Upgrade.php b/app/Console/Commands/Upgrade.php new file mode 100644 index 00000000..561080aa --- /dev/null +++ b/app/Console/Commands/Upgrade.php @@ -0,0 +1,45 @@ +upgrade(); + } +} diff --git a/app/Enums/ConfigKey.php b/app/Enums/ConfigKey.php index ce3b1214..63d9ffa6 100644 --- a/app/Enums/ConfigKey.php +++ b/app/Enums/ConfigKey.php @@ -19,6 +19,9 @@ final class ConfigKey /** @var string 程序url */ const AppUrl = 'app_url'; + /** @var string 程序版本 */ + const AppVersion = 'app_version'; + /** @var string 站点关键字 */ const SiteKeywords = 'site_keywords'; diff --git a/app/Http/Controllers/Admin/SettingController.php b/app/Http/Controllers/Admin/SettingController.php index 1330a677..25ff35a6 100644 --- a/app/Http/Controllers/Admin/SettingController.php +++ b/app/Http/Controllers/Admin/SettingController.php @@ -2,9 +2,11 @@ namespace App\Http\Controllers\Admin; +use App\Enums\ConfigKey; use App\Http\Controllers\Controller; use App\Mail\Test; use App\Models\Config; +use App\Services\UpgradeService; use App\Utils; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -38,4 +40,34 @@ class SettingController extends Controller } return $this->success('发送成功'); } + + public function checkUpdate(): Response + { + $version = Utils::config(ConfigKey::AppVersion); + $service = new UpgradeService($version); + $data = [ + 'is_update' => $service->check(), + ]; + if ($data['is_update']) { + $data['version'] = $service->getVersions()->first(); + } + return $this->success('success', $data); + } + + public function upgrade() + { + ignore_user_abort(true); + set_time_limit(0); + + $version = Utils::config(ConfigKey::AppVersion); + $service = new UpgradeService($version); + $this->success()->send(); + $service->upgrade(); + flush(); + } + + public function upgradeProgress(): Response + { + return $this->success('success', Cache::get('upgrade_progress')); + } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index f96ee7c5..1b5a0826 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -53,6 +53,7 @@ class Controller extends BaseController ['name' => 'Tokenizer', 'intro' => '令牌处理拓展'], ['name' => 'XML', 'intro' => 'Xml 解析器'], ['name' => 'Imagick', 'intro' => '高性能图片处理拓展'], + ['name' => 'Zip', 'intro' => '解压缩文件拓展,用于更新程序'], ])->transform(function ($item) { $item['result'] = extension_loaded(strtolower($item['name'])); return $item; diff --git a/app/Services/UpgradeService.php b/app/Services/UpgradeService.php new file mode 100644 index 00000000..66168cff --- /dev/null +++ b/app/Services/UpgradeService.php @@ -0,0 +1,139 @@ +version, $this->getVersions()->first()['name']) === -1; + } + + public function getVersions(): Collection + { + if (! $this->versions) { + // TODO 获取所有版本 + $this->versions = [ + [ + 'icon' => 'https://raw.githubusercontent.com/wisp-x/lsky-pro/master/public/static/app/images/icon.png', + 'name' => 'V 2.0.1', + 'size' => '33.5 MB', + 'changelog' => (new \Parsedown())->parse('### Added +- 一键复制全部链接 ([#167](https://github.com/wisp-x/lsky-pro/issues/167)) + +### Changed +- 将所有静态资源放置本地 +- 接口增加刷新 token 属性 +- 个人中心、后台显示用户注册时间 ([#263](https://github.com/wisp-x/lsky-pro/pull/263)) + +FAQ: +- 为了保证可用性,此次更新主要是为了静态文件放置本地,不再使用第三方静态资源托管服务。 +- 如没有特殊情况,这次更新为 1.x 版本最后一个小版本。最新版本动态请[戳我](https://github.com/wisp-x/lsky-pro/projects/1) + +'), + 'pushed_at' => '2022-02-26 12:21', + 'download_url' => 'https://github.com/wisp-x/lsky-pro/archive/v1.6.4.zip', + ], + ]; + } + return collect($this->versions); + } + + public function upgrade(): bool + { + $lock = base_path('upgrading.lock'); + + try { + if (file_exists($lock)) { + return false; + } + + $package = base_path('upgrade.zip'); + // 如果有安装包则直接进行安装,否则下载安装包 + if (! file_exists($package)) { + $this->setProgress('开始下载安装包...'); + if (! $this->check()) { + throw new \Exception('No need to upgrade.'); + } + + $version = $this->getVersions()->first(); + $response = Http::timeout(1800)->get($version['download_url'])->onError(function () { + $this->setProgress('安装包下载异常'); + }); + if ($response->successful()) { + file_put_contents($package, $response->body()); + $this->setProgress('安装包下载完成'); + } + } + + $this->setProgress('正在解压安装包...'); + $name = md5_file($package); + + $zip = new ZipArchive; + if (! $zip->open($package)) { + throw new \Exception('Installation package decompression failed.'); + } + $zip->extractTo(base_path($name)); + $zip->close(); + $this->setProgress('执行安装中...'); + + // TODO 读取已存在的软连接,移动到更新目录 + // TODO 移动本地文件到更新目录 + + Artisan::call('cache:clear'); + Artisan::call('package:discover'); + } catch (\Throwable $e) { + Log::error('升级失败', ['message' => $e->getMessage(), $e->getTraceAsString()]); + $this->setProgress('安装失败,请刷新页面重试', 'fail'); + @unlink($lock); + return false; + } + $this->setProgress('安装成功,请刷新页面', 'success'); + @unlink($lock); + + return true; + } + + /** + * 设置安装进度 + * + * @param string $message + * @param string $status in installing、success、fail + * @return void + */ + private function setProgress(string $message, string $status = 'installing') + { + Cache::put('upgrade_progress', compact('status', 'message'), 1800); + } + + /** + * 获取安装进度 + * + * @return void + */ + public function getProgress() + { + Cache::get('upgrade_progress'); + } +} diff --git a/composer.json b/composer.json index 3a6888fa..306d3c0b 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,7 @@ "license": "MIT", "require": { "php": "^8.0", + "ext-zip": "*", "alibabacloud/green": "^1.8", "doctrine/dbal": "^3.3", "erusev/parsedown": "^1.7", diff --git a/composer.lock b/composer.lock index 25c074d1..bde35a1a 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "2878b5ed6357c0ed4ddd1f6c7f330286", + "content-hash": "33062cc0e8a424e26ab6ddbfcd64153e", "packages": [ { "name": "adbario/php-dot-notation", @@ -11086,7 +11086,8 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.0" + "php": "^8.0", + "ext-zip": "*" }, "platform-dev": [], "plugin-api-version": "2.0.0" diff --git a/config/convention.php b/config/convention.php index 60d26d00..a874180b 100644 --- a/config/convention.php +++ b/config/convention.php @@ -13,6 +13,7 @@ return [ 'app' => [ ConfigKey::AppName => 'Lsky Pro', ConfigKey::AppUrl => env('APP_URL'), + ConfigKey::AppVersion => '2.0', ConfigKey::SiteKeywords => 'Lsky Pro,lsky,兰空图床', ConfigKey::SiteDescription => 'Lsky Pro, Your photo album on the cloud.', ConfigKey::SiteNotice => '', diff --git a/public/css/app.css b/public/css/app.css index d2fc1c37..bb74b4d7 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -694,6 +694,12 @@ select { .left-1 { left: 0.25rem; } +.top-3 { + top: 0.75rem; +} +.left-3 { + left: 0.75rem; +} .z-0 { z-index: 0; } @@ -952,6 +958,15 @@ select { .w-24 { width: 6rem; } +.w-16 { + width: 4rem; +} +.w-14 { + width: 3.5rem; +} +.w-\[95\%\] { + width: 95%; +} .min-w-full { min-width: 100%; } @@ -1084,6 +1099,9 @@ select { .flex-nowrap { flex-wrap: nowrap; } +.items-end { + align-items: flex-end; +} .items-center { align-items: center; } @@ -1857,6 +1875,15 @@ select { .duration-75 { transition-duration: 75ms; } +.duration-1000 { + transition-duration: 1000ms; +} +.duration-700 { + transition-duration: 700ms; +} +.duration-\[7000\] { + transition-duration: 7000; +} .ease-in-out { transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } @@ -2038,6 +2065,27 @@ select { --tw-brightness: brightness(.5); filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); } +@media (prefers-reduced-motion: no-preference) { + + @-webkit-keyframes spin { + + to { + transform: rotate(360deg); + } + } + + @keyframes spin { + + to { + transform: rotate(360deg); + } + } + + .motion-safe\:animate-spin { + -webkit-animation: spin 1s linear infinite; + animation: spin 1s linear infinite; + } +} .dark .dark\:bg-gray-900 { --tw-bg-opacity: 1; background-color: rgb(17 24 39 / var(--tw-bg-opacity)); diff --git a/resources/views/admin/setting/index.blade.php b/resources/views/admin/setting/index.blade.php index f77b881c..b99d70be 100644 --- a/resources/views/admin/setting/index.blade.php +++ b/resources/views/admin/setting/index.blade.php @@ -1,5 +1,9 @@ @section('title', '系统设置') +@push('styles') + +@endpush +

通用

@@ -131,8 +135,38 @@
+ +

系统升级

+
+ + + +
+ + @push('scripts') @endpush diff --git a/resources/views/install.blade.php b/resources/views/install.blade.php index 316c461c..3dee52a3 100644 --- a/resources/views/install.blade.php +++ b/resources/views/install.blade.php @@ -29,7 +29,7 @@ @foreach($extensions as $extension)
-
+
{{ $extension['name'] }}

{{ $extension['intro'] }}

diff --git a/routes/web.php b/routes/web.php index e0c56869..34c79619 100644 --- a/routes/web.php +++ b/routes/web.php @@ -94,6 +94,9 @@ Route::group(['prefix' => 'admin', 'middleware' => ['auth.admin']], function () Route::get('', [AdminSettingController::class, 'index'])->name('admin.settings'); Route::put('save', [AdminSettingController::class, 'save'])->name('admin.settings.save'); Route::post('mail-test', [AdminSettingController::class, 'mailTest'])->name('admin.settings.mail.test'); + Route::get('check-update', [AdminSettingController::class, 'checkUpdate'])->name('admin.settings.check.update'); + Route::post('upgrade', [AdminSettingController::class, 'upgrade'])->name('admin.settings.upgrade'); + Route::get('upgrade/progress', [AdminSettingController::class, 'upgradeProgress'])->name('admin.settings.upgrade.progress'); }); });