mirror of
https://github.com/bs-community/blessing-skin-server.git
synced 2025-01-08 12:07:42 +08:00
Simplify downloading packages
This commit is contained in:
parent
45aaa819b8
commit
d7b78324f8
@ -8,6 +8,7 @@ use Illuminate\Support\Arr;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\PluginManager;
|
||||
use Composer\Semver\Comparator;
|
||||
use App\Services\PackageManager;
|
||||
|
||||
class MarketController extends Controller
|
||||
{
|
||||
@ -73,7 +74,7 @@ class MarketController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function download(Request $request, PluginManager $manager)
|
||||
public function download(Request $request, PluginManager $manager, PackageManager $package)
|
||||
{
|
||||
$name = $request->get('name');
|
||||
$metadata = $this->getPluginMetadata($name);
|
||||
@ -82,43 +83,16 @@ class MarketController extends Controller
|
||||
return json(trans('admin.plugins.market.non-existent', ['plugin' => $name]), 1);
|
||||
}
|
||||
|
||||
// Gather plugin distribution URL
|
||||
$url = $metadata['dist']['url'];
|
||||
$filename = Arr::last(explode('/', $url));
|
||||
$plugins_dir = $manager->getPluginsDir();
|
||||
$tmp_path = $plugins_dir.DIRECTORY_SEPARATOR.$filename;
|
||||
$pluginsDir = $manager->getPluginsDir();
|
||||
$path = storage_path("packages/$filename");
|
||||
|
||||
// Download
|
||||
try {
|
||||
$this->guzzle->request('GET', $url, ['sink' => $tmp_path]);
|
||||
$package->download($url, $path, $metadata['dist']['shasum'])->extract($pluginsDir);
|
||||
} catch (Exception $e) {
|
||||
report($e);
|
||||
|
||||
return json(trans('admin.plugins.market.download-failed', ['error' => $e->getMessage()]), 2);
|
||||
return json($e->getMessage(), 1);
|
||||
}
|
||||
|
||||
// Check file's sha1 hash
|
||||
if (sha1_file($tmp_path) !== $metadata['dist']['shasum']) {
|
||||
@unlink($tmp_path);
|
||||
|
||||
return json(trans('admin.plugins.market.shasum-failed'), 3);
|
||||
}
|
||||
|
||||
// Unzip
|
||||
$zip = new ZipArchive();
|
||||
$res = $zip->open($tmp_path);
|
||||
|
||||
if ($res === true) {
|
||||
if ($zip->extractTo($plugins_dir) === false) {
|
||||
return json(trans('admin.plugins.market.unzip-failed', ['error' => 'Unable to extract the file.']), 4);
|
||||
}
|
||||
$manager->copyPluginAssets(plugin($name));
|
||||
} else {
|
||||
return json(trans('admin.plugins.market.unzip-failed', ['error' => $res]), 4);
|
||||
}
|
||||
$zip->close();
|
||||
@unlink($tmp_path);
|
||||
|
||||
return json(trans('admin.plugins.market.install-success'), 0);
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ use ZipArchive;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Http\Request;
|
||||
use Composer\Semver\Comparator;
|
||||
use App\Services\PackageManager;
|
||||
|
||||
class UpdateController extends Controller
|
||||
{
|
||||
@ -49,23 +50,12 @@ class UpdateController extends Controller
|
||||
*/
|
||||
protected $guzzle;
|
||||
|
||||
/**
|
||||
* Default request options for Guzzle HTTP client.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guzzleConfig;
|
||||
|
||||
public function __construct(\GuzzleHttp\Client $guzzle)
|
||||
{
|
||||
$this->updateSource = config('app.update_source');
|
||||
$this->currentVersion = config('app.version');
|
||||
|
||||
$this->guzzle = $guzzle;
|
||||
$this->guzzleConfig = [
|
||||
'headers' => ['User-Agent' => config('secure.user_agent')],
|
||||
'verify' => config('secure.certificates'),
|
||||
];
|
||||
}
|
||||
|
||||
public function showUpdatePage()
|
||||
@ -110,7 +100,7 @@ class UpdateController extends Controller
|
||||
$connectivity = true;
|
||||
|
||||
try {
|
||||
$this->guzzle->request('GET', $this->updateSource, $this->guzzleConfig);
|
||||
$this->guzzle->request('GET', $this->updateSource);
|
||||
} catch (Exception $e) {
|
||||
$connectivity = $e->getMessage();
|
||||
}
|
||||
@ -134,129 +124,25 @@ class UpdateController extends Controller
|
||||
return Comparator::greaterThan($latest, $this->currentVersion) && $this->getReleaseInfo($latest);
|
||||
}
|
||||
|
||||
public function download(Request $request)
|
||||
public function download(Request $request, PackageManager $package)
|
||||
{
|
||||
if (! $this->newVersionAvailable()) {
|
||||
return json([]);
|
||||
}
|
||||
|
||||
$action = $request->get('action');
|
||||
$release_url = $this->getReleaseInfo($this->latestVersion)['release_url'];
|
||||
$tmp_path = Cache::get('tmp_path');
|
||||
|
||||
switch ($action) {
|
||||
case 'prepare-download':
|
||||
|
||||
Cache::forget('download-progress');
|
||||
$update_cache = storage_path('update_cache');
|
||||
|
||||
if (! is_dir($update_cache)) {
|
||||
if (false === Storage::disk('root')->makeDirectory('storage/update_cache')) {
|
||||
return json(trans('admin.update.errors.write-permission'), 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Set temporary path for the update package
|
||||
$tmp_path = $update_cache.'/update_'.time().'.zip';
|
||||
Cache::put('tmp_path', $tmp_path, 3600);
|
||||
Log::info('[Update Wizard] Prepare to download update package', compact('release_url', 'tmp_path'));
|
||||
|
||||
// We won't get remote file size here since HTTP HEAD method is not always reliable
|
||||
return json(compact('release_url', 'tmp_path'));
|
||||
|
||||
case 'start-download':
|
||||
|
||||
if (! $tmp_path) {
|
||||
return json('No temp path available, please try again.', 1);
|
||||
}
|
||||
|
||||
@set_time_limit(0);
|
||||
$GLOBALS['last_downloaded'] = 0;
|
||||
|
||||
Log::info('[Update Wizard] Start downloading update package');
|
||||
|
||||
$url = $this->getReleaseInfo($this->latestVersion)['release_url'];
|
||||
$path = storage_path('packages/bs_'.$this->latestVersion.'.zip');
|
||||
switch ($request->get('action')) {
|
||||
case 'download':
|
||||
try {
|
||||
$this->guzzle->request('GET', $release_url, array_merge($this->guzzleConfig, [
|
||||
'sink' => $tmp_path,
|
||||
'progress' => function ($total, $downloaded) {
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($total == 0) {
|
||||
return;
|
||||
}
|
||||
// Log current progress per 100 KiB
|
||||
if ($total == $downloaded || floor($downloaded / 102400) > floor($GLOBALS['last_downloaded'] / 102400)) {
|
||||
$GLOBALS['last_downloaded'] = $downloaded;
|
||||
Log::info('[Update Wizard] Download progress (in bytes):', [$total, $downloaded]);
|
||||
Cache::put('download-progress', compact('total', 'downloaded'), 3600);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
},
|
||||
]));
|
||||
} catch (Exception $e) {
|
||||
@unlink($tmp_path);
|
||||
|
||||
return json(trans('admin.update.errors.prefix').$e->getMessage(), 1);
|
||||
}
|
||||
|
||||
Log::info('[Update Wizard] Finished downloading update package');
|
||||
|
||||
return json(compact('tmp_path'));
|
||||
|
||||
case 'get-progress':
|
||||
|
||||
return json((array) Cache::get('download-progress'));
|
||||
|
||||
case 'extract':
|
||||
|
||||
if (! file_exists($tmp_path)) {
|
||||
return json('No file available', 1);
|
||||
}
|
||||
|
||||
$extract_dir = storage_path("update_cache/{$this->latestVersion}");
|
||||
|
||||
$zip = new ZipArchive();
|
||||
$res = $zip->open($tmp_path);
|
||||
|
||||
if ($res === true) {
|
||||
Log::info("[Update Wizard] Extracting file $tmp_path");
|
||||
|
||||
if ($zip->extractTo($extract_dir) === false) {
|
||||
return json(trans('admin.update.errors.prefix').'Cannot unzip file.', 1);
|
||||
}
|
||||
} else {
|
||||
return json(trans('admin.update.errors.unzip').$res, 1);
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
try {
|
||||
File::copyDirectory("$extract_dir/vendor", base_path('vendor'));
|
||||
$package->download($url, $path)->extract(base_path());
|
||||
return json(trans('admin.update.complete'), 0);
|
||||
} catch (Exception $e) {
|
||||
report($e);
|
||||
Log::error('[Update Wizard] Unable to extract vendors');
|
||||
// Skip copying vendor
|
||||
File::deleteDirectory("$extract_dir/vendor");
|
||||
return json($e->getMessage(), 1);
|
||||
}
|
||||
|
||||
try {
|
||||
File::copyDirectory($extract_dir, base_path());
|
||||
|
||||
Log::info('[Update Wizard] Overwrite with extracted files');
|
||||
} catch (Exception $e) {
|
||||
report($e);
|
||||
Log::error('[Update Wizard] Error occured when overwriting files');
|
||||
|
||||
// Response can be returned, while cache will be cleared
|
||||
// @see https://gist.github.com/g-plane/2f88ad582826a78e0a26c33f4319c1e0
|
||||
return json(trans('admin.update.errors.overwrite').$e->getMessage(), 1);
|
||||
} finally {
|
||||
File::deleteDirectory(storage_path('update_cache'));
|
||||
Log::info('[Update Wizard] Cleaning cache');
|
||||
}
|
||||
|
||||
Log::info('[Update Wizard] Done');
|
||||
|
||||
return json(trans('admin.update.complete'), 0);
|
||||
|
||||
case 'progress':
|
||||
return $package->progress();
|
||||
default:
|
||||
return json(trans('general.illegal-parameters'), 1);
|
||||
}
|
||||
@ -271,7 +157,7 @@ class UpdateController extends Controller
|
||||
: $this->updateSource;
|
||||
|
||||
try {
|
||||
$response = $this->guzzle->request('GET', $url, $this->guzzleConfig)->getBody();
|
||||
$response = $this->guzzle->request('GET', $url)->getBody();
|
||||
} catch (Exception $e) {
|
||||
Log::error('[CheckingUpdate] Failed to get update information: '.$e->getMessage());
|
||||
}
|
||||
|
68
app/Services/PackageManager.php
Normal file
68
app/Services/PackageManager.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Cache;
|
||||
use Exception;
|
||||
|
||||
class PackageManager
|
||||
{
|
||||
protected $guzzle;
|
||||
protected $path;
|
||||
protected $cacheKey;
|
||||
protected $onProgress;
|
||||
|
||||
public function __construct(\GuzzleHttp\Client $guzzle)
|
||||
{
|
||||
$this->guzzle = $guzzle;
|
||||
$this->onProgress = function ($total, $done) {
|
||||
Cache::put($this->cacheKey, serialize(['total' => $total, 'done' => $done]));
|
||||
};
|
||||
}
|
||||
|
||||
public function download($url, $path, $shasum = null)
|
||||
{
|
||||
$this->path = $path;
|
||||
$this->cacheKey = "download_$url";
|
||||
Cache::forget($this->cacheKey);
|
||||
try {
|
||||
$this->guzzle->request('GET', $url, [
|
||||
'sink' => $path,
|
||||
'progress' => $this->onProgress
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception(trans('admin.download.errors.download', ['error' => $e->getMessage()]));
|
||||
}
|
||||
|
||||
Cache::forget($this->cacheKey);
|
||||
if (is_string($shasum) && sha1_file($path) != $shasum) {
|
||||
@unlink($path);
|
||||
throw new Exception(trans('admin.download.errors.shasum'));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extract($destination)
|
||||
{
|
||||
$zip = new \ZipArchive();
|
||||
$resource = $zip->open($this->path);
|
||||
|
||||
if ($resource === true && $zip->extractTo($destination)) {
|
||||
$zip->close();
|
||||
@unlink($this->path);
|
||||
} else {
|
||||
throw new Exception(trans('admin.download.errors.unzip'));
|
||||
}
|
||||
}
|
||||
|
||||
public function progress()
|
||||
{
|
||||
$progress = unserialize(Cache::get($this->cacheKey));
|
||||
if ($progress['total'] == 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return $progress['done'] / $progress['total'];
|
||||
}
|
||||
}
|
||||
}
|
@ -7,11 +7,11 @@
|
||||
@click="update"
|
||||
>{{ $t('admin.updateButton') }}</el-button>
|
||||
<el-button v-else disabled type="primary">
|
||||
<i class="fa fa-spinner fa-spin" /> {{ $t('admin.preparing') }}
|
||||
<i class="fa fa-spinner fa-spin" /> {{ $t('admin.downloading') }}
|
||||
</el-button>
|
||||
|
||||
<div
|
||||
id="modal-start-download"
|
||||
id="modal-download"
|
||||
class="modal fade"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
@ -19,15 +19,9 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<el-button
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
><span aria-hidden="true">×</span></el-button>
|
||||
<h4 v-t="'admin.downloading'" class="modal-title" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ $t('admin.updateSize') }}<span>{{ total }}</span> KB</p>
|
||||
<div class="progress">
|
||||
<div
|
||||
class="progress-bar progress-bar-striped active"
|
||||
@ -37,7 +31,7 @@
|
||||
aria-valuemax="100"
|
||||
:style="{ width: `${percentage}%` }"
|
||||
>
|
||||
<span>{{ ~~percentage }}</span>%
|
||||
<span>{{ percentage }}</span>%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -55,54 +49,35 @@ export default {
|
||||
data: () => ({
|
||||
canUpdate: blessing.extra.canUpdate,
|
||||
updating: false,
|
||||
total: 0,
|
||||
downloaded: 0,
|
||||
percentage: 0,
|
||||
}),
|
||||
computed: {
|
||||
percentage() {
|
||||
return this.downloaded / this.total * 100
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async update() {
|
||||
this.updating = true
|
||||
|
||||
await this.takeAction('prepare-download')
|
||||
|
||||
this.updating && $('#modal-start-download').modal({
|
||||
$('#modal-download').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: false,
|
||||
})
|
||||
|
||||
setTimeout(this.polling, POLLING_INTERVAL)
|
||||
|
||||
this.updating && await this.takeAction('start-download')
|
||||
|
||||
this.updating && await this.takeAction('extract')
|
||||
|
||||
setTimeout(() => this.polling(), POLLING_INTERVAL)
|
||||
const { errno, msg } = await this.$http.post(
|
||||
'/admin/update/download',
|
||||
{ action: 'download' }
|
||||
)
|
||||
this.updating = false
|
||||
if (this.downloaded) {
|
||||
await this.$alert(this.$t('admin.updateCompleted'), { type: 'success' })
|
||||
window.location = blessing.base_url
|
||||
}
|
||||
},
|
||||
async takeAction(action) {
|
||||
const { errno, msg } = await this.$http.post('/admin/update/download', {
|
||||
action,
|
||||
})
|
||||
if (errno) {
|
||||
this.$alert(msg, { type: 'error' })
|
||||
this.updating = false
|
||||
return
|
||||
}
|
||||
await this.$alert(this.$t('admin.updateCompleted'), { type: 'success' })
|
||||
window.location = blessing.base_url
|
||||
},
|
||||
async polling() {
|
||||
const { downloaded, total } = await this.$http.get(
|
||||
const percentage = await this.$http.get(
|
||||
'/admin/update/download',
|
||||
{ action: 'get-progress' }
|
||||
{ action: 'progress' }
|
||||
)
|
||||
this.downloaded = ~~(+downloaded / 1024)
|
||||
this.total = ~~(+total / 1024)
|
||||
|
||||
this.percentage = ~~percentage * 100
|
||||
this.updating && setTimeout(this.polling, POLLING_INTERVAL)
|
||||
},
|
||||
},
|
||||
|
@ -17,34 +17,35 @@ test('perform update', async () => {
|
||||
modal() {},
|
||||
}))
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ errno: 1 })
|
||||
.mockResolvedValueOnce({ errno: 1, msg: 'fail' })
|
||||
.mockResolvedValue({})
|
||||
Vue.prototype.$http.get
|
||||
.mockResolvedValue({ total: 2048, downloaded: 2048 })
|
||||
const wrapper = mount(Update)
|
||||
const button = wrapper.find('button')
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect($).not.toBeCalled()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/admin/update/download',
|
||||
{ action: 'prepare-download' }
|
||||
)
|
||||
expect(Vue.prototype.$alert).toBeCalledWith('fail', { type: 'error' })
|
||||
|
||||
button.trigger('click')
|
||||
jest.runOnlyPendingTimers()
|
||||
await flushPromises()
|
||||
expect($).toBeCalled()
|
||||
expect(Vue.prototype.$http.get).toBeCalledWith(
|
||||
'/admin/update/download',
|
||||
{ action: 'get-progress' }
|
||||
{ action: 'progress' }
|
||||
)
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/admin/update/download',
|
||||
{ action: 'start-download' }
|
||||
)
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/admin/update/download',
|
||||
{ action: 'extract' }
|
||||
{ action: 'download' }
|
||||
)
|
||||
})
|
||||
|
||||
test('polling for querying download progress', async () => {
|
||||
const wrapper = mount<Vue & { polling(): Promise<void> }>(Update)
|
||||
wrapper.setData({ updating: true })
|
||||
await wrapper.vm.polling()
|
||||
expect(Vue.prototype.$http.get).toBeCalledWith(
|
||||
'/admin/update/download',
|
||||
{ action: 'progress' }
|
||||
)
|
||||
})
|
||||
|
@ -91,11 +91,8 @@ plugins:
|
||||
not-found: No such plugin.
|
||||
|
||||
market:
|
||||
connection-error: Unable to connect to the plugins registry (to change the plugins registry URL, please refer to http://t.cn/Rk6X37l). :error
|
||||
connection-error: Unable to connect to the plugins registry. :error
|
||||
non-existent: The plugin :plugin does not exist.
|
||||
download-failed: Unable to download the plugin. :error
|
||||
shasum-failed: The downloaded file failed hash check, please retry.
|
||||
unzip-failed: Unable to extract the plugin. :error
|
||||
install-success: Plugin was installed.
|
||||
|
||||
empty: No result
|
||||
@ -140,11 +137,13 @@ update:
|
||||
size: "Size of package:"
|
||||
|
||||
errors:
|
||||
prefix: "An error occured: "
|
||||
connection: "Unable to access to current update source. Details:"
|
||||
write-permission: Unable to create the cache directory. Please check the permission.
|
||||
unzip: "Failed to extract update package. Error code: "
|
||||
overwrite: Unable to overwrite files.
|
||||
|
||||
download:
|
||||
errors:
|
||||
download: 'Failed to download. Error: :error'
|
||||
shasum: File validation failed. Please download again.
|
||||
unzip: Failed to unpack files.
|
||||
|
||||
report-reviewed: This report has been processed.
|
||||
|
||||
|
@ -294,9 +294,7 @@ admin:
|
||||
enabling it may cause unexpected problems. Do you really want to enable the
|
||||
plugin?
|
||||
updateButton: Update Now
|
||||
updateSize: "Size of package:"
|
||||
preparing: Preparing
|
||||
downloading: Downloading update package...
|
||||
downloading: Downloading...
|
||||
updateCompleted: Update completed.
|
||||
change-color:
|
||||
title: Change theme color
|
||||
|
@ -96,11 +96,8 @@ plugins:
|
||||
not-found: 插件不存在
|
||||
|
||||
market:
|
||||
connection-error: 无法连接至插件市场源(更换市场源请参考:http://t.cn/Rk6X37l),错误信息::error
|
||||
connection-error: 无法连接至插件市场源,错误信息::error
|
||||
non-existent: 插件 :plugin 不存在
|
||||
download-failed: 插件下载失败,错误信息::error
|
||||
shasum-failed: 文件校验失败,请尝试重新下载
|
||||
unzip-failed: 插件解压缩失败,错误信息::error
|
||||
install-success: 插件安装成功
|
||||
|
||||
empty: 无结果
|
||||
@ -145,11 +142,13 @@ update:
|
||||
size: 更新包大小:
|
||||
|
||||
errors:
|
||||
prefix: 发生错误:
|
||||
connection: 无法访问当前更新源。详细信息:
|
||||
write-permission: 你的服务器不支持自动更新:创建下载缓存文件夹失败,请检查目录权限。
|
||||
unzip: 更新包解压缩失败。错误代码:
|
||||
overwrite: 你的服务器不支持自动更新:无法覆盖文件。
|
||||
|
||||
download:
|
||||
errors:
|
||||
download: 下载失败。原因::error
|
||||
shasum: 文件校验失败,请重新下载。
|
||||
unzip: 解压失败。
|
||||
|
||||
report-reviewed: 这一条举报已经处理过了。
|
||||
|
||||
|
@ -288,8 +288,6 @@ admin:
|
||||
此插件没有声明任何依赖关系,这代表它有可能并不兼容此版本的 Blessing
|
||||
Skin,请将此插件升级至可能的最新版本。强行启用可能导致无法预料的后果。你确定要启用此插件吗?
|
||||
updateButton: 马上升级
|
||||
updateSize: 更新包大小:
|
||||
preparing: 正在准备
|
||||
downloading: 正在下载更新包
|
||||
updateCompleted: 更新完成
|
||||
change-color:
|
||||
|
2
storage/packages/.gitignore
vendored
Normal file
2
storage/packages/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
33
tests/Concerns/FakePackageManager.php
Normal file
33
tests/Concerns/FakePackageManager.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Concerns;
|
||||
|
||||
class FakePackageManager extends \App\Services\PackageManager
|
||||
{
|
||||
private $throw;
|
||||
|
||||
public function __construct(\GuzzleHttp\Client $guzzle = null, bool $throw = false)
|
||||
{
|
||||
$this->guzzle = $guzzle;
|
||||
$this->throw = $throw;
|
||||
}
|
||||
|
||||
public function download($url, $path, $shasum = null)
|
||||
{
|
||||
if ($this->throw) {
|
||||
throw new \Exception('');
|
||||
} else {
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
public function extract($destination)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function progress()
|
||||
{
|
||||
return '0';
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ namespace Tests;
|
||||
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use App\Services\PackageManager;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Tests\Concerns\MocksGuzzleClient;
|
||||
use Tests\Concerns\GeneratesFakePlugins;
|
||||
@ -33,79 +34,19 @@ class MarketControllerTest extends TestCase
|
||||
'msg' => trans('admin.plugins.market.non-existent', ['plugin' => 'non-existent-plugin']),
|
||||
]);
|
||||
|
||||
// Can't download due to connection error
|
||||
$this->appendToGuzzleQueue([
|
||||
new Response(200, [], $this->generateFakePluginsRegistry('fake-test-download', '0.0.1')),
|
||||
new RequestException('Connection Error', new Request('GET', 'whatever')),
|
||||
]);
|
||||
// Download
|
||||
$fakeRegistry = $this->generateFakePluginsRegistry('fake-test-download', '0.0.1');
|
||||
$this->appendToGuzzleQueue([new Response(200, [], $fakeRegistry)]);
|
||||
app()->instance(PackageManager::class, new Concerns\FakePackageManager(null, true));
|
||||
$this->postJson('/admin/plugins/market/download', [
|
||||
'name' => 'fake-test-download',
|
||||
])->assertJson([
|
||||
'errno' => 2,
|
||||
'msg' => trans('admin.plugins.market.download-failed', ['error' => 'Connection Error']),
|
||||
]);
|
||||
])->assertJson(['errno' => 1]);
|
||||
|
||||
// Downloaded plugin archive was tampered
|
||||
$fakeArchive = $this->generateFakePluginArchive(['name' => 'fake-test-download', 'version' => '0.0.1']);
|
||||
$this->appendToGuzzleQueue([
|
||||
new Response(200, [], $this->generateFakePluginsRegistry('fake-test-download', '0.0.1')),
|
||||
new Response(200, [], fopen($fakeArchive, 'r')),
|
||||
]);
|
||||
$this->appendToGuzzleQueue([new Response(200, [], $fakeRegistry)]);
|
||||
app()->bind(PackageManager::class, Concerns\FakePackageManager::class);
|
||||
$this->postJson('/admin/plugins/market/download', [
|
||||
'name' => 'fake-test-download',
|
||||
])->assertJson([
|
||||
'errno' => 3,
|
||||
'msg' => trans('admin.plugins.market.shasum-failed'),
|
||||
]);
|
||||
|
||||
// Download and extract plugin
|
||||
$shasum = sha1_file($fakeArchive);
|
||||
$this->appendToGuzzleQueue([
|
||||
new Response(200, [], $this->generateFakePluginsRegistry([
|
||||
[
|
||||
'name' => 'fake-test-download',
|
||||
'version' => '0.0.1',
|
||||
'dist' => [
|
||||
'url' => 'whatever',
|
||||
'shasum' => $shasum,
|
||||
],
|
||||
],
|
||||
])),
|
||||
new Response(200, [], fopen($fakeArchive, 'r')),
|
||||
]);
|
||||
$this->postJson('/admin/plugins/market/download', [
|
||||
'name' => 'fake-test-download',
|
||||
])->assertJson([
|
||||
'errno' => 0,
|
||||
'msg' => trans('admin.plugins.market.install-success'),
|
||||
]);
|
||||
$this->assertTrue(is_dir(config('plugins.directory').DIRECTORY_SEPARATOR.'fake-test-download'));
|
||||
$this->assertTrue(
|
||||
empty(glob(config('plugins.directory').DIRECTORY_SEPARATOR.'fake-test-download_*.zip'))
|
||||
);
|
||||
|
||||
// Broken archive
|
||||
file_put_contents($fakeArchive, 'broken');
|
||||
$shasum = sha1_file($fakeArchive);
|
||||
$this->appendToGuzzleQueue([
|
||||
new Response(200, [], $this->generateFakePluginsRegistry([
|
||||
[
|
||||
'name' => 'fake-test-download',
|
||||
'version' => '0.0.1',
|
||||
'dist' => [
|
||||
'url' => 'whatever',
|
||||
'shasum' => $shasum,
|
||||
],
|
||||
],
|
||||
])),
|
||||
new Response(200, [], fopen($fakeArchive, 'r')),
|
||||
]);
|
||||
$this->postJson('/admin/plugins/market/download', [
|
||||
'name' => 'fake-test-download',
|
||||
])->assertJson([
|
||||
'errno' => 4,
|
||||
'msg' => trans('admin.plugins.market.unzip-failed', ['error' => 19]),
|
||||
]);
|
||||
])->assertJson(['errno' => 0, 'msg' => trans('admin.plugins.market.install-success')]);
|
||||
}
|
||||
|
||||
public function testCheckUpdates()
|
||||
|
89
tests/ServicesTest/PackageManagerTest.php
Normal file
89
tests/ServicesTest/PackageManagerTest.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Cache;
|
||||
use Exception;
|
||||
use ZipArchive;
|
||||
use ReflectionClass;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use App\Services\PackageManager;
|
||||
use GuzzleHttp\Handler\MockHandler;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
|
||||
class PackageManagerTest extends TestCase
|
||||
{
|
||||
public function testDownload()
|
||||
{
|
||||
$mock = new MockHandler([
|
||||
new Response(200, [], 'contents'),
|
||||
new Response(200, [], 'contents'),
|
||||
new RequestException('error', new Request('GET', 'url')),
|
||||
]);
|
||||
$handler = HandlerStack::create($mock);
|
||||
$client = new Client(['handler' => $handler]);
|
||||
|
||||
$package = new PackageManager($client);
|
||||
$this->assertInstanceOf(
|
||||
PackageManager::class,
|
||||
$package->download('url', storage_path('packages/temp'))
|
||||
);
|
||||
|
||||
$this->expectException(Exception::class);
|
||||
$package->download('url', storage_path('packages/temp'), 'deadbeef');
|
||||
|
||||
$this->expectException(Exception::class);
|
||||
$package->download('url', storage_path('packages/temp'));
|
||||
}
|
||||
|
||||
public function testExtract()
|
||||
{
|
||||
$mock = new MockHandler([new Response(200, [], 'contents')]);
|
||||
$handler = HandlerStack::create($mock);
|
||||
$client = new Client(['handler' => $handler]);
|
||||
$package = new PackageManager($client);
|
||||
$path = storage_path('packages/temp.zip');
|
||||
|
||||
$package->download('url', $path);
|
||||
$zip = new ZipArchive();
|
||||
$this->assertTrue($zip->open($path, ZipArchive::OVERWRITE));
|
||||
$this->assertTrue($zip->addEmptyDir('zip-test'));
|
||||
$zip->close();
|
||||
$package->extract(storage_path('testing'));
|
||||
|
||||
$this->expectException(Exception::class);
|
||||
$package->download('url', $path)->extract(storage_path('testing'));
|
||||
}
|
||||
|
||||
public function testProgress()
|
||||
{
|
||||
$package = new PackageManager(new Client());
|
||||
$reflect = new ReflectionClass($package);
|
||||
$property = $reflect->getProperty('cacheKey');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($package, 'key');
|
||||
|
||||
Cache::put('key', serialize(['total' => 0, 'done' => 0]));
|
||||
$this->assertEquals(0, $package->progress());
|
||||
|
||||
Cache::put('key', serialize(['total' => 2, 'done' => 1]));
|
||||
$this->assertEquals(0.5, $package->progress());
|
||||
}
|
||||
|
||||
public function testOnProgress()
|
||||
{
|
||||
$package = new PackageManager(new Client());
|
||||
$reflect = new ReflectionClass($package);
|
||||
$property = $reflect->getProperty('cacheKey');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($package, 'key');
|
||||
$closure = $reflect->getProperty('onProgress');
|
||||
$closure->setAccessible(true);
|
||||
|
||||
Cache::shouldReceive('put')->with('key', serialize(['total' => 5, 'done' => 4]));
|
||||
call_user_func($closure->getValue($package), 5, 4);
|
||||
}
|
||||
}
|
@ -4,10 +4,10 @@ namespace Tests;
|
||||
|
||||
use Cache;
|
||||
use Exception;
|
||||
use ZipArchive;
|
||||
use Carbon\Carbon;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use App\Services\PackageManager;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Tests\Concerns\MocksGuzzleClient;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@ -22,7 +22,6 @@ class UpdateControllerTest extends TestCase
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->actAs('superAdmin');
|
||||
}
|
||||
|
||||
@ -83,151 +82,31 @@ class UpdateControllerTest extends TestCase
|
||||
$this->getJson('/admin/update/download')
|
||||
->assertDontSee(trans('general.illegal-parameters'));
|
||||
|
||||
// Lack write permission
|
||||
$this->appendToGuzzleQueue(200, [], $this->generateFakeUpdateInfo('8.9.3'));
|
||||
File::deleteDirectory(storage_path('update_cache'));
|
||||
Storage::shouldReceive('disk')
|
||||
->with('root')
|
||||
->once()
|
||||
->andReturnSelf();
|
||||
Storage::shouldReceive('makeDirectory')
|
||||
->with('storage/update_cache')
|
||||
->once()
|
||||
->andReturn(false);
|
||||
$this->withNewVersionAvailable()
|
||||
->getJson('/admin/update/download?action=prepare-download')
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('admin.update.errors.write-permission'),
|
||||
]);
|
||||
|
||||
// Prepare for downloading
|
||||
mkdir(storage_path('update_cache'));
|
||||
// Download
|
||||
$this->appendToGuzzleQueue([
|
||||
new Response(200, [], $this->generateFakeUpdateInfo('8.9.3')),
|
||||
new Response(200, [], $this->generateFakeUpdateInfo('8.9.3')),
|
||||
]);
|
||||
$this->withNewVersionAvailable()
|
||||
->getJson('/admin/update/download?action=prepare-download')
|
||||
->assertJsonStructure(['release_url', 'tmp_path']);
|
||||
$this->assertTrue(Cache::has('tmp_path'));
|
||||
$this->assertFalse(Cache::has('download-progress'));
|
||||
|
||||
// Start downloading
|
||||
Cache::flush();
|
||||
$this->withNewVersionAvailable()
|
||||
->getJson('/admin/update/download?action=start-download')
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => 'No temp path available, please try again.',
|
||||
]);
|
||||
|
||||
// Can't download update package
|
||||
$this->appendToGuzzleQueue([
|
||||
new Response(200, [], $this->generateFakeUpdateInfo('8.9.3')),
|
||||
new RequestException('Connection Error', new Request('GET', 'whatever')),
|
||||
]);
|
||||
Cache::put('tmp_path', storage_path('update_cache/update.zip'));
|
||||
$this->getJson('/admin/update/download?action=start-download');
|
||||
|
||||
// Download update package
|
||||
$fakeUpdatePackage = $this->generateFakeUpdateFile();
|
||||
$this->appendToGuzzleQueue([
|
||||
new Response(200, [], $this->generateFakeUpdateInfo('8.9.3')),
|
||||
new Response(200, [], fopen($fakeUpdatePackage, 'r')),
|
||||
]);
|
||||
Cache::put('tmp_path', storage_path('update_cache/update.zip'));
|
||||
$this->getJson('/admin/update/download?action=start-download')
|
||||
->assertJson([
|
||||
'tmp_path' => storage_path('update_cache/update.zip'),
|
||||
]);
|
||||
$this->assertFileExists(storage_path('update_cache/update.zip'));
|
||||
|
||||
// No download progress available
|
||||
Cache::flush();
|
||||
$this->withNewVersionAvailable()
|
||||
->getJson('/admin/update/download?action=get-progress')
|
||||
->assertJson([]);
|
||||
app()->instance(PackageManager::class, new Concerns\FakePackageManager(null, true));
|
||||
$this->getJson('/admin/update/download?action=download')
|
||||
->assertJson(['errno' => 1]);
|
||||
app()->bind(PackageManager::class, Concerns\FakePackageManager::class);
|
||||
$this->getJson('/admin/update/download?action=download')
|
||||
->assertJson(['errno' => 0, 'msg' => trans('admin.update.complete')]);
|
||||
|
||||
// Get download progress
|
||||
Cache::put('download-progress', ['total' => 514, 'downloaded' => 114]);
|
||||
$this->withNewVersionAvailable()
|
||||
->getJson('/admin/update/download?action=get-progress')
|
||||
->assertJson([
|
||||
'total' => 514,
|
||||
'downloaded' => 114,
|
||||
]);
|
||||
|
||||
// No such zip archive
|
||||
Cache::put('tmp_path', storage_path('update_cache/nope.zip'));
|
||||
$this->withNewVersionAvailable()
|
||||
->getJson('/admin/update/download?action=extract')
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => 'No file available',
|
||||
]);
|
||||
|
||||
// Can't extract zip archive
|
||||
file_put_contents(storage_path('update_cache/update.zip'), 'text');
|
||||
Cache::put('tmp_path', storage_path('update_cache/update.zip'));
|
||||
$this->withNewVersionAvailable()
|
||||
->getJson('/admin/update/download?action=extract')
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('admin.update.errors.unzip').'19',
|
||||
]);
|
||||
|
||||
// Extract
|
||||
copy(storage_path('testing/update.zip'), storage_path('update_cache/update.zip'));
|
||||
$this->withNewVersionAvailable()
|
||||
->getJson('/admin/update/download?action=extract')
|
||||
->assertJson([
|
||||
'errno' => 0,
|
||||
'msg' => trans('admin.update.complete'),
|
||||
]);
|
||||
|
||||
// Can't overwrite vendor directory, skip
|
||||
mkdir(storage_path('update_cache'));
|
||||
copy(storage_path('testing/update.zip'), storage_path('update_cache/update.zip'));
|
||||
File::shouldReceive('copyDirectory')
|
||||
->with(storage_path('update_cache/8.9.3/vendor'), base_path('vendor'))
|
||||
->andThrow(new Exception);
|
||||
File::shouldReceive('deleteDirectory')
|
||||
->with(storage_path('update_cache/8.9.3/vendor'));
|
||||
$this->withNewVersionAvailable()
|
||||
->getJson('/admin/update/download?action=extract');
|
||||
|
||||
// Can't apply update package
|
||||
File::shouldReceive('copyDirectory')
|
||||
->with(storage_path('update_cache/8.9.3'), base_path())
|
||||
->andThrow(new Exception);
|
||||
File::shouldReceive('deleteDirectory')
|
||||
->with(storage_path('update_cache'));
|
||||
File::shouldReceive('deleteDirectory')
|
||||
->with(storage_path('update_cache'));
|
||||
$this->withNewVersionAvailable()
|
||||
->getJson('/admin/update/download?action=extract')
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('admin.update.errors.overwrite'),
|
||||
]);
|
||||
$this->getJson('/admin/update/download?action=progress')
|
||||
->assertSee('0');
|
||||
|
||||
// Invalid action
|
||||
$this->withNewVersionAvailable()
|
||||
->getJson('/admin/update/download?action=no')
|
||||
$this->appendToGuzzleQueue(200, [], $this->generateFakeUpdateInfo('8.9.3'));
|
||||
$this->getJson('/admin/update/download?action=no')
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('general.illegal-parameters'),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function withNewVersionAvailable()
|
||||
{
|
||||
$this->appendToGuzzleQueue(200, [], $this->generateFakeUpdateInfo('8.9.3'));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function generateFakeUpdateInfo($version, $preview = false, $time = null)
|
||||
{
|
||||
$time = $time ?: time();
|
||||
@ -247,20 +126,4 @@ class UpdateControllerTest extends TestCase
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function generateFakeUpdateFile()
|
||||
{
|
||||
$zipPath = storage_path('testing/update.zip');
|
||||
|
||||
if (file_exists($zipPath)) {
|
||||
unlink($zipPath);
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
$zip->open($zipPath, ZipArchive::CREATE);
|
||||
$zip->addEmptyDir('coverage');
|
||||
$zip->close();
|
||||
|
||||
return $zipPath;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user