Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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() {
Expand All @@ -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<DataBuffer> downloadAsset(String downloadUrl) {
return githubWebClient.get()
.uri(URI.create(downloadUrl)) // Clean URL
return downloadWebClient.get()
.uri(URI.create(downloadUrl))
.retrieve()
.bodyToFlux(DataBuffer.class);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> command = List.of(
"bash", "-c", script, "bash",
Expand Down
2 changes: 1 addition & 1 deletion backend/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
app:
version: "0.1.2-alpha"
version: "0.1.3-alpha"

spring:
application:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ class GitHubClientTest {
@Mock
private WebClient webClient;

@Mock
private WebClient downloadWebClient;

@Mock
private WebClient.RequestHeadersUriSpec requestHeadersUriSpec;

Expand All @@ -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
Expand Down Expand Up @@ -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<DataBuffer> resultFlux = gitHubClient.downloadAsset(downloadUrl);
Expand All @@ -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();
}
}