Lekcja 11 Obsługa plików

Obsługa plików i operacje wejścia/wyjścia w JavaScript

W JavaScripcie uruchomionym w przeglądarce nie masz swobodnego dostępu do dysku komputera (to celowe – kwestia bezpieczeństwa). Mimo to da się realizować wiele praktycznych operacji wejścia/wyjścia (I/O): wczytywać pliki od użytkownika, odczytywać je jako tekst lub dane binarne, przetwarzać, a potem zapisywać wynik jako nowy plik do pobrania. Poniżej masz kompletne podstawy + gotowe przykłady z formularzem.

1) Co oznacza I/O (wejście/wyjście) w programowaniu?

Wejście (Input) to pobieranie danych z zewnątrz programu, np. z klawiatury, formularza, pliku, sieci. Wyjście (Output) to wysyłanie danych na zewnątrz, np. do ekranu, do pliku, do sieci.

W przeglądarce typowe I/O to m.in.:

  • odczyt danych z formularzy (input/textarea/select),
  • wczytanie pliku poprzez <input type="file">,
  • zapis wyniku jako plik do pobrania (Blob + link download),
  • (opcjonalnie) zapis małych danych w localStorage / indexedDB.

2) Dlaczego „czysty JS” nie zapisuje plików gdzie chce?

Przeglądarka działa w sandboxie. Kod strony nie może bez pytania użytkownika: przeszukiwać dysku, czytać dowolnych plików ani zapisywać ich w wybranym katalogu. Dlatego w przeglądarce typowy scenariusz wygląda tak:

  1. Użytkownik wybiera plik w formularzu (to jest świadoma zgoda).
  2. JS czyta zawartość pliku (tekst lub binarnie).
  3. JS tworzy nowy plik i udostępnia go do pobrania.

3) Formularz do obsługi plików (wejście/wyjście)

Poniżej jest prosty formularz, który:

  • wczytuje plik tekstowy lub binarny,
  • wyświetla podgląd (dla tekstu),
  • pozwala zapisać wynik jako nowy plik (TXT lub BIN).
<form id="fileForm">
  <label>Wybierz plik:</label><br>
  <input id="fileInput" type="file" accept=".txt,.csv,.json,.bin,image/*" />
  <div style="margin-top:10px">
    <label>Tryb odczytu:</label>
    <select id="mode">
      <option value="text">Tekst (FileReader.readAsText)</option>
      <option value="binary">Binarny (ArrayBuffer)</option>
    </select>
  </div>
  <div style="margin-top:10px">
    <button type="submit">Wczytaj</button>
  </div>
</form>
<hr>
<h3>Podgląd / wynik</h3>
<textarea id="output" rows="10" style="width:100%" placeholder="Tu pojawi się tekst lub informacje o pliku..."></textarea>
<div style="margin-top:10px; display:flex; gap:10px; flex-wrap:wrap;">
  <button id="saveTxt" type="button">Zapisz jako TXT</button>
  <button id="saveBin" type="button">Zapisz jako BIN (z aktualnych danych)</button>
</div>

4) Odczyt pliku tekstowego (FileReader / text())

W przeglądarce najprościej odczytać tekst przez FileReader albo przez file.text(). Poniżej wersja z async/await i file.text() (czytelna i nowoczesna).

<script>
const form = document.getElementById("fileForm");
const fileInput = document.getElementById("fileInput");
const mode = document.getElementById("mode");
const output = document.getElementById("output");
const saveTxt = document.getElementById("saveTxt");
const saveBin = document.getElementById("saveBin");
// buffer na dane wejściowe (tekst albo ArrayBuffer)
let lastText = "";
let lastBuffer = null;
form.addEventListener("submit", async (e) => {
  e.preventDefault();
  try {
    const file = fileInput.files?.[0];
    if (!file) {
      output.value = "Błąd: nie wybrano pliku.";
      return;
    }
    output.value = `Nazwa: ${file.name}nRozmiar: ${file.size} BnTyp MIME: ${file.type || "(brak)"}nn`;
    if (mode.value === "text") {
      // ODCZYT TEKSTU
      const text = await file.text();
      lastText = text;
      lastBuffer = null;
      output.value += "=== PODGLĄD TEKSTU ===n" + text.slice(0, 5000);
      if (text.length > 5000) output.value += "nn... (ucięto podgląd do 5000 znaków)";
    } else {
      // ODCZYT BINARNY
      const buffer = await file.arrayBuffer();
      lastBuffer = buffer;
      lastText = "";
      // pokażmy np. pierwsze 32 bajty w hex
      const bytes = new Uint8Array(buffer);
      const preview = Array.from(bytes.slice(0, 32))
        .map(b => b.toString(16).padStart(2, "0"))
        .join(" ");
      output.value += "=== PODGLĄD BINARNY (pierwsze 32 bajty HEX) ===n" + preview;
    }
  } catch (err) {
    // Obsługa błędów I/O
    output.value = "Wystąpił błąd podczas odczytu pliku:n" + (err?.message || String(err));
  }
});
// ZAPIS do pliku TXT (na podstawie output textarea lub lastText)
saveTxt.addEventListener("click", () => {
  try {
    const textToSave = output.value; // albo lastText, zależnie od celu
    const blob = new Blob([textToSave], { type: "text/plain;charset=utf-8" });
    downloadBlob(blob, "wynik.txt");
  } catch (err) {
    alert("Błąd zapisu TXT: " + (err?.message || String(err)));
  }
});
// ZAPIS do pliku BIN (na podstawie lastBuffer; jeśli brak, spróbujemy zakodować tekst do UTF-8)
saveBin.addEventListener("click", () => {
  try {
    let blob;
    if (lastBuffer) {
      blob = new Blob([lastBuffer], { type: "application/octet-stream" });
    } else {
      // jeśli mamy tekst, zamieniamy go na bajty UTF-8
      const encoder = new TextEncoder();
      const bytes = encoder.encode(output.value);
      blob = new Blob([bytes], { type: "application/octet-stream" });
    }
    downloadBlob(blob, "wynik.bin");
  } catch (err) {
    alert("Błąd zapisu BIN: " + (err?.message || String(err)));
  }
});
// funkcja pomocnicza: pobranie pliku (download)
function downloadBlob(blob, filename) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  a.remove();
  URL.revokeObjectURL(url);
}
</script>

To jest najważniejszy „trik” zapisu w przeglądarce: tworzymy Blob, robimy z niego URL i klikamy link download. To nie zapisuje pliku „w tle” w dowolnym folderze – tylko daje użytkownikowi pobrać plik, a przeglądarka pyta gdzie go zapisać (lub zapisuje do domyślnego folderu pobrań).

5) Tekst vs binarka – co to znaczy?

Plik tekstowy

  • to znaki (np. TXT, CSV, JSON),
  • czytamy jako string: file.text() lub readAsText,
  • łatwo edytować w textarea.

Plik binarny

  • to bajty (np. obraz, PDF, ZIP, dowolny BIN),
  • czytamy jako ArrayBuffer: file.arrayBuffer() lub readAsArrayBuffer,
  • przetwarzamy przez Uint8Array i inne typed arrays.
// ArrayBuffer -> bajty
const bytes = new Uint8Array(buffer);
// bajty -> tekst UTF-8
const decoder = new TextDecoder("utf-8");
const text = decoder.decode(bytes);
// tekst -> bajty UTF-8
const encoder = new TextEncoder();
const bytes2 = encoder.encode(text);

6) Obsługa wyjątków i błędów podczas operacji na plikach

W przeglądarce błędy podczas operacji na plikach to zwykle:

  • brak pliku (użytkownik nic nie wybrał),
  • niewłaściwy tryb odczytu (np. próbujesz traktować binarkę jako tekst),
  • zbyt duży plik (warto ograniczyć rozmiar),
  • odmowa uprawnień (gdy korzystasz z API wymagających zgody użytkownika),
  • problem z kodowaniem znaków (np. plik w innym kodowaniu niż UTF-8).

Przykładowa walidacja: limit rozmiaru i dopuszczalne rozszerzenia:

function validateFile(file) {
  const maxBytes = 2 * 1024 * 1024; // 2 MB
  if (file.size > maxBytes) throw new Error("Plik jest za duży (max 2MB).");
  const allowed = ["text/plain", "application/json", "text/csv", ""];
  // file.type bywa puste w niektórych przypadkach – wtedy można sprawdzać rozszerzenie
  if (file.type && !allowed.includes(file.type)) {
    throw new Error("Niedozwolony typ pliku: " + file.type);
  }
}

Najprostszy wzorzec obsługi błędów w I/O to try/catch wokół miejsca, gdzie czytasz lub zapisujesz. W UI warto wtedy wyświetlić komunikat użytkownikowi (np. w textarea lub w <div> z alertem).

7) Przykład praktyczny: wczytaj TXT, przetwórz, zapisz wynik

Załóżmy, że wczytujesz plik TXT (np. listę uczniów), a potem chcesz: usunąć puste linie, posortować i zapisać wynik do nowego pliku.

function processText(text) {
  return text
    .split(/r?n/)
    .map(line => line.trim())
    .filter(line => line.length > 0)
    .sort((a, b) => a.localeCompare(b, "pl"));
}
// po odczycie:
const wynik = processText(lastText);
output.value = wynik.join("n");

8) Ważna uwaga: „prawdziwy zapis do pliku” bez pobierania

W „czystym JS” w przeglądarce standardowo zapisujesz plik poprzez pobranie (download). Istnieje też nowocześniejsze API pozwalające zapisać do pliku wskazanego przez użytkownika (okno „Zapisz jako”), ale nadal wymaga to interakcji użytkownika i nie działa wszędzie jednakowo. Jeśli chcesz trzymać się maksymalnej kompatybilności, metoda z Blob + download jest najpewniejsza.

Podsumowanie

Bez Node.js też da się robić I/O: użytkownik wybiera plik w formularzu, JavaScript czyta go jako tekst lub binarnie, a wynik zapisujesz jako nowy plik do pobrania (Blob). Kluczowe elementy to: input type="file", file.text() / file.arrayBuffer(), TextEncoder/TextDecoder, oraz obsługa błędów przez try/catch.

Koszyk