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.
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
- Zapisz kod jako
PobieranieZPostepem.java(plik musi nazywać się jak klasa publiczna) i uruchom przezjava 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. - 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. - Nie licz na to, że limity czasu uratują Cię przed zawieszeniem.
connectTimeoutpilnuje wyłącznie zestawienia połączenia, a limit zHttpRequest.timeout(...)przyBodyHandlers.ofInputStream()kończy się w chwili odebrania nagłówków — kolejnychread()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. - Jeżeli pasek ma trafiać do GUI, zastąp
System.out.printfwywołaniem przekazanego callbacku, np.IntConsumer. W Swingu aktualizuj kontrolki przezSwingUtilities.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 sprawdzarozmiar >= 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łymREPLACE_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.parti wymieszają dane, więc przy takim scenariuszu dopisz do niej unikalny sufiks albo załóż blokadę. - Samo zakończenie z kodem 2xx i zgodny
Content-Lengthnie 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-Dispositionmoże zawierać sekwencje../lub nazwę specjalną; najbezpieczniej samodzielnie wybrać ścieżkę docelową, tak jak robi metoda w przykładzie.
Podobne skrypty
Pobieranie danych z API przez HttpClient
Klient HTTP na wbudowanym HttpClient z Javy 11: żądania GET i POST z ciałem JSON, limity czasu, sprawdzanie kodu odpowiedzi, pobieranie pliku prosto na dysk i wersja asynchroniczna — bez żadnych bibliotek.
średni Zobacz kod → WWW i APIProsty serwer HTTP bez frameworka
Mały serwer WWW na wbudowanej klasie HttpServer: routing, parametry zapytania, odpowiedzi UTF-8 oraz poprawne kody HTTP z obsługą HEAD i 405. Przykład sam uruchamia serwer lokalny, odpytuje go i zatrzymuje, więc nic nie zostaje w tle.
średni Zobacz kod → WWW i APIBudowanie adresów URL i kodowanie parametrów
Sklejanie adresów URL bez błędów: kodowanie kluczy i wartości przez URLEncoder, osobna metoda dla segmentów ścieżki z %20 zamiast plusa, rozbieranie query stringu na mapę oraz odczyt części adresu przez URI.
początkujący Zobacz kod →