diff --git a/src/Exception/PhpEnvironmentException.php b/src/Exception/PhpEnvironmentException.php new file mode 100644 index 0000000..2cf35b4 --- /dev/null +++ b/src/Exception/PhpEnvironmentException.php @@ -0,0 +1,18 @@ +setOptions($options); + } + } + + /** + * Set options for a upload handler. Accepted options are: + * - session_namespace: session namespace for upload progress + * - progress_adapter: progressbar adapter to use for updating progress + * + * @param array|\Traversable $options + * @return AbstractUploadHandler + * @throws Exception\InvalidArgumentException + */ + public function setOptions($options) + { + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } elseif (!is_array($options)) { + throw new Exception\InvalidArgumentException( + 'The options parameter must be an array or a Traversable' + ); + } + + if (isset($options['session_namespace'])) { + $this->setSessionNamespace($options['session_namespace']); + } + if (isset($options['progress_adapter'])) { + $this->setProgressAdapter($options['progress_adapter']); + } + + return $this; + } + + /** + * @param string $sessionNamespace + * @return AbstractUploadHandler|UploadHandlerInterface + */ + public function setSessionNamespace($sessionNamespace) + { + $this->sessionNamespace = $sessionNamespace; + return $this; + } + + /** + * @return string + */ + public function getSessionNamespace() + { + return $this->sessionNamespace; + } + + /** + * @param AbstractProgressAdapter|ProgressBar $progressAdapter + * @return AbstractUploadHandler|UploadHandlerInterface + */ + public function setProgressAdapter($progressAdapter) + { + $this->progressAdapter = $progressAdapter; + return $this; + } + + /** + * @return AbstractProgressAdapter|ProgressBar + */ + public function getProgressAdapter() + { + return $this->progressAdapter; + } + + /** + * @param string $id + * @return array + */ + public function getProgress($id) + { + $status = array( + 'total' => 0, + 'current' => 0, + 'rate' => 0, + 'message' => 'No upload in progress', + 'done' => true + ); + if (empty($id)) { + return $status; + } + + $newStatus = $this->getUploadProgress($id); + if (false === $newStatus) { + return $status; + } + $status = $newStatus; + if ('' === $status['message']) { + $status['message'] = $this->toByteString($status['current']) . + " - " . $this->toByteString($status['total']); + } + $status['id'] = $id; + + $adapter = $this->getProgressAdapter(); + if (isset($adapter)) { + if ($adapter instanceof AbstractProgressAdapter) { + $adapter = new ProgressBar( + $adapter, 0, $status['total'], $this->getSessionNamespace() + ); + $this->setProgressAdapter($adapter); + } + + if (!$adapter instanceof ProgressBar) { + throw new Exception\RuntimeException('Unknown Adapter type given'); + } + + if ($status['done']) { + $adapter->finish(); + } else { + $adapter->update($status['current'], $status['message']); + } + } + + return $status; + } + + /** + * @param string $id + * @return array|boolean + */ + abstract protected function getUploadProgress($id); + + /** + * Returns the formatted size + * + * @param integer $size + * @return string + */ + protected function toByteString($size) + { + $sizes = array('B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'); + for ($i=0; $size >= 1024 && $i < 9; $i++) { + $size /= 1024; + } + + return round($size, 2) . $sizes[$i]; + } +} diff --git a/src/Upload/ApcProgress.php b/src/Upload/ApcProgress.php new file mode 100644 index 0000000..3273208 --- /dev/null +++ b/src/Upload/ApcProgress.php @@ -0,0 +1,68 @@ +isApcAvailable()) { + throw new Exception\PhpEnvironmentException('APC extension is not installed'); + } + + $uploadInfo = apc_fetch(ini_get('apc.rfc1867_prefix') . $id); + if (!is_array($uploadInfo)) { + return false; + } + + $status = array( + 'total' => 0, + 'current' => 0, + 'rate' => 0, + 'message' => '', + 'done' => false + ); + $status = $uploadInfo + $status; + if (!empty($status['cancel_upload'])) { + $status['done'] = true; + $status['message'] = 'The upload has been canceled'; + } + + return $status; + } + + /** + * Checks for the APC extension + * + * @return boolean + */ + public function isApcAvailable() + { + return (bool) ini_get('apc.enabled') + && (bool) ini_get('apc.rfc1867') + && is_callable('apc_fetch'); + } +} diff --git a/src/Upload/SessionProgress.php b/src/Upload/SessionProgress.php new file mode 100644 index 0000000..5cf6cbf --- /dev/null +++ b/src/Upload/SessionProgress.php @@ -0,0 +1,72 @@ +isSessionUploadProgressAvailable()) { + throw new Exception\PhpEnvironmentException( + 'Session Upload Progress is not available' + ); + } + + $uploadInfo = $_SESSION[ini_get('session.upload_progress.prefix') . $id]; + if (!is_array($uploadInfo)) { + return false; + } + + $status = array( + 'total' => 0, + 'current' => 0, + 'rate' => 0, + 'message' => '', + 'done' => false + ); + $status = $uploadInfo + $status; + $status['total'] = $status['content_length']; + $status['current'] = $status['bytes_processed']; + $status['rate'] = $status['bytes_processed'] / (time() - $status['start_time']); + if (!empty($status['cancel_upload'])) { + $status['done'] = true; + $status['message'] = 'The upload has been canceled'; + } + + return $status; + } + + /** + * Checks if Session Upload Progress is available + * + * @return boolean + */ + public function isSessionUploadProgressAvailable() + { + return (bool) version_compare(PHP_VERSION, '5.4.0rc1', '>=') + && (bool) ini_get('session.upload_progress.enabled'); + } +} diff --git a/src/Upload/UploadHandlerInterface.php b/src/Upload/UploadHandlerInterface.php new file mode 100644 index 0000000..ff65de5 --- /dev/null +++ b/src/Upload/UploadHandlerInterface.php @@ -0,0 +1,29 @@ +isUploadProgressAvailable()) { + throw new Exception\PhpEnvironmentException( + 'UploadProgress extension is not installed' + ); + } + + $uploadInfo = uploadprogress_get_info($id); + if (!is_array($uploadInfo)) { + return false; + } + + $status = array( + 'total' => 0, + 'current' => 0, + 'rate' => 0, + 'message' => '', + 'done' => false + ); + $status = $uploadInfo + $status; + $status['total'] = $status['bytes_total']; + $status['current'] = $status['bytes_uploaded']; + $status['rate'] = $status['speed_average']; + if ($status['total'] == $status['current']) { + $status['done'] = true; + } + + return $status; + } + + /** + * Checks for the UploadProgress extension + * + * @return boolean + */ + public function isUploadProgressAvailable() + { + return is_callable('uploadprogress_get_info'); + } +} diff --git a/test/Upload/AbstractUploadHandlerTest.php b/test/Upload/AbstractUploadHandlerTest.php new file mode 100644 index 0000000..a10a61f --- /dev/null +++ b/test/Upload/AbstractUploadHandlerTest.php @@ -0,0 +1,171 @@ + 1000, + 'current' => 500, + 'rate' => 0, + 'message' => '', + 'done' => false, + ); + $stub = $this->getMockForAbstractClass( + 'Zend\ProgressBar\Upload\AbstractUploadHandler' + ); + $stub->expects($this->any()) + ->method('getUploadProgress') + ->will($this->returnValue($progressData)); + + $progressData['id'] = '123'; + $progressData['message'] = '500B - 1000B'; + $this->assertEquals($progressData, $stub->getProgress('123')); + } + + /** + * @return void + */ + public function testGetNoFileInProgress() + { + $status = array( + 'total' => 0, + 'current' => 0, + 'rate' => 0, + 'message' => 'No upload in progress', + 'done' => true + ); + $stub = $this->getMockForAbstractClass( + 'Zend\ProgressBar\Upload\AbstractUploadHandler' + ); + $stub->expects($this->any()) + ->method('getUploadProgress') + ->will($this->returnValue(false)); + $this->assertEquals($status, $stub->getProgress('123')); + } + + /** + * @return array + */ + public function progressDataProvider() + { + return array( + array(array( + 'total' => 1000, + 'current' => 200, + 'rate' => 0, + 'message' => '', + 'done' => false, + )), + array(array( + 'total' => 1000, + 'current' => 600, + 'rate' => 300, + 'message' => '', + 'done' => false, + )), + array(array( + 'total' => 1000, + 'current' => 1000, + 'rate' => 500, + 'message' => '', + 'done' => true, + )), + ); + } + + /** + * @dataProvider progressDataProvider + * @param array $progressData + * @return void + */ + public function testProgressAdapterNotify($progressData) + { + $adapterStub = $this->getMockForAbstractClass( + 'Zend\ProgressBar\Adapter\AbstractAdapter' + ); + if ($progressData['done']) { + $adapterStub->expects($this->once()) + ->method('finish'); + } else { + $adapterStub->expects($this->once()) + ->method('notify'); + } + + $stub = $this->getMockForAbstractClass( + 'Zend\ProgressBar\Upload\AbstractUploadHandler' + ); + $stub->expects($this->once()) + ->method('getUploadProgress') + ->will($this->returnValue($progressData)); + $stub->setOptions(array( + 'session_namespace' => 'testSession', + 'progress_adapter' => $adapterStub, + )); + + $this->assertEquals('testSession', $stub->getSessionNamespace()); + $this->assertEquals($adapterStub, $stub->getProgressAdapter()); + + $this->assertNotEmpty($stub->getProgress('123')); + } + + /** + * @dataProvider progressDataProvider + * @param array $progressData + * @return void + */ + public function testProgressBarUpdate($progressData) + { + $adapterStub = $this->getMockForAbstractClass( + 'Zend\ProgressBar\Adapter\AbstractAdapter' + ); + if ($progressData['done']) { + $adapterStub->expects($this->once()) + ->method('finish'); + } else { + $adapterStub->expects($this->once()) + ->method('notify'); + } + $progressBar = new ProgressBar( + $adapterStub, 0, $progressData['total'], 'testSession' + ); + + + $stub = $this->getMockForAbstractClass( + 'Zend\ProgressBar\Upload\AbstractUploadHandler' + ); + $stub->expects($this->once()) + ->method('getUploadProgress') + ->will($this->returnValue($progressData)); + $stub->setOptions(array( + 'session_namespace' => 'testSession', + 'progress_adapter' => $progressBar, + )); + + $this->assertEquals('testSession', $stub->getSessionNamespace()); + $this->assertEquals($progressBar, $stub->getProgressAdapter()); + + $this->assertNotEmpty($stub->getProgress('123')); + } +}