Created
February 16, 2017 09:52
-
-
Save LostKobrakai/ddb11c64dcb0f1bf1c8eed4dd534a868 to your computer and use it in GitHub Desktop.
WireUpload subclass to support chunked uploads by http://www.resumablejs.com/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Dropzone.prototype.uploadFiles = function (files) { | |
var resumable = new Resumable ({ | |
target: '/', | |
testChunks: true, | |
chunkSize: 1 * 1024 * 1024 | |
}); | |
if (resumable.support) { | |
resumable.on('fileProgress', function (file) { | |
console.log(file) | |
var progressValue = Math.floor(resumable.progress() * 100); | |
Dropzone.prototype.defaultOptions.uploadprogress(file.file, progressValue, null); | |
}); | |
resumable.on('fileSuccess', (file) => { | |
return this._finished([file.file], "success", null); | |
}); | |
resumable.on('error', (message, file) => { | |
return this._errorProcessing([file.file], message, null); | |
}); | |
resumable.on('fileAdded', () => resumable.upload()); | |
files.forEach(file => resumable.addFile(file)) | |
} else { | |
// Otherwise use the old upload function | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php namespace ProcessWire; | |
require __DIR__ . '/src/WireUploadChunked.php'; | |
if($input->get->resumableIdentifier) { | |
if(\LostKobrakai\WireUploadChunked::testChunkFile()){ | |
http_response_code(200); | |
}else{ | |
http_response_code(204); | |
} | |
return $this->halt(); | |
} | |
if($_SERVER["REQUEST_METHOD"] == "POST") { | |
$upload_path = new WireTempDir('file_upload'); | |
$upload = new \LostKobrakai\WireUploadChunked('file'); | |
$upload | |
->setMaxFiles(1) | |
->setOverwrite(false) | |
->setMaxFileSize(50 * 1024 * 1024) | |
->setDestinationPath($upload_path) | |
->setValidExtensions(['jpg', 'png', 'jpeg', 'mp4']); | |
$files = $upload->execute(); | |
$errors = $upload->getErrors(); | |
if (count($errors)) { | |
http_response_code(500); | |
foreach ($errors as $error) echo $error; | |
die(); | |
} | |
$page->of(false); | |
foreach ($files as $file) { | |
$path = $upload_path . $file; | |
$page->files->add($path); | |
if (is_file($path)) unlink($path); | |
} | |
$page->save('files'); | |
return $this->halt(); | |
} | |
?> | |
<!DOCTYPE html> | |
<html lang="de"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Document</title> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dropzone/4.3.0/min/dropzone.min.css"> | |
</head> | |
<body> | |
<form action="." class="dropzone" id="my-awesome-dropzone"> | |
<div class="fallback"> | |
<input name="file" type="file" multiple /> | |
</div> | |
</form> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/4.3.0/min/dropzone.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/resumable.js/1.0.2/resumable.min.js"></script> | |
<script src="main.js"> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php namespace LostKobrakai; | |
class WireUploadChunked extends \ProcessWire\WireUpload { | |
/** | |
* @var bool $uploadingChunkFile | |
*/ | |
protected $uploadingChunkFile = false; | |
/** | |
* @var array $fileInfo | |
*/ | |
protected $fileInfo; | |
/** | |
* @var array $chunkInfo | |
*/ | |
protected $chunkInfo; | |
/** | |
* @var array $chunkTempDir | |
*/ | |
protected $chunkTempDir; | |
public function __construct($name) { | |
parent::__construct($name); | |
$this->fileInfo = [ | |
'totalChunks' => $this->input->post->resumableTotalChunks, | |
'sizeChunk' => $this->input->post->resumableChunkSize, | |
'relPath' => $this->input->post->resumableRelativePath, | |
'name' => $this->input->post->resumableFilename, | |
'identifier' => $this->input->post->resumableIdentifier, | |
'type' => $this->input->post->resumableType, | |
'size' => $this->input->post->resumableTotalSize | |
]; | |
$this->chunkInfo = [ | |
'size' => $this->input->post->resumableCurrentChunkSize, | |
'number' => $this->input->post->resumableChunkNumber | |
]; | |
$this->chunkTempDir = $this->config->paths->files . 'Chunks/' . $this->fileInfo['identifier'] . '/'; | |
if(!is_dir($this->chunkTempDir)) \ProcessWire\wireMkdir($this->chunkTempDir, true); | |
} | |
/** | |
* Check if a chunk file is present and valid | |
* | |
* @return bool | |
* | |
*/ | |
static public function testChunkFile() { | |
$fileInfo = [ | |
'totalChunks' => \ProcessWire\wire('input')->get->resumableTotalChunks, | |
'sizeChunk' => \ProcessWire\wire('input')->get->resumableChunkSize, | |
'relPath' => \ProcessWire\wire('input')->get->resumableRelativePath, | |
'name' => \ProcessWire\wire('input')->get->resumableFilename, | |
'identifier' => \ProcessWire\wire('input')->get->resumableIdentifier, | |
'type' => \ProcessWire\wire('input')->get->resumableType, | |
'size' => \ProcessWire\wire('input')->get->resumableTotalSize | |
]; | |
$chunkInfo = [ | |
'size' => \ProcessWire\wire('input')->get->resumableCurrentChunkSize, | |
'number' => \ProcessWire\wire('input')->get->resumableChunkNumber | |
]; | |
$dir = \ProcessWire\wire('config')->paths->files . 'Chunks/' . $fileInfo['identifier'] . '/'; | |
if(!is_dir($dir)) return false; | |
$pathname = $dir . $fileInfo['identifier'] . ".part" . $chunkInfo['number']; | |
if(!is_file($pathname)) return false; | |
if(filesize($pathname) != $chunkInfo['size']) return false; | |
return true; | |
} | |
/** | |
* Returns PHP's $_FILES or one constructed from an ajax upload | |
* | |
* @return array|bool | |
* @throws WireException | |
* | |
*/ | |
protected function getPhpFiles() { | |
if($this->fileInfo['totalChunks'] !== null) | |
return $this->getChunkedFile(); | |
return parent::getPhpFiles(); | |
} | |
/** | |
* Does the given filename have a valid extension? | |
* | |
* @param string $name | |
* @return bool | |
* | |
*/ | |
protected function isValidExtension($name) { | |
if($this->uploadingChunkFile) return true; | |
return parent::isValidExtension($name); | |
} | |
/** | |
* Move chunks to temp folder (60 mins timeout) | |
* | |
* @return array|bool | |
* @throws WireException | |
* | |
*/ | |
protected function getChunkedFile() { | |
if(!$this->isValidFile($this->fileInfo['name'], $this->fileInfo['size'])) return false; | |
$f = parent::getPhpFiles(); | |
$this->setupChunkFileUpload(function() use($f) { | |
if($this->isValidUpload($f['name'], $f['size'], $f['error'])){ | |
$name = $this->fileInfo['identifier'] . ".part" . $this->chunkInfo['number']; | |
$this->saveUpload($f['tmp_name'], $name, !empty($f['ajax'])); | |
} | |
}); | |
return $this->checkCompleteFile(); | |
} | |
/** | |
* Check if file is complete | |
* | |
* @return array|bool | |
* @throws WireException | |
* | |
*/ | |
protected function checkCompleteFile() { | |
// List all the needed chunk files | |
$files = array_map(function($i){ | |
$filename = $this->fileInfo['identifier'] . ".part$i"; | |
return new \SplFileInfo($this->chunkTempDir . $filename); | |
}, range(1, $this->fileInfo['totalChunks'])); | |
// Return if any file is not existing or not readable | |
foreach ($files as $file) | |
if(!$file->isFile() || !$file->isReadable()) | |
return false; | |
// Check the summed file size of chunks | |
$totalSize = array_sum(array_map(function(\SplFileInfo $file) { | |
return $file->getSize(); | |
}, $files)); | |
if ($totalSize < $this->fileInfo['size']) return false; | |
// Write file to temp dir | |
$pathname = tempnam($this->getTempDir(), 'WireUpload'); | |
$fp = fopen($pathname, 'w'); | |
if ($fp === false) return false; | |
foreach($files as $file) | |
fwrite($fp, file_get_contents($file->getPathname())); | |
fclose($fp); | |
// Remove chunks | |
\ProcessWire\wireRmdir($this->chunkTempDir, true); | |
// Return fileinfo for processwire | |
$filesize = is_file($pathname) ? filesize($pathname) : 0; | |
$error = $filesize ? \UPLOAD_ERR_OK : \UPLOAD_ERR_NO_FILE; | |
return array( | |
'name' => $this->fileInfo['name'], | |
'tmp_name' => $pathname, | |
'size' => $filesize, | |
'error' => $error, | |
'ajax' => true, | |
); | |
} | |
protected function getTempDir() { | |
$dir = $this->wire('config')->uploadTmpDir; | |
if(!$dir || !is_writable($dir)) $dir = ini_get('upload_tmp_dir'); | |
if(!$dir || !is_writable($dir)) $dir = sys_get_temp_dir(); | |
if(!$dir || !is_writable($dir)) { | |
throw new WireException("Error writing to $dir. Please define \$config->uploadTmpDir and ensure it is writable."); | |
} | |
return $dir; | |
} | |
/** | |
* Setup needed to upload chunks | |
* - Skip extention checks | |
* - Use different destination for chunks | |
* - Do not track uploaded files (no need to return their paths) | |
* - Do always overwrite chunk files | |
* | |
* @param callable $callback | |
* | |
*/ | |
protected function setupChunkFileUpload(callable $callback) { | |
$this->uploadingChunkFile = true; | |
$originalDestination = $this->destinationPath; | |
$this->destinationPath = $this->chunkTempDir; | |
$originalOverwrite = $this->overwrite; | |
$originalCompletedFilenames = $this->completedFilenames; | |
$this->overwrite = true; | |
$callback(); | |
$this->completedFilenames = $originalCompletedFilenames; | |
$this->overwrite = $originalOverwrite; | |
$this->destinationPath = $originalDestination; | |
$this->uploadingChunkFile = false; | |
} | |
/** | |
* Check if the uploaded file will be valid (as soon as it's uploaded) | |
* | |
* @param string $name | |
* @param int $size | |
* @return bool | |
* | |
*/ | |
protected function isValidFile($name, $size) { | |
$valid = false; | |
$fname = $this->wire('sanitizer')->name($name); | |
if(!$size) { | |
$valid = false; // no data | |
} else if($name[0] == '.') { | |
$valid = false; | |
} else if(!$this->isValidExtension($name)) { | |
$this->error( | |
"$fname - " . $this->_('Invalid file extension, please use one of:') . ' ' . | |
implode(', ', $this->validExtensions) | |
); | |
} else if($this->maxFileSize > 0 && $size > $this->maxFileSize) { | |
$this->error("$fname - " . $this->_('Exceeds max allowed file size')); | |
} else { | |
$valid = true; | |
} | |
return $valid; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment