mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-26 01:43:47 +01:00
Async file uploads
This commit is contained in:
parent
0607a2c9a8
commit
8875c94c24
88
etemplate/inc/class.etemplate_widget_file.inc.php
Normal file
88
etemplate/inc/class.etemplate_widget_file.inc.php
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* EGroupware - eTemplate serverside file upload widget
|
||||||
|
*
|
||||||
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||||
|
* @package etemplate
|
||||||
|
* @subpackage api
|
||||||
|
* @link http://www.egroupware.org
|
||||||
|
* @author Nathan Gray
|
||||||
|
* @copyright 2011 Nathan Gray
|
||||||
|
* @version $Id$
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* eTemplate file upload widget
|
||||||
|
* Uses AJAX to send file(s) to server, and stores for submit
|
||||||
|
*/
|
||||||
|
class etemplate_widget_file extends etemplate_widget
|
||||||
|
{
|
||||||
|
|
||||||
|
public function __construct($xml='') {
|
||||||
|
if($xml) parent::__construct($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajax callback to receive an incoming file
|
||||||
|
*
|
||||||
|
* The incoming file is moved from its temporary location (otherwise server will delete it) and
|
||||||
|
* the file information is stored into the widget's value. When the form is submitted, the information for all
|
||||||
|
* files uploaded is available in the returned $content array. Because files are uploaded asynchronously,
|
||||||
|
* submission should be quick.
|
||||||
|
*
|
||||||
|
* @note Currently, no attempt is made to clean up files automatically.
|
||||||
|
*/
|
||||||
|
public static function ajax_upload() {
|
||||||
|
$response = egw_json_response::get();
|
||||||
|
$request_id = str_replace(' ', '+', rawurldecode($_REQUEST['request_id']));
|
||||||
|
$widget_id = $_REQUEST['widget_id'];
|
||||||
|
if(!self::$request = etemplate_request::read($request_id)) {
|
||||||
|
$response->error("Could not read session");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach ($_FILES as $field => $file) {
|
||||||
|
if ($file['error'] == UPLOAD_ERR_OK) {
|
||||||
|
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 = $value['file']['tmp_name'].'+';
|
||||||
|
}
|
||||||
|
// Files come from ajax Base64 encoded
|
||||||
|
|
||||||
|
$handle = fopen($new_file, 'w');
|
||||||
|
list($prefix, $data) = explode(',', file_get_contents($file['tmp_name']));
|
||||||
|
$file['tmp_name'] = $new_file;
|
||||||
|
fwrite($handle, base64_decode($data));
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
// Store info for future submit
|
||||||
|
$data = egw_session::appsession($request_id.'_files');
|
||||||
|
$form_name = self::form_name($cname, $field);
|
||||||
|
$data[$form_name][] = $file;
|
||||||
|
egw_session::appsession($request_id.'_files','',$data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate input
|
||||||
|
* Merge any already uploaded files into the content array
|
||||||
|
*
|
||||||
|
* @param string $cname current namespace
|
||||||
|
* @param array $content
|
||||||
|
* @param array &$validated=array() validated content
|
||||||
|
*/
|
||||||
|
public function validate($cname, array $content, &$validated=array())
|
||||||
|
{
|
||||||
|
$form_name = self::form_name($cname, $this->id);
|
||||||
|
$value = $value_in = self::get_array($content, $form_name);
|
||||||
|
$valid =& self::get_array($validated, $form_name, true);
|
||||||
|
|
||||||
|
$files = egw_session::appsession(self::$request->id().'_files');
|
||||||
|
$valid = $files[$form_name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
etemplate_widget::registerWidget('etemplate_widget_file', array('file'));
|
@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
/*egw:uses
|
/*egw:uses
|
||||||
et2_core_inputWidget;
|
et2_core_inputWidget;
|
||||||
|
phpgwapi.jquery.jquery.html5_upload;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,11 +49,27 @@ var et2_file = et2_inputWidget.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
asyncOptions: {},
|
||||||
|
|
||||||
init: function() {
|
init: function() {
|
||||||
this._super.apply(this, arguments)
|
this._super.apply(this, arguments)
|
||||||
|
|
||||||
this.node = null;
|
this.node = null;
|
||||||
|
|
||||||
|
// Set up the URL to have the request ID & the widget ID
|
||||||
|
var instance = this.getInstanceManager();
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
this.asyncOptions = {
|
||||||
|
// Callbacks
|
||||||
|
onStartOne: function(event, file_name) { return self.createStatus(event,file_name);},
|
||||||
|
onFinishOne: function(event, response, name, number, total) { return self.finishUpload(event,response,name,number,total);},
|
||||||
|
|
||||||
|
sendBoundary: window.FormData || jQuery.browser.mozilla,
|
||||||
|
url: egw_json_request.prototype._assembleAjaxUrl("etemplate_widget_file::ajax_upload") +
|
||||||
|
"&request_id="+ instance.etemplate_exec_id
|
||||||
|
};
|
||||||
|
this.asyncOptions.fieldName = this.options.id;
|
||||||
this.createInputWidget();
|
this.createInputWidget();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -60,8 +77,13 @@ var et2_file = et2_inputWidget.extend({
|
|||||||
this.node = $j(document.createElement("div")).addClass("et2_file");
|
this.node = $j(document.createElement("div")).addClass("et2_file");
|
||||||
this.input = $j(document.createElement("input"))
|
this.input = $j(document.createElement("input"))
|
||||||
.attr("type", "file").attr("placeholder", this.options.blur)
|
.attr("type", "file").attr("placeholder", this.options.blur)
|
||||||
.appendTo(this.node)
|
.appendTo(this.node);
|
||||||
.change(this, this.change);
|
|
||||||
|
// 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.progress = this.options.progress ?
|
this.progress = this.options.progress ?
|
||||||
$j(document.getElementById(this.options.progress)) :
|
$j(document.getElementById(this.options.progress)) :
|
||||||
$j(document.createElement("div")).appendTo(this.node);
|
$j(document.createElement("div")).appendTo(this.node);
|
||||||
@ -78,52 +100,26 @@ var et2_file = et2_inputWidget.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User has selected some files file, send them off
|
* Creates the elements used for displaying the file, and it's upload status, and
|
||||||
|
* attaches them to the DOM
|
||||||
*/
|
*/
|
||||||
change: function(e) {
|
createStatus: function(event, file_name) {
|
||||||
console.log(e);
|
if(this.progress)
|
||||||
if(!e.target.files || e.target.files.length == 0) return;
|
{
|
||||||
|
$j("<li file='"+file_name+"'>"+file_name+"<div class='progressBar'><p/></div></li>").appendTo(this.progress);
|
||||||
for(var i = 0; i < e.target.files.length; i++) {
|
|
||||||
var file = e.target.files[i];
|
|
||||||
|
|
||||||
// Add to list
|
|
||||||
var status = this.createStatus(file);
|
|
||||||
this.progress.append(status);
|
|
||||||
|
|
||||||
// Check if OK
|
|
||||||
if(file.size > this.options.max_file_size) {
|
|
||||||
// TODO: Try to slice into acceptable pieces
|
|
||||||
status.addClass("ui-state-error");
|
|
||||||
this.showMessage(egw.lang("File is too large."), "validation_error");
|
|
||||||
var self = this;
|
|
||||||
window.setTimeout(function() {self.hideMessage(true); status.remove();}, 5000);
|
|
||||||
}
|
}
|
||||||
}
|
return true;
|
||||||
|
|
||||||
// Reset field
|
|
||||||
e.data.input.attr("type", "input").attr("type","file");
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload a single file asyncronously
|
* A file upload is finished, update the UI
|
||||||
*/
|
*/
|
||||||
_upload_file: function(file) {
|
finishUpload: function(event, response, name, number, total) {
|
||||||
// Start upload
|
if(this.progress)
|
||||||
console.log(this, file);
|
{
|
||||||
|
$j("[file='"+name+"']",this.progress).addClass("upload_finished");
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the elements used for displaying the file, and it's upload status
|
|
||||||
*
|
|
||||||
* @param file A JS File object
|
|
||||||
* @return a jQuery object with the elements
|
|
||||||
*/
|
|
||||||
createStatus: function(file) {
|
|
||||||
return $j("<li>"+file.name+"<div class='progressBar'><p/></div></li>");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
et2_register_widget(et2_file, ["file"]);
|
et2_register_widget(et2_file, ["file"]);
|
||||||
|
@ -190,6 +190,30 @@ span.et2_date span {
|
|||||||
font-size: 9pt;
|
font-size: 9pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File upload
|
||||||
|
*/
|
||||||
|
.et2_file .progress {
|
||||||
|
max-height: 6em;
|
||||||
|
overflow: auto;
|
||||||
|
margin-left: 20px;
|
||||||
|
|
||||||
|
-moz-column-count: 4;
|
||||||
|
-moz-column-gap: 20px;
|
||||||
|
-webkit-column-count: 4;
|
||||||
|
-webkit-column-gap: 20px;
|
||||||
|
column-count: 4;
|
||||||
|
column-gap: 20px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.et2_file .progress li {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
.et2_file .progress li.upload_finished {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
.egw_tooltip
|
.egw_tooltip
|
||||||
{
|
{
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -16,8 +16,18 @@ include('../../../header.inc.php');
|
|||||||
|
|
||||||
egw_framework::validate_file('.','etemplate2','etemplate');
|
egw_framework::validate_file('.','etemplate2','etemplate');
|
||||||
egw_framework::validate_file('jquery','jquery.tools.min','phpgwapi'); // Not needed once JS require works for files like this
|
egw_framework::validate_file('jquery','jquery.tools.min','phpgwapi'); // Not needed once JS require works for files like this
|
||||||
|
egw_framework::validate_file('jquery','jquery.html5_upload','phpgwapi'); // Not needed once JS require works for files like this
|
||||||
egw_framework::includeCSS('/etemplate/js/test/test.css');
|
egw_framework::includeCSS('/etemplate/js/test/test.css');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Test using any actual template
|
||||||
|
*/
|
||||||
|
// $template = 'etemplate.et2_test_file_upload';
|
||||||
|
if($template) {
|
||||||
|
$etemplate = new etemplate_new('etemplate.et2_test_file_upload');
|
||||||
|
$etemplate->exec('',array());
|
||||||
|
return;
|
||||||
|
}
|
||||||
common::egw_header();
|
common::egw_header();
|
||||||
// parse_navbar();
|
// parse_navbar();
|
||||||
?>
|
?>
|
||||||
|
222
phpgwapi/js/jquery/jquery.html5_upload.js
Normal file
222
phpgwapi/js/jquery/jquery.html5_upload.js
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
(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 file.fileName},
|
||||||
|
"X-File-Size": function(file){return file.fileSize},
|
||||||
|
"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('html5_upload.onStart', [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('html5_upload.onFinish', [total]);
|
||||||
|
options.setStatus(options.genStatus(1, true));
|
||||||
|
$this.attr("disabled", false);
|
||||||
|
if (options.autoclear) {
|
||||||
|
$this.val("");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var file = files[number];
|
||||||
|
if (!$this.triggerHandler('html5_upload.onStartOne', [file.fileName, number, total])) {
|
||||||
|
return upload_file(number+1);
|
||||||
|
}
|
||||||
|
options.setStatus(options.genStatus(0));
|
||||||
|
options.setName(options.genName(file.fileName, number, total));
|
||||||
|
options.setProgress(options.genProgress(0, file.fileSize));
|
||||||
|
xhr.upload['onprogress'] = function(rpe) {
|
||||||
|
$this.trigger('html5_upload.onProgress', [rpe.loaded / rpe.total, file.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('html5_upload.onFinishOne', [xhr.responseText, file.fileName, number, total]);
|
||||||
|
options.setStatus(options.genStatus(1, true));
|
||||||
|
options.setProgress(options.genProgress(file.fileSize, file.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('html5_upload.onError', [file.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);
|
||||||
|
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(file.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;
|
||||||
|
|
||||||
|
/* 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("html5_upload."+available_events[event], options[available_events[event]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$(this)
|
||||||
|
.bind('html5_upload.start', upload)
|
||||||
|
.bind('html5_upload.cancelOne', function() {
|
||||||
|
this.html5_upload['xhr'].abort();
|
||||||
|
})
|
||||||
|
.bind('html5_upload.cancelAll', function() {
|
||||||
|
this.html5_upload['continue_after_abort'] = false;
|
||||||
|
this.html5_upload['xhr'].abort();
|
||||||
|
})
|
||||||
|
.bind('html5_upload.destroy', 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