/** * # ChunkedUpload * * ## Initialisierung * * ```html * *
* * * ``` * * ## Callback * * ### `fileComplete` * * ```javascript * $('#chunky').chunkedUpload({ * fileComplete: function(fileInfo) { * // Do something when file upload is completed * // fileInfo.id = Unique id; example: 'chunked_upload_2377390993' * // fileInfo.name = Client file name * // fileInfo.type = File mime type * // fileInfo.size = File size in bytes * } * }); * ``` * */ (function ($) { 'use strict'; var ChunkedUpload = function ($elem, options) { var STATUS = { WAITING: 'waiting', // Datei wurde hinzugefügt; Es wird gewartet dass Benutzer den Upload startet UPLOADING: 'uploading', // Datei wird gerade hochgeladen FINISHED: 'finished', // Upload verarbeitet FAILURE: 'failure' // Fehler beim Upload }; var me = { /** @property {Object} me.options Default configuration */ options: { chunkSize: 6291456, // 6291456 = 6MB upload: { url: null, view: 'standard', formData: {} }, filesContainer: '#chunked-upload-files', /** * Callback wenn Datei erfolgreich hochgeladen wurde * * @param {FileInfo} fileInfo */ fileComplete: function (fileInfo) {} }, /** @property {Object} me.storage Runtime storage */ storage: { $fileInput: null, $filesContainer: null, $filesList: null, uploads: {} }, /** * @param {HTMLElement} element * @param {Object} options */ init: function (element, options) { // Prüfen ob HTML5-File-API verfügbar if (!me.isFileApiAvailable()) { alert( 'Die HTML5 File-API wird von ihrem Browser nicht unterstützt. ' + 'Bitte verwenden sie einen moderneren Browser.' ); return; } // Optionen mit Defaults mergen me.options = $.extend({}, me.options, options); if (typeof me.options.upload.url === 'undefined' || me.options.upload.url === null) { throw 'Initialisierung nicht möglich. Upload-URL fehlt.'; } if (typeof me.options.upload.formData === 'undefined' || me.options.upload.formData === null) { me.options.upload.formData = {}; } var $fileInput = $(element); if ($fileInput.length !== 1) { throw 'File-Input Element wurde nicht gefunden.'; } if (!$fileInput.is('input[type=file]')) { alert('Init-Element muss ein "input"-Element vom Typ "file" sein.'); return; } $fileInput.on('change', me.onSelectFilesEventHandler); me.storage.$fileInput = $fileInput; me.createFilesContainer(); }, /** * @param {Event} event */ onSelectFilesEventHandler: function (event) { /** @var {FileList} files */ var files = event.target.files; $.each(files, function (index, file) { me.addFileUpload(file); }); // File-Input leeren var $input = $(this); $input.replaceWith($input.val('').clone(true)); }, /** * @param {File} fileObject */ addFileUpload: function (fileObject) { var fileId = me.generateRandomId(); var fileSize = me.formatBytes(fileObject.size); var uploadObject = { id: fileId, file: fileObject, reader: new FileReader(), status: STATUS.WAITING, elements: { $progressBar: null, $statusInfo: null, $actionCell: null } }; var $removeFileButton = $(''); $removeFileButton.attr('href', '#').text('Entfernen'); $removeFileButton.addClass('chuncked-removefile-trigger').addClass('button'); $removeFileButton.on('click', function (event) { event.preventDefault(); var $link = $(this); var $row = $link.parents('tr'); var fileId = $row.data('fileId'); me.removeFile(fileId); }); var $uploadFileButton = $(''); $uploadFileButton.attr('href', '#').text('Hochladen'); $uploadFileButton.addClass('chuncked-startupload-trigger').addClass('button'); $uploadFileButton.on('click', function (event) { event.preventDefault(); var $link = $(this); var $row = $link.parents('tr'); var fileId = $row.data('fileId'); me.startUpload(fileId); }); var $progressBar = $('').attr('min', '0').attr('max', '100').val(0); var $row = $('').attr('id', fileId).data('fileId', fileId); if(me.options.upload.view === 'sidebar') { var $td = $('').appendTo($row); var $table = $('').appendTo($td); $('').appendTo($table)).before(''); $('').appendTo($table)).before(''); $('').appendTo($table)).before(''); var $statusInfo = $('').appendTo($table)).before(''); var $actionsCell = $('').data('fileId', fileId).appendTo($table)); } else { $('' + '' + '' + '
').addClass('filename').html(fileObject.name).appendTo($('
Dateiname:').addClass('filesize').html(fileSize).appendTo($('
Größe:').addClass('fileprogress').html($progressBar).appendTo($('
Fortschritt:').addClass('filestatus').html('Bereit zum Hochladen').appendTo($('
Status:').addClass('fileaction').appendTo($('
').addClass('filename').html(fileObject.name).appendTo($row); $('').addClass('filesize').html(fileSize).appendTo($row); $('').addClass('fileprogress').html($progressBar).appendTo($row); var $statusInfo = $('').addClass('filestatus').html('Bereit zum Hochladen').appendTo($row); var $actionsCell = $('').addClass('fileaction').appendTo($row); } $actionsCell.append($removeFileButton); $actionsCell.append($uploadFileButton); $row.appendTo(me.storage.$filesList); uploadObject.elements.$progressBar = $progressBar; uploadObject.elements.$statusInfo = $statusInfo; uploadObject.elements.$actionCell = $actionsCell; me.storage.uploads[fileId] = uploadObject; me.storage.$filesContainer.show(); me.storage.$filesContainer.find('table').show(); }, /** * @param {String} uploadId */ removeFile: function (uploadId) { var $row = $('#' + uploadId); if ($row.length === 0) { alert('Can not remove file upload. Element "#' + uploadId + '" not found.'); return; } // Datei existiert nicht (mehr?) if (!me.storage.uploads.hasOwnProperty(uploadId)) { return; } // Upload läuft gerade if (me.storage.uploads[uploadId].status === STATUS.UPLOADING) { return; // @todo Laufenden Upload abbrechen } // Tabellenzeile entfernen $row.remove(); delete me.storage.uploads[uploadId]; }, /** * @ŧodo Upload starten * * @param {String} uploadId */ startUpload: function (uploadId) { if (me.storage.uploads.hasOwnProperty(uploadId)) { var upload = me.storage.uploads[uploadId]; me.startSingleUpload(upload); } }, /** * @param {Object} upload Einzelner Wert aus me.storage.waiting */ startSingleUpload: function (upload) { if (upload === null) { return; } var fileId = upload.id; if (typeof fileId === 'undefined') { throw 'Upload fehlgeschlagen. Unique-ID is missing.'; } var $tableRow = $('#' + fileId); var $statusCell = $tableRow.find('.filestatus'); $statusCell.html('Bitte warten...'); upload.elements.$progressBar.val(0); me.uploadFileChunk(upload, 0); }, /** * @param {object} upload * @param {number} start */ uploadFileChunk: function (upload, start) { var chunkSize = me.options.chunkSize; // ChunkSize kleiner 10KB macht keinen Sinn; Upload verhindern if (chunkSize < 10240) { me.displayUploadError(upload.id, 'ChunkSize ist zu gering (<= 10KB). Upload nicht möglich.'); return; } // Im allerersten Upload die ChunkSize auf 100KB stellen // Server schickt in seiner Antwort das PHP-Upload-Limit mit if (start === 0) { chunkSize = 102400; // 102400 = 100KB } var offset = start + chunkSize + 1; var chunkBlob = upload.file.slice(start, offset); var fileId = upload.id; if (typeof fileId === 'undefined') { throw 'Upload fehlgeschlagen. Unique-ID is missing.'; } upload.status = STATUS.UPLOADING; upload.elements.$statusInfo.html('Hochladen...'); upload.elements.$actionCell.html(''); // Datei-Inhalt fertig eingelesen => Upload starten upload.reader.onloadend = function (event) { if (event.target.readyState !== FileReader.DONE) { return; } var ajaxData = me.options.upload.formData; ajaxData.file_data = event.target.result; ajaxData.file_name = upload.file.name; ajaxData.file_type = upload.file.type; ajaxData.file_size = upload.file.size; ajaxData.file_id = upload.id; ajaxData.file_offset = start; upload.xhr = $.ajax({ url: me.options.upload.url, type: 'POST', dataType: 'json', cache: false, data: ajaxData, error: function (jqXHR, textStatus, errorThrown) { var errorMessage = 'Unbekannter Fehler #21: ' + errorThrown; // User hat "Abbrechen" geklickt if (textStatus === 'abort') { errorMessage = 'Upload abgebrochen'; } // PHP-Skript hat Fehler geliefert (z.b. 404) if (textStatus === 'error') { errorMessage = 'Unbekannter Server-Fehler'; } // PHP-Skript liefer JSON-Error-Response if (jqXHR.hasOwnProperty('responseJSON') && jqXHR.responseJSON.hasOwnProperty('error')) { errorMessage = 'Server-Fehler: ' + jqXHR.responseJSON.error; } upload.elements.$statusInfo.html('' + errorMessage + ''); upload.elements.$progressBar.val(null); }, success: function (data) { if (data.hasOwnProperty('success') && data.success === false) { upload.elements.$progressBar.val(null); upload.elements.$statusInfo.html('Server-Fehler: ' + data.error + ''); return; } if (!data.hasOwnProperty('file') && !data.file.hasOwnProperty('bytes')) { alert('Fehlerhafte Antwort vom Server. #1'); return; } if (data.file.bytes === false) { alert('Fehlerhafte Antwort vom Server. #2'); return; } var bytesSend = data.file.bytes; var sizeDone = start + bytesSend; var sizeTotal = upload.file.size; var percentDone = Math.floor((sizeDone / sizeTotal) * 100); upload.elements.$progressBar.val(percentDone); // Die erste Response vom Server enthält das PHP-Upload-Limit // => ChunkSize anpassen falls diese über dem PHP-Limit liegt if (data.hasOwnProperty('uploadLimit') && typeof data.uploadLimit === 'number' && data.uploadLimit > 0 ) { var uploadLimit = Math.floor(data.uploadLimit / 100 * 95); // 5% als Reserve freihalten var transferSize = me.calculateBase64SizeFromRawSize(me.options.chunkSize); if (uploadLimit < transferSize) { var maxChunkSize = me.calculateRawSizeFromBase64Size(uploadLimit); me.options.chunkSize = maxChunkSize; console.warn('ChunkedUpload: PHP upload limit is ' + data.uploadLimit + ' bytes.'); console.warn('ChunkedUpload: Chunk size set to ' + maxChunkSize + ' bytes.'); } } if (offset < sizeTotal) { me.uploadFileChunk(upload, offset); } else { me.finishUpload(upload.id); } } }); }; // Datei einlesen starten upload.reader.readAsDataURL(chunkBlob); }, /** * Erfolgreichen Upload abschließen * * @param {String} uploadId */ finishUpload: function (uploadId) { if (me.storage.uploads.hasOwnProperty(uploadId)) { var upload = me.storage.uploads[uploadId]; upload.elements.$statusInfo.html('Upload erfolgreich'); upload.elements.$actionCell.html(''); upload.status = STATUS.FINISHED; // Callback aufrufen var fileInfo = new FileInfo(upload.id, upload.file.name, upload.file.type, upload.file.size); me.options.fileComplete(fileInfo); } }, /** * Upload als fehlerhaft markieren * * @param {String} uploadId * @param {String} errorMessage */ displayUploadError: function (uploadId, errorMessage) { if (me.storage.uploads.hasOwnProperty(uploadId)) { var upload = me.storage.uploads[uploadId]; upload.elements.$statusInfo.html('Upload-Fehler: ' + errorMessage); upload.elements.$actionCell.html(''); upload.status = STATUS.FAILURE; } }, /** * Datei-Tabelle erzeugen */ createFilesContainer: function () { var template = ''; if(me.options.upload.view === 'sidebar') { template += '' + '' + '
'; } else { template += '
DateinameGrößeFortschrittStatusAktionen
'; } var $list = $(template).hide(); var $filesContainer = $(me.options.filesContainer); if ($filesContainer.length === 0) { $filesContainer = $('
').insertAfter(me.storage.$fileInput); } $filesContainer.append($list); $filesContainer.addClass('chunked-file-upload-container'); me.storage.$filesContainer = $filesContainer; me.storage.$filesList = $list.find('tbody'); }, /** * HTML5 File-API vorhanden? Oder Uralt-Browser? * * @return {boolean} */ isFileApiAvailable: function () { return typeof window.File !== 'undefined' && typeof window.FileList !== 'undefined' && typeof window.FileReader !== 'undefined'; }, /** * Zufällige ID generieren * * @return {string} */ generateRandomId: function () { return 'chunked_upload_' + Math.floor(Math.random() * Math.floor(9999999999)); }, /** * @param {string|number} value * * @return {string} */ formatBytes: function (value) { var bytes = parseInt(value, 10); if (bytes === 0) { return '0 Bytes'; } var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; var exponent = Math.floor(Math.log(bytes) / Math.log(1024)); var decimalString = (bytes / Math.pow(1024, exponent)).toFixed(1) + ''; return decimalString.replace('.', ',') + ' ' + sizes[exponent]; }, /** * @param {number} rawSize Größe der Binärdaten (in Bytes) * * @return {number} Größe in Bytes wenn Base64-kodiert */ calculateBase64SizeFromRawSize: function (rawSize) { return Math.floor(rawSize / 3 * 4); }, /** * * @param {number} encodedSize Größe eines Base64-kodierten Strings (in Bytes) * * @return {number} Größe in Bytes nach Base64-Dekodierung */ calculateRawSizeFromBase64Size: function (encodedSize) { return Math.floor(encodedSize / 4 * 3); } }; /** * @param {string} id Unique id; example: 'chunked_upload_2377390993' * @param {string} name file name * @param {string} type Mime type * @param {number} size File size in bytes * @constructor */ var FileInfo = function (id, name, type, size) { this.id = id; this.name = name; this.type = type; this.size = size; }; me.init($elem, options); /** * Return public api */ return {}; }; // Dokumentation: Siehe Dateianfang $.fn.chunkedUpload = function (options) { return this.each(function () { var $elem = $(this); if (!$elem.data('chunkedUpload')) { var api = new ChunkedUpload(this, options); $elem.data('chunkedUpload', api); } }); }; }(jQuery));