diff --git a/README.md b/README.md
index 9b8d2bc..969018d 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)
+ - [ng-file-upload](#ng-file-upload-driver)
- [Plupload](#plupload-driver)
- [Resumable.js](#resumable-js-driver)
- [simple-uploader.js](#simple-uploader-js-driver)
@@ -153,6 +154,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
+[ng-file-upload](#ng-file-upload-driver) | `ng-file-upload` | yes | no
[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
@@ -179,6 +181,12 @@ This driver handles requests made by the DropzoneJS client library.
This driver handles requests made by the Flow.js client library.
+### ng-file-upload driver
+
+[website](https://github.com/danialfarid/ng-file-upload)
+
+This driver handles requests made by the ng-file-upload client library.
+
### Plupload driver
[website](https://github.com/moxiecode/plupload)
diff --git a/config/chunk-uploader.php b/config/chunk-uploader.php
index 8ebc04c..27ee52a 100644
--- a/config/chunk-uploader.php
+++ b/config/chunk-uploader.php
@@ -12,8 +12,8 @@
| throughout your application here. By default, the module is setup for
| monolith upload.
|
- | Supported: "monolith", "blueimp", "dropzone", "flow-js", "resumable-js",
- | "simple-uploader-js"
+ | Supported: "monolith", "blueimp", "dropzone", "flow-js",
+ | "ng-file-upload", "resumable-js", "simple-uploader-js"
|
*/
diff --git a/examples/ng-file-upload.blade.php b/examples/ng-file-upload.blade.php
new file mode 100644
index 0000000..a047e55
--- /dev/null
+++ b/examples/ng-file-upload.blade.php
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+ ng-file-upload
+
+
+ ng-file-upload
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Driver/NgFileUploadDriver.php b/src/Driver/NgFileUploadDriver.php
new file mode 100644
index 0000000..6f5ee66
--- /dev/null
+++ b/src/Driver/NgFileUploadDriver.php
@@ -0,0 +1,174 @@
+identifier = $identifier;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function handle(Request $request, StorageConfig $config, Closure $fileUploaded = null): Response
+ {
+ if ($this->isRequestMethodIn($request, [Request::METHOD_GET])) {
+ return $this->resume($request, $config);
+ }
+
+ if ($this->isRequestMethodIn($request, [Request::METHOD_POST])) {
+ return $this->save($request, $config, $fileUploaded);
+ }
+
+ throw new MethodNotAllowedHttpException([
+ Request::METHOD_GET,
+ Request::METHOD_POST,
+ ]);
+ }
+
+ private function resume(Request $request, StorageConfig $config): Response
+ {
+ $request->validate([
+ 'file' => 'required',
+ 'totalSize' => 'required',
+ ]);
+
+ $originalFilename = $request->get('file');
+ $totalSize = $request->get('totalSize');
+ $uuid = $this->identifier->generateFileIdentifier($totalSize, $originalFilename);
+
+ if (!$this->chunkExists($config, $uuid)) {
+ return new JsonResponse([
+ 'file' => $originalFilename,
+ 'size' => 0,
+ ]);
+ }
+
+ $chunk = Arr::last($this->chunks($config, $uuid));
+ $size = explode('-', basename($chunk))[1] + 1;
+
+ return new JsonResponse([
+ 'file' => $originalFilename,
+ 'size' => $size,
+ ]);
+ }
+
+ private function save(Request $request, StorageConfig $config, Closure $fileUploaded = null): Response
+ {
+ $file = $request->file('file');
+
+ $this->validateUploadedFile($file);
+
+ if ($this->isMonolithRequest($request)) {
+ return $this->saveMonolith($file, $config, $fileUploaded);
+ }
+
+ $this->validateChunkRequest($request);
+
+ return $this->saveChunk($file, $request, $config, $fileUploaded);
+ }
+
+ private function isMonolithRequest(Request $request)
+ {
+ return empty($request->post());
+ }
+
+ /**
+ * @param \Illuminate\Http\Request $request
+ */
+ private function validateChunkRequest(Request $request): void
+ {
+ $request->validate([
+ '_chunkNumber' => 'required|numeric',
+ '_chunkSize' => 'required|numeric',
+ '_totalSize' => 'required|numeric',
+ '_currentChunkSize' => 'required|numeric',
+ ]);
+ }
+
+ /**
+ * @param \Illuminate\Http\UploadedFile $file
+ * @param \CodingSocks\ChunkUploader\StorageConfig $config
+ * @param \Closure|null $fileUploaded
+ *
+ * @return \Symfony\Component\HttpFoundation\Response
+ */
+ private function saveMonolith(UploadedFile $file, StorageConfig $config, Closure $fileUploaded = null): Response
+ {
+ $path = $file->store($config->getMergedDirectory(), [
+ 'disk' => $config->getDisk(),
+ ]);
+
+ $this->triggerFileUploadedEvent($config->getDisk(), $path, $fileUploaded);
+
+ return new PercentageJsonResponse(100);
+ }
+
+ /**
+ * @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 NgFileUploadRange($request);
+ } catch (InvalidArgumentException $e) {
+ throw new BadRequestHttpException($e->getMessage(), $e);
+ }
+
+ $originalFilename = $file->getClientOriginalName();
+ $totalSize = $request->get('_totalSize');
+ $uuid = $this->identifier->generateFileIdentifier($totalSize, $originalFilename);
+
+ $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(100);
+ }
+}
diff --git a/src/Range/NgFileUploadRange.php b/src/Range/NgFileUploadRange.php
new file mode 100644
index 0000000..d3c3d50
--- /dev/null
+++ b/src/Range/NgFileUploadRange.php
@@ -0,0 +1,95 @@
+chunkNumber = (int) $request->get(self::$CHUNK_NUMBER_PARAMETER_NAME);
+ $this->chunkSize = (int) $request->get(self::$CHUNK_SIZE_PARAMETER_NAME);
+ $this->currentChunkSize = (int) $request->get(self::$CURRENT_CHUNK_SIZE_PARAMETER_NAME);
+ $this->totalSize = (double) $request->get(self::$TOTAL_SIZE_PARAMETER_NAME);
+
+ if ($this->chunkNumber < 0) {
+ throw new InvalidArgumentException(sprintf('`%s` must be greater than or equal to zero', self::$CHUNK_NUMBER_PARAMETER_NAME));
+ }
+ if ($this->chunkSize < 1) {
+ throw new InvalidArgumentException(sprintf('`%s` must be greater than zero', self::$CHUNK_SIZE_PARAMETER_NAME));
+ }
+ if ($this->currentChunkSize < 1) {
+ throw new InvalidArgumentException(sprintf('`%s` must be greater than zero', self::$CURRENT_CHUNK_SIZE_PARAMETER_NAME));
+ }
+ if ($this->totalSize < 1) {
+ throw new InvalidArgumentException(sprintf('`%s` must be greater than zero', self::$TOTAL_SIZE_PARAMETER_NAME));
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getStart(): float
+ {
+ return $this->chunkNumber * $this->chunkSize;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getEnd(): float
+ {
+ return $this->getStart() + $this->currentChunkSize - 1;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getTotal(): float
+ {
+ return $this->totalSize;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isFirst(): bool
+ {
+ return $this->chunkNumber === 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isLast(): bool
+ {
+ return $this->getEnd() === ($this->getTotal() - 1);
+ }
+
+ /**
+ * @return float
+ */
+ public function getPercentage(): float
+ {
+ return floor(($this->getEnd() + 1) / $this->getTotal() * 100);
+ }
+}
diff --git a/src/UploadManager.php b/src/UploadManager.php
index 84bdadf..0d60f00 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\NgFileUploadDriver;
use CodingSocks\ChunkUploader\Driver\PluploadUploadDriver;
use CodingSocks\ChunkUploader\Driver\ResumableJsUploadDriver;
use CodingSocks\ChunkUploader\Driver\SimpleUploaderJsUploadDriver;
@@ -36,6 +37,14 @@ public function createFlowJsDriver()
return new FlowJsUploadDriver($this->app['config']['chunk-uploader.resumable-js']);
}
+ public function createNgFileUploadDriver()
+ {
+ /** @var \Illuminate\Support\Manager $identityManager */
+ $identityManager = $this->app['chunk-uploader.identity-manager'];
+
+ return new NgFileUploadDriver($identityManager->driver());
+ }
+
public function createPluploadDriver()
{
/** @var \Illuminate\Support\Manager $identityManager */
diff --git a/tests/Driver/NgFileUploadDriverTest.php b/tests/Driver/NgFileUploadDriverTest.php
new file mode 100644
index 0000000..318627d
--- /dev/null
+++ b/tests/Driver/NgFileUploadDriverTest.php
@@ -0,0 +1,291 @@
+app->make('config')->set('chunk-uploader.identifier', 'nop');
+ $this->app->make('config')->set('chunk-uploader.uploader', 'ng-file-upload');
+ $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(NgFileUploadDriver::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()
+ {
+ $request = Request::create('', Request::METHOD_GET, [
+ 'file' => 'test.txt',
+ 'totalSize' => '200',
+ ]);
+
+ $response = $this->createTestResponse($this->handler->handle($request));
+ $response->assertSuccessful();
+ $response->assertJson(['size' => 0]);
+ }
+
+ public function testResume()
+ {
+ $this->createFakeLocalFile('chunks/200_test.txt', '000-099');
+
+ $request = Request::create('', Request::METHOD_GET, [
+ 'file' => 'test.txt',
+ 'totalSize' => '200',
+ ]);
+
+ $response = $this->createTestResponse($this->handler->handle($request));
+ $response->assertSuccessful();
+ $response->assertJson(['size' => 100]);
+ }
+
+ 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 testUploadMonolith()
+ {
+ $file = UploadedFile::fake()->create('test.txt', 100);
+ $request = Request::create('', Request::METHOD_POST, [], [], [
+ 'file' => $file,
+ ]);
+
+ $response = $this->createTestResponse($this->handler->handle($request));
+ $response->assertSuccessful();
+ $response->assertJson(['done' => 100]);
+
+ Storage::disk('local')->assertExists($file->hashName('merged'));
+
+ Event::assertDispatched(FileUploaded::class, function ($event) use ($file) {
+ return $event->file = $file->hashName('merged');
+ });
+ }
+
+ public function testUploadMonolithWithCallback()
+ {
+ $file = UploadedFile::fake()->create('test.txt', 100);
+ $request = Request::create('', Request::METHOD_POST, [], [], [
+ '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');
+ });
+ }
+
+ public function excludedPostParameterProvider()
+ {
+ return [
+ '_chunkNumber' => ['_chunkNumber'],
+ '_chunkSize' => ['_chunkSize'],
+ '_totalSize' => ['_totalSize'],
+ '_currentChunkSize' => ['_currentChunkSize'],
+ ];
+ }
+
+ /**
+ * @dataProvider excludedPostParameterProvider
+ */
+ public function testPostParameterValidation($exclude)
+ {
+ $arr = [
+ '_chunkNumber' => 1,
+ '_chunkSize' => 100,
+ '_totalSize' => 200,
+ '_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' => 0,
+ '_chunkSize' => 100,
+ '_totalSize' => 200,
+ '_currentChunkSize' => 100,
+ ], [], [
+ 'file' => $file,
+ ]);
+
+ $response = $this->createTestResponse($this->handler->handle($request));
+ $response->assertSuccessful();
+ $response->assertJson(['done' => 50]);
+
+ Storage::disk('local')->assertExists('chunks/200_test.txt/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' => 0,
+ '_chunkSize' => 100,
+ '_totalSize' => 200,
+ '_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_test.txt', '000-099');
+
+ $file = UploadedFile::fake()->create('test.txt', 100);
+ $request = Request::create('', Request::METHOD_POST, [
+ '_chunkNumber' => 1,
+ '_chunkSize' => 100,
+ '_totalSize' => 200,
+ '_currentChunkSize' => 100,
+ ], [], [
+ 'file' => $file,
+ ]);
+
+ $response = $this->createTestResponse($this->handler->handle($request));
+ $response->assertSuccessful();
+ $response->assertJson(['done' => 100]);
+
+ Storage::disk('local')->assertExists('chunks/200_test.txt/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_test.txt', '000-099');
+
+ $file = UploadedFile::fake()->create('test.txt', 100);
+ $request = Request::create('', Request::METHOD_POST, [
+ '_chunkNumber' => 1,
+ '_chunkSize' => 100,
+ '_totalSize' => 200,
+ '_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');
+ });
+ }
+}
diff --git a/tests/Range/NgFileUploadRangeTest.php b/tests/Range/NgFileUploadRangeTest.php
new file mode 100644
index 0000000..54e5a0a
--- /dev/null
+++ b/tests/Range/NgFileUploadRangeTest.php
@@ -0,0 +1,127 @@
+ [-1, 10, 10, 100, '`_chunkNumber` must be greater than or equal to zero'],
+ 'Chunk size less than one' => [0, 0, 10, 100, '`_chunkSize` must be greater than zero'],
+ 'Current chunk size less than one' => [0, 10, 0, 100, '`_currentChunkSize` must be greater than zero'],
+ 'Total size less than one' => [0, 10, 10, 0, '`_totalSize` must be greater than zero'],
+ ];
+ }
+
+ /**
+ * @dataProvider invalidArgumentProvider
+ *
+ * @param $chunkNumber
+ * @param $chunkSize
+ * @param $currentChunkSize
+ * @param $totalSize
+ * @param $expectedExceptionMessage
+ */
+ public function testArgumentValidation($chunkNumber, $chunkSize, $currentChunkSize, $totalSize, $expectedExceptionMessage)
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage($expectedExceptionMessage);
+
+ $this->createRequestBodyRange($chunkNumber, $chunkSize, $currentChunkSize, $totalSize);
+ }
+
+ public function testIsFirst()
+ {
+ $range = $this->createRequestBodyRange(0, 10, 10, 30);
+ $this->assertTrue($range->isFirst());
+
+ $range = $this->createRequestBodyRange(1, 10, 10, 30);
+ $this->assertFalse($range->isFirst());
+ }
+
+ public function testIsLast()
+ {
+ $range = $this->createRequestBodyRange(2, 10, 10, 30);
+ $this->assertTrue($range->isLast());
+
+ $range = $this->createRequestBodyRange(1, 10, 10, 30);
+ $this->assertFalse($range->isLast());
+ }
+
+ public function testIsFirstAndIsLast()
+ {
+ $range = $this->createRequestBodyRange(0, 10, 10, 10);
+ $this->assertTrue($range->isLast());
+ $this->assertTrue($range->isLast());
+ }
+
+ public function testGetTotal()
+ {
+ $range = $this->createRequestBodyRange(4, 10, 10, 190);
+ $this->assertEquals(190, $range->getTotal());
+ }
+
+ public function testGetStart()
+ {
+ $range = $this->createRequestBodyRange(4, 10, 10, 190);
+ $this->assertEquals(40, $range->getStart());
+ }
+
+ public function testGetEnd()
+ {
+ $range = $this->createRequestBodyRange(4, 10, 10, 190);
+ $this->assertEquals(49, $range->getEnd());
+ }
+
+ public function testGetPercentage()
+ {
+ $range = $this->createRequestBodyRange(4, 10, 10, 100);
+ $this->assertEquals(50, $range->getPercentage());
+
+ $range = $this->createRequestBodyRange(9, 10, 10, 100);
+ $this->assertEquals(100, $range->getPercentage());
+ }
+
+ public function testCreateFromRequest()
+ {
+ $request = new Request([], [
+ '_chunkNumber' => (string) 5,
+ '_chunkSize' => (string) 10,
+ '_currentChunkSize' => (string) 10,
+ '_totalSize' => (string) 100,
+ ]);
+
+ $range = new NgFileUploadRange($request);
+
+ $this->assertEquals(50, $range->getStart());
+ $this->assertEquals(59, $range->getEnd());
+ $this->assertEquals(100, $range->getTotal());
+ }
+
+ /**
+ * @param int $chunkNumber
+ * @param int $chunkSize
+ * @param int $currentChunkSize
+ * @param float $totalSize
+ *
+ * @return \CodingSocks\ChunkUploader\Range\NgFileUploadRange
+ */
+ private function createRequestBodyRange(int $chunkNumber, int $chunkSize, int $currentChunkSize, float $totalSize)
+ {
+ $request = new ParameterBag([
+ '_chunkNumber' => (string) $chunkNumber,
+ '_chunkSize' => (string) $chunkSize,
+ '_currentChunkSize' => (string) $currentChunkSize,
+ '_totalSize' => (string) $totalSize,
+ ]);
+
+ return new NgFileUploadRange($request);
+ }
+}