Das jQuery FileUpload-Plugin ist eine feine Sache, welches ich inzwischen fast immer verwende, wenn ich auf einer Website eine Upload-Möglichkeit hinzufügen muss.
Mit jQuery FileUpload sind Uploads großer und vieler Dateien keine große Sache. Das Plugin hat nur ein Manko: Zur Dokumentation lässt sich sagen "Ja, ist vorhanden". Aber gerade wegen der vielen Optionen und Möglichkeiten ist sie leider nicht so übersichtlich, wie ein php.net oder api.jquery.com
So beschäftige ich mich immer wieder Stunden damit, eine Lösung zu finden, nur weil bei einer neuen Implementierung eine Kleinigkeit anders sein soll.

Ich will im Folgenden mal meine Best Practices mit dem jQuery FileUpload-Plugin unter dem Gesichtspunkt "Verwendung mit PHP" zusammenfassen.

Download

Nach einem Download bei GitHub wird man von einer Unzahl an Dateien erschlagen.

Für eine einfache Verwendung des Plugins mit PHP werden allerdings nur folgende beiden Dateien benötigt:

js
 +- jquery.fileupload.js
server
 +- php
     +- UploadHandler.php

Die restlichen 60 Dateien kann man getrost wegschmeißen (oder sich hier darüber informieren, wozu man sie benötigen könnte).

Will man Bild-, Audio- oder Video-Dateien hochladen und von Preview- oder simplen Bild-Edit-Funktionen profitieren, könnte die jeweilige zusätzliche Datei noch mit eingebunden werden:

js
 +- jquery.fileupload-image.js
 +- jquery.fileupload-audio.js
 +- jquery.fileupload-video.js

Um Browser ohne XHR-Support zu unterstützen (wobei schon InternetExplorer 5, Chrome und Firefox ab Version 1 und Safari ab Version 1.2 XHR unterstützten), wäre noch folgende Datei einzubinden:

js
 +- jquery.iframe-transport

Mit den beiden UI-Dateien, sowie mit Process, Validate und Angular hatte ich mich noch nie befasst.

Grundlegendes Setup

Irgendwo innerhalb des
<body/>
-Tags und vor der eigentlichen Verwendung die JavaScript-Bibliotheken einbinden...

<script type='text/javascript' src="_lib/upload/jquery.fileupload.js"></script>

...und dann wird's je nach Verwendungsart schon differenzierter.

Einerseits kann man nun ein

<input id="fileupload" type="file" name="files[]" multiple/>
(als Button, der dann zum Dateiauswahl-Dialogfenster des Systems führt) oder einen
<div id="fileupload" />
(als Dropzone) in die Seite einbinden.

Jedes der beiden Objekte wird durch ein

$("#fileupload").fileupload({ optionen und callbacks });  

zum Fileuploader.

Optionen und Methoden

Eine tiefergehende Beschreibung mit teilweise noch Beispielen ist hier zu finden. Ich wollte nur eine kurze Ãœbersicht zusammenstellen.

Mit * gekennzeichnete Werte sind Defaultwerte
Beschreibungen von Optionen, die ich normalerweise nicht verwende, habe ich einfach von der GitHub-Seite kopiert und nicht übersetzt.

Für den normalen Gebrauch sind nur die ersten beiden Abschnitte mit den generellen Optionen und Callbacks relevant.
Alle weiteren Abschnitte beziehen sich dann auf die nur optional benötigten Plugins wie z.B. jquery.fileupload-image.js

$("#selector").fileupload({ 

Generelle Optionen
  url: '/pfad/zu/verarbeitetem/skript.php', // Falls leer wird die action-Property derumschließenden <form/> verwendet oder falls nicht vorhanden die aktuelle Seite
  type: "POST*|PUT|PATCH",
  dataType: "json*|???",        // Datentyp, der als Antwort zurück erwartet wird
  dropZone:$('#selector'),      // Objekt, auf welches Dateien zum Upload gezogen werden können. Default: $(document), null um Drag&Drop zu deaktivieren, in meinen Anwendungsfällen entspricht der Selektor hier immer dem, mit dem ich den fileupload initialisiere (hier: $("#selector").fileupload({ in Zeile 1)
  pasteZone: $('#selector'),    // Objekt, auf welches Dateien zum Upload per Copy&Paste hinzugefügt werden. Default: undefined Momentan nur von Google Chrome unterstütztes Feature
  fileInput: $('#selector'),    // <input type='file'> welches auf Änderungen überwacht werden soll. Default: Input-Feld, welches innerhalb des Widgets ist. null, um zu deaktivieren
  replaceFileInput: true*|false,//By default, the file input field is replaced with a clone after each input field change event
  paramName: undefined,         // The parameter name for the file form data (the request argument name). If undefined or empty, the name property of the file input field is used, or "files[]" if the file input name property is also empty. Can be a string or an array of strings
  formAcceptCharset:'utf-8',    // The accept-charset attribute for the iframe upload forms
  singleFileUploads:true*|false,// Jede Datei als individuellen XHR-Request senden. Ist dies auf false gesetzt, muss multipart:true gesetzt sein!
  multipart:true*|false,        // Request als multipart/form-data senden. Note: Additional form data is ignored when the multipart option is set to false. Non-multipart uploads (multipart: false) are broken in Safari 5.1
  limitMultiFileUploads:undefined*,    // Max. Dateien je XHR-Request. Note: This option is ignored, if singleFileUploads is set to true or limitMultiFileUploadSize is set and the browser reports file sizes.
  limitMultiFileUploadSize:undefined*, // 1000000 = 1MB, limits the number of files uploaded with one XHR request to keep the request size under or equal to the defined limit in bytes. Note: This option is ignored, if singleFileUploads is set to true or limitMultiFileUploadSize is set
  limitMultiFileUploadSizeOverhead:512*,
  sequentialUploads:true|false*,  // Upload mehrerer Dateien nacheinander statt alle auf's Mal senden
  limitConcurrentUploads:<integer-wert>|undefined*, // Anzahl gleichzeitiger Uploads beschränken. Wird ignoriert, wenn sequentialUploads:true
  forceIframeTransport:true|false*, // iframe transport statt XHR-Requests verwenden
  initialIframeSrc:'javascript:false;'*,
  redirect:??*,
  redirectParamName:??*,
  postMessage:??*,
  maxChunkSize: <integer-wert>|undefined*, // Große Dateien in kleinere Teile zerstückeln. Wert ist in Bytes, 10MB = 10485760
  uploadedBytes:<integer-wert>|undefined*,
  recalculateProgress:true*|false,
  progressInterval:100*,
  bitrateInterval:500*,
  autoUpload:true*|false, // Dateien automatisch hochladen, sobald sie hinzugefügt wurden
  formData:<objekt>|<name/value array>|<funktion>, // Post-Variablen, die der Anfrage mitgegeben werden sollen

Callbacks
  add: function(e,data) {},     // Wird für JEDE EINZELNE Datei ausgeführt, die hinzugefügt wird, sobald sie hinzugefügt wird. Ein data.submit(); muss vorhanden sein, damit die Datei verarbeitet wird
  submit: function(e,data) {},  // Callback für den submit-Event jedes Uploads. Wird also für JEDE DATEI aufgerufen. Ein 'return false;' verhindert den Upload
  start: function(e,data) {},   // Callback für den generellen Upload-Start. Wird nur 1x aufgerufen, wenn man mehrere Dateien hinzufügt
  send: function(e,data) {},    // Callback für den Start jedes Uploads. Wird also für JEDE DATEI aufgerufen. Ein 'return false;' verhindert den Upload
  done: function(e,data) {},    // Erfolgreicher Upload-Request (je Datei)
  fail: function(e,data) {},    // Fehlgeschlagener Upload-Request (je Datei)
  always: function(e,data) {},  // Erfolgreicher oder fehlgeschlagener Upload-Request (je Datei)
  stop: function(e,data) {},    // Callback für den generelles Upload-Ende.  Wird nur 1x aufgerufen, wenn man mehrere Dateien hinzugefügt hatte
  progress: function(e,data) {},    // Callback for upload progress events
  progressall: function(e,data) {}, // Callback for global upload progress events
  change: function(e,data) {},      // Callback for change events of the fileInput collection
  paste: function(e,data) {},       // Callback for paste events to the dropZone collection
  dragover: function(e,data) {},    // Wird immer wieder aufgerufen, SO LANGE Dateien über der dropZone gehalten werden
  drop: function(e,data) {},        // Wird 1x ausgeführt, wenn Dateien auf der dropZone losgelassen wurden (nicht je Datei)
  chunksend: function(e,data) {},   // Callback for the start of each chunk upload request
  chunkdone: function(e,data) {},   // Callback for successful chunk upload requests
  chunkfail: function(e,data) {},   // Callback for failed (abort or error) chunk upload requests
  chunkalways: function(e,data) {}  // Callback for completed (success, abort or error) chunk upload requests

BILD-OPTIONEN für jquery.fileupload-image.js
  disableExif:true|false*,               // Disable parsing Exif data
  disableExifThumbnail: true|false*,     // Disable parsing the Exif Sub IFD
  disableExifGps: true|false*,           // Disable parsing Exif Gps data
  disableImageMetaDataLoad: true|false*, // Disable parsing image meta data
  disableImageMetaDataSave: true|false*, // Disables saving the image meta data into the resized images
  loadImageFileTypes: <RegEx>,           // Nur angegebene Mime-Typen akzeptieren (z.B. /^image\/(gif|jpeg|png)$/ )
  loadImageMaxFileSize: <integer-wert>|10000000*, // The maximum file size of images to load
  loadImageNoRevoke: true|false*,            // Don't revoke the object URL created to load the image
  disableImageLoad: true|false*,             // Disable loading and therefore processing of images
  imageMaxWidth: <integer-wert>|5000000*,    // The maximum width of resized images
  imageMaxHeight: <integer-wert>|5000000*,   // The maximum height of resized images
  imageMinWidth: <integer-wert>|undefined*,  // The minimum width of resized images
  imageMinHeight: <integer-wert>|undefined*, // The minimum height of resized images
  imageCrop: true|false*,                    // Define if resized images should be cropped or only scaled
  imageOrientation: 1|2|3|4|5|6|7|8|true|false*,          // Defines the image orientation (1-8) or takes the orientation value from Exif data if set to true
  imageForceResize: undefined*|true|false|<integer-wert>, // If set to true, forces writing to and saving images from canvas, even if the original image fits the maximum image constraints
  disableImageResize: true*|false,       // Disables the resize image functionality
  imageQuality: <float-wert>|undefined*, // Sets the quality parameter given to the canvas.toBlob() call when saving resized images
  imageType: <string>                    // Sets the type parameter given to the canvas.toBlob() call when saving resized images
  previewMaxWidth: <integer-wert>|80*,   // The maximum width of the preview images
  previewMaxHeight: <integer-wert>|80*,  // The maximum height of the preview images
  previewMinWidth: <integer-wert>|undefined*,      // The minimum width of the preview images
  previewMinHeight: <integer-wert>|undefined*,     // The minimum height of the preview images
  previewCrop: true|false*,                        // Define if preview images should be cropped or only scaled
  previewOrientation: 1|2|3|4|5|6|7|8|true|false*, // Defines the preview orientation (1-8) or takes the orientation value from Exif data if set to true
  previewThumbnail: true*|false,         // Create the preview using the Exif data thumbnail
  previewCanvas: true*|false,           // Define if preview images should be resized as canvas elements
  imagePreviewName: <string>|'preview', // Define the name of the property that the preview element is stored as on the File object
  disableImagePreview: true|false*      // Disables image previews

AUDIO-OPTIONEN für jquery.fileupload-audio.js
  loadAudioFileTypes: <RegEx>,                     // Nur angegebene Mime-Typen akzeptieren (z.B. /^audio\/.*$/ )
  loadAudioMaxFileSize: <integer-wert>|undefined*, // Maximale Dateigröße
  audioPreviewName: <string>|'preview'*,           // Define the name of the property that the preview element is stored as on the File object
  disableAudioPreview: true|false*,                // Disable audio previews

VIDEO-OPTIONEN für jquery.fileupload-video.js
  loadVideoFileTypes: <RegEx>,                     // Nur angegebene Mime-Typen akzeptieren (z.B. /^video\/.*$/ )
  loadVideoMaxFileSize: <integer-wert>|undefined*, // Maximale Dateigröße
  videoPreviewName: <string>|'preview'*,           // Define the name of the property that the preview element is stored as on the File object
  disableVideoPreview: true|false',                // Disable video previews

CALLBACKS für jquery.process.js
  processQueue: <array>,               // A list of file processing actions https://github.com/blueimp/jQuery-File-Upload/wiki/Options#file-processing-options
  processstart: function(e) {},        // Callback for the start of the fileupload processing queue
  process: function(e, data) {},       // Callback for the start of an individual file processing queue
  processdone: function(e, data) {},   // Callback for the successful end of an individual file processing queue
  processfail: function(e, data) {},   // Callback for the failure of an individual file processing queue
  processalways: function(e, data) {}, // Callback for the end (done or fail) of an individual file processing queue
  processstop: function(e, data) {},   // Callback for the stop of the fileupload processing queue

VALIDATION-OPTIONEN für jquery.fileupload-validate.js (benötigt zusätzlich jquery.process.js)
  acceptFileTypes: <regEx>|undefined*,    // Je nachdem, ob der Browser die File API unterstützt, wird hier die Dateierweiterung (Dateiname) oder der MIME-Type geprüft. Beispiels-Regex, die z.B. image/jpeg und .jpg durch gehen lässt: /(\.|\/)(gif|jpe?g|png)$/i
  maxFileSize: <integer-wert>|undefined*, // The maximum allowed file size in bytes. This option has only an effect for browsers supporting the File API
  minFileSize: <integer-wert>|undefined*, // The minimum allowed file size in bytes. This option has only an effect for browsers supporting the File API
  maxNumberOfFiles: <integer-wert>,       // This option limits the number of files that are allowed to be uploaded using this widget. By default, unlimited file uploads are allowed
  disableValidation: true|false*,         // Disables file validation

UI-OPTIONEN UND CALLBACKS für jquery.fileupload-ui.js
  getFilesFromResponse: function(data) {}, // Callback to retrieve the list of files from the server response
  getNumberOfFiles: function() {}, // This option is a function that returns the current number of files selected and uploaded. It is used in the maxNumberOfFiles validation
  This option is a function that returns the current number of files selected and uploaded.
  filesContainer: <objekt>|widgetContainer.find('.files')*, // The container for the files listed for upload / download. Is transformed into a jQuery object if set as DOM node or string
  prependFiles: true|false*, // By default, files are appended to the files container
  uploadTemplate: function() {}, // The upload template function
  uploadTemplateId: <string>|'template-upload'*, // The ID of the upload template, given as parameter to the tmpl() method to set the uploadTemplate option
  downloadTemplate: function() {}, // The download template function
  downloadTemplateId: <string>|'template-download'*, // The ID of the download template, given as parameter to the tmpl() method to set the downloadTemplate option
 
  destroy: function(e, data) {},   // Callback for file deletion events
  destroyed: function(e, data) {}, // The destroyed callback is the equivalent to the destroy callback and is triggered after files have been deleted, the transition effects have completed and the download template has been removed
  added: function(e, data) {},     // The added callback is the equivalent to the add callback and is triggered after the upload template has been rendered and the transition effects have completed
  sent: function(e, data) {},      // The sent (callback is the equivalent to the send callback and is triggered after the send callback has run and the files are about to be sent
  completed: function(e, data) {}, // The completed callback is the equivalent to the done callback and is triggered after successful uploads after the download template has been rendered and the transition effects have completed
  failed: function(e, data) {},    // The failed callback is the equivalent to the fail callback and is triggered after failed uploads after the download template has been rendered and the transition effects have completed
  finished: function(e, data) {},  // The finished callback is the equivalent to the always callback and is triggered after both completed and failed uploads after the equivalent template has been rendered and the transition effects have completed
  started: function(e, data) {},   // The started callback is the equivalent to the start callback and is triggered after the start callback has run and the transition effects called in the start callback have completed
  stopped: function(e, data) {},   // The stopped callback is the equivalent to the stop callback and is triggered after the stop callback has run and the transition effects called in the stop callback and all done callbacks have completed

}); 

Die weiter oben aufgeführten Callbacks

add
,
submit
, etc können auch später noch als Event-Listener an das fileUpload-Objekt gebunden werden, dann muss allerdings noch zusätzlich
 fileupload
als Prefix verwendet werden:

$('#fileupload')
    .bind('fileuploadadd', function (e, data) {/* ... */})
    .bind('fileuploadsubmit', function (e, data) {/* ... */})
    ...

Serverseitig...

Das in den JavaScript-Optionen angegebene PHP-Skript, welches die Daten entgegennimmt

$("#selector").fileupload({ url: '/pfad/zu/verarbeitetem/skript.php'});

macht Gebrauch von
UploadHandler.php
:

/pfad/zu/verarbeitetem/skript.php
require('/pfad/zu/UploadHandler.php');
$upload_handler = new UploadHandler($optionen_array);

Mehr Zeilen werden in dem PHP-Skript eigentlich nicht benötigt, um Dateien entgegenzunehmen und irgendwo zu speichern.
Das "irgendwo" ist noch das Problem, denn die Sache mit den Pfaden ist eine haarige Angelegenheit. Am besten, man gibt

upload_dir
und
upload_url
gleich als absoluten Pfad mit. Was der genaue Unterschied zwischen den beiden ist, habe ich auch nicht so richtig verstanden.

Außerdem kann man an dieser Stelle nochmals die Dateitypen angeben, welche beim Hochladen akzeptiert werden und, sollten Bilder hochgeladen werden, deaktiviert ein hinzugefügtes

'image_versions' => array()
das automatische Erstellen von Thumbnails.

/pfad/zu/verarbeitetem/skript.php
require('/pfad/zu/UploadHandler.php');
$upload_handler = new UploadHandler(array('upload_dir' => '/pfad/zu/verzeichnis/',
                                         'upload_url' => '/pfad/zu/verzeichnis/',
                                         'accept_file_types' => '/(jp.?g|gif|png)$/i',
                                         'image_versions' => array() // Verhindert Thumbnail-Generierung
                                         ));

Oft will man die erfolgreich hochgeladene Datei aber noch weiter verarbeiten oder deren Existenz in einer Datenbank eintragen. Dies konnte ich mit einer neuen UploadHandler-Klasse realisieren, die ich zu Beginn von

UploadHandler.php
hinzugefügt hatte:

class apfelzUploadHandler extends UploadHandler {

    protected function handle_form_data($file, $index) {
        // Hier nehmen wir einen neuen Request entgegeben
        // Zum Beispiel übernehme ich hier Werte von zwei
        // $_POST-Variablen in das $file-Objekt
        $file->parent       = @$_REQUEST['parent'];
        $file->lastModified = @$_REQUEST['fileModified'];
        }

    protected function handle_file_upload($uploaded_file, $name, $size, $type, $error, $index = null, $content_range = null) {
        $file = parent::handle_file_upload($uploaded_file, $name, $size, $type, $error, $index, $content_range);
        if (empty($file->error) && $file->completed) {
                // Wenn ein Upload beendet ist...
                // Hier kann man sich mit Datenbanken und Datei-Verschieben oder
                // -Konvertieren austoben.
                }
        return $file;
        }
    }

Der Code des PHP-Zweizeilers ändert sich entsprechend, weil wir nun die neue Klasse ansprechen:

/pfad/zu/verarbeitetem/skript.php
require('/pfad/zu/UploadHandler.php');
$upload_handler = new apfelzUploadHandler($optionen_array);

Achtung:

$file->completed
, was ich in meiner neuen Klasse
apfelzUploadHandler
 verwende, musste ich noch in die ursprüngliche Klasse reinfriemeln. Irgendwie wird diese Funktion an sich schon vorher einige Male ausgelöst. Keine Ahnung, ob sich das besser lösen lässt...ist halt meine Best Practise.

Die Änderungen ist innerhalb von

protected function handle_file_upload() 
~Zeile 1090:

Aus

            $file_size = $this->get_file_size($file_path, $append_file);
            if ($file_size === $file->size) {
               ...

mach'

            $file_size = $this->get_file_size($file_path, $append_file);
            $file->completed = false;
            if ($file_size === $file->size) {
               $file->completed = true;
               ...

Prüfen von Upload-Größe

Es gibt da ein unschönes Zusammenspiel von PHP und jQuery FileUpload:
Ist die Datenflut, die PHP empfängt, größer als die in php.ini maximal erlaubte Dateigröße, werden auch mögliche $_POST-Variablen von PHP verworfen. Das kann heißen: Vom Browser wird eine 30MB-Datei samt $_POST-Variablen an ein PHP-Skript übergeben. Das PHP-Skript ist nicht nur für diesen Daten-Upload zuständig, sondern übernimmt je nach mit übergebenen $_POST-Variablen andere Aufgaben. Nur werden in diesem Fall keine $_POST-Variablen übergeben, was zu einem unerwarteten Ergebnis oder zu unerklärlichen Fehlermeldungen führen kann.

Man sollte darauf achten, dass sowohl post_max_size wie auch upload_max_filesize in php.ini mindestens dem maximalen Wert entsprechen, den man auch in seinem JavaScript-Code abfängt.
Die UploadHandler.php-Klasse selbst würde in diesem Fall gar nicht mehr dazu kommen, eine zu große Datei abfangen zu können.

Man umgeht dieses Problem auf jeden Fall, wenn man maxChunkSize auf einen viel niedrigeren Wert als post_max_size/upload_max_filesize (der php.ini) setzt, da größere Dateien dann in mehrere Teile gesplittet werden.

Beispiel: Große Dateien bei Dateiupload auf 10MB-Pakete unterteilen:
$('#fileupload').fileupload({
   url: 'verarbeitungsskript.php',
   formData:{'anfrage': 'upload', 'sessionid': 'df87asd98d7sasd8s7'},
   maxChunkSize:10*1024*1024, // In 10MB-Pakete unterteilen
   ...

Ich empfehle, maxChunkSize immer zu verwenden.

Möchte man außerdem die Größe von hochzuladenen Dateien beschränken, habe ich mit dem JavaScript-Attribut maxFileSize (welches die zusätzlichen Bibliotheken jquery.fileupload-validate.js und jquery.process.js benötigt) keine guten Erfahrungen gemacht und fahre besser damit, die jeweilige Dateigröße innerhalb der add-Methode zu prüfen:

Dateigröße vor Upload prüfen
$('#fileupload').fileupload({
   url: 'verarbeitungsskript.php',
   formData:{'anfrage': 'upload', 'sessionid': 'df87asd98d7sasd8s7'},
   maxChunkSize:10*1024*1024,
   add: function(e, data) {
      for (var x=0; x<data.files.length; x++) {
         if (data.files[x].size > 20*1024*1024) { // Max. 20 MB erlauben
            alert("Die Datei "+data.files[x].name+" ist leider zu gro\u00df.\n" +
                  "Es ist eine Gr\u00F6\u00dfe von maximal 20 MB je Datei erlaubt." +
                  "Ihre Datei hat "+(data.files[x].size/1024/1024)+" MB.");
            return false; // Upload unterbinden
            }
         }
         data.submit(); // Wenn kein Fehler, Upload absenden
      },
      ...

Dateityp prüfen

Sowohl die UploadHandler-PHP-Klasse wie auch das JavaScript-Plugin erlauben es, die hochzuladenen Dateitypen zu prüfen. So kann man unterbinden, dass z.B. etwas anderes als Bild-Dateien hochgeladen werden, wenn man nur Bild-Dateien hochladen möchte. Oder man verhindert, dass problematische Dateien wie .php-Skripte oder .exe-Dateien auf den Server geladen werden können.
Leider muss man die Einschränkung an zwei Stellen machen: Sowohl in PHP, wie auch in JavaScript. Und mit dem JavaScript-Attribut acceptFileTypes (welches die zusätzlichen Bibliotheken jquery.fileupload-validate.js und jquery.process.js benötigt) habe ich keine guten Erfahrungen gemacht und fahre besser damit, die jeweilige Dateigröße innerhalb der add-Methode zu prüfen.

Einschränkung im JavaScript
$('#fileupload').fileupload({
   url: 'verarbeitungsskript.php',
   formData:{'anfrage': 'upload', 'sessionid': 'df87asd98d7sasd8s7'},
   maxChunkSize:10*1024*1024,
   //acceptFileTypes:/(\.|\/)(gif|jpe?g|png)$/i, // Klappt meiner Erfahrung nach nicht so ganz
   add: function(e, data) {
      for (var x=0; x<data.files.length; x++) {
         if (!data.files[x].name.match(/\.(jpe?g|gif|png)$/i)) {
            alert("Der Dateityp von "+data.files[x].name+" wird nicht unterst\u00fctzt.");
            return false; // Upload unterbinden
            }
         }
         data.submit(); // Wenn kein Fehler, Upload absenden
      },
      ...

Zusätzliche Einschränkung in php
$conf = array('accept_file_types' => "/(jpe?g|gif|png)$/i",
              'image_versions'    => array('' => array('auto_orient' => true)) // Kein Thumbnail bei Bildern erstellen
              );
$upload_handler = new UploadHandler($conf);

Grab & Paste: Beliebige Dateien via Dropzone hochladen

Anwendungsfall: Man soll beliebige und beliebig viele Dateien auf eine Dropzone innerhalb einer Website ziehen können.
Diese werden dann auf einem Fileserver (nicht dem Webserver, der die Seite ausliefert und den Upload entgegennimmt) abgelegt. Sonderfall: Jeder Upload (der aus n vielen Dateien besteht) bekommt sein eigenes Verzeichnis und jeder Upload wird in einer Datenbank erfasst.

Knackpunkt: Bevor der eigentliche Upload beginnt (oder zumindest bei der ersten hochgeladenen Datei) muss ich einen Ordner mit einer eindeutigen Bezeichnung erstellen und das dann in einer Datenbank notieren.
Nach einigen gescheiterten Versuchen, den Upload zu verzögern, den Ordner zu kreieren und den noch folgenden Dateien den Ordnernamen mitzugeben, hatte ich mich dazu entschlossen

autoUpload: false
zu setzen. Nachdem Dateien auf der Website hinzugefügt wurden, wird also nichts hochgeladen. Dafür muss erst ein Button geklickt werden, der dann alles stressfrei in der gewünschten Reihenfolge erledigt.
Hätte es zwar schicker gefunden, auf den "Upload Start-Button" zu verzichten, aber man kann nicht alles haben...

Auf dem offiziellen Demo Basic plus UI Demo sieht das sehr gut und einfach aus, ungünstigerweise kommt man nicht wirklich dahinter, wie das dort funktioniert und vor allem deckt es meinen Anwendungsfall doch nicht so 100%ig ab.
Dank diesem StackOverflow-Tipp kam ich allerdings dann doch zum Ziel. Es ist unglaublich, welche Klimmzüge man hier für so einen einfachen Anwendungsfall machen muss...

Und dass eine Datei, die ich zwar erst auf der Website hinzugefügt, aber dann wieder von der Upload-Liste entfernt habe, wirklich nicht hochzuladen, musste ich auch wieder etwas tricksen.

jQuery und jQuery-UI wurden bereits im

<head/>
eingebunden.

HTML

<script type='text/javascript' src="pfad/zu/jquery.fileupload.js"></script>

<style type='text/css'>
  #uploader {
    border:#003375 5px dashed;
    border-radius:10px;
    }
  #uploader.hover {
    border:#d3d3d3 5px dashed;
    }
  .fileitem {
    border:#003375 1px solid;
    padding:3px 20px 3px 3px;
    margin-right:5px;
    font-size:8px;
    background:url(_img/remove-x.png) 98% 50% no-repeat;
    cursor:pointer;
    display:inline-block;
    }
</style>

<div id='uploader'>
  Wollen Sie Dateien versenden?<br/>
  Ziehen Sie diese einfach in diesen Bereich.<br/>
  <br/>
  <button id='ulStartBtn' style='font-size:10px; display:none;'>Dateien jetzt hochladen</button>
  <div id='filelist' style='word-wrap: break-word; text-align:left; margin:10px;'></div>
</div>

<div id='progressbar'></div>

Dateien können auf das Element #uploader gezogen werden, um sie der Upload-Warteschlange hinzuzufügen.
Danach erscheint der Dateiname innerhalb der #filelist, wo er durch einen Klick darauf wieder entfernt werden kann.
Durch einen Klick auf den Button #ulStartBtn werden alle Dateien letztendlich hochgeladen.

JavaScript

// jQuery-UI Progressbar für Downloadfortschritt
$("#progressbar").progressbar({ value: false, max: 100 });

// Der Button, der letztendlich den Upload startet und alle Dateien hochlädt
$("#ulStartBtn").button({icons: {primary: "ui-icon-circle-arrow-n"}, text: true}).on("click", function(e) {
  e.preventDefault(); // Notwendig, da sich #ulStartBtn im #uploader-Element befindet
  $.ajax({ // Vom PHP-Skript einen Ordner anlegen lassen und den Ordnernamen später jeder einzelnen Datei mitgeben
    method: "POST",
    url: "/pfad/zu/uploader-skript.php",
    data: { aktion: "vorbereiten" }
  }).done(function( ordnername ) {
    $('#uploader').fileupload('option', 'ordnername', ordnername); // Zurückgemeldeten Ordnernamen zwischenspeichern
    $("#ulStartBtn").trigger("startupload"); // <-- Das sorgt für den eigentlichen Upload-Start
    });
  });

// jQuery File Upload
$('#uploader').fileupload({
  dropZone:    $('#uploader'), // Notwendig, sonst reagiert der gesamte <body/> auf drag&drop
  url:         '/pfad/zu/uploader-skript.php',
  maxChunkSize: 10*1024*1024,
  autoUpload:   false,
  ordnername:   null, // Von mir selbst erfundene Variable zum Ordnernamen zwischenspeichern
  add: function(e, data) {
    // Eine Datei wurde hinzugefügt
    // Dateiname in Dropzone anzeigen
    data.context = $('<span/>').appendTo('#filelist');
    $.each(data.files, function (index, file) {
      var node = $('<span/>').addClass("fileitem").text(file.name);
      $(node).on("click", function() { $(this).remove(); });
      node.appendTo(data.context);
      });
    // (möglicherweise ausgeblendeten) Upload-Start-Button aktivieren
    // und Event anhängen, der bei Klick des Buttons diese eine Datei hochlädt
    $("#ulStartBtn").show().on("startupload", function() {
      if (data.context[0].childNodes.length == 0) return false; // Abbrechen, wenn Datei wieder von Liste geschmissen wurde
      data.formData = {aktion: 'upload', ordnername: $('#uploader').fileupload('option', 'ordnername')};
      data.submit();
      });
    },
  progressall: function (e, data) {
    // Die Progress-Bar etwas bewegen und Fortschritt anzeigen
    var progress = parseInt(data.loaded / data.total * 100, 10);
    $("#progressbar").progressbar({ value: progress });
    },
  stop: function(e, data) {
        alert("Fertig!");
    }
  });  

PHP

Simpel gehaltene Form

<?php
$parentFolder = "/pfad/zu/ueberverzeichnis";
switch ($_POST['aktion']) {
 
  case 'vorbereiten':
    $ordnername = strtoupper(rndStr(14)); // rndStr() ist eine theoretische Funktion, die einen String mit 14 Zufallszeichen erzeugt
    while ($db->sql("select count(ordnername) as anz from downloads where ordnername = '$ordnername'", "anz") > 0) { $ordnername = strtoupper(rndStr(14)); } // Keine doppelten Verzeichnisse
    if ($db->sql("insert into downloads (ordnername) values ('$ordnername')") !== false) {
      mkdir("$parentFolder/$ordnername");
      die($ordnername); // <-- Hiermit geben wir den Ordnernamen zurück
      }
    break;
 
  case 'upload':
    require('/pfad/zu/UploadHandler.php');
    $upload_handler = new UploadHandler(array('upload_dir' => $parentFolder."/".$_POST['ordnername']."/", 'upload_url' => $parentFolder."/".$_POST['ordnername']."/"));
    break;
  }
?>

Grab & Paste: Dropzone mit Button, welcher System-Filepicker öffnet

Man kennt ihn noch, aber die Benutzung war schon vom ersten Tag an nicht so toll:

<input type='file'/>

Nach einem Klick auf den dadurch generierten "Durchsuchen..."-Button erscheint das Dateiauswahl-Fenster des Betriebssystems.
Leider sieht dieses HTML-Element unter jedem System/Browser anders aus. Aber anders kommt man nicht an das Dateiauswahl-Fenster des Betriebssystems, weswegen man nicht drumherum kommt, dieses Element zumindest unsichtbar auf der Seite hinzuzufügen.

<div id='uploader'>
  Wollen Sie Dateien versenden?<br/>
  Ziehen Sie diese einfach in diesen Bereich.<br/>
  <br/>
  <button id='ulPickerBtn' style='font-size:10px;'>Dateien wählen...</button>
  <input type='file' id='filepicker_helper' style='display:none;' multiple>
</div>

Durch einen Klick auf den Button #ulPickerBtn erscheint das Dateiauswahl-Fenster des Betriebssystems, als ob wir das eigentliche

<input/>
-Element verwenden würden.
Und noch dazu macht es dem jQuery File Upload nachher keinen Unterschied, ob ich die Dateien auf diese Art oder per Drag&Drop auf die
dropZone
 hinzugefügt habe (so lange sich das
<input/>
-Element innerhalb des
dropZone
-Elementes befindet).

$("#ulPickerBtn").button({icons: {primary: "ui-icon-folder-open"}, text: true})
   .on("click", function() { $("#filepicker_helper").trigger("click"); });

Den fileuploader kann ich so auf zwei verschiedene Arten initialisieren:

$('#uploader').fileupload({ ... });

oder

$('#filepicker_helper').fileupload({ ... });

In letzterem Fall kann ich mir es allerdings sparen, den #filepicker_helper per CSS auszublenden.

Grab & Paste: Dropzone mit Hover-Effekt, Filepicker-Button, Fortschrittsanzeige

Der Standard-Anwendungsfall: Dateien per Drag&Drop auf ein bestimmtes Gebiet auf der Website hinzufügen (welches sich verfärbt, während man die Datei darüber hält), zusätzlich noch ein "Upload-Button", welcher bei Klick den System-Dialog zur Dateien-Auswahl öffnet und ausgewählte Dateien sofort hochlädt und eine Status-Anzeige (Progressbar) für den Upload-Fortschritt, falls auch mal größere Dateien im Spiel sein sollten.

HTML-Code für die Upload-Fortschrittsanzeige, den Button, der den System-Dialog zur Dateien-Auswahl öffnet, das versteckte HTML-File-Element und der Bereich, auf welchen sich Dateien zum Hochladen ziehen lassen (umschließt alle anderen Elemente).
Außerdem binden wir noch jQuery, jQueryUI (optional. Verwende ich in diesem Beispiel für den Button und die Progressbar) und jQuery fileupload ein.

HTML
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="jquery-ui.js"></script>
<script src="jquery.fileupload.js"></script>

<div id='postimages' style='width:680px; height:590px; overflow:auto;'>
  <div id='progressbar' style='width:500px; display:none;'></div>
  <button id='fileuploadPicker'>Upload...</button>
  <input id='fileupload' type="file" name="files[]" style="display:none;" multiple>
</div>

JavaScript
$("#progressbar").progressbar({ value: 0 }); //<div/> zur jQuery-UI Progressbar machen
$('#fileupload').fileupload({ // <input/> zum Fileupload-Element machen
   url: 'myuploader.php', // Uploader-Skript
   dropZone:$("#postimages"), // <div/> zur Drag&Drop-Hotzone machen
   formData:{'postvariable1': 'abc', 'postvariable2': '3746473'}, // Ein paar $_POST-Werte mit der Datei an myuploader.php mitgeben
   maxChunkSize:10*1024*1024,
   add: function(e, data) {
      // Beim Upload-Start wird die zuvor per CSS ausgeblendete Progressbar
      // nun sichtbar gemacht und mit data.submit() der Upload letztendlich gestartet
      $("#progressbar").show();
      data.submit();
      },
   progressall: function (e, data) {
      // Prozentwert des Fortschritts berechnen und Progressbar
      // dementsprechend erweitern
      var progress = parseInt(data.loaded / data.total * 100, 10);
      $("#progressbar").progressbar({ value: progress });
      },
   done: function(e, data) {
      // Upload erledigt. Wir können die Progressbar nun wieder
      // auf 0 setzen und ausblenden
      $("#progressbar").progressbar({ value: 0 });
      $("#progressbar").hide();
      // In der Variable result würden einige Daten wie Dateiname und
      // Pfad der hochgeladenen Datei zu finden sein
      var result = eval("("+data.result+")");
      // Der beim Upload geänderte Dateiname (siehe PHP-Code weiter unten)
      console.log(result.files[0].neuerName);
      }
   });

// <button/> zum Button machen, der den Systemdialog "Dateiauswahl" bei Klick öffnet
$("#fileuploadPicker").button({icons: {primary: "ui-icon-plusthick"}, text: true})
                      .on("click", function() { $("#fileupload").trigger("click"); });

// Dragover-Effekt
// (leider kein Einzeiler)
$(document).bind('dragover', function (e) {
  var dropZone = $('#postimages'),
     timeout  = window.dropZoneTimeout;
  clearTimeout(timeout);
  var found = false,
     node = e.target;
  do {
     if (node === dropZone[0]) {
        found = true;
        break;
     }
     node = node.parentNode;
  } while (node != null);
  if (found) {
     dropZone.addClass('hover');
  } else {
     dropZone.removeClass('hover');
  }
  window.dropZoneTimeout = setTimeout(function () {
     window.dropZoneTimeout = null;
     dropZone.removeClass('hover');
  }, 100);
  });

Die Standard-UploadHandler-Klasse erweitern wir etwas, um die $_POST-Daten des JavaScriptes entgegenzunehmen (auch wenn ich in diesem Beispiel nichts mit den Daten anfange) und die hochgeladene Datei umzubenennen.

PHP-Skript myuploader.php
include 'UploadHandler.php';

class apfelzUploadHandler extends UploadHandler {

    protected function handle_form_data($file, $index) {
        $file->variableAusJavaScript = @$_REQUEST['postvariable1'];
        }

    protected function handle_file_upload($uploaded_file, $name, $size, $type, $error, $index = null, $content_range = null) {
        // Leerzeichen in Dateinamen in Unterstrich ändern und alle anderen
        // Zeichen bis auf Zahlen, Buchstaben, Binde- und Unterstrich entfernen
        // Außerdem nur Kleinbuchstaben für Dateinamen verwenden
        $name = strtolower(preg_replace("/[^-_\.a-zA-Z0-9]/", "", str_replace(" ", "_", $name)));
        $file = parent::handle_file_upload($uploaded_file, $name, $size, $type, $error, $index, $content_range);
        // Den geänderten Dateinamen nachher wieder zurückmelden
        // (in JavaScript in data.result zu finden)
        $file->neuerName = $name;
        return $file;
        }
    }

$conf = array('upload_dir'        => "../pfad/zur/dateiverzeichnis/",
              'upload_url'        => "../pfad/zur/dateiverzeichnis/",
              'accept_file_types' => "/(xls|xlt|xlm|xlsx)$/i")
$upload_handler = new apfelzUploadHandler($conf);

Oder ohne den ganzen Schnickschnack:

PHP-Skript myuploader.php schnickschnackfrei
include 'UploadHandler.php';

$conf = array('upload_dir'        => "../pfad/zur/dateiverzeichnis/",
              'upload_url'        => "../pfad/zur/dateiverzeichnis/",
              'accept_file_types' => "/(xls|xlt|xlm|xlsx)$/i",
              'image_versions'    => array() // Verhindert Thumbnail-Generierung
              );
$upload_handler = new UploadHandler($conf);