Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix upload quota check and provide better error message #893

Merged
merged 13 commits into from Oct 30, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -137,6 +137,11 @@ public enum SpServerError {
*/
SP_QUOTA_EXCEEDED("hawkbit.server.error.quota.tooManyEntries", "Too many entries have been inserted."),

/**
*
*/
SP_SIZE_QUOTA_EXCEEDED("hawkbit.server.error.quota.sizeExceeded", "File size exceeds quota."),
dobleralex marked this conversation as resolved.
Show resolved Hide resolved

/**
* error message, which describes that the action can not be canceled cause
* the action is inactive.
Expand Down
Expand Up @@ -13,7 +13,7 @@
import org.eclipse.hawkbit.repository.model.BaseEntity;

/**
* Thrown if too many entries are added to repository.
* Thrown if quota is exceeded (too many entries, file size, storage size).
*
*/
public final class QuotaExceededException extends AbstractServerRtException {
Expand All @@ -22,22 +22,45 @@ public final class QuotaExceededException extends AbstractServerRtException {

private static final String ASSIGNMENT_QUOTA_EXCEEDED_MESSAGE = "Quota exceeded: Cannot assign %s more %s entities to %s '%s'. The maximum is %s.";

public enum QuotaType {
dobleralex marked this conversation as resolved.
Show resolved Hide resolved
SIZE_QUOTA(SpServerError.SP_SIZE_QUOTA_EXCEEDED), ASSIGNMENT_QUOTA(SpServerError.SP_QUOTA_EXCEEDED);

private final SpServerError errorType;

QuotaType(SpServerError errorType) {
this.errorType = errorType;
}
}

/**
* Creates a new QuotaExceededException with
* {@link SpServerError#SP_QUOTA_EXCEEDED} error.
*/
public QuotaExceededException() {
super(SpServerError.SP_QUOTA_EXCEEDED);
super(QuotaType.ASSIGNMENT_QUOTA.errorType);
}

/**
* Creates a new QuotaExceededException with a custom error message.
*
*
* @param message
* The custom error message.
*/
public QuotaExceededException(final String message) {
super(message, SpServerError.SP_QUOTA_EXCEEDED);
this(message, QuotaType.ASSIGNMENT_QUOTA);
}

/**
* Creates a new QuotaExceededException with a custom error message and
* quota type.
*
* @param message
* The custom error message.
* @param quotaType
* {@link QuotaType} that will lead to the connected error type
*/
public QuotaExceededException(final String message, QuotaType quotaType) {
super(message, quotaType.errorType);
}

/**
Expand All @@ -50,7 +73,7 @@ public QuotaExceededException(final String message) {
* for the exception
*/
public QuotaExceededException(final String message, final Throwable cause) {
super(message, SpServerError.SP_QUOTA_EXCEEDED, cause);
super(message, QuotaType.ASSIGNMENT_QUOTA.errorType, cause);
}

/**
Expand All @@ -76,7 +99,7 @@ public QuotaExceededException(final Class<? extends BaseEntity> type, final long
*/
public QuotaExceededException(final String type, final long inserted, final int quota) {
super("Request contains too many entries of {" + type + "}. {" + inserted + "} is beyond the permitted {"
+ quota + "}.", SpServerError.SP_QUOTA_EXCEEDED);
+ quota + "}.", QuotaType.ASSIGNMENT_QUOTA.errorType);
}

/**
Expand Down Expand Up @@ -121,10 +144,9 @@ public QuotaExceededException(final Class<?> type, final Class<?> parentType, fi
* parent entity.
*/
public QuotaExceededException(final String type, final String parentType, final Object parentId,
final long requested,
final long quota) {
final long requested, final long quota) {
super(String.format(ASSIGNMENT_QUOTA_EXCEEDED_MESSAGE, requested, type, parentType,
parentId != null ? String.valueOf(parentId) : "<new>", quota), SpServerError.SP_QUOTA_EXCEEDED);
parentId != null ? String.valueOf(parentId) : "<new>", quota), QuotaType.ASSIGNMENT_QUOTA.errorType);
}

}
Expand Up @@ -53,10 +53,6 @@ public class JpaArtifactManagement implements ArtifactManagement {

private static final Logger LOG = LoggerFactory.getLogger(JpaArtifactManagement.class);

private static final String MAX_ARTIFACT_SIZE_EXCEEDED = "Quota exceeded: The artifact '%s' (%s bytes) which has been uploaded for software module '%s' exceeds the maximum artifact size of %s bytes.";

private static final String MAX_ARTIFACT_SIZE_TOTAL_EXCEEDED = "Quota exceeded: The artifact '%s' (%s bytes) cannot be uploaded. The maximum total artifact storage of %s bytes would be exceeded.";

private final LocalArtifactRepository localArtifactRepository;

private final SoftwareModuleRepository softwareModuleRepository;
Expand Down Expand Up @@ -105,8 +101,6 @@ public Artifact create(final ArtifactUpload artifactUpload) {
final Artifact existing = checkForExistingArtifact(filename, artifactUpload.overrideExisting(), softwareModule);

assertArtifactQuota(moduleId, 1);
assertMaxArtifactSizeQuota(filename, moduleId, artifactUpload.getFilesize());
assertMaxArtifactStorageQuota(filename, artifactUpload.getFilesize());

dobleralex marked this conversation as resolved.
Show resolved Hide resolved
return getOrCreateArtifact(artifactUpload)
.map(artifact -> storeArtifactMetadata(softwareModule, filename, artifact, existing)).orElse(null);
Expand All @@ -125,10 +119,22 @@ private Optional<AbstractDbArtifact> getOrCreateArtifact(final ArtifactUpload ar
}

private AbstractDbArtifact storeArtifact(final ArtifactUpload artifactUpload) {
AbstractDbArtifact dbArtifact = null;
try {
return artifactRepository.store(tenantAware.getCurrentTenant(), artifactUpload.getInputStream(),
dbArtifact = artifactRepository.store(tenantAware.getCurrentTenant(), artifactUpload.getInputStream(),
artifactUpload.getFilename(), artifactUpload.getContentType(),
new DbArtifactHash(artifactUpload.getProvidedSha1Sum(), artifactUpload.getProvidedMd5Sum(), artifactUpload.getProvidedSha256Sum()));
new DbArtifactHash(artifactUpload.getProvidedSha1Sum(), artifactUpload.getProvidedMd5Sum(),
artifactUpload.getProvidedSha256Sum()));

assertMaxArtifactStorageQuota(artifactUpload.getFilename(), dbArtifact.getSize());
assertMaxArtifactSizeQuota(artifactUpload.getFilename(), artifactUpload.getModuleId(),
dbArtifact.getSize());
return dbArtifact;
} catch (QuotaExceededException e) {
if (dbArtifact != null) {
dobleralex marked this conversation as resolved.
Show resolved Hide resolved
artifactRepository.deleteBySha1(tenantAware.getCurrentTenant(), dbArtifact.getHashes().getSha1());
}
throw e;
} catch (final ArtifactStoreException e) {
throw new ArtifactUploadFailedException(e);
} catch (final HashNotMatchException e) {
Expand All @@ -147,29 +153,14 @@ private void assertArtifactQuota(final long id, final int requested) {

private void assertMaxArtifactSizeQuota(final String filename, final long id, final long artifactSize) {
final long maxArtifactSize = quotaManagement.getMaxArtifactSize();
if (maxArtifactSize <= 0) {
return;
}
if (artifactSize > maxArtifactSize) {
final String msg = String.format(MAX_ARTIFACT_SIZE_EXCEEDED, filename, artifactSize, id, maxArtifactSize);
LOG.warn(msg);
throw new QuotaExceededException(msg);
}
QuotaHelper.assertMaxArtifactSizeQuota(filename, id, artifactSize, maxArtifactSize);
}

private void assertMaxArtifactStorageQuota(final String filename, final long artifactSize) {
final Long currentlyUsed = localArtifactRepository.getSumOfUndeletedArtifactSize().orElse(0L);
final long maxArtifactSizeTotal = quotaManagement.getMaxArtifactStorage();
if (maxArtifactSizeTotal <= 0) {
return;
}

final Long currentlyUsed = localArtifactRepository.getSumOfUndeletedArtifactSize().orElse(0L);
if (currentlyUsed + artifactSize > maxArtifactSizeTotal) {
final String msg = String.format(MAX_ARTIFACT_SIZE_TOTAL_EXCEEDED, filename, artifactSize,
maxArtifactSizeTotal);
LOG.warn(msg);
throw new QuotaExceededException(msg);
}
QuotaHelper.assertMaxArtifactStorageQuota(filename, artifactSize, currentlyUsed, maxArtifactSizeTotal);
}

@Override
Expand Down
Expand Up @@ -8,6 +8,7 @@
*/
package org.eclipse.hawkbit.repository.jpa.utils;

import java.text.DecimalFormat;
import java.util.function.ToLongFunction;

import javax.validation.constraints.NotNull;
Expand All @@ -17,7 +18,7 @@
import org.slf4j.LoggerFactory;

/**
* Helper class to check assignment quotas.
* Helper class to check quotas.
*/
public final class QuotaHelper {

Expand All @@ -26,6 +27,14 @@ public final class QuotaHelper {
*/
private static final Logger LOG = LoggerFactory.getLogger(QuotaHelper.class);

private static final String MAX_ARTIFACT_SIZE_EXCEEDED = "Quota exceeded: The artifact '%s' (%s) which has been uploaded for software module '%s' exceeds the maximum artifact size of %s.";

private static final String MAX_ARTIFACT_SIZE_TOTAL_EXCEEDED = "Quota exceeded: The artifact '%s' (%s) cannot be uploaded. The maximum total artifact storage of %s bytes would be exceeded.";
dobleralex marked this conversation as resolved.
Show resolved Hide resolved

private static final String MAX_ASSIGNMENT_QUOTA_EXCEEDED = "Quota exceeded: Cannot assign %s entities at once. The maximum is %s.";
public static final String KB = "KB";
dobleralex marked this conversation as resolved.
Show resolved Hide resolved
public static final String MB = "MB";

private QuotaHelper() {
// no need to instantiate this class
}
Expand Down Expand Up @@ -142,10 +151,76 @@ public static <T> void assertAssignmentQuota(final T parentId, final long reques
*/
public static void assertAssignmentRequestSizeQuota(final long requested, final long limit) {
if (requested > limit) {
final String message = String.format(
"Quota exceeded: Cannot assign %s entities at once. The maximum is %s.", requested, limit);
final String message = String.format(MAX_ASSIGNMENT_QUOTA_EXCEEDED, requested, limit);
LOG.warn(message);
throw new QuotaExceededException(message);
}
}

/**
* Assert that the size of a single artifact does not exceed the limit
*
* @param filename
* name of the artifact, used in logs
* @param softwareModuleId
* id of the software module, used on logs
* @param artifactSize
* size of the artifact
* @param maxArtifactSize
* max allowed file size
*/
public static void assertMaxArtifactSizeQuota(final String filename, final long softwareModuleId,
final long artifactSize, final long maxArtifactSize) {
if (maxArtifactSize <= 0) {
return;
}
if (artifactSize > maxArtifactSize) {
final String msg = String.format(MAX_ARTIFACT_SIZE_EXCEEDED, filename,
byteValueToReadableString(artifactSize), softwareModuleId,
byteValueToReadableString(maxArtifactSize));
LOG.warn(msg);
throw new QuotaExceededException(msg, QuotaExceededException.QuotaType.SIZE_QUOTA);
}
}

/**
* Assert that the size of an artifact does not exceed the allowed total
* artifact storage size
*
* @param filename
* name of the artifact, used in logs
* @param artifactSize
* size of the artifact
* @param currentlyUsed
* currently occupied artifact storage
* @param maxArtifactSizeTotal
* max allowed total artifact storage size
*/
public static void assertMaxArtifactStorageQuota(final String filename, final long artifactSize,
final long currentlyUsed, final long maxArtifactSizeTotal) {
if (maxArtifactSizeTotal <= 0) {
return;
}

if (currentlyUsed + artifactSize > maxArtifactSizeTotal) {
final String msg = String.format(MAX_ARTIFACT_SIZE_TOTAL_EXCEEDED, filename,
byteValueToReadableString(artifactSize), byteValueToReadableString(maxArtifactSizeTotal));
LOG.warn(msg);
throw new QuotaExceededException(msg, QuotaExceededException.QuotaType.SIZE_QUOTA);
}
}

/**
* Convert byte values to human readable strings with units
*/
private static String byteValueToReadableString(long byteValue) {
dobleralex marked this conversation as resolved.
Show resolved Hide resolved
double outputValue = byteValue / 1024.0;
String unit = KB;
if (outputValue >= 1024) {
outputValue = outputValue / 1024.0;
unit = MB;
}
DecimalFormat df = new DecimalFormat("#.##");
return new StringBuilder(df.format(outputValue)).append(" ").append(unit).toString();
}
}
Expand Up @@ -205,7 +205,9 @@ public void uploadArtifactFailsIfTooLarge() throws Exception {
// try to upload
mvc.perform(fileUpload("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId()).file(file)
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
.andExpect(status().isForbidden());
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.exceptionClass", equalTo(QuotaExceededException.class.getName())))
.andExpect(jsonPath("$.errorCode", equalTo(SpServerError.SP_SIZE_QUOTA_EXCEEDED.getKey())));
}

@Test
Expand Down Expand Up @@ -431,7 +433,7 @@ public void uploadArtifactsUntilStorageQuotaExceeded() throws Exception {
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.exceptionClass", equalTo(QuotaExceededException.class.getName())))
.andExpect(jsonPath("$.errorCode", equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey())));
.andExpect(jsonPath("$.errorCode", equalTo(SpServerError.SP_SIZE_QUOTA_EXCEEDED.getKey())));

}

Expand Down
Expand Up @@ -61,6 +61,7 @@ public class ResponseExceptionHandler {
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_DELETE_FAILED, HttpStatus.INTERNAL_SERVER_ERROR);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ARTIFACT_LOAD_FAILED, HttpStatus.INTERNAL_SERVER_ERROR);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_QUOTA_EXCEEDED, HttpStatus.FORBIDDEN);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_SIZE_QUOTA_EXCEEDED, HttpStatus.FORBIDDEN);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ACTION_NOT_CANCELABLE, HttpStatus.METHOD_NOT_ALLOWED);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_ACTION_NOT_FORCE_QUITABLE, HttpStatus.METHOD_NOT_ALLOWED);
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_DS_CREATION_FAILED_MISSING_MODULE, HttpStatus.BAD_REQUEST);
Expand Down
Expand Up @@ -22,6 +22,7 @@
import org.eclipse.hawkbit.repository.exception.ArtifactUploadFailedException;
import org.eclipse.hawkbit.repository.exception.InvalidMD5HashException;
import org.eclipse.hawkbit.repository.exception.InvalidSHA1HashException;
import org.eclipse.hawkbit.repository.exception.QuotaExceededException;
import org.eclipse.hawkbit.repository.model.Artifact;
import org.eclipse.hawkbit.repository.model.ArtifactUpload;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
Expand Down Expand Up @@ -106,6 +107,10 @@ protected void interruptUploadDueToUploadFailed() {
interruptUploadAndSetReason(i18n.getMessage("message.upload.failed"));
}

protected void interruptUploadDueToQuotaExceeded() {
interruptUploadAndSetReason(i18n.getMessage("message.upload.quota"));
}

protected void interruptUploadDueToDuplicateFile() {
interruptUploadAndSetReason(i18n.getMessage("message.no.duplicateFiles"));
}
Expand Down Expand Up @@ -175,6 +180,8 @@ protected void publishUploadFailedAndFinishedEvent(final FileUploadId fileUpload
LOG.info("Upload failed for file {} due to reason: {}, exception: {}", fileUploadId, failureReason,
uploadException.getMessage());

uiNotification.displayValidationError(uploadException.getMessage());

final FileUploadProgress fileUploadProgress = new FileUploadProgress(fileUploadId,
FileUploadStatus.UPLOAD_FAILED,
StringUtils.isBlank(failureReason) ? i18n.getMessage("message.upload.failed") : failureReason);
Expand Down Expand Up @@ -230,6 +237,9 @@ public void run() {
try {
UI.setCurrent(vaadinUi);
streamToRepository();
} catch (final QuotaExceededException e) {
interruptUploadDueToQuotaExceeded();
publishUploadFailedAndFinishedEvent(fileUploadId, e);
} catch (final RuntimeException e) {
interruptUploadDueToUploadFailed();
publishUploadFailedAndFinishedEvent(fileUploadId, e);
Expand Down
1 change: 1 addition & 0 deletions hawkbit-ui/src/main/resources/messages.properties
Expand Up @@ -466,6 +466,7 @@ message.delete.artifact = Are you sure you want to delete artifact {0} ?
message.swModule.deleted = {0} Software Module(s) deleted
message.error.swModule.notDeleted = Upload is running for Software Module(s)
message.upload.failed = Streaming Failed
message.upload.quota = Quota exceeded
message.uploadedfile.size.exceeded = File size exceeded. Allowed size {0} bytes
message.uploadedfile.illegalFilename = Filename contains illegal characters
message.artifact.deleted = Artifact with file {0} deleted successfully
Expand Down