Simplify downloading packages

This commit is contained in:
Pig Fang 2019-04-05 17:23:27 +08:00
parent 45aaa819b8
commit d7b78324f8
14 changed files with 278 additions and 452 deletions

View File

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

View File

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

View 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'];
}
}
}

View File

@ -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">&times;</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)
},
},

View File

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

View File

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

View File

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

View File

@ -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: 这一条举报已经处理过了。

View File

@ -288,8 +288,6 @@ admin:
此插件没有声明任何依赖关系,这代表它有可能并不兼容此版本的 Blessing
Skin请将此插件升级至可能的最新版本。强行启用可能导致无法预料的后果。你确定要启用此插件吗
updateButton: 马上升级
updateSize: 更新包大小:
preparing: 正在准备
downloading: 正在下载更新包
updateCompleted: 更新完成
change-color:

2
storage/packages/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View 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';
}
}

View File

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

View 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);
}
}

View File

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