系统更新页面布局

This commit is contained in:
Wisp X 2022-02-26 17:46:25 +08:00
parent c108e9bc76
commit ebb352782f
14 changed files with 375 additions and 5 deletions

2
.gitignore vendored
View File

@ -1,4 +1,6 @@
/installed.lock
/upgrading.lock
/*.zip
/node_modules
/public/hot
/public/storage

View File

@ -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.

View File

@ -0,0 +1,45 @@
<?php
namespace App\Console\Commands;
use App\Enums\ConfigKey;
use App\Services\UpgradeService;
use App\Utils;
use Illuminate\Console\Command;
class Upgrade extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'lsky:upgrade';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Upgrade Lsky Pro.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
return (int) (new UpgradeService(Utils::config(ConfigKey::AppVersion)))->upgrade();
}
}

View File

@ -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';

View File

@ -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'));
}
}

View File

@ -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;

View File

@ -0,0 +1,139 @@
<?php
namespace App\Services;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use ZipArchive;
class UpgradeService
{
const ApiUrl = 'https://api.lsky.pro';
/** @var array|array[] 所有版本 */
protected array $versions = [];
public function __construct(protected string $version)
{
}
/**
* 是否需要更新
*
* @return bool
*/
public function check(): bool
{
return version_compare($this->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');
}
}

View File

@ -6,6 +6,7 @@
"license": "MIT",
"require": {
"php": "^8.0",
"ext-zip": "*",
"alibabacloud/green": "^1.8",
"doctrine/dbal": "^3.3",
"erusev/parsedown": "^1.7",

5
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": "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"

View File

@ -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 => '',

View File

@ -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));

View File

@ -1,5 +1,9 @@
@section('title', '系统设置')
@push('styles')
<link rel="stylesheet" href="{{ asset('css/markdown-css/github-markdown.css') }}">
@endpush
<x-app-layout>
<div class="my-6 md:my-9">
<p class="mb-3 font-semibold text-lg text-gray-700">通用</p>
@ -131,8 +135,38 @@
</form>
</div>
</div>
<p class="mb-3 font-semibold text-lg text-gray-700">系统升级</p>
<div class="relative p-4 rounded-md bg-white mb-8">
<p id="check-update" class="text-gray-600 text-center p-4" style="display: none">
<i class="fas fa-cog animate-spin"></i> 正在检查更新...
</p>
<p id="not-update" class="text-center p-6" style="display: none">
<span class="text-gray-700">{{ \App\Utils::config(\App\Enums\ConfigKey::AppVersion) }}</span>
<span class="text-gray-500">已是最新版本</span>
</p>
<div id="have-update" style="display: none"></div>
</div>
</div>
<script type="text/html" id="update-tpl">
<div class="flex items-center">
<img id="icon" src="__icon__" alt="icon" class="w-16" style="animation-duration: 5s">
<div class="flex flex-col text-gray-700 ml-4">
<p class="font-semibold">Lsky Pro __name__</p>
<p class="text-sm">__size__</p>
<p class="text-sm">发布于 __pushed_at__</p>
</div>
</div>
<p id="upgrade-message" class="mt-4 text-sm text-gray-500"></p>
<div class="mt-4 text-sm markdown-body">
__changelog__
</div>
<div class="mt-6 text-right">
<a href="javascript:void(0)" id="install" class="rounded-md px-4 py-2 bg-blue-500 text-white">立即安装</a>
</div>
</script>
@push('scripts')
<script>
// 设置选中驱动
@ -184,6 +218,67 @@
}
})
});
let timer;
let upgrade = function () {
return {
start: function () {
$('#icon').addClass('animate-spin')
$('#install').attr('disabled', true).removeClass('bg-blue-500').addClass('cursor-not-allowed bg-gray-400').text('执行升级中...')
$('#upgrade-message').text('准备升级...').removeClass('text-red-500').addClass('text-gray-500');
timer = setInterval(getProgress, 1500);
axios.post('{{ route('admin.settings.upgrade') }}');
},
stop: function () {
$('#icon').removeClass('animate-spin')
$('#install').attr('disabled', false).removeClass('cursor-not-allowed bg-gray-400').addClass('bg-blue-500').text('立即安装')
clearInterval(timer);
}
};
};
$('#check-update').show();
axios.get('{{ route('admin.settings.check.update') }}').then(response => {
if (response.data.status && response.data.data.is_update) {
$('#check-update').hide();
let version = response.data.data.version;
let html = $('#update-tpl').html()
.replace(/__icon__/g, version.icon)
.replace(/__name__/g, version.name)
.replace(/__size__/g, version.size)
.replace(/__pushed_at__/g, version.pushed_at)
.replace(/__changelog__/g, version.changelog);
$('#have-update').html(html).show();
$('.markdown-body a').attr('target', '_blank');
} else {
$('#not-update').show();
$('#check-update').hide();
}
});
let getProgress = function () {
axios.get('{{ route('admin.settings.upgrade.progress') }}').then(response => {
$('#upgrade-message').text(response.data.data.message);
if (response.data.data.status === 'success') {
$('#upgrade-message').removeClass('text-gray-500').addClass('text-green-500');
$('#install').hide();
}
if (response.data.data.status === 'fail') {
$('#upgrade-message').removeClass('text-gray-500').addClass('text-red-500');
}
if (response.data.data.status !== 'installing') {
upgrade().stop();
}
});
};
$(document).on('click', '#install', function () {
if ($(this).attr('disabled')) {
return;
}
upgrade().start();
});
</script>
@endpush

View File

@ -29,7 +29,7 @@
@foreach($extensions as $extension)
<dl>
<div class="rounded-md bg-gray-50 px-3 py-3 flex items-center justify-between">
<dt class="text-md font-medium text-gray-700 {{ ! $extension['result'] ? 'text-red-500' : '' }}">
<dt class="w-[95%] text-md font-medium text-gray-700 {{ ! $extension['result'] ? 'text-red-500' : '' }}">
{{ $extension['name'] }}
<p class="mt-2 text-sm text-gary-400">{{ $extension['intro'] }}</p>
</dt>

View File

@ -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');
});
});