diff --git a/README.md b/README.md index 50c2f38..4aa5d52 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ project at the moment is [tus](https://tus.io/). - [DropzoneJS](#dropzonejs-driver) - [Flow.js](#flow-js-driver) - [Resumable.js](#resumable-js-driver) + - [simple-uploader.js](#simple-uploader-js-driver) - [Identifiers](#identifiers) - [Session identifier](#session-identifier) - [Contribution](#contribution) @@ -144,13 +145,14 @@ If you wrote a custom driver that others might find useful, please consider addi Below is a list of available drivers along with their individual specs: -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 +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 +[simple-uploader.js](#simple-uploader-js-driver) | `simple-uploader-js` | yes | yes ### Monolith driver @@ -180,6 +182,12 @@ This driver handles requests made by the Flow.js client library. This driver handles requests made by the Resumable.js client library. +### simple-uploader.js driver + +[website](https://github.com/simple-uploader/Uploader) + +This driver handles requests made by the simple-uploader.js client library. + ## Identifiers In some cases an identifier is needed for the uploaded file when the client side library does not provide one. diff --git a/config/chunk-uploader.php b/config/chunk-uploader.php index 589de9a..8ebc04c 100644 --- a/config/chunk-uploader.php +++ b/config/chunk-uploader.php @@ -12,7 +12,8 @@ | throughout your application here. By default, the module is setup for | monolith upload. | - | Supported: "monolith", "blueimp", "dropzone", "flow-js", "resumable-js" + | Supported: "monolith", "blueimp", "dropzone", "flow-js", "resumable-js", + | "simple-uploader-js" | */ @@ -184,4 +185,25 @@ ], + /* + |-------------------------------------------------------------------------- + | simple-uploader.js Options + |-------------------------------------------------------------------------- + | + | Here you may configure the options for the simple-uploader.js driver. + | + */ + + 'simple-uploader-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, + + ], + ]; diff --git a/src/Driver/SimpleUploaderJsUploadDriver.php b/src/Driver/SimpleUploaderJsUploadDriver.php new file mode 100644 index 0000000..70183f2 --- /dev/null +++ b/src/Driver/SimpleUploaderJsUploadDriver.php @@ -0,0 +1,30 @@ + 'chunkNumber', + // The name of the total number of chunks POST parameter to use for the file chunk. + 'total-chunks' => 'totalChunks', + // The name of the general chunk size POST parameter to use for the file chunk. + 'chunk-size' => 'chunkSize', + // The name of the total file size number POST parameter to use for the file chunk. + 'total-size' => 'totalSize', + // The name of the unique identifier POST parameter to use for the file chunk. + 'identifier' => 'identifier', + // The name of the original file name POST parameter to use for the file chunk. + 'file-name' => 'filename', + // The name of the file's relative path POST parameter to use for the file chunk. + 'relative-path' => 'relativePath', + // The name of the current chunk size POST parameter to use for the file chunk. + 'current-chunk-size' => 'currentChunkSize', + ]; + parent::__construct($config); + } +} diff --git a/src/UploadManager.php b/src/UploadManager.php index 7c767d1..eadc498 100644 --- a/src/UploadManager.php +++ b/src/UploadManager.php @@ -8,6 +8,7 @@ use CodingSocks\ChunkUploader\Driver\FlowJsUploadDriver; use CodingSocks\ChunkUploader\Driver\MonolithUploadDriver; use CodingSocks\ChunkUploader\Driver\ResumableJsUploadDriver; +use CodingSocks\ChunkUploader\Driver\SimpleUploaderJsUploadDriver; class UploadManager extends Manager { @@ -39,6 +40,11 @@ public function createResumableJsDriver() return new ResumableJsUploadDriver($this->app['config']['chunk-uploader.resumable-js']); } + public function createSimpleUploaderJsDriver() + { + return new SimpleUploaderJsUploadDriver($this->app['config']['chunk-uploader.simple-uploader-js']); + } + /** * Get the default driver name. * diff --git a/tests/Driver/SimpleUploaderUploadDriverTest.php b/tests/Driver/SimpleUploaderUploadDriverTest.php new file mode 100644 index 0000000..b00359a --- /dev/null +++ b/tests/Driver/SimpleUploaderUploadDriverTest.php @@ -0,0 +1,291 @@ +app->make('config')->set('chunk-uploader.uploader', 'simple-uploader-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(SimpleUploaderJsUploadDriver::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, [ + 'chunkNumber' => 2, + 'totalChunks' => 2, + 'chunkSize' => 100, + 'totalSize' => 200, + 'identifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', + 'filename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'relativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'currentChunkSize' => 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, [ + 'chunkNumber' => 1, + 'totalChunks' => 2, + 'chunkSize' => 100, + 'totalSize' => 200, + 'identifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', + 'filename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'relativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'currentChunkSize' => 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 [ + 'chunkNumber' => ['chunkNumber'], + 'totalChunks' => ['totalChunks'], + 'chunkSize' => ['chunkSize'], + 'totalSize' => ['totalSize'], + 'identifier' => ['identifier'], + 'filename' => ['filename'], + 'relativePath' => ['relativePath'], + 'currentChunkSize' => ['currentChunkSize'], + ]; + } + + /** + * @dataProvider excludedPostParameterProvider + */ + public function testPostParameterValidation($exclude) + { + $arr = [ + 'chunkNumber' => 1, + 'totalChunks' => 2, + 'chunkSize' => 100, + 'totalSize' => 200, + 'identifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', + 'filename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'relativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'currentChunkSize' => 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, [ + 'chunkNumber' => 1, + 'totalChunks' => 2, + 'chunkSize' => 100, + 'totalSize' => 200, + 'identifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', + 'filename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'relativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'currentChunkSize' => 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, [ + 'chunkNumber' => 1, + 'totalChunks' => 2, + 'chunkSize' => 100, + 'totalSize' => 200, + 'identifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', + 'filename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'relativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'currentChunkSize' => 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, [ + 'chunkNumber' => 2, + 'totalChunks' => 2, + 'chunkSize' => 100, + 'totalSize' => 200, + 'identifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', + 'filename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'relativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'currentChunkSize' => 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, [ + 'chunkNumber' => 2, + 'totalChunks' => 2, + 'chunkSize' => 100, + 'totalSize' => 200, + 'identifier' => '200-0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zftxt', + 'filename' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'relativePath' => '0jWZTB1ZDfRQU6VTcXy0mJnL9xKMeEz3HoSPU0Zf.txt', + 'currentChunkSize' => 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'); + }); + } +}