Skip to content

Commit 8f4ab05

Browse files
author
Martin Kluska
committed
initial commit
0 parents  commit 8f4ab05

File tree

11 files changed

+735
-0
lines changed

11 files changed

+735
-0
lines changed

composer.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "pion/laravel-chunk-upload",
3+
"description": "Service for chunked upload with several js providers",
4+
"require": {
5+
"laravel/framework": "5.*"
6+
},
7+
"require-dev": {
8+
},
9+
"autoload": {
10+
"psr-4": {
11+
"Pion\\Laravel\\ChunkUpload\\": "src/"
12+
}
13+
},
14+
"license": "MIT",
15+
"authors": [
16+
{
17+
"name": "Martin Kluska",
18+
"email": "martin.kluska@imakers.cz"
19+
}
20+
],
21+
"minimum-stability": "stable"
22+
}

readme.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Laravel chunked upload
2+
3+
___Project in progress___
4+
5+
## Instalation
6+
7+
composer require pion/laravel-chunk-upload
8+
9+
## Usage
10+
11+
In your own controller create the `FileReceiver`, more in example
12+
13+
## Supports
14+
15+
* Laravel 5+
16+
* [blueimp-file-upload](https://github.com/blueimp/jQuery-File-Upload) - partial support (simple chunked and single upload)
17+
18+
## Example
19+
20+
### Javascript
21+
22+
$element.fileupload({
23+
url: "upload_url",
24+
maxChunkSize: 1000000,
25+
method: "POST",
26+
sequentialUploads: true,
27+
formData: function(form) {
28+
//laravel token for communication
29+
return [{name: "_token", value: $form.find("[name=_token]").val()}];
30+
},
31+
progressall: function(e, data) {
32+
var progress = parseInt(data.loaded / data.total * 100, 10);
33+
console.log(progress+"%");
34+
}
35+
})
36+
.bind('fileuploadchunksend', function (e, data) {
37+
//console.log("fileuploadchunksend");
38+
})
39+
.bind('fileuploadchunkdone', function (e, data) {
40+
//console.log("fileuploadchunkdone");
41+
})
42+
.bind('fileuploadchunkfail', function (e, data) {
43+
console.log("fileuploadchunkfail")
44+
});
45+
46+
### Laravel controller
47+
Create laravel controller `UploadController` and create the file receiver with the desired handler.
48+
49+
#### Controller
50+
You must import the full namespace in your controler (`use`).
51+
52+
/**
53+
* Handles the file upload
54+
*
55+
* @param Request $request
56+
*
57+
* @return \Illuminate\Http\JsonResponse
58+
*
59+
* @throws UploadMissingFileException
60+
*/
61+
public function upload(Request $request) {
62+
63+
// create the file receiver
64+
$receiver = new FileReceiver("file", $request, ContentRangeUploadHandler::class);
65+
66+
// check if the upload is success
67+
if ($receiver->isUploaded()) {
68+
69+
// receive the file
70+
$save = $receiver->receive();
71+
72+
// check if the upload has finished (in chunk mode it will send smaller files)
73+
if ($save->isFinished()) {
74+
// save the file and return any response you need
75+
return $this->saveFile($save->getFile());
76+
} else {
77+
// we are in chunk mode, lets send the current progress
78+
79+
/** @var ContentRangeUploadHandler $handler */
80+
$handler = $save->handler();
81+
82+
return response()->json([
83+
"start" => $handler->getBytesStart(),
84+
"end" => $handler->getBytesEnd(),
85+
"total" => $handler->getBytesTotal()
86+
]);
87+
}
88+
} else {
89+
throw new UploadMissingFileException();
90+
}
91+
}
92+
93+
#### Route
94+
Add a route to your controller
95+
96+
Route::post('upload', 'UploadController@upload');
97+
98+
## Todo
99+
100+
- [ ] add more providers (like pbupload)
101+
- [ ] add facade for a quick usage with callback and custom response based on the handler
102+
- [ ] cron to delete uncompleted files
103+
- [ ] file per session (to support multiple)
104+
- [ ] add a config with custom storage location
105+
106+
## Contribution
107+
Are welcome. To add a new provider, just add a new Handler (which extends AbstractHandler), implement the chunk
108+
upload and progress
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
namespace Pion\Laravel\ChunkUpload\Exceptions;
3+
4+
class ChunkSaveException extends \Exception
5+
{
6+
7+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
namespace Pion\Laravel\ChunkUpload\Exceptions;
3+
4+
use Exception;
5+
6+
class UploadMissingFileException extends \Exception
7+
{
8+
/**
9+
* Construct the exception. Note: The message is NOT binary safe.
10+
* @link http://php.net/manual/en/exception.construct.php
11+
*
12+
* @param string $message [optional] The Exception message to throw.
13+
* @param int $code [optional] The Exception code.
14+
* @param Exception $previous [optional] The previous exception used for the exception chaining. Since 5.3.0
15+
*
16+
* @since 5.1.0
17+
*/
18+
public function __construct($message = "The request is missing a file", $code = 400, Exception $previous = null)
19+
{
20+
parent::__construct($message, $code, $previous);
21+
}
22+
23+
}

src/Handler/AbstractHandler.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
namespace Pion\Laravel\ChunkUpload\Handler;
3+
4+
use Illuminate\Http\Request;
5+
use Symfony\Component\HttpFoundation\File\UploadedFile;
6+
7+
/**
8+
* The handler that will detect if we can continue the chunked upload
9+
*
10+
* @package Pion\Laravel\ChunkUpload\Handler
11+
*/
12+
abstract class AbstractHandler
13+
{
14+
/**
15+
* @var Request
16+
*/
17+
protected $request;
18+
19+
/**
20+
* @var UploadedFile
21+
*/
22+
protected $file;
23+
24+
/**
25+
* AbstractReceiver constructor.
26+
*
27+
* @param Request $request
28+
* @param UploadedFile $file
29+
*/
30+
public function __construct(Request $request, UploadedFile $file)
31+
{
32+
$this->request = $request;
33+
$this->file = $file;
34+
}
35+
36+
/**
37+
* Returns the chunk file name
38+
*
39+
* @return string
40+
*/
41+
abstract public function getChunkFileName();
42+
43+
/**
44+
* Returns the first chunk
45+
* @return bool
46+
*/
47+
abstract public function isFirstChunk();
48+
49+
/**
50+
* Returns the chunks count
51+
*
52+
* @return int
53+
*/
54+
abstract public function isLastChunk();
55+
56+
/**
57+
* Returns the current chunk index
58+
*
59+
* @return bool
60+
*/
61+
abstract public function isChunkedUpload();
62+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php
2+
namespace Pion\Laravel\ChunkUpload\Handler;
3+
4+
use Illuminate\Http\Request;
5+
use Symfony\Component\HttpFoundation\File\UploadedFile;
6+
7+
/**
8+
* Class JqueryUploadReceiver
9+
*
10+
* Upload receiver that detects the content range by the the header value.
11+
*
12+
* @package Pion\Laravel\ChunkUpload\Handler
13+
*/
14+
class ContentRangeUploadHandler extends AbstractHandler
15+
{
16+
/**
17+
* The index for the header
18+
*/
19+
const CONTENT_RANGE_INDEX = "content-range";
20+
21+
/**
22+
* Determines if the upload is via chunked upload
23+
* @var bool
24+
*/
25+
protected $chunkedUpload = false;
26+
27+
/**
28+
* Current chunk start bytes
29+
* @var int
30+
*/
31+
protected $bytesStart = 0;
32+
33+
/**
34+
* Current chunk bytes end
35+
* @var int
36+
*/
37+
protected $bytesEnd = 0;
38+
39+
/**
40+
* The files total bytes
41+
* @var int
42+
*/
43+
protected $bytesTotal = 0;
44+
45+
/**
46+
* AbstractReceiver constructor.
47+
*
48+
* @param Request $request
49+
* @param UploadedFile $file
50+
*/
51+
public function __construct(Request $request, UploadedFile $file)
52+
{
53+
parent::__construct($request, $file);
54+
55+
$contentRange = $this->request->header(self::CONTENT_RANGE_INDEX);
56+
57+
$this->tryToParseContentRange($contentRange);
58+
}
59+
60+
/**
61+
* Tries to parse the content range from the string
62+
*
63+
* @param string $contentRange
64+
*/
65+
protected function tryToParseContentRange($contentRange)
66+
{
67+
// try to get the content range
68+
if (preg_match("/bytes ([\d]+)-([\d]+)\/([\d]+)/", $contentRange, $matches)) {
69+
70+
$this->chunkedUpload = true;
71+
72+
// write the bytes values
73+
$this->bytesStart = intval($matches[1]);
74+
$this->bytesEnd = intval($matches[2]);
75+
$this->bytesTotal = intval($matches[3]);
76+
}
77+
}
78+
79+
/**
80+
* Returns the first chunk
81+
* @return bool
82+
*/
83+
public function isFirstChunk()
84+
{
85+
return $this->bytesStart == 0;
86+
}
87+
88+
/**
89+
* Returns the chunks count
90+
*
91+
* @return int
92+
*/
93+
public function isLastChunk()
94+
{
95+
// the bytes starts from zero, remove 1 byte from total
96+
return $this->bytesEnd >= ($this->bytesTotal - 1);
97+
}
98+
99+
/**
100+
* Returns the current chunk index
101+
*
102+
* @return bool
103+
*/
104+
public function isChunkedUpload()
105+
{
106+
return $this->chunkedUpload;
107+
}
108+
109+
/**
110+
* @return int
111+
*/
112+
public function getBytesStart()
113+
{
114+
return $this->bytesStart;
115+
}
116+
117+
/**
118+
* @return int
119+
*/
120+
public function getBytesEnd()
121+
{
122+
return $this->bytesEnd;
123+
}
124+
125+
/**
126+
* @return int
127+
*/
128+
public function getBytesTotal()
129+
{
130+
return $this->bytesTotal;
131+
}
132+
133+
/**
134+
* Returns the chunk file name
135+
*
136+
* @return string
137+
*/
138+
public function getChunkFileName()
139+
{
140+
return $this->file->getClientOriginalName()."-".$this->bytesTotal.".part";
141+
}
142+
143+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
namespace Pion\Laravel\ChunkUpload\Providers;
3+
4+
use Illuminate\Support\ServiceProvider;
5+
6+
class ChunkUploadServiceProvider extends ServiceProvider
7+
{
8+
/**
9+
* Register the service provider.
10+
*
11+
* @return void
12+
*/
13+
public function register()
14+
{
15+
16+
}
17+
18+
}

0 commit comments

Comments
 (0)