From ad7c4eb9a153a23f68ec66c95bdbbf7a8a9b9bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20G=C3=B6r=C3=B6g?= Date: Fri, 12 Jun 2020 13:19:49 +0200 Subject: [PATCH 1/5] Add Flow.js driver --- config/chunk-uploader.php | 21 ++ src/Driver/FlowJsUploadDriver.php | 49 ++++ src/UploadManager.php | 6 + tests/Driver/FlowJsUploadDriverTest.php | 291 ++++++++++++++++++++++++ 4 files changed, 367 insertions(+) create mode 100644 src/Driver/FlowJsUploadDriver.php create mode 100644 tests/Driver/FlowJsUploadDriverTest.php diff --git a/config/chunk-uploader.php b/config/chunk-uploader.php index 04c98f5..111f9d8 100644 --- a/config/chunk-uploader.php +++ b/config/chunk-uploader.php @@ -118,6 +118,27 @@ ], + /* + |-------------------------------------------------------------------------- + | Flow.js Options + |-------------------------------------------------------------------------- + | + | Here you may configure the options for the Flow.js driver. + | + */ + + 'flow-js' => [ + + // The name of the multipart request parameter to use for the file chunk + 'param' => 'file', + + // HTTP method for chunk test request. + 'test-method' => Illuminate\Http\Request::METHOD_GET, + // HTTP method to use when sending chunks to the server (POST, PUT, PATCH). + 'upload-method' => Illuminate\Http\Request::METHOD_POST, + + ], + /* |-------------------------------------------------------------------------- | Resumable.js Options diff --git a/src/Driver/FlowJsUploadDriver.php b/src/Driver/FlowJsUploadDriver.php new file mode 100644 index 0000000..2366709 --- /dev/null +++ b/src/Driver/FlowJsUploadDriver.php @@ -0,0 +1,49 @@ + 'flowChunkNumber', + // The name of the total number of chunks POST parameter to use for the file chunk. + 'total-chunks' => 'flowTotalChunks', + // The name of the general chunk size POST parameter to use for the file chunk. + 'chunk-size' => 'flowChunkSize', + // The name of the total file size number POST parameter to use for the file chunk. + 'total-size' => 'flowTotalSize', + // The name of the unique identifier POST parameter to use for the file chunk. + 'identifier' => 'flowIdentifier', + // The name of the original file name POST parameter to use for the file chunk. + 'file-name' => 'flowFilename', + // The name of the file's relative path POST parameter to use for the file chunk. + 'relative-path' => 'flowRelativePath', + // The name of the current chunk size POST parameter to use for the file chunk. + 'current-chunk-size' => 'flowCurrentChunkSize', + ]; + parent::__construct($config); + } +} diff --git a/src/UploadManager.php b/src/UploadManager.php index f53fac9..d0e62fd 100644 --- a/src/UploadManager.php +++ b/src/UploadManager.php @@ -5,6 +5,7 @@ use Illuminate\Support\Manager; use LaraCrafts\ChunkUploader\Driver\BlueimpUploadDriver; use LaraCrafts\ChunkUploader\Driver\DropzoneUploadDriver; +use LaraCrafts\ChunkUploader\Driver\FlowJsUploadDriver; use LaraCrafts\ChunkUploader\Driver\MonolithUploadDriver; use LaraCrafts\ChunkUploader\Driver\ResumableJsUploadDriver; @@ -28,6 +29,11 @@ public function createDropzoneDriver() return new DropzoneUploadDriver($this->app['config']['chunk-uploader.dropzone']); } + public function createFlowJsDriver() + { + return new FlowJsUploadDriver($this->app['config']['chunk-uploader.resumable-js']); + } + public function createResumableJsDriver() { return new ResumableJsUploadDriver($this->app['config']['chunk-uploader.resumable-js']); diff --git a/tests/Driver/FlowJsUploadDriverTest.php b/tests/Driver/FlowJsUploadDriverTest.php new file mode 100644 index 0000000..86d2a6d --- /dev/null +++ b/tests/Driver/FlowJsUploadDriverTest.php @@ -0,0 +1,291 @@ +app->make('config')->set('chunk-uploader.uploader', 'flow-js'); + $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(FlowJsUploadDriver::class, $manager->driver()); + } + + public function notAllowedRequestMethods() + { + return [ + '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 testResumeWhenChunkDoesNotExists() + { + $this->createFakeLocalFile('chunks/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', '000-099'); + + $request = Request::create('', Request::METHOD_GET, [ + 'flowChunkNumber' => 2, + 'flowTotalChunks' => 2, + 'flowChunkSize' => 100, + 'flowTotalSize' => 200, + 'flowIdentifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', + 'flowFilename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'flowRelativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'flowCurrentChunkSize' => 100, + ]); + + $response = $this->createTestResponse($this->handler->handle($request)); + $response->assertStatus(Response::HTTP_NO_CONTENT); + } + + public function testResume() + { + $this->createFakeLocalFile('chunks/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', '000-099'); + + $request = Request::create('', Request::METHOD_GET, [ + 'flowChunkNumber' => 1, + 'flowTotalChunks' => 2, + 'flowChunkSize' => 100, + 'flowTotalSize' => 200, + 'flowIdentifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', + 'flowFilename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'flowRelativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'flowCurrentChunkSize' => 100, + ]); + + $response = $this->createTestResponse($this->handler->handle($request)); + $response->assertSuccessful(); + } + + 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 [ + 'flowChunkNumber' => ['flowChunkNumber'], + 'flowTotalChunks' => ['flowTotalChunks'], + 'flowChunkSize' => ['flowChunkSize'], + 'flowTotalSize' => ['flowTotalSize'], + 'flowIdentifier' => ['flowIdentifier'], + 'flowFilename' => ['flowFilename'], + 'flowRelativePath' => ['flowRelativePath'], + 'flowCurrentChunkSize' => ['flowCurrentChunkSize'], + ]; + } + + /** + * @dataProvider excludedPostParameterProvider + */ + public function testPostParameterValidation($exclude) + { + $arr = [ + 'flowChunkNumber' => 1, + 'flowTotalChunks' => 2, + 'flowChunkSize' => 100, + 'flowTotalSize' => 200, + 'flowIdentifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', + 'flowFilename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'flowRelativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'flowCurrentChunkSize' => 100, + ]; + + 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, [ + 'flowChunkNumber' => 1, + 'flowTotalChunks' => 2, + 'flowChunkSize' => 100, + 'flowTotalSize' => 200, + 'flowIdentifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', + 'flowFilename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'flowRelativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'flowCurrentChunkSize' => 100, + ], [], [ + 'file' => $file, + ]); + + /** @var \Illuminate\Foundation\Testing\TestResponse $response */ + $response = $this->createTestResponse($this->handler->handle($request)); + $response->assertSuccessful(); + $response->assertJson(['done' => 50]); + + Storage::disk('local')->assertExists('chunks/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt/000-099'); + + 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, [ + 'flowChunkNumber' => 1, + 'flowTotalChunks' => 2, + 'flowChunkSize' => 100, + 'flowTotalSize' => 200, + 'flowIdentifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', + 'flowFilename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'flowRelativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'flowCurrentChunkSize' => 100, + ], [], [ + '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/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', '000-099'); + + $file = UploadedFile::fake()->create('test.txt', 100); + $request = Request::create('', Request::METHOD_POST, [ + 'flowChunkNumber' => 2, + 'flowTotalChunks' => 2, + 'flowChunkSize' => 100, + 'flowTotalSize' => 200, + 'flowIdentifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', + 'flowFilename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'flowRelativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'flowCurrentChunkSize' => 100, + ], [], [ + 'file' => $file, + ]); + + /** @var \Illuminate\Foundation\Testing\TestResponse $response */ + $response = $this->createTestResponse($this->handler->handle($request)); + $response->assertSuccessful(); + $response->assertJson(['done' => 100]); + + Storage::disk('local')->assertExists('chunks/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt/100-199'); + 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/200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', '000-099'); + + $file = UploadedFile::fake()->create('test.txt', 100); + $request = Request::create('', Request::METHOD_POST, [ + 'flowChunkNumber' => 2, + 'flowTotalChunks' => 2, + 'flowChunkSize' => 100, + 'flowTotalSize' => 200, + 'flowIdentifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', + 'flowFilename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'flowRelativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'flowCurrentChunkSize' => 100, + ], [], [ + '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'); + }); + } +} From e0c81ac88879df442f81b9d42bb9f68c9082085e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20G=C3=B6r=C3=B6g?= Date: Fri, 12 Jun 2020 15:11:49 +0200 Subject: [PATCH 2/5] Remove imports --- src/Driver/FlowJsUploadDriver.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/Driver/FlowJsUploadDriver.php b/src/Driver/FlowJsUploadDriver.php index 2366709..bb15444 100644 --- a/src/Driver/FlowJsUploadDriver.php +++ b/src/Driver/FlowJsUploadDriver.php @@ -2,20 +2,6 @@ namespace LaraCrafts\ChunkUploader\Driver; -use Closure; -use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; -use Illuminate\Http\UploadedFile; -use InvalidArgumentException; -use LaraCrafts\ChunkUploader\Helper\ChunkHelpers; -use LaraCrafts\ChunkUploader\Range\OneBasedRequestBodyRange; -use LaraCrafts\ChunkUploader\Response\PercentageJsonResponse; -use LaraCrafts\ChunkUploader\StorageConfig; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; -use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; - class FlowJsUploadDriver extends ResumableJsUploadDriver { /** From c947453f009f9a1c46e7723f7f3a2c7e89d48682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20G=C3=B6r=C3=B6g?= Date: Fri, 12 Jun 2020 15:19:45 +0200 Subject: [PATCH 3/5] Update README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 84f0a73..ecfcff8 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ project at the moment is [tus](https://tus.io/). - [Monolith](#monolith-driver) - [Blueimp](#blueimp-driver) - [DropzoneJS](#dropzonejs-driver) + - [Flow.js](#flow-js-driver) - [Resumable.js](#resumable-js-driver) - [Identifiers](#identifiers) - [Session identifier](#session-identifier) @@ -148,6 +149,7 @@ Service | Driver name | Chunk upload | Resumable [Monolith](#monolith-driver) | `monolith` | no | no [Blueimp](#blueimp-driver) | `blueimp` | yes | yes [DropzoneJS](#dropzonejs-driver) | `dropzone` | yes | no +[Flow.js](#flow-js-driver) | `flow-js` | yes | yes [Resumable.js](#resumable-js-driver) | `resumable-js` | yes | yes ### Monolith driver @@ -166,6 +168,12 @@ This driver handles requests made by the Blueimp jQuery File Upload client libra This driver handles requests made by the DropzoneJS client library. +### Flow.js driver + +[website](https://github.com/flowjs/flow.js) + +This driver handles requests made by the Flow.js client library. + ### Resumable.js driver [website](http://resumablejs.com/) From 6d1286a5f99ccdb2b031a2b6ed95714af68ccb2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20G=C3=B6r=C3=B6g?= Date: Fri, 12 Jun 2020 16:58:10 +0200 Subject: [PATCH 4/5] Update supported list of drivers in config --- config/chunk-uploader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/chunk-uploader.php b/config/chunk-uploader.php index 111f9d8..589de9a 100644 --- a/config/chunk-uploader.php +++ b/config/chunk-uploader.php @@ -12,7 +12,7 @@ | throughout your application here. By default, the module is setup for | monolith upload. | - | Supported: "monolith", "blueimp", "dropzone", "resumable-js" + | Supported: "monolith", "blueimp", "dropzone", "flow-js", "resumable-js" | */ From b10fdd005489548c0d77147ebe0ecec2d51f695b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20G=C3=B6r=C3=B6g?= Date: Sat, 20 Jun 2020 16:52:05 +0200 Subject: [PATCH 5/5] Change namespace --- src/Driver/FlowJsUploadDriver.php | 2 +- tests/Driver/FlowJsUploadDriverTest.php | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Driver/FlowJsUploadDriver.php b/src/Driver/FlowJsUploadDriver.php index bb15444..f17d9c7 100644 --- a/src/Driver/FlowJsUploadDriver.php +++ b/src/Driver/FlowJsUploadDriver.php @@ -1,6 +1,6 @@