From 819937472eb7bf37b09f4864c3925f7aad5bc31c Mon Sep 17 00:00:00 2001 From: Matheus Cunha Date: Sun, 26 Jul 2026 06:17:21 -0300 Subject: [PATCH] Add dedicated WebClient for downloading large binary assets - Introduced a `downloadWebClient` with extended timeouts for handling large file downloads. - Updated `GitHubClient` to use `downloadWebClient` for binary asset downloads. - Adjusted installation script to relaunch the app in detached mode after package installation. - Incremented app version to `0.1.3-alpha`. --- .../out/external/github/GitHubClient.java | 21 +++++++++-- .../external/github/common/GitHubConfig.java | 37 ++++++++++++++++--- .../impl/release/InstallUpdateImpl.java | 6 ++- backend/src/main/resources/application.yaml | 2 +- .../out/external/github/GitHubClientTest.java | 25 ++++++++++--- 5 files changed, 75 insertions(+), 16 deletions(-) diff --git a/backend/src/main/java/com/devaulty/backend/adapter/out/external/github/GitHubClient.java b/backend/src/main/java/com/devaulty/backend/adapter/out/external/github/GitHubClient.java index b135df3..94ba828 100644 --- a/backend/src/main/java/com/devaulty/backend/adapter/out/external/github/GitHubClient.java +++ b/backend/src/main/java/com/devaulty/backend/adapter/out/external/github/GitHubClient.java @@ -1,6 +1,7 @@ package com.devaulty.backend.adapter.out.external.github; import com.devaulty.backend.adapter.out.external.github.dto.GitHubReleaseResponse; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; @@ -16,10 +17,19 @@ public class GitHubClient { private final WebClient githubWebClient; + /** + * A separate WebClient used exclusively for downloading binary release assets. + * It has no base URL, no GitHub-specific Accept header, and a much longer + * read/write timeout to handle large file downloads without timing out. + */ + private final WebClient downloadWebClient; + private static final String LATEST_RELEASES_URL = "/repos/MathCunha16/Devaulty/releases/latest"; - public GitHubClient(WebClient githubWebClient) { + public GitHubClient(@Qualifier("githubWebClient") WebClient githubWebClient, + @Qualifier("downloadWebClient") WebClient downloadWebClient) { this.githubWebClient = githubWebClient; + this.downloadWebClient = downloadWebClient; } public GitHubReleaseResponse getLatestRelease() { @@ -34,9 +44,14 @@ public GitHubReleaseResponse getLatestRelease() { .block(); } + /** + * Downloads a binary release asset from an absolute URL (e.g. GitHub CDN). + * Uses a dedicated WebClient with no GitHub API headers and a generous + * timeout so that large installers (.deb, .rpm, .msi) are not truncated. + */ public Flux downloadAsset(String downloadUrl) { - return githubWebClient.get() - .uri(URI.create(downloadUrl)) // Clean URL + return downloadWebClient.get() + .uri(URI.create(downloadUrl)) .retrieve() .bodyToFlux(DataBuffer.class); } diff --git a/backend/src/main/java/com/devaulty/backend/adapter/out/external/github/common/GitHubConfig.java b/backend/src/main/java/com/devaulty/backend/adapter/out/external/github/common/GitHubConfig.java index ace50ae..f2cc74f 100644 --- a/backend/src/main/java/com/devaulty/backend/adapter/out/external/github/common/GitHubConfig.java +++ b/backend/src/main/java/com/devaulty/backend/adapter/out/external/github/common/GitHubConfig.java @@ -16,16 +16,22 @@ public class GitHubConfig { private static final int CONNECT_TIMEOUT_MS = 10000; - private static final int READ_WRITE_TIMEOUT_SECONDS = 15; + private static final int API_READ_WRITE_TIMEOUT_SECONDS = 15; - @Bean + /** Generous timeout for downloading large binary assets (e.g. .deb, .rpm, .msi). */ + private static final int DOWNLOAD_READ_WRITE_TIMEOUT_SECONDS = 600; // 10 minutes + + /** + * WebClient for GitHub REST API calls (JSON responses, short timeout). + */ + @Bean(name = "githubWebClient") public WebClient githubWebClient() { HttpClient httpClient = HttpClient.create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MS) - .responseTimeout(Duration.ofSeconds(READ_WRITE_TIMEOUT_SECONDS)) + .responseTimeout(Duration.ofSeconds(API_READ_WRITE_TIMEOUT_SECONDS)) .doOnConnected(conn -> conn - .addHandlerLast(new ReadTimeoutHandler(READ_WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) - .addHandlerLast(new WriteTimeoutHandler(READ_WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS))) + .addHandlerLast(new ReadTimeoutHandler(API_READ_WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) + .addHandlerLast(new WriteTimeoutHandler(API_READ_WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS))) .followRedirect(true); return WebClient.builder() @@ -36,4 +42,25 @@ public WebClient githubWebClient() { .defaultHeader("X-GitHub-Api-Version", "2026-03-10") .build(); } + + /** + * WebClient for downloading binary release assets from GitHub CDN. + * No base URL, no GitHub API Accept header, and a much longer timeout + * to prevent large installer files from being truncated mid-download. + */ + @Bean(name = "downloadWebClient") + public WebClient downloadWebClient() { + HttpClient httpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MS) + .responseTimeout(Duration.ofSeconds(DOWNLOAD_READ_WRITE_TIMEOUT_SECONDS)) + .doOnConnected(conn -> conn + .addHandlerLast(new ReadTimeoutHandler(DOWNLOAD_READ_WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) + .addHandlerLast(new WriteTimeoutHandler(DOWNLOAD_READ_WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS))) + .followRedirect(true); + + return WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .defaultHeader("User-Agent", "Devaulty-Desktop-App") + .build(); + } } diff --git a/backend/src/main/java/com/devaulty/backend/application/impl/release/InstallUpdateImpl.java b/backend/src/main/java/com/devaulty/backend/application/impl/release/InstallUpdateImpl.java index f42810e..15c3e79 100644 --- a/backend/src/main/java/com/devaulty/backend/application/impl/release/InstallUpdateImpl.java +++ b/backend/src/main/java/com/devaulty/backend/application/impl/release/InstallUpdateImpl.java @@ -77,10 +77,14 @@ private void launchLinuxInstaller(String filePath, long currentPid) throws IOExc String installProgram = filePath.endsWith(".rpm") ? "rpm" : "dpkg"; String installFlag = filePath.endsWith(".rpm") ? "-Uvh" : "-i"; + // Wait for the current process to exit, install the package with pkexec, + // then relaunch the app fully detached (nohup + setsid) so it survives + // after the installer script exits. String script = "PID=\"$1\"; FILE=\"$2\"; PROGRAM=\"$3\"; FLAG=\"$4\"; " + "while kill -0 \"$PID\" 2>/dev/null; do sleep 0.2; done; " + - "pkexec \"$PROGRAM\" \"$FLAG\" \"$FILE\" && devaulty"; + "pkexec \"$PROGRAM\" \"$FLAG\" \"$FILE\" && " + + "nohup setsid devaulty >/dev/null 2>&1 &"; List command = List.of( "bash", "-c", script, "bash", diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index 94d449f..4ade15b 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -1,5 +1,5 @@ app: - version: "0.1.2-alpha" + version: "0.1.3-alpha" spring: application: diff --git a/backend/src/test/java/com/devaulty/backend/adapter/out/external/github/GitHubClientTest.java b/backend/src/test/java/com/devaulty/backend/adapter/out/external/github/GitHubClientTest.java index 202e132..470ed79 100644 --- a/backend/src/test/java/com/devaulty/backend/adapter/out/external/github/GitHubClientTest.java +++ b/backend/src/test/java/com/devaulty/backend/adapter/out/external/github/GitHubClientTest.java @@ -32,6 +32,9 @@ class GitHubClientTest { @Mock private WebClient webClient; + @Mock + private WebClient downloadWebClient; + @Mock private WebClient.RequestHeadersUriSpec requestHeadersUriSpec; @@ -41,11 +44,20 @@ class GitHubClientTest { @Mock private WebClient.ResponseSpec responseSpec; + @Mock + private WebClient.RequestHeadersUriSpec downloadRequestHeadersUriSpec; + + @Mock + private WebClient.RequestHeadersSpec downloadRequestHeadersSpec; + + @Mock + private WebClient.ResponseSpec downloadResponseSpec; + private GitHubClient gitHubClient; @BeforeEach void setUp() { - gitHubClient = new GitHubClient(webClient); + gitHubClient = new GitHubClient(webClient, downloadWebClient); } @Test @@ -122,10 +134,10 @@ void downloadAsset_shouldReturnDataBufferFlux_whenUrlIsValid() { DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); DataBuffer mockBuffer = factory.wrap("binary content".getBytes(StandardCharsets.UTF_8)); - when(webClient.get()).thenReturn(requestHeadersUriSpec); - when(requestHeadersUriSpec.uri(any(URI.class))).thenReturn(requestHeadersSpec); - when(requestHeadersSpec.retrieve()).thenReturn(responseSpec); - when(responseSpec.bodyToFlux(DataBuffer.class)).thenReturn(Flux.just(mockBuffer)); + when(downloadWebClient.get()).thenReturn(downloadRequestHeadersUriSpec); + when(downloadRequestHeadersUriSpec.uri(any(URI.class))).thenReturn(downloadRequestHeadersSpec); + when(downloadRequestHeadersSpec.retrieve()).thenReturn(downloadResponseSpec); + when(downloadResponseSpec.bodyToFlux(DataBuffer.class)).thenReturn(Flux.just(mockBuffer)); // Act Flux resultFlux = gitHubClient.downloadAsset(downloadUrl); @@ -134,6 +146,7 @@ void downloadAsset_shouldReturnDataBufferFlux_whenUrlIsValid() { // Assert assertNotNull(buffers); assertEquals(1, buffers.size()); - verify(webClient, times(1)).get(); + verify(downloadWebClient, times(1)).get(); + verify(webClient, never()).get(); } }