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:
- Wskazanie wielu plików i upload jeden po drugim.
- Plik ładowany w segmentach jeden po drugim, tzn, że następny segment jest ładowany tylko wtedy, gdy poprzedni zostanie przetworzony po stronie serwera.
- 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ą.