W tym tutorialu wykonamy skrypt odpowiedzialny za wysyłanie wielu plików na raz za pomocą PHP na serwer. Co więcej, wykorzystując AJAX w stylu jQuery wykonamy pasek postępu (lub jak kto woli: progressbar), który znacznie poprawi atrakcyjność interakcji z naszą aplikacją internetową przy tym procesie.

Aby wizualnie wszystko było ładniejsze użyję biblioteki Bootstrap gdzie mamy możliwość stworzenia estetycznego formularza do uploadu pliku na serwer oraz przyjemnego dla oka i animowane paska postępu.

HTML5 – formularz HTML do wysyłania plików

W części HTML należy dodać formularz do wysłania pliku. Aby można było dodać więcej niż jeden plik należy dodać artybut multiple:

<form action="upload.php" method="POST" enctype="multipart/form-data">
         <input type="submit" value="Wyślij pliki"/>
            <input type="file" class="custom-file-input"  name="image[]" multiple="">
</form>

Jeżeli zależy nam na wysłaniu tylko jednego pliku możemy po prostu usunąć atrybut multiple.

Całość po ostylowaniu powinna wyglądać mniej-więcej tak:

Formularz do wysyłania plików

Gdzieś pod formularzem dodajmy sobie pasek postępu ustawmy go na 0% i ukryjmy:

<div class="card-body">
   <h4 class="card-title">Pliki w trakcie wysyłania, nie zamykaj tego okna… <i class="fa fa-upload"></i></h4>
   <div class="progress m-t-20">
      <div class="progress-bar bg-success" style="width: 0%; height:15px;" role="progressbar">0%</div>
   </div>
</div>
Pasek postępu

PHP – zapis plików po stronie serwera (wiele plików na raz)

W części odpowiedzialnej za PHP mamy za to taki skrypt. Folder „pliki” musi istnieć tam gdzie jest skrypt. Oczywiście, można tam wprowadzić własną ścieżkę:

<?php
if(isset($_FILES['image'])){
     $errors= array();
     $file_name = $_FILES['image']['name'];
     $file_size =$_FILES['image']['size'];
     $file_tmp =$_FILES['image']['tmp_name']; 
     $file_type=$_FILES['image']['type'];
     $extensions= array("jpeg","jpg","png", "webp", "pdf"); 
     foreach($file_name as $key => $value){ 
         $tmp = explode('.',$_FILES['image']['name'][$key]);
         $file_ext = strtolower(end($tmp));
         if(in_array($file_ext,$extensions)=== false){
             $errors[]="Rozszerzenie niedozwolone.";
         } 
         if($file_size[$key] > 2097152){
             $errors[]='Plik nie może być większy niż 2 MB.';
         } 
     }  
     if(empty($errors)==true){        
         foreach($file_name as $key => $value){ 
             move_uploaded_file($file_tmp[$key],"pliki/".$file_name[$key]);
             echo "Pliki poprawnie wysłane!";
         } 
     }
     else{
     print_r($errors);
     }
 }

Jeżeli zależy nam na wysłaniu pojedynczego pliku wystarczy usunąć foreach i [$key], który odnosi się do poszczególnych pól tablicy.

Na początku sprawdzamy czy cokolwiek zostało wprowadzone w polu input o nazwie image. Tworzymy tablicę błędów aby zbierać po drodze ewentualne błędy. Później wykorzystujemy tę samą strukturę danych ale dla dozwolonych rozszerzeń plików przesyłanych plików. Sprawdzamy czy pliki są zgodne z wymogami. Jeżeli tak to w pętli foreach używamy funkcji odpowiedzialnej za upload plików: move_uploaded_file() i tam określamy ewentualnie ścieżkę do zapisu. Jak widać u mnie zapisuje się do w folderze o niezbyt oryginalnej nazwie – files.

Oczywiście wymogi co do rozszerzenia pliku czy jego wielkości można dowolnie podmienić (wielkość w granicach ustawień serwera)

JS/jQuery/AJAX – progress bar czyli pasek postępu

Poniższy kod robi całą sprawę. Kluczowa jest tutaj zmienna xhr, która przechowuje obiekt window.XMLHttpRequest oraz eventListener „progress:

$('form').submit(function(e){
     e.preventDefault();
     var formData = new FormData($(this)[0]);
$.ajax({
      xhr: function() {
         var xhr = new window.XMLHttpRequest();
         xhr.upload.addEventListener("progress", function(evt) {
             if (evt.lengthComputable) {
                 var percentComplete = evt.loaded / evt.total;
                 var progressval = Math.round(percentComplete*100)+'%';
$('.progress-bar').css('width', progressval); /* Animacja paska postępu */
$('.progress-bar').text(progressval);  /* Zmiana tekstu z procentami */
             }
        }, false);
        return xhr;
},
     url:"upload.php",
     method:"POST",
     data: formData,
     contentType:false,
     processData:false,
     enctype: 'multipart/form-data',
     success:function(output){
         /* Instrukcje wykonywane po poprawnym załadowaniu */    
     }
 });
});

Przed tym kodem musi się znajdować odniesienie do biblioteki jQuery, np:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>

Wszędzie stosuj własne nazwy plików. Ten przykład odnosi się do sytuacji kiedy zarówno kod php jak i JS znajduje się w pliku upload.php. Dobrym pomysłem byłoby rozdzielenie kodu na osobne pliki index.php zawierający formularz i kod JavaScript, oraz plik upload.php, który zawiera tylko kod PHP odpowiedzialny za zapis obrazka.

Za pomocą metody CSS powiększamy szerokość paska, który na początku jest ustawiony na 0. Po zakończeniu wysyłania plików możemy wyświetlić jakiś alert lub ukryć pasek – wedle uznania.

Ten kod pozostaje niezmieniony w przypadku wysyłania jednego lub wielu plików.

Kwestie bezpieczeństwa, utrzymanie i obsługa błędów

Mam jeszcze kilka porad, które mogą się przydać w aplikacji agregującej pliki użytkowników za pomocą przesyłania (ang. upload):

Jeżeli wysyłanie plików kończy się niepowodzeniem prawdopodobnie problem powoduje nieprawidłowy chmod do folderu lub błędna ścieżka (folder do którego wysyłamy pliki musi istnieć).

Dobrze jest zabronić wysyłania plików .php i innych skryptów które można włączyć w przeglądarce. Użytkownik, który może wysyłać pliki wykonywalne a potem je uruchamiać automatycznie uzyskuje pełny dostęp do PHP serwera. To dlatego nawet w najprostszych skryptach do wysyłania plików implementuje się proste zabezpieczenie polegające na filtrowaniu plików na podstawie rozszerzenia.

Pamiętaj, że pliku przy takim wysyłaniu są nadpisywane. Aby sobie z tym poradzić, częstą praktyką jest zmiana nazwy pliku na losowy hash, pieczątkę czasu lub dopisanie do istniejącej nazwy losowego ciągu lub czasu uniksowego.

Ilość plików w jednym folderze w systemach uniksowych może być z góry ograniczona z powodu wykorzystywania danego systemu plików. Rozdzielaj równomiernie pliki do osobnych folderów. Można tutaj polegać na aktualnej dacie lub odczytywać ilość plików w bieżącym folderze za pomocą funkcji scandir();

Pamiętaj że miejsce na serwerze jest skończone. Jeżeli kończy Ci się miejsce wykorzystaj dowolną chmurę do przechowywania statycznej zawartości. Przerobienie tego skryptu na taki który wyśle pliki do chmury jest trywialne – możesz to wykonać w pętli po poprawnym zapisaniu się pliku na serwerze.

Pamiętaj aby zablokować możliwość odczytu folderu files kierując się interesami i prywatnością użytkowników. Wystarczy dodać pusty plik index.php.

Rozważ wykorzystanie archiwów zip – w przypadku kiedy spodziewasz się przysłania wielu drobnych plików – to zaoszczędzi czas i zasoby serwera. Czytaj jak to zrobić w tutorialu: pakowanie i rozpakowywanie ZIP w PHP.

Efekt końcowy

Pasek pokazałem za pomocą metody SlideDown() w tej głównej metodzie submit(). Na końcu jest odświeżenie bo zaraz obok jest lista załadowanych plików a nie chciało mi się tego robić asynchronicznie:

Dodatek 1: Hasze i pieczątki czasu w nazwie pliku

Do nazw plików można dodawać losowe ciągi znaków by zapobiec nadpisywaniu plików o identycznych nazwach lub zapobiec sytuacji, że ktoś będzie próbował odgadnąć nazwy innych plików, które nie są dla niego przeznaczone. Jak tego dokonać? Wystarczy dokonać preferowanej konkatenacji w linijce z nazwą pliku:

Zamiast tej linii:

move_uploaded_file($file_tmp[$key],"pliki/".$file_name[$key]);

Możemy umieścić coś takiego:

move_uploaded_file($file_tmp[$key],"pliki/".time().$file_name[$key]);

To doda na początku nazwy pliku ilość sekund jaka minęła od początku epoki unixowej (Unix Epoch). Można nieco obfuskować tę wartość poprzez zamienienie time() na dechex(time()); Taki hash można traktować jako dodatkowe meta-dane, które trwale zapisują datę wysłania pliku w jego nazwie. Nadal jednak istnieje problem kolizji, jeżeli w tym samym czasie wyślemy identycznie nazwane pliki.

Pseudolosową liczbę z przedziału od 0 do 1000 w PHP można wygenerować za pomocą funkcji rand(0, 1000); Jednak najefektywniej prawdziwy hasz w PHP można uzyskać za pomocą funkcji bin2hex() oraz randombytes():

$filename_hash = bin2hex(random_bytes(16));
move_uploaded_file($file_tmp[$key],"pliki/".$filename_hash.$file_name[$key]);

Podsumowanie

Pasek postępu jest bardzo ważny w przypadku wysyłania wielu plików. Informuje użytkownika ile jeszcze trzeba poczekać na zakończenie wysyłania i może zapobiec wystąpieniu różnego rodzaju błędów. To bardzo podstawowa wersja skryptu, którą można dopracowywać według własnych potrzeb i pomysłów.

Oceń artykuł na temat: Wysyłanie plików na serwer PHP (z paskiem postępu)
Średnia : 4.6 , Maksymalnie : 5 , Głosów : 17