mirror of
https://github.com/lsky-org/lsky-pro.git
synced 2025-01-08 11:57:52 +08:00
✨ 系统更新页面布局
This commit is contained in:
parent
c108e9bc76
commit
ebb352782f
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,6 @@
|
||||
/installed.lock
|
||||
/upgrading.lock
|
||||
/*.zip
|
||||
/node_modules
|
||||
/public/hot
|
||||
/public/storage
|
||||
|
@ -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.
|
||||
|
45
app/Console/Commands/Upgrade.php
Normal file
45
app/Console/Commands/Upgrade.php
Normal 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();
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
||||
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
139
app/Services/UpgradeService.php
Normal file
139
app/Services/UpgradeService.php
Normal 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');
|
||||
}
|
||||
}
|
@ -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
5
composer.lock
generated
@ -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"
|
||||
|
@ -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 => '',
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user