diff --git a/README.md b/README.md
index bb2d525..9b8d2bc 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,7 @@ project at the moment is [tus](https://tus.io/).
- [Blueimp](#blueimp-driver)
- [DropzoneJS](#dropzonejs-driver)
- [Flow.js](#flow-js-driver)
+ - [Plupload](#plupload-driver)
- [Resumable.js](#resumable-js-driver)
- [simple-uploader.js](#simple-uploader-js-driver)
- [Identifiers](#identifiers)
@@ -152,6 +153,7 @@ Service | Driver name | Chunk
[Blueimp](#blueimp-driver) | `blueimp` | yes | yes
[DropzoneJS](#dropzonejs-driver) | `dropzone` | yes | no
[Flow.js](#flow-js-driver) | `flow-js` | yes | yes
+[Plupload](#plupload-driver) | `plupload` | yes | no
[Resumable.js](#resumable-js-driver) | `resumable-js` | yes | yes
[simple-uploader.js](#simple-uploader-js-driver) | `simple-uploader-js` | yes | yes
@@ -177,6 +179,12 @@ This driver handles requests made by the DropzoneJS client library.
This driver handles requests made by the Flow.js client library.
+### Plupload driver
+
+[website](https://github.com/moxiecode/plupload)
+
+This driver handles requests made by the Plupload client library.
+
### Resumable.js driver
[website](http://resumablejs.com/)
diff --git a/examples/plupload.blade.php b/examples/plupload.blade.php
new file mode 100644
index 0000000..7d57de0
--- /dev/null
+++ b/examples/plupload.blade.php
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+ Plupload
+
+
+ Plupload
+
+
+
+
+ - Your browser doesn't have HTML5 support.
+
+
+
+
+
+
diff --git a/src/Driver/PluploadUploadDriver.php b/src/Driver/PluploadUploadDriver.php
new file mode 100644
index 0000000..38e377f
--- /dev/null
+++ b/src/Driver/PluploadUploadDriver.php
@@ -0,0 +1,118 @@
+identifier = $identifier;
+ }
+
+
+ /**
+ * @inheritDoc
+ */
+ public function handle(Request $request, StorageConfig $config, Closure $fileUploaded = null): Response
+ {
+ if ($this->isRequestMethodIn($request, [Request::METHOD_POST])) {
+ return $this->save($request, $config, $fileUploaded);
+ }
+
+ throw new MethodNotAllowedHttpException([
+ Request::METHOD_POST,
+ ]);
+ }
+
+ /**
+ * @param \Illuminate\Http\Request $request
+ * @param \CodingSocks\ChunkUploader\StorageConfig $config
+ * @param \Closure|null $fileUploaded
+ *
+ * @return mixed
+ */
+ private function save(Request $request, StorageConfig $config, ?Closure $fileUploaded)
+ {
+ $file = $request->file('file');
+
+ $this->validateUploadedFile($file);
+
+ $this->validateChunkRequest($request);
+
+ return $this->saveChunk($file, $request, $config, $fileUploaded);
+ }
+
+ /**
+ * @param \Illuminate\Http\Request $request
+ */
+ private function validateChunkRequest(Request $request): void
+ {
+ $request->validate([
+ 'name' => 'required',
+ 'chunk' => 'required|integer',
+ 'chunks' => 'required|integer',
+ ]);
+ }
+
+ /**
+ * @param \Illuminate\Http\UploadedFile $file
+ * @param \Illuminate\Http\Request $request
+ * @param \CodingSocks\ChunkUploader\StorageConfig $config
+ * @param \Closure|null $fileUploaded
+ *
+ * @return \Symfony\Component\HttpFoundation\Response
+ */
+ private function saveChunk(UploadedFile $file, Request $request, StorageConfig $config, Closure $fileUploaded = null): Response
+ {
+ try {
+ $range = new PluploadRange($request);
+ } catch (InvalidArgumentException $e) {
+ throw new BadRequestHttpException($e->getMessage(), $e);
+ }
+
+ $uuid = $this->identifier->generateFileIdentifier($range->getTotal(), $file->getClientOriginalName());
+
+ $chunks = $this->storeChunk($config, $range, $file, $uuid);
+
+ if (!$range->isLast()) {
+ return new PercentageJsonResponse($range->getPercentage());
+ }
+
+ $targetFilename = $file->hashName();
+
+ $path = $this->mergeChunks($config, $chunks, $targetFilename);
+
+ if ($config->sweep()) {
+ $this->deleteChunkDirectory($config, $uuid);
+ }
+
+ $this->triggerFileUploadedEvent($config->getDisk(), $path, $fileUploaded);
+
+ return new PercentageJsonResponse($range->getPercentage());
+ }
+}
diff --git a/src/Range/PluploadRange.php b/src/Range/PluploadRange.php
new file mode 100644
index 0000000..8f15597
--- /dev/null
+++ b/src/Range/PluploadRange.php
@@ -0,0 +1,101 @@
+request;
+ }
+
+ $this->current = (float) $request->get(self::$CHUNK_NUMBER_PARAMETER_NAME);
+ $this->total = (float) $request->get(self::$TOTAL_CHUNK_NUMBER_PARAMETER_NAME);
+
+ if ($this->current < 0) {
+ throw new InvalidArgumentException(
+ sprintf('`%s` must be greater than or equal to zero', self::$CHUNK_NUMBER_PARAMETER_NAME)
+ );
+ }
+ if ($this->total < 1) {
+ throw new InvalidArgumentException(
+ sprintf('`%s` must be greater than zero', self::$TOTAL_CHUNK_NUMBER_PARAMETER_NAME)
+ );
+ }
+ if ($this->current >= $this->total) {
+ throw new InvalidArgumentException(
+ sprintf('`%s` must be less than `%s`', self::$CHUNK_NUMBER_PARAMETER_NAME, self::$TOTAL_CHUNK_NUMBER_PARAMETER_NAME)
+ );
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getStart(): float
+ {
+ return $this->current;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getEnd(): float
+ {
+ return $this->current + 1;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getTotal(): float
+ {
+ return $this->total;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isFirst(): bool
+ {
+ return $this->current === 0.0;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isLast(): bool
+ {
+ return $this->current >= ($this->total - 1);
+ }
+
+ /**
+ * @return float
+ */
+ public function getPercentage(): float
+ {
+ return floor(($this->current + 1) / $this->total * 100);
+ }
+}
diff --git a/src/UploadManager.php b/src/UploadManager.php
index b1ec436..84bdadf 100644
--- a/src/UploadManager.php
+++ b/src/UploadManager.php
@@ -6,6 +6,7 @@
use CodingSocks\ChunkUploader\Driver\DropzoneUploadDriver;
use CodingSocks\ChunkUploader\Driver\FlowJsUploadDriver;
use CodingSocks\ChunkUploader\Driver\MonolithUploadDriver;
+use CodingSocks\ChunkUploader\Driver\PluploadUploadDriver;
use CodingSocks\ChunkUploader\Driver\ResumableJsUploadDriver;
use CodingSocks\ChunkUploader\Driver\SimpleUploaderJsUploadDriver;
use Illuminate\Support\Manager;
@@ -35,6 +36,14 @@ public function createFlowJsDriver()
return new FlowJsUploadDriver($this->app['config']['chunk-uploader.resumable-js']);
}
+ public function createPluploadDriver()
+ {
+ /** @var \Illuminate\Support\Manager $identityManager */
+ $identityManager = $this->app['chunk-uploader.identity-manager'];
+
+ return new PluploadUploadDriver($identityManager->driver());
+ }
+
public function createResumableJsDriver()
{
return new ResumableJsUploadDriver($this->app['config']['chunk-uploader.resumable-js']);
diff --git a/tests/Driver/PluploadUploadDriverTest.php b/tests/Driver/PluploadUploadDriverTest.php
new file mode 100644
index 0000000..1539e5a
--- /dev/null
+++ b/tests/Driver/PluploadUploadDriverTest.php
@@ -0,0 +1,222 @@
+app->make('config')->set('chunk-uploader.identifier', 'nop');
+ $this->app->make('config')->set('chunk-uploader.uploader', 'plupload');
+ $this->app->make('config')->set('chunk-uploader.sweep', false);
+ $this->handler = $this->app->make(UploadHandler::class);
+
+ Storage::fake('local');
+ Event::fake();
+ }
+
+ public function testDriverInstance()
+ {
+ $manager = $this->app->make('chunk-uploader.upload-manager');
+
+ $this->assertInstanceOf(PluploadUploadDriver::class, $manager->driver());
+ }
+
+ public function notAllowedRequestMethods()
+ {
+ return [
+ 'GET' => [Request::METHOD_GET],
+ 'HEAD' => [Request::METHOD_HEAD],
+ 'PUT' => [Request::METHOD_PUT],
+ 'PATCH' => [Request::METHOD_PATCH],
+ 'DELETE' => [Request::METHOD_DELETE],
+ 'PURGE' => [Request::METHOD_PURGE],
+ 'OPTIONS' => [Request::METHOD_OPTIONS],
+ 'TRACE' => [Request::METHOD_TRACE],
+ 'CONNECT' => [Request::METHOD_CONNECT],
+ ];
+ }
+
+ /**
+ * @dataProvider notAllowedRequestMethods
+ */
+ public function testMethodNotAllowed($requestMethod)
+ {
+ $request = Request::create('', $requestMethod);
+
+ $this->expectException(MethodNotAllowedHttpException::class);
+
+ $this->createTestResponse($this->handler->handle($request));
+ }
+
+ public function testUploadWhenFileParameterIsEmpty()
+ {
+ $request = Request::create('', Request::METHOD_POST);
+
+ $this->expectException(BadRequestHttpException::class);
+
+ $this->handler->handle($request);
+ }
+
+ public function testUploadWhenFileParameterIsInvalid()
+ {
+ $file = Mockery::mock(UploadedFile::class)
+ ->makePartial();
+ $file->shouldReceive('isValid')
+ ->andReturn(false);
+
+ $request = Request::create('', Request::METHOD_POST, [], [], [
+ 'file' => $file,
+ ]);
+
+ $this->expectException(InternalServerErrorHttpException::class);
+
+ $this->handler->handle($request);
+ }
+
+ public function excludedPostParameterProvider()
+ {
+ return [
+ 'name' => ['name'],
+ 'chunk' => ['chunk'],
+ 'chunks' => ['chunks'],
+ ];
+ }
+
+ /**
+ * @dataProvider excludedPostParameterProvider
+ */
+ public function testPostParameterValidation($exclude)
+ {
+ $arr = [
+ 'name' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt',
+ 'chunk' => 1,
+ 'chunks' => 2,
+ ];
+
+ unset($arr[$exclude]);
+
+ $request = Request::create('', Request::METHOD_POST, $arr, [], [
+ 'file' => UploadedFile::fake()
+ ->create('test.txt', 100),
+ ]);
+
+ $this->expectException(ValidationException::class);
+
+ $this->handler->handle($request);
+ }
+
+ public function testUploadFirstChunk()
+ {
+ $file = UploadedFile::fake()->create('test.txt', 100);
+ $request = Request::create('', Request::METHOD_POST, [
+ 'name' => 'test.txt',
+ 'chunk' => '0',
+ 'chunks' => '2',
+ ], [], [
+ 'file' => $file,
+ ]);
+
+ $response = $this->createTestResponse($this->handler->handle($request));
+ $response->assertSuccessful();
+ $response->assertJson(['done' => 50]);
+
+ Storage::disk('local')->assertExists('chunks/2_test.txt/0-1');
+
+ Event::assertNotDispatched(FileUploaded::class, function ($event) use ($file) {
+ return $event->file = $file->hashName('merged');
+ });
+ }
+
+ public function testUploadFirstChunkWithCallback()
+ {
+ $file = UploadedFile::fake()->create('test.txt', 100);
+ $request = Request::create('', Request::METHOD_POST, [
+ 'name' => 'test.txt',
+ 'chunk' => '0',
+ 'chunks' => '2',
+ ], [], [
+ 'file' => $file,
+ ]);
+
+ $callback = $this->createClosureMock($this->never());
+
+ $this->handler->handle($request, $callback);
+
+ Event::assertNotDispatched(FileUploaded::class, function ($event) use ($file) {
+ return $event->file = $file->hashName('merged');
+ });
+ }
+
+ public function testUploadLastChunk()
+ {
+ $this->createFakeLocalFile('chunks/2_test.txt', '0-1');
+
+ $file = UploadedFile::fake()->create('test.txt', 100);
+ $request = Request::create('', Request::METHOD_POST, [
+ 'name' => 'test.txt',
+ 'chunk' => '1',
+ 'chunks' => '2',
+ ], [], [
+ 'file' => $file,
+ ]);
+
+ $response = $this->createTestResponse($this->handler->handle($request));
+ $response->assertSuccessful();
+ $response->assertJson(['done' => 100]);
+
+ Storage::disk('local')->assertExists('chunks/2_test.txt/1-2');
+ Storage::disk('local')->assertExists($file->hashName('merged'));
+
+ Event::assertDispatched(FileUploaded::class, function ($event) use ($file) {
+ return $event->file = $file->hashName('merged');
+ });
+ }
+
+ public function testUploadLastChunkWithCallback()
+ {
+ $this->createFakeLocalFile('chunks/2_test.txt', '0-1');
+
+ $file = UploadedFile::fake()->create('test.txt', 100);
+ $request = Request::create('', Request::METHOD_POST, [
+ 'name' => 'test.txt',
+ 'chunk' => '1',
+ 'chunks' => '2',
+ ], [], [
+ 'file' => $file,
+ ]);
+
+ $callback = $this->createClosureMock(
+ $this->once(),
+ 'local',
+ $file->hashName('merged')
+ );
+
+ $this->handler->handle($request, $callback);
+
+ Event::assertDispatched(FileUploaded::class, function ($event) use ($file) {
+ return $event->file = $file->hashName('merged');
+ });
+ }
+}
diff --git a/tests/Range/PluploadRangeTest.php b/tests/Range/PluploadRangeTest.php
new file mode 100644
index 0000000..9ea3973
--- /dev/null
+++ b/tests/Range/PluploadRangeTest.php
@@ -0,0 +1,109 @@
+ [-1, 10, '`chunk` must be greater than or equal to zero'],
+ 'Number of chunks size smaller than zero' => [0, 0, '`chunks` must be greater than zero'],
+ 'Index smaller than zero' => [10, 10, '`chunk` must be less than `chunks`'],
+ ];
+ }
+
+ /**
+ * @dataProvider invalidArgumentProvider
+ *
+ * @param $chunk
+ * @param $chunks
+ * @param $expectedExceptionMessage
+ */
+ public function testArgumentValidation($chunk, $chunks, $expectedExceptionMessage)
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage($expectedExceptionMessage);
+
+ $this->createRequestBodyRange($chunk, $chunks);
+ }
+
+ public function testIsFirst()
+ {
+ $range = $this->createRequestBodyRange(0, 20);
+ $this->assertTrue($range->isFirst());
+ }
+
+ public function testIsLast()
+ {
+ $range = $this->createRequestBodyRange(19, 20);
+ $this->assertTrue($range->isLast());
+ }
+
+ public function testIsFirstAndIsLast()
+ {
+ $range = $this->createRequestBodyRange(0, 1);
+ $this->assertTrue($range->isFirst());
+ $this->assertTrue($range->isLast());
+ }
+
+ public function testGetTotal()
+ {
+ $range = $this->createRequestBodyRange(4, 20);
+ $this->assertEquals(20, $range->getTotal());
+ }
+
+ public function testGetStart()
+ {
+ $range = $this->createRequestBodyRange(4, 20);
+ $this->assertEquals(4, $range->getStart());
+ }
+
+ public function testGetEnd()
+ {
+ $range = $this->createRequestBodyRange(4, 20);
+ $this->assertEquals(5, $range->getEnd());
+ }
+
+ public function testGetPercentage()
+ {
+ $range = $this->createRequestBodyRange(4, 20);
+ $this->assertEquals(25, $range->getPercentage());
+ }
+
+ public function testCreateFromRequest()
+ {
+ $request = new Request([], [
+ 'chunk' => 4,
+ 'chunks' => 10,
+ ]);
+
+ $range = new PluploadRange($request);
+
+ $this->assertEquals(4, $range->getStart());
+ $this->assertEquals(5, $range->getEnd());
+ $this->assertEquals(10, $range->getTotal());
+ }
+
+ /**
+ * @param float $chunk
+ * @param float $chunks
+ *
+ * @return \CodingSocks\ChunkUploader\Range\PluploadRange
+ */
+ private function createRequestBodyRange(float $chunk, float $chunks)
+ {
+ $request = new ParameterBag([
+ 'chunk' => (string) $chunk,
+ 'chunks' => (string) $chunks,
+ ]);
+
+ return new PluploadRange($request);
+ }
+}