Uploader plików z wykorzystaniem HTML5

Opublikowano 21.6.2011  w JavaScript » 4 komentarzy
ikonka kopia

Swego czasu problemem dla twórców stron internetowych było wysyłanie plików bez przeładowywania strony. Mieliśmy do wyboru albo użyć apleta flash albo szpecić swój kod iframami. Czasy na szczęście się zmieniają i powstają nowe technologie które to pozwalają na coraz więcej. Aktualnie nowoczesne przeglądarki udostępniają obiektFile z poziomu kodu JS który w połączeniu z ajaxem pozwala stworzyć uploader plików dorównujący możliwościami tym flashowym lub javowym.

Stawiamy cele

Najpierw postaram się omówić co będę chciał zbudować w tym tutorialu. Moim celem jest napisanie uploadera który będzie w przeglądarkach Chrome i Firefox zastępować tradycyjny HTMLowy formularz. Nasz skrypt bedzie przyjmował pliki metodą drag & drop i wyświetlał ich listę w postaci kontenerów. Po wykryciu, że internauta trzyma pliki nad przeglądarką nastąpi zachęcenie go do upuszczenia ich w odpowiednim miejscu poprzez podmianę napisu. Każdy kontener zaś będzie wyświetlał nazwę wybranego pliku, miniaturkę jeśli wybrany plik jest grafiką lub ewentualnie ikonkę zastępczą, pole tekstowe gdzie można dodać opis oraz przycisk wysyłania. Kontener będzie zawierał także przycisk usuwający dany plik z listy do wysłania oraz pole na wyświetlanie komunikatów. W momencie kliknięcia przycisku wysłania sam przycisk zniknie (aby użytkownik nie wysłał kilka razy tego samego pliku) i pojawi się w razie nie udanego uploadu. Każdorazowa próba wysłania spowoduje wyświetlenie odpowiedniego napisu w polu komunikatów. Jeśli chodzi o przesłane pliki to będą one zapisywane pod ustawionym adresem a opisy do nich będą lądować w pliku tekstowym o nazwie <nazwa pliku>.txt.

Wersja demonstracyjna skryptu (nie zapisuje plików na serwerze)

Gotowiec do pobrania na dole strony.

Kod HTML

Zacznijmy od napisania kodu tradycyjnego formularza:

<div id="uploadHTML">
    <form enctype="multipart/form-data" action="uploadHtml.php" method="POST">
        <input name="upload" type="file" />
        <input value="Wyślij" type="submit" />
    </form>
</div>

Ważne jest by objąć tą część kodu w blok z nadaną klasą uploadHTML gdyż później będziemy usuwać stary typ formularz poprzez odwołanie się do tej klasy. Podobny blok tworzymy dla naszego nowoczesnego uploadera:

<div id="uploadJS" style="display: none">
    //tu dalsza część kodu
</div>

Teraz wrzucamy do niego dwa elementy. Pole gdzie użytkownik ma upuścić plik:

<div class="uploader" id="uploader">
    Przenieś tu pliki które chcesz<br />wysłać na serwer
</div>

oraz miejsce na listę plików do wysłania na serwer:

<div class="uploadList">
    //tu dalsza część kodu
</div>

Do tego diva wstawimy teraz przykładowy element a następnie go schowamy. Będzie to szablon dla skryptu jak mają wyglądać nowe elementy na liście.

<div id="uploadFileTemplate" class="uploadFile">
    <div class="header">
        <div class="name">Nazwa pliku.jpg</div>
        <div class="delete">×</div>
    </div>
    <div class="thumb">
        <img src="file.png" alt="Miniaturka" class="image" />
    </div>
    <div class="info">
        <textarea name="description"></textarea>
        <p class="alert"></p>
        <input type="hidden" name="file" value="">
        <div id="startUpload" class="submit">Wyślij plik</div>
    </div>
</div>

W powyższym nie trzeba raczej niczego tłumaczyć po za tym, że div z id startUpload będzie przyciskiem który rozpocznie upload a zaś plik file.png jest naszą domyślną ikonką. Aby szablon był niewidoczny dla użytkownika w arkuszu CSS piszemy:

#uploadFileTemplate{
        display: none;
}

A teraz Javascript

Po pierwsze dołączymy do naszego kodu bibliotekę jQuery ponieważ dla łatwości pisania najlepiej będzie oprzeć nasz skrypt na tym frameworku.

<script type="text/javascript" src="http://code.jquery.com/jquery-1.6.1.min.js"></script>

Teraz zdefiniujemy parę zmiennym których użycie będę wyjaśniał później oraz napiszemy instrukcję która zapewni nam, że nasz kod nie wykona się za nim wszystkie elementy strony nie będą załadowane.

var ePage;
var eDropPlace;
var eStartUpload;
var filesToUpload;
var newId;
var idi = 0;

$(document).ready(function(){
    //tu dalsza część kodu
});

Wewnątrz tego kodu umieszczamy teraz instrukcję warunkową która wyświetli nasz uploader w przeglądarkah obsługujących wykorzystywane przez nas technologie:

if (window.File && window.FileReader && window.FileList) {
    $("#uploadHTML").remove();
    $("#uploadJS").show();
    ePage               = document.getElementsByTagName('body').item(0);
    eDropPlace          = document.getElementById("uploader");
    //tu dalsza część kodu
}

Powyższy kod działa w ten sposób, że sprawdzane jest czy przeglądarka obsługuje używane przez nas obiekty FileFileReader oraz FileList. Jeśli tak to usuwamy tradycyjny formularz a wyświetlamy ten nasz. Po tej operacji możemy już pobrać referencje do dwóch ważnych obiektów w naszym kodzie. ePage to cała nasza strona dla której będziemy wykrywali czy użytkownik przeniósł pliki nad okno przeglądarki. eDropPlace natomiast to miejsce gdzie użytkownik ma upuścić pliki. Mogłoby być to to samo miejsce ale w tym tutorialu zrobimy rozróżnienie aby lepiej wytłumaczyć czym to się różni.

Zdarzenia ondragover i ondragleave

Teraz poniżej tego (ale przed nawiasem klamrowym) dodamy obsługę zdarzeń. Po pierwsze obsłużmy przeniesienie plików nad przeglądarkę:

ePage.ondragover=function(f){
    eDropPlace.innerHTML="Upuśc plik na to pole";
    f.dataTransfer.dropEffect="copy";
    f.preventDefault();
    return false
};

ondragover to właśnie zdarzenie polegające na tym, że użytkownik zaznaczył w folderze pliki i za pomocą myszki przeniósł je nad okno przeglądarki. Jeśli takie coś wykryjemy to zmieniamy treść napisu oraz zmieniamy kursor na informujący, że będziemy kopiować pliki (w Chrome np. przy kursorze pojawia się napis Kopiuj). MetodapreventDefault() natomiast zapewni nam, że przeglądarka nie zacznie się dziwnie zachowywać np. nie będzie zaznaczany tekst podczas przesuwania kursora nad oknem.

Podobne operacje wykonujemy dla zdarzenia przeciwnego czyli ondragleave:

ePage.ondragleave=function(f){
    eDropPlace.innerHTML="Przenieś tu pliki które chcesz<br />wysłać na serwer";
    f.dataTransfer.dropEffect="copy";
    f.preventDefault();
    return false
};

Teraz czas na najważniejszą część kodu która obsłuży upuszczenie plików na okno przeglądarki (zdarzenie ondrop) i doda je do listy.

Zdarzenie ondrop

ePage.ondrop=function(g){
    g.preventDefault();
    files = g.dataTransfer.files;
    eDropPlace.innerHTML ="Przenieś tu pliki które chcesz<br />wysłać na serwer";

    //tu dalsza część kodu
}

Ponownie skorzystaliśmy z metody preventDefault() gdyż przeglądarki domyślnie po upuszczeniu na nie pliku próbują je wyświetlać lub „ściągnąć”. Musimy temu zapobiec aby mógł się wykonać dalszy kod. Gdy to już mamy zapewnione to możemy skorzystać z instrukcji g.dataTransfer.files która zwróci nam tablicę obiektów poszczególnych plików. G zostało nam podane jako argument funkcji która wykona się w momencie zajścia zdarzenia ondrop. A jako, że internauta nie trzyma już plików to nie zajdzie zdarzenie ondragleave więc w powyższym kodzie obsługujemy przywrócenie napisu.

Teraz możemy już sobie iterować po tej tablicy i wyciągać kolejne obiekty plików do uploadu:

for(var i = 0, f; f = files[i]; i++){
    var reader=new FileReader();

    //tu dalsza część kodu

    reader.readAsDataURL(f);
}

Powyższa pętla przypisuje kolejne obiekty plików do zmiennej f, tworzy nowy obiekt klasy FileReader i wczytuje plik do stworzonego obiektu. I teraz naszym zadaniem jest napisanie funkcji która zostanie wykonana gdy cały plik zostanie wczytany (jest on wczytywany asynchronicznie z powodu opóźnienia jakie może wywołać obciążony dysk twardy). Wykonuje się to za pomocą nietypowej konstrukcji która należy umieścić przed użyciem metody readAsDataURL:

reader.onload = (function(theFile) {
    return function(e) {
        //tu dalsza część kodu
    };
})(f);

Jakkolwiek dziwnie powyższy kod wygląda to nadszedł czas na napisanie funkcji tworzącego nowe elementy na liście. Na początek musimy przyjąć, że każdemu nowemu kontenerowi będziemy nadawać ID wg schematu: file1file2file3 itd. Realizuje to poniższy kod:

var newId = "file"+idi; idi++;
$(".uploadList").prepend("<div id=\""+newId+"\" class=\"uploadFile\">"+$("#uploadFileTemplate").html()+"</div>");

Jak widać stworzyliśmy nowy kontener, nadaliśmy mu id i skopiowaliśmy do niego nasz szablon z diva uploadFileTemplate. Nowy element pojawi się na liście plików na samej górze dzięki metodzie prepend z biblioteki jQuery.

Teraz czas na wypełnienie naszego szablonu i nadaniu zdarzeń dla przycisków:

$("#"+newId+" .header .name").text(theFile.name);

//wrzuca miniatirku zdjęć
if(theFile.type.match(/image.*/))
    $("#"+newId+" .thumb .image").attr("src",e.target.result);

Obiekt theFile w tym momencie przechowuje informacje i naszym pliku. Pobieramy z niego nazwę pliku (theFile.name) i wpisujemy w odpowiednim miejscu. Dalej sprawdzamy czy nasz plik jest obrazkiem za pomocą theFile.type.match(/image.*/). Jeśli tak to odczytujemy nasz plik z zmiennej e.target.result i wpisujemy jako atrybutsrc miniaturki. Plik jest przechowywany w wspomnianej zmiennej w formacie base64. Polecam kliknąć na tak wyświetloną grafikę PPM i wybrać opcję Pokaż obrazek. Zobaczymy wtedy w pasku adresu jak wygląda grafika w takim kodowaniu.

//zapisuje plik do pola tekstowego
$("#"+newId+" input[name=\"file\"]").val(e.target.result.split(",")[1]);

//obsługa usuwania pliku
$("#"+newId+" .delete").click(function(z){
    $("#"+newId).remove();
});

//obsługa wysyłania
$("#"+newId+" #startUpload").click(function(z){
    trySendFile(newId);
});

Wracając do naszego szablonu to teraz znowu musimy odczytać nasz plik i zapisać go do pola tekstowego o nazwie file. Teraz jednak przypiszemy tylko sam kod bez informacji o typie kodowania. Przy użyciu javascriptowej metody split wytniemy zbędny w tym przypadku napis data:image/png;base64,. W dalszej części kodu oskryptowujemy przycisk anulowania wysyłania pliku którego kliknięcie spowoduje usunięcie całego kontenera oraz przycisk wysyłania pliku którego kliknięcie wywoła większą funkcję trySendFile.

I to wszystko jeśli chodzi o obsługę zdarzenia ondrop. Czy czegoś nam brakuje? Tak, definicji funkcji…

trySendFile

function trySendFile(eId){
    $("#"+eId+" #startUpload").hide();

    var fileData = $("#"+eId+" input[name=\"file\"]").val();
    fileData = fileData.replace(/\+/gi, '-');
    fileData = fileData.replace(/\//gi, '_');

    //dalsza część kodu
}

Po kliknięciu przycisku wysyłania pierwsze co musi się stać to schowanie owego guzika. Następnie z pola tekstowego file odczytujemy zakodowany plik i podmieniamy w nim pewne znaki. Ktoś może zapytać po co? Po odpowiedź odprowadzam do artykułu o base64 na Wikipedii a dokładniej to do akapitu pod tabelką. My będziemy transportować plik w ciągu zmienna1=wartosc1&zmienna1=wartosc2… więc musimy zadbać by żadne znaki nam nie uszkodziły danych. Dwie linijki z powyższego kodu realizują właśnie podmianę omówioną na Wikipedii.

Teraz możemy w końcu znapisać kod które wyśle wszystkie dane na serwer (np do pliku php/getFile.php):

$.ajax({
    type: "POST",
    url: "php/getFile.php",
    data:   "file="+fileData+
            "&name="+encodeURIComponent($("#"+eId+" .header .name").html())+
            "&description="+encodeURIComponent($("#"+eId+" textarea[name=\"description\"]").val()),
    //tu za chwilę dalszy ciąg kodu
});

Jak widać używamy tu funkcji ajax z biblioteki jQuery. Jako parametr data podajemy ciąg gdzie kolejno składamy zmienne file zawierającą kod base64, name zawierającą nazwę pliku oraz description zawierającą treść z pola tekstowego. Wartości pól namedescription zabezpieczyliśmy funkcją encodeURIComponent aby dało się je bezpiecznie przesłać w tej formie. I tu znowu ktoś może zapytać dlaczego nie użyliśmy tej metody wcześniej przy zabezpieczaniu base64. Powód jest prosty. Wspomniana metoda podmienia wszystkie znaki specjalne. Jeśli plik waży kilka megabajtów (np. plik .mp3) to użycie omawianej metody powoduje chwilowe zawieszenie przeglądarki. A, że na szczęście w base64 występują tylko dwa psujaki to o wiele optymalnej jest użyć podmiany metodą replace.

Dane zostały wysłane więc teraz trzeba by było obsłużyć odpowiedź. Za nim to jednak zrobimy napiszmy:

Kod po stronie serwera

Tu nie będziemy się rozpisywać. Nie podam tu jakiegoś bardzo użytecznego kodu a tylko skrypt który pozwoli przetestować nam uploader i zrozumieć jak należy odbierać kod base64.

<?php
    $name = str_replace('..',' ',$_POST['name']);
    $name = str_replace('/',' ',$_POST['name']);
    $name = str_replace('.exe',' ',$name);

    if(!file_exists("files/".$name)){
        //dalszy kod
    } else echo "error!Taki plik już istnieje";
?>

Jak widać w pliku getFile.php najpierw usuwamy z nazwy pliku podwójne kropki i ukośniki aby zapobiec, że jakiś spryciarz zapisze nam plik po za naszym folderem. Usuwamy także rozszerzenia z plików wykonywalnych dla bezpieczeństwa serwera i osoby która będzie te pliki przeglądać. Gdy to już zrobimy sprawdzamy czy nie ma takiego pliku w folderze do uploadu. Jeśli nie to wykonujemy dalszy kod, w przeciwnym wypadku zwracamy przeglądarce komunikat o błędzie. Komunikaty będziemy zwracać w formacie <typ komunikatu>!<treść komunikatu>. Wspomniany dalszy kod wygląda następująco:

$file = str_replace('-','+',$_POST['file']);
$file = str_replace('_','/',$file);
$f = fopen("files/".$name, "wb");
fwrite($f, base64_decode($file));
fclose($f);
$f = fopen("files/".$name.".txt", "wb");
fwrite($f, $_POST['description']);
fclose($f);
echo "ok!Plik zapisano";

Po pierwsze przywracamy podmienione wcześniej znaki. Po drugie otwieramy plik o odpowiedniej nazwie i zapisujemy do niego odkodowaną za pomocą base64_decodetreść pliku. Po trzecie zaś otwieramy plik o nazwie <nazwa pliku uploadowanego>.txt i wrzucamy do niego opis napisany przez internautę. Po czwarte zwracamy komunikat, że wszystko poczło dobrze i po piąte…

Piszemy ostatni fragment kodu JS

Teraz wracamy do naszej niedokończonej funkcji ajaxowej i dopisujemy odbiór komunikatów:

    error: function(xhr, ajaxOptions, thrownError){
            $("#"+eId+" p.alert").text("Błąd połęczenia");
            $("#"+eId+" #startUpload").show();
        },
    success: function(msg){
            $("#"+eId+" #startUpload").show();
            var data = msg.split("!");
            switch(data[0]){
                case "error":    $("#"+eId+" p.alert").text(data[1]);
                                 break
                case "ok":       $("#"+eId+" p.alert").text(data[1]);
                                 $("#"+eId+" #startUpload").remove();
                                 break;
                default:         alert(msg);
                                 break;        
            }
        }

Funkcja napisana po parametrze error jest wykonywana gdy przeglądarka nie może połączyć się z serwerem lub znaleźć pliku. W takim przypadku wyświetlamy internaucie napis, że Brak połączenia i przywracamy przycisk by mógł spróbować raz jeszcze. Zaś success odnosi się do przypadku gdy serwer zwrócił nam jakąś sensowną odpowiedź. Wtedy także przywracamy przycisk a odpowiedź z serwera (zmienna msg) rozwalamy na dwie części znaną już metodą split. Potem sprawdzamy czy komunikat jest typu error czy ok i wykonujemy odpowiednie operacje: wyświetlamy treść komunikatu i ewentualnie kasujemy przycisk wysyłania jak wszystko poszło ok. Gdyby zaś nie udało nam się rozpoznać typu komunikatu (np. zwrócono nam błąd php) to wyświetlamy całość za pomocć alertu

Gotowiec

Wszystkie pliki potrzebne do uruchomienia stworzone uploadera można pobrać stąd.

 

4 odpowiedzi do tego artykułu

  1. Pod operą (najnowszą) to nie działa.

    • Elektryk

      Tak jak pisałem w pierwszym akapicie: działa tylko pod Chrome i Firefoxem póki co.

  2. Robert

    Witam,

    czy jest możliwość zrobienia przycisku ‘wyślij wszystko’? Jeśli tak, to proszę o nakierowanie.

    Pozdrawiam!

    • Elektryk

      Podczas tworzenia nowego kontenera należałoby kolekcjonować w jakiejś globalnej tablicy wszystkie ID i następnie przy kliknięciu w przycisk ‘Wyślij wszystko’ wykonać funkcję trySendFile() na wszystkich nich po kolei.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

*

Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Markup Controls
Emoticons Smile Grin Sad Surprised Shocked Confused Cool Mad Razz Neutral Wink Lol Red Face Cry Evil Twisted Roll Exclaim Question Idea Arrow Mr Green