forked from extern/egroupware
Switch AJAX upload library to Resumable for chunked uploads.
This commit is contained in:
parent
6bf826628b
commit
d3c0314b4d
@ -31,7 +31,6 @@ class etemplate_widget_file extends etemplate_widget
|
||||
{
|
||||
$this->setElementAttribute($this->id, 'multiple', true);
|
||||
}
|
||||
$this->setElementAttribute($this->id, 'max_file_size', egw_vfs::int_size(ini_get('upload_max_filesize')));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -113,6 +112,13 @@ class etemplate_widget_file extends etemplate_widget
|
||||
*/
|
||||
protected static function process_uploaded_file($field, Array &$file, $mime, Array &$file_data)
|
||||
{
|
||||
// Chunks get mangled a little
|
||||
if($file['name'] == 'blob' && $file['type'] == 'application/octet-stream')
|
||||
{
|
||||
$file['name'] = $_POST['resumableFilename'];
|
||||
$file['type'] = $_POST['resumableType'];
|
||||
}
|
||||
|
||||
if ($file['error'] == UPLOAD_ERR_OK && trim($file['name']) != '' && $file['size'] > 0 && is_uploaded_file($file['tmp_name'])) {
|
||||
// Mime check
|
||||
if($mime)
|
||||
@ -127,15 +133,31 @@ class etemplate_widget_file extends etemplate_widget
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (is_dir($GLOBALS['egw_info']['server']['temp_dir']) && is_writable($GLOBALS['egw_info']['server']['temp_dir']))
|
||||
|
||||
// Resumable / chunked uploads
|
||||
// init the destination file (format <filename.ext>.part<#chunk>
|
||||
// the file is stored in a temporary directory
|
||||
$temp_dir = $GLOBALS['egw_info']['server']['temp_dir'].'/'.str_replace('/','_',$_POST['resumableIdentifier']);
|
||||
$dest_file = $temp_dir.'/'.str_replace('/','_',$_POST['resumableFilename']).'.part'.(int)$_POST['resumableChunkNumber'];
|
||||
|
||||
// create the temporary directory
|
||||
if (!is_dir($temp_dir))
|
||||
{
|
||||
$new_file = tempnam($GLOBALS['egw_info']['server']['temp_dir'],'egw_');
|
||||
mkdir($temp_dir, 0755, true);
|
||||
}
|
||||
|
||||
// move the temporary file
|
||||
if (!move_uploaded_file($file['tmp_name'], $dest_file))
|
||||
{
|
||||
$file_date[$file['name']] = 'Error saving (move_uploaded_file) chunk '.(int)$_POST['resumableChunkNumber'].' for file '.$_POST['resumableFilename'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$new_file = $file['tmp_name'].'+';
|
||||
// check if all the parts present, and create the final destination file
|
||||
$new_file = self::createFileFromChunks($temp_dir, str_replace('/','_',$_POST['resumableFilename']),
|
||||
$_POST['resumableChunkSize'], $_POST['resumableTotalSize']);
|
||||
}
|
||||
if(move_uploaded_file($file['tmp_name'], $new_file)) {
|
||||
if( $new_file) {
|
||||
$file['tmp_name'] = $new_file;
|
||||
|
||||
// Data to send back to client
|
||||
@ -149,6 +171,86 @@ class etemplate_widget_file extends etemplate_widget
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Check if all the parts exist, and
|
||||
* gather all the parts of the file together
|
||||
*
|
||||
* From Resumable samples - http://resumablejs.com/
|
||||
* @param string $dir - the temporary directory holding all the parts of the file
|
||||
* @param string $fileName - the original file name
|
||||
* @param string $chunkSize - each chunk size (in bytes)
|
||||
* @param string $totalSize - original file size (in bytes)
|
||||
*/
|
||||
private static function createFileFromChunks($temp_dir, $fileName, $chunkSize, $totalSize) {
|
||||
|
||||
// count all the parts of this file
|
||||
$total_files = 0;
|
||||
foreach(scandir($temp_dir) as $file) {
|
||||
if (stripos($file, $fileName) !== false) {
|
||||
$total_files++;
|
||||
}
|
||||
}
|
||||
|
||||
// check that all the parts are present
|
||||
// the size of the last part is between chunkSize and 2*$chunkSize
|
||||
if ($total_files * $chunkSize >= ($totalSize - $chunkSize + 1)) {
|
||||
if (is_dir($GLOBALS['egw_info']['server']['temp_dir']) && is_writable($GLOBALS['egw_info']['server']['temp_dir']))
|
||||
{
|
||||
$new_file = tempnam($GLOBALS['egw_info']['server']['temp_dir'],'egw_');
|
||||
}
|
||||
else
|
||||
{
|
||||
$new_file = $file['tmp_name'].'+';
|
||||
}
|
||||
|
||||
// create the final destination file
|
||||
if (($fp = fopen($new_file, 'w')) !== false) {
|
||||
for ($i=1; $i<=$total_files; $i++) {
|
||||
fwrite($fp, file_get_contents($temp_dir.'/'.$fileName.'.part'.$i));
|
||||
}
|
||||
fclose($fp);
|
||||
} else {
|
||||
_log('cannot create the destination file');
|
||||
return false;
|
||||
}
|
||||
|
||||
// rename the temporary directory (to avoid access from other
|
||||
// concurrent chunks uploads) and than delete it
|
||||
if (rename($temp_dir, $temp_dir.'_UNUSED')) {
|
||||
self::rrmdir($temp_dir.'_UNUSED');
|
||||
} else {
|
||||
self::rrmdir($temp_dir);
|
||||
}
|
||||
|
||||
return $new_file;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a directory RECURSIVELY
|
||||
* @param string $dir - directory path
|
||||
* @link http://php.net/manual/en/function.rmdir.php
|
||||
*/
|
||||
private static function rrmdir($dir) {
|
||||
if (is_dir($dir)) {
|
||||
$objects = scandir($dir);
|
||||
foreach ($objects as $object) {
|
||||
if ($object != "." && $object != "..") {
|
||||
if (filetype($dir . "/" . $object) == "dir") {
|
||||
rrmdir($dir . "/" . $object);
|
||||
} else {
|
||||
unlink($dir . "/" . $object);
|
||||
}
|
||||
}
|
||||
}
|
||||
reset($objects);
|
||||
rmdir($dir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate input
|
||||
* Merge any already uploaded files into the content array
|
||||
|
@ -31,7 +31,6 @@ class etemplate_widget_link extends etemplate_widget
|
||||
if($xml) {
|
||||
parent::__construct($xml);
|
||||
}
|
||||
$this->setElementAttribute($this->id.'_file', 'max_file_size', egw_vfs::int_size(ini_get('upload_max_filesize')));
|
||||
}
|
||||
|
||||
/* Changes all link widgets to template
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
/*egw:uses
|
||||
et2_core_inputWidget;
|
||||
phpgwapi.jquery.jquery.html5_upload;
|
||||
phpgwapi.Resumable.resumable;
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -34,8 +34,8 @@ var et2_file = et2_inputWidget.extend(
|
||||
"max_file_size": {
|
||||
"name": "Maximum file size",
|
||||
"type": "integer",
|
||||
"default":"8388608",
|
||||
"description": "Largest file accepted, in bytes. Subject to server limitations. 8Mb = 8388608"
|
||||
"default":0,
|
||||
"description": "Largest file accepted, in bytes. Subject to server limitations. 8MB = 8388608"
|
||||
},
|
||||
"mime": {
|
||||
"name": "Allowed file types",
|
||||
@ -115,39 +115,24 @@ var et2_file = et2_inputWidget.extend(
|
||||
this.asyncOptions = jQuery.extend({
|
||||
// Callbacks
|
||||
onStart: function(event, file_count) {
|
||||
// Hide any previous errors
|
||||
self.hideMessage();
|
||||
return self.onStart(event, file_count);
|
||||
},
|
||||
onFinish: function(event, file_count) {
|
||||
if(self.onFinish.apply(self, [event, file_count]))
|
||||
{
|
||||
// Fire legacy change action when done
|
||||
self.change(self.input);
|
||||
}
|
||||
self.onFinish.apply(self, [event, file_count])
|
||||
},
|
||||
onStartOne: function(event, file_name, index, file_count) {
|
||||
// Here 'this' is the input
|
||||
if(self.checkMime(this.files[index]))
|
||||
{
|
||||
return self.createStatus(event,file_name, index,file_count);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Wrong mime type - show in the list of files
|
||||
return self.createStatus(
|
||||
self.egw().lang("File is of wrong type (%1 != %2)!", this.files[index].type, self.options.mime),
|
||||
file_name
|
||||
);
|
||||
}
|
||||
|
||||
},
|
||||
onFinishOne: function(event, response, name, number, total) { return self.finishUpload(event,response,name,number,total);},
|
||||
onProgress: function(event, progress, name, number, total) { return self.onProgress(event,progress,name,number,total);},
|
||||
onError: function(event, name, error) { return self.onError(event,name,error);},
|
||||
sendBoundary: window.FormData || jQuery.browser.mozilla,
|
||||
beforeSend: function(form) { return self.beforeSend(form);},
|
||||
url: egw.ajaxUrl(self.egw().getAppName()+".etemplate_widget_file.ajax_upload.etemplate"),
|
||||
autoclear: !(this.options.onchange)
|
||||
|
||||
|
||||
target: egw.ajaxUrl(self.egw().getAppName()+".etemplate_widget_file.ajax_upload.etemplate"),
|
||||
query: function(file) {return self.beforeSend(file);},
|
||||
// Disable checking for already uploaded chunks
|
||||
testChunks: false
|
||||
},this.asyncOptions);
|
||||
this.asyncOptions.fieldName = this.options.id;
|
||||
this.createInputWidget();
|
||||
@ -188,7 +173,12 @@ var et2_file = et2_inputWidget.extend(
|
||||
// Check for File interface, should fall back to normal form submit if missing
|
||||
if(typeof File != "undefined" && typeof (new XMLHttpRequest()).upload != "undefined")
|
||||
{
|
||||
this.input.html5_upload(this.asyncOptions);
|
||||
this.resumable = new Resumable(this.asyncOptions);
|
||||
this.resumable.assignBrowse(this.input);
|
||||
this.resumable.on('fileAdded', jQuery.proxy(this._fileAdded, this));
|
||||
this.resumable.on('fileProgress', jQuery.proxy(this._fileProgress, this));
|
||||
this.resumable.on('fileSuccess', jQuery.proxy(this.finishUpload, this));
|
||||
this.resumable.on('complete', jQuery.proxy(this.onFinish, this));
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -229,7 +219,10 @@ var et2_file = et2_inputWidget.extend(
|
||||
{
|
||||
var widget = this.getRoot().getWidgetById(this.options.drop_target);
|
||||
var drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target);
|
||||
$j(drop_target).off("."+this.id);
|
||||
if(drop_target)
|
||||
{
|
||||
this.resumable.unAssignDrop(drop_target);
|
||||
}
|
||||
}
|
||||
|
||||
this.options.drop_target = new_target;
|
||||
@ -241,48 +234,7 @@ var et2_file = et2_inputWidget.extend(
|
||||
var drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target);
|
||||
if(drop_target)
|
||||
{
|
||||
// Tell jQuery to include this property
|
||||
jQuery.event.props.push('dataTransfer');
|
||||
var self = this;
|
||||
drop_target.ondrop =function(event) {
|
||||
return false;
|
||||
};
|
||||
$j(drop_target)
|
||||
.on("drop."+this.id, jQuery.proxy(function(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.input.removeClass("ui-state-active");
|
||||
if(event.dataTransfer && event.dataTransfer.files.length > 0)
|
||||
{
|
||||
this.input[0].files = event.dataTransfer.files;
|
||||
}
|
||||
return false;
|
||||
}, this))
|
||||
.on("dragenter."+this.id,function(e) {
|
||||
// Accept the drop if at least one mimetype matches
|
||||
// Individual files will be rejected later
|
||||
var mime_ok = false;
|
||||
for(var file in e.dataTransfer.files)
|
||||
{
|
||||
mime_ok = mime_ok || self.checkMime(file);
|
||||
}
|
||||
if(!self.disabled && mime_ok)
|
||||
{
|
||||
self.input.addClass("ui-state-active");
|
||||
}
|
||||
// Returning true cancels, return false to allow
|
||||
return self.disabled || !mime_ok;
|
||||
})
|
||||
.on("dragleave."+this.id, function(e) {
|
||||
self.input.removeClass("ui-state-active");
|
||||
// Returning true cancels, return false to allow
|
||||
return self.disabled
|
||||
})
|
||||
.on("dragover."+this.id, function(e) {
|
||||
// Returning true cancels, return false to allow
|
||||
return self.disabled;
|
||||
})
|
||||
.on("dragend."+this.id, false);
|
||||
this.resumable.assignDrop(drop_target);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -396,27 +348,58 @@ var et2_file = et2_inputWidget.extend(
|
||||
return false;
|
||||
},
|
||||
|
||||
_fileAdded: function(file,event) {
|
||||
// Trigger start of uploading, calls callback
|
||||
if(!this.resumable.isUploading())
|
||||
{
|
||||
if (!(this.onStart(event,this.resumable.files.length))) return;
|
||||
}
|
||||
|
||||
// Here 'this' is the input
|
||||
if(this.checkMime(file.file))
|
||||
{
|
||||
if(this.createStatus(event,file))
|
||||
{
|
||||
// Actually start uploading
|
||||
this.resumable.upload();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Wrong mime type - show in the list of files
|
||||
return self.createStatus(
|
||||
self.egw().lang("File is of wrong type (%1 != %2)!", this.files[index].type, self.options.mime),
|
||||
file_name
|
||||
);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Add in the request id
|
||||
*/
|
||||
beforeSend: function(form) {
|
||||
var instance = this.getInstanceManager();
|
||||
// Form object available, mess with it directly
|
||||
if(form) {
|
||||
return form.append("request_id", instance.etemplate_exec_id);
|
||||
}
|
||||
|
||||
// No Form object, add in as string
|
||||
return 'Content-Disposition: form-data; name="request_id"\r\n\r\n'+instance.etemplate_exec_id+'\r\n';
|
||||
|
||||
return {
|
||||
request_id: instance.etemplate_exec_id,
|
||||
widget_id: this.id
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Disables submit buttons while uploading
|
||||
*/
|
||||
onStart: function(event, file_count) {
|
||||
// Hide any previous errors
|
||||
this.hideMessage();
|
||||
|
||||
// Disable buttons
|
||||
this.disabled_buttons = $j("input[type='submit'], button").not("[disabled]").attr("disabled", true);
|
||||
|
||||
event.data = this;
|
||||
|
||||
// Callback
|
||||
if(this.options.onStart) return et2_call(this.options.onStart, event, file_count);
|
||||
return true;
|
||||
},
|
||||
@ -424,14 +407,34 @@ var et2_file = et2_inputWidget.extend(
|
||||
/**
|
||||
* Re-enables submit buttons when done
|
||||
*/
|
||||
onFinish: function(event, file_count) {
|
||||
onFinish: function() {
|
||||
this.disabled_buttons.attr("disabled", false);
|
||||
|
||||
var file_count = this.resumable.files.length;
|
||||
|
||||
// Remove files from list
|
||||
for(var i = 0; i < this.resumable.files.length; i++)
|
||||
{
|
||||
this.resumable.removeFile(this.resumable.files[i]);
|
||||
}
|
||||
|
||||
var event = new Event('upload');
|
||||
event.data = this;
|
||||
|
||||
var result = false;
|
||||
if(this.options.onFinish && !jQuery.isEmptyObject(this.getValue()))
|
||||
{
|
||||
return et2_call(this.options.onFinish, event, file_count);
|
||||
result = et2_call(this.options.onFinish, event, file_count);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = (file_count == 0 || !jQuery.isEmptyObject(this.getValue()));
|
||||
}
|
||||
if(result)
|
||||
{
|
||||
// Fire legacy change action when done
|
||||
this.change(this.input);
|
||||
}
|
||||
return (file_count == 0 || !jQuery.isEmptyObject(this.getValue()));
|
||||
},
|
||||
|
||||
|
||||
@ -441,14 +444,13 @@ var et2_file = et2_inputWidget.extend(
|
||||
*
|
||||
* @param _event Either the event, or an error message
|
||||
*/
|
||||
createStatus: function(_event, file_name, index, file_count) {
|
||||
createStatus: function(_event, file) {
|
||||
var error = (typeof _event == "object" ? "" : _event);
|
||||
if(this.input[0].files[index]) {
|
||||
var file = this.input[0].files[index];
|
||||
if(file.size > this.options.max_file_size) {
|
||||
error = this.egw().lang("File too large. Maximum %1", et2_vfsSize.prototype.human_size(this.options.max_file_size));
|
||||
}
|
||||
|
||||
if(this.options.max_file_size && file.size > this.options.max_file_size) {
|
||||
error = this.egw().lang("File too large. Maximum %1", et2_vfsSize.prototype.human_size(this.options.max_file_size));
|
||||
}
|
||||
|
||||
if(this.options.progress)
|
||||
{
|
||||
var widget = this.getRoot().getWidgetById(this.options.progress);
|
||||
@ -460,10 +462,11 @@ var et2_file = et2_inputWidget.extend(
|
||||
}
|
||||
if(this.progress)
|
||||
{
|
||||
var status = $j("<li file='"+file_name+"'>"+file_name
|
||||
var fileName = file.fileName || 'file';
|
||||
var status = $j("<li data-file='"+fileName+"'>"+fileName
|
||||
+"<div class='remove'/><span class='progressBar'><p/></span></li>")
|
||||
.appendTo(this.progress);
|
||||
$j("div.remove",status).click(this, this.cancel);
|
||||
$j("div.remove",status).on('click', file, jQuery.proxy(this.cancel,this));
|
||||
if(error != "")
|
||||
{
|
||||
status.addClass("message ui-state-error");
|
||||
@ -474,10 +477,10 @@ var et2_file = et2_inputWidget.extend(
|
||||
return error == "";
|
||||
},
|
||||
|
||||
onProgress: function(event, percent, name, number, total) {
|
||||
_fileProgress: function(file) {
|
||||
if(this.progress)
|
||||
{
|
||||
$j("li[file='"+name+"'] > span.progressBar > p").css("width", Math.ceil(percent*100)+"%");
|
||||
$j("li[data-file='"+file.fileName+"'] > span.progressBar > p").css("width", Math.ceil(file.progress()*100)+"%");
|
||||
|
||||
}
|
||||
return true;
|
||||
@ -490,7 +493,9 @@ var et2_file = et2_inputWidget.extend(
|
||||
/**
|
||||
* A file upload is finished, update the UI
|
||||
*/
|
||||
finishUpload: function(event, response, name, number, total) {
|
||||
finishUpload: function(file, response) {
|
||||
var name = file.fileName || 'file';
|
||||
|
||||
if(typeof response == 'string') response = jQuery.parseJSON(response);
|
||||
if(response.response[0] && typeof response.response[0].data.length == 'undefined') {
|
||||
if(typeof this.options.value != 'object') this.options.value = {};
|
||||
@ -498,7 +503,7 @@ var et2_file = et2_inputWidget.extend(
|
||||
if(typeof response.response[0].data[key] == "string")
|
||||
{
|
||||
// Message from server - probably error
|
||||
$j("[file='"+name+"']",this.progress)
|
||||
$j("[data-file='"+name+"']",this.progress)
|
||||
.addClass("error")
|
||||
.css("display", "block")
|
||||
.text(response.response[0].data[key]);
|
||||
@ -508,21 +513,23 @@ var et2_file = et2_inputWidget.extend(
|
||||
this.options.value[key] = response.response[0].data[key];
|
||||
if(this.progress)
|
||||
{
|
||||
$j("[file='"+name+"']",this.progress).addClass("message success");
|
||||
$j("[data-file='"+name+"']",this.progress).addClass("message success");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (this.progress)
|
||||
{
|
||||
$j("[file='"+name+"']",this.progress)
|
||||
$j("[data-file='"+name+"']",this.progress)
|
||||
.addClass("ui-state-error")
|
||||
.css("display", "block")
|
||||
.text(this.egw().lang("Server error"));
|
||||
}
|
||||
|
||||
// Callback
|
||||
if(this.options.onFinishOne)
|
||||
{
|
||||
return et2_call(this.options.onFinishOne,event,response,name,number,total);
|
||||
return et2_call(this.options.onFinishOne,event,response,name);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
@ -530,18 +537,19 @@ var et2_file = et2_inputWidget.extend(
|
||||
/**
|
||||
* Remove a file from the list of values
|
||||
*/
|
||||
remove_file: function(filename)
|
||||
remove_file: function(file)
|
||||
{
|
||||
//console.info(filename);
|
||||
for(var key in this.options.value)
|
||||
{
|
||||
if(this.options.value[key].name == filename)
|
||||
if(this.options.value[key].name == file.fileName)
|
||||
{
|
||||
delete this.options.value[key];
|
||||
$j('[file="'+filename+'"]',this.node).remove();
|
||||
$j('[data-file="'+file.fileName+'"]',this.node).remove();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(!file.isComplete()) file.cancel();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -551,8 +559,10 @@ var et2_file = et2_inputWidget.extend(
|
||||
{
|
||||
e.preventDefault();
|
||||
// Look for file name in list
|
||||
var target = $j(e.target).parents("li.message");
|
||||
e.data.remove_file.apply(e.data,[target.attr("file")]);
|
||||
var target = $j(e.target).parents("li");
|
||||
|
||||
this.remove_file(e.data);
|
||||
|
||||
// In case it didn't make it to the list (error)
|
||||
target.remove();
|
||||
$j(e.target).remove();
|
||||
|
@ -449,7 +449,6 @@ div.et2_file {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
overflow: hidden;
|
||||
width: 150px;
|
||||
}
|
||||
.et2_file .progress {
|
||||
@ -466,9 +465,11 @@ div.et2_file {
|
||||
}
|
||||
/* Remove icon displayed when hovering */
|
||||
.et2_file .progress li div.remove {
|
||||
display: none;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.et2_file .progress li:hover div.remove {
|
||||
width: 16px;
|
||||
@ -844,6 +845,7 @@ div.message.floating {
|
||||
background-image: url(images/tick.png);
|
||||
background-repeat: no-repeat;
|
||||
padding-left: 20px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.message.hint {
|
||||
font-style: normal;
|
||||
|
@ -53,6 +53,10 @@ div.filemanager_navigation > label > input {
|
||||
input#filemanager-index_upload {
|
||||
width: 315px;
|
||||
}
|
||||
#filemanager-index-buttons div.et2_file .progress {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select file dialog
|
||||
|
20
phpgwapi/js/Resumable/MIT-LICENSE
Normal file
20
phpgwapi/js/Resumable/MIT-LICENSE
Normal file
@ -0,0 +1,20 @@
|
||||
Copyright (c) 2011, 23, http://www.23developer.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
172
phpgwapi/js/Resumable/README.md
Normal file
172
phpgwapi/js/Resumable/README.md
Normal file
@ -0,0 +1,172 @@
|
||||
## What is Resumable.js
|
||||
|
||||
Resumable.js is a JavaScript library providing multiple simultaneous, stable and resumable uploads via the [`HTML5 File API`](http://www.w3.org/TR/FileAPI/).
|
||||
|
||||
The library is designed to introduce fault-tolerance into the upload of large files through HTTP. This is done by splitting each file into small chunks. Then, whenever the upload of a chunk fails, uploading is retried until the procedure completes. This allows uploads to automatically resume uploading after a network connection is lost either locally or to the server. Additionally, it allows for users to pause, resume and even recover uploads without losing state because only the currently uploading chunks will be aborted, not the entire upload.
|
||||
|
||||
Resumable.js does not have any external dependencies other than the `HTML5 File API`. This is relied on for the ability to chunk files into smaller pieces. Currently, this means that support is limited to Firefox 4+, Chrome 11+ and Safari 6+.
|
||||
|
||||
Samples and examples are available in the `samples/` folder. Please push your own as Markdown to help document the project.
|
||||
|
||||
|
||||
## How can I use it?
|
||||
|
||||
A new `Resumable` object is created with information of what and where to post:
|
||||
|
||||
var r = new Resumable({
|
||||
target:'/api/photo/redeem-upload-token',
|
||||
query:{upload_token:'my_token'}
|
||||
});
|
||||
// Resumable.js isn't supported, fall back on a different method
|
||||
if(!r.support) location.href = '/some-old-crappy-uploader';
|
||||
|
||||
To allow files to be selected and drag-dropped, you need to assign a drop target and a DOM item to be clicked for browsing:
|
||||
|
||||
r.assignBrowse(document.getElementById('browseButton'));
|
||||
r.assignDrop(document.getElementById('dropTarget'));
|
||||
|
||||
After this, interaction with Resumable.js is done by listening to events:
|
||||
|
||||
r.on('fileAdded', function(file, event){
|
||||
...
|
||||
});
|
||||
r.on('fileSuccess', function(file, message){
|
||||
...
|
||||
});
|
||||
r.on('fileError', function(file, message){
|
||||
...
|
||||
});
|
||||
|
||||
## How do I set it up with my server?
|
||||
|
||||
Most of the magic for Resumable.js happens in the user's browser, but files still need to be reassembled from chunks on the server side. This should be a fairly simple task, which and can be achieved using any web framework or language that is capable of handling file uploads.
|
||||
|
||||
To handle the state of upload chunks, a number of extra parameters are sent along with all requests:
|
||||
|
||||
* `resumableChunkNumber`: The index of the chunk in the current upload. First chunk is `1` (no base-0 counting here).
|
||||
* `resumableTotalChunks`: The total number of chunks.
|
||||
* `resumableChunkSize`: The general chunk size. Using this value and `resumableTotalSize` you can calculate the total number of chunks. Please note that the size of the data received in the HTTP might be lower than `resumableChunkSize` of this for the last chunk for a file.
|
||||
* `resumableTotalSize`: The total file size.
|
||||
* `resumableIdentifier`: A unique identifier for the file contained in the request.
|
||||
* `resumableFilename`: The original file name (since a bug in Firefox results in the file name not being transmitted in chunk multipart posts).
|
||||
* `resumableRelativePath`: The file's relative path when selecting a directory (defaults to file name in all browsers except Chrome).
|
||||
|
||||
You should allow for the same chunk to be uploaded more than once; this isn't standard behaviour, but on an unstable network environment it could happen, and this case is exactly what Resumable.js is designed for.
|
||||
|
||||
For every request, you can confirm reception in HTTP status codes (can be change through the `permanentErrors` option):
|
||||
|
||||
* `200`: The chunk was accepted and correct. No need to re-upload.
|
||||
* `404`, `415`. `500`, `501`: The file for which the chunk was uploaded is not supported, cancel the entire upload.
|
||||
* _Anything else_: Something went wrong, but try reuploading the file.
|
||||
|
||||
## Handling GET (or `test()` requests)
|
||||
|
||||
Enabling the `testChunks` option will allow uploads to be resumed after browser restarts and even across browsers (in theory you could even run the same file upload across multiple tabs or different browsers). The `POST` data requests listed are required to use Resumable.js to receive data, but you can extend support by implementing a corresponding `GET` request with the same parameters:
|
||||
|
||||
* If this request returns a `200` HTTP code, the chunks is assumed to have been completed.
|
||||
* If the request returns anything else, the chunk will be uploaded in the standard fashion. (It is recommended to return *204 No Content* in these cases if possible to [avoid unwarrented notices in brower consoles](https://github.com/23/resumable.js/issues/160).)
|
||||
|
||||
After this is done and `testChunks` enabled, an upload can quickly catch up even after a browser restart by simply verifying already uploaded chunks that do not need to be uploaded again.
|
||||
|
||||
## Full documentation
|
||||
|
||||
### Resumable
|
||||
#### Configuration
|
||||
|
||||
The object is loaded with a configuation hash:
|
||||
|
||||
var r = new Resumable({opt1:'val', ...});
|
||||
|
||||
Available configuration options are:
|
||||
|
||||
* `target` The target URL for the multipart POST request (Default: `/`)
|
||||
* `chunkSize` The size in bytes of each uploaded chunk of data. The last uploaded chunk will be at least this size and up to two the size, see [Issue #51](https://github.com/23/resumable.js/issues/51) for details and reasons. (Default: `1*1024*1024`)
|
||||
* `forceChunkSize` Force all chunks to be less or equal than chunkSize. Otherwise, the last chunk will be greater than or equal to `chunkSize`. (Default: `false`)
|
||||
* `simultaneousUploads` Number of simultaneous uploads (Default: `3`)
|
||||
* `fileParameterName` The name of the multipart POST parameter to use for the file chunk (Default: `file`)
|
||||
* `query` Extra parameters to include in the multipart POST with data. This can be an object or a function. If a function, it will be passed a ResumableFile and a ResumableChunk object (Default: `{}`)
|
||||
* `headers` Extra headers to include in the multipart POST with data (Default: `{}`)
|
||||
* `method` Method to use when POSTing chunks to the server (`multipart` or `octet`) (Default: `multipart`)
|
||||
* `prioritizeFirstAndLastChunk` Prioritize first and last chunks of all files. This can be handy if you can determine if a file is valid for your service from only the first or last chunk. For example, photo or video meta data is usually located in the first part of a file, making it easy to test support from only the first chunk. (Default: `false`)
|
||||
* `testChunks` Make a GET request to the server for each chunks to see if it already exists. If implemented on the server-side, this will allow for upload resumes even after a browser crash or even a computer restart. (Default: `true`)
|
||||
* `preprocess` Optional function to process each chunk before testing & sending. Function is passed the chunk as parameter, and should call the `preprocessFinished` method on the chunk when finished. (Default: `null`)
|
||||
* `generateUniqueIdentifier` Override the function that generates unique identifiers for each file. (Default: `null`)
|
||||
* `maxFiles` Indicates how many files can be uploaded in a single session. Valid values are any positive integer and `undefined` for no limit. (Default: `undefined`)
|
||||
* `maxFilesErrorCallback(files, errorCount)` A function which displays the *please upload n file(s) at a time* message. (Default: displays an alert box with the message *Please n one file(s) at a time.*)
|
||||
* `minFileSize` The minimum allowed file size. (Default: `undefined`)
|
||||
* `minFileSizeErrorCallback(file, errorCount)` A function which displays an error a selected file is smaller than allowed. (Default: displays an alert for every bad file.)
|
||||
* `maxFileSize` The maximum allowed file size. (Default: `undefined`)
|
||||
* `maxFileSizeErrorCallback(file, errorCount)` A function which displays an error a selected file is larger than allowed. (Default: displays an alert for every bad file.)
|
||||
* `fileType` The file types allowed to upload. An empty array allow any file type. (Default: `[]`)
|
||||
* `fileTypeErrorCallback(file, errorCount)` A function which displays an error a selected file has type not allowed. (Default: displays an alert for every bad file.)
|
||||
* `maxChunkRetries` The maximum number of retries for a chunk before the upload is failed. Valid values are any positive integer and `undefined` for no limit. (Default: `undefined`)
|
||||
* `chunkRetryInterval` The number of milliseconds to wait before retrying a chunk on a non-permanent error. Valid values are any positive integer and `undefined` for immediate retry. (Default: `undefined`)
|
||||
* `withCredentials` Standard CORS requests do not send or set any cookies by default. In order to include cookies as part of the request, you need to set the `withCredentials` property to true. (Default: `false`)
|
||||
|
||||
#### Properties
|
||||
|
||||
* `.support` A boolean value indicator whether or not Resumable.js is supported by the current browser.
|
||||
* `.opts` A hash object of the configuration of the Resumable.js instance.
|
||||
* `.files` An array of `ResumableFile` file objects added by the user (see full docs for this object type below).
|
||||
|
||||
#### Methods
|
||||
|
||||
* `.assignBrowse(domNodes, isDirectory)` Assign a browse action to one or more DOM nodes. Pass in `true` to allow directories to be selected (Chrome only).
|
||||
* `.assignDrop(domNodes)` Assign one or more DOM nodes as a drop target.
|
||||
* `.on(event, callback)` Listen for event from Resumable.js (see below)
|
||||
* `.upload()` Start or resume uploading.
|
||||
* `.pause()` Pause uploading.
|
||||
* `.cancel()` Cancel upload of all `ResumableFile` objects and remove them from the list.
|
||||
* `.progress()` Returns a float between 0 and 1 indicating the current upload progress of all files.
|
||||
* `.isUploading()` Returns a boolean indicating whether or not the instance is currently uploading anything.
|
||||
* `.addFile(file)` Add a HTML5 File object to the list of files.
|
||||
* `.removeFile(file)` Cancel upload of a specific `ResumableFile` object on the list from the list.
|
||||
* `.getFromUniqueIdentifier(uniqueIdentifier)` Look up a `ResumableFile` object by its unique identifier.
|
||||
* `.getSize()` Returns the total size of the upload in bytes.
|
||||
|
||||
#### Events
|
||||
|
||||
* `.fileSuccess(file)` A specific file was completed.
|
||||
* `.fileProgress(file)` Uploading progressed for a specific file.
|
||||
* `.fileAdded(file, event)` A new file was added. Optionally, you can use the browser `event` object from when the file was added.
|
||||
* `.filesAdded(array)` New files were added.
|
||||
* `.fileRetry(file)` Something went wrong during upload of a specific file, uploading is being retried.
|
||||
* `.fileError(file, message)` An error occured during upload of a specific file.
|
||||
* `.uploadStart()` Upload has been started on the Resumable object.
|
||||
* `.complete()` Uploading completed.
|
||||
* `.progress()` Uploading progress.
|
||||
* `.error(message, file)` An error, including fileError, occured.
|
||||
* `.pause()` Uploading was paused.
|
||||
* `.cancel()` Uploading was canceled.
|
||||
* `.chunkingStart(file)` Started preparing file for upload
|
||||
* `.chunkingProgress(file,ratio)` Show progress in file preparation
|
||||
* `.chunkingComplete(file) File is ready for upload
|
||||
* `.catchAll(event, ...)` Listen to all the events listed above with the same callback function.
|
||||
|
||||
### ResumableFile
|
||||
#### Properties
|
||||
|
||||
* `.resumableObj` A back-reference to the parent `Resumable` object.
|
||||
* `.file` The correlating HTML5 `File` object.
|
||||
* `.fileName` The name of the file.
|
||||
* `.relativePath` The relative path to the file (defaults to file name if relative path doesn't exist)
|
||||
* `.size` Size in bytes of the file.
|
||||
* `.uniqueIdentifier` A unique identifier assigned to this file object. This value is included in uploads to the server for reference, but can also be used in CSS classes etc when building your upload UI.
|
||||
* `.chunks` An array of `ResumableChunk` items. You shouldn't need to dig into these.
|
||||
|
||||
#### Methods
|
||||
|
||||
* `.progress(relative)` Returns a float between 0 and 1 indicating the current upload progress of the file. If `relative` is `true`, the value is returned relative to all files in the Resumable.js instance.
|
||||
* `.abort()` Abort uploading the file.
|
||||
* `.cancel()` Abort uploading the file and delete it from the list of files to upload.
|
||||
* `.retry()` Retry uploading the file.
|
||||
* `.bootstrap()` Rebuild the state of a `ResumableFile` object, including reassigning chunks and XMLHttpRequest instances.
|
||||
* `.isUploading()` Returns a boolean indicating whether file chunks is uploading.
|
||||
* `.isComplete()` Returns a boolean indicating whether the file has completed uploading and received a server response.
|
||||
|
||||
## Alternatives
|
||||
|
||||
This library is explicitly designed for modern browsers supporting advanced HTML5 file features, and the motivation has been to provide stable and resumable support for large files (allowing uploads of several GB files through HTTP in a predictable fashion).
|
||||
|
||||
If your aim is just to support progress indications during upload/uploading multiple files at once, Resumable.js isn't for you. In those cases, [SWFUpload](http://swfupload.org/) and [Plupload](http://plupload.com/) provides the same features with wider browser support.
|
||||
|
14
phpgwapi/js/Resumable/bower.json
Normal file
14
phpgwapi/js/Resumable/bower.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "resumable.js",
|
||||
"version": "1.0.0",
|
||||
"main": "resumable.js",
|
||||
"ignore": [
|
||||
".gitignore",
|
||||
"*.md"
|
||||
],
|
||||
"keywords": [
|
||||
"HTML5 File API",
|
||||
"Upload",
|
||||
"Large files"
|
||||
]
|
||||
}
|
7
phpgwapi/js/Resumable/component.json
Normal file
7
phpgwapi/js/Resumable/component.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "resumable.js",
|
||||
"repo": "23/resumable.js",
|
||||
"version": "1.0.0",
|
||||
"main": "resumable.js",
|
||||
"scripts": ["resumable.js"]
|
||||
}
|
816
phpgwapi/js/Resumable/resumable.js
Normal file
816
phpgwapi/js/Resumable/resumable.js
Normal file
@ -0,0 +1,816 @@
|
||||
/*
|
||||
* MIT Licensed
|
||||
* http://www.23developer.com/opensource
|
||||
* http://github.com/23/resumable.js
|
||||
* Steffen Tiedemann Christensen, steffen@23company.com
|
||||
*/
|
||||
|
||||
(function(){
|
||||
"use strict";
|
||||
|
||||
var Resumable = function(opts){
|
||||
if ( !(this instanceof Resumable) ) {
|
||||
return new Resumable(opts);
|
||||
}
|
||||
this.version = 1.0;
|
||||
// SUPPORTED BY BROWSER?
|
||||
// Check if these features are support by the browser:
|
||||
// - File object type
|
||||
// - Blob object type
|
||||
// - FileList object type
|
||||
// - slicing files
|
||||
this.support = (
|
||||
(typeof(File)!=='undefined')
|
||||
&&
|
||||
(typeof(Blob)!=='undefined')
|
||||
&&
|
||||
(typeof(FileList)!=='undefined')
|
||||
&&
|
||||
(!!Blob.prototype.webkitSlice||!!Blob.prototype.mozSlice||!!Blob.prototype.slice||false)
|
||||
);
|
||||
if(!this.support) return(false);
|
||||
|
||||
|
||||
// PROPERTIES
|
||||
var $ = this;
|
||||
$.files = [];
|
||||
$.defaults = {
|
||||
chunkSize:1*1024*1024,
|
||||
forceChunkSize:false,
|
||||
simultaneousUploads:3,
|
||||
fileParameterName:'file',
|
||||
throttleProgressCallbacks:0.5,
|
||||
query:{},
|
||||
headers:{},
|
||||
preprocess:null,
|
||||
method:'multipart',
|
||||
prioritizeFirstAndLastChunk:false,
|
||||
target:'/',
|
||||
testChunks:true,
|
||||
generateUniqueIdentifier:null,
|
||||
maxChunkRetries:undefined,
|
||||
chunkRetryInterval:undefined,
|
||||
permanentErrors:[404, 415, 500, 501],
|
||||
maxFiles:undefined,
|
||||
withCredentials:false,
|
||||
xhrTimeout:0,
|
||||
maxFilesErrorCallback:function (files, errorCount) {
|
||||
var maxFiles = $.getOpt('maxFiles');
|
||||
alert('Please upload ' + maxFiles + ' file' + (maxFiles === 1 ? '' : 's') + ' at a time.');
|
||||
},
|
||||
minFileSize:1,
|
||||
minFileSizeErrorCallback:function(file, errorCount) {
|
||||
alert(file.fileName||file.name +' is too small, please upload files larger than ' + $h.formatSize($.getOpt('minFileSize')) + '.');
|
||||
},
|
||||
maxFileSize:undefined,
|
||||
maxFileSizeErrorCallback:function(file, errorCount) {
|
||||
alert(file.fileName||file.name +' is too large, please upload files less than ' + $h.formatSize($.getOpt('maxFileSize')) + '.');
|
||||
},
|
||||
fileType: [],
|
||||
fileTypeErrorCallback: function(file, errorCount) {
|
||||
alert(file.fileName||file.name +' has type not allowed, please upload files of type ' + $.getOpt('fileType') + '.');
|
||||
}
|
||||
};
|
||||
$.opts = opts||{};
|
||||
$.getOpt = function(o) {
|
||||
var $opt = this;
|
||||
// Get multiple option if passed an array
|
||||
if(o instanceof Array) {
|
||||
var options = {};
|
||||
$h.each(o, function(option){
|
||||
options[option] = $opt.getOpt(option);
|
||||
});
|
||||
return options;
|
||||
}
|
||||
// Otherwise, just return a simple option
|
||||
if ($opt instanceof ResumableChunk) {
|
||||
if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
|
||||
else { $opt = $opt.fileObj; }
|
||||
}
|
||||
if ($opt instanceof ResumableFile) {
|
||||
if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
|
||||
else { $opt = $opt.resumableObj; }
|
||||
}
|
||||
if ($opt instanceof Resumable) {
|
||||
if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
|
||||
else { return $opt.defaults[o]; }
|
||||
}
|
||||
};
|
||||
|
||||
// EVENTS
|
||||
// catchAll(event, ...)
|
||||
// fileSuccess(file), fileProgress(file), fileAdded(file, event), fileRetry(file), fileError(file, message),
|
||||
// complete(), progress(), error(message, file), pause()
|
||||
$.events = [];
|
||||
$.on = function(event,callback){
|
||||
$.events.push(event.toLowerCase(), callback);
|
||||
};
|
||||
$.fire = function(){
|
||||
// `arguments` is an object, not array, in FF, so:
|
||||
var args = [];
|
||||
for (var i=0; i<arguments.length; i++) args.push(arguments[i]);
|
||||
// Find event listeners, and support pseudo-event `catchAll`
|
||||
var event = args[0].toLowerCase();
|
||||
for (var i=0; i<=$.events.length; i+=2) {
|
||||
if($.events[i]==event) $.events[i+1].apply($,args.slice(1));
|
||||
if($.events[i]=='catchall') $.events[i+1].apply(null,args);
|
||||
}
|
||||
if(event=='fileerror') $.fire('error', args[2], args[1]);
|
||||
if(event=='fileprogress') $.fire('progress');
|
||||
};
|
||||
|
||||
|
||||
// INTERNAL HELPER METHODS (handy, but ultimately not part of uploading)
|
||||
var $h = {
|
||||
stopEvent: function(e){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
each: function(o,callback){
|
||||
if(typeof(o.length)!=='undefined') {
|
||||
for (var i=0; i<o.length; i++) {
|
||||
// Array or FileList
|
||||
if(callback(o[i])===false) return;
|
||||
}
|
||||
} else {
|
||||
for (i in o) {
|
||||
// Object
|
||||
if(callback(i,o[i])===false) return;
|
||||
}
|
||||
}
|
||||
},
|
||||
generateUniqueIdentifier:function(file){
|
||||
var custom = $.getOpt('generateUniqueIdentifier');
|
||||
if(typeof custom === 'function') {
|
||||
return custom(file);
|
||||
}
|
||||
var relativePath = file.webkitRelativePath||file.fileName||file.name; // Some confusion in different versions of Firefox
|
||||
var size = file.size;
|
||||
return(size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, ''));
|
||||
},
|
||||
contains:function(array,test) {
|
||||
var result = false;
|
||||
|
||||
$h.each(array, function(value) {
|
||||
if (value == test) {
|
||||
result = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
formatSize:function(size){
|
||||
if(size<1024) {
|
||||
return size + ' bytes';
|
||||
} else if(size<1024*1024) {
|
||||
return (size/1024.0).toFixed(0) + ' KB';
|
||||
} else if(size<1024*1024*1024) {
|
||||
return (size/1024.0/1024.0).toFixed(1) + ' MB';
|
||||
} else {
|
||||
return (size/1024.0/1024.0/1024.0).toFixed(1) + ' GB';
|
||||
}
|
||||
},
|
||||
getTarget:function(params){
|
||||
var target = $.getOpt('target');
|
||||
if(target.indexOf('?') < 0) {
|
||||
target += '?';
|
||||
} else {
|
||||
target += '&';
|
||||
}
|
||||
return target + params.join('&');
|
||||
}
|
||||
};
|
||||
|
||||
var onDrop = function(event){
|
||||
$h.stopEvent(event);
|
||||
appendFilesFromFileList(event.dataTransfer.files, event);
|
||||
};
|
||||
var onDragOver = function(e) {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// INTERNAL METHODS (both handy and responsible for the heavy load)
|
||||
var appendFilesFromFileList = function(fileList, event){
|
||||
// check for uploading too many files
|
||||
var errorCount = 0;
|
||||
var o = $.getOpt(['maxFiles', 'minFileSize', 'maxFileSize', 'maxFilesErrorCallback', 'minFileSizeErrorCallback', 'maxFileSizeErrorCallback', 'fileType', 'fileTypeErrorCallback']);
|
||||
if (typeof(o.maxFiles)!=='undefined' && o.maxFiles<(fileList.length+$.files.length)) {
|
||||
// if single-file upload, file is already added, and trying to add 1 new file, simply replace the already-added file
|
||||
if (o.maxFiles===1 && $.files.length===1 && fileList.length===1) {
|
||||
$.removeFile($.files[0]);
|
||||
} else {
|
||||
o.maxFilesErrorCallback(fileList, errorCount++);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
var files = [];
|
||||
$h.each(fileList, function(file){
|
||||
var fileName = file.name.split('.');
|
||||
var fileType = fileName[fileName.length-1].toLowerCase();
|
||||
|
||||
if (o.fileType.length > 0 && !$h.contains(o.fileType, fileType)) {
|
||||
o.fileTypeErrorCallback(file, errorCount++);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof(o.minFileSize)!=='undefined' && file.size<o.minFileSize) {
|
||||
o.minFileSizeErrorCallback(file, errorCount++);
|
||||
return false;
|
||||
}
|
||||
if (typeof(o.maxFileSize)!=='undefined' && file.size>o.maxFileSize) {
|
||||
o.maxFileSizeErrorCallback(file, errorCount++);
|
||||
return false;
|
||||
}
|
||||
|
||||
// directories have size == 0
|
||||
if (!$.getFromUniqueIdentifier($h.generateUniqueIdentifier(file))) {(function(){
|
||||
var f = new ResumableFile($, file);
|
||||
window.setTimeout(function(){
|
||||
$.files.push(f);
|
||||
files.push(f);
|
||||
f.container = event.srcElement;
|
||||
$.fire('fileAdded', f, event)
|
||||
},0);
|
||||
})()};
|
||||
});
|
||||
window.setTimeout(function(){
|
||||
$.fire('filesAdded', files)
|
||||
},0);
|
||||
};
|
||||
|
||||
// INTERNAL OBJECT TYPES
|
||||
function ResumableFile(resumableObj, file){
|
||||
var $ = this;
|
||||
$.opts = {};
|
||||
$.getOpt = resumableObj.getOpt;
|
||||
$._prevProgress = 0;
|
||||
$.resumableObj = resumableObj;
|
||||
$.file = file;
|
||||
$.fileName = file.fileName||file.name; // Some confusion in different versions of Firefox
|
||||
$.size = file.size;
|
||||
$.relativePath = file.webkitRelativePath || $.fileName;
|
||||
$.uniqueIdentifier = $h.generateUniqueIdentifier(file);
|
||||
$._pause = false;
|
||||
$.container = '';
|
||||
var _error = false;
|
||||
|
||||
// Callback when something happens within the chunk
|
||||
var chunkEvent = function(event, message){
|
||||
// event can be 'progress', 'success', 'error' or 'retry'
|
||||
switch(event){
|
||||
case 'progress':
|
||||
$.resumableObj.fire('fileProgress', $);
|
||||
break;
|
||||
case 'error':
|
||||
$.abort();
|
||||
_error = true;
|
||||
$.chunks = [];
|
||||
$.resumableObj.fire('fileError', $, message);
|
||||
break;
|
||||
case 'success':
|
||||
if(_error) return;
|
||||
$.resumableObj.fire('fileProgress', $); // it's at least progress
|
||||
if($.isComplete()) {
|
||||
$.resumableObj.fire('fileSuccess', $, message);
|
||||
}
|
||||
break;
|
||||
case 'retry':
|
||||
$.resumableObj.fire('fileRetry', $);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Main code to set up a file object with chunks,
|
||||
// packaged to be able to handle retries if needed.
|
||||
$.chunks = [];
|
||||
$.abort = function(){
|
||||
// Stop current uploads
|
||||
var abortCount = 0;
|
||||
$h.each($.chunks, function(c){
|
||||
if(c.status()=='uploading') {
|
||||
c.abort();
|
||||
abortCount++;
|
||||
}
|
||||
});
|
||||
if(abortCount>0) $.resumableObj.fire('fileProgress', $);
|
||||
};
|
||||
$.cancel = function(){
|
||||
// Reset this file to be void
|
||||
var _chunks = $.chunks;
|
||||
$.chunks = [];
|
||||
// Stop current uploads
|
||||
$h.each(_chunks, function(c){
|
||||
if(c.status()=='uploading') {
|
||||
c.abort();
|
||||
$.resumableObj.uploadNextChunk();
|
||||
}
|
||||
});
|
||||
$.resumableObj.removeFile($);
|
||||
$.resumableObj.fire('fileProgress', $);
|
||||
};
|
||||
$.retry = function(){
|
||||
$.bootstrap();
|
||||
var firedRetry = false;
|
||||
$.resumableObj.on('chunkingComplete', function(){
|
||||
if(!firedRetry) $.resumableObj.upload();
|
||||
firedRetry = true;
|
||||
});
|
||||
};
|
||||
$.bootstrap = function(){
|
||||
$.abort();
|
||||
_error = false;
|
||||
// Rebuild stack of chunks from file
|
||||
$.chunks = [];
|
||||
$._prevProgress = 0;
|
||||
var round = $.getOpt('forceChunkSize') ? Math.ceil : Math.floor;
|
||||
var maxOffset = Math.max(round($.file.size/$.getOpt('chunkSize')),1);
|
||||
for (var offset=0; offset<maxOffset; offset++) {(function(offset){
|
||||
window.setTimeout(function(){
|
||||
$.chunks.push(new ResumableChunk($.resumableObj, $, offset, chunkEvent));
|
||||
$.resumableObj.fire('chunkingProgress',$,offset/maxOffset);
|
||||
},0);
|
||||
})(offset)}
|
||||
window.setTimeout(function(){
|
||||
$.resumableObj.fire('chunkingComplete',$);
|
||||
},0);
|
||||
};
|
||||
$.progress = function(){
|
||||
if(_error) return(1);
|
||||
// Sum up progress across everything
|
||||
var ret = 0;
|
||||
var error = false;
|
||||
$h.each($.chunks, function(c){
|
||||
if(c.status()=='error') error = true;
|
||||
ret += c.progress(true); // get chunk progress relative to entire file
|
||||
});
|
||||
ret = (error ? 1 : (ret>0.999 ? 1 : ret));
|
||||
ret = Math.max($._prevProgress, ret); // We don't want to lose percentages when an upload is paused
|
||||
$._prevProgress = ret;
|
||||
return(ret);
|
||||
};
|
||||
$.isUploading = function(){
|
||||
var uploading = false;
|
||||
$h.each($.chunks, function(chunk){
|
||||
if(chunk.status()=='uploading') {
|
||||
uploading = true;
|
||||
return(false);
|
||||
}
|
||||
});
|
||||
return(uploading);
|
||||
};
|
||||
$.isComplete = function(){
|
||||
var outstanding = false;
|
||||
$h.each($.chunks, function(chunk){
|
||||
var status = chunk.status();
|
||||
if(status=='pending' || status=='uploading' || chunk.preprocessState === 1) {
|
||||
outstanding = true;
|
||||
return(false);
|
||||
}
|
||||
});
|
||||
return(!outstanding);
|
||||
};
|
||||
$.pause = function(pause){
|
||||
if(typeof(pause)==='undefined'){
|
||||
$._pause = ($._pause ? false : true);
|
||||
}else{
|
||||
$._pause = pause;
|
||||
}
|
||||
};
|
||||
$.isPaused = function() {
|
||||
return $._pause;
|
||||
};
|
||||
|
||||
|
||||
// Bootstrap and return
|
||||
$.resumableObj.fire('chunkingStart', $);
|
||||
$.bootstrap();
|
||||
return(this);
|
||||
}
|
||||
|
||||
function ResumableChunk(resumableObj, fileObj, offset, callback){
|
||||
var $ = this;
|
||||
$.opts = {};
|
||||
$.getOpt = resumableObj.getOpt;
|
||||
$.resumableObj = resumableObj;
|
||||
$.fileObj = fileObj;
|
||||
$.fileObjSize = fileObj.size;
|
||||
$.fileObjType = fileObj.file.type;
|
||||
$.offset = offset;
|
||||
$.callback = callback;
|
||||
$.lastProgressCallback = (new Date);
|
||||
$.tested = false;
|
||||
$.retries = 0;
|
||||
$.pendingRetry = false;
|
||||
$.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished
|
||||
|
||||
// Computed properties
|
||||
var chunkSize = $.getOpt('chunkSize');
|
||||
$.loaded = 0;
|
||||
$.startByte = $.offset*chunkSize;
|
||||
$.endByte = Math.min($.fileObjSize, ($.offset+1)*chunkSize);
|
||||
if ($.fileObjSize-$.endByte < chunkSize && !$.getOpt('forceChunkSize')) {
|
||||
// The last chunk will be bigger than the chunk size, but less than 2*chunkSize
|
||||
$.endByte = $.fileObjSize;
|
||||
}
|
||||
$.xhr = null;
|
||||
|
||||
// test() makes a GET request without any data to see if the chunk has already been uploaded in a previous session
|
||||
$.test = function(){
|
||||
// Set up request and listen for event
|
||||
$.xhr = new XMLHttpRequest();
|
||||
|
||||
var testHandler = function(e){
|
||||
$.tested = true;
|
||||
var status = $.status();
|
||||
if(status=='success') {
|
||||
$.callback(status, $.message());
|
||||
$.resumableObj.uploadNextChunk();
|
||||
} else {
|
||||
$.send();
|
||||
}
|
||||
};
|
||||
$.xhr.addEventListener('load', testHandler, false);
|
||||
$.xhr.addEventListener('error', testHandler, false);
|
||||
|
||||
// Add data from the query options
|
||||
var params = [];
|
||||
var customQuery = $.getOpt('query');
|
||||
if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $);
|
||||
$h.each(customQuery, function(k,v){
|
||||
params.push([encodeURIComponent(k), encodeURIComponent(v)].join('='));
|
||||
});
|
||||
// Add extra data to identify chunk
|
||||
params.push(['resumableChunkNumber', encodeURIComponent($.offset+1)].join('='));
|
||||
params.push(['resumableChunkSize', encodeURIComponent($.getOpt('chunkSize'))].join('='));
|
||||
params.push(['resumableCurrentChunkSize', encodeURIComponent($.endByte - $.startByte)].join('='));
|
||||
params.push(['resumableTotalSize', encodeURIComponent($.fileObjSize)].join('='));
|
||||
params.push(['resumableType', encodeURIComponent($.fileObjType)].join('='));
|
||||
params.push(['resumableIdentifier', encodeURIComponent($.fileObj.uniqueIdentifier)].join('='));
|
||||
params.push(['resumableFilename', encodeURIComponent($.fileObj.fileName)].join('='));
|
||||
params.push(['resumableRelativePath', encodeURIComponent($.fileObj.relativePath)].join('='));
|
||||
// Append the relevant chunk and send it
|
||||
$.xhr.open('GET', $h.getTarget(params));
|
||||
$.xhr.timeout = $.getOpt('xhrTimeout');
|
||||
$.xhr.withCredentials = $.getOpt('withCredentials');
|
||||
// Add data from header options
|
||||
$h.each($.getOpt('headers'), function(k,v) {
|
||||
$.xhr.setRequestHeader(k, v);
|
||||
});
|
||||
$.xhr.send(null);
|
||||
};
|
||||
|
||||
$.preprocessFinished = function(){
|
||||
$.preprocessState = 2;
|
||||
$.send();
|
||||
};
|
||||
|
||||
// send() uploads the actual data in a POST call
|
||||
$.send = function(){
|
||||
var preprocess = $.getOpt('preprocess');
|
||||
if(typeof preprocess === 'function') {
|
||||
switch($.preprocessState) {
|
||||
case 0: preprocess($); $.preprocessState = 1; return;
|
||||
case 1: return;
|
||||
case 2: break;
|
||||
}
|
||||
}
|
||||
if($.getOpt('testChunks') && !$.tested) {
|
||||
$.test();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up request and listen for event
|
||||
$.xhr = new XMLHttpRequest();
|
||||
|
||||
// Progress
|
||||
$.xhr.upload.addEventListener('progress', function(e){
|
||||
if( (new Date) - $.lastProgressCallback > $.getOpt('throttleProgressCallbacks') * 1000 ) {
|
||||
$.callback('progress');
|
||||
$.lastProgressCallback = (new Date);
|
||||
}
|
||||
$.loaded=e.loaded||0;
|
||||
}, false);
|
||||
$.loaded = 0;
|
||||
$.pendingRetry = false;
|
||||
$.callback('progress');
|
||||
|
||||
// Done (either done, failed or retry)
|
||||
var doneHandler = function(e){
|
||||
var status = $.status();
|
||||
if(status=='success'||status=='error') {
|
||||
$.callback(status, $.message());
|
||||
$.resumableObj.uploadNextChunk();
|
||||
} else {
|
||||
$.callback('retry', $.message());
|
||||
$.abort();
|
||||
$.retries++;
|
||||
var retryInterval = $.getOpt('chunkRetryInterval');
|
||||
if(retryInterval !== undefined) {
|
||||
$.pendingRetry = true;
|
||||
setTimeout($.send, retryInterval);
|
||||
} else {
|
||||
$.send();
|
||||
}
|
||||
}
|
||||
};
|
||||
$.xhr.addEventListener('load', doneHandler, false);
|
||||
$.xhr.addEventListener('error', doneHandler, false);
|
||||
|
||||
// Set up the basic query data from Resumable
|
||||
var query = {
|
||||
resumableChunkNumber: $.offset+1,
|
||||
resumableChunkSize: $.getOpt('chunkSize'),
|
||||
resumableCurrentChunkSize: $.endByte - $.startByte,
|
||||
resumableTotalSize: $.fileObjSize,
|
||||
resumableType: $.fileObjType,
|
||||
resumableIdentifier: $.fileObj.uniqueIdentifier,
|
||||
resumableFilename: $.fileObj.fileName,
|
||||
resumableRelativePath: $.fileObj.relativePath,
|
||||
resumableTotalChunks: $.fileObj.chunks.length
|
||||
};
|
||||
// Mix in custom data
|
||||
var customQuery = $.getOpt('query');
|
||||
if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $);
|
||||
$h.each(customQuery, function(k,v){
|
||||
query[k] = v;
|
||||
});
|
||||
|
||||
var func = ($.fileObj.file.slice ? 'slice' : ($.fileObj.file.mozSlice ? 'mozSlice' : ($.fileObj.file.webkitSlice ? 'webkitSlice' : 'slice'))),
|
||||
bytes = $.fileObj.file[func]($.startByte,$.endByte),
|
||||
data = null,
|
||||
target = $.getOpt('target');
|
||||
|
||||
if ($.getOpt('method') === 'octet') {
|
||||
// Add data from the query options
|
||||
data = bytes;
|
||||
var params = [];
|
||||
$h.each(query, function(k,v){
|
||||
params.push([encodeURIComponent(k), encodeURIComponent(v)].join('='));
|
||||
});
|
||||
target = $h.getTarget(params);
|
||||
} else {
|
||||
// Add data from the query options
|
||||
data = new FormData();
|
||||
$h.each(query, function(k,v){
|
||||
data.append(k,v);
|
||||
});
|
||||
data.append($.getOpt('fileParameterName'), bytes);
|
||||
}
|
||||
|
||||
$.xhr.open('POST', target);
|
||||
$.xhr.timeout = $.getOpt('xhrTimeout');
|
||||
$.xhr.withCredentials = $.getOpt('withCredentials');
|
||||
// Add data from header options
|
||||
$h.each($.getOpt('headers'), function(k,v) {
|
||||
$.xhr.setRequestHeader(k, v);
|
||||
});
|
||||
$.xhr.send(data);
|
||||
};
|
||||
$.abort = function(){
|
||||
// Abort and reset
|
||||
if($.xhr) $.xhr.abort();
|
||||
$.xhr = null;
|
||||
};
|
||||
$.status = function(){
|
||||
// Returns: 'pending', 'uploading', 'success', 'error'
|
||||
if($.pendingRetry) {
|
||||
// if pending retry then that's effectively the same as actively uploading,
|
||||
// there might just be a slight delay before the retry starts
|
||||
return('uploading')
|
||||
} else if(!$.xhr) {
|
||||
return('pending');
|
||||
} else if($.xhr.readyState<4) {
|
||||
// Status is really 'OPENED', 'HEADERS_RECEIVED' or 'LOADING' - meaning that stuff is happening
|
||||
return('uploading');
|
||||
} else {
|
||||
if($.xhr.status==200) {
|
||||
// HTTP 200, perfect
|
||||
return('success');
|
||||
} else if($h.contains($.getOpt('permanentErrors'), $.xhr.status) || $.retries >= $.getOpt('maxChunkRetries')) {
|
||||
// HTTP 415/500/501, permanent error
|
||||
return('error');
|
||||
} else {
|
||||
// this should never happen, but we'll reset and queue a retry
|
||||
// a likely case for this would be 503 service unavailable
|
||||
$.abort();
|
||||
return('pending');
|
||||
}
|
||||
}
|
||||
};
|
||||
$.message = function(){
|
||||
return($.xhr ? $.xhr.responseText : '');
|
||||
};
|
||||
$.progress = function(relative){
|
||||
if(typeof(relative)==='undefined') relative = false;
|
||||
var factor = (relative ? ($.endByte-$.startByte)/$.fileObjSize : 1);
|
||||
if($.pendingRetry) return(0);
|
||||
var s = $.status();
|
||||
switch(s){
|
||||
case 'success':
|
||||
case 'error':
|
||||
return(1*factor);
|
||||
case 'pending':
|
||||
return(0*factor);
|
||||
default:
|
||||
return($.loaded/($.endByte-$.startByte)*factor);
|
||||
}
|
||||
};
|
||||
return(this);
|
||||
}
|
||||
|
||||
// QUEUE
|
||||
$.uploadNextChunk = function(){
|
||||
var found = false;
|
||||
|
||||
// In some cases (such as videos) it's really handy to upload the first
|
||||
// and last chunk of a file quickly; this let's the server check the file's
|
||||
// metadata and determine if there's even a point in continuing.
|
||||
if ($.getOpt('prioritizeFirstAndLastChunk')) {
|
||||
$h.each($.files, function(file){
|
||||
if(file.chunks.length && file.chunks[0].status()=='pending' && file.chunks[0].preprocessState === 0) {
|
||||
file.chunks[0].send();
|
||||
found = true;
|
||||
return(false);
|
||||
}
|
||||
if(file.chunks.length>1 && file.chunks[file.chunks.length-1].status()=='pending' && file.chunks[file.chunks.length-1].preprocessState === 0) {
|
||||
file.chunks[file.chunks.length-1].send();
|
||||
found = true;
|
||||
return(false);
|
||||
}
|
||||
});
|
||||
if(found) return(true);
|
||||
}
|
||||
|
||||
// Now, simply look for the next, best thing to upload
|
||||
$h.each($.files, function(file){
|
||||
if(file.isPaused()===false){
|
||||
$h.each(file.chunks, function(chunk){
|
||||
if(chunk.status()=='pending' && chunk.preprocessState === 0) {
|
||||
chunk.send();
|
||||
found = true;
|
||||
return(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
if(found) return(false);
|
||||
});
|
||||
if(found) return(true);
|
||||
|
||||
// The are no more outstanding chunks to upload, check is everything is done
|
||||
var outstanding = false;
|
||||
$h.each($.files, function(file){
|
||||
if(!file.isComplete()) {
|
||||
outstanding = true;
|
||||
return(false);
|
||||
}
|
||||
});
|
||||
if(!outstanding) {
|
||||
// All chunks have been uploaded, complete
|
||||
$.fire('complete');
|
||||
}
|
||||
return(false);
|
||||
};
|
||||
|
||||
|
||||
// PUBLIC METHODS FOR RESUMABLE.JS
|
||||
$.assignBrowse = function(domNodes, isDirectory){
|
||||
if(typeof(domNodes.length)=='undefined') domNodes = [domNodes];
|
||||
|
||||
$h.each(domNodes, function(domNode) {
|
||||
var input;
|
||||
if(domNode.tagName==='INPUT' && domNode.type==='file'){
|
||||
input = domNode;
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.setAttribute('type', 'file');
|
||||
input.style.display = 'none';
|
||||
domNode.addEventListener('click', function(){
|
||||
input.style.opacity = 0;
|
||||
input.style.display='block';
|
||||
input.focus();
|
||||
input.click();
|
||||
input.style.display='none';
|
||||
}, false);
|
||||
domNode.appendChild(input);
|
||||
}
|
||||
var maxFiles = $.getOpt('maxFiles');
|
||||
if (typeof(maxFiles)==='undefined'||maxFiles!=1){
|
||||
input.setAttribute('multiple', 'multiple');
|
||||
} else {
|
||||
input.removeAttribute('multiple');
|
||||
}
|
||||
if(isDirectory){
|
||||
input.setAttribute('webkitdirectory', 'webkitdirectory');
|
||||
} else {
|
||||
input.removeAttribute('webkitdirectory');
|
||||
}
|
||||
// When new files are added, simply append them to the overall list
|
||||
input.addEventListener('change', function(e){
|
||||
appendFilesFromFileList(e.target.files,e);
|
||||
e.target.value = '';
|
||||
}, false);
|
||||
});
|
||||
};
|
||||
$.assignDrop = function(domNodes){
|
||||
if(typeof(domNodes.length)=='undefined') domNodes = [domNodes];
|
||||
|
||||
$h.each(domNodes, function(domNode) {
|
||||
domNode.addEventListener('dragover', onDragOver, false);
|
||||
domNode.addEventListener('drop', onDrop, false);
|
||||
});
|
||||
};
|
||||
$.unAssignDrop = function(domNodes) {
|
||||
if (typeof(domNodes.length) == 'undefined') domNodes = [domNodes];
|
||||
|
||||
$h.each(domNodes, function(domNode) {
|
||||
domNode.removeEventListener('dragover', onDragOver);
|
||||
domNode.removeEventListener('drop', onDrop);
|
||||
});
|
||||
};
|
||||
$.isUploading = function(){
|
||||
var uploading = false;
|
||||
$h.each($.files, function(file){
|
||||
if (file.isUploading()) {
|
||||
uploading = true;
|
||||
return(false);
|
||||
}
|
||||
});
|
||||
return(uploading);
|
||||
};
|
||||
$.upload = function(){
|
||||
// Make sure we don't start too many uploads at once
|
||||
if($.isUploading()) return;
|
||||
// Kick off the queue
|
||||
$.fire('uploadStart');
|
||||
for (var num=1; num<=$.getOpt('simultaneousUploads'); num++) {
|
||||
$.uploadNextChunk();
|
||||
}
|
||||
};
|
||||
$.pause = function(){
|
||||
// Resume all chunks currently being uploaded
|
||||
$h.each($.files, function(file){
|
||||
file.abort();
|
||||
});
|
||||
$.fire('pause');
|
||||
};
|
||||
$.cancel = function(){
|
||||
for(var i = $.files.length - 1; i >= 0; i--) {
|
||||
$.files[i].cancel();
|
||||
}
|
||||
$.fire('cancel');
|
||||
};
|
||||
$.progress = function(){
|
||||
var totalDone = 0;
|
||||
var totalSize = 0;
|
||||
// Resume all chunks currently being uploaded
|
||||
$h.each($.files, function(file){
|
||||
totalDone += file.progress()*file.size;
|
||||
totalSize += file.size;
|
||||
});
|
||||
return(totalSize>0 ? totalDone/totalSize : 0);
|
||||
};
|
||||
$.addFile = function(file){
|
||||
appendFilesFromFileList([file]);
|
||||
};
|
||||
$.removeFile = function(file){
|
||||
for(var i = $.files.length - 1; i >= 0; i--) {
|
||||
if($.files[i] === file) {
|
||||
$.files.splice(i, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
$.getFromUniqueIdentifier = function(uniqueIdentifier){
|
||||
var ret = false;
|
||||
$h.each($.files, function(f){
|
||||
if(f.uniqueIdentifier==uniqueIdentifier) ret = f;
|
||||
});
|
||||
return(ret);
|
||||
};
|
||||
$.getSize = function(){
|
||||
var totalSize = 0;
|
||||
$h.each($.files, function(file){
|
||||
totalSize += file.size;
|
||||
});
|
||||
return(totalSize);
|
||||
};
|
||||
|
||||
return(this);
|
||||
};
|
||||
|
||||
|
||||
// Node.js-style export for Node and Component
|
||||
if (typeof module != 'undefined') {
|
||||
module.exports = Resumable;
|
||||
} else if (typeof define === "function" && define.amd) {
|
||||
// AMD/requirejs: Define the module
|
||||
define(function(){
|
||||
return Resumable;
|
||||
});
|
||||
} else {
|
||||
// Browser: Expose to window
|
||||
window.Resumable = Resumable;
|
||||
}
|
||||
|
||||
})();
|
51
phpgwapi/js/Resumable/test.html
Normal file
51
phpgwapi/js/Resumable/test.html
Normal file
@ -0,0 +1,51 @@
|
||||
<a href="#" id="browseButton">Select files</a>
|
||||
|
||||
<script src="resumable.js"></script>
|
||||
<script>
|
||||
var r = new Resumable({
|
||||
target:'test.html'
|
||||
});
|
||||
|
||||
r.assignBrowse(document.getElementById('browseButton'));
|
||||
|
||||
r.on('fileSuccess', function(file){
|
||||
console.debug(file);
|
||||
});
|
||||
r.on('fileProgress', function(file){
|
||||
console.debug(file);
|
||||
});
|
||||
r.on('fileAdded', function(file, event){
|
||||
r.upload();
|
||||
//console.debug(file, event);
|
||||
});
|
||||
r.on('filesAdded', function(array){
|
||||
//console.debug(array);
|
||||
});
|
||||
r.on('fileRetry', function(file){
|
||||
//console.debug(file);
|
||||
});
|
||||
r.on('fileError', function(file, message){
|
||||
//console.debug(file, message);
|
||||
});
|
||||
r.on('uploadStart', function(){
|
||||
//console.debug();
|
||||
});
|
||||
r.on('complete', function(){
|
||||
//console.debug();
|
||||
});
|
||||
r.on('progress', function(){
|
||||
//console.debug();
|
||||
});
|
||||
r.on('error', function(message, file){
|
||||
//console.debug(message, file);
|
||||
});
|
||||
r.on('pause', function(){
|
||||
//console.debug();
|
||||
});
|
||||
r.on('cancel', function(){
|
||||
//console.debug();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
@ -1,242 +0,0 @@
|
||||
/**
|
||||
* jquery-html5-upload
|
||||
*
|
||||
* @author Mikhail Dektyarev <mihail.dektyarow@gmail.com>
|
||||
* @link https://github.com/mihaild/jquery-html5-upload
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
jQuery.fn.html5_upload = function(options) {
|
||||
|
||||
var available_events = ['onStart', 'onStartOne', 'onProgress', 'onFinishOne', 'onFinish', 'onError'];
|
||||
var options = jQuery.extend({
|
||||
onStart: function(event, total) {
|
||||
return true;
|
||||
},
|
||||
onStartOne: function(event, name, number, total) {
|
||||
return true;
|
||||
},
|
||||
onProgress: function(event, progress, name, number, total) {
|
||||
},
|
||||
onFinishOne: function(event, response, name, number, total) {
|
||||
},
|
||||
onFinish: function(event, total) {
|
||||
},
|
||||
onError: function(event, name, error) {
|
||||
},
|
||||
onBrowserIncompatible: function() {
|
||||
alert("Sorry, but your browser is incompatible with uploading files using HTML5 (at least, with current preferences.\n Please install the latest version of Firefox, Safari or Chrome");
|
||||
},
|
||||
autostart: true,
|
||||
autoclear: true,
|
||||
stopOnFirstError: false,
|
||||
sendBoundary: false,
|
||||
fieldName: 'user_file[]',//ignore if sendBoundary is false
|
||||
method: 'post',
|
||||
|
||||
STATUSES: {
|
||||
'STARTED': 'Запуск',
|
||||
'PROGRESS': 'Загрузка',
|
||||
'LOADED': 'Обработка',
|
||||
'FINISHED': 'Завершено'
|
||||
},
|
||||
headers: {
|
||||
"Cache-Control":"no-cache",
|
||||
"X-Requested-With":"XMLHttpRequest",
|
||||
"X-File-Name": function(file){return encodeURIComponent(file.fileName ? file.fileName : file.name);},
|
||||
"X-File-Size": function(file){return file.fileSize ? file.fileSize : file.size;},
|
||||
"Content-Type": function(file){
|
||||
if (!options.sendBoundary) return 'multipart/form-data';
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
setName: function(text) {},
|
||||
setStatus: function(text) {},
|
||||
setProgress: function(value) {},
|
||||
|
||||
genName: function(file, number, total) {
|
||||
return file + "(" + (number+1) + " из " + total + ")";
|
||||
},
|
||||
genStatus: function(progress, finished) {
|
||||
if (finished) {
|
||||
return options.STATUSES['FINISHED'];
|
||||
}
|
||||
if (progress == 0) {
|
||||
return options.STATUSES['STARTED'];
|
||||
}
|
||||
else if (progress == 1) {
|
||||
return options.STATUSES['LOADED'];
|
||||
}
|
||||
else {
|
||||
return options.STATUSES['PROGRESS'];
|
||||
}
|
||||
},
|
||||
genProgress: function(loaded, total) {
|
||||
return loaded / total;
|
||||
}
|
||||
}, options);
|
||||
|
||||
function upload() {
|
||||
var files = this.files;
|
||||
var total = files.length;
|
||||
var $this = $(this);
|
||||
if (!$this.triggerHandler('onStart.html5_upload', [total])) {
|
||||
return false;
|
||||
}
|
||||
this.disabled = true;
|
||||
var uploaded = 0;
|
||||
var xhr = this.html5_upload['xhr'];
|
||||
this.html5_upload['continue_after_abort'] = true;
|
||||
function upload_file(number) {
|
||||
if (number == total) {
|
||||
$this.trigger('onFinish.html5_upload', [total]);
|
||||
options.setStatus(options.genStatus(1, true));
|
||||
$this.attr("disabled", false);
|
||||
if (options.autoclear) {
|
||||
$this.val("");
|
||||
}
|
||||
return;
|
||||
}
|
||||
var file = files[number];
|
||||
var fileName = file.fileName ? file.fileName : file.name;
|
||||
var fileSize = file.fileSize ? file.fileSize : file.size;
|
||||
if (!$this.triggerHandler('onStartOne.html5_upload', [fileName, number, total])) {
|
||||
return upload_file(number+1);
|
||||
}
|
||||
options.setStatus(options.genStatus(0));
|
||||
options.setName(options.genName(fileName, number, total));
|
||||
options.setProgress(options.genProgress(0, fileSize));
|
||||
xhr.upload['onprogress'] = function(rpe) {
|
||||
$this.trigger('onProgress.html5_upload', [rpe.loaded / rpe.total, fileName, number, total]);
|
||||
options.setStatus(options.genStatus(rpe.loaded / rpe.total));
|
||||
options.setProgress(options.genProgress(rpe.loaded, rpe.total));
|
||||
};
|
||||
xhr.onload = function(load) {
|
||||
$this.trigger('onFinishOne.html5_upload', [xhr.responseText, fileName, number, total]);
|
||||
options.setStatus(options.genStatus(1, true));
|
||||
options.setProgress(options.genProgress(fileSize, fileSize));
|
||||
upload_file(number+1);
|
||||
};
|
||||
xhr.onabort = function() {
|
||||
if ($this[0].html5_upload['continue_after_abort']) {
|
||||
upload_file(number+1);
|
||||
}
|
||||
else {
|
||||
$this.attr("disabled", false);
|
||||
if (options.autoclear) {
|
||||
$this.val("");
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.onerror = function(e) {
|
||||
$this.trigger('onError.html5_upload', [fileName, e]);
|
||||
if (!options.stopOnFirstError) {
|
||||
upload_file(number+1);
|
||||
}
|
||||
};
|
||||
xhr.open(options.method, typeof(options.url) == "function" ? options.url(number) : options.url, true);
|
||||
$.each(options.headers,function(key,val){
|
||||
val = typeof(val) == "function" ? val(file) : val; // resolve value
|
||||
if (val === false) return true; // if resolved value is boolean false, do not send this header
|
||||
xhr.setRequestHeader(key, val);
|
||||
});
|
||||
|
||||
if (!options.sendBoundary) {
|
||||
xhr.send(file);
|
||||
}
|
||||
else {
|
||||
if (window.FormData) {//Many thanks to scottt.tw
|
||||
var f = new FormData();
|
||||
f.append(typeof(options.fieldName) == "function" ? options.fieldName() : options.fieldName, file);
|
||||
if(typeof(options.beforeSend) == "function") { options.beforeSend(f);} // Give eGW a chance to interfere
|
||||
xhr.send(f);
|
||||
}
|
||||
else if (file.getAsBinary) {//Thanks to jm.schelcher
|
||||
var boundary = '------multipartformboundary' + (new Date).getTime();
|
||||
var dashdash = '--';
|
||||
var crlf = '\r\n';
|
||||
|
||||
/* Build RFC2388 string. */
|
||||
var builder = '';
|
||||
|
||||
builder += dashdash;
|
||||
builder += boundary;
|
||||
builder += crlf;
|
||||
|
||||
builder += 'Content-Disposition: form-data; name="'+(typeof(options.fieldName) == "function" ? options.fieldName() : options.fieldName)+'"';
|
||||
|
||||
//thanks to oyejo...@gmail.com for this fix
|
||||
fileName = unescape(encodeURIComponent(fileName)); //encode_utf8
|
||||
|
||||
builder += '; filename="' + fileName + '"';
|
||||
builder += crlf;
|
||||
|
||||
builder += 'Content-Type: application/octet-stream';
|
||||
builder += crlf;
|
||||
builder += crlf;
|
||||
|
||||
/* Append binary data. */
|
||||
builder += file.getAsBinary();
|
||||
builder += crlf;
|
||||
|
||||
// Give eGW a chance to interfere
|
||||
if(typeof(options.beforeSend) == "function") {
|
||||
builder += dashdash;
|
||||
builder += boundary;
|
||||
builder += crlf;
|
||||
|
||||
builder+=options.beforeSend();
|
||||
builder += crlf;
|
||||
}
|
||||
|
||||
/* Write boundary. */
|
||||
builder += dashdash;
|
||||
builder += boundary;
|
||||
builder += dashdash;
|
||||
builder += crlf;
|
||||
|
||||
xhr.setRequestHeader('content-type', 'multipart/form-data; boundary=' + boundary);
|
||||
xhr.sendAsBinary(builder);
|
||||
}
|
||||
else {
|
||||
options.onBrowserIncompatible();
|
||||
}
|
||||
}
|
||||
}
|
||||
upload_file(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.each(function() {
|
||||
this.html5_upload = {
|
||||
xhr: new XMLHttpRequest(),
|
||||
continue_after_abort: true
|
||||
};
|
||||
if (options.autostart) {
|
||||
$(this).bind('change', upload);
|
||||
}
|
||||
for (event in available_events) {
|
||||
if (options[available_events[event]]) {
|
||||
$(this).bind(available_events[event]+".html5_upload", options[available_events[event]]);
|
||||
}
|
||||
}
|
||||
$(this)
|
||||
.bind('start.html5_upload', upload)
|
||||
.bind('cancelOne.html5_upload', function() {
|
||||
this.html5_upload['xhr'].abort();
|
||||
})
|
||||
.bind('cancelAll.html5_upload', function() {
|
||||
this.html5_upload['continue_after_abort'] = false;
|
||||
this.html5_upload['xhr'].abort();
|
||||
})
|
||||
.bind('destroy.html5_upload', function() {
|
||||
this.html5_upload['continue_after_abort'] = false;
|
||||
this.xhr.abort();
|
||||
delete this.html5_upload;
|
||||
$(this).unbind('.html5_upload').unbind('change', upload);
|
||||
});
|
||||
});
|
||||
};
|
||||
})(jQuery);
|
Loading…
Reference in New Issue
Block a user