20250828 cleanup (#2639)

* Cleanup

* Refactor artifact management
This commit is contained in:
Avgustin Marinov
2025-09-02 16:08:14 +03:00
committed by GitHub
parent 4f0a8893c7
commit 2a636328a0
305 changed files with 2253 additions and 4566 deletions

View File

@@ -23,6 +23,6 @@ public final class MultiPartFileUploadException extends AbstractServerRtExceptio
private static final long serialVersionUID = 1L;
public MultiPartFileUploadException(final Throwable cause) {
super(cause.getMessage(), SpServerError.SP_ARTIFACT_UPLOAD_FAILED, cause);
super(SpServerError.SP_ARTIFACT_UPLOAD_FAILED, cause.getMessage(), cause);
}
}

View File

@@ -28,7 +28,7 @@ public final class FileStreamingFailedException extends AbstractServerRtExceptio
* @param message of the error
*/
public FileStreamingFailedException(final String message) {
super(message, SpServerError.SP_ARTIFACT_LOAD_FAILED);
super(SpServerError.SP_ARTIFACT_LOAD_FAILED, message);
}
/**
@@ -38,6 +38,6 @@ public final class FileStreamingFailedException extends AbstractServerRtExceptio
* @param cause for the exception
*/
public FileStreamingFailedException(final String message, final Throwable cause) {
super(message, SpServerError.SP_ARTIFACT_LOAD_FAILED, cause);
super(SpServerError.SP_ARTIFACT_LOAD_FAILED, message, cause);
}
}

View File

@@ -26,7 +26,7 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.eclipse.hawkbit.repository.artifact.model.DbArtifact;
import org.eclipse.hawkbit.artifact.model.ArtifactStream;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -46,7 +46,6 @@ public final class FileStreamingUtil {
* Write response with target relation and publishes events concerning the
* download progress based on given update action status.
* </p>
*
* <p>
* The request supports RFC7233 range requests.
* </p>
@@ -59,16 +58,16 @@ public final class FileStreamingUtil {
* @param progressListener to write progress updates to
* @return http response
* @throws FileStreamingFailedException if streaming fails
* @see <a href="https://tools.ietf.org/html/rfc7233">https://tools.ietf.org
* /html/rfc7233</a>
* @see <a href="https://tools.ietf.org/html/rfc7233">https://tools.ietf.org/html/rfc7233</a>
*/
public static ResponseEntity<InputStream> writeFileResponse(final DbArtifact artifact, final String filename,
public static ResponseEntity<InputStream> writeFileResponse(
final ArtifactStream artifact, final String filename,
final long lastModified, final HttpServletResponse response, final HttpServletRequest request,
final FileStreamingProgressListener progressListener) {
ResponseEntity<InputStream> result;
final String etag = artifact.getHashes().getSha1();
final String etag = artifact.getSha1Hash();
final long length = artifact.getSize();
resetResponseExceptHeaders(response);
@@ -114,20 +113,21 @@ public final class FileStreamingUtil {
}
}
// full request - no range
if (ranges.isEmpty() || ranges.get(0).equals(full)) {
log.debug("filename ({}) results into a full request: ", filename);
result = handleFullFileRequest(artifact, filename, response, progressListener, full);
}
// standard range request
else if (ranges.size() == 1) {
log.debug("filename ({}) results into a standard range request: ", filename);
result = handleStandardRangeRequest(artifact, filename, response, progressListener, ranges);
}
// multipart range request
else {
log.debug("filename ({}) results into a multipart range request: ", filename);
result = handleMultipartRangeRequest(artifact, filename, response, progressListener, ranges);
try (final InputStream inputStream = artifact) {
// full request - no range
if (ranges.isEmpty() || ranges.get(0).equals(full)) {
log.debug("filename ({}) results into a full request: ", filename);
result = handleFullFileRequest(inputStream, filename, response, progressListener, full);
} else if (ranges.size() == 1) { // standard range request
log.debug("filename ({}) results into a standard range request: ", filename);
result = handleStandardRangeRequest(inputStream, filename, response, progressListener, ranges.get(0));
} else { // multipart range request
log.debug("filename ({}) results into a multipart range request: ", filename);
result = handleMultipartRangeRequest(inputStream, filename, response, progressListener, ranges);
}
} catch (final IOException e) {
log.error("streaming of file ({}) failed!", filename, e);
throw new FileStreamingFailedException(filename, e);
}
return result;
@@ -146,16 +146,15 @@ public final class FileStreamingUtil {
storedHeaders.forEach(response::addHeader);
}
private static ResponseEntity<InputStream> handleFullFileRequest(final DbArtifact artifact, final String filename,
final HttpServletResponse response, final FileStreamingProgressListener progressListener,
final ByteRange full) {
final ByteRange r = full;
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal());
response.setContentLengthLong(r.getLength());
private static ResponseEntity<InputStream> handleFullFileRequest(
final InputStream inputStream, final String filename, final HttpServletResponse response,
final FileStreamingProgressListener progressListener, final ByteRange full) {
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + full.getStart() + "-" + full.getEnd() + "/" + full.getTotal());
response.setContentLengthLong(full.getLength());
try (final InputStream from = artifact.getFileInputStream()) {
try {
final ServletOutputStream to = response.getOutputStream();
copyStreams(from, to, progressListener, r.getStart(), r.getLength(), filename);
copyStreams(inputStream, to, progressListener, full.getStart(), full.getLength(), filename);
} catch (final IOException e) {
throw new FileStreamingFailedException("fullfileRequest " + filename, e);
}
@@ -214,28 +213,30 @@ public final class FileStreamingUtil {
}
}
private static ResponseEntity<InputStream> handleMultipartRangeRequest(final DbArtifact artifact,
final String filename, final HttpServletResponse response,
private static ResponseEntity<InputStream> handleMultipartRangeRequest(
final InputStream inputStream, final String filename, final HttpServletResponse response,
final FileStreamingProgressListener progressListener, final List<ByteRange> ranges) {
ranges.sort((r1, r2) -> Long.compare(r1.getStart(), r2.getStart()));
response.setContentType("multipart/byteranges; boundary=" + ByteRange.MULTIPART_BOUNDARY);
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
try {
final ServletOutputStream to = response.getOutputStream();
for (final ByteRange r : ranges) {
try (final InputStream from = artifact.getFileInputStream()) {
// Add multipart boundary and header fields for every range.
to.println();
to.println("--" + ByteRange.MULTIPART_BOUNDARY);
to.println(HttpHeaders.CONTENT_RANGE + ": bytes " + r.getStart() + "-" + r.getEnd() + "/"
+ r.getTotal());
// Copy single part range of multi part range.
copyStreams(from, to, progressListener, r.getStart(), r.getLength(), filename);
long streamPos = 0;
for (final ByteRange range : ranges) {
if (streamPos > range.getStart()) {
throw new FileStreamingFailedException("Ranges are overlapping or not in order");
}
// Add multipart boundary and header fields for every range.
to.println();
to.println("--" + ByteRange.MULTIPART_BOUNDARY);
to.println(HttpHeaders.CONTENT_RANGE + ": bytes " + range.getStart() + "-" + range.getEnd() + "/" + range.getTotal());
// Copy single part range of multipart range.
copyStreams(inputStream, to, progressListener, range.getStart() - streamPos, range.getLength(), filename);
streamPos = range.getStart() + range.getLength();
}
// End with final multipart boundary.
@@ -248,17 +249,16 @@ public final class FileStreamingUtil {
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).build();
}
private static ResponseEntity<InputStream> handleStandardRangeRequest(final DbArtifact artifact,
final String filename, final HttpServletResponse response,
final FileStreamingProgressListener progressListener, final List<ByteRange> ranges) {
final ByteRange r = ranges.get(0);
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal());
response.setContentLengthLong(r.getLength());
private static ResponseEntity<InputStream> handleStandardRangeRequest(
final InputStream inputStream, final String filename, final HttpServletResponse response,
final FileStreamingProgressListener progressListener, final ByteRange range) {
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + range.getStart() + "-" + range.getEnd() + "/" + range.getTotal());
response.setContentLengthLong(range.getLength());
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
try (final InputStream from = artifact.getFileInputStream()) {
try {
final ServletOutputStream to = response.getOutputStream();
copyStreams(from, to, progressListener, r.getStart(), r.getLength(), filename);
copyStreams(inputStream, to, progressListener, range.getStart(), range.getLength(), filename);
} catch (final IOException e) {
log.error("standardRangeRequest of file ({}) failed!", filename, e);
throw new FileStreamingFailedException(filename);
@@ -267,10 +267,11 @@ public final class FileStreamingUtil {
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).build();
}
private static long copyStreams(final InputStream from, final OutputStream to,
final FileStreamingProgressListener progressListener, final long start, final long length,
private static long copyStreams(
final InputStream from, final OutputStream to,
final FileStreamingProgressListener progressListener,
final long start, final long length,
final String filename) throws IOException {
final long startMillis = System.currentTimeMillis();
log.trace("Start of copy-streams of file {} from {} to {}", filename, start, length);
@@ -287,7 +288,7 @@ public final class FileStreamingUtil {
long shippedSinceLastEvent = 0;
while (toContinue) {
final int r = from.read(buf);
final int r = from.read(buf, 0, Math.min(BUFFER_SIZE, toRead > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) toRead));
if (r == -1) {
break;
}
@@ -319,8 +320,8 @@ public final class FileStreamingUtil {
final long totalTime = System.currentTimeMillis() - startMillis;
if (total < length) {
throw new FileStreamingFailedException(filename + ": " + (length - total)
+ " bytes could not be written to client, total time on write: !" + totalTime + " ms");
throw new FileStreamingFailedException(
filename + ": " + (length - total) + " bytes could not be written to client, total time on write: !" + totalTime + " ms");
}
log.trace("Finished copy-stream of file {} with length {} in {} ms", filename, length, totalTime);
@@ -417,6 +418,5 @@ public final class FileStreamingUtil {
private long getTotal() {
return total;
}
}
}
}

View File

@@ -20,13 +20,13 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.function.Supplier;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.hawkbit.repository.artifact.model.DbArtifact;
import org.eclipse.hawkbit.repository.artifact.model.DbArtifactHash;
import org.eclipse.hawkbit.artifact.model.ArtifactStream;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
@@ -43,33 +43,8 @@ class FileStreamingUtilTest {
private static final String CONTENT = "This is some very long string which is intended to test";
private static final byte[] CONTENT_BYTES = CONTENT.getBytes(StandardCharsets.UTF_8);
private static final DbArtifact TEST_ARTIFACT = new DbArtifact() {
@Override
public String getArtifactId() {
return "1";
}
@Override
public DbArtifactHash getHashes() {
return new DbArtifactHash("sha1-111", "md5-123", "sha256-123");
}
@Override
public long getSize() {
return CONTENT_BYTES.length;
}
@Override
public String getContentType() {
return "text/plain";
}
@Override
public InputStream getFileInputStream() {
return new ByteArrayInputStream(CONTENT_BYTES);
}
};
private static final Supplier<ArtifactStream> TEST_ARTIFACT =
() -> new ArtifactStream(new ByteArrayInputStream(CONTENT_BYTES), CONTENT_BYTES.length, "sha1-111");
@Test
void shouldProcessRangeHeaderForMultipartRequests() throws IOException {
@@ -78,11 +53,11 @@ class FileStreamingUtilTest {
Mockito.when(servletResponse.getOutputStream()).thenReturn(outputStream);
final HttpServletRequest servletRequest = Mockito.mock(HttpServletRequest.class);
Mockito.when(servletRequest.getHeader("Range")).thenReturn("bytes=0-10,9-15,16-");
Mockito.when(servletRequest.getHeader("Range")).thenReturn("bytes=0-10,11-15,16-");
long lastModified = System.currentTimeMillis();
final ResponseEntity<InputStream> responseEntity = FileStreamingUtil.writeFileResponse(TEST_ARTIFACT,
"test.file", lastModified, servletResponse, servletRequest, null);
final ResponseEntity<InputStream> responseEntity = FileStreamingUtil.writeFileResponse(
TEST_ARTIFACT.get(), "test.file", lastModified, servletResponse, servletRequest, null);
assertThat(responseEntity).isNotNull();
verify(servletResponse).setDateHeader(HttpHeaders.LAST_MODIFIED, lastModified);
@@ -92,7 +67,7 @@ class FileStreamingUtilTest {
verify(outputStream).print(stringCaptor.capture());
assertThat(stringCaptor.getValue()).contains("--THIS_STRING_SEPARATES_MULTIPART--");
verify(outputStream, times(3)).write(any(), anyInt(), lenCaptor.capture());
assertThat(lenCaptor.getAllValues()).containsExactly(11, 7, 39); // Range lengths
assertThat(lenCaptor.getAllValues()).containsExactly(11, 5, 39); // Range lengths
}
@Test
@@ -105,8 +80,8 @@ class FileStreamingUtilTest {
final HttpServletRequest servletRequest = Mockito.mock(HttpServletRequest.class);
Mockito.when(servletRequest.getHeader("Range")).thenReturn("bytes=0-10***,9-15,16-");
final ResponseEntity<InputStream> responseEntity = FileStreamingUtil.writeFileResponse(TEST_ARTIFACT,
"test.file", lastModified, servletResponse, servletRequest, null);
final ResponseEntity<InputStream> responseEntity = FileStreamingUtil.writeFileResponse(
TEST_ARTIFACT.get(), "test.file", lastModified, servletResponse, servletRequest, null);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
verify(outputStream, times(0)).print(anyString());