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:
@@ -10,13 +10,14 @@ package org.eclipse.hawkbit.artifact.repository;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||
|
||||
import com.mongodb.gridfs.GridFSDBFile;
|
||||
import com.mongodb.gridfs.GridFSFile;
|
||||
|
||||
/**
|
||||
* A wrapper object for the {@link DbArtifact} object which returns the
|
||||
* A wrapper object for the {@link AbstractDbArtifact} object which returns the
|
||||
* {@link InputStream} directly from {@link GridFSDBFile#getInputStream()} which
|
||||
* retrieves when calling {@link #getFileInputStream()} always a new
|
||||
* {@link InputStream} and not the same.
|
||||
@@ -24,7 +25,7 @@ import com.mongodb.gridfs.GridFSFile;
|
||||
*
|
||||
*
|
||||
*/
|
||||
public class GridFsArtifact extends DbArtifact {
|
||||
public class GridFsArtifact extends AbstractDbArtifact {
|
||||
|
||||
private final GridFSFile dbFile;
|
||||
|
||||
@@ -32,6 +33,8 @@ public class GridFsArtifact extends DbArtifact {
|
||||
* @param dbFile
|
||||
*/
|
||||
public GridFsArtifact(final GridFSFile dbFile) {
|
||||
super(dbFile.getId().toString(), new DbArtifactHash(dbFile.getFilename(), dbFile.getMD5()), dbFile.getLength(),
|
||||
dbFile.getContentType());
|
||||
this.dbFile = dbFile;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.slf4j.Logger;
|
||||
@@ -89,7 +89,7 @@ public class MongoDBArtifactStore implements ArtifactRepository {
|
||||
* @return The DbArtifact object or {@code null} if no file exists.
|
||||
*/
|
||||
@Override
|
||||
public DbArtifact getArtifactBySha1(final String tenant, final String sha1Hash) {
|
||||
public AbstractDbArtifact getArtifactBySha1(final String tenant, final String sha1Hash) {
|
||||
|
||||
GridFSDBFile found = gridFs.findOne(new Query()
|
||||
.addCriteria(Criteria.where(FILENAME).is(sha1Hash).and(TENANT_QUERY).is(sanitizeTenant(tenant))));
|
||||
@@ -104,13 +104,13 @@ public class MongoDBArtifactStore implements ArtifactRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DbArtifact store(final String tenant, final InputStream content, final String filename,
|
||||
public AbstractDbArtifact store(final String tenant, final InputStream content, final String filename,
|
||||
final String contentType) {
|
||||
return store(tenant, content, filename, contentType, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DbArtifact store(final String tenant, final InputStream content, final String filename,
|
||||
public AbstractDbArtifact store(final String tenant, final InputStream content, final String filename,
|
||||
final String contentType, final DbArtifactHash hash) {
|
||||
File tempFile = null;
|
||||
try {
|
||||
@@ -155,8 +155,8 @@ public class MongoDBArtifactStore implements ArtifactRepository {
|
||||
|
||||
}
|
||||
|
||||
private DbArtifact store(final String t, final InputStream content, final String contentType, final OutputStream os,
|
||||
final File tempFile, final DbArtifactHash hash) {
|
||||
private AbstractDbArtifact store(final String t, final InputStream content, final String contentType,
|
||||
final OutputStream os, final File tempFile, final DbArtifactHash hash) {
|
||||
final GridFsArtifact storedArtifact;
|
||||
final String tenant = sanitizeTenant(t);
|
||||
try {
|
||||
@@ -216,15 +216,16 @@ public class MongoDBArtifactStore implements ArtifactRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a list of {@link GridFSDBFile} to paged list of {@link DbArtifact}s.
|
||||
* Maps a list of {@link GridFSDBFile} to a list of
|
||||
* {@link AbstractDbArtifact}s.
|
||||
*
|
||||
* @param tenant
|
||||
* the tenant
|
||||
* @param dbFiles
|
||||
* the list of mongoDB gridFs files.
|
||||
* @return a paged list of artifacts mapped from the given dbFiles
|
||||
* @return list of artifacts mapped from the given dbFiles
|
||||
*/
|
||||
private static List<DbArtifact> map(final List<GridFSDBFile> dbFiles) {
|
||||
private static List<AbstractDbArtifact> map(final List<GridFSDBFile> dbFiles) {
|
||||
return dbFiles.stream().map(MongoDBArtifactStore::map).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -233,14 +234,14 @@ public class MongoDBArtifactStore implements ArtifactRepository {
|
||||
*
|
||||
* @param ids
|
||||
* the ids of the files to lookup.
|
||||
* @return list of artfiacts
|
||||
* @return list of artifacts
|
||||
*/
|
||||
public List<DbArtifact> getArtifactsByIds(final List<String> ids) {
|
||||
public List<AbstractDbArtifact> getArtifactsByIds(final List<String> ids) {
|
||||
return map(gridFs.find(new Query().addCriteria(Criteria.where(ID).in(ids))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a single {@link GridFSFile} to {@link DbArtifact}.
|
||||
* Maps a single {@link GridFSFile} to {@link AbstractDbArtifact}.
|
||||
*
|
||||
* @param tenant
|
||||
* the tenant
|
||||
@@ -252,12 +253,8 @@ public class MongoDBArtifactStore implements ArtifactRepository {
|
||||
if (fsFile == null) {
|
||||
return null;
|
||||
}
|
||||
final GridFsArtifact artifact = new GridFsArtifact(fsFile);
|
||||
artifact.setArtifactId(fsFile.getId().toString());
|
||||
artifact.setSize(fsFile.getLength());
|
||||
artifact.setContentType(fsFile.getContentType());
|
||||
artifact.setHashes(new DbArtifactHash(fsFile.getFilename(), fsFile.getMD5()));
|
||||
return artifact;
|
||||
|
||||
return new GridFsArtifact(fsFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -10,21 +10,28 @@ package org.eclipse.hawkbit.artifact.repository;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.amazonaws.services.s3.AmazonS3;
|
||||
|
||||
/**
|
||||
* An {@link DbArtifact} implementation which retrieves the {@link InputStream}
|
||||
* from the {@link AmazonS3} client.
|
||||
* An {@link AbstractDbArtifact} implementation which retrieves the
|
||||
* {@link InputStream} from the {@link AmazonS3} client.
|
||||
*/
|
||||
public class S3Artifact extends DbArtifact {
|
||||
public class S3Artifact extends AbstractDbArtifact {
|
||||
|
||||
private final AmazonS3 amazonS3;
|
||||
private final S3RepositoryProperties s3Properties;
|
||||
private final String key;
|
||||
|
||||
S3Artifact(final AmazonS3 amazonS3, final S3RepositoryProperties s3Properties, final String key) {
|
||||
S3Artifact(final AmazonS3 amazonS3, final S3RepositoryProperties s3Properties, final String key,
|
||||
final String artifactId, final DbArtifactHash hashes, final Long size, final String contentType) {
|
||||
super(artifactId, hashes, size, contentType);
|
||||
Assert.notNull(amazonS3, "S3 cannot be null");
|
||||
Assert.notNull(s3Properties, "Properties cannot be null");
|
||||
Assert.notNull(key, "Key cannot be null");
|
||||
this.amazonS3 = amazonS3;
|
||||
this.s3Properties = s3Properties;
|
||||
this.key = key;
|
||||
|
||||
@@ -20,7 +20,7 @@ import java.security.DigestOutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -77,7 +77,7 @@ public class S3Repository implements ArtifactRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DbArtifact store(final String tenant, final InputStream content, final String filename,
|
||||
public AbstractDbArtifact store(final String tenant, final InputStream content, final String filename,
|
||||
final String contentType) {
|
||||
return store(tenant, content, filename, contentType, null);
|
||||
}
|
||||
@@ -86,7 +86,7 @@ public class S3Repository implements ArtifactRepository {
|
||||
// suppress warning, of not strong enough hashing algorithm, SHA-1 and MD5
|
||||
// is not used security related
|
||||
@SuppressWarnings("squid:S2070")
|
||||
public DbArtifact store(final String tenant, final InputStream content, final String filename,
|
||||
public AbstractDbArtifact store(final String tenant, final InputStream content, final String filename,
|
||||
final String contentType, final DbArtifactHash hash) {
|
||||
final MessageDigest mdSHA1;
|
||||
final MessageDigest mdMD5;
|
||||
@@ -117,7 +117,7 @@ public class S3Repository implements ArtifactRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private DbArtifact store(final String tenant, final String sha1Hash16, final String mdMD5Hash16,
|
||||
private AbstractDbArtifact store(final String tenant, final String sha1Hash16, final String mdMD5Hash16,
|
||||
final String contentType, final File file, final DbArtifactHash hash) {
|
||||
final S3Artifact s3Artifact = createS3Artifact(tenant, sha1Hash16, mdMD5Hash16, contentType, file);
|
||||
checkHashes(s3Artifact, hash);
|
||||
@@ -145,14 +145,8 @@ public class S3Repository implements ArtifactRepository {
|
||||
|
||||
private S3Artifact createS3Artifact(final String tenant, final String sha1Hash, final String mdMD5Hash16,
|
||||
final String contentType, final File file) {
|
||||
final String key = objectKey(tenant, sha1Hash);
|
||||
final S3Artifact s3Artifact = new S3Artifact(amazonS3, s3Properties, key);
|
||||
s3Artifact.setContentType(contentType);
|
||||
s3Artifact.setArtifactId(sha1Hash);
|
||||
s3Artifact.setSize(file.length());
|
||||
s3Artifact.setContentType(contentType);
|
||||
s3Artifact.setHashes(new DbArtifactHash(sha1Hash, mdMD5Hash16));
|
||||
return s3Artifact;
|
||||
return new S3Artifact(amazonS3, s3Properties, objectKey(tenant, sha1Hash), sha1Hash,
|
||||
new DbArtifactHash(sha1Hash, mdMD5Hash16), file.length(), contentType);
|
||||
}
|
||||
|
||||
private ObjectMetadata createObjectMetadata(final String mdMD5Hash16, final String contentType, final File file) {
|
||||
@@ -181,7 +175,7 @@ public class S3Repository implements ArtifactRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DbArtifact getArtifactBySha1(final String tenant, final String sha1Hash) {
|
||||
public AbstractDbArtifact getArtifactBySha1(final String tenant, final String sha1Hash) {
|
||||
final String key = objectKey(tenant, sha1Hash);
|
||||
|
||||
LOG.info("Retrieving S3 object from bucket {} and key {}", s3Properties.getBucketName(), key);
|
||||
@@ -192,17 +186,15 @@ public class S3Repository implements ArtifactRepository {
|
||||
|
||||
final ObjectMetadata s3ObjectMetadata = s3Object.getObjectMetadata();
|
||||
|
||||
final S3Artifact s3Artifact = new S3Artifact(amazonS3, s3Properties, key);
|
||||
s3Artifact.setArtifactId(sha1Hash);
|
||||
s3Artifact.setSize(s3ObjectMetadata.getContentLength());
|
||||
// the MD5Content is stored in the ETag
|
||||
s3Artifact.setHashes(new DbArtifactHash(sha1Hash,
|
||||
BaseEncoding.base16().lowerCase().encode(BaseEncoding.base64().decode(s3ObjectMetadata.getETag()))));
|
||||
s3Artifact.setContentType(s3ObjectMetadata.getContentType());
|
||||
return s3Artifact;
|
||||
return new S3Artifact(amazonS3, s3Properties, key, sha1Hash,
|
||||
new DbArtifactHash(sha1Hash,
|
||||
BaseEncoding.base16().lowerCase()
|
||||
.encode(BaseEncoding.base64().decode(s3ObjectMetadata.getETag()))),
|
||||
s3ObjectMetadata.getContentLength(), s3ObjectMetadata.getContentType());
|
||||
}
|
||||
|
||||
private static void checkHashes(final DbArtifact artifact, final DbArtifactHash hash) {
|
||||
private static void checkHashes(final AbstractDbArtifact artifact, final DbArtifactHash hash) {
|
||||
if (hash == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Random;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -117,7 +117,7 @@ public class S3RepositoryTest {
|
||||
when(s3ObjectMetadataMock.getContentType()).thenReturn(knownContentType);
|
||||
|
||||
// test
|
||||
final DbArtifact artifactBySha1 = s3RepositoryUnderTest.getArtifactBySha1(TENANT, knownSHA1Hash);
|
||||
final AbstractDbArtifact artifactBySha1 = s3RepositoryUnderTest.getArtifactBySha1(TENANT, knownSHA1Hash);
|
||||
|
||||
// verify
|
||||
assertThat(artifactBySha1.getArtifactId()).isEqualTo(knownSHA1Hash);
|
||||
@@ -152,7 +152,7 @@ public class S3RepositoryTest {
|
||||
when(amazonS3Mock.getObject(s3Properties.getBucketName(), knownSHA1Hash)).thenReturn(null);
|
||||
|
||||
// test
|
||||
final DbArtifact artifactBySha1NotExists = s3RepositoryUnderTest.getArtifactBySha1(TENANT, knownSHA1Hash);
|
||||
final AbstractDbArtifact artifactBySha1NotExists = s3RepositoryUnderTest.getArtifactBySha1(TENANT, knownSHA1Hash);
|
||||
|
||||
// verify
|
||||
assertThat(artifactBySha1NotExists).isNull();
|
||||
|
||||
@@ -14,19 +14,24 @@ import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
|
||||
/**
|
||||
* A {@link DbArtifact} implementation which dynamically creates a
|
||||
* {@link AbstractDbArtifact} implementation which dynamically creates a
|
||||
* {@link FileInputStream} on calling {@link #getFileInputStream()}.
|
||||
*/
|
||||
public class ArtifactFilesystem extends DbArtifact {
|
||||
public class ArtifactFilesystem extends AbstractDbArtifact {
|
||||
|
||||
private final File file;
|
||||
|
||||
ArtifactFilesystem(final File file) {
|
||||
public ArtifactFilesystem(final File file, final String artifactId, final DbArtifactHash hashes, final Long size,
|
||||
final String contentType) {
|
||||
super(artifactId, hashes, size, contentType);
|
||||
Assert.notNull(file, "File cannot be null");
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -88,7 +88,7 @@ public class ArtifactFilesystemRepository implements ArtifactRepository {
|
||||
}
|
||||
|
||||
final File file = createTempFile();
|
||||
final DbArtifact artifact = store(content, contentType, hash, mdSHA1, mdMD5, file);
|
||||
final AbstractDbArtifact artifact = store(content, contentType, hash, mdSHA1, mdMD5, file);
|
||||
return renameFileToSHA1Naming(tenant, file, artifact);
|
||||
}
|
||||
|
||||
@@ -103,25 +103,22 @@ public class ArtifactFilesystemRepository implements ArtifactRepository {
|
||||
if (!file.exists()) {
|
||||
return null;
|
||||
}
|
||||
final ArtifactFilesystem artifact = new ArtifactFilesystem(file);
|
||||
artifact.setArtifactId(sha1);
|
||||
artifact.setHashes(new DbArtifactHash(sha1, null));
|
||||
artifact.setSize(file.length());
|
||||
return artifact;
|
||||
|
||||
return new ArtifactFilesystem(file, sha1, new DbArtifactHash(sha1, null), file.length(), null);
|
||||
}
|
||||
|
||||
private DbArtifact store(final InputStream content, final String contentType, final DbArtifactHash hash,
|
||||
private AbstractDbArtifact store(final InputStream content, final String contentType, final DbArtifactHash hash,
|
||||
final MessageDigest mdSHA1, final MessageDigest mdMD5, final File file) {
|
||||
final DbArtifact artifact = new DbArtifact();
|
||||
AbstractDbArtifact artifact;
|
||||
try (final DigestOutputStream outputstream = openFileOutputStream(file, mdSHA1, mdMD5)) {
|
||||
final long artifactSize = ByteStreams.copy(content, outputstream);
|
||||
outputstream.flush();
|
||||
final String sha1Hash = BaseEncoding.base16().lowerCase().encode(mdSHA1.digest());
|
||||
final String md5Hash = BaseEncoding.base16().lowerCase().encode(mdMD5.digest());
|
||||
artifact.setArtifactId(sha1Hash);
|
||||
artifact.setSize(artifactSize);
|
||||
artifact.setContentType(contentType);
|
||||
artifact.setHashes(new DbArtifactHash(sha1Hash, md5Hash));
|
||||
|
||||
artifact = new ArtifactFilesystem(file, sha1Hash, new DbArtifactHash(sha1Hash, md5Hash), artifactSize,
|
||||
contentType);
|
||||
|
||||
checkHashes(artifact, hash);
|
||||
} catch (final IOException e) {
|
||||
throw new ArtifactStoreException(e.getMessage(), e);
|
||||
@@ -134,9 +131,11 @@ public class ArtifactFilesystemRepository implements ArtifactRepository {
|
||||
return artifact;
|
||||
}
|
||||
|
||||
private ArtifactFilesystem renameFileToSHA1Naming(final String tenant, final File file, final DbArtifact artifact) {
|
||||
private ArtifactFilesystem renameFileToSHA1Naming(final String tenant, final File file,
|
||||
final AbstractDbArtifact artifact) {
|
||||
final File fileSHA1Naming = getFile(tenant, artifact.getHashes().getSha1());
|
||||
final ArtifactFilesystem fileSystemArtifact = new ArtifactFilesystem(fileSHA1Naming);
|
||||
final ArtifactFilesystem fileSystemArtifact = new ArtifactFilesystem(fileSHA1Naming, artifact.getArtifactId(),
|
||||
artifact.getHashes(), artifact.getSize(), artifact.getContentType());
|
||||
if (fileSHA1Naming.exists()) {
|
||||
FileUtils.deleteQuietly(file);
|
||||
} else {
|
||||
@@ -150,14 +149,11 @@ public class ArtifactFilesystemRepository implements ArtifactRepository {
|
||||
if (!file.delete()) {
|
||||
LOG.debug("Could not delete temp file {}", file);
|
||||
}
|
||||
fileSystemArtifact.setArtifactId(artifact.getArtifactId());
|
||||
fileSystemArtifact.setContentType(artifact.getContentType());
|
||||
fileSystemArtifact.setHashes(artifact.getHashes());
|
||||
fileSystemArtifact.setSize(artifact.getSize());
|
||||
|
||||
return fileSystemArtifact;
|
||||
}
|
||||
|
||||
private DbArtifact checkHashes(final DbArtifact artifact, final DbArtifactHash hash) {
|
||||
private AbstractDbArtifact checkHashes(final AbstractDbArtifact artifact, final DbArtifactHash hash) {
|
||||
if (hash == null) {
|
||||
return artifact;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import java.util.Random;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.junit.Test;
|
||||
|
||||
import ru.yandex.qatools.allure.annotations.Description;
|
||||
@@ -52,7 +52,7 @@ public class ArtifactFilesystemRepositoryTest {
|
||||
final byte[] fileContent = randomBytes();
|
||||
final ArtifactFilesystem artifact = storeRandomArtifact(fileContent);
|
||||
|
||||
final DbArtifact artifactBySha1 = artifactFilesystemRepository.getArtifactBySha1(TENANT,
|
||||
final AbstractDbArtifact artifactBySha1 = artifactFilesystemRepository.getArtifactBySha1(TENANT,
|
||||
artifact.getHashes().getSha1());
|
||||
assertThat(artifactBySha1).isNotNull();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.io.IOException;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||
import org.junit.Test;
|
||||
|
||||
import ru.yandex.qatools.allure.annotations.Description;
|
||||
@@ -30,7 +31,8 @@ public class ArtifactFilesystemTest {
|
||||
@Description("Verifies that an exception is thrown on opening an InputStream when file does not exists")
|
||||
public void getInputStreamOfNonExistingFileThrowsException() {
|
||||
final File file = new File("fileWhichTotalDoesNotExists");
|
||||
final ArtifactFilesystem underTest = new ArtifactFilesystem(file);
|
||||
final ArtifactFilesystem underTest = new ArtifactFilesystem(file, "fileWhichTotalDoesNotExists",
|
||||
new DbArtifactHash("1", "2"), 0L, null);
|
||||
try {
|
||||
underTest.getFileInputStream();
|
||||
Assertions.fail("Expected a FileNotFoundException because file does not exists");
|
||||
@@ -45,7 +47,8 @@ public class ArtifactFilesystemTest {
|
||||
final File createTempFile = File.createTempFile(ArtifactFilesystemTest.class.getSimpleName(), "");
|
||||
createTempFile.deleteOnExit();
|
||||
|
||||
final ArtifactFilesystem underTest = new ArtifactFilesystem(createTempFile);
|
||||
final ArtifactFilesystem underTest = new ArtifactFilesystem(createTempFile,
|
||||
ArtifactFilesystemTest.class.getSimpleName(), new DbArtifactHash("1", "2"), 0L, null);
|
||||
final byte[] buffer = new byte[1024];
|
||||
IOUtils.read(underTest.getFileInputStream(), buffer);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.io.InputStream;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
@@ -42,7 +42,7 @@ public interface ArtifactRepository {
|
||||
* @throws ArtifactStoreException
|
||||
* in case storing of the artifact was not successful
|
||||
*/
|
||||
DbArtifact store(@NotEmpty String tenant, @NotNull InputStream content, @NotEmpty String filename,
|
||||
AbstractDbArtifact store(@NotEmpty String tenant, @NotNull InputStream content, @NotEmpty String filename,
|
||||
String contentType);
|
||||
|
||||
/**
|
||||
@@ -69,7 +69,7 @@ public interface ArtifactRepository {
|
||||
* in case {@code hash} is provided and not matching to the
|
||||
* calculated hashes during storing
|
||||
*/
|
||||
DbArtifact store(@NotEmpty String tenant, @NotNull InputStream content, @NotEmpty String filename,
|
||||
AbstractDbArtifact store(@NotEmpty String tenant, @NotNull InputStream content, @NotEmpty String filename,
|
||||
String contentType, DbArtifactHash hash);
|
||||
|
||||
/**
|
||||
@@ -86,7 +86,7 @@ public interface ArtifactRepository {
|
||||
void deleteBySha1(@NotEmpty String tenant, @NotEmpty String sha1Hash);
|
||||
|
||||
/**
|
||||
* Retrieves a {@link DbArtifact} from the store by it's SHA1 hash.
|
||||
* Retrieves a {@link AbstractDbArtifact} from the store by it's SHA1 hash.
|
||||
*
|
||||
* @param tenant
|
||||
* the tenant to store the artifact
|
||||
@@ -97,7 +97,7 @@ public interface ArtifactRepository {
|
||||
* @throws MethodNotSupportedException
|
||||
* if implementation does not support the operation
|
||||
*/
|
||||
DbArtifact getArtifactBySha1(@NotEmpty String tenant, @NotEmpty String sha1Hash);
|
||||
AbstractDbArtifact getArtifactBySha1(@NotEmpty String tenant, @NotEmpty String sha1Hash);
|
||||
|
||||
/**
|
||||
* Deletes all artifacts of given tenant.
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.artifact.repository.model;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Database representation of artifact.
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractDbArtifact {
|
||||
|
||||
private final String artifactId;
|
||||
private final DbArtifactHash hashes;
|
||||
private final long size;
|
||||
private final String contentType;
|
||||
|
||||
protected AbstractDbArtifact(final String artifactId, final DbArtifactHash hashes, final long size,
|
||||
final String contentType) {
|
||||
Assert.notNull(artifactId, "Artifact ID cannot be null");
|
||||
Assert.notNull(hashes, "Hashes cannot be null");
|
||||
this.artifactId = artifactId;
|
||||
this.hashes = hashes;
|
||||
this.size = size;
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ID of the artifact
|
||||
*/
|
||||
public String getArtifactId() {
|
||||
return artifactId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return hashes of the artifact
|
||||
*/
|
||||
public DbArtifactHash getHashes() {
|
||||
return hashes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return site of the artifact in bytes
|
||||
*/
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return content-type if known by the repository or <code>null</code>
|
||||
*/
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link InputStream} on this artifact. Caller has to take care
|
||||
* of closing the stream. Repeatable calls open a new {@link InputStream}.
|
||||
*
|
||||
* @return {@link InputStream} to read from artifact.
|
||||
*/
|
||||
public abstract InputStream getFileInputStream();
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.artifact.repository.model;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Database representation of artifact.
|
||||
*
|
||||
*/
|
||||
public class DbArtifact {
|
||||
|
||||
private String artifactId;
|
||||
|
||||
private DbArtifactHash hashes;
|
||||
|
||||
private Long size;
|
||||
|
||||
private String contentType;
|
||||
|
||||
private InputStream fileInputStream;
|
||||
|
||||
private OutputStream fileOutputStream;
|
||||
|
||||
public void setArtifactId(final String artifactId) {
|
||||
this.artifactId = artifactId;
|
||||
}
|
||||
|
||||
public String getArtifactId() {
|
||||
return artifactId;
|
||||
}
|
||||
|
||||
public DbArtifactHash getHashes() {
|
||||
return hashes;
|
||||
}
|
||||
|
||||
public void setHashes(final DbArtifactHash hashes) {
|
||||
this.hashes = hashes;
|
||||
}
|
||||
|
||||
public Long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(final Long size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public void setContentType(final String contentType) {
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public void setFileInputStream(final InputStream fileInputStream) {
|
||||
this.fileInputStream = fileInputStream;
|
||||
}
|
||||
|
||||
public InputStream getFileInputStream() {
|
||||
return fileInputStream;
|
||||
}
|
||||
|
||||
public OutputStream getFileOutputStream() {
|
||||
return fileOutputStream;
|
||||
}
|
||||
|
||||
public void setFileOutputStream(final OutputStream fileOutputStream) {
|
||||
this.fileOutputStream = fileOutputStream;
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,9 @@ package org.eclipse.hawkbit.artifact.repository.model;
|
||||
*/
|
||||
public class DbArtifactHash {
|
||||
|
||||
private String sha1;
|
||||
private final String sha1;
|
||||
|
||||
private String md5;
|
||||
private final String md5;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -27,19 +27,10 @@ public class DbArtifactHash {
|
||||
* the md5 hash
|
||||
*/
|
||||
public DbArtifactHash(final String sha1, final String md5) {
|
||||
super();
|
||||
this.sha1 = sha1;
|
||||
this.md5 = md5;
|
||||
}
|
||||
|
||||
public void setSha1(final String sha1) {
|
||||
this.sha1 = sha1;
|
||||
}
|
||||
|
||||
public void setMd5(final String md5) {
|
||||
this.md5 = md5;
|
||||
}
|
||||
|
||||
public String getSha1() {
|
||||
return sha1;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.eclipse.hawkbit.api.HostnameResolver;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||
import org.eclipse.hawkbit.cache.DownloadArtifactCache;
|
||||
import org.eclipse.hawkbit.cache.DownloadIdCache;
|
||||
@@ -188,7 +188,7 @@ public class AmqpAuthenticationMessageHandler extends BaseAmqpService {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static DmfArtifact convertDbArtifact(final DbArtifact dbArtifact) {
|
||||
private static DmfArtifact convertDbArtifact(final AbstractDbArtifact dbArtifact) {
|
||||
final DmfArtifact artifact = new DmfArtifact();
|
||||
artifact.setSize(dbArtifact.getSize());
|
||||
final DbArtifactHash dbArtifactHash = dbArtifact.getHashes();
|
||||
|
||||
@@ -16,11 +16,13 @@ import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.eclipse.hawkbit.api.HostnameResolver;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.ArtifactFilesystem;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||
import org.eclipse.hawkbit.cache.DownloadIdCache;
|
||||
import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey;
|
||||
@@ -160,9 +162,8 @@ public class AmqpControllerAuthenticationTest {
|
||||
when(artifactManagementMock.findArtifact(ARTIFACT_ID)).thenReturn(Optional.of(testArtifact));
|
||||
when(artifactManagementMock.findFirstArtifactBySHA1(SHA1)).thenReturn(Optional.of(testArtifact));
|
||||
|
||||
final DbArtifact artifact = new DbArtifact();
|
||||
artifact.setSize(ARTIFACT_SIZE);
|
||||
artifact.setHashes(new DbArtifactHash(SHA1, "md5 test"));
|
||||
final AbstractDbArtifact artifact = new ArtifactFilesystem(new File("does not exist"), SHA1,
|
||||
new DbArtifactHash(SHA1, "md5 test"), ARTIFACT_SIZE, null);
|
||||
when(artifactManagementMock.loadArtifactBinary(SHA1)).thenReturn(Optional.of(artifact));
|
||||
|
||||
amqpMessageHandlerService = new AmqpMessageHandlerService(rabbitTemplate,
|
||||
|
||||
@@ -17,6 +17,7 @@ import static org.mockito.Matchers.anyObject;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -26,7 +27,9 @@ import java.util.UUID;
|
||||
|
||||
import org.eclipse.hawkbit.api.ArtifactUrl;
|
||||
import org.eclipse.hawkbit.api.ArtifactUrlHandler;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.ArtifactFilesystem;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||
import org.eclipse.hawkbit.dmf.amqp.api.EventTopic;
|
||||
import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey;
|
||||
import org.eclipse.hawkbit.dmf.amqp.api.MessageType;
|
||||
@@ -173,9 +176,10 @@ public class AmqpMessageDispatcherServiceTest extends AbstractIntegrationTest {
|
||||
public void testSendDownloadRequest() {
|
||||
DistributionSet dsA = testdataFactory.createDistributionSet(UUID.randomUUID().toString());
|
||||
SoftwareModule module = dsA.getModules().iterator().next();
|
||||
final List<DbArtifact> receivedList = new ArrayList<>();
|
||||
final List<AbstractDbArtifact> receivedList = new ArrayList<>();
|
||||
for (final Artifact artifact : testdataFactory.createArtifacts(module.getId())) {
|
||||
receivedList.add(new DbArtifact());
|
||||
receivedList.add(new ArtifactFilesystem(new File("./test"), artifact.getSha1Hash(),
|
||||
new DbArtifactHash(artifact.getSha1Hash(), null), artifact.getSize(), null));
|
||||
}
|
||||
module = softwareModuleManagement.findSoftwareModuleById(module.getId()).get();
|
||||
dsA = distributionSetManagement.findDistributionSetById(dsA.getId()).get();
|
||||
|
||||
@@ -28,7 +28,7 @@ import java.util.Optional;
|
||||
|
||||
import org.eclipse.hawkbit.api.HostnameResolver;
|
||||
import org.eclipse.hawkbit.artifact.repository.ArtifactRepository;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||
import org.eclipse.hawkbit.cache.DownloadIdCache;
|
||||
import org.eclipse.hawkbit.dmf.amqp.api.EventTopic;
|
||||
@@ -371,7 +371,7 @@ public class AmqpMessageHandlerServiceTest {
|
||||
final Artifact localArtifactMock = mock(Artifact.class);
|
||||
when(localArtifactMock.getSha1Hash()).thenReturn(SHA1);
|
||||
|
||||
final DbArtifact dbArtifactMock = mock(DbArtifact.class);
|
||||
final AbstractDbArtifact dbArtifactMock = mock(AbstractDbArtifact.class);
|
||||
when(artifactManagementMock.findFirstArtifactBySHA1(SHA1)).thenReturn(Optional.of(localArtifactMock));
|
||||
when(controllerManagementMock.hasTargetArtifactAssigned(securityToken.getControllerId(), SHA1))
|
||||
.thenReturn(true);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
*/
|
||||
package org.eclipse.hawkbit.mgmt.rest.api;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -37,7 +39,7 @@ public interface MgmtDownloadRestApi {
|
||||
*/
|
||||
@RequestMapping(method = RequestMethod.GET, value = MgmtRestConstants.DOWNLOAD_ID_V1_REQUEST_MAPPING)
|
||||
@ResponseBody
|
||||
ResponseEntity<Void> downloadArtifactByDownloadId(@PathVariable("tenant") String tenant,
|
||||
ResponseEntity<InputStream> downloadArtifactByDownloadId(@PathVariable("tenant") String tenant,
|
||||
@PathVariable("downloadId") String downloadId);
|
||||
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.io.InputStream;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtDownloadArtifactRestApi;
|
||||
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
||||
import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
|
||||
@@ -20,14 +20,17 @@ import org.eclipse.hawkbit.repository.exception.ArtifactBinaryNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
@@ -37,7 +40,6 @@ import org.springframework.web.context.WebApplicationContext;
|
||||
@RestController
|
||||
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
|
||||
public class MgmtDownloadArtifactResource implements MgmtDownloadArtifactRestApi {
|
||||
|
||||
@Autowired
|
||||
private SoftwareModuleManagement softwareModuleManagement;
|
||||
|
||||
@@ -58,25 +60,25 @@ public class MgmtDownloadArtifactResource implements MgmtDownloadArtifactRestApi
|
||||
* @return responseEntity with status ok if successful
|
||||
*/
|
||||
@Override
|
||||
@ResponseBody
|
||||
public ResponseEntity<InputStream> downloadArtifact(@PathVariable("softwareModuleId") final Long softwareModuleId,
|
||||
@PathVariable("artifactId") final Long artifactId) {
|
||||
|
||||
final SoftwareModule module = softwareModuleManagement.findSoftwareModuleById(softwareModuleId)
|
||||
.orElseThrow(() -> new EntityNotFoundException(SoftwareModule.class, softwareModuleId));
|
||||
final Artifact artifact = module.getArtifact(artifactId)
|
||||
.orElseThrow(() -> new EntityNotFoundException(Artifact.class, artifactId));
|
||||
|
||||
final DbArtifact file = artifactManagement.loadArtifactBinary(artifact.getSha1Hash())
|
||||
final AbstractDbArtifact file = artifactManagement.loadArtifactBinary(artifact.getSha1Hash())
|
||||
.orElseThrow(() -> new ArtifactBinaryNotFoundException(artifact.getSha1Hash()));
|
||||
final HttpServletRequest request = requestResponseContextHolder.getHttpServletRequest();
|
||||
final String ifMatch = request.getHeader("If-Match");
|
||||
if (ifMatch != null && !RestResourceConversionHelper.matchesHttpHeader(ifMatch, artifact.getSha1Hash())) {
|
||||
final String ifMatch = request.getHeader(HttpHeaders.IF_MATCH);
|
||||
if (ifMatch != null && !HttpUtil.matchesHttpHeader(ifMatch, artifact.getSha1Hash())) {
|
||||
return new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED);
|
||||
}
|
||||
|
||||
return RestResourceConversionHelper.writeFileResponse(artifact,
|
||||
requestResponseContextHolder.getHttpServletResponse(), request, file);
|
||||
|
||||
return FileStreamingUtil.writeFileResponse(file, artifact.getFilename(),
|
||||
artifact.getLastModifiedAt() != null ? artifact.getLastModifiedAt() : artifact.getCreatedAt(),
|
||||
requestResponseContextHolder.getHttpServletResponse(), request, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,33 +8,26 @@
|
||||
*/
|
||||
package org.eclipse.hawkbit.mgmt.rest.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.ArtifactRepository;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.cache.DownloadArtifactCache;
|
||||
import org.eclipse.hawkbit.cache.DownloadIdCache;
|
||||
import org.eclipse.hawkbit.cache.DownloadType;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtDownloadRestApi;
|
||||
import org.eclipse.hawkbit.rest.util.FileStreamingUtil;
|
||||
import org.eclipse.hawkbit.rest.util.RequestResponseContextHolder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
|
||||
/**
|
||||
* A resource for download artifacts.
|
||||
*/
|
||||
@@ -55,8 +48,9 @@ public class MgmtDownloadResource implements MgmtDownloadRestApi {
|
||||
|
||||
@Override
|
||||
@ResponseBody
|
||||
public ResponseEntity<Void> downloadArtifactByDownloadId(@PathVariable("tenant") final String tenant,
|
||||
public ResponseEntity<InputStream> downloadArtifactByDownloadId(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("downloadId") final String downloadId) {
|
||||
|
||||
try {
|
||||
final DownloadArtifactCache artifactCache = downloadIdCache.get(downloadId);
|
||||
if (artifactCache == null) {
|
||||
@@ -64,7 +58,7 @@ public class MgmtDownloadResource implements MgmtDownloadRestApi {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
DbArtifact artifact = null;
|
||||
AbstractDbArtifact artifact = null;
|
||||
|
||||
if (DownloadType.BY_SHA1.equals(artifactCache.getDownloadType())) {
|
||||
artifact = artifactRepository.getArtifactBySha1(tenant, artifactCache.getId());
|
||||
@@ -78,24 +72,12 @@ public class MgmtDownloadResource implements MgmtDownloadRestApi {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
final HttpServletResponse response = requestResponseContextHolder.getHttpServletResponse();
|
||||
final String etag = artifact.getHashes().getSha1();
|
||||
final long length = artifact.getSize();
|
||||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + downloadId);
|
||||
response.setHeader(HttpHeaders.ETAG, etag);
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
response.setContentLengthLong(length);
|
||||
return FileStreamingUtil.writeFileResponse(artifact, downloadId, null,
|
||||
requestResponseContextHolder.getHttpServletResponse(),
|
||||
requestResponseContextHolder.getHttpServletRequest(), null);
|
||||
|
||||
try (InputStream inputstream = artifact.getFileInputStream()) {
|
||||
ByteStreams.copy(inputstream, requestResponseContextHolder.getHttpServletResponse().getOutputStream());
|
||||
} catch (final IOException e) {
|
||||
LOGGER.error("Cannot copy streams", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
} finally {
|
||||
downloadIdCache.evict(downloadId);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ package org.eclipse.hawkbit.mgmt.rest.resource;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.validation.ValidationException;
|
||||
|
||||
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutResponseBody;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBody;
|
||||
@@ -26,7 +28,6 @@ import org.eclipse.hawkbit.repository.RolloutManagement;
|
||||
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
|
||||
import org.eclipse.hawkbit.repository.builder.RolloutCreate;
|
||||
import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate;
|
||||
import org.eclipse.hawkbit.repository.exception.ConstraintViolationException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Rollout;
|
||||
@@ -122,7 +123,7 @@ public class MgmtRolloutResource implements MgmtRolloutRestApi {
|
||||
rolloutGroupConditions);
|
||||
|
||||
} else {
|
||||
throw new ConstraintViolationException("Either 'amountGroups' or 'groups' must be defined in the request");
|
||||
throw new ValidationException("Either 'amountGroups' or 'groups' must be defined in the request");
|
||||
}
|
||||
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(MgmtRolloutMapper.toResponseRollout(rollout, true));
|
||||
|
||||
@@ -13,6 +13,8 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.validation.ValidationException;
|
||||
|
||||
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionRequestBodyPut;
|
||||
@@ -29,7 +31,6 @@ import org.eclipse.hawkbit.repository.DeploymentManagement;
|
||||
import org.eclipse.hawkbit.repository.EntityFactory;
|
||||
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
|
||||
import org.eclipse.hawkbit.repository.TargetManagement;
|
||||
import org.eclipse.hawkbit.repository.exception.ConstraintViolationException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.Action.ActionType;
|
||||
@@ -293,7 +294,7 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
|
||||
}
|
||||
|
||||
if (!MgmtActionType.FORCED.equals(actionUpdate.getForceType())) {
|
||||
throw new ConstraintViolationException("Resource supports only switch to FORCED.");
|
||||
throw new ValidationException("Resource supports only switch to FORCED.");
|
||||
}
|
||||
|
||||
action = deploymentManagement.forceTargetAction(actionId);
|
||||
|
||||
@@ -32,12 +32,13 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.validation.ConstraintViolationException;
|
||||
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
import org.eclipse.hawkbit.im.authentication.SpPermission;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
|
||||
import org.eclipse.hawkbit.repository.ActionFields;
|
||||
import org.eclipse.hawkbit.repository.exception.ConstraintViolationException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.Action.ActionType;
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.util.Optional;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
|
||||
import org.eclipse.hawkbit.repository.exception.ArtifactDeleteFailedException;
|
||||
import org.eclipse.hawkbit.repository.exception.ArtifactUploadFailedException;
|
||||
@@ -196,15 +196,15 @@ public interface ArtifactManagement {
|
||||
Page<Artifact> findArtifactBySoftwareModule(@NotNull Pageable pageReq, @NotNull Long swId);
|
||||
|
||||
/**
|
||||
* Loads {@link DbArtifact} from store for given {@link Artifact}.
|
||||
* Loads {@link AbstractDbArtifact} from store for given {@link Artifact}.
|
||||
*
|
||||
* @param sha1Hash
|
||||
* to search for
|
||||
* @return loaded {@link DbArtifact}
|
||||
* @return loaded {@link AbstractDbArtifact}
|
||||
*
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_DOWNLOAD_ARTIFACT + SpringEvalExpressions.HAS_AUTH_OR
|
||||
+ SpringEvalExpressions.HAS_CONTROLLER_DOWNLOAD)
|
||||
Optional<DbArtifact> loadArtifactBinary(@NotEmpty String sha1Hash);
|
||||
Optional<AbstractDbArtifact> loadArtifactBinary(@NotEmpty String sha1Hash);
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import javax.validation.constraints.NotNull;
|
||||
|
||||
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
|
||||
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
|
||||
import org.eclipse.hawkbit.repository.event.remote.DownloadProgressEvent;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.exception.QuotaExceededException;
|
||||
@@ -61,22 +60,6 @@ public interface ControllerManagement {
|
||||
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
|
||||
Action addCancelActionStatus(@NotNull ActionStatusCreate create);
|
||||
|
||||
/**
|
||||
* Sends the download progress and notifies the event publisher with a
|
||||
* {@link DownloadProgressEvent}.
|
||||
*
|
||||
* @param statusId
|
||||
* the ID of the {@link ActionStatus}
|
||||
* @param requestedBytes
|
||||
* requested bytes of the request
|
||||
* @param shippedBytesSinceLast
|
||||
* since the last report
|
||||
* @param shippedBytesOverall
|
||||
* for the {@link ActionStatus}
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
|
||||
void downloadProgress(Long statusId, Long requestedBytes, Long shippedBytesSinceLast, Long shippedBytesOverall);
|
||||
|
||||
/**
|
||||
* Simple addition of a new {@link ActionStatus} entry to the {@link Action}
|
||||
* . No state changes.
|
||||
|
||||
@@ -11,13 +11,13 @@ package org.eclipse.hawkbit.repository;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.validation.ConstraintViolationException;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
|
||||
import org.eclipse.hawkbit.repository.builder.RolloutCreate;
|
||||
import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate;
|
||||
import org.eclipse.hawkbit.repository.builder.RolloutUpdate;
|
||||
import org.eclipse.hawkbit.repository.exception.ConstraintViolationException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException;
|
||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.event.remote;
|
||||
|
||||
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||
|
||||
/**
|
||||
* TenantAwareEvent that contains an updated download progress for a given
|
||||
* ActionStatus that was written for a download request.
|
||||
@@ -17,7 +19,7 @@ public class DownloadProgressEvent extends RemoteTenantAwareEvent {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long shippedBytesSinceLast;
|
||||
private long shippedBytesSinceLast;
|
||||
|
||||
/**
|
||||
* Default constructor.
|
||||
@@ -31,13 +33,16 @@ public class DownloadProgressEvent extends RemoteTenantAwareEvent {
|
||||
*
|
||||
* @param tenant
|
||||
* the tenant
|
||||
* @param actionStatusId
|
||||
* of the {@link ActionStatus} the download belongs to
|
||||
* @param shippedBytesSinceLast
|
||||
* the shippedBytesSinceLast
|
||||
* @param applicationId
|
||||
* the application id.
|
||||
*/
|
||||
public DownloadProgressEvent(final String tenant, final Long shippedBytesSinceLast, final String applicationId) {
|
||||
super(shippedBytesSinceLast, tenant, applicationId);
|
||||
public DownloadProgressEvent(final String tenant, final Long actionStatusId, final long shippedBytesSinceLast,
|
||||
final String applicationId) {
|
||||
super(actionStatusId, tenant, applicationId);
|
||||
this.shippedBytesSinceLast = shippedBytesSinceLast;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.exception;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
|
||||
/**
|
||||
* the {@link ConstraintViolationException} is thrown when an entity is tried to
|
||||
* be saved which has constraint violations
|
||||
*/
|
||||
public class ConstraintViolationException extends AbstractServerRtException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final String MESSAGE_FORMATTER_SEPARATOR = " ";
|
||||
|
||||
/**
|
||||
* Constructor for {@link ConstraintViolationException}
|
||||
*
|
||||
* @param ex
|
||||
* the javax.validation.ConstraintViolationException which is
|
||||
* thrown
|
||||
*/
|
||||
public ConstraintViolationException(final javax.validation.ConstraintViolationException ex) {
|
||||
this(getExceptionMessage(ex));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ConstraintViolationException} with the error code
|
||||
* {@link SpServerError#SP_REPO_CONSTRAINT_VIOLATION}.
|
||||
*
|
||||
* @param msgText
|
||||
* the message text for this exception
|
||||
*/
|
||||
public ConstraintViolationException(final String msgText) {
|
||||
super(msgText, SpServerError.SP_REPO_CONSTRAINT_VIOLATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the information of
|
||||
* {@link javax.validation.ConstraintViolationException} to provide a proper
|
||||
* error message for {@link ConstraintViolationException}
|
||||
*
|
||||
* @param ex
|
||||
* javax.validation.ConstraintViolationException which is thrown
|
||||
* @return message String with proper error information
|
||||
*/
|
||||
public static String getExceptionMessage(final javax.validation.ConstraintViolationException ex) {
|
||||
return ex
|
||||
.getConstraintViolations().stream().map(violation -> violation.getPropertyPath()
|
||||
+ MESSAGE_FORMATTER_SEPARATOR + violation.getMessage() + ".")
|
||||
.collect(Collectors.joining(MESSAGE_FORMATTER_SEPARATOR));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.model;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
|
||||
/**
|
||||
* Binaries for a {@link SoftwareModule} Note: the decision which artifacts have
|
||||
@@ -34,7 +34,7 @@ public interface Artifact extends TenantAwareBaseEntity {
|
||||
|
||||
/**
|
||||
* @return SHA-1 hash of the artifact in Base16 format that identifies the
|
||||
* {@link DbArtifact} in the system.
|
||||
* {@link AbstractDbArtifact} in the system.
|
||||
*/
|
||||
String getSha1Hash();
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.hawkbit.repository.exception.ConstraintViolationException;
|
||||
import javax.validation.ValidationException;
|
||||
|
||||
import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException;
|
||||
import org.eclipse.hawkbit.repository.model.Rollout;
|
||||
import org.eclipse.hawkbit.repository.model.RolloutGroup;
|
||||
@@ -35,10 +36,10 @@ public final class RolloutHelper {
|
||||
*/
|
||||
public static void verifyRolloutGroupConditions(final RolloutGroupConditions conditions) {
|
||||
if (conditions.getSuccessCondition() == null) {
|
||||
throw new ConstraintViolationException("Rollout group is missing success condition");
|
||||
throw new ValidationException("Rollout group is missing success condition");
|
||||
}
|
||||
if (conditions.getSuccessAction() == null) {
|
||||
throw new ConstraintViolationException("Rollout group is missing success action");
|
||||
throw new ValidationException("Rollout group is missing success action");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,14 +53,14 @@ public final class RolloutHelper {
|
||||
*/
|
||||
public static RolloutGroup verifyRolloutGroupHasConditions(final RolloutGroup group) {
|
||||
if (group.getTargetPercentage() < 1F || group.getTargetPercentage() > 100F) {
|
||||
throw new ConstraintViolationException("Target percentage has to be between 1 and 100");
|
||||
throw new ValidationException("Target percentage has to be between 1 and 100");
|
||||
}
|
||||
|
||||
if (group.getSuccessCondition() == null) {
|
||||
throw new ConstraintViolationException("Rollout group is missing success condition");
|
||||
throw new ValidationException("Rollout group is missing success condition");
|
||||
}
|
||||
if (group.getSuccessAction() == null) {
|
||||
throw new ConstraintViolationException("Rollout group is missing success action");
|
||||
throw new ValidationException("Rollout group is missing success action");
|
||||
}
|
||||
return group;
|
||||
}
|
||||
@@ -74,9 +75,9 @@ public final class RolloutHelper {
|
||||
*/
|
||||
public static void verifyRolloutGroupParameter(final int amountGroup, final QuotaManagement quotaManagement) {
|
||||
if (amountGroup <= 0) {
|
||||
throw new ConstraintViolationException("the amount of groups cannot be lower than zero");
|
||||
throw new ValidationException("the amount of groups cannot be lower than zero");
|
||||
} else if (amountGroup > quotaManagement.getMaxRolloutGroupsPerRollout()) {
|
||||
throw new ConstraintViolationException("the amount of groups cannot be greater than 500");
|
||||
throw new ValidationException("the amount of groups cannot be greater than 500");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,9 +89,9 @@ public final class RolloutHelper {
|
||||
*/
|
||||
public static void verifyRolloutGroupTargetPercentage(final float percentage) {
|
||||
if (percentage <= 0) {
|
||||
throw new ConstraintViolationException("the percentage must be greater than zero");
|
||||
throw new ValidationException("the percentage must be greater than zero");
|
||||
} else if (percentage > 100) {
|
||||
throw new ConstraintViolationException("the percentage must not be greater than 100");
|
||||
throw new ValidationException("the percentage must not be greater than 100");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,11 +241,10 @@ public final class RolloutHelper {
|
||||
*/
|
||||
public static void verifyRemainingTargets(final long targetCount) {
|
||||
if (targetCount > 0) {
|
||||
throw new ConstraintViolationException(
|
||||
"Rollout groups don't match all targets that are targeted by the rollout");
|
||||
throw new ValidationException("Rollout groups don't match all targets that are targeted by the rollout");
|
||||
}
|
||||
if (targetCount != 0) {
|
||||
throw new ConstraintViolationException("Rollout groups target count verification failed");
|
||||
throw new ValidationException("Rollout groups target count verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import java.util.Optional;
|
||||
import org.eclipse.hawkbit.artifact.repository.ArtifactRepository;
|
||||
import org.eclipse.hawkbit.artifact.repository.ArtifactStoreException;
|
||||
import org.eclipse.hawkbit.artifact.repository.HashNotMatchException;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash;
|
||||
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
||||
import org.eclipse.hawkbit.repository.exception.ArtifactDeleteFailedException;
|
||||
@@ -88,7 +88,7 @@ public class JpaArtifactManagement implements ArtifactManagement {
|
||||
public Artifact createArtifact(final InputStream stream, final Long moduleId, final String filename,
|
||||
final String providedMd5Sum, final String providedSha1Sum, final boolean overrideExisting,
|
||||
final String contentType) {
|
||||
DbArtifact result = null;
|
||||
AbstractDbArtifact result = null;
|
||||
|
||||
final SoftwareModule softwareModule = getModuleAndThrowExceptionIfThatFails(moduleId);
|
||||
|
||||
@@ -184,12 +184,12 @@ public class JpaArtifactManagement implements ArtifactManagement {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<DbArtifact> loadArtifactBinary(final String sha1Hash) {
|
||||
public Optional<AbstractDbArtifact> loadArtifactBinary(final String sha1Hash) {
|
||||
return Optional.ofNullable(artifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), sha1Hash));
|
||||
}
|
||||
|
||||
private Artifact storeArtifactMetadata(final SoftwareModule softwareModule, final String providedFilename,
|
||||
final DbArtifact result, final Artifact existing) {
|
||||
final AbstractDbArtifact result, final Artifact existing) {
|
||||
JpaArtifact artifact = (JpaArtifact) existing;
|
||||
if (existing == null) {
|
||||
artifact = new JpaArtifact(result.getHashes().getSha1(), providedFilename, softwareModule);
|
||||
|
||||
@@ -26,7 +26,6 @@ import org.eclipse.hawkbit.repository.RepositoryConstants;
|
||||
import org.eclipse.hawkbit.repository.RepositoryProperties;
|
||||
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
|
||||
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
|
||||
import org.eclipse.hawkbit.repository.event.remote.DownloadProgressEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent;
|
||||
import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
@@ -483,13 +482,6 @@ public class JpaControllerManagement implements ControllerManagement {
|
||||
.orElseThrow(() -> new EntityNotFoundException(Action.class, actionId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downloadProgress(final Long statusId, final Long requestedBytes, final Long shippedBytesSinceLast,
|
||||
final Long shippedBytesOverall) {
|
||||
eventPublisher.publishEvent(new DownloadProgressEvent(tenantAware.getCurrentTenant(), shippedBytesSinceLast,
|
||||
applicationContext.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Target> findByControllerId(final String controllerId) {
|
||||
return targetRepository.findByControllerId(controllerId);
|
||||
|
||||
@@ -19,6 +19,7 @@ import java.util.stream.StreamSupport;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.validation.ConstraintDeclarationException;
|
||||
import javax.validation.ValidationException;
|
||||
|
||||
import org.eclipse.hawkbit.repository.AbstractRolloutManagement;
|
||||
import org.eclipse.hawkbit.repository.DeploymentManagement;
|
||||
@@ -37,7 +38,6 @@ import org.eclipse.hawkbit.repository.builder.RolloutUpdate;
|
||||
import org.eclipse.hawkbit.repository.event.remote.RolloutGroupDeletedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.RolloutGroupCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.RolloutUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.exception.ConstraintViolationException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException;
|
||||
import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException;
|
||||
@@ -216,7 +216,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
|
||||
|
||||
final Long totalTargets = targetManagement.countTargetByTargetFilterQuery(rollout.getTargetFilterQuery());
|
||||
if (totalTargets == 0) {
|
||||
throw new ConstraintViolationException("Rollout does not match any existing targets");
|
||||
throw new ValidationException("Rollout does not match any existing targets");
|
||||
}
|
||||
rollout.setTotalTargets(totalTargets);
|
||||
return rolloutRepository.save(rollout);
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.jpa.builder;
|
||||
|
||||
import javax.validation.ValidationException;
|
||||
|
||||
import org.eclipse.hawkbit.repository.SoftwareModuleTypeManagement;
|
||||
import org.eclipse.hawkbit.repository.builder.AbstractSoftwareModuleUpdateCreate;
|
||||
import org.eclipse.hawkbit.repository.builder.SoftwareModuleCreate;
|
||||
import org.eclipse.hawkbit.repository.exception.ConstraintViolationException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
|
||||
@@ -36,7 +37,7 @@ public class JpaSoftwareModuleCreate extends AbstractSoftwareModuleUpdateCreate<
|
||||
|
||||
private SoftwareModuleType getSoftwareModuleTypeFromKeyString(final String type) {
|
||||
if (type == null) {
|
||||
throw new ConstraintViolationException("type cannot be null");
|
||||
throw new ValidationException("type cannot be null");
|
||||
}
|
||||
|
||||
return softwareModuleTypeManagement.findSoftwareModuleTypeByKey(type.trim())
|
||||
|
||||
@@ -21,7 +21,7 @@ import javax.persistence.Table;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
@@ -71,9 +71,9 @@ public class JpaArtifact extends AbstractJpaTenantAwareBaseEntity implements Art
|
||||
* Constructs artifact.
|
||||
*
|
||||
* @param sha1Hash
|
||||
* that is the link to the {@link DbArtifact} entity.
|
||||
* that is the link to the {@link AbstractDbArtifact} entity.
|
||||
* @param filename
|
||||
* that is used by {@link DbArtifact} store.
|
||||
* that is used by {@link AbstractDbArtifact} store.
|
||||
* @param softwareModule
|
||||
* of this artifact
|
||||
*/
|
||||
|
||||
@@ -29,7 +29,7 @@ public class RemoteTenantAwareEventTest extends AbstractRemoteEventTest {
|
||||
@Test
|
||||
@Description("Verifies that the download progress reloading by remote events works")
|
||||
public void reloadDownloadProgessByRemoteEvent() {
|
||||
final DownloadProgressEvent downloadProgressEvent = new DownloadProgressEvent("DEFAULT", 3L, "Node");
|
||||
final DownloadProgressEvent downloadProgressEvent = new DownloadProgressEvent("DEFAULT", 1L, 3L, "Node");
|
||||
|
||||
DownloadProgressEvent remoteEvent = (DownloadProgressEvent) createProtoStuffEvent(downloadProgressEvent);
|
||||
assertThat(downloadProgressEvent).isEqualTo(remoteEvent);
|
||||
|
||||
@@ -26,7 +26,6 @@ public class ActionTest {
|
||||
@Description("Ensures that timeforced moded switch from soft to forces after defined timeframe.")
|
||||
public void timeforcedHitNewHasCodeIsGenerated() throws InterruptedException {
|
||||
|
||||
final boolean active;
|
||||
// current time + 1 seconds
|
||||
final long sleepTime = 1000;
|
||||
final long timeForceTimeAt = System.currentTimeMillis() + sleepTime;
|
||||
|
||||
@@ -104,10 +104,8 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
final Artifact result = artifactManagement.createArtifact(new ByteArrayInputStream(random), sm.getId(), "file1",
|
||||
false);
|
||||
final Artifact result11 = artifactManagement.createArtifact(new ByteArrayInputStream(random), sm.getId(),
|
||||
"file11", false);
|
||||
final Artifact result12 = artifactManagement.createArtifact(new ByteArrayInputStream(random), sm.getId(),
|
||||
"file12", false);
|
||||
artifactManagement.createArtifact(new ByteArrayInputStream(random), sm.getId(), "file11", false);
|
||||
artifactManagement.createArtifact(new ByteArrayInputStream(random), sm.getId(), "file12", false);
|
||||
final Artifact result2 = artifactManagement.createArtifact(new ByteArrayInputStream(random), sm2.getId(),
|
||||
"file2", false);
|
||||
|
||||
@@ -270,8 +268,7 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
assertThat(artifactManagement.findArtifactBySoftwareModule(PAGE, sm.getId())).isEmpty();
|
||||
|
||||
final Artifact result = artifactManagement.createArtifact(new RandomGeneratedInputStream(5 * 1024), sm.getId(),
|
||||
"file1", false);
|
||||
artifactManagement.createArtifact(new RandomGeneratedInputStream(5 * 1024), sm.getId(), "file1", false);
|
||||
|
||||
assertThat(artifactManagement.findArtifactBySoftwareModule(PAGE, sm.getId())).hasSize(1);
|
||||
}
|
||||
|
||||
@@ -747,7 +747,7 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Deletes multiple targets and verfies that all related metadat is also deleted.")
|
||||
@Description("Deletes multiple targets and verfies that all related metadata is also deleted.")
|
||||
public void deletesTargetsAndVerifyCascadeDeletes() {
|
||||
|
||||
final String undeployedTargetPrefix = "undep-T";
|
||||
|
||||
@@ -21,6 +21,8 @@ import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.validation.ValidationException;
|
||||
|
||||
import org.assertj.core.api.Condition;
|
||||
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
|
||||
import org.eclipse.hawkbit.repository.builder.RolloutCreate;
|
||||
@@ -37,7 +39,6 @@ import org.eclipse.hawkbit.repository.event.remote.entity.RolloutUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.exception.ConstraintViolationException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException;
|
||||
import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException;
|
||||
@@ -139,7 +140,7 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
||||
@Expect(type = RolloutUpdatedEvent.class, count = 1),
|
||||
@Expect(type = TargetCreatedEvent.class, count = 10) })
|
||||
public void entityQueriesReferringToNotExistingEntitiesThrowsException() {
|
||||
final Rollout createdRollout = testdataFactory.createRollout("xxx");
|
||||
testdataFactory.createRollout("xxx");
|
||||
|
||||
verifyThrownExceptionBy(() -> rolloutManagement.deleteRollout(NOT_EXIST_IDL), "Rollout");
|
||||
|
||||
@@ -1089,7 +1090,7 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsFor" + rolloutName);
|
||||
|
||||
assertThatExceptionOfType(ConstraintViolationException.class)
|
||||
assertThatExceptionOfType(ValidationException.class)
|
||||
.isThrownBy(() -> testdataFactory.createRolloutByVariables(rolloutName, "desc", amountGroups,
|
||||
"id==notExisting", distributionSet, successCondition, errorCondition))
|
||||
.withMessageContaining("does not match any existing");
|
||||
@@ -1336,7 +1337,7 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
||||
rolloutGroups.add(generateRolloutGroup(0, percentTargetsInGroup1, null));
|
||||
rolloutGroups.add(generateRolloutGroup(1, percentTargetsInGroup2, null));
|
||||
|
||||
assertThatExceptionOfType(ConstraintViolationException.class)
|
||||
assertThatExceptionOfType(ValidationException.class)
|
||||
.isThrownBy(() -> rolloutManagement.createRollout(myRollout, rolloutGroups, conditions))
|
||||
.withMessageContaining("groups don't match");
|
||||
|
||||
@@ -1357,7 +1358,7 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
||||
generateRolloutGroup(0, percentTargetsInGroup1, null),
|
||||
generateRolloutGroup(1, percentTargetsInGroup2, null));
|
||||
|
||||
assertThatExceptionOfType(ConstraintViolationException.class)
|
||||
assertThatExceptionOfType(ValidationException.class)
|
||||
.isThrownBy(() -> rolloutManagement.createRollout(myRollout, rolloutGroups, conditions))
|
||||
.withMessageContaining("percentage has to be between 1 and 100");
|
||||
|
||||
@@ -1373,7 +1374,7 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest {
|
||||
final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults().build();
|
||||
final RolloutCreate myRollout = generateTargetsAndRollout(rolloutName, amountTargetsForRollout);
|
||||
|
||||
assertThatExceptionOfType(ConstraintViolationException.class)
|
||||
assertThatExceptionOfType(ValidationException.class)
|
||||
.isThrownBy(() -> rolloutManagement.createRollout(myRollout, illegalGroupAmount, conditions))
|
||||
.withMessageContaining("not be greater than " + quotaManagement.getMaxRolloutGroupsPerRollout());
|
||||
|
||||
|
||||
@@ -252,8 +252,8 @@ public class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest {
|
||||
@Description("Counts all software modules in the repsitory that are not marked as deleted.")
|
||||
public void countSoftwareModulesAll() {
|
||||
// found in test
|
||||
final SoftwareModule one = testdataFactory.createSoftwareModuleOs("one");
|
||||
final SoftwareModule two = testdataFactory.createSoftwareModuleOs("two");
|
||||
testdataFactory.createSoftwareModuleOs("one");
|
||||
testdataFactory.createSoftwareModuleOs("two");
|
||||
final SoftwareModule deleted = testdataFactory.createSoftwareModuleOs("deleted");
|
||||
// ignored
|
||||
softwareModuleManagement.deleteSoftwareModule(deleted.getId());
|
||||
|
||||
@@ -484,8 +484,6 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest {
|
||||
*/
|
||||
private void checkTargetHasTags(final boolean strict, final Iterable<Target> targets, final TargetTag... tags) {
|
||||
_target: for (final Target tl : targets) {
|
||||
final Target t = targetManagement.findTargetByControllerID(tl.getControllerId()).get();
|
||||
|
||||
for (final Tag tt : tagManagement.findAllTargetTags(PAGE, tl.getControllerId())) {
|
||||
for (final Tag tag : tags) {
|
||||
if (tag.getName().equals(tt.getName())) {
|
||||
@@ -502,7 +500,7 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
private void checkTargetHasNotTags(final Iterable<Target> targets, final TargetTag... tags) {
|
||||
for (final Target tl : targets) {
|
||||
final Target t = targetManagement.findTargetByControllerID(tl.getControllerId()).get();
|
||||
targetManagement.findTargetByControllerID(tl.getControllerId()).get();
|
||||
|
||||
for (final Tag tag : tags) {
|
||||
for (final Tag tt : tagManagement.findAllTargetTags(PAGE, tl.getControllerId())) {
|
||||
|
||||
@@ -315,8 +315,8 @@ public class RSQLUtilityTest {
|
||||
.thenReturn(mock(Predicate.class));
|
||||
|
||||
// test
|
||||
final Predicate result = RSQLUtility.parse(correctRsql, TestFieldEnum.class, setupMacroLookup())
|
||||
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
||||
RSQLUtility.parse(correctRsql, TestFieldEnum.class, setupMacroLookup()).toPredicate(baseSoftwareModuleRootMock,
|
||||
criteriaQueryMock, criteriaBuilderMock);
|
||||
|
||||
// verification
|
||||
verify(macroResolver).lookup(overdueProp);
|
||||
|
||||
@@ -14,8 +14,6 @@ import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.powermock.api.mockito.PowerMockito.mockStatic;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import org.apache.commons.lang3.text.StrSubstitutor;
|
||||
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
|
||||
import org.eclipse.hawkbit.repository.TimestampCalculator;
|
||||
@@ -48,8 +46,6 @@ public class VirtualPropertyResolverTest {
|
||||
|
||||
private StrSubstitutor substitutor;
|
||||
|
||||
private Long nowTestTime;
|
||||
|
||||
private static final TenantConfigurationValue<String> TEST_POLLING_TIME_INTERVAL = TenantConfigurationValue
|
||||
.<String> builder().value("00:05:00").build();
|
||||
private static final TenantConfigurationValue<String> TEST_POLLING_OVERDUE_TIME_INTERVAL = TenantConfigurationValue
|
||||
@@ -57,7 +53,6 @@ public class VirtualPropertyResolverTest {
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
nowTestTime = Instant.now().toEpochMilli();
|
||||
when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_TIME_INTERVAL, String.class))
|
||||
.thenReturn(TEST_POLLING_TIME_INTERVAL);
|
||||
when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME_INTERVAL, String.class))
|
||||
|
||||
@@ -21,22 +21,34 @@
|
||||
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-jpa</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.hateoas</groupId>
|
||||
<artifactId>spring-hateoas</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-commons</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
@@ -58,6 +70,12 @@
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-jpa</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ru.yandex.qatools.allure</groupId>
|
||||
<artifactId>allure-junit-adaptor</artifactId>
|
||||
|
||||
@@ -12,9 +12,6 @@ import org.eclipse.hawkbit.rest.exception.SortParameterUnsupportedDirectionExcep
|
||||
|
||||
/**
|
||||
* A definition of possible sorting direction.
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
public enum SortDirection {
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.exception;
|
||||
package org.eclipse.hawkbit.rest.exception;
|
||||
|
||||
import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
@@ -11,14 +11,15 @@ package org.eclipse.hawkbit.rest.exception;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.ConstraintViolationException;
|
||||
import javax.validation.ValidationException;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
import org.eclipse.hawkbit.repository.exception.MultiPartFileUploadException;
|
||||
import org.eclipse.hawkbit.rest.json.model.ExceptionInfo;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -41,6 +42,8 @@ public class ResponseExceptionHandler {
|
||||
private static final Map<SpServerError, HttpStatus> ERROR_TO_HTTP_STATUS = new EnumMap<>(SpServerError.class);
|
||||
private static final HttpStatus DEFAULT_RESPONSE_STATUS = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
private static final String MESSAGE_FORMATTER_SEPARATOR = " ";
|
||||
|
||||
static {
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REPO_ENTITY_NOT_EXISTS, HttpStatus.NOT_FOUND);
|
||||
ERROR_TO_HTTP_STATUS.put(SpServerError.SP_REPO_ENTITY_ALRREADY_EXISTS, HttpStatus.CONFLICT);
|
||||
@@ -138,12 +141,40 @@ public class ResponseExceptionHandler {
|
||||
*/
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public ResponseEntity<ExceptionInfo> handleConstraintViolationException(final HttpServletRequest request,
|
||||
final Exception ex) {
|
||||
final ConstraintViolationException ex) {
|
||||
logRequest(request, ex);
|
||||
|
||||
final ExceptionInfo response = createExceptionInfo(
|
||||
new org.eclipse.hawkbit.repository.exception.ConstraintViolationException(
|
||||
(ConstraintViolationException) ex));
|
||||
final ExceptionInfo response = new ExceptionInfo();
|
||||
response.setMessage(ex.getConstraintViolations().stream().map(
|
||||
violation -> violation.getPropertyPath() + MESSAGE_FORMATTER_SEPARATOR + violation.getMessage() + ".")
|
||||
.collect(Collectors.joining(MESSAGE_FORMATTER_SEPARATOR)));
|
||||
response.setExceptionClass(ex.getClass().getName());
|
||||
response.setErrorCode(SpServerError.SP_REPO_CONSTRAINT_VIOLATION.getKey());
|
||||
|
||||
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for handling exception of type ValidationException which is thrown
|
||||
* in case the request is rejected due to invalid requests. Called by the
|
||||
* Spring-Framework for exception handling.
|
||||
*
|
||||
* @param request
|
||||
* the Http request
|
||||
* @param ex
|
||||
* the exception which occurred
|
||||
* @return the entity to be responded containing the exception information
|
||||
* as entity.
|
||||
*/
|
||||
@ExceptionHandler(ValidationException.class)
|
||||
public ResponseEntity<ExceptionInfo> handleValidationException(final HttpServletRequest request,
|
||||
final ValidationException ex) {
|
||||
logRequest(request, ex);
|
||||
|
||||
final ExceptionInfo response = new ExceptionInfo();
|
||||
response.setMessage(ex.getMessage());
|
||||
response.setExceptionClass(ex.getClass().getName());
|
||||
response.setErrorCode(SpServerError.SP_REPO_CONSTRAINT_VIOLATION.getKey());
|
||||
|
||||
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
/**
|
||||
* Byte range for resume download operations.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
public class ByteRange {
|
||||
public static final String MULTIPART_BOUNDARY = "THIS_STRING_SEPARATES_MULTIPART";
|
||||
|
||||
private final long start;
|
||||
private final long end;
|
||||
private final long length;
|
||||
private final long total;
|
||||
|
||||
/**
|
||||
* Construct a byte range.
|
||||
*
|
||||
* @param start
|
||||
* Start of the byte range.
|
||||
* @param end
|
||||
* End of the byte range.
|
||||
* @param total
|
||||
* Total length of the byte source.
|
||||
*/
|
||||
public ByteRange(final long start, final long end, final long total) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
length = end - start + 1;
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the start
|
||||
*/
|
||||
public long getStart() {
|
||||
return start;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the end
|
||||
*/
|
||||
public long getEnd() {
|
||||
return end;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the length
|
||||
*/
|
||||
public long getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the total
|
||||
*/
|
||||
public long getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
@Override
|
||||
// NOSONAR - as this is generated
|
||||
@SuppressWarnings("squid:S864")
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + (int) (end ^ end >>> 32);
|
||||
result = prime * result + (int) (length ^ length >>> 32);
|
||||
result = prime * result + (int) (start ^ start >>> 32);
|
||||
result = prime * result + (int) (total ^ total >>> 32);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) { // NOSONAR - as this is generated
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
final ByteRange other = (ByteRange) obj;
|
||||
if (end != other.end) {
|
||||
return false;
|
||||
}
|
||||
if (length != other.length) {
|
||||
return false;
|
||||
}
|
||||
if (start != other.start) {
|
||||
return false;
|
||||
}
|
||||
if (total != other.total) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import org.eclipse.hawkbit.exception.SpServerError;
|
||||
/**
|
||||
* Thrown if artifact content streaming to client failed.
|
||||
*/
|
||||
public final class FileSteamingFailedException extends AbstractServerRtException {
|
||||
public final class FileStreamingFailedException extends AbstractServerRtException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@@ -22,7 +22,7 @@ public final class FileSteamingFailedException extends AbstractServerRtException
|
||||
* Creates a new FileUploadFailedException with
|
||||
* {@link SpServerError#SP_REST_BODY_NOT_READABLE} error.
|
||||
*/
|
||||
public FileSteamingFailedException() {
|
||||
public FileStreamingFailedException() {
|
||||
super(SpServerError.SP_ARTIFACT_LOAD_FAILED);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public final class FileSteamingFailedException extends AbstractServerRtException
|
||||
* @param cause
|
||||
* for the exception
|
||||
*/
|
||||
public FileSteamingFailedException(final Throwable cause) {
|
||||
public FileStreamingFailedException(final Throwable cause) {
|
||||
super(SpServerError.SP_ARTIFACT_LOAD_FAILED, cause);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,19 @@ public final class FileSteamingFailedException extends AbstractServerRtException
|
||||
* @param message
|
||||
* of the error
|
||||
*/
|
||||
public FileSteamingFailedException(final String message) {
|
||||
public FileStreamingFailedException(final String message) {
|
||||
super(message, SpServerError.SP_ARTIFACT_LOAD_FAILED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with error string and cause.
|
||||
*
|
||||
* @param message
|
||||
* of the error
|
||||
* @param cause
|
||||
* for the exception
|
||||
*/
|
||||
public FileStreamingFailedException(final String message, final Throwable cause) {
|
||||
super(message, SpServerError.SP_ARTIFACT_LOAD_FAILED, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
/**
|
||||
* Listener for progress on artifact file streaming.
|
||||
*
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface FileStreamingProgressListener {
|
||||
|
||||
/**
|
||||
* Called multiple times during streaming.
|
||||
*
|
||||
* @param requestedBytes
|
||||
* requested bytes of the request
|
||||
* @param shippedBytesSinceLast
|
||||
* since the last report
|
||||
* @param shippedBytesOverall
|
||||
* during the request
|
||||
*/
|
||||
void progress(long requestedBytes, long shippedBytesSinceLast, long shippedBytesOverall);
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.math.RoundingMode;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.math.DoubleMath;
|
||||
|
||||
/**
|
||||
* Utility class for artifact file streaming.
|
||||
*/
|
||||
public final class FileStreamingUtil {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FileStreamingUtil.class);
|
||||
|
||||
/**
|
||||
* File suffix for MDH hash download (see Linux md5sum).
|
||||
*/
|
||||
public static final String ARTIFACT_MD5_DWNL_SUFFIX = ".MD5SUM";
|
||||
|
||||
private static final int BUFFER_SIZE = 0x2000; // 8k
|
||||
|
||||
private FileStreamingUtil() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a md5 file response.
|
||||
*
|
||||
* @param response
|
||||
* the response
|
||||
* @param md5Hash
|
||||
* of the artifact
|
||||
* @param filename
|
||||
* as provided by the client
|
||||
* @return the response
|
||||
* @throws IOException
|
||||
* cannot write output stream
|
||||
*/
|
||||
public static ResponseEntity<Void> writeMD5FileResponse(final HttpServletResponse response, final String md5Hash,
|
||||
final String filename) throws IOException {
|
||||
|
||||
if (md5Hash == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
builder.append(md5Hash);
|
||||
builder.append(" ");
|
||||
builder.append(filename);
|
||||
final byte[] content = builder.toString().getBytes(StandardCharsets.US_ASCII);
|
||||
|
||||
final StringBuilder header = new StringBuilder().append("attachment;filename=").append(filename)
|
||||
.append(ARTIFACT_MD5_DWNL_SUFFIX);
|
||||
|
||||
response.setContentLength(content.length);
|
||||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, header.toString());
|
||||
|
||||
response.getOutputStream().write(content);
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 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>
|
||||
*
|
||||
* @param artifact
|
||||
* the artifact
|
||||
* @param filename
|
||||
* to be written to the client response
|
||||
* @param lastModified
|
||||
* unix timestamp of the artifact
|
||||
* @param response
|
||||
* to be sent back to the requesting client
|
||||
* @param request
|
||||
* from the client
|
||||
* @param progressListener
|
||||
* to write progress updates to
|
||||
*
|
||||
* @return http response
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7233">https://tools.ietf.org
|
||||
* /html/rfc7233</a>
|
||||
*
|
||||
* @throws FileStreamingFailedException
|
||||
* if streaming fails
|
||||
*/
|
||||
public static ResponseEntity<InputStream> writeFileResponse(final AbstractDbArtifact 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 long length = artifact.getSize();
|
||||
|
||||
response.reset();
|
||||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename);
|
||||
response.setHeader(HttpHeaders.ETAG, etag);
|
||||
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
|
||||
if (lastModified != null) {
|
||||
response.setDateHeader(HttpHeaders.LAST_MODIFIED, lastModified);
|
||||
}
|
||||
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
response.setBufferSize(BUFFER_SIZE);
|
||||
|
||||
final ByteRange full = new ByteRange(0, length - 1, length);
|
||||
final List<ByteRange> ranges = new ArrayList<>();
|
||||
|
||||
// Validate and process Range and If-Range headers.
|
||||
final String range = request.getHeader("Range");
|
||||
if (lastModified != null && range != null) {
|
||||
LOG.debug("range header for filename ({}) is: {}", filename, range);
|
||||
|
||||
// Range header matches"bytes=n-n,n-n,n-n..."
|
||||
if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
|
||||
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + length);
|
||||
LOG.debug("range header for filename ({}) is not satisfiable: ", filename);
|
||||
return new ResponseEntity<>(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
}
|
||||
|
||||
// RFC: if the representation is unchanged, send me the part(s) that
|
||||
// I am requesting in
|
||||
// Range; otherwise, send me the entire representation.
|
||||
checkForShortcut(request, etag, lastModified, full, ranges);
|
||||
|
||||
// it seems there are valid ranges
|
||||
result = extractRange(response, length, ranges, range);
|
||||
// return if range extraction turned out to be invalid
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ResponseEntity<InputStream> handleFullFileRequest(final AbstractDbArtifact 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());
|
||||
|
||||
try (InputStream from = artifact.getFileInputStream()) {
|
||||
final ServletOutputStream to = response.getOutputStream();
|
||||
copyStreams(from, to, progressListener, r.getStart(), r.getLength(), filename);
|
||||
} catch (final IOException e) {
|
||||
throw new FileStreamingFailedException("fullfileRequest " + filename, e);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
private static ResponseEntity<InputStream> extractRange(final HttpServletResponse response, final long length,
|
||||
final List<ByteRange> ranges, final String range) {
|
||||
|
||||
if (ranges.isEmpty()) {
|
||||
for (final String part : range.substring(6).split(",")) {
|
||||
long start = sublong(part, 0, part.indexOf('-'));
|
||||
long end = sublong(part, part.indexOf('-') + 1, part.length());
|
||||
|
||||
if (start == -1) {
|
||||
start = length - end;
|
||||
end = length - 1;
|
||||
} else if (end == -1 || end > length - 1) {
|
||||
end = length - 1;
|
||||
}
|
||||
|
||||
// Check if Range is syntactically valid. If not, then return
|
||||
// 416.
|
||||
if (start > end) {
|
||||
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + length);
|
||||
return new ResponseEntity<>(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
}
|
||||
|
||||
// Add range.
|
||||
ranges.add(new ByteRange(start, end, length));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static long sublong(final String value, final int beginIndex, final int endIndex) {
|
||||
final String substring = value.substring(beginIndex, endIndex);
|
||||
return substring.length() > 0 ? Long.parseLong(substring) : -1;
|
||||
}
|
||||
|
||||
private static void checkForShortcut(final HttpServletRequest request, final String etag, final long lastModified,
|
||||
final ByteRange full, final List<ByteRange> ranges) {
|
||||
final String ifRange = request.getHeader(HttpHeaders.IF_RANGE);
|
||||
if (ifRange != null && !ifRange.equals(etag)) {
|
||||
try {
|
||||
final long ifRangeTime = request.getDateHeader(HttpHeaders.IF_RANGE);
|
||||
if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) {
|
||||
ranges.add(full);
|
||||
}
|
||||
} catch (final IllegalArgumentException ignore) {
|
||||
LOG.info("Invalid if-range header field", ignore);
|
||||
ranges.add(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ResponseEntity<InputStream> handleMultipartRangeRequest(final AbstractDbArtifact artifact,
|
||||
final String filename, final HttpServletResponse response,
|
||||
final FileStreamingProgressListener progressListener, final List<ByteRange> ranges) {
|
||||
|
||||
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 (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);
|
||||
}
|
||||
}
|
||||
|
||||
// End with final multipart boundary.
|
||||
to.println();
|
||||
to.print("--" + ByteRange.MULTIPART_BOUNDARY + "--");
|
||||
} catch (final IOException e) {
|
||||
throw new FileStreamingFailedException("multipartRangeRequest " + filename, e);
|
||||
}
|
||||
|
||||
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).build();
|
||||
}
|
||||
|
||||
private static ResponseEntity<InputStream> handleStandardRangeRequest(final AbstractDbArtifact 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());
|
||||
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
|
||||
try (InputStream from = artifact.getFileInputStream()) {
|
||||
final ServletOutputStream to = response.getOutputStream();
|
||||
copyStreams(from, to, progressListener, r.getStart(), r.getLength(), filename);
|
||||
} catch (final IOException e) {
|
||||
LOG.error("standardRangeRequest of file ({}) failed!", filename, e);
|
||||
throw new FileStreamingFailedException(filename);
|
||||
}
|
||||
|
||||
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,
|
||||
final String filename) throws IOException {
|
||||
|
||||
final long startMillis = System.currentTimeMillis();
|
||||
LOG.trace("Start of copy-streams of file {} from {} to {}", filename, start, length);
|
||||
|
||||
Preconditions.checkNotNull(from);
|
||||
Preconditions.checkNotNull(to);
|
||||
final byte[] buf = new byte[BUFFER_SIZE];
|
||||
long total = 0;
|
||||
int progressPercent = 1;
|
||||
|
||||
ByteStreams.skipFully(from, start);
|
||||
|
||||
long toRead = length;
|
||||
boolean toContinue = true;
|
||||
long shippedSinceLastEvent = 0;
|
||||
|
||||
while (toContinue) {
|
||||
final int r = from.read(buf);
|
||||
if (r == -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
toRead -= r;
|
||||
if (toRead > 0) {
|
||||
to.write(buf, 0, r);
|
||||
total += r;
|
||||
shippedSinceLastEvent += r;
|
||||
} else {
|
||||
to.write(buf, 0, (int) toRead + r);
|
||||
total += toRead + r;
|
||||
shippedSinceLastEvent += toRead + r;
|
||||
toContinue = false;
|
||||
}
|
||||
|
||||
if (progressListener != null) {
|
||||
final int newPercent = DoubleMath.roundToInt(total * 100.0 / length, RoundingMode.DOWN);
|
||||
|
||||
// every 10 percent an event
|
||||
if (newPercent == 100 || newPercent > progressPercent + 10) {
|
||||
progressPercent = newPercent;
|
||||
progressListener.progress(length, shippedSinceLastEvent, total);
|
||||
shippedSinceLastEvent = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
LOG.trace("Finished copy-stream of file {} with length {} in {} ms", filename, length, totalTime);
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private static final class ByteRange {
|
||||
private static final String MULTIPART_BOUNDARY = "THIS_STRING_SEPARATES_MULTIPART";
|
||||
|
||||
private final long start;
|
||||
private final long end;
|
||||
private final long length;
|
||||
private final long total;
|
||||
|
||||
private ByteRange(final long start, final long end, final long total) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
length = end - start + 1;
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
private long getStart() {
|
||||
return start;
|
||||
}
|
||||
|
||||
private long getEnd() {
|
||||
return end;
|
||||
}
|
||||
|
||||
private long getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
private long getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
@Override
|
||||
// Generated code
|
||||
@SuppressWarnings("squid:S864")
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + (int) (end ^ (end >>> 32));
|
||||
result = prime * result + (int) (length ^ (length >>> 32));
|
||||
result = prime * result + (int) (start ^ (start >>> 32));
|
||||
result = prime * result + (int) (total ^ (total >>> 32));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
// Generated code
|
||||
@SuppressWarnings("squid:S1126")
|
||||
public boolean equals(final Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
final ByteRange other = (ByteRange) obj;
|
||||
if (end != other.end) {
|
||||
return false;
|
||||
}
|
||||
if (length != other.length) {
|
||||
return false;
|
||||
}
|
||||
if (start != other.start) {
|
||||
return false;
|
||||
}
|
||||
if (total != other.total) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Utility class for the Rest Source API.
|
||||
*/
|
||||
public final class HttpUtil {
|
||||
|
||||
private HttpUtil() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks given CSV string for defined match value or wildcard.
|
||||
*
|
||||
* @param matchHeader
|
||||
* to search through
|
||||
* @param toMatch
|
||||
* to search for
|
||||
*
|
||||
* @return <code>true</code> if string matches.
|
||||
*/
|
||||
public static boolean matchesHttpHeader(final String matchHeader, final String toMatch) {
|
||||
final String[] matchValues = matchHeader.split("\\s*,\\s*");
|
||||
Arrays.sort(matchValues);
|
||||
return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,352 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
|
||||
*
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*/
|
||||
package org.eclipse.hawkbit.rest.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.repository.ControllerManagement;
|
||||
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.math.DoubleMath;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
|
||||
/**
|
||||
* Utility class for the Rest Source API.
|
||||
*/
|
||||
public final class RestResourceConversionHelper {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RestResourceConversionHelper.class);
|
||||
|
||||
private static final int BUFFER_SIZE = 4096;
|
||||
|
||||
private RestResourceConversionHelper() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Write response without target relation.
|
||||
*
|
||||
* @param artifact
|
||||
* the artifact
|
||||
* @param servletResponse
|
||||
* to be sent back to the requesting client
|
||||
* @param request
|
||||
* from the client
|
||||
* @param file
|
||||
* to be write to the client response
|
||||
*
|
||||
* @return http code
|
||||
*/
|
||||
public static ResponseEntity<InputStream> writeFileResponse(final Artifact artifact,
|
||||
final HttpServletResponse servletResponse, final HttpServletRequest request, final DbArtifact file) {
|
||||
return writeFileResponse(artifact, servletResponse, request, file, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 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>
|
||||
*
|
||||
* @param artifact
|
||||
* the artifact
|
||||
* @param response
|
||||
* to be sent back to the requesting client
|
||||
* @param request
|
||||
* from the client
|
||||
* @param file
|
||||
* to be write to the client response
|
||||
* @param controllerManagement
|
||||
* to write progress updates to
|
||||
* @param statusId
|
||||
* of the {@link ActionStatus}
|
||||
*
|
||||
* @return http code
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7233">https://tools.ietf.org
|
||||
* /html/rfc7233</a>
|
||||
*/
|
||||
public static ResponseEntity<InputStream> writeFileResponse(final Artifact artifact,
|
||||
final HttpServletResponse response, final HttpServletRequest request, final DbArtifact file,
|
||||
final ControllerManagement controllerManagement, final Long statusId) {
|
||||
|
||||
ResponseEntity<InputStream> result;
|
||||
|
||||
final String etag = artifact.getSha1Hash();
|
||||
final Long lastModified = artifact.getLastModifiedAt() != null ? artifact.getLastModifiedAt()
|
||||
: artifact.getCreatedAt();
|
||||
final long length = file.getSize();
|
||||
|
||||
response.reset();
|
||||
response.setBufferSize(BUFFER_SIZE);
|
||||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + artifact.getFilename());
|
||||
response.setHeader(HttpHeaders.ETAG, etag);
|
||||
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
|
||||
response.setDateHeader(HttpHeaders.LAST_MODIFIED, lastModified);
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
|
||||
final ByteRange full = new ByteRange(0, length - 1, length);
|
||||
final List<ByteRange> ranges = new ArrayList<>();
|
||||
|
||||
// Validate and process Range and If-Range headers.
|
||||
final String range = request.getHeader("Range");
|
||||
if (range != null) {
|
||||
LOG.debug("range header for filename ({}) is: {}", artifact.getFilename(), range);
|
||||
|
||||
// Range header matches"bytes=n-n,n-n,n-n..."
|
||||
if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
|
||||
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + length);
|
||||
LOG.debug("range header for filename ({}) is not satisfiable: ", artifact.getFilename());
|
||||
return new ResponseEntity<>(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
}
|
||||
|
||||
// RFC: if the representation is unchanged, send me the part(s) that
|
||||
// I am requesting in
|
||||
// Range; otherwise, send me the entire representation.
|
||||
checkForShortcut(request, etag, lastModified, full, ranges);
|
||||
|
||||
// it seems there are valid ranges
|
||||
result = extractRange(response, length, ranges, range);
|
||||
// return if range extraction turned out to be invalid
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// full request - no range
|
||||
if (ranges.isEmpty() || ranges.get(0).equals(full)) {
|
||||
LOG.debug("filename ({}) results into a full request: ", artifact.getFilename());
|
||||
handleFullFileRequest(artifact, response, file, controllerManagement, statusId, full);
|
||||
result = ResponseEntity.ok().build();
|
||||
}
|
||||
// standard range request
|
||||
else if (ranges.size() == 1) {
|
||||
LOG.debug("filename ({}) results into a standard range request: ", artifact.getFilename());
|
||||
handleStandardRangeRequest(artifact, response, file, controllerManagement, statusId, ranges);
|
||||
result = new ResponseEntity<>(HttpStatus.PARTIAL_CONTENT);
|
||||
}
|
||||
// multipart range request
|
||||
else {
|
||||
LOG.debug("filename ({}) results into a multipart range request: ", artifact.getFilename());
|
||||
handleMultipartRangeRequest(artifact, response, file, controllerManagement, statusId, ranges);
|
||||
result = new ResponseEntity<>(HttpStatus.PARTIAL_CONTENT);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void handleFullFileRequest(final Artifact artifact, final HttpServletResponse response,
|
||||
final DbArtifact file, final ControllerManagement controllerManagement, final Long statusId,
|
||||
final ByteRange full) {
|
||||
final ByteRange r = full;
|
||||
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal());
|
||||
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(r.getLength()));
|
||||
|
||||
try (InputStream inputStream = file.getFileInputStream()) {
|
||||
copyStreams(inputStream, response.getOutputStream(), controllerManagement, statusId, r.getStart(),
|
||||
r.getLength());
|
||||
} catch (final IOException e) {
|
||||
LOG.error("fullfileRequest of file ({}) failed!", artifact.getFilename(), e);
|
||||
throw new FileSteamingFailedException(artifact.getFilename());
|
||||
}
|
||||
}
|
||||
|
||||
private static ResponseEntity<InputStream> extractRange(final HttpServletResponse response, final long length,
|
||||
final List<ByteRange> ranges, final String range) {
|
||||
|
||||
if (ranges.isEmpty()) {
|
||||
for (final String part : range.substring(6).split(",")) {
|
||||
long start = sublong(part, 0, part.indexOf('-'));
|
||||
long end = sublong(part, part.indexOf('-') + 1, part.length());
|
||||
|
||||
if (start == -1) {
|
||||
start = length - end;
|
||||
end = length - 1;
|
||||
} else if (end == -1 || end > length - 1) {
|
||||
end = length - 1;
|
||||
}
|
||||
|
||||
// Check if Range is syntactically valid. If not, then return
|
||||
// 416.
|
||||
if (start > end) {
|
||||
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes */" + length);
|
||||
return new ResponseEntity<>(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
}
|
||||
|
||||
// Add range.
|
||||
ranges.add(new ByteRange(start, end, length));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static long sublong(final String value, final int beginIndex, final int endIndex) {
|
||||
final String substring = value.substring(beginIndex, endIndex);
|
||||
return substring.length() > 0 ? Long.parseLong(substring) : -1;
|
||||
}
|
||||
|
||||
private static void checkForShortcut(final HttpServletRequest request, final String etag, final long lastModified,
|
||||
final ByteRange full, final List<ByteRange> ranges) {
|
||||
final String ifRange = request.getHeader(HttpHeaders.IF_RANGE);
|
||||
if (ifRange != null && !ifRange.equals(etag)) {
|
||||
try {
|
||||
final long ifRangeTime = request.getDateHeader(HttpHeaders.IF_RANGE);
|
||||
if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) {
|
||||
ranges.add(full);
|
||||
}
|
||||
} catch (final IllegalArgumentException ignore) {
|
||||
LOG.info("Invalid if-range header field", ignore);
|
||||
ranges.add(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleMultipartRangeRequest(final Artifact artifact, final HttpServletResponse response,
|
||||
final DbArtifact file, final ControllerManagement controllerManagement, final Long statusId,
|
||||
final List<ByteRange> ranges) {
|
||||
response.setContentType("multipart/byteranges; boundary=" + ByteRange.MULTIPART_BOUNDARY);
|
||||
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
|
||||
for (final ByteRange r : ranges) {
|
||||
try (InputStream inputStream = file.getFileInputStream()) {
|
||||
|
||||
// Add multipart boundary and header fields for every range.
|
||||
response.getOutputStream().println();
|
||||
response.getOutputStream().println("--" + ByteRange.MULTIPART_BOUNDARY);
|
||||
response.getOutputStream()
|
||||
.println("Content-Range: bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal());
|
||||
|
||||
// Copy single part range of multi part range.
|
||||
copyStreams(inputStream, response.getOutputStream(), controllerManagement, statusId, r.getStart(),
|
||||
r.getLength());
|
||||
} catch (final IOException e) {
|
||||
throwFileStreamingFailedException(artifact, e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
// End with final multipart boundary.
|
||||
response.getOutputStream().println();
|
||||
response.getOutputStream().print("--" + ByteRange.MULTIPART_BOUNDARY + "--");
|
||||
} catch (final IOException e) {
|
||||
throwFileStreamingFailedException(artifact, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void throwFileStreamingFailedException(final Artifact artifact, final IOException e) {
|
||||
LOG.error("multipartRangeRequest of file ({}) failed!", artifact.getFilename(), e);
|
||||
throw new FileSteamingFailedException(artifact.getFilename());
|
||||
}
|
||||
|
||||
private static void handleStandardRangeRequest(final Artifact artifact, final HttpServletResponse response,
|
||||
final DbArtifact file, final ControllerManagement controllerManagement, final Long statusId,
|
||||
final List<ByteRange> ranges) {
|
||||
final ByteRange r = ranges.get(0);
|
||||
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal());
|
||||
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(r.getLength()));
|
||||
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
|
||||
try (InputStream inputStream = file.getFileInputStream()) {
|
||||
copyStreams(inputStream, response.getOutputStream(), controllerManagement, statusId, r.getStart(),
|
||||
r.getLength());
|
||||
} catch (final IOException e) {
|
||||
LOG.error("standardRangeRequest of file ({}) failed!", artifact.getFilename(), e);
|
||||
throw new FileSteamingFailedException(artifact.getFilename());
|
||||
}
|
||||
}
|
||||
|
||||
private static long copyStreams(final InputStream from, final OutputStream to,
|
||||
final ControllerManagement controllerManagement, final Long statusId, final long start, final long length)
|
||||
throws IOException {
|
||||
Preconditions.checkNotNull(from);
|
||||
Preconditions.checkNotNull(to);
|
||||
final byte[] buf = new byte[BUFFER_SIZE];
|
||||
long total = 0;
|
||||
int progressPercent = 1;
|
||||
|
||||
// skipp until start is reached
|
||||
long skipped = 0;
|
||||
do {
|
||||
skipped += from.skip(start);
|
||||
} while (skipped < start);
|
||||
|
||||
long toRead = length;
|
||||
boolean toContinue = true;
|
||||
long shippedSinceLastEvent = 0;
|
||||
|
||||
while (toContinue) {
|
||||
final int r = from.read(buf);
|
||||
if (r == -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
toRead -= r;
|
||||
if (toRead > 0) {
|
||||
to.write(buf, 0, r);
|
||||
total += r;
|
||||
shippedSinceLastEvent += r;
|
||||
} else {
|
||||
to.write(buf, 0, (int) toRead + r);
|
||||
total += toRead + r;
|
||||
shippedSinceLastEvent += toRead + r;
|
||||
toContinue = false;
|
||||
}
|
||||
|
||||
if (controllerManagement != null) {
|
||||
final int newPercent = DoubleMath.roundToInt(total * 100.0 / length, RoundingMode.DOWN);
|
||||
|
||||
// every 10 percent an event
|
||||
if (newPercent == 100 || newPercent > progressPercent + 10) {
|
||||
progressPercent = newPercent;
|
||||
controllerManagement.downloadProgress(statusId, length, shippedSinceLastEvent, total);
|
||||
shippedSinceLastEvent = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks given CSV string for defined match value or * wildcard.
|
||||
*
|
||||
* @param matchHeader
|
||||
* to search through
|
||||
* @param toMatch
|
||||
* to search for
|
||||
*
|
||||
* @return <code>true</code> if string matches.
|
||||
*/
|
||||
public static boolean matchesHttpHeader(final String matchHeader, final String toMatch) {
|
||||
final String[] matchValues = matchHeader.split("\\s*,\\s*");
|
||||
Arrays.sort(matchValues);
|
||||
return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -41,7 +41,7 @@ public class StreamAwareErrorController extends BasicErrorController {
|
||||
}
|
||||
|
||||
@RequestMapping(produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||
ResponseEntity<Void> errorStream(final HttpServletRequest request, final HttpServletResponse response) {
|
||||
public ResponseEntity<Void> errorStream(final HttpServletRequest request, final HttpServletResponse response) {
|
||||
final HttpStatus status = getStatus(request);
|
||||
return new ResponseEntity<>(status);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user