This commit is contained in:
Xes
2025-08-14 22:37:50 +02:00
parent fb6d5d5926
commit 3641e93527
9156 changed files with 1813532 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
-------------------------------------------------------------------------
BigUpload
version 1.2
Created by: Sean Thielen <sean@p27.us>
[BigUpload: Uploading really big files in the browser](http://p27.us/2013/03/bigupload-uploading-really-big-files-in-the-browser/)
-------------------------------------------------------------------------
BigUpload is a tool for handling large file uploads (tested up to 2GB) through the browser.
![Screenshot](http://i.imgur.com/vESk5dp.png)
-------------------------------------------------------------------------
It uses the HTML5 FileReader library to split large files into manageable chunks,
and then sends these chunks to the server one at a time using an XmlHttpRequest.
The php script then pieces these chunks together into one large file.
Because the chunks are all the same size, it is easy to calculate an accurate progress bar
and a fairly accurate time remaining variable.
This tool is capable of handling file uploads of up to 2GB in size, without the need to tweak
the max_upload and timeout variables on your httpd.
This tool only works on Chrome and Firefox, but falls back to a normal file upload form on other browsers.
If you want to deploy this as-is, the variables you need to worry about are in the top of
* js/bigUpload.js
* inc/bigUpload.php
And you need to be sure to make /BigUpload/files and /BigUpload/files/tmp writeable
Please feel free to contribute and use this in your projects!
-------------------------------------------------------------------------
v 1.2
* Cleaned up the code quite a lot
* Added pause/resume functionality
* Added fallback for unsupported browsers
v 1.0.1
* Added time remaining calculator
* Response from php script is now a json object, allowing for error processing
* Minor script changes and bugfixes
* Better comments
v 1.0.0
* Initial version

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1,299 @@
<?php
require_once '../../../../global.inc.php';
require_once api_get_path(SYS_CODE_PATH).'work/work.lib.php';
class BigUploadResponse
{
/**
* Max allowed filesize. This is for unsupported browsers and
* as an additional security check in case someone bypasses the js filesize check.
*/
private $maxSize;
/**
* Temporary directory.
*
* @var string
*/
private $tempDirectory;
/**
* Name of the temporary file. Used as a reference to make sure chunks get written to the right file.
*
* @var string
*/
private $tempName;
/**
* Constructor function, sets the temporary directory and main directory.
*/
public function __construct()
{
$tempDirectory = api_get_path(SYS_ARCHIVE_PATH);
$this->setTempDirectory($tempDirectory);
$this->maxSize = getIniMaxFileSizeInBytes();
}
/**
* Create a random file name for the file to use as it's being uploaded.
*
* @param string $value Temporary filename
*/
public function setTempName($value = null)
{
if ($value) {
$this->tempName = $value;
} else {
if ('learnpath' === $_REQUEST['origin'] && !empty($_REQUEST['name'])) {
$this->tempName = disable_dangerous_file(
api_replace_dangerous_char($_REQUEST['name'])
);
} else {
$this->tempName = mt_rand().'.tmp';
}
}
}
/**
* Return the name of the temporary file.
*
* @return string Temporary filename
*/
public function getTempName()
{
return $this->tempName;
}
/**
* Set the name of the temporary directory.
*
* @param string $value Temporary directory
*/
public function setTempDirectory($value)
{
$this->tempDirectory = $value;
return true;
}
/**
* Return the name of the temporary directory.
*
* @return string Temporary directory
*/
public function getTempDirectory()
{
return $this->tempDirectory;
}
/**
* Function to upload the individual file chunks.
*
* @return string JSON object with result of upload
*/
public function uploadFile()
{
//Make sure the total file we're writing to hasn't surpassed the file size limit
if (file_exists($this->getTempDirectory().$this->getTempName())) {
if (filesize($this->getTempDirectory().$this->getTempName()) > $this->maxSize) {
$this->abortUpload();
return json_encode([
'errorStatus' => 1,
'errorText' => get_lang('UplFileTooBig'),
]);
}
}
//Open the raw POST data from php://input
$fileData = file_get_contents('php://input');
//Write the actual chunk to the larger file
$handle = fopen($this->getTempDirectory().$this->getTempName(), 'a');
fwrite($handle, $fileData);
fclose($handle);
return json_encode([
'key' => $this->getTempName(),
'errorStatus' => 0,
]);
}
/**
* Function for cancelling uploads while they're in-progress; deletes the temp file.
*
* @return string JSON object with result of deletion
*/
public function abortUpload()
{
if (unlink($this->getTempDirectory().$this->getTempName())) {
return json_encode(['errorStatus' => 0]);
} else {
return json_encode([
'errorStatus' => 1,
'errorText' => get_lang('UnableToDeleteTempFile'),
]);
}
}
/**
* Function to rename and move the finished file.
*
* @return string JSON object with result of rename
*/
public function finishUpload()
{
$tempName = $this->getTempName();
$sessionBigUpload = ChamiloSession::read('bigupload', []);
if (!isset($sessionBigUpload[$tempName])) {
return json_encode(
[
'errorStatus' => 1,
'errorText' => get_lang('UnableToDeleteTempFile'),
]
);
}
/** @var string $finalName Name to rename the finished upload to */
$finalName = $sessionBigUpload[$tempName];
$origin = $_POST['origin'];
if ($origin == 'document') {
$tmpFile = $this->getTempDirectory().$this->getTempName();
chmod($tmpFile, '0777');
$file = [
'name' => $finalName,
'type' => $_POST['type'],
'tmp_name' => $tmpFile,
'error' => 0,
'size' => $_POST['size'],
'copy_file' => true,
];
$files = ['file' => $file];
$unzip = isset($_POST['unzip']) ? $_POST['unzip'] : null;
$index = isset($_POST['index_document']) ? $_POST['index_document'] : null;
DocumentManager::upload_document(
$files,
$_POST['curdirpath'],
$_POST['title'],
$_POST['comment'],
$unzip,
$_POST['if_exists'],
$index,
true
);
$redirectUrl = api_get_path(WEB_CODE_PATH).'document/document.php?'.api_get_cidreq();
if (!empty($_POST['id'])) {
$redirectUrl .= '&'.http_build_query(
[
'id' => $_POST['id'],
]
);
}
return json_encode(['errorStatus' => 0, 'redirect' => $redirectUrl]);
} elseif ($origin == 'learnpath') {
unset($_REQUEST['origin']);
$redirectUrl = api_get_path(WEB_CODE_PATH).'upload/upload.php?'.api_get_cidreq().'&'
.http_build_query(
[
'from' => 'bigUpload',
'name' => $this->getTempName(),
'use_max_score' => $_POST['use_max_score'] ?? 0,
]
);
return json_encode(['errorStatus' => 0, 'redirect' => $redirectUrl]);
} elseif ($origin == 'work') {
$tmpFile = $this->getTempDirectory().$this->getTempName();
chmod($tmpFile, '0777');
$workInfo = get_work_data_by_id($_REQUEST['id']);
$values = $_REQUEST;
$courseInfo = api_get_course_info();
$sessionId = api_get_session_id();
$groupId = api_get_group_id();
$userId = api_get_user_id();
$values['contains_file'] = 1;
$values['title'] = $finalName;
$file = [
'name' => $finalName,
'type' => $_POST['type'],
'tmp_name' => $tmpFile,
'error' => 0,
'size' => $_POST['size'],
'copy_file' => true,
];
// Process work
$result = processWorkForm(
$workInfo,
$values,
$courseInfo,
$sessionId,
$groupId,
$userId,
$file,
api_get_configuration_value('assignment_prevent_duplicate_upload')
);
$extraParams = '';
if (!empty($_SESSION['oLP'])) {
$extraParams .= '&origin=learnpath';
}
$redirectUrl = api_get_path(WEB_CODE_PATH).'work/work.php?'.api_get_cidreq().$extraParams;
return json_encode(['errorStatus' => 0, 'redirect' => $redirectUrl]);
}
return json_encode(['errorStatus' => 0]);
}
}
$sessionBigUpload = ChamiloSession::read('bigupload', []);
//Instantiate the class
$bigUpload = new BigUploadResponse();
//Set the temporary filename
$tempName = null;
if (isset($_GET['key'])) {
$tempName = $_GET['key'];
}
if (isset($_POST['key'])) {
$tempName = $_POST['key'];
}
if (!empty($tempName)) {
$tempName = api_replace_dangerous_char($tempName);
$tempName = disable_dangerous_file($tempName);
}
$bigUpload->setTempName($tempName);
if (isset($_GET['name'])) {
$sessionBigUpload[$bigUpload->getTempName()] = disable_dangerous_file(
api_replace_dangerous_char($_GET['name'])
);
ChamiloSession::write('bigupload', $sessionBigUpload);
}
switch ($_GET['action']) {
case 'upload':
print $bigUpload->uploadFile();
break;
case 'abort':
print $bigUpload->abortUpload();
break;
case 'finish':
print $bigUpload->finishUpload();
if (isset($sessionBigUpload[$bigUpload->getTempName()])) {
unset($sessionBigUpload[$bigUpload->getTempName()]);
ChamiloSession::write('bigupload', $sessionBigUpload);
}
break;
}

View File

@@ -0,0 +1,347 @@
function bigUpload () {
//These are the main config variables and should be able to take care of most of the customization
this.settings = {
//The id of the file input
'inputField': 'bigUploadFile',
//The id of the form with the file upload.
//This should be a valid html form (see index.html) so there is a fallback for unsupported browsers
'formId': 'bigUploadForm',
//The id of the progress bar
//Width of this element will change based on progress
//Content of this element will display a percentage
//See bigUpload.progressUpdate() to change this code
'progressBarField': 'bigUploadProgressBarFilled',
//The id of the time remaining field
//Content of this element will display the estimated time remaining for the upload
//See bigUpload.progressUpdate() to change this code
'timeRemainingField': 'bigUploadTimeRemaining',
//The id of the text response field
//Content of this element will display the response from the server on success or error
'responseField': 'bigUploadResponse',
//The id of the submit button
//This is then changed to become the pause/resume button based on the status of the upload
'submitButton': 'bigUploadSubmit',
//Color of the background of the progress bar
//This must also be defined in the progressBarField css, but it's used here to reset the color after an error
//Default: green
'progressBarColor': '#5bb75b',
//Color of the background of the progress bar when an error is triggered
//Default: red
'progressBarColorError': '#da4f49',
//Path to the php script for handling the uploads
'scriptPath': 'inc/bigUpload.php',
//Size of chunks to upload (in bytes)
//Default: 1MB
'chunkSize': 1000000,
//Max file size allowed
//Default: 2GB
'maxFileSize': 2147483648,
//CidReq
'cidReq': '',
// Message error upload filesize
'errMessageFileSize': '',
};
//Upload specific variables
this.uploadData = {
'uploadStarted': false,
'file': false,
'numberOfChunks': 0,
'aborted': false,
'paused': false,
'pauseChunk': 0,
'key': 0,
'timeStart': 0,
'totalTime': 0
};
parent = this;
//Quick function for accessing objects
this.$ = function(id) {
return document.getElementById(id);
};
//Resets all the upload specific data before a new upload
this.resetKey = function() {
this.uploadData = {
'uploadStarted': false,
'file': false,
'numberOfChunks': 0,
'aborted': false,
'paused': false,
'pauseChunk': 0,
'key': 0,
'timeStart': 0,
'totalTime': 0
};
};
//Inital method called
//Determines whether to begin/pause/resume an upload based on whether or not one is already in progress
this.fire = function() {
if(this.uploadData.uploadStarted === true && this.uploadData.paused === false) {
this.pauseUpload();
}
else if(this.uploadData.uploadStarted === true && this.uploadData.paused === true) {
this.resumeUpload();
}
else {
this.processFiles();
}
};
//Initial upload method
//Pulls the size of the file being uploaded and calculated the number of chunks, then calls the recursive upload method
this.processFiles = function() {
//If the user is using an unsupported browser, the form just submits as a regular form
if(!Blob.prototype.slice) {
this.$(this.settings.formId).submit();
return;
}
//Reset the upload-specific variables
this.resetKey();
this.uploadData.uploadStarted = true;
//Some HTML tidying
//Reset the background color of the progress bar in case it was changed by any earlier errors
//Change the Upload button to a Pause button
this.$(this.settings.progressBarField).style.backgroundColor = this.settings.progressBarColor;
this.$(this.settings.progressBarField).style.width = '0%';
this.$(this.settings.progressBarField).textContent = '0%';
this.$(this.settings.responseField).textContent = '';
this.$(this.settings.submitButton).value = 'Pause';
this.$(this.settings.submitButton).disabled = true;
//Alias the file input object to this.uploadData
this.uploadData.file = this.$(this.settings.inputField).files[0];
//Check the filesize. Obviously this is not very secure, so it has another check in inc/bigUpload.php
//But this should be good enough to catch any immediate errors
var fileSize = this.uploadData.file.size;
if(fileSize > this.settings.maxFileSize) {
this.printResponse(this.settings.errMessageFileSize, true);
this.$(this.settings.submitButton).disabled = false;
return;
}
//Calculate the total number of file chunks
this.uploadData.numberOfChunks = Math.ceil(fileSize / this.settings.chunkSize);
//Start the upload
this.sendFile(0);
};
//Main upload method
this.sendFile = function (chunk) {
//Set the time for the beginning of the upload, used for calculating time remaining
this.uploadData.timeStart = new Date().getTime();
//Check if the upload has been cancelled by the user
if(this.uploadData.aborted === true) {
return;
}
//Check if the upload has been paused by the user
if(this.uploadData.paused === true) {
this.uploadData.pauseChunk = chunk;
this.printResponse('Upload paused.', false);
return;
}
//Set the byte to start uploading from and the byte to end uploading at
var start = chunk * this.settings.chunkSize;
var stop = start + this.settings.chunkSize;
//Initialize a new FileReader object
var reader = new FileReader();
reader.onloadend = function(evt) {
//Build the AJAX request
//
//this.uploadData.key is the temporary filename
//If the server sees it as 0 it will generate a new filename and pass it back in the JSON object
//this.uploadData.key is then populated with the filename to use for subsequent requests
//When this method sends a valid filename (i.e. key != 0), the server will just append the data being sent to that file.
xhr = new XMLHttpRequest();
xhr.open("POST", parent.settings.scriptPath + '?' + parent.settings.cidReq + '&action=upload' + '&key=' + parent.uploadData.key + '&origin=' + parent.settings.origin + (parent.uploadData.key ? '' : '&name=' + parent.uploadData.file.name), true);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function() {
if(xhr.readyState == 4) {
var response = JSON.parse(xhr.response);
//If there's an error, call the error method and break the loop
if(response.errorStatus !== 0 || xhr.status != 200) {
parent.printResponse(response.errorText, true);
return;
}
//If it's the first chunk, set this.uploadData.key to the server response (see above)
if(chunk === 0 || parent.uploadData.key === 0) {
parent.uploadData.key = response.key;
}
//If the file isn't done uploading, update the progress bar and run this.sendFile again for the next chunk
if(chunk < parent.uploadData.numberOfChunks) {
parent.progressUpdate(chunk + 1);
parent.sendFile(chunk + 1);
}
//If the file is complete uploaded, instantiate the finalizing method
else {
parent.sendFileData();
}
}
};
//Send the file chunk
xhr.send(blob);
};
//Slice the file into the desired chunk
//This is the core of the script. Everything else is just fluff.
var blob = this.uploadData.file.slice(start, stop);
reader.readAsBinaryString(blob);
};
//This method is for whatever housekeeping work needs to be completed after the file is finished uploading.
//As it's setup now, it passes along the original filename to the server and the server renames the file and removes it form the temp directory.
//This function could also pass things like this.uploadData.file.type for the mime-type (although it would be more accurate to use php for that)
//Or it could pass along user information or something like that, depending on the context of the application.
this.sendFileData = function() {
var data = 'key=' + this.uploadData.key + '&name=' + this.uploadData.file.name + '&type=' + this.uploadData.file.type + '&size=' + this.uploadData.file.size + '&origin=' + parent.settings.origin + '&' + parent.settings.formParams;
xhr = new XMLHttpRequest();
xhr.open("POST", parent.settings.scriptPath + '?' + parent.settings.cidReq + '&action=finish', true);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function() {
if(xhr.readyState == 4) {
var response = JSON.parse(xhr.response);
//If there's an error, call the error method
if(response.errorStatus !== 0 || xhr.status != 200) {
parent.printResponse(response.errorText, true);
return;
}
//Reset the upload-specific data so we can process another upload
parent.resetKey();
//Change the submit button text so it's ready for another upload and spit out a sucess message
parent.$(parent.settings.submitButton).value = 'Start Upload';
parent.printResponse('File uploaded successfully.', false);
if (response.redirect) {
location.href = response.redirect;
}
}
};
//Send the reques
xhr.send(data);
};
//This method cancels the upload of a file.
//It sets this.uploadData.aborted to true, which stops the recursive upload script.
//The server then removes the incomplete file from the temp directory, and the html displays an error message.
this.abortFileUpload = function() {
this.uploadData.aborted = true;
var data = 'key=' + this.uploadData.key;
xhr = new XMLHttpRequest();
xhr.open("POST", this.settings.scriptPath + '?' + this.settings.cidReq + '&action=abort', true);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function() {
if(xhr.readyState == 4) {
var response = JSON.parse(xhr.response);
//If there's an error, call the error method.
if(response.errorStatus !== 0 || xhr.status != 200) {
parent.printResponse(response.errorText, true);
return;
}
parent.printResponse('File upload was cancelled.', true);
}
};
//Send the request
xhr.send(data);
};
//Pause the upload
//Sets this.uploadData.paused to true, which breaks the upload loop.
//The current chunk is still stored in this.uploadData.pauseChunk, so the upload can later be resumed.
//In a production environment, you might want to have a cron job to clean up files that have been paused and never resumed,
//because this method won't delete the file from the temp directory if the user pauses and then leaves the page.
this.pauseUpload = function() {
this.uploadData.paused = true;
this.printResponse('', false);
this.$(this.settings.submitButton).value = 'Resume';
};
//Resume the upload
//Undoes the doings of this.pauseUpload and then re-enters the loop at the last chunk uploaded
this.resumeUpload = function() {
this.uploadData.paused = false;
this.$(this.settings.submitButton).value = 'Pause';
this.sendFile(this.uploadData.pauseChunk);
};
//This method updates a simple progress bar by calculating the percentage of chunks uploaded.
//Also includes a method to calculate the time remaining by taking the average time to upload individual chunks
//and multiplying it by the number of chunks remaining.
this.progressUpdate = function(progress) {
var percent = Math.ceil((progress / this.uploadData.numberOfChunks) * 100);
this.$(this.settings.progressBarField).style.width = percent + '%';
this.$(this.settings.progressBarField).textContent = percent + '%';
//Calculate the estimated time remaining
//Only run this every five chunks, otherwise the time remaining jumps all over the place (see: http://xkcd.com/612/)
if(progress % 5 === 0) {
//Calculate the total time for all of the chunks uploaded so far
this.uploadData.totalTime += (new Date().getTime() - this.uploadData.timeStart);
console.log(this.uploadData.totalTime);
//Estimate the time remaining by finding the average time per chunk upload and
//multiplying it by the number of chunks remaining, then convert into seconds
var timeLeft = Math.ceil((this.uploadData.totalTime / progress) * (this.uploadData.numberOfChunks - progress) / 100);
//Update this.settings.timeRemainingField with the estimated time remaining
this.$(this.settings.timeRemainingField).textContent = timeLeft + ' seconds remaining';
}
};
//Simple response/error handler
this.printResponse = function(responseText, error) {
this.$(this.settings.responseField).textContent = responseText;
this.$(this.settings.timeRemainingField).textContent = '';
if(error === true) {
this.$(this.settings.progressBarField).style.backgroundColor = this.settings.progressBarColorError;
}
};
}