Browse Source

Resumable class completion

xu ding 9 years ago
commit
b8f5a7a7ab

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+vendor/*
+composer.lock
+.idea

+ 28 - 0
composer.json

@@ -0,0 +1,28 @@
+{
+    "name": "dilab/resumable-php",
+    "description": "PHP package for Resumable.js",
+    "authors": [
+        {
+            "name": "xu ding",
+            "email": "thedilab@gmail.com"
+        }
+    ],
+    "require": {
+      "php": ">=5.3.0",
+      "cakephp/filesystem": "^3.0"
+    },
+    "require-dev": {
+      "phpunit/phpunit": "~4.0"
+    },
+    "license": "MIT",
+    "autoload": {
+      "psr-4": {
+        "Dilab\\": "src/"
+      }
+    },
+    "autoload-dev": {
+      "psr-4": {
+        "Dilab\\Test\\": "test/src/"
+      }
+    }
+}

+ 18 - 0
phpunit.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit backupGlobals="false"
+         backupStaticAttributes="false"
+         bootstrap="./vendor/autoload.php"
+         colors="true"
+         convertErrorsToExceptions="true"
+         convertNoticesToExceptions="true"
+         convertWarningsToExceptions="true"
+         processIsolation="false"
+         stopOnFailure="true"
+         syntaxCheck="false"
+        >
+    <testsuites>
+        <testsuite name="Package Test Suite">
+            <directory suffix=".php">./test/</directory>
+        </testsuite>
+    </testsuites>
+</phpunit>

+ 22 - 0
src/Network/Request.php

@@ -0,0 +1,22 @@
+<?php
+namespace Dilab\Network;
+
+interface Request {
+
+    /**
+     * @param $type get/post
+     * @return boolean
+     */
+    public function is($type);
+
+    /**
+     * @param $requestType GET/POST
+     * @return mixed
+     */
+    public function data($requestType);
+
+    /**
+     * @return FILES data
+     */
+    public function file();
+}

+ 12 - 0
src/Network/Response.php

@@ -0,0 +1,12 @@
+<?php
+namespace Dilab\Network;
+
+interface Response {
+
+    /**
+     * @param $statusCode
+     * @return mixed
+     */
+    public function header($statusCode);
+
+}

+ 34 - 0
src/Network/SimpleRequest.php

@@ -0,0 +1,34 @@
+<?php
+namespace Dilab\Network;
+
+use Dilab\Network\Request;
+
+class SimpleRequest implements Request
+{
+    /**
+     * @param $type get/post
+     * @return boolean
+     */
+    public function is($type)
+    {
+        $type = strtolower($type);
+    }
+
+    /**
+     * @param $requestType GET/POST
+     * @return mixed
+     */
+    public function data($requestType)
+    {
+        // TODO: Implement data() method.
+    }
+
+    /**
+     * @return FILES data
+     */
+    public function file()
+    {
+        // TODO: Implement file() method.
+    }
+
+}

+ 17 - 0
src/Network/SimpleResponse.php

@@ -0,0 +1,17 @@
+<?php
+namespace Dilab\Network;
+
+use Dilab\Network\Response;
+
+class SimpleResponse implements Response
+{
+    /**
+     * @param $statusCode
+     * @return mixed
+     */
+    public function header($statusCode)
+    {
+        // TODO: Implement header() method.
+    }
+
+}

+ 160 - 0
src/Resumable.php

@@ -0,0 +1,160 @@
+<?php
+namespace Dilab;
+
+use Cake\Filesystem\File;
+use Cake\Filesystem\Folder;
+use Dilab\Network\Request;
+use Dilab\Network\Response;
+
+class Resumable
+{
+
+    public $tempFolder = 'tmp';
+
+    public $uploadFolder = 'test/files/uploads';
+
+    // for testing
+    public $deleteTmpFolder = true;
+
+    protected $request;
+
+    protected $response;
+
+    protected $params;
+
+    protected $chunkFile;
+
+    public function __construct(Request $request, Response $response)
+    {
+        $this->request = $request;
+        $this->response = $response;
+    }
+
+    public function process()
+    {
+        if (!empty($this->resumableParams())) {
+            if (!empty($this->request->file())) {
+                $this->handleChunk();
+            } else {
+                $this->handleTestChunk();
+            }
+        }
+    }
+
+    public function handleTestChunk()
+    {
+        $identifier = $this->_resumableParam('identifier');
+        $filename = $this->_resumableParam('filename');
+        $chunkNumber = $this->_resumableParam('chunkNumber');
+
+        if (!$this->isChunkUploaded($identifier,$filename,$chunkNumber)) {
+            return $this->response->header(404);
+        } else {
+            return $this->response->header(200);
+        }
+    }
+
+    public function handleChunk()
+    {
+        $file = $this->request->file();
+        $identifier = $this->_resumableParam('identifier');
+        $filename = $this->_resumableParam('filename');
+        $chunkNumber = $this->_resumableParam('chunkNumber');
+        $chunkSize = $this->_resumableParam('chunkSize');
+        $totalSize = $this->_resumableParam('totalSize');
+
+        if (!$this->isChunkUploaded($identifier,$filename,$chunkNumber)) {
+           $chunkFile = $this->tmpChunkDir($identifier).DIRECTORY_SEPARATOR.$this->tmpChunkFilename($filename, $chunkNumber);
+           $this->moveUploadedFile($file['tmp_name'], $chunkFile);
+        }
+
+        if ($this->isFileUploadComplete($filename,$identifier,$chunkSize, $totalSize)) {
+            $tmpFolder = new Folder($this->tmpChunkDir($identifier));
+            $chunkFiles = $tmpFolder->read(true,true,true)[1];
+            $this->createFileFromChunks($chunkFiles, $this->uploadFolder.DIRECTORY_SEPARATOR.$filename);
+            if ($this->deleteTmpFolder) {
+                $tmpFolder->delete();
+            }
+        }
+
+        return $this->response->header(200);
+    }
+
+    private function _resumableParam($shortName) {
+        $resumableParams = $this->resumableParams();
+        if (!isset($resumableParams['resumable'.ucfirst($shortName)])) {
+            return null;
+        }
+        return $resumableParams['resumable'.ucfirst($shortName)];
+    }
+
+    public function resumableParams()
+    {
+        if ($this->request->is('get')) {
+            return $this->request->data('get');
+        }
+        if ($this->request->is('post')) {
+            return $this->request->data('post');
+        }
+    }
+
+    public function isFileUploadComplete($filename, $identifier, $chunkSize, $totalSize)
+    {
+        if ($chunkSize <= 0) {
+            return false;
+        }
+        $numOfChunks = intval($totalSize / $chunkSize) + ($totalSize % $chunkSize == 0 ? 0 : 1);
+        for ($i = 1; $i < $numOfChunks; $i++) {
+            if (!$this->isChunkUploaded($identifier, $filename, $i)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public function isChunkUploaded($identifier, $filename, $chunkNumber)
+    {
+        $file = new File($this->tmpChunkDir($identifier) . DIRECTORY_SEPARATOR . $this->tmpChunkFilename($filename, $chunkNumber));
+        return $file->exists();
+    }
+
+    public function tmpChunkDir($identifier)
+    {
+        return $this->tempFolder . DIRECTORY_SEPARATOR . $identifier;
+    }
+
+    public function tmpChunkFilename($filename, $chunkNumber)
+    {
+        return $filename . '.part' . $chunkNumber;
+    }
+
+    public function createFileFromChunks($chunkFiles, $destFile)
+    {
+        $destFile = new File($destFile, true);
+        foreach ($chunkFiles as $chunkFile) {
+            $file = new File($chunkFile);
+            $destFile->append($file->read());
+        }
+        return $destFile->exists();
+    }
+
+    public function moveUploadedFile($file, $destFile)
+    {
+        $file = new File($file);
+        if ($file->exists()) {
+            return $file->copy($destFile);
+        }
+        return false;
+    }
+
+    public function setRequest($request)
+    {
+        $this->request = $request;
+    }
+
+    public function setResponse($response)
+    {
+        $this->response = $response;
+    }
+
+}

BIN
test/files/mock.png.part1


BIN
test/files/mock.png.part2


BIN
test/files/mock.png.part3


+ 261 - 0
test/src/ResumableTest.php

@@ -0,0 +1,261 @@
+<?php
+namespace Dilab\Test;
+
+use Dilab\Network\SimpleRequest;
+use Dilab\Resumable;
+use Cake\Filesystem\File;
+
+/**
+ * Class ResumbableTest
+ * @package Dilab\Test
+ * @property $resumbable Resumable
+ * @property $request Request
+ * @property $response Response
+ */
+class ResumbableTest extends \PHPUnit_Framework_TestCase
+{
+    public $resumbable;
+
+    protected $provider;
+
+    protected function setUp()
+    {
+        $this->request = $this->getMockBuilder('Dilab\Network\SimpleRequest')
+                        ->getMock();
+
+        $this->response = $this->getMockBuilder('Dilab\Network\SimpleResponse')
+                        ->getMock();
+    }
+
+    public function tearDown()
+    {
+        unset($this->request);
+        unset($this->response);
+        parent::tearDown();
+    }
+
+    public function testProcessHandleChunk()
+    {
+        $resumableParams = array(
+            'resumableChunkNumber'=> 3,
+            'resumableTotalChunks'=> 600,
+            'resumableChunkSize'=>  200,
+            'resumableIdentifier'=> 'identifier',
+            'resumableFilename'=> 'mock.png',
+            'resumableRelativePath'=> 'upload',
+        );
+
+        $this->request->method('is')->will($this->returnValue(true));
+
+        $this->request->method('file')
+                    ->will($this->returnValue(array(
+                            'name'=> 'mock.png',
+                            'tmp_name'=>  'test/files/mock.png.part3',
+                            'error'=> 0,
+                            'size'=> 27000,
+                        )));
+
+        $this->request->method('data')->willReturn($resumableParams);
+
+        $this->resumbable = $this->getMockBuilder('Dilab\Resumable')
+                                ->setConstructorArgs(array($this->request,$this->response))
+                                ->setMethods(array('handleChunk'))
+                                ->getMock();
+
+        $this->resumbable->expects($this->once())
+                        ->method('handleChunk')
+                        ->willReturn(true);
+
+        $this->resumbable->process();
+    }
+
+    public function testProcessHandleTestChunk()
+    {
+        $resumableParams = array(
+            'resumableChunkNumber'=> 3,
+            'resumableTotalChunks'=> 600,
+            'resumableChunkSize'=>  200,
+            'resumableIdentifier'=> 'identifier',
+            'resumableFilename'=> 'mock.png',
+            'resumableRelativePath'=> 'upload',
+        );
+
+        $this->request->method('is')->will($this->returnValue(true));
+
+        $this->request->method('file')->will($this->returnValue(array()));
+
+        $this->request->method('data')->willReturn($resumableParams);
+
+        $this->resumbable = $this->getMockBuilder('Dilab\Resumable')
+                                ->setConstructorArgs(array($this->request,$this->response))
+                                ->setMethods(array('handleTestChunk'))
+                                ->getMock();
+
+        $this->resumbable->expects($this->once())
+                        ->method('handleTestChunk')
+                        ->willReturn(true);
+
+        $this->resumbable->process();
+    }
+
+    public function testHandleTestChunk()
+    {
+        $this->request->method('is')
+                      ->will($this->returnValue(true));
+
+        $this->request->method('data')
+                      ->willReturn(array(
+                           'resumableChunkNumber'=> 1,
+                           'resumableTotalChunks'=> 600,
+                           'resumableChunkSize'=>  200,
+                           'resumableIdentifier'=> 'identifier',
+                           'resumableFilename'=> 'mock.png',
+                           'resumableRelativePath'=> 'upload',
+                      ));
+
+        $this->response->expects($this->once())
+                        ->method('header')
+                        ->with($this->equalTo(200));
+
+        $this->resumbable = new Resumable($this->request,$this->response);
+        $this->resumbable->tempFolder = 'test/tmp';
+        $this->resumbable->handleTestChunk();
+    }
+
+    public function testHandleChunk() {
+        $resumableParams = array(
+            'resumableChunkNumber'=> 3,
+            'resumableTotalChunks'=> 600,
+            'resumableChunkSize'=>  200,
+            'resumableIdentifier'=> 'identifier',
+            'resumableFilename'=> 'mock.png',
+            'resumableRelativePath'=> 'upload',
+        );
+
+
+        $this->request->method('is')
+            ->will($this->returnValue(true));
+
+        $this->request->method('data')
+                ->willReturn($resumableParams);
+
+        $this->request->method('file')
+                ->willReturn(array(
+                    'name'=> 'mock.png',
+                    'tmp_name'=>  'test/files/mock.png.part3',
+                    'error'=> 0,
+                    'size'=> 27000,
+                ));
+
+        $this->resumbable = new Resumable($this->request, $this->response);
+        $this->resumbable->tempFolder = 'test/tmp';
+        $this->resumbable->uploadFolder = 'test/uploads';
+        $this->resumbable->deleteTmpFolder = false;
+        $this->resumbable->handleChunk();
+
+        $this->assertFileExists('test/uploads/mock.png');
+        unlink('test/tmp/identifier/mock.png.part3');
+        unlink('test/uploads/mock.png');
+    }
+
+    public function testResumableParamsGetRequest()
+    {
+        $resumableParams = array(
+            'resumableChunkNumber'=> 1,
+            'resumableTotalChunks'=> 100,
+            'resumableChunkSize'=>  1000,
+            'resumableIdentifier'=> 100,
+            'resumableFilename'=> 'mock_file_name',
+            'resumableRelativePath'=> 'upload',
+        );
+
+        $this->request = $this->getMockBuilder('Dilab\Network\SimpleRequest')
+            ->getMock();
+
+        $this->request->method('is')
+            ->will($this->returnValue(true));
+
+        $this->request->method('data')->willReturn($resumableParams);
+
+        $this->resumbable = new Resumable($this->request,$this->response);
+        $this->assertEquals($resumableParams, $this->resumbable->resumableParams());
+    }
+
+    public function isFileUploadCompleteProvider()
+    {
+        return array(
+            array('mock.png', 'files', 20, 60, true),
+            array('mock.png','files', 25, 60, true),
+            array('mock.png','files', 10, 60, false),
+            array('mock.png','not-exist',20, 30, false),
+        );
+    }
+
+    /**
+     *
+     * @dataProvider isFileUploadCompleteProvider
+     */
+    public function testIsFileUploadComplete($filename,$identifier, $chunkSize, $totalSize, $expected)
+    {
+        $this->resumbable = new Resumable($this->request,$this->response);
+        $this->resumbable->tempFolder ='test';
+        $this->assertEquals($expected, $this->resumbable->isFileUploadComplete($filename, $identifier, $chunkSize, $totalSize));
+    }
+
+    public function testIsChunkUploaded()
+    {
+        $this->resumbable = new Resumable($this->request,$this->response);
+        $this->resumbable->tempFolder ='test';
+        $identifier = 'files';
+        $filename = 'mock.png';
+        $this->assertTrue($this->resumbable->isChunkUploaded($identifier,$filename,1));
+        $this->assertFalse($this->resumbable->isChunkUploaded($identifier,$filename,10));
+    }
+
+    public function testTmpChunkDir()
+    {
+        $this->resumbable = new Resumable($this->request,$this->response);
+        $identifier = 'mock-identifier';
+        $expected = $this->resumbable->tempFolder.DIRECTORY_SEPARATOR.$identifier;
+        $this->assertEquals($expected, $this->resumbable->tmpChunkDir($identifier));
+    }
+
+    public function testTmpChunkFile()
+    {
+        $this->resumbable = new Resumable($this->request,$this->response);
+        $filename = 'mock-file.png';
+        $chunkNumber = 1;
+        $expected = $filename.'.part'.$chunkNumber;
+        $this->assertEquals($expected, $this->resumbable->tmpChunkFilename($filename,$chunkNumber));
+    }
+
+    public function testCreateFileFromChunks()
+    {
+        $files = array(
+            'test/files/mock.png.part1',
+            'test/files/mock.png.part2',
+            'test/files/mock.png.part3',
+        );
+        $totalFileSize = array_sum(array(
+            filesize('test/files/mock.png.part1'),
+            filesize('test/files/mock.png.part2'),
+            filesize('test/files/mock.png.part3')
+        ));
+        $destFile = 'test/files/5.png';
+
+        $this->resumbable = new Resumable($this->request,$this->response);
+        $this->resumbable->createFileFromChunks($files, $destFile);
+        $this->assertFileExists($destFile);
+        $this->assertEquals($totalFileSize, filesize($destFile));
+        unlink('test/files/5.png');
+    }
+
+    public function testMoveUploadedFile()
+    {
+        $destFile = 'test/files/4.png';
+        $this->resumbable = new Resumable($this->request,$this->response);
+        $this->resumbable->moveUploadedFile('test/files/mock.png.part1', $destFile);
+        $this->assertFileExists($destFile);
+        unlink($destFile);
+    }
+}

BIN
test/tmp/identifier/mock.png.part1


BIN
test/tmp/identifier/mock.png.part2


+ 0 - 0
test/uploads/empty