diff --git a/.gitignore b/.gitignore
index c105493..7a1e76a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
vendor/*
composer.lock
-.idea
\ No newline at end of file
+.idea
+.phpunit.cache/*
+.phpunit.*.cache
\ No newline at end of file
diff --git a/composer.json b/composer.json
index db97502..b67d234 100644
--- a/composer.json
+++ b/composer.json
@@ -8,12 +8,12 @@
}
],
"require": {
- "php": ">=5.3.0",
+ "php": ">=8.1.0",
"cakephp/filesystem": "^3.0",
- "monolog/monolog": "^1.17"
+ "monolog/monolog": "^2.0"
},
"require-dev": {
- "phpunit/phpunit": "~4.0"
+ "phpunit/phpunit": "~10.0"
},
"license": "MIT",
"autoload": {
diff --git a/phpunit.xml b/phpunit.xml
index 9447c87..851d307 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,18 +1,8 @@
-
-
-
- ./test/
-
-
-
\ No newline at end of file
+
+
+
+ ./test/
+
+
+
diff --git a/readme.md b/readme.md
index d3e3462..ce08d66 100644
--- a/readme.md
+++ b/readme.md
@@ -1,6 +1,5 @@
# PHP backend for resumable.js
-
## Installation
To install, use composer:
@@ -21,11 +20,23 @@ use Dilab\Resumable;
$request = new SimpleRequest();
$response = new SimpleResponse();
+// optional instanceId to seperate uploads from diffrent users like if two users want to upload untitled.jpg there would be no conflict anymore
+$instanceId = session_id();
-$resumable = new Resumable($request, $response);
+$resumable = new Resumable($request, $response, $instanceId);
$resumable->tempFolder = 'tmps';
$resumable->uploadFolder = 'uploads';
-$resumable->process();
+$status = $resumable->process();
+
+return match ($status){
+ 200 => ['message' => 'OK'], // Uploading of chunk is complete.
+ 201 => [
+ 'message' => 'File uploaded',
+ 'file' => $_REQUEST['resumableFilename']
+ ],// Uploading of whole file is complete.
+ 204 => ['message' => 'Chunk not found'],
+ default => ['message' => 'An error occurred'] //status => 404
+ };
```
diff --git a/src/Network/SimpleResponse.php b/src/Network/SimpleResponse.php
index 715d8a2..5d37b96 100644
--- a/src/Network/SimpleResponse.php
+++ b/src/Network/SimpleResponse.php
@@ -11,12 +11,14 @@ class SimpleResponse implements Response
*/
public function header($statusCode)
{
- if (200==$statusCode) {
- return header($_SERVER["SERVER_PROTOCOL"]." 200 Ok");
- } else if (404==$statusCode) {
- return header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
+ if($statusCode >= 500) {
+ $statusCode = 204;
}
- return header($_SERVER["SERVER_PROTOCOL"]." 204 No Content");
+ if (!in_array($statusCode, [200,201,204,404])) {
+ $statusCode = 404;
+ }
+ http_response_code($statusCode);
+ return $statusCode;
}
}
diff --git a/src/Resumable.php b/src/Resumable.php
index f2ae54c..a487237 100644
--- a/src/Resumable.php
+++ b/src/Resumable.php
@@ -24,6 +24,8 @@ class Resumable
protected $response;
+ protected $instanceId;
+
protected $params;
protected $chunkFile;
@@ -45,15 +47,17 @@ class Resumable
'filename' => 'filename',
'chunkNumber' => 'chunkNumber',
'chunkSize' => 'chunkSize',
- 'totalSize' => 'totalSize'
+ 'totalSize' => 'totalSize',
+ 'totalChunks' => 'totalChunks'
];
const WITHOUT_EXTENSION = true;
- public function __construct(Request $request, Response $response)
+ public function __construct(Request $request, Response $response, string|null $instanceId = null)
{
$this->request = $request;
$this->response = $response;
+ $this->instanceId = $instanceId;
$this->log = new Logger('debug');
$this->log->pushHandler(new StreamHandler('debug.log', Logger::DEBUG));
@@ -81,9 +85,9 @@ public function process()
{
if (!empty($this->resumableParams())) {
if (!empty($this->request->file())) {
- $this->handleChunk();
+ return $this->handleChunk();
} else {
- $this->handleTestChunk();
+ return $this->handleTestChunk();
}
}
}
@@ -174,10 +178,17 @@ public function handleTestChunk()
$identifier = $this->resumableParam($this->resumableOption['identifier']);
$filename = $this->resumableParam($this->resumableOption['filename']);
$chunkNumber = $this->resumableParam($this->resumableOption['chunkNumber']);
+ $chunkSize = $this->resumableParam($this->resumableOption['chunkSize']);
+ $totalChunks = $this->resumableParam($this->resumableOption['totalChunks']);
if (!$this->isChunkUploaded($identifier, $filename, $chunkNumber)) {
return $this->response->header(204);
} else {
+ if ($this->isFileUploadComplete($filename, $identifier, $totalChunks)) {
+ $this->isUploadComplete = true;
+ $this->createFileAndDeleteTmp($identifier, $filename);
+ return $this->response->header(201);
+ }
return $this->response->header(200);
}
@@ -190,16 +201,17 @@ public function handleChunk()
$filename = $this->resumableParam($this->resumableOption['filename']);
$chunkNumber = $this->resumableParam($this->resumableOption['chunkNumber']);
$chunkSize = $this->resumableParam($this->resumableOption['chunkSize']);
- $totalSize = $this->resumableParam($this->resumableOption['totalSize']);
+ $totalChunks = $this->resumableParam($this->resumableOption['totalChunks']);
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)) {
+ if ($this->isFileUploadComplete($filename, $identifier, $totalChunks)) {
$this->isUploadComplete = true;
$this->createFileAndDeleteTmp($identifier, $filename);
+ return $this->response->header(201);
}
return $this->response->header(200);
@@ -221,7 +233,12 @@ private function createFileAndDeleteTmp($identifier, $filename)
}
// replace filename reference by the final file
- $this->filepath = $this->uploadFolder . DIRECTORY_SEPARATOR . $finalFilename;
+ $this->filepath = $this->uploadFolder . DIRECTORY_SEPARATOR;
+ if (!empty($this->instanceId)) {
+ $this->filepath .= $this->instanceId . DIRECTORY_SEPARATOR;
+ }
+ $this->filepath .= $finalFilename;
+
$this->extension = $this->findExtension($this->filepath);
if ($this->createFileFromChunks($chunkFiles, $this->filepath) && $this->deleteTmpFolder) {
@@ -249,13 +266,9 @@ public function resumableParams()
}
}
- public function isFileUploadComplete($filename, $identifier, $chunkSize, $totalSize)
+ public function isFileUploadComplete($filename, $identifier, $totalChunks)
{
- if ($chunkSize <= 0) {
- return false;
- }
- $numOfChunks = intval($totalSize / $chunkSize) + ($totalSize % $chunkSize == 0 ? 0 : 1);
- for ($i = 1; $i < $numOfChunks; $i++) {
+ for ($i = 1; $i <= $totalChunks; $i++) {
if (!$this->isChunkUploaded($identifier, $filename, $i)) {
return false;
}
@@ -271,13 +284,38 @@ public function isChunkUploaded($identifier, $filename, $chunkNumber)
public function tmpChunkDir($identifier)
{
- $tmpChunkDir = $this->tempFolder . DIRECTORY_SEPARATOR . $identifier;
- if (!file_exists($tmpChunkDir)) {
- mkdir($tmpChunkDir);
+ $tmpChunkDir = $this->tempFolder. DIRECTORY_SEPARATOR;
+ if (!empty($this->instanceId)){
+ $tmpChunkDir .= $this->instanceId . DIRECTORY_SEPARATOR;
}
+ $tmpChunkDir .= $identifier;
+ $this->ensureDirExists($tmpChunkDir);
return $tmpChunkDir;
}
+ /**
+ * make directory if it doesn't exists (Immune against the race condition)
+ *
+ *
+ * since the resuamble is usually used with simultaneously uploads,
+ * this sometimes resulted in directory creation btween the *is_dir* check
+ * and *mkdir* then following race condition.
+ * in this setup it will shut down the mkdir error
+ * then try to check if directory is created after that
+ *
+ * @param string $path the directoryPath to ensure
+ * @return void
+ * @throws \Exception
+ */
+ private function ensureDirExists($path)
+ {
+ umask(0);
+ if ( is_dir($path) || @mkdir($path, 0775, true) || is_dir($path)) {
+ return;
+ }
+ throw new \Exception("could not mkdir $path");
+ }
+
public function tmpChunkFilename($filename, $chunkNumber)
{
return $filename . '.' . str_pad($chunkNumber, 4, 0, STR_PAD_LEFT);
@@ -299,6 +337,10 @@ public function createFileFromChunks($chunkFiles, $destFile)
natsort($chunkFiles);
+ if (!empty($this->instanceId)) {
+ $this->ensureDirExists(dirname($destFile));
+ }
+
$handle = $this->getExclusiveFileHandle($destFile);
if (!$handle) {
return false;
@@ -319,6 +361,9 @@ public function createFileFromChunks($chunkFiles, $destFile)
public function moveUploadedFile($file, $destFile)
{
+ //workaround cakephp error regarding: TMP not defined
+ define("TMP",sys_get_temp_dir());
+
$file = new File($file);
if ($file->exists()) {
return $file->copy($destFile);
diff --git a/test/src/Network/SimpleRequestTest.php b/test/src/Network/SimpleRequestTest.php
index 9fa3301..e6d8843 100644
--- a/test/src/Network/SimpleRequestTest.php
+++ b/test/src/Network/SimpleRequestTest.php
@@ -8,14 +8,14 @@
* @package Dilab\Network
* @property $request Request
*/
-class SimpleRequestTest extends \PHPUnit_Framework_TestCase
+class SimpleRequestTest extends \PHPUnit\Framework\TestCase
{
- protected function setUp()
+ protected function setUp() : void
{
$this->request = new SimpleRequest();
}
- public function tearDown()
+ public function tearDown() : void
{
unset($this->request);
parent::tearDown();
diff --git a/test/src/Network/SimpleResponseTest.php b/test/src/Network/SimpleResponseTest.php
index 8772214..d567c4d 100644
--- a/test/src/Network/SimpleResponseTest.php
+++ b/test/src/Network/SimpleResponseTest.php
@@ -8,28 +8,28 @@
* @package Dilab\Network
* @property $response Response
*/
-class SimpleResponseTest extends \PHPUnit_Framework_TestCase
+class SimpleResponseTest extends \PHPUnit\Framework\TestCase
{
- protected function setUp()
+ protected function setUp() : void
{
$this->response = new SimpleResponse();
}
- public function tearDown()
+ public function tearDown() : void
{
unset($this->response);
parent::tearDown();
}
- public function headerProvider()
+ public static function headerProvider()
{
- return array(
- array(404,404),
- array(204,204),
- array(200,200),
- array(500,204),
- );
+ return [
+ [404,404],
+ [204,204],
+ [200,200],
+ [500,204],
+ ];
}
/**
diff --git a/test/src/ResumableTest.php b/test/src/ResumableTest.php
index 635dedc..4afe706 100644
--- a/test/src/ResumableTest.php
+++ b/test/src/ResumableTest.php
@@ -12,13 +12,13 @@
* @property $request Request
* @property $response Response
*/
-class ResumbableTest extends \PHPUnit_Framework_TestCase
+class ResumableTest extends \PHPUnit\Framework\TestCase
{
public $resumbable;
protected $provider;
- protected function setUp()
+ protected function setUp() : void
{
$this->request = $this->getMockBuilder('Dilab\Network\SimpleRequest')
->getMock();
@@ -27,7 +27,7 @@ protected function setUp()
->getMock();
}
- public function tearDown()
+ public function tearDown() : void
{
unset($this->request);
unset($this->response);
@@ -45,21 +45,21 @@ public function testProcessHandleChunk()
'resumableRelativePath'=> 'upload',
);
- $this->request->method('is')->will($this->returnValue(true));
+ $this->request->method('is')->willReturn(true);
$this->request->method('file')
- ->will($this->returnValue(array(
+ ->willReturn([
'name'=> 'mock.png',
- 'tmp_name'=> 'test/files/mock.png.003',
+ 'tmp_name'=> 'test/files/mock.png.0003',
'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'))
+ ->onlyMethods(array('handleChunk'))
->getMock();
$this->resumbable->expects($this->once())
@@ -80,15 +80,15 @@ public function testProcessHandleTestChunk()
'resumableRelativePath'=> 'upload',
);
- $this->request->method('is')->will($this->returnValue(true));
+ $this->request->method('is')->willReturn(true);
- $this->request->method('file')->will($this->returnValue(array()));
+ $this->request->method('file')->willReturn([]);
$this->request->method('data')->willReturn($resumableParams);
$this->resumbable = $this->getMockBuilder('Dilab\Resumable')
->setConstructorArgs(array($this->request,$this->response))
- ->setMethods(array('handleTestChunk'))
+ ->onlyMethods(array('handleTestChunk'))
->getMock();
$this->resumbable->expects($this->once())
@@ -101,12 +101,12 @@ public function testProcessHandleTestChunk()
public function testHandleTestChunk()
{
$this->request->method('is')
- ->will($this->returnValue(true));
+ ->willReturn(true);
$this->request->method('data')
->willReturn(array(
'resumableChunkNumber'=> 1,
- 'resumableTotalChunks'=> 600,
+ 'resumableTotalSize'=> 600,
'resumableChunkSize'=> 200,
'resumableIdentifier'=> 'identifier',
'resumableFilename'=> 'mock.png',
@@ -125,7 +125,8 @@ public function testHandleTestChunk()
public function testHandleChunk() {
$resumableParams = array(
'resumableChunkNumber'=> 3,
- 'resumableTotalChunks'=> 600,
+ // 'resumableTotalChunks'=> 600,
+ 'resumableTotalSize'=> 600,
'resumableChunkSize'=> 200,
'resumableIdentifier'=> 'identifier',
'resumableFilename'=> 'mock.png',
@@ -134,7 +135,7 @@ public function testHandleChunk() {
$this->request->method('is')
- ->will($this->returnValue(true));
+ ->willReturn(true);
$this->request->method('data')
->willReturn($resumableParams);
@@ -142,7 +143,7 @@ public function testHandleChunk() {
$this->request->method('file')
->willReturn(array(
'name'=> 'mock.png',
- 'tmp_name'=> 'test/files/mock.png.003',
+ 'tmp_name'=> 'test/files/mock.png.0003',
'error'=> 0,
'size'=> 27000,
));
@@ -154,7 +155,7 @@ public function testHandleChunk() {
$this->resumbable->handleChunk();
$this->assertFileExists('test/uploads/mock.png');
- file_exists('test/tmp/identifier/mock.png.003') && unlink('test/tmp/identifier/mock.png.003');
+ file_exists('test/tmp/identifier/mock.png.0003') && unlink('test/tmp/identifier/mock.png.0003');
unlink('test/uploads/mock.png');
}
@@ -173,7 +174,7 @@ public function testResumableParamsGetRequest()
->getMock();
$this->request->method('is')
- ->will($this->returnValue(true));
+ ->willReturn(true);
$this->request->method('data')->willReturn($resumableParams);
@@ -181,7 +182,7 @@ public function testResumableParamsGetRequest()
$this->assertEquals($resumableParams, $this->resumbable->resumableParams());
}
- public function isFileUploadCompleteProvider()
+ public static function isFileUploadCompleteProvider()
{
return array(
array('mock.png', 'files', 20, 60, true),