Wątek przeniesiony 2023-12-26 20:23 z JavaScript przez Riddle.

Jak wczytać zawartość archiwum ZIP z komputera użytkownika?

0

Chciałbym zrobić w HTML/JS coś takiego, że pobiera się pliki od użytkownika (jeden lub wiele plików), dla każdego wykonuje się pewne przetwarzanie lub zapamiętanie, dla którego danymi wejściowymi jest nazwa i treść pliku w formie BASE64. Na potrzeby tego tematu, owe "przetwarzanie" to wypisanie komunikatu z nazwą i fragmentem treści. Dodatkowo, jeżeli jest plik ZIP, to przetworzony ma być nie ten plik ZIP, tylko jego zawartość. Do tego celu znalazłem to: https://stuk.github.io/jszip/

Zrobiłem kod, który działa, robi to, co ma robić, ale jest przekombinowany, okrężny, stąd zakładam temat.

Tak naprawdę, są dwa sposoby przetwarzania ZIP, z których pierwszy FileZipProcess wykorzystuje zmienne globalne i robi to prawidłowo, a drugi sposób FileZipProcess2Test nie korzysta ze zmiennych globalnych, ale źle przypisuje nazwy plików, tylko same treści plików przetwarza zgodnie z oczekiwaniem.

Pytania są następujące:

  1. Funkcja FileUploadEvent - W jaki sposób prawidłowo zrobić przetworzenie listy plików pobranych od użytkownika? We funkcji ja dorabiam pole X do FileReader po to, żeby jakoś zachować nazwę i korzystam z tego, że w zdarzeniu onload mam dostęp do tego samego obiektu FileReader. Działa to poprawnie, ale w jaki sposób to się powinno zrobić, żeby w zdarzeniu onload mieć i treść pliku i nazwę pliku?
  2. Funkcja FileZipProcess2Test - W tym przypadku nie jest możliwe zrobienie tego samego, co w funkcji FileUploadEvent, bo w argumencie wywołania then mam samą treść pliku, nic więcej. Jak skojarzyć obiekt, który wywołał daną obietnicę? Przekazanie nazwy pliku z poza wywołania zdarzenia nie daje oczekiwanego efektu, dla każdej pozycji jest przekazywana nazwa ostatniego pliku.
  3. Rozumiem, że taki asynchroniczny styl programowania ma swoje zalety widoczne, jak operacje są długotrwałe, ale skąd taka duża popularność i wręcz przymus takiego programowania, bez możliwości wyboru? W tym przypadku, jak w 95% przypadków, wywołania asynchroniczne trwają niemierzalny ułamek sekundy, a przez to zaprogramowanie tego samego jest trudniejsze, dłuższy kod trzeba wyprodukować, kod jest mniej czytelny, działanie programu jest bardziej skomplikowane, podczas, gdy to samo np. w C++ zrobiłbym w pół godziny za pomocą zwykłej pętli for i rekurencji, bez zadawania żadnych pytań.
  4. Ostatnie, ale najważniejsze. W jaki sposób skrócić kod, żeby robił to samo, ale bez zmiennych globalnych i prawidłowo?
<!doctype html>
<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Test</title>
  </head>
  <body>
    <script type='text/javascript' src="jszip.js"></script>
    <script type='text/javascript'>
    

        // Lista nazw plików wewnątrz ZIP
        let FileZipName = [];
        
        // Lista treści plików wewnątrz ZIP
        let FileZipData = [];
        
        // Obiekt listy obiektów-plików z biblioteki JSZip
        let FileZipObj;


        // Funkcja wykonujaca jakies przetwarzanie, która przyjmuje nazwe pliku i treść pliku w formie BASE64
        function FileProcess(Name, Data)
        {
            let DataView = Data.length > 100 ? Data.substr(0, 100) : Data;
            console.log("Jakaś czynność z plikiem o nazwie \"" + Name + "\" i treści " + DataView);
        }


        // Przetwarzanie pliku ZIP poprzez wyciąganie plików składowych i przetwarzanie tych plików
        function FileZipProcess()
        {
            // Zmienna FileZipData zawiera nazwy wszystkich plików składowych
            // Przy pierwszym wywołaniu tej funkcji, zmienna FileZipData jest pusta tablicą,
            // przy kolejnych wywołaniach FileZipData zawiera treści plików uzyskacne przy poprzednich wywołaniach
            
            let I = FileZipData.length;
            if (FileZipName.length > I)
            {
                // Jeżeli tablica treści (FileZipData) jest krótsza niż tablica nazw (FileZipName),
                // to należy odczytać i zapamiętać treść pliku o nazwie będącej
                // na pozycji kolejnej niż ostatnia pozycja tablicy treści plików.
            
                // Odczytanie treści w formie BASE64 i wywołanie zdarzenia po odczytaniu
                FileZipObj[FileZipName[I]].async("base64").then(function (XData) {
                
                    // Wstawienie treści jako kolejny element tablicy treści
                    FileZipData.push(XData);
                    
                    // Ponowne wywołanie funkcji przetwarzania pliku ZIP
                    FileZipProcess();
                });

            }
            else
            {
                // Jeżeli tablice FileZipName i FileZipData są tej samej długości, to znaczy,
                // że zostały odczytane treści wszystkich plików i przyporzadkowane do nazw,
                // w tym momencie można wykonać docelowe przetwarzanie nazw i treści plików
                for (I = 0; I < FileZipName.length; I++)
                {
                    FileProcess(FileZipName[I], FileZipData[I]);
                }
            }
        }

        // Inny sposób przetwarzania pliku ZIP w celach testowych
        function FileZipProcess2Test(FileList)
        {
            console.log(FileList);
        
            // Przetwarzane są tylko nazwy plików, katalogi sa pomijane
            for (F in FileList)
            {
                if (F.length > 0)
                {
                    if (F.charAt(F.length - 1) != '/')
                    {
                        console.log("Nazwa pliku \"" + F + "\"");
                        
                                
                        // Odczytanie treści w formie BASE64 i przetworzenie treści
                        FileList[F].async("base64").then(function (XData) {
                        
                            // W tym miejscu nie wiadomo, jaką nazwę ma przetwarzana pozycja,
                            // nazwa przekazywana do funkcji przetwarzającej w tym miejscu jest błędna
                            FileProcess(F, XData);
                        });                        
                                
                        
                    }
                }
            }
        }



        // Ładowanie pliku - czynność wykonywana na jednej parze składającej się z nazwy i treści pliku
        function FileLoad(Name, Data) {
                
            // Sprawdzanie, czy jest to ZIP po rozszerzeniu nazwy
            let IsZIP = false;
            if (Name.length > 4) {
                if (Name.substr(Name.length - 4).toLowerCase() == ".zip") { IsZIP = true; }
            }
            
            console.log("Ładowanie pliku " + Name + " - plik ZIP: " + IsZIP);
            
            if (IsZIP)
            {
                // Tworzenie obiektu JSZip z biblioteki
                var zip = new JSZip();

                // Czyszczenie zmiennych globalnych związanych z plikiem ZIP
                ZipFileObj = [];
                FileZipName = [];
                FileZipData = [];
                
                // Ładowanie treści pliku ZIP do obiektu biblioteki
                zip.loadAsync(FileTextToBlob(Data)).then(function (zip) {
                
                    // Po załadowaniu treści - zachowanie listy plików składowych
                    FileZipObj = zip.files;
                    
                    // Zachowanie globalne listy nazw plików wewnątrz plików ZIP
                    // Katalogi nie są zapamiętywane, zapamiętane sa nazwy plików ze ścieżkami
                    for (F in FileZipObj)
                    {
                        if (F.length > 0)
                        {
                            if (F.charAt(F.length - 1) != '/') { FileZipName.push(F); }
                        }
                    }
                    
                    // Rozpoczęcie przetwarzania globalnej listy plików
                    if (confirm("Przetworzyć ZIP w sposób 1 - zmienne globalne?"))
                    {
                        FileZipProcess();
                    }
                    
                    // Inny sposób przetwarzania plików ZIP w cealach testowych
                    if (confirm("Przetworzyć ZIP w sposób 2 test - tylko zmienne logalne?"))
                    {
                        FileZipProcess2Test(zip.files);
                    }
                });

            }
            else
            {
                // Przetworzenie pliku, który nie jest plikiem ZIP
                FileProcess(Name, Data);
            }
        }

        // Zamiana tekstu BASE64 na Blob - potrzebne do załadowania treści pliku ZIP do biblioteki
        function FileTextToBlob(Data) {
            const DataStr = atob(Data);
            const DataNum = new Array(DataStr.length);
            for (let i = 0; i < DataStr.length; i++)
            {
                DataNum[i] = DataStr.charCodeAt(i);
            }
            return new Blob([new Uint8Array(DataNum)], {type: "application/octet-stream"});
        }


        // Zdarzenie po wybraniu plików w polu typu "file"
        function FileUploadEvent()
        {
            // Pole typu "file"
            let Fld = document.getElementById("TempUpload");
            
            // Odczytanie i przetworzenie treści dla każdego wskazanego pliku,
            // możliwe jest jednoczesne wskazanie wielu plików
            for (var I = 0; I < Fld.files.length; I++)
            {
                let FR = new FileReader();
                
                // Dorobienie pola "X", żeby przenieść nazwę pliku
                FR.X = Fld.files[I].name;
                
                // Zdarzenie po pobraniu treści pliku
                FR.onload = function(e) {
                    // Parametrami funkcji FileLoad jest nazwa i treść pliku w formie BASE64
                    // Nazwa jest przekazywana dzięki dodatkowemu polu "X"
                    // i możliwości dotarcia do obiektu FilleReader,
                    // do którego jest dorobione pole "X"
                    FileLoad(e.target.X, e.target.result.substr(e.target.result.indexOf(',') + 1));
                };
                
                // Wywołanie pobrania treści pliku
                FR.readAsDataURL(Fld.files[I]);
            }
        }
    
    
    </script>

    <input type="file" id="TempUpload" onchange="FileUploadEvent()" multiple="multiple">



    
    
  </body>
</html>
1
andrzejlisek napisał(a):

Pytania są następujące:

  1. Funkcja FileUploadEvent - W jaki sposób prawidłowo zrobić przetworzenie listy plików pobranych od użytkownika? We funkcji ja dorabiam pole X do FileReader po to, żeby jakoś zachować nazwę i korzystam z tego, że w zdarzeniu onload mam dostęp do tego samego obiektu FileReader. Działa to poprawnie, ale w jaki sposób to się powinno zrobić, żeby w zdarzeniu onload mieć i treść pliku i nazwę pliku?
andrzejlisek napisał(a):
  1. Funkcja FileZipProcess2Test - W tym przypadku nie jest możliwe zrobienie tego samego, co w funkcji FileUploadEvent, bo w argumencie wywołania then mam samą treść pliku, nic więcej. Jak skojarzyć obiekt, który wywołał daną obietnicę? Przekazanie nazwy pliku z poza wywołania zdarzenia nie daje oczekiwanego efektu, dla każdej pozycji jest przekazywana nazwa ostatniego pliku.

Hmm... Błąd masz przez pętle, deklarujesz je tak:

for (F in FileZipObj)

a zdefiniowana w ten sposób pętla zadeklaruje zmienną F jako globalną.

Powinieneś zrobić:

for (const F in FileZipObj)

To samo z Twoimi pętlami które iterują po i, zamień je tak żeby były:

for (I = 0; I < FileZipName.length; I++) // to tworzy zmienną globalną, której wartość będzie taka sama dla wszystkich callbacków

for (let I = 0; I < FileZipName.length; I++)  // to będzie miało poprawną wartość
andrzejlisek napisał(a):
  1. Rozumiem, że taki asynchroniczny styl programowania ma swoje zalety widoczne, jak operacje są długotrwałe, ale skąd taka duża popularność i wręcz przymus takiego programowania, bez możliwości wyboru?

Dlatego, że większość (jak nie wszystkie) silniki JavaScript działają w jednym wątku. Koniecznością stało się wiec wymyślenie mechanizmu, który pozwalałby wykonywać operacje nie blokujące głównego wątku, tak żeby nie zablokować UI. To jest częsty problem np w aplikacjach desktopowych - że np klikasz jakiś przycisk i cała aplikacja freezuje na pół sekundy? To własnie dlatego. Jednym z takich mechanizmów jest odroczenie wykonania pewnego kodu w callbacki - czyli programowanie asynchroniczne. Później callbacki zostały też owrapowane w Promise'y, ale to jest praktycznie to samo.

andrzejlisek napisał(a):

W tym przypadku, jak w 95% przypadków, wywołania asynchroniczne trwają niemierzalny ułamek sekundy, a przez to zaprogramowanie tego samego jest trudniejsze, dłuższy kod trzeba wyprodukować, kod jest mniej czytelny, działanie programu jest bardziej skomplikowane, podczas, gdy to samo np. w C++ zrobiłbym w pół godziny za pomocą zwykłej pętli for i rekurencji, bez zadawania żadnych pytań.

Nadal możesz pisać w JavaScript synchronicznie, nie ma ku temu przeciwskazań - tylko wtedy musiałbyś się liczyć z tym że czasami Ci się strona zatnie (na tyle czasu ile potrwa wykonywanie tych obliczeń). Mało która biblioteka w ogóle dostarcza synchroniczne rozwiązania, więc w dużej mierze pewnie musiałbyś je napisać sam od zera. Nie jest to łatwe, ale nikt Ci nie broni.

andrzejlisek napisał(a):
  1. Ostatnie, ale najważniejsze. W jaki sposób skrócić kod, żeby robił to samo, ale bez zmiennych globalnych i prawidłowo?

W ogóle nie powinieneś pisać kodu ze zmiennymi globalnymi od początku.

Wyczyściłem Twój kod, jeśli chcesz to możesz go użyć.

Co poprawiłem:

  1. Zamiast pisać iflogię na stringach, lepiej po prostu użyć .endsWith() albo wydzielić funkcję, np substringAfter
  2. Zawsze deklaruj zmienne, nie zostawiaj niezadeklarowanych zmiennych, tak jak tych w pętlach
  3. Jak deklarujesz to najlepiej z const (chyba że musi być zmieniona, to wtedy let)
  4. Nadawaj znaczące nazwy zmiennym - nazwa F nic nie mówi.
<!doctype html>
<html lang="en-us">
<head>
  <meta charset="utf-8">
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Test</title>
</head>
<body>
<script type='text/javascript' src="jszip.js"></script>
<script type='text/javascript'>
  function onFileUpload(event) {
    const fileInput = event.target;
    for (let i = 0; i < fileInput.files.length; i++) {
      const name = fileInput.files[i].name;
      const fileReader = new FileReader();
      fileReader.onload = function (e) {
        processFileOrArchive(name, substringAfter(e.target.result, ','));
      };
      fileReader.readAsDataURL(fileInput.files[i]);
    }
  }

  function substringAfter(string, substring) {
    return string.substring(string.indexOf(substring) + 1);
  }

  function processFileOrArchive(filename, fileText) {
    if (filename.endsWith('.zip')) {
      processZipArchive(fileText);
    } else {
      processFile(filename, fileText);
    }
  }

  function processZipArchive(archiveBase64) {
    new JSZip()
      .loadAsync(base64ToBlob(archiveBase64))
      .then(zip => processZipFiles(zip.files));
  }

  function base64ToBlob(base64) {
    const binary = atob(base64);
    const charCodes = new Array(binary.length);
    for (let i = 0; i < binary.length; i++) {
      charCodes[i] = binary.charCodeAt(i);
    }
    return new Blob([new Uint8Array(charCodes)], {type: "application/octet-stream"});
  }

  function processZipFiles(zipFileList) {
    for (const filename in zipFileList) {
      if (!filename.endsWith('/')) {
        const name = filename;
        zipFileList[filename]
          .async('base64')
          .then(content => processFile(name, content));
      }
    }
  }

  function processFile(filename, content) {
    console.log("Jakaś czynność z plikiem o nazwie \"" + filename + "\" i treści " + content.substring(0, 100));
  }
</script>
<input type="file" onChange="onFileUpload()" multiple="multiple">
</body>
</html>
0

Teraz załóżmy, że chce napisać funkcję, która pobiera 3 pliki i wykonuje jakąś czynność po pobraniu wszystkich trzech plików.

Czy poniżej prezentowany sposób jest prawidłowy?

function DownloadThreeFiles()
{
    // Początkowa wartość zmiennych może być dowolna, ale musi być równoważna false po podstawieniu do if()
    let file1Blob = false;
    let file2Blob = false;
    let file3Blob = false;


    fetch("file1.txt").then(resp => resp.blob()).then(bl => {

        // Zapamiętanie zawartości pliku do zmiennej, każde wywołanie fetch zapisuje pozyskaną zawartość pliku do innej zmiennej
        file1Blob = bl;

        // Do funkcji wchodzą wszystkie trzy zmienne, w tym miejscu pierwsza zmienna na pewno zawiera
        // zawartość pliku, a druga i trzecia nie wiadomo, czy zawiera, bo to zależy od kolejności
        // zakończenia pobrania się poszczególnych plików
        ProcessDownloadedFiles(file1Blob, file2Blob, file3Blob);
    })

    fetch("file2.txt").then(resp => resp.blob()).then(bl => {
        file2Blob = bl;
        ProcessDownloadedFiles(file1Blob, file2Blob, file3Blob);
    })

    fetch("file3.txt").then(resp => resp.blob()).then(bl => {
        file3Blob = bl;
        ProcessDownloadedFiles(file1Blob, file2Blob, file3Blob);
    })
}

function ProcessDownloadedFiles(file1, file2, file3)
{
    // Zakłada się, że jak nie ma jeszcze pliku, to nie spełnia warunku po podstawieniu do if(),
    // a treść pliku Blob po podstawieniu do if() spełnia warunek
    if (file1 && file2 && file3)
    {
        console.log("Pobrano wszystkie trzy pliki, ich zawartości w formie Blob są w parametrach i można wykonać to, co potrzeba.");
    }
}

Założenie jest takie, że pobieranie plików uruchamia się jednocześnie, czas samego pobrania jest mierzalny (trwa kilka sekund) i jest funkcja, która ma coś zrobić dopiero po pobraniu wszystkich trzech plików, przy czym nie można przewidzieć kolejności zakończenia pobrania się plików. W powyższym kodzie, funkcja ProcessDownloadedFiles uruchomi się trzy razy, za pierwszym razem tylko jedna zmienna zwróci true, za drugim razem dwie zmienne zwrócą true, a za trzecim razem wszystkie trzy zmienne zwrócą true, zawierają zawartość wszystkich trzech plików i wtedy cały if() jest spełniony i wykonuje się zamierzoną czynnosć (w tym przypadku wypisanie informacji pobraniu plików).

Czy taki sposób realizacji jest prawidłowy? Czy można powiedzieć, że zasada działania silnika JavaScript gwarantuje, że w jednym momencie będzie wywołana tylko jedna instancja funkcji? Czy to jest analogiczne do założenia mutex lub monitor na całą funkcje ProcessDownloadedFiles w przypadku realizacji pobrania plików w trzech osobnych wątkach, np. programując to w C++?

Wydaje się, że w analogiczny sposób można zrobić dowolne czynnosci implementowane asynchronicznie, czyli w sposób zastępujący:

  1. Każda czynność ustawia zmienną pozwalającą jednoznacznie stwierdic, czy czynność została wykonana.
  2. Każda czynność po zakończeniu uruchamia funkcję, którą należy uruchomić po zakończeniu wszystkich czynności.
  3. Poprzez zmienne ustawione przez poszczególne czynnosci, funkcja sprawcza, czy zostały wykonane wszystkie czynności. Jeżeli tak, to funkcja wykonuje to, co ma robić, w przeciwnym razie nic nie zrobi,
  4. Funkcja na zakończenie wszystkich czynności uruchomi się tyle razy, ile jest czynności, przy czym tylko po zakończeniu ostatniej czynnosci wykona zamierzoną pracę, bo funkcja stwierdzi fakt wykonania wszystkich czynności.
0
andrzejlisek napisał(a):

Teraz załóżmy, że chce napisać funkcję, która pobiera 3 pliki i wykonuje jakąś czynność po pobraniu wszystkich trzech plików.

Czy poniżej prezentowany sposób jest prawidłowy?

Jest bardzo średni.

Powinieneś użyć Promise.all().

function fetchFile(filename) {
  return fetch(filename).then(response => response.blob());
}

function DownloadThreeFiles() {
  const allFiles = Promise.all([
    fetchFile("file1.txt"),
    fetchFile("file2.txt"),
    fetchFile("file3.txt"),
  ]);

  allFiles.then(ProcessDownloadedFiles);
}

function ProcessDownloadedFiles(files) {
  const file1 = files[0];
  const file2 = files[1];
  const file3 = files[2];
}

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