Cleanup file streaming utilities (#559)

* Cleanup file streaming.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Added missing comments.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Fix typo.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Split utility class.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Dependency cleanup.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Add missing dependency,

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Remove repository api dependency from rest core.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Fix build and sonar issues.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Remove custom ConstraintViolationException

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* RequestMapping should be public.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Fix errors.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Removed dead code.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Not null

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Fix nullpointer.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Code cleanup.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>
This commit is contained in:
Kai Zimmermann
2017-07-19 12:43:07 +02:00
committed by GitHub
parent 66feae2756
commit 09b24fa97d
59 changed files with 911 additions and 908 deletions

View File

@@ -8,12 +8,9 @@
*/
package org.eclipse.hawkbit.ddi.rest.resource;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.hawkbit.api.ApiType;
import org.eclipse.hawkbit.api.ArtifactUrlHandler;
import org.eclipse.hawkbit.api.URLPlaceholder;
@@ -35,8 +32,6 @@ import org.springframework.hateoas.Link;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.http.HttpRequest;
import com.google.common.base.Charsets;
/**
* Utility class for the DDI API.
*/
@@ -148,23 +143,4 @@ public final class DataConversionHelper {
return result;
}
static void writeMD5FileResponse(final String fileName, final HttpServletResponse response, final Artifact artifact)
throws IOException {
final StringBuilder builder = new StringBuilder();
builder.append(artifact.getMd5Hash());
builder.append(" ");
builder.append(fileName);
final byte[] content = builder.toString().getBytes(Charsets.US_ASCII);
final StringBuilder header = new StringBuilder();
header.append("attachment;filename=");
header.append(fileName);
header.append(DdiRestConstants.ARTIFACT_MD5_DWNL_SUFFIX);
response.setContentLength(content.length);
response.setHeader("Content-Disposition", header.toString());
response.getOutputStream().write(content, 0, content.length);
}
}

View File

@@ -17,7 +17,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import org.eclipse.hawkbit.api.ArtifactUrlHandler;
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback;
import org.eclipse.hawkbit.ddi.json.model.DdiActionHistory;
import org.eclipse.hawkbit.ddi.json.model.DdiCancel;
@@ -38,6 +38,7 @@ import org.eclipse.hawkbit.repository.RepositoryConstants;
import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
import org.eclipse.hawkbit.repository.SystemManagement;
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
import org.eclipse.hawkbit.repository.event.remote.DownloadProgressEvent;
import org.eclipse.hawkbit.repository.exception.ArtifactBinaryNotFoundException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.SoftwareModuleNotAssignedToTargetException;
@@ -47,8 +48,9 @@ import org.eclipse.hawkbit.repository.model.ActionStatus;
import org.eclipse.hawkbit.repository.model.Artifact;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.rest.util.FileStreamingUtil;
import org.eclipse.hawkbit.rest.util.HttpUtil;
import org.eclipse.hawkbit.rest.util.RequestResponseContextHolder;
import org.eclipse.hawkbit.rest.util.RestResourceConversionHelper;
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.util.IpUtil;
@@ -56,7 +58,10 @@ import org.hibernate.validator.constraints.NotEmpty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Scope;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.ServletServerHttpRequest;
@@ -80,6 +85,12 @@ public class DdiRootController implements DdiRootControllerRestApi {
private static final Logger LOG = LoggerFactory.getLogger(DdiRootController.class);
private static final String GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET = "given action ({}) is not assigned to given target ({}).";
@Autowired
private ApplicationEventPublisher eventPublisher;
@Autowired
private ApplicationContext applicationContext;
@Autowired
private ControllerManagement controllerManagement;
@@ -142,7 +153,7 @@ public class DdiRootController implements DdiRootControllerRestApi {
@PathVariable("controllerId") final String controllerId,
@PathVariable("softwareModuleId") final Long softwareModuleId,
@PathVariable("fileName") final String fileName) {
ResponseEntity<InputStream> result;
final ResponseEntity<InputStream> result;
final Target target = controllerManagement.findByControllerId(controllerId)
.orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId));
@@ -159,19 +170,26 @@ public class DdiRootController implements DdiRootControllerRestApi {
@SuppressWarnings("squid:S3655")
final Artifact artifact = module.getArtifactByFilename(fileName).get();
final DbArtifact file = artifactManagement.loadArtifactBinary(artifact.getSha1Hash())
final AbstractDbArtifact file = artifactManagement.loadArtifactBinary(artifact.getSha1Hash())
.orElseThrow(() -> new ArtifactBinaryNotFoundException(artifact.getSha1Hash()));
final String ifMatch = requestResponseContextHolder.getHttpServletRequest().getHeader("If-Match");
if (ifMatch != null && !RestResourceConversionHelper.matchesHttpHeader(ifMatch, artifact.getSha1Hash())) {
final String ifMatch = requestResponseContextHolder.getHttpServletRequest().getHeader(HttpHeaders.IF_MATCH);
if (ifMatch != null && !HttpUtil.matchesHttpHeader(ifMatch, artifact.getSha1Hash())) {
result = new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED);
} else {
final ActionStatus action = checkAndLogDownload(requestResponseContextHolder.getHttpServletRequest(),
target, module.getId());
result = RestResourceConversionHelper.writeFileResponse(artifact,
final Long statusId = action.getId();
result = FileStreamingUtil.writeFileResponse(file, artifact.getFilename(),
artifact.getLastModifiedAt() != null ? artifact.getLastModifiedAt() : artifact.getCreatedAt(),
requestResponseContextHolder.getHttpServletResponse(),
requestResponseContextHolder.getHttpServletRequest(), file, controllerManagement,
action.getId());
requestResponseContextHolder.getHttpServletRequest(),
(length, shippedSinceLastEvent, total) -> eventPublisher
.publishEvent(new DownloadProgressEvent(tenantAware.getCurrentTenant(), statusId,
shippedSinceLastEvent, applicationContext.getId())));
}
}
return result;
@@ -218,15 +236,19 @@ public class DdiRootController implements DdiRootControllerRestApi {
return ResponseEntity.notFound().build();
}
final Artifact artifact = module.getArtifactByFilename(fileName)
.orElseThrow(() -> new EntityNotFoundException(Artifact.class, fileName));
try {
DataConversionHelper.writeMD5FileResponse(fileName, requestResponseContextHolder.getHttpServletResponse(),
module.getArtifactByFilename(fileName).get());
FileStreamingUtil.writeMD5FileResponse(requestResponseContextHolder.getHttpServletResponse(),
artifact.getMd5Hash(), fileName);
} catch (final IOException e) {
LOG.error("Failed to stream MD5 File", e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
return ResponseEntity.ok().build();
}
@Override
@@ -254,11 +276,11 @@ public class DdiRootController implements DdiRootControllerRestApi {
final HandlingType handlingType = action.isForce() ? HandlingType.FORCED : HandlingType.ATTEMPT;
List<String> actionHistoryMsgs = controllerManagement.getActionHistoryMessages(action.getId(),
final List<String> actionHistoryMsgs = controllerManagement.getActionHistoryMessages(action.getId(),
actionHistoryMessageCount == null ? Integer.parseInt(DdiRestConstants.NO_ACTION_HISTORY)
: actionHistoryMessageCount);
DdiActionHistory actionHistory = actionHistoryMsgs.isEmpty() ? null
final DdiActionHistory actionHistory = actionHistoryMsgs.isEmpty() ? null
: new DdiActionHistory(action.getStatus().name(), actionHistoryMsgs);
final DdiDeploymentBase base = new DdiDeploymentBase(Long.toString(action.getId()),

View File

@@ -112,7 +112,7 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest {
// test failed If-match
mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
.header("If-Match", "fsjkhgjfdhg"))
.header(HttpHeaders.IF_MATCH, "fsjkhgjfdhg"))
.andExpect(status().isPreconditionFailed());
// test invalid range
@@ -311,6 +311,18 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest {
assertThat(result.getResponse().getContentAsByteArray())
.isEqualTo(Arrays.copyOfRange(random, 1000, resultLength));
// Start download from file end fails
mvc.perform(
get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), "file1")
.header("Range", "bytes=" + random.length + "-"))
.andExpect(status().isRequestedRangeNotSatisfiable())
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
.andExpect(header().string("Accept-Ranges", "bytes"))
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
.andExpect(header().string("Content-Range", "bytes */" + random.length))
.andExpect(header().string("Content-Disposition", "attachment;filename=file1"));
// multipart download - first 20 bytes in 2 parts
result = mvc
.perform(