diff --git a/CHANGELOG.md b/CHANGELOG.md index f1f7ed57c..05468bc9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ -#Change Log +# Change Log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org) +## MASTER +### Added +- Added `--to=` option to `backup:get` to allow specifying of a local download location. (#1520) + ## 1.0.0-beta.2 - 2017-01-10 ### Fixed - Fixed fatal error by adding back the use statement for ProcessUtils in SSHBaseCommand. (#1494) diff --git a/src/Commands/Backup/GetCommand.php b/src/Commands/Backup/GetCommand.php index fc09e489c..56bbcd253 100644 --- a/src/Commands/Backup/GetCommand.php +++ b/src/Commands/Backup/GetCommand.php @@ -3,6 +3,8 @@ namespace Pantheon\Terminus\Commands\Backup; use Pantheon\Terminus\Commands\TerminusCommand; +use Pantheon\Terminus\Request\RequestAwareInterface; +use Pantheon\Terminus\Request\RequestAwareTrait; use Pantheon\Terminus\Site\SiteAwareInterface; use Pantheon\Terminus\Site\SiteAwareTrait; use Pantheon\Terminus\Exceptions\TerminusNotFoundException; @@ -11,8 +13,9 @@ * Class GetCommand * @package Pantheon\Terminus\Commands\Backup */ -class GetCommand extends TerminusCommand implements SiteAwareInterface +class GetCommand extends TerminusCommand implements SiteAwareInterface, RequestAwareInterface { + use RequestAwareTrait; use SiteAwareTrait; /** @@ -25,16 +28,21 @@ class GetCommand extends TerminusCommand implements SiteAwareInterface * @param string $site_env Site & environment in the format `site-name.env` * @option string $file [filename.tgz] Name of backup file * @option string $element [code|files|database|db] Backup element to retrieve + * @option string $to Local path to save to * @throws TerminusNotFoundException * * @usage terminus backup:get . * Displays the URL for the most recent backup of any type in 's environment. - * @usage terminus backup:get awesome-site.dev --file=2016-08-18T23-16-20_UTC_code.tar.gz - * Displays the URL for the backup with the specified file name in 's environment. - * @usage terminus backup:get awesome-site.dev --element=code - * Displays the URL for the most recent code backup in 's environment. + * @usage terminus backup:get . --file= + * Displays the URL for the backup with the file name in 's environment. + * @usage terminus backup:get . --element= + * Displays the URL for the most recent backup in 's environment. + * @usage terminus backup:get . --to= + * Saves the most recent backup of any type in 's environment to . + * @usage terminus backup:get . --element= --to= + * Saves the most recent backup in 's environment to . */ - public function getBackup($site_env, array $options = ['file' => null, 'element' => null,]) + public function getBackup($site_env, array $options = ['file' => null, 'element' => null, 'to' => null,]) { list($site, $env) = $this->getSiteEnv($site_env); @@ -52,6 +60,10 @@ public function getBackup($site_env, array $options = ['file' => null, 'element' $backup = array_shift($backups); } - return $backup->getUrl(); + $backup_url = $backup->getUrl(); + if (!isset($options['to']) || is_null($save_path = $options['to'])) { + return $backup_url; + } + $this->request()->download($backup_url, $save_path); } } diff --git a/src/Request/Request.php b/src/Request/Request.php index 4c9022590..736743199 100755 --- a/src/Request/Request.php +++ b/src/Request/Request.php @@ -7,13 +7,14 @@ use GuzzleHttp\Psr7\Request as HttpRequest; use League\Container\ContainerAwareInterface; use League\Container\ContainerAwareTrait; +use Pantheon\Terminus\Exceptions\TerminusException; +use Pantheon\Terminus\Helpers\LocalMachineHelper; use Pantheon\Terminus\Session\SessionAwareInterface; use Pantheon\Terminus\Session\SessionAwareTrait; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use Robo\Common\ConfigAwareTrait; use Robo\Contract\ConfigAwareInterface; -use Pantheon\Terminus\Exceptions\TerminusException; /** * Class Request @@ -27,33 +28,34 @@ * * @package Pantheon\Terminus\Request */ -class Request implements ConfigAwareInterface, SessionAwareInterface, LoggerAwareInterface, ContainerAwareInterface +class Request implements ConfigAwareInterface, ContainerAwareInterface, LoggerAwareInterface, SessionAwareInterface { - use LoggerAwareTrait; use ConfigAwareTrait; - use SessionAwareTrait; use ContainerAwareTrait; + use LoggerAwareTrait; + use SessionAwareTrait; + + const PAGED_REQUEST_ENTRY_LIMIT = 100; /** * Download file from target URL * * @param string $url URL to download from * @param string $target Target file's name - * @return bool True if download succeeded * @throws TerminusException */ - public static function download($url, $target) + public function download($url, $target) { - if (file_exists($target)) { - throw new TerminusException( - 'Target file {target} already exists.', - compact('target') - ); + if ($this->getContainer()->get(LocalMachineHelper::class)->getFilesystem()->exists($target)) { + throw new TerminusException('Target file {target} already exists.', compact('target')); } - $client = new Client(); + $parsed_url = parse_url($url); + $client = $this->getContainer()->get(Client::class, [[ + 'base_uri' => $parsed_url['host'], + RequestOptions::VERIFY => (boolean)$this->getConfig()->get('verify_host_cert', true), + ],]); $client->request('GET', $url, ['sink' => $target,]); - return true; } /** @@ -63,14 +65,12 @@ public static function download($url, $target) * @param array $options Options for the request * string method GET is default * array form_params Fed into the body of the request + * integer limit Max number of entries to return * @return array */ public function pagedRequest($path, array $options = []) { - $limit = 100; - if (isset($options['limit'])) { - $limit = $options['limit']; - } + $limit = isset($options['limit']) ? $options['limit'] : self::PAGED_REQUEST_ENTRY_LIMIT; //$results is an associative array so we don't refetch $results = []; diff --git a/tests/unit_tests/Commands/Backup/GetCommandTest.php b/tests/unit_tests/Commands/Backup/GetCommandTest.php index ac9119188..ff392e92f 100644 --- a/tests/unit_tests/Commands/Backup/GetCommandTest.php +++ b/tests/unit_tests/Commands/Backup/GetCommandTest.php @@ -4,6 +4,7 @@ use Pantheon\Terminus\Commands\Backup\GetCommand; use Pantheon\Terminus\Exceptions\TerminusNotFoundException; +use Pantheon\Terminus\Request\Request; /** * Class GetCommandTest @@ -109,4 +110,35 @@ public function testGetBackupNoBackups() $out = $this->command->getBackup("$site.{$this->environment->id}", compact('element')); $this->assertNull($out); } + + /** + * Tests the backup:get command when saving the backup to a file + */ + public function testGetBackupToFile() + { + $test_filename = 'test.tar.gz'; + $test_download_url = 'http://download'; + $test_save_path = '/tmp/file.tar.gz'; + $request = $this->getMockBuilder(Request::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->backups->expects($this->once()) + ->method('getBackupByFileName') + ->with($test_filename) + ->willReturn($this->backup); + $this->backup->expects($this->once()) + ->method('getUrl') + ->willReturn($test_download_url); + $request->expects($this->once()) + ->method('download') + ->with( + $this->equalTo($test_download_url), + $this->equalTo($test_save_path) + ); + + $this->command->setRequest($request); + $out = $this->command->getBackup('mysite.dev', ['file' => $test_filename, 'to' => $test_save_path,]); + $this->assertNull($out); + } } diff --git a/tests/unit_tests/Request/RequestTest.php b/tests/unit_tests/Request/RequestTest.php index ebddb5086..4803710d6 100644 --- a/tests/unit_tests/Request/RequestTest.php +++ b/tests/unit_tests/Request/RequestTest.php @@ -2,35 +2,78 @@ namespace Pantheon\Terminus\UnitTests\Request; -use GuzzleHttp\Psr7\Response; -use GuzzleHttp\Psr7\Stream; +use GuzzleHttp\Client; use GuzzleHttp\RequestOptions; use GuzzleHttp\Psr7\Request as HttpRequest; -use GuzzleHttp\Client; +use GuzzleHttp\Psr7\Response; +use GuzzleHttp\Psr7\Stream; use League\Container\Container; use Pantheon\Terminus\Config\TerminusConfig; +use Pantheon\Terminus\Exceptions\TerminusException; +use Pantheon\Terminus\Helpers\LocalMachineHelper; use Pantheon\Terminus\Request\Request; use Pantheon\Terminus\Session\Session; use Psr\Log\LoggerInterface; +use Symfony\Component\Filesystem\Filesystem; +/** + * Class RequestTest + * Testing class for Pantheon\Terminus\Request\Request + * @package Pantheon\Terminus\UnitTests\Request + */ class RequestTest extends \PHPUnit_Framework_TestCase { - protected $request; - protected $http_request; + /** + * @var Client + */ protected $client; - protected $container; + /** + * @var TerminusConfig + */ protected $config; + /** + * @var Container + */ + protected $container; + /** + * @var Filesystem + */ + protected $filesystem; + /** + * @var HttpRequest + */ + protected $http_request; + /** + * @var LocalMachineHelper + */ + protected $local_machine_helper; + /** + * @var Request + */ + protected $request; + /** + * @var Session + */ protected $session; + /** + * @inheritdoc + */ public function setUp() { parent::setUp(); $this->request = new Request(); $this->http_request = $this->getMockBuilder(HttpRequest::class) - ->disableOriginalConstructor() - ->getMock(); + ->disableOriginalConstructor() + ->getMock(); $this->client = $this->getMock(Client::class); + $this->local_machine_helper = $this->getMockBuilder(LocalMachineHelper::class) + ->disableOriginalConstructor() + ->getMock(); + $this->filesystem = $this->getMockBuilder(Filesystem::class) + ->disableOriginalConstructor() + ->getMock(); $this->config = new TerminusConfig(); $this->config->set('host', 'example.com'); @@ -42,8 +85,8 @@ public function setUp() $this->container = $this->getMock(Container::class); $this->session = $this->getMockBuilder(Session::class) - ->disableOriginalConstructor() - ->getMock(); + ->disableOriginalConstructor() + ->getMock(); $this->logger = $this->getMock(LoggerInterface::class); $this->request->setContainer($this->container); @@ -52,40 +95,75 @@ public function setUp() $this->request->setLogger($this->logger); } - private function makeRequest($client_options, $request_options, $url, $options = []) + /** + * Tests a successful download + */ + public function testDownload() { - $this->container->expects($this->at(0)) - ->method('get') - ->with(Client::class, [$client_options]) - ->willReturn($this->client); + $domain = 'pantheon.io'; + $url = "http://$domain/somefile.tar.gz"; + $target = 'some local path'; + $this->container->expects($this->at(0)) + ->method('get') + ->with($this->equalTo(LocalMachineHelper::class)) + ->willReturn($this->local_machine_helper); + $this->local_machine_helper->expects($this->once()) + ->method('getFilesystem') + ->with() + ->willReturn($this->filesystem); + $this->filesystem->expects($this->once()) + ->method('exists') + ->with($target) + ->willReturn(false); $this->container->expects($this->at(1)) - ->method('get') - ->with(HttpRequest::class, $request_options) - ->willReturn($this->http_request); - - $message = $this->getMock(Response::class); - $body = $this->getMockBuilder(Stream::class) - ->disableOriginalConstructor() - ->getMock(); - $body->method('getContents')->willReturn(json_encode(['abc' => '123'])); - $message->expects($this->once()) - ->method('getBody') - ->willReturn($body); - $message->expects($this->once()) - ->method('getHeaders') - ->willReturn(['Content-type' => 'application/json']); - $message->expects($this->once()) - ->method('getStatusCode') - ->willReturn(200); + ->method('get') + ->with( + $this->equalTo(Client::class), + $this->equalTo([['base_uri' => $domain, RequestOptions::VERIFY => true,],]) + ) + ->willReturn($this->client); $this->client->expects($this->once()) - ->method('send') - ->with($this->http_request) - ->willReturn($message); + ->method('request') + ->with( + $this->equalTo('GET'), + $this->equalTo($url), + $this->equalTo(['sink' => $target,]) + ); - return $this->request->request($url, $options); + $out = $this->request->download($url, $target); + $this->assertNull($out); } + /** + * Tests an unsuccessful download because the target file already exists + */ + public function testDownloadPathExists() + { + $domain = 'pantheon.io'; + $url = "http://$domain/somefile.tar.gz"; + $target = 'some local path'; + + $this->container->expects($this->once()) + ->method('get') + ->with($this->equalTo(LocalMachineHelper::class)) + ->willReturn($this->local_machine_helper); + $this->local_machine_helper->expects($this->once()) + ->method('getFilesystem') + ->with() + ->willReturn($this->filesystem); + $this->filesystem->expects($this->once()) + ->method('exists') + ->with($target) + ->willReturn(true); + $this->client->expects($this->never()) + ->method('request'); + + $this->setExpectedException(TerminusException::class, "Target file $target already exists."); + + $out = $this->request->download($url, $target); + $this->assertNull($out); + } public function testRequest() { @@ -96,17 +174,17 @@ public function testRequest() $method = 'GET'; $uri = 'https://example.com:443/api/foo/bar'; $headers = [ - 'foo' => 'bar', - 'Content-type' => 'application/json', - 'User-Agent' => 'Terminus/1.1.1 (php_version=7.0.0&script=foo/bar/baz.php)' + 'foo' => 'bar', + 'Content-type' => 'application/json', + 'User-Agent' => 'Terminus/1.1.1 (php_version=7.0.0&script=foo/bar/baz.php)' ]; $body = ''; $request_options = [$method, $uri, $headers, $body]; $actual = $this->makeRequest($client_options, $request_options, 'foo/bar', ['headers' => ['foo' => 'bar']]); $expected = [ - 'data' => (object)['abc' => '123'], - 'headers' => ['Content-type' => 'application/json'], - 'status_code' => 200, + 'data' => (object)['abc' => '123'], + 'headers' => ['Content-type' => 'application/json'], + 'status_code' => 200, ]; $this->assertEquals($expected, $actual); } @@ -119,15 +197,33 @@ public function testRequestAuth() $method = 'GET'; $uri = 'https://example.com:443/api/foo/bar'; $headers = [ - 'Content-type' => 'application/json', - 'User-Agent' => 'Terminus/1.1.1 (php_version=7.0.0&script=foo/bar/baz.php)', - 'Authorization' => 'Bearer abc123' + 'Content-type' => 'application/json', + 'User-Agent' => 'Terminus/1.1.1 (php_version=7.0.0&script=foo/bar/baz.php)', + 'Authorization' => 'Bearer abc123' ]; $body = ''; $request_options = [$method, $uri, $headers, $body]; $this->makeRequest($client_options, $request_options, 'foo/bar'); } + public function testRequestFullPath() + { + $this->config->set('verify_host_cert', false); + $this->session->method('get')->with('session')->willReturn('abc123'); + + $client_options = ['base_uri' => 'https://example.com:443', RequestOptions::VERIFY => false]; + + $method = 'GET'; + $uri = 'http://foo.bar/a/b/c'; + $headers = [ + 'Content-type' => 'application/json', + 'User-Agent' => 'Terminus/1.1.1 (php_version=7.0.0&script=foo/bar/baz.php)' + ]; + $body = ''; + $request_options = [$method, $uri, $headers, $body]; + $this->makeRequest($client_options, $request_options, 'http://foo.bar/a/b/c'); + } + public function testRequestNoVerify() { $this->config->set('verify_host_cert', false); @@ -138,29 +234,97 @@ public function testRequestNoVerify() $method = 'GET'; $uri = 'https://example.com:443/api/foo/bar'; $headers = [ - 'Content-type' => 'application/json', - 'User-Agent' => 'Terminus/1.1.1 (php_version=7.0.0&script=foo/bar/baz.php)' + 'Content-type' => 'application/json', + 'User-Agent' => 'Terminus/1.1.1 (php_version=7.0.0&script=foo/bar/baz.php)' ]; $body = ''; $request_options = [$method, $uri, $headers, $body]; $this->makeRequest($client_options, $request_options, 'foo/bar'); } - public function testRequestFullPath() + public function testPagedRequest() { - $this->config->set('verify_host_cert', false); - $this->session->method('get')->with('session')->willReturn('abc123'); + $this->session->method('get')->with('session')->willReturn(false); - $client_options = ['base_uri' => 'https://example.com:443', RequestOptions::VERIFY => false]; + $client_options = ['base_uri' => 'https://example.com:443', RequestOptions::VERIFY => true]; $method = 'GET'; - $uri = 'http://foo.bar/a/b/c'; + $uri = 'https://example.com:443/api/foo/bar'; $headers = [ - 'Content-type' => 'application/json', - 'User-Agent' => 'Terminus/1.1.1 (php_version=7.0.0&script=foo/bar/baz.php)' + 'Content-type' => 'application/json', + 'User-Agent' => 'Terminus/1.1.1 (php_version=7.0.0&script=foo/bar/baz.php)', ]; $body = ''; $request_options = [$method, $uri, $headers, $body]; - $this->makeRequest($client_options, $request_options, 'http://foo.bar/a/b/c'); + + $expected_options = $request_options; + $expected_options[1] .= '?limit=' . Request::PAGED_REQUEST_ENTRY_LIMIT; + + $this->container->expects($this->at(0)) + ->method('get') + ->with(Client::class, [$client_options]) + ->willReturn($this->client); + $this->container->expects($this->at(1)) + ->method('get') + ->with(HttpRequest::class, $expected_options) + ->willReturn($this->http_request); + + $message = $this->getMock(Response::class); + $body = $this->getMockBuilder(Stream::class) + ->disableOriginalConstructor() + ->getMock(); + $body->method('getContents')->willReturn(json_encode(['abc' => (object)['id' => 'abc123',],])); + $message->expects($this->once()) + ->method('getBody') + ->willReturn($body); + $message->expects($this->once()) + ->method('getHeaders') + ->willReturn(['Content-type' => 'application/json']); + $message->expects($this->once()) + ->method('getStatusCode') + ->willReturn(200); + $this->client->expects($this->once()) + ->method('send') + ->with($this->http_request) + ->willReturn($message); + + $actual = $this->request->pagedRequest($uri, $request_options); + $expected = [ + 'data' => [(object)['id' => 'abc123',],], + ]; + $this->assertEquals($expected, $actual); + } + + private function makeRequest($client_options, $request_options, $url, $options = []) + { + $this->container->expects($this->at(0)) + ->method('get') + ->with(Client::class, [$client_options]) + ->willReturn($this->client); + $this->container->expects($this->at(1)) + ->method('get') + ->with(HttpRequest::class, $request_options) + ->willReturn($this->http_request); + + $message = $this->getMock(Response::class); + $body = $this->getMockBuilder(Stream::class) + ->disableOriginalConstructor() + ->getMock(); + $body->method('getContents')->willReturn(json_encode(['abc' => '123'])); + $message->expects($this->once()) + ->method('getBody') + ->willReturn($body); + $message->expects($this->once()) + ->method('getHeaders') + ->willReturn(['Content-type' => 'application/json']); + $message->expects($this->once()) + ->method('getStatusCode') + ->willReturn(200); + $this->client->expects($this->once()) + ->method('send') + ->with($this->http_request) + ->willReturn($message); + + return $this->request->request($url, $options); } }