From 777c3ee3252fa97f1f564e26739f713af09c8c89 Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Tue, 23 Aug 2016 21:43:13 +0200 Subject: [PATCH] Add FileDisplayResponse A lazy implementation of the DisplayResponse that only hits the filesystem if the etag and mtime do not match. --- .../AppFramework/Http/FileDisplayResponse.php | 70 +++++++++++ .../Http/FileDisplayResponseTest.php | 109 ++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 lib/public/AppFramework/Http/FileDisplayResponse.php create mode 100644 tests/lib/AppFramework/Http/FileDisplayResponseTest.php diff --git a/lib/public/AppFramework/Http/FileDisplayResponse.php b/lib/public/AppFramework/Http/FileDisplayResponse.php new file mode 100644 index 0000000000..22171e2b37 --- /dev/null +++ b/lib/public/AppFramework/Http/FileDisplayResponse.php @@ -0,0 +1,70 @@ + + * + * @author Roeland Jago Douma + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCP\AppFramework\Http; + +use OCP\AppFramework\Http; +use OCP\Files\File; + +/** + * Class FileDisplayResponse + * + * @package OCP\AppFramework\Http + * @since 9.2.0 + */ +class FileDisplayResponse extends Response implements ICallbackResponse { + + /** @var File */ + private $file; + + /** + * FileDisplayResponse constructor. + * + * @param File $file + * @param int $statusCode + * @param array $headers + * @since 9.2.0 + */ + public function __construct(File $file, $statusCode=Http::STATUS_OK, + $headers=[]) { + $this->file = $file; + $this->setStatus($statusCode); + $this->setHeaders(array_merge($this->getHeaders(), $headers)); + $this->addHeader('Content-Disposition', 'inline; filename="' . rawurldecode($file->getName()) . '"'); + + $this->setETag($file->getEtag()); + $lastModified = new \DateTime(); + $lastModified->setTimestamp($file->getMTime()); + $this->setLastModified($lastModified); + } + + /** + * @param IOutput $output + * @since 9.2.0 + */ + public function callback(IOutput $output) { + if ($output->getHttpResponseCode() !== Http::STATUS_NOT_MODIFIED) { + $output->setHeader('Content-Length: ' . $this->file->getSize()); + $output->setOutput($this->file->getContent()); + } + } +} diff --git a/tests/lib/AppFramework/Http/FileDisplayResponseTest.php b/tests/lib/AppFramework/Http/FileDisplayResponseTest.php new file mode 100644 index 0000000000..49334c17e7 --- /dev/null +++ b/tests/lib/AppFramework/Http/FileDisplayResponseTest.php @@ -0,0 +1,109 @@ + + * + * @author Roeland Jago Douma + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace Test\AppFramework\Http; + +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\FileDisplayResponse; +use OCP\Files\File; + +class FileDisplayResponseTest extends \Test\TestCase { + /** @var File|\PHPUnit_Framework_MockObject_MockObject */ + private $file; + + /** @var FileDisplayResponse */ + private $response; + + public function setup() { + $this->file = $this->getMockBuilder('OCP\Files\File') + ->getMock(); + + $this->file->expects($this->once()) + ->method('getETag') + ->willReturn('myETag'); + $this->file->expects($this->once()) + ->method('getName') + ->willReturn('myFileName'); + $this->file->expects($this->once()) + ->method('getMTime') + ->willReturn(1464825600); + + $this->response = new FileDisplayResponse($this->file); + } + + public function testHeader() { + $headers = $this->response->getHeaders(); + $this->assertArrayHasKey('Content-Disposition', $headers); + $this->assertSame('inline; filename="myFileName"', $headers['Content-Disposition']); + } + + public function testETag() { + $this->assertSame('myETag', $this->response->getETag()); + } + + public function testLastModified() { + $lastModified = $this->response->getLastModified(); + $this->assertNotNull($lastModified); + $this->assertSame(1464825600, $lastModified->getTimestamp()); + } + + public function test304() { + $output = $this->getMockBuilder('OCP\AppFramework\Http\IOutput') + ->disableOriginalConstructor() + ->getMock(); + + $output->expects($this->any()) + ->method('getHttpResponseCode') + ->willReturn(Http::STATUS_NOT_MODIFIED); + $output->expects($this->never()) + ->method('setOutput'); + $this->file->expects($this->never()) + ->method('getContent'); + + $this->response->callback($output); + } + + + public function testNon304() { + $output = $this->getMockBuilder('OCP\AppFramework\Http\IOutput') + ->disableOriginalConstructor() + ->getMock(); + + $output->expects($this->any()) + ->method('getHttpResponseCode') + ->willReturn(Http::STATUS_OK); + $output->expects($this->once()) + ->method('setOutput') + ->with($this->equalTo('my data')); + $output->expects($this->once()) + ->method('setHeader') + ->with($this->equalTo('Content-Length: 42')); + $this->file->expects($this->once()) + ->method('getContent') + ->willReturn('my data'); + $this->file->expects($this->any()) + ->method('getSize') + ->willReturn(42); + + $this->response->callback($output); + } +}