Przejdź do treści

Pobieranie pliku z paskiem postępu

  • WWW i API
  • poziom: średni
  • aktualizacja: 16 sierpnia 2026

BodyHandlers.ofByteArray() jest wygodny, ale przy obrazie ISO albo dużym archiwum ładuje całość do pamięci. Ten wariant odbiera InputStream, zapisuje kolejne bloki do pliku .part i liczy postęp na podstawie nagłówka Content-Length. Dopiero po pełnym pobraniu przenosi plik tymczasowy pod nazwę docelową, więc zerwane łącze nie zostawi Ci kadłubka pod właściwą nazwą. Metody użyjesz pod prawdziwym adresem HTTPS, a main tworzy deterministyczny plik i udostępnia go z lokalnego HttpServer, dzięki czemu test nie zależy od sieci zewnętrznej.

JAVA PobieranieZPostepem.java 132 linii Pobierz .java
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.util.Arrays;

/**
 * Strumieniowe pobieranie pliku z postępem — skryptyJAVA.pl
 * Zapisz jako: PobieranieZPostepem.java
 */
public class PobieranieZPostepem {

    private static final HttpClient KLIENT = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(5))
            .followRedirects(HttpClient.Redirect.NORMAL)
            .build();

    /** Pobiera adres do pliku, nie trzymając całej odpowiedzi w pamięci. */
    public static Path pobierz(URI adres, Path cel) throws IOException, InterruptedException {
        Path bezwzglednyCel = cel.toAbsolutePath();
        Path katalog = bezwzglednyCel.getParent();
        if (katalog != null) { Files.createDirectories(katalog); }
        Path czesciowy = bezwzglednyCel.resolveSibling(bezwzglednyCel.getFileName() + ".part");
        Files.deleteIfExists(czesciowy);

        HttpRequest zadanie = HttpRequest.newBuilder(adres)
                .timeout(Duration.ofMinutes(5)).GET().build();
        HttpResponse<InputStream> odpowiedz = KLIENT.send(
                zadanie, HttpResponse.BodyHandlers.ofInputStream());

        if (odpowiedz.statusCode() < 200 || odpowiedz.statusCode() > 299) {
            odpowiedz.body().close();
            throw new IOException("Serwer zwrócił HTTP " + odpowiedz.statusCode());
        }

        long rozmiar = odpowiedz.headers().firstValueAsLong("Content-Length").orElse(-1);
        long pobrano = 0;
        int ostatniProcent = -1;
        try (InputStream wejscie = odpowiedz.body();
             OutputStream wyjscie = Files.newOutputStream(czesciowy)) {
            byte[] bufor = new byte[16 * 1024];
            int liczba;
            while ((liczba = wejscie.read(bufor)) != -1) {
                wyjscie.write(bufor, 0, liczba);
                pobrano += liczba;
                if (rozmiar > 0) {
                    int procent = (int) Math.min(100, pobrano * 100 / rozmiar);
                    if (procent != ostatniProcent) {
                        System.out.printf("\rPobieranie: %3d%% (%d/%d bajtów)",
                                procent, pobrano, rozmiar);
                        ostatniProcent = procent;
                    }
                }
            }
        } catch (IOException blad) {
            Files.deleteIfExists(czesciowy);
            throw blad;
        }

        if (rozmiar >= 0 && pobrano != rozmiar) { // 0 to znany rozmiar, nie brak nagłówka
            Files.deleteIfExists(czesciowy);
            throw new IOException("Niepełny plik: oczekiwano " + rozmiar + ", odebrano " + pobrano);
        }
        System.out.println(ostatniProcent >= 0 ? "" : "Pobrano " + pobrano + " bajtów.");
        przeniesGotowyPlik(czesciowy, bezwzglednyCel);
        return bezwzglednyCel;
    }

    /** Najpierw próbuje ruchu atomowego; po dowolnym IOException ponawia zwykłe zastąpienie. */
    private static void przeniesGotowyPlik(Path zrodlo, Path cel) throws IOException {
        try {
            Files.move(zrodlo, cel, StandardCopyOption.ATOMIC_MOVE);
        } catch (IOException atomowy) {
            try {
                Files.move(zrodlo, cel, StandardCopyOption.REPLACE_EXISTING);
            } catch (IOException zwykly) {
                zwykly.addSuppressed(atomowy);
                throw zwykly;
            }
        }
    }

    /** Lokalny serwer testowy udostępnia dane w pamięci i nie korzysta z internetu. */
    private static HttpServer serwerTestowy(byte[] dane) throws IOException {
        HttpServer serwer = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
        serwer.createContext("/plik.bin", wymiana -> {
            if (!wymiana.getRequestMethod().equalsIgnoreCase("GET")) {
                wymiana.sendResponseHeaders(405, -1);
                wymiana.close();
                return;
            }
            wymiana.getResponseHeaders().set("Content-Type", "application/octet-stream");
            wymiana.sendResponseHeaders(200, dane.length);
            try (OutputStream wyjscie = wymiana.getResponseBody()) {
                wyjscie.write(dane);
            }
        });
        return serwer;
    }

    // ── PRZYKŁADY ───────────────────────────────────────────
    public static void main(String[] args) throws Exception {
        byte[] dane = new byte[256 * 1024];
        for (int i = 0; i < dane.length; i++) { dane[i] = (byte) (i * 31); }
        HttpServer serwer = serwerTestowy(dane);
        Path katalog = Files.createTempDirectory("pobieranie-demo-");
        Path cel = katalog.resolve("pobrany-plik.bin");
        serwer.start();

        try {
            URI adres = URI.create("http://127.0.0.1:" + serwer.getAddress().getPort() + "/plik.bin");
            Path zapisany = pobierz(adres, cel);
            boolean zgodny = Arrays.equals(dane, Files.readAllBytes(zapisany));
            System.out.println("Zapisano: " + zapisany + "; dane zgodne: " + zgodny);
        } finally {
            serwer.stop(0);
            Files.deleteIfExists(cel);
            Files.deleteIfExists(cel.resolveSibling(cel.getFileName() + ".part"));
            Files.deleteIfExists(katalog);
        }
    }
}

Jak użyć tego skryptu

  1. Zapisz kod jako PobieranieZPostepem.java (plik musi nazywać się jak klasa publiczna) i uruchom przez java PobieranieZPostepem.java. Lokalny serwer wyśle 256 KiB danych, program pobierający pokaże procenty, porówna wynik z oryginałem i posprząta pliki tymczasowe.
  2. We własnej aplikacji wywołaj pobierz(URI.create("https://example.com/plik.zip"), Path.of("plik.zip")). Metoda tworzy katalog docelowy, jeśli jeszcze nie istnieje, i zwraca bezwzględną ścieżkę gotowego pliku.
  3. Nie licz na to, że limity czasu uratują Cię przed zawieszeniem. connectTimeout pilnuje wyłącznie zestawienia połączenia, a limit z HttpRequest.timeout(...) przy BodyHandlers.ofInputStream() kończy się w chwili odebrania nagłówków — kolejnych read() na ciele już nie obejmuje. Serwer, który wyśle nagłówki i zamilknie, potrafi trzymać pobieranie w nieskończoność. Jeżeli to realne ryzyko, dołóż własnego strażnika: osobny wątek, który sprawdza, czy licznik pobranych bajtów rośnie, i po zbyt długim zastoju zamyka strumień odpowiedzi — read() kończy się wtedy wyjątkiem.
  4. Jeżeli pasek ma trafiać do GUI, zastąp System.out.printf wywołaniem przekazanego callbacku, np. IntConsumer. W Swingu aktualizuj kontrolki przez SwingUtilities.invokeLater, a samo pobieranie wykonuj poza wątkiem interfejsu.

Warto wiedzieć

  • Procent policzysz tylko wtedy, gdy serwer poda wiarygodny nagłówek Content-Length. Przy transmisji fragmentowanej rozmiar wynosi -1; kod nadal pobierze plik, ale pokaże jedynie liczbę odebranych bajtów po zakończeniu. Nie myl tego z wartością 0 — to znany rozmiar pustego pliku, a nie brak informacji, dlatego kontrola kompletności sprawdza rozmiar >= 0.
  • Dane najpierw trafiają do pliku z końcówką .part. Dzięki temu przerwane połączenie nie pozostawia niekompletnego pliku pod właściwą nazwą. Po sukcesie kod próbuje atomowego przeniesienia, a po dowolnym błędzie tej próby ponawia ruch ze zwykłym REPLACE_EXISTING; jeśli i on zawiedzie, zachowuje oba wyjątki. Pamiętaj tylko, że nazwa pliku roboczego wynika wprost z nazwy docelowej: dwa równoległe pobrania tego samego celu skasują sobie nawzajem .part i wymieszają dane, więc przy takim scenariuszu dopisz do niej unikalny sufiks albo załóż blokadę.
  • Samo zakończenie z kodem 2xx i zgodny Content-Length nie dowodzą autentyczności pliku. Dla aktualizacji programu lub innych wrażliwych danych porównaj po pobraniu kryptograficzny skrót SHA-256 z wartością uzyskaną z zaufanego kanału albo zweryfikuj podpis cyfrowy.
  • Nie zapisuj pliku pod nazwą przesłaną przez obcy serwer bez walidacji. Nagłówek Content-Disposition może zawierać sekwencje ../ lub nazwę specjalną; najbezpieczniej samodzielnie wybrać ścieżkę docelową, tak jak robi metoda w przykładzie.
#http #httpclient #pliki #nio #bezpieczeństwo

Podobne skrypty