Upload plików a smartfony i tablety

0

Chciałbym utworzyć prostą aplikację webową, w której jest możliwy upload plików, tzn. użytkownik wskazuje plik na swoim dysku i on ładuje się.

Wymagania mam takie:

  1. Ładowanie dowolnie dużego pliku (w praktyce pliki będą mieć od kilku MB do góra 4-5GB na plik)
  2. Bez zbędnych bajerów, najprościej jak można
  3. Widoczny postęp ładowania pliku
  4. Musi działać na współcześnie używanych smartfonach i tabletach IOS i Android, najlepiej trównież Windows Phone lub przynajmniej Android

Wymyśliłem, że to będzie ładowanie danych w kawałkach. Od strony serwera (robię a ASP.NET) skorzystam z JavaScript Callback https://msdn.microsoft.com/en-us/library/ms178208.aspx , szukałem, jak zrealizować ładowanie pliku w kawałkach. Trafiałem na biblioteki JQuery, nb-flow i jakieś inne, ale znalazłem to:

http://www.html5rocks.com/en/tutorials/file/dndfiles/

Wygląda obiecująco, prościej już się nie da, a nie chcę strzelać do muchy z armaty, jednak mam wątpliwości co do działania. Na komputerze PC z aktualną przeglądarką nie będzie żadnego problemu, ale jak wygląda obsługa HTML5 i FileApi w Androidzie i IOS z fabrycznym oprogramowaniem? Na jakiej wersji Androida i IOS będzie to działać (obsługuje FileApi i HTML5)?

Idea jest taka, że po wybraniu pliku i kliknięciu przycisku przeglądarka wyśle pierwszy fragment pliku, serwer przyjmie i zapisze, potem klient wykona wywołanie zwrotne, wyświetli postęp i wyśle następny kawałek pliku, i tak w kółko. Z tym to na razie będę próbować sam, pytanie było, czy mogę w ciemno wykorzystać FileApi od Javascriptu wierząc, że zadziała na każdym smartfonie i tablecie nie starszym niż 2-3 lata. Jak nie, to muszę poszukać innego sposobu odczytu pliku i wysłania żądanego kawałka.

0

Jest jednak dużo większy problem z JavaScript.

Testuję przykłady na http://www.html5rocks.com/en/tutorials/file/dndfiles/ na plikach od 500MB do 4GB.

Pierwszy przykład działa poprawnie, tzn., prawidłowo podaje wielkość pliku, natomiast ostatni przykład (ładowanie z paskiem postępu), po wczytaniu dużego pliku zatrzymuje się w trakcie i pokazuje komunikat "File not found". Co jest tego przyczyną? Przy pliku 100MB i mniejszym nie ma żadnego problemu.

0

Wielkość Bloba w przeglądarkach jest ograniczona. Sam FileReader nie wiem czy sobie ze wszystkim radzi, chyba tak (nie znalazłem sensownych informacji, o blobach też za dużo nie ma). Więc dzielenia na mniejsze kawałki nie zrobisz zawsze.

Tu masz info jaka przeglądarka ile udźwignie:
https://github.com/eligrey/FileSaver.js/

Info może być nieaktualne, wisi tak od długiego czasu, a z tego co wiem Chrome miał zwiększać ten limit (choć możliwe, że jeszcze tego nie zrobili).

0

Nie wiedziałem, że ostatni przykład, to wczytywanie całego pliku do pamięci, co na pewno nie uda się przy dużych plikach, ale też nie o to chodziło.

Udało mi się zrealizować funkcjonalność po stronie klienta symulując wywołania procedur serwera.

Na pierwszy rzut oka działa poprawnie, jak będę mieć trochę czasu, to będę realizować stronę serwerową. Wtedy wyrzucę procedurę InvokeCallback, bo tą procedurę będzie dodawać ASP.NET przy kompilacji i generowaniu HTMLa.

<html>
 <head>
 </head>
 <body>
  <form name="AppForm">
   <script type="text/javascript">
    // Lista obiektow plikow
    var FileList = [];

    // Wielkosc jednego segmentu
    var FileChunkSize = 1000000;

    // Iterator listy plikow
    var FileListCurrent = 0;

    // Liczba plikow
    var FileListCount = 0;

    // Identyfikator pliku
    var FileListName = [];

    // Wielkosc pliku
    var FileListSize = [];

    // Polozenie poczatku segmentu
    var FileListOffset = [];

    // Okresla, czy trwa ladowanie danych
    var FileWorking = 0;

    // Czyszczenie tekstu kontrolnego
    function Clear()
    {
     document.getElementById("Debug").innerHTML = "";
    }

    // Wyswietlanie tekstu kontrolnego
    function Print(X)
    {
     document.getElementById("Debug").innerHTML += X + "<br />";
    }

    // Rozpoczecie ladowania plikow
    function fStart(evt)
    {
     Clear();
     FileWorking = 1;
     Print("Start");
     UploadChunk();
    }

    // Przerwanie ladowania plikow
    function fStop(evt)
    {
     Print("Abort");
     FileWorking = 0;
    }

    // Ladowanie jednego segmentu
    function UploadChunk()
    {
     // Aktualizacja statusu wysylania
     UpdateFileList();

     // Sprawdzanie, czy ladowanie segmentow nie zostalo zakonczone lub przerwane
     if (!FileWorking)
     {
      Print("Stop");
      return;
     }

     // Informacje o segmencie dla serwera - Nazwa pliku, wielkosc pliku i polozenie segmentu
     var ChunkInfo = FileListName[FileListCurrent] + '|' + FileListSize[FileListCurrent] + '|' + FileListOffset[FileListCurrent] + '|';

     // Obliczanie wielkosci segmentu
     var CurrentChunkSize = FileChunkSize;
     if ((FileListOffset[FileListCurrent] + CurrentChunkSize) > FileListSize[FileListCurrent])
     {
      CurrentChunkSize = FileListSize[FileListCurrent] - FileListOffset[FileListCurrent];
     }

     // Odczyt segmentu z pliku w sposob asynchroniczny
     var reader = new FileReader();
     reader.onerror = function(evt)
     {
      switch(evt.target.error.code)
      {
       case evt.target.error.NOT_FOUND_ERR: Print("File Not Found!"); break;
       case evt.target.error.NOT_READABLE_ERR: Print("File is not readable"); break;
       case evt.target.error.ABORT_ERR: Print("Aborted"); break; // noop
       default: Print("An error occurred reading this file."); break;
      }
     };
     reader.onloadend = function(evt)
     {
      if (evt.target.readyState == FileReader.DONE)
      {
       // Wywolanie zaladowania segmentu na serwer (w celach testowych zamiast tresci segmentu jest informacja o wielkosci odczytanego segmentu)
//       InvokeCallback(ChunkInfo + "{" + evt.target.result.length + "}");
       InvokeCallback(ChunkInfo + evt.target.result);
      }
     };
     var blob = FileList[FileListCurrent].slice(FileListOffset[FileListCurrent], FileListOffset[FileListCurrent] + CurrentChunkSize);
     reader.readAsDataURL(blob);
//     reader.readAsArrayBuffer(blob);
    }



    // Zdarzenie wybrania plikow - przygotowanie listy plikow i zerowanie statusu wyslania
    function fSelect(evt)
    {
     FileList = evt.files;
     FileListCount = FileList.length;
     FileListCurrent = 0;
     for (var i = 0; i < FileListCount; i++)
     {
      FileListOffset[i] = 0;
      FileListSize[i] = FileList[i].size;
      FileListName[i] = FileList[i].name;
     }
     UpdateFileList();
    }

    // Aktualizacja listy plikow ze statusami zaladowania
    function UpdateFileList()
    {
     document.getElementById("FileListDisp").innerHTML = "";
     var DispPercent = 0;
     for (var i = 0; i < FileListCount; i++)
     {
      if (FileListSize[i] > 0)
      {
       DispPercent = Math.floor(FileListOffset[i] * 100 / FileListSize[i]);
      }
      document.getElementById("FileListDisp").innerHTML += FileListName[i] + "     " +  FileListOffset[i] + "/" + FileListSize[i] + " (" + DispPercent + "%)" + "<br />";
     }
    }


    // Wywolanie procedury serwera (symulowane)
    function InvokeCallback(Param)
    {
     setTimeout(function(){ CallbackEvent(Param); }, 5);
    }


    // Procedura wywolywana po wykonaniu czynnosci przez serwer
    function CallbackEvent(Result, Context)
    {
     // Wyswietlanie tekstu zwracanego przez serwer
     Print(Result);

     // Przesuwanie wskaznika pliku, sprawdzanie, czy caly plik zostal zaladowany
     FileListOffset[FileListCurrent] += FileChunkSize;
     if (FileListOffset[FileListCurrent] > FileListSize[FileListCurrent])
     {
      FileListOffset[FileListCurrent] = FileListSize[FileListCurrent];
      FileListCurrent++;
      if (FileListCurrent >= FileListCount)
      {
       FileWorking = 0;
      }
     }

     // Odczyt i wyslanie nastepnego segmentu pliku
     UploadChunk();
    }
   </script>
   <input type="file" id="PlikTest" name="files[]" multiple="multiple" onchange="fSelect(this)">
   <br />
   <a href="javascript:void(0)" onclick="fStart()">Start</a>
   <a href="javascript:void(0)" onclick="fStop()">Stop</a>
   <br />
   <br />
  </form>
  <p id="FileListDisp"></p>
  <p id="Debug"></p>
 </body>
</html>

Zrealizowane założenia:

  1. Wskazanie wielu plików i upload jeden po drugim.
  2. Plik ładowany w segmentach jeden po drugim, tzn, że następny segment jest ładowany tylko wtedy, gdy poprzedni zostanie przetworzony po stronie serwera.
  3. Wyświetlony postęp w ładowaniu plików.

Czytałem o AJAX, ale tam to w każdym wywołaniu trzeba tworzyć HTTPRequest czy jakoś tak i w nim wysłać komunikat, jaki potrzeba. Tutaj tylko uruchamia się jedną funkcję, a całą resztą zajmuje się ASP.NET.

Wyczytałem gdzieś, że readAsBinaryString jest "deprecated", więc pozostaje readAsDataURL lub readAsArrayBuffer, readAsTextString nie nadaje się, bo musi obsługiwać pliki binarne, więc pozostaje przesyłanie w BASE64.

Komunikat zawiera wszystkie informacje złączone znakiem '|', gdzie po stronie serwera zrobi się "split" i otrzyma się tablicę z wymaganymi danymi. Użycie JSON lub XML do tego celu to jak armata na muchę. Docelowo ma zawierać nazwę pliku, wielkość pliku, położenie segmentu i treść segmentu w BASE64.

Czy powyższe podejście ładowania plików praktykuje się w poważnych projektach (pomijam kwestię obsługi błędów uprawnień do pliku, niepożądanych działań użytkownika, itp.)? Czy jest coś, co warto zmienić lub ulepszyć?

W jaki sposób na ogół realizowało się upload dużego pliku z postępem, jak jeszcze nie znano HTML5? Czy było to możliwe bez Flasha, apletów Javy itp., żeby działało na przykład w IE starszych niż 10? Generalnie chodzi o odczyt wielkości pliku oraz odczyt wskazanego fragmentu pliku, cała reszta to żaden problem.

Na pierwszy rzut oka, z tego, co czytałem, w bieżących wersjach Androida i IOS nie powinno być żadnego problemu, ale będę testować u kolegów, jak uruchomię wersję używalną.

0

Wykorzystując kod umieszczony w poprzednim poście, napisałem całą aplikację ASP.NET realizującą proces wgrywania plików i aplikacja działa poprawnie.

Właśnie myślę, jak ulepszyć proces uploadu. Pierwsza rzecz, to jeżeli komunikaty do serwera przechodzą synchronicznie, czyli najpierw od klienta do serwera, a potem od serwera do klienta, potem znowu od klienta do serwera, to kolejność komunikatów jest z góry ustalona. W takim razie wydaje się, że można przysyłać komunikaty, w których jest kod rozkazu i komunikat, czyli otwarcie pliku, a potem sama treść bez podawania danych pliku za każdym razem.

Testowo działa to tak, że na serwerze plik jest otwierany, wprowadzana zawartość, a następnie zamykany. Myślę, że byłoby lepiej, gdyby plik był otwarty i pozostawał otwarty cały czas (obiekt strumienia plikowego byłby w sesji), to by się tylko dorzucało kolejne partie danych. Jednak widzę problem taki, że użytkownik zamknie przeglądarkę lub łączność z serwerem zostanie zerwana. Jak oprogramować w ASP.NET, żeby po ostatniej czynności zamykać plik po upływie 5 minut w przypadku braku nowych danych ani polecenia zamknięcia? Wydaje się, że stopwatch lub timer się nada (w osobnym wątku odmierzyć czas i zamknąć plik), ale możliwe, że są lepsze sposoby.

Czy podczas takich czynności praktykuje się otwieranie i zamykanie pliku przy każdej iteracji, czy otwarcie przy pierwszej i zamknięcie przy ostatniej iteracji?

Druga sprawa to wielkość jednego segmentu. Ja myślę o ok. 1MB, można też 100kB. Jakiego rzędu wielkości segmentu w takich celach się praktykuje? Przy zbyt małym proces wprowadzania danych się spowolni, bo będzie więcej iteracji, a przy zbyt dużym obawiam się problemów z łącznością i zawieszaniem się przeglądarki przy bardzo słabych łączach.

0
andrzejlisek napisał(a):

Druga sprawa to wielkość jednego segmentu. Ja myślę o ok. 1MB, można też 100kB. Jakiego rzędu wielkości segmentu w takich celach się praktykuje? Przy zbyt małym proces wprowadzania danych się spowolni, bo będzie więcej iteracji, a przy zbyt dużym obawiam się problemów z łącznością i zawieszaniem się przeglądarki przy bardzo słabych łączach.

100KB to zdecydowanie za mało przy dzisiejszych łączach. U mnie oznaczałoby to 15 fragmentów plików na sekundę. Zmiana wielkości chunku w trakcie byłaby pewnie najlepszą opcją (bazując na zmierzonej prędkości wysyłania), ale jednocześnie trudniejszą do implementacji niż hardkodowana wartość.

A zamiast readAsBinaryString polecają korzystać readAsArrayBuffer i z tego co po kodzie widzę - tak właśnie robisz. Ja sam nigdy nie miałem potrzeby pisania takiego uploadu, więc jestem trochę ciemny w tych tematach.

1 użytkowników online, w tym zalogowanych: 0, gości: 1