18
.3rd-party/README.md
vendored
18
.3rd-party/README.md
vendored
@@ -1,10 +1,16 @@
|
||||
# Third-Party Dependencies
|
||||
Third-Party Dependencies
|
||||
===
|
||||
|
||||
This folder provides listings of all 3rd-party dependencies incl. their licenses. There is a dedicated subfolder for
|
||||
each release (and milestone) holding the release-specific information.
|
||||
This folder contains DEPENDENCIES that has the list of all 3rd-party dependencies (including transitive) with their licenses and approval status. Each release (and milestone) holds the release-specific information.
|
||||
|
||||
The DEPENDENCIES file could be generated manually using [Eclipse Dash License Tool](https://github.com/eclipse/dash-licenses) maven plugin by running in root folder:
|
||||
DEPENDENCIES file is generated (automatically and committed) by [../.github/workflows/reusable_workflow_license-scan.yaml](../.github/workflows/reusable_workflow_license-scan.yaml) during the release process ([../.github/workflows/release.yaml](../.github/workflows/release.yaml)) and on daily basis ([../.github/workflows/license-scan.yaml](../.github/workflows/license-scan.yaml)). It is also
|
||||
|
||||
DEPENDENCIES file could be generated manually using [Eclipse Dash License Tool](https://github.com/eclipse/dash-licenses) maven plugin by running:
|
||||
```shell
|
||||
mvn clean install -PcheckLicense -DskipTests \
|
||||
--projects '!org.eclipse.hawkbit:hawkbit-repository-test,!org.eclipse.hawkbit:hawkbit-dmf-rabbitmq-test'
|
||||
$ cd .. && mvn license-tool:license-check -Ddash.fail=false -PcheckLicense
|
||||
```
|
||||
|
||||
Note: Some projects (e.g. test artifacts) could be excluded with *--projects* parameter, e.g:
|
||||
```shell
|
||||
$ cd .. && mvn license-tool:license-check -Ddash.fail=false -PcheckLicense \ --projects '!org.eclipse.hawkbit:hawkbit-repository-test,!org.eclipse.hawkbit:hawkbit-dmf-rabbitmq-test'
|
||||
```
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
- name: Check dependency licenses with dash tool (and open issues to Dash IP lab, doesn't fail)
|
||||
if: ${{ inputs.open_tickets }}
|
||||
run: |
|
||||
mvn license-tool:license-check -Ddash.fail=false -PcheckLicense -Ddash.iplab.token=${GITLAB_API_TOKEN} --projects '!org.eclipse.hawkbit:hawkbit-repository-test,!org.eclipse.hawkbit:hawkbit-dmf-rabbitmq-test'
|
||||
mvn license-tool:license-check -Ddash.fail=false -PcheckLicense -Ddash.iplab.token=${GITLAB_API_TOKEN}
|
||||
CHANGED_FILES_COUNT=$(git status --short | wc -l)
|
||||
CHANGED_FILES_COUNT=${CHANGED_FILES_COUNT//[[:space:]]/}
|
||||
echo "Number of changed files: ${CHANGED_FILES_COUNT}"
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
hawkBit Docker
|
||||
===
|
||||
|
||||
# Setup
|
||||
## Overview
|
||||
This folder contains example Docker build and Docker Compose files to build and start the hawkBit as monolith or as microservices.
|
||||
|
||||
## A: Docker Container
|
||||
Start the hawkBit Update Server as a single container (requires Docker to be installed and all dependencies to be available)
|
||||
## Build
|
||||
You could build the hawkbit Docker images following the [README.md](build/README.md) instructions.
|
||||
|
||||
## Start
|
||||
You can start hawkbit as a Docker Container (only monolith) or with Docker Compose
|
||||
|
||||
#### A: Docker Container (only as monolith)
|
||||
_Note: You need to have Docker installed on your machine._
|
||||
|
||||
Start the hawkBit Update Server (monolith) as a single container (with embedded H2, if you configure a different database, e.g. MySQL or PostgreSQL, you should start it separately):
|
||||
|
||||
```bash
|
||||
$ docker run -d -p 8080:8080 hawkbit/hawkbit-update-server:latest
|
||||
```
|
||||
|
||||
## B: Docker Compose
|
||||
Start the hawkBit Update Server together with an MySQL and RabbitMQ instance as containers (Requires Docker Compose to be installed)
|
||||
### B: Docker Compose
|
||||
_Note: You need to have Docker Compose installed on your machine._
|
||||
|
||||
Start the hawkBit Update Server (monolith) together with an MySQL and RabbitMQ instance as containers (Requires Docker Compose to be installed)
|
||||
|
||||
```bash
|
||||
$ docker compose -f mysql/docker-compose-monolith-mysql.yml up
|
||||
```
|
||||
You could, also start it in different flavours, with UI or in microservices mode.
|
||||
|
||||
Note: Whit the upper command CTRL+C shuts down all services. Add '-d' at the end to start all into detached mode:
|
||||
With the upper command CTRL+C shuts down all services. Add '-d' at the end to start all into detached mode:
|
||||
```bash
|
||||
$ docker compose -f mysql/docker-compose-monolith-mysql.yml up -d
|
||||
```
|
||||
@@ -27,15 +36,20 @@ Then stop all services with:
|
||||
$ docker compose -f mysql/docker-compose-monolith-mysql.yml down
|
||||
```
|
||||
|
||||
# Access
|
||||
| Service / Container | URL | Login | A | B | C |
|
||||
|--------------------------|--------------------------------------------------|-------------|----------|----------|----------|
|
||||
| hawkBit Update Server | [http://localhost:8080/](http://localhost:8080/) | admin:admin | ✓ | ✓ | ✓ |
|
||||
| MySQL | localhost:3306/hawkbit | root | | ✓ | ✓ |
|
||||
| RabbitMQ | [http://localhost:15672](http://localhost:15672) | guest:guest | | ✓ | ✓ |
|
||||
You could, also start it in different flavours, with UI or in microservices mode (see Docker Compose files in [mysql](./mysql) and [postgres](./postgres) folders). For instance to start with PostgreSQL, with RabbitMQ, in microservices mode and with UI you could use:
|
||||
```bash
|
||||
$ docker compose -f postgres/docker-compose-micro-services-with-simple-ui-postgres.yml up
|
||||
```
|
||||
|
||||
# Configuration
|
||||
You can override application.properties by setting an environment variable SPRING_APPLICATION_JSON for hawkbit container.
|
||||
### Access
|
||||
| Service / Container | URL | Login | A | B |
|
||||
|-----------------------|------------------------|-------------|----------|----------|
|
||||
| hawkBit Update Server | [http://localhost:8080/](http://localhost:8080/) | admin:admin | ✓ | ✓ |
|
||||
| MySQL | localhost:3306/hawkbit | root | | ✓ |
|
||||
| RabbitMQ | [http://localhost:15672](http://localhost:15672) | guest:guest | | ✓ |
|
||||
|
||||
### Configuration
|
||||
You can override _application.properties_ by setting an environment variable _SPRING_APPLICATION_JSON_ to the hawkbit container, e.g.:
|
||||
|
||||
```yaml
|
||||
hawkbit:
|
||||
@@ -55,4 +69,4 @@ hawkbit:
|
||||
"hawkbit.security.user.hawkbit.password": "{noop}isAwesome!",
|
||||
"hawkbit.security.user.hawkbit.roles": "TENANT_ADMIN"
|
||||
}'
|
||||
```
|
||||
```
|
||||
@@ -28,9 +28,7 @@ Docker image could be build, for example, with (fixed version 0.4.1 is just an e
|
||||
```shell
|
||||
docker build --build-arg HAWKBIT_APP=hawkbit-update-server --build-arg HAWKBIT_VERSION=0.4.1 -t hawkbit_update_server:0.4.1 . -f Dockerfile
|
||||
```
|
||||
|
||||
or just by:
|
||||
|
||||
```shell
|
||||
docker build --build-arg HAWKBIT_VERSION=0.4.1 -t hawkbit_update_server:0.4.1 .
|
||||
```
|
||||
|
||||
5
hawkbit-artifact/README.md
Normal file
5
hawkbit-artifact/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
hawkBit Artifact
|
||||
===
|
||||
The module contains internal modules for artifact storage and encryption:
|
||||
* [hawkbit-artifact-api](hawkbit-artifact-api/README.md) - see for artifact API module
|
||||
* [hawkbit-artifact-fs](hawkbit-artifact-fs/README.md) - see for file-system based artifact storage implementation
|
||||
@@ -1,3 +1,6 @@
|
||||
# hawkBit Artifact API
|
||||
|
||||
Various internal interfaces artifact API classes.
|
||||
hawkBit Artifact API
|
||||
===
|
||||
The module contains artifact API classes supporting following main concepts:
|
||||
* Artifact Storage - represented by the [ArtifactStorage](src/main/java/org/eclipse/hawkbit/artifact/ArtifactStorage.java) interface. It serves for artifact binary store operations
|
||||
* Artifact Encryption - represented by the [ArtifactEncryptionService](src/main/java/org/eclipse/hawkbit/artifact/encryption/ArtifactEncryptionService.java). It is a pluggable implementation of artifact encryption operations.
|
||||
* Artifact URL handling - represented by[ArtifactUrlResolver](src/main/java/org/eclipse/hawkbit/artifact/urlresolver/ArtifactUrlResolver.java) interface. It provides resolving URLs to the artifacts. The module provides a simple property based implementation ([PropertyBasedArtifactUrlResolver](src/main/java/org/eclipse/hawkbit/artifact/urlresolver/PropertyBasedArtifactUrlResolver.java))
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact;
|
||||
package org.eclipse.hawkbit.artifact;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
@@ -22,17 +22,17 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.repository.artifact.exception.ArtifactStoreException;
|
||||
import org.eclipse.hawkbit.repository.artifact.exception.HashNotMatchException;
|
||||
import org.eclipse.hawkbit.repository.artifact.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.repository.artifact.model.DbArtifactHash;
|
||||
import org.eclipse.hawkbit.artifact.exception.ArtifactStoreException;
|
||||
import org.eclipse.hawkbit.artifact.exception.HashNotMatchException;
|
||||
import org.eclipse.hawkbit.artifact.model.ArtifactHashes;
|
||||
import org.eclipse.hawkbit.artifact.model.StoredArtifactInfo;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Abstract utility class for ArtifactRepository implementations with common functionality, e.g. computation of hashes.
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractArtifactRepository implements ArtifactRepository {
|
||||
public abstract class AbstractArtifactStorage implements ArtifactStorage {
|
||||
|
||||
private static final String TEMP_FILE_PREFIX = "tmp";
|
||||
private static final String TEMP_FILE_SUFFIX = "artifactrepo";
|
||||
@@ -40,9 +40,9 @@ public abstract class AbstractArtifactRepository implements ArtifactRepository {
|
||||
// suppress warning, of not strong enough hashing algorithm, SHA-1 and MD5 is not used security related
|
||||
@SuppressWarnings("squid:S2070")
|
||||
@Override
|
||||
public AbstractDbArtifact store(
|
||||
public StoredArtifactInfo store(
|
||||
final String tenant, final InputStream content, final String filename, final String contentType,
|
||||
final DbArtifactHash providedHashes) {
|
||||
final ArtifactHashes providedHashes) {
|
||||
final MessageDigest mdSHA1;
|
||||
final MessageDigest mdMD5;
|
||||
final MessageDigest mdSHA256;
|
||||
@@ -60,19 +60,19 @@ public abstract class AbstractArtifactRepository implements ArtifactRepository {
|
||||
|
||||
final HexFormat hexFormat = HexFormat.of().withLowerCase();
|
||||
|
||||
final String sha1Hash16 = hexFormat.formatHex(mdSHA1.digest());
|
||||
final String md5Hash16 = hexFormat.formatHex(mdMD5.digest());
|
||||
final String sha256Hash16 = hexFormat.formatHex(mdSHA256.digest());
|
||||
final String sha1Hash = hexFormat.formatHex(mdSHA1.digest());
|
||||
final String md5Hash = hexFormat.formatHex(mdMD5.digest());
|
||||
final String sha256Hash = hexFormat.formatHex(mdSHA256.digest());
|
||||
|
||||
checkHashes(providedHashes, sha1Hash16, md5Hash16, sha256Hash16);
|
||||
checkHashes(providedHashes, sha1Hash, md5Hash, sha256Hash);
|
||||
|
||||
// Check if file with same sha1 hash exists and if so return it
|
||||
if (existsBySha1(tenant, sha1Hash16)) {
|
||||
if (existsBySha1(tenant, sha1Hash)) {
|
||||
// TODO - shall check if the file is really the same as bytes or just sha1 hash is the same
|
||||
return addMissingHashes(getBySha1(tenant, sha1Hash16), sha1Hash16, md5Hash16, sha256Hash16);
|
||||
return new StoredArtifactInfo(contentType, tempFile.length(), new ArtifactHashes(sha1Hash, md5Hash, sha256Hash));
|
||||
}
|
||||
|
||||
return store(sanitizeTenant(tenant), new DbArtifactHash(sha1Hash16, md5Hash16, sha256Hash16), contentType, tempFile);
|
||||
return store(sanitizeTenant(tenant), new ArtifactHashes(sha1Hash, md5Hash, sha256Hash), contentType, tempFile);
|
||||
} catch (final IOException e) {
|
||||
throw new ArtifactStoreException(e.getMessage(), e);
|
||||
} finally {
|
||||
@@ -104,13 +104,13 @@ public abstract class AbstractArtifactRepository implements ArtifactRepository {
|
||||
return file.getPath();
|
||||
}
|
||||
|
||||
protected abstract AbstractDbArtifact store(final String tenant, final DbArtifactHash base16Hashes,
|
||||
final String contentType, final String tempFile) throws IOException;
|
||||
protected abstract StoredArtifactInfo store(
|
||||
final String tenant, final ArtifactHashes base16Hashes, final String contentType, final String tempFile) throws IOException;
|
||||
|
||||
// java:S1066 - more readable with separate "if" statements
|
||||
// java:S4042 - delete reason is not needed
|
||||
@SuppressWarnings({ "java:S1066", "java:S4042" })
|
||||
static File createTempFile(final boolean directory) {
|
||||
public static File createTempFile(final boolean directory) {
|
||||
try {
|
||||
final File file = (directory
|
||||
? Files.createTempDirectory(TEMP_FILE_PREFIX)
|
||||
@@ -138,22 +138,22 @@ public abstract class AbstractArtifactRepository implements ArtifactRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkHashes(final DbArtifactHash providedHashes,
|
||||
final String sha1Hash16, final String md5Hash16, final String sha256Hash16) {
|
||||
private static void checkHashes(
|
||||
final ArtifactHashes providedHashes, final String sha1Hash16, final String md5Hash16, final String sha256Hash16) {
|
||||
if (providedHashes == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (areHashesNotMatching(providedHashes.getSha1(), sha1Hash16)) {
|
||||
throw new HashNotMatchException("The given sha1 hash " + providedHashes.getSha1() +
|
||||
if (areHashesNotMatching(providedHashes.sha1(), sha1Hash16)) {
|
||||
throw new HashNotMatchException("The given sha1 hash " + providedHashes.sha1() +
|
||||
" does not match the calculated sha1 hash " + sha1Hash16, HashNotMatchException.SHA1);
|
||||
}
|
||||
if (areHashesNotMatching(providedHashes.getMd5(), md5Hash16)) {
|
||||
throw new HashNotMatchException("The given md5 hash " + providedHashes.getMd5() +
|
||||
if (areHashesNotMatching(providedHashes.md5(), md5Hash16)) {
|
||||
throw new HashNotMatchException("The given md5 hash " + providedHashes.md5() +
|
||||
" does not match the calculated md5 hash " + md5Hash16, HashNotMatchException.MD5);
|
||||
}
|
||||
if (areHashesNotMatching(providedHashes.getSha256(), sha256Hash16)) {
|
||||
throw new HashNotMatchException("The given sha256 hash " + providedHashes.getSha256() +
|
||||
if (areHashesNotMatching(providedHashes.sha256(), sha256Hash16)) {
|
||||
throw new HashNotMatchException("The given sha256 hash " + providedHashes.sha256() +
|
||||
" does not match the calculated sha256 hash " + sha256Hash16, HashNotMatchException.SHA256);
|
||||
}
|
||||
}
|
||||
@@ -167,16 +167,6 @@ public abstract class AbstractArtifactRepository implements ArtifactRepository {
|
||||
return new DigestInputStream(new DigestInputStream(new DigestInputStream(input, mdSHA256), mdMD5), mdSHA1);
|
||||
}
|
||||
|
||||
private AbstractDbArtifact addMissingHashes(final AbstractDbArtifact existing,
|
||||
final String calculatedSha1, final String calculatedMd5, final String calculatedSha256) {
|
||||
final String sha1 = checkEmpty(existing.getHashes().getSha1(), calculatedSha1);
|
||||
final String md5 = checkEmpty(existing.getHashes().getMd5(), calculatedMd5);
|
||||
final String sha256 = checkEmpty(existing.getHashes().getSha256(), calculatedSha256);
|
||||
|
||||
existing.setHashes(new DbArtifactHash(sha1, md5, sha256));
|
||||
return existing;
|
||||
}
|
||||
|
||||
private String checkEmpty(final String value, final String fallback) {
|
||||
return ObjectUtils.isEmpty(value) ? fallback : value;
|
||||
}
|
||||
@@ -7,22 +7,23 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact;
|
||||
package org.eclipse.hawkbit.artifact;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import org.eclipse.hawkbit.repository.artifact.exception.ArtifactStoreException;
|
||||
import org.eclipse.hawkbit.repository.artifact.exception.HashNotMatchException;
|
||||
import org.eclipse.hawkbit.repository.artifact.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.repository.artifact.model.DbArtifactHash;
|
||||
import org.eclipse.hawkbit.artifact.exception.ArtifactBinaryNotFoundException;
|
||||
import org.eclipse.hawkbit.artifact.exception.ArtifactStoreException;
|
||||
import org.eclipse.hawkbit.artifact.exception.HashNotMatchException;
|
||||
import org.eclipse.hawkbit.artifact.model.ArtifactHashes;
|
||||
import org.eclipse.hawkbit.artifact.model.StoredArtifactInfo;
|
||||
|
||||
/**
|
||||
* ArtifactRepository service interface.
|
||||
* Artifact Store service interface.
|
||||
*/
|
||||
public interface ArtifactRepository {
|
||||
public interface ArtifactStorage {
|
||||
|
||||
/**
|
||||
* Stores an artifact into the repository.
|
||||
@@ -37,20 +38,20 @@ public interface ArtifactRepository {
|
||||
* @throws ArtifactStoreException in case storing of the artifact was not successful
|
||||
* @throws HashNotMatchException in case {@code hash} is provided and not matching to the calculated hashes during storing
|
||||
*/
|
||||
AbstractDbArtifact store(
|
||||
StoredArtifactInfo store(
|
||||
@NotEmpty String tenant, @NotNull InputStream content, @NotEmpty String filename,
|
||||
String contentType, DbArtifactHash hash);
|
||||
String contentType, ArtifactHashes hash);
|
||||
|
||||
/**
|
||||
* Retrieves a {@link AbstractDbArtifact} from the store by its SHA1 hash. Throws {@link org.eclipse.hawkbit.repository.artifact.exception.ArtifactBinaryNotFoundException} if not found.
|
||||
* Retrieves a {@link StoredArtifactInfo} from the store by its SHA1 hash. Throws {@link ArtifactBinaryNotFoundException} if not found.
|
||||
* The caller is responsible to close the InputStream.
|
||||
*
|
||||
* @param tenant the tenant to store the artifact
|
||||
* @param sha1Hash the sha1-hash of the file to lookup.
|
||||
* @return The artifact file object or {@code null} if no file exists.
|
||||
* @throws UnsupportedOperationException if implementation does not support the operation
|
||||
*/
|
||||
AbstractDbArtifact getBySha1(@NotEmpty String tenant, @NotEmpty String sha1Hash);
|
||||
|
||||
InputStream getBySha1(@NotEmpty String tenant, @NotEmpty String sha1Hash);
|
||||
|
||||
/**
|
||||
* Checks if an artifact exists for a given tenant by its sha1 hash
|
||||
@@ -7,13 +7,13 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.encryption;
|
||||
package org.eclipse.hawkbit.artifact.encryption;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.hawkbit.repository.artifact.exception.ArtifactEncryptionFailedException;
|
||||
import org.eclipse.hawkbit.artifact.exception.ArtifactEncryptionFailedException;
|
||||
|
||||
/**
|
||||
* Interface definition for artifact encryption.
|
||||
@@ -23,7 +23,7 @@ public interface ArtifactEncryption {
|
||||
/**
|
||||
* Defines the required secret keys for particular encryption algorithm.
|
||||
*
|
||||
* @return list of required secret keys
|
||||
* @return set of required secret keys
|
||||
*/
|
||||
Set<String> requiredSecretKeys();
|
||||
|
||||
@@ -61,4 +61,4 @@ public interface ArtifactEncryption {
|
||||
* @return encryption overhead in byte
|
||||
*/
|
||||
int encryptionSizeOverhead();
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.encryption;
|
||||
package org.eclipse.hawkbit.artifact.encryption;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -15,7 +15,7 @@ import java.util.Optional;
|
||||
* Interface definition for artifact encryption secrets store. It maintains secret key/value pairs
|
||||
* identified by id (e.g. software module id)
|
||||
*/
|
||||
public interface ArtifactEncryptionSecretsStore {
|
||||
public interface ArtifactEncryptionSecretsStorage {
|
||||
|
||||
/**
|
||||
* Adds secret key/value pair associated with particular id (e.g. software module id) to the store.
|
||||
@@ -26,14 +26,6 @@ public interface ArtifactEncryptionSecretsStore {
|
||||
*/
|
||||
void addSecret(final long id, final String secretKey, final String secretValue);
|
||||
|
||||
/**
|
||||
* Checks if secret is present for particular id and key in the store.
|
||||
*
|
||||
* @param id id of the secret
|
||||
* @param secretKey key of the secret
|
||||
*/
|
||||
boolean secretExists(final long id, final String secretKey);
|
||||
|
||||
/**
|
||||
* Retrieves secret value associated with particular id and key from the store.
|
||||
*
|
||||
@@ -41,12 +33,4 @@ public interface ArtifactEncryptionSecretsStore {
|
||||
* @param secretKey key of the secret
|
||||
*/
|
||||
Optional<String> getSecret(final long id, final String secretKey);
|
||||
|
||||
/**
|
||||
* Removes secret key/value pair associated with particular id from the store.
|
||||
*
|
||||
* @param id id of the secret
|
||||
* @param secretKey key of the secret
|
||||
*/
|
||||
void removeSecret(final long id, final String secretKey);
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.encryption;
|
||||
package org.eclipse.hawkbit.artifact.encryption;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
@@ -17,12 +17,11 @@ import java.util.Set;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.eclipse.hawkbit.repository.artifact.exception.ArtifactEncryptionUnsupportedException;
|
||||
import org.eclipse.hawkbit.artifact.exception.ArtifactEncryptionUnsupportedException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
/**
|
||||
* Service responsible for encryption operations. Should be registered as a bean in order its autowired dependencies
|
||||
* to be injected.
|
||||
* Service responsible for encryption operations. Should be registered as a bean in order its autowired dependencies to be injected.
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@SuppressWarnings("java:S6548") // singleton holder ensures static access to spring resources in some places
|
||||
@@ -31,7 +30,7 @@ public final class ArtifactEncryptionService {
|
||||
private static final ArtifactEncryptionService SINGLETON = new ArtifactEncryptionService();
|
||||
|
||||
private ArtifactEncryption artifactEncryption;
|
||||
private ArtifactEncryptionSecretsStore artifactEncryptionSecretsStore;
|
||||
private ArtifactEncryptionSecretsStorage artifactEncryptionSecretsStore;
|
||||
|
||||
/**
|
||||
* @return the artifact encryption service singleton instance
|
||||
@@ -46,7 +45,7 @@ public final class ArtifactEncryptionService {
|
||||
}
|
||||
|
||||
@Autowired(required = false) // spring setter injection
|
||||
public void setArtifactEncryptionSecretsStore(final ArtifactEncryptionSecretsStore artifactEncryptionSecretsStore) {
|
||||
public void setArtifactEncryptionSecretsStore(final ArtifactEncryptionSecretsStorage artifactEncryptionSecretsStore) {
|
||||
this.artifactEncryptionSecretsStore = artifactEncryptionSecretsStore;
|
||||
}
|
||||
|
||||
@@ -111,7 +110,7 @@ public final class ArtifactEncryptionService {
|
||||
throw new ArtifactEncryptionUnsupportedException("Artifact decryption is not supported.");
|
||||
}
|
||||
|
||||
final var secrets = getEncryptionSecrets(id);
|
||||
final Map<String, String> secrets = getEncryptionSecrets(id);
|
||||
try {
|
||||
return artifactEncryption.decryptStream(secrets, encryptedArtifactStream);
|
||||
} finally {
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.exception;
|
||||
package org.eclipse.hawkbit.artifact.exception;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
@@ -28,29 +28,7 @@ public class ArtifactBinaryNoLongerExistsException extends AbstractServerRtExcep
|
||||
|
||||
private static final SpServerError THIS_ERROR = SpServerError.SP_ARTIFACT_BINARY_DELETED;
|
||||
|
||||
/**
|
||||
* Creates a new ArtifactBinaryGoneException error.
|
||||
*/
|
||||
public ArtifactBinaryNoLongerExistsException() {
|
||||
super(THIS_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ArtifactBinaryGoneException error with cause.
|
||||
*
|
||||
* @param cause for the exception
|
||||
*/
|
||||
public ArtifactBinaryNoLongerExistsException(final Throwable cause) {
|
||||
super(THIS_ERROR, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ArtifactBinaryGoneException error with message.
|
||||
*
|
||||
* @param message of the error
|
||||
*/
|
||||
public ArtifactBinaryNoLongerExistsException(final String message) {
|
||||
super(message, THIS_ERROR);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.exception;
|
||||
package org.eclipse.hawkbit.artifact.exception;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
@@ -23,25 +23,7 @@ public final class ArtifactBinaryNotFoundException extends AbstractServerRtExcep
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Creates a new FileUploadFailedException with
|
||||
* {@link SpServerError#SP_ARTIFACT_LOAD_FAILED} error.
|
||||
*/
|
||||
public ArtifactBinaryNotFoundException() {
|
||||
super(SpServerError.SP_ARTIFACT_LOAD_FAILED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cause for the exception
|
||||
*/
|
||||
public ArtifactBinaryNotFoundException(final Throwable cause) {
|
||||
super(SpServerError.SP_ARTIFACT_LOAD_FAILED, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message of the error
|
||||
*/
|
||||
public ArtifactBinaryNotFoundException(final String message) {
|
||||
super(message, SpServerError.SP_ARTIFACT_LOAD_FAILED);
|
||||
super(SpServerError.SP_ARTIFACT_LOAD_FAILED, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.exception;
|
||||
package org.eclipse.hawkbit.artifact.exception;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
@@ -26,25 +26,7 @@ public final class ArtifactDeleteFailedException extends AbstractServerRtExcepti
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Creates a new FileUploadFailedException with
|
||||
* {@link SpServerError#SP_ARTIFACT_DELETE_FAILED} error.
|
||||
*/
|
||||
public ArtifactDeleteFailedException() {
|
||||
super(SpServerError.SP_ARTIFACT_DELETE_FAILED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cause for the exception
|
||||
*/
|
||||
public ArtifactDeleteFailedException(final Throwable cause) {
|
||||
super(SpServerError.SP_ARTIFACT_DELETE_FAILED, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message of the error
|
||||
*/
|
||||
public ArtifactDeleteFailedException(final String message) {
|
||||
super(message, SpServerError.SP_ARTIFACT_DELETE_FAILED);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.exception;
|
||||
package org.eclipse.hawkbit.artifact.exception;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
@@ -35,16 +35,8 @@ public final class ArtifactEncryptionFailedException extends AbstractServerRtExc
|
||||
@Getter
|
||||
private final EncryptionOperation encryptionOperation;
|
||||
|
||||
public ArtifactEncryptionFailedException(final EncryptionOperation encryptionOperation) {
|
||||
this(encryptionOperation, null, null);
|
||||
}
|
||||
|
||||
public ArtifactEncryptionFailedException(final EncryptionOperation encryptionOperation, final String message) {
|
||||
this(encryptionOperation, message, null);
|
||||
}
|
||||
|
||||
public ArtifactEncryptionFailedException(final EncryptionOperation encryptionOperation, final String message, final Throwable cause) {
|
||||
super(message, SpServerError.SP_ARTIFACT_ENCRYPTION_FAILED, cause);
|
||||
super(SpServerError.SP_ARTIFACT_ENCRYPTION_FAILED, message, cause);
|
||||
this.encryptionOperation = encryptionOperation;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.exception;
|
||||
package org.eclipse.hawkbit.artifact.exception;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
@@ -22,17 +22,7 @@ public final class ArtifactEncryptionUnsupportedException extends AbstractServer
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public ArtifactEncryptionUnsupportedException() {
|
||||
super(SpServerError.SP_ARTIFACT_ENCRYPTION_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message of the error
|
||||
*/
|
||||
public ArtifactEncryptionUnsupportedException(final String message) {
|
||||
super(message, SpServerError.SP_ARTIFACT_ENCRYPTION_NOT_SUPPORTED);
|
||||
super(SpServerError.SP_ARTIFACT_ENCRYPTION_NOT_SUPPORTED, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.exception;
|
||||
package org.eclipse.hawkbit.artifact.exception;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
@@ -19,12 +19,10 @@ public class ArtifactStoreException extends RuntimeException {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Constructs a ArtifactStoreException with message and cause.
|
||||
*
|
||||
* @param message the message of the exception
|
||||
* @param cause of the exception
|
||||
*/
|
||||
public ArtifactStoreException(final String message) {
|
||||
this(message, null);
|
||||
}
|
||||
|
||||
public ArtifactStoreException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.exception;
|
||||
package org.eclipse.hawkbit.artifact.exception;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
@@ -23,34 +23,7 @@ public final class ArtifactUploadFailedException extends AbstractServerRtExcepti
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Creates a new FileUploadFailedException with
|
||||
* {@link SpServerError#SP_ARTIFACT_UPLOAD_FAILED} error.
|
||||
*/
|
||||
public ArtifactUploadFailedException() {
|
||||
super(SpServerError.SP_ARTIFACT_UPLOAD_FAILED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cause for the exception
|
||||
*/
|
||||
public ArtifactUploadFailedException(final Throwable cause) {
|
||||
super(SpServerError.SP_ARTIFACT_UPLOAD_FAILED, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message of the error
|
||||
*/
|
||||
public ArtifactUploadFailedException(final String message) {
|
||||
super(message, SpServerError.SP_ARTIFACT_UPLOAD_FAILED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message for the error
|
||||
* @param cause of the error
|
||||
*/
|
||||
public ArtifactUploadFailedException(final String message, final Throwable cause) {
|
||||
super(message, SpServerError.SP_ARTIFACT_UPLOAD_FAILED, cause);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019 Bosch Software Innovations GmbH and others
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
@@ -7,9 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.exception;
|
||||
|
||||
import static org.eclipse.hawkbit.repository.SizeConversionHelper.byteValueToReadableString;
|
||||
package org.eclipse.hawkbit.artifact.exception;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
@@ -26,28 +24,28 @@ public class FileSizeQuotaExceededException extends AbstractServerRtException {
|
||||
private static final String MAX_ARTIFACT_SIZE_EXCEEDED = "Maximum artifact size (%s) exceeded.";
|
||||
private static final SpServerError errorType = SpServerError.SP_FILE_SIZE_QUOTA_EXCEEDED;
|
||||
|
||||
private static final String KB = "KB";
|
||||
private static final String MB = "MB";
|
||||
|
||||
private final long exceededQuotaValue;
|
||||
|
||||
/**
|
||||
* Creates a new FileSizeQuotaExceededException with a quota value.
|
||||
*
|
||||
* @param exceededQuotaValue Value by how much the quota was exceeded
|
||||
*/
|
||||
public FileSizeQuotaExceededException(final long exceededQuotaValue) {
|
||||
super(createQuotaErrorMessage(exceededQuotaValue), errorType);
|
||||
super(errorType, createQuotaErrorMessage(exceededQuotaValue));
|
||||
this.exceededQuotaValue = exceededQuotaValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a readable string of size quota including unit
|
||||
*
|
||||
* @return file size quota with unit
|
||||
*/
|
||||
public String getExceededQuotaValueString() {
|
||||
return byteValueToReadableString(exceededQuotaValue);
|
||||
}
|
||||
|
||||
private static String createQuotaErrorMessage(final long exceededQuotaValue) {
|
||||
return String.format(MAX_ARTIFACT_SIZE_EXCEEDED, byteValueToReadableString(exceededQuotaValue));
|
||||
}
|
||||
|
||||
static String byteValueToReadableString(final long byteValue) {
|
||||
double outputValue = byteValue / 1024.0;
|
||||
String unit = KB;
|
||||
if (outputValue >= 1024) {
|
||||
outputValue = outputValue / 1024.0;
|
||||
unit = MB;
|
||||
}
|
||||
// We cut decimal places to avoid localization handling
|
||||
return (long) outputValue + " " + unit;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.exception;
|
||||
package org.eclipse.hawkbit.artifact.exception;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
@@ -28,12 +28,6 @@ public class HashNotMatchException extends RuntimeException {
|
||||
|
||||
private final String hashFunction;
|
||||
|
||||
/**
|
||||
* Constructs a HashNotMatchException with message.
|
||||
*
|
||||
* @param message the message of the exception
|
||||
* @param hashFunction the hash function which caused this exception
|
||||
*/
|
||||
public HashNotMatchException(final String message, final String hashFunction) {
|
||||
super(message);
|
||||
this.hashFunction = hashFunction;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019 Bosch Software Innovations GmbH and others
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
@@ -7,9 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.exception;
|
||||
|
||||
import static org.eclipse.hawkbit.repository.SizeConversionHelper.byteValueToReadableString;
|
||||
package org.eclipse.hawkbit.artifact.exception;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
@@ -28,26 +26,12 @@ public class StorageQuotaExceededException extends AbstractServerRtException {
|
||||
|
||||
private final long exceededQuotaValue;
|
||||
|
||||
/**
|
||||
* Creates a new StorageQuotaExceededException with a quota value.
|
||||
*
|
||||
* @param exceededQuotaValue Value by how much the quota was exceeded
|
||||
*/
|
||||
public StorageQuotaExceededException(final long exceededQuotaValue) {
|
||||
super(createQuotaErrorMessage(exceededQuotaValue), errorType);
|
||||
super(errorType, createQuotaErrorMessage(exceededQuotaValue));
|
||||
this.exceededQuotaValue = exceededQuotaValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a readable string of size quota including unit
|
||||
*
|
||||
* @return file size quota with unit
|
||||
*/
|
||||
public String getExceededQuotaValueString() {
|
||||
return byteValueToReadableString(exceededQuotaValue);
|
||||
}
|
||||
|
||||
private static String createQuotaErrorMessage(final long exceededQuotaValue) {
|
||||
return String.format(MAX_ARTIFACT_SIZE_TOTAL_EXCEEDED, byteValueToReadableString(exceededQuotaValue));
|
||||
return String.format(MAX_ARTIFACT_SIZE_TOTAL_EXCEEDED, FileSizeQuotaExceededException.byteValueToReadableString(exceededQuotaValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.artifact.model;
|
||||
|
||||
/**
|
||||
* Representation of artifact hashes.
|
||||
*/
|
||||
public record ArtifactHashes(String sha1, String md5, String sha256) {}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.artifact.model;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@ToString
|
||||
public class ArtifactStream extends InputStream {
|
||||
|
||||
@EqualsAndHashCode.Exclude
|
||||
@ToString.Exclude
|
||||
private final InputStream inputStream;
|
||||
@Getter
|
||||
private final long size;
|
||||
@Getter
|
||||
private final String sha1Hash;
|
||||
|
||||
public ArtifactStream(InputStream inputStream, long size, String sha1Hash) {
|
||||
this.inputStream = inputStream;
|
||||
this.size = size;
|
||||
this.sha1Hash = sha1Hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return inputStream.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(final byte[] b, final int off, final int len) throws IOException {
|
||||
return inputStream.read(b, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
inputStream.close();
|
||||
}
|
||||
}
|
||||
@@ -7,28 +7,25 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.model;
|
||||
package org.eclipse.hawkbit.artifact.model;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Database representation of artifact.
|
||||
* Info for an imported artifact binary.
|
||||
*/
|
||||
@Data
|
||||
public abstract class AbstractDbArtifact implements DbArtifact {
|
||||
public class StoredArtifactInfo {
|
||||
|
||||
private final String artifactId;
|
||||
private final long size;
|
||||
private final String contentType;
|
||||
private final long size;
|
||||
private final ArtifactHashes hashes;
|
||||
|
||||
private DbArtifactHash hashes;
|
||||
|
||||
protected AbstractDbArtifact(final String artifactId, final DbArtifactHash hashes, final long size, final String contentType) {
|
||||
this.artifactId = Objects.requireNonNull(artifactId, "Artifact ID cannot be null");
|
||||
public StoredArtifactInfo(final String contentType, final long size, final ArtifactHashes hashes) {
|
||||
this.hashes = Objects.requireNonNull(hashes, "Hashes cannot be null");
|
||||
this.size = size;
|
||||
this.contentType = contentType;
|
||||
this.size = size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.artifact.urlresolver;
|
||||
|
||||
/**
|
||||
* Container for a generated Artifact URL.
|
||||
*/
|
||||
public record ArtifactUrl(String protocol, String rel, String ref) {}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.artifact.urlresolver;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface declaration of the {@link ArtifactUrlResolver} which generates the URLs to specific artifacts.
|
||||
*/
|
||||
public interface ArtifactUrlResolver {
|
||||
|
||||
/**
|
||||
* hawkBit API type.
|
||||
*/
|
||||
enum ApiType {
|
||||
|
||||
/**
|
||||
* Support for Device Management Federation API.
|
||||
*/
|
||||
DMF,
|
||||
|
||||
/**
|
||||
* Support for Direct Device Integration API.
|
||||
*/
|
||||
DDI,
|
||||
|
||||
/**
|
||||
* Support for Management API.
|
||||
*/
|
||||
MGMT
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a generated download URL for a given artifact parameters for a specific protocol.
|
||||
*
|
||||
* @param downloadDescriptor data for URL generation
|
||||
* @param api given protocol that URL needs to support
|
||||
* @return a URL for the given artifact parameters in a given protocol
|
||||
*/
|
||||
List<ArtifactUrl> getUrls(DownloadDescriptor downloadDescriptor, ApiType api);
|
||||
|
||||
/**
|
||||
* Returns a generated download URL for a given artifact parameters for a specific protocol.
|
||||
*
|
||||
* @param downloadDescriptor data for URL generation
|
||||
* @param api given protocol that URL needs to support
|
||||
* @param requestUri of the request that allows the handler to align the generated URL to the original request.
|
||||
* @return a URL for the given artifact parameters in a given protocol
|
||||
*/
|
||||
List<ArtifactUrl> getUrls(DownloadDescriptor downloadDescriptor, ApiType api, URI requestUri);
|
||||
|
||||
/**
|
||||
* Container for variables available to the {@link ArtifactUrlResolver}.
|
||||
*/
|
||||
record DownloadDescriptor(String tenant, String controllerId, Long softwareModuleId, String filename, String sha1) {}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.artifact.urlresolver;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.urlresolver.PropertyBasedArtifactUrlResolverProperties.UrlProtocol;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Implementation for ArtifactUrlHandler for creating urls to download resource based on patterns configured by
|
||||
* {@link PropertyBasedArtifactUrlResolverProperties}.
|
||||
* <p/>
|
||||
* This mechanism can be used to generate links to arbitrary file hosting infrastructure. However, the hawkBit update server
|
||||
* supports hosting files as well in the following {@link UrlProtocol#getRef()} patterns:
|
||||
* <p/>
|
||||
* Default: </br>
|
||||
* {protocol}://{hostname}:{port}{contextPath}/{tenant}/controller/v1/{controllerId}
|
||||
* /softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}
|
||||
* <p/>
|
||||
* Default (MD5SUM files):
|
||||
* {protocol}://{hostname}:{port}{contextPath}/{tenant}/controller/v1/{controllerId}/
|
||||
* softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}.MD5SUM
|
||||
*/
|
||||
public class PropertyBasedArtifactUrlResolver implements ArtifactUrlResolver {
|
||||
|
||||
private static final String PROTOCOL_PLACEHOLDER = "protocol";
|
||||
private static final String PROTOCOL_REQUEST_PLACEHOLDER = "protocolRequest";
|
||||
private static final String HOSTNAME_PLACEHOLDER = "hostname";
|
||||
private static final String HOSTNAME_REQUEST_PLACEHOLDER = "hostnameRequest";
|
||||
private static final String HOSTNAME_WITH_DOMAIN_REQUEST_PLACEHOLDER = "domainRequest";
|
||||
private static final String IP_PLACEHOLDER = "ip";
|
||||
private static final String PORT_PLACEHOLDER = "port";
|
||||
private static final String PORT_REQUEST_PLACEHOLDER = "portRequest";
|
||||
private static final String TENANT_PLACEHOLDER = "tenant";
|
||||
private static final String CONTEXT_PATH = "contextPath";
|
||||
private static final String CONTROLLER_ID_PLACEHOLDER = "controllerId";
|
||||
private static final String SOFTWARE_MODULE_ID_PLACEHOLDER = "softwareModuleId";
|
||||
private static final String ARTIFACT_FILENAME_PLACEHOLDER = "artifactFileName";
|
||||
private static final String ARTIFACT_SHA1_PLACEHOLDER = "artifactSHA1";
|
||||
// by default, we download via the controller / DDI API download endpoint
|
||||
static final String DEFAULT_URL_PROTOCOL_REF = "{" + PROTOCOL_REQUEST_PLACEHOLDER + "}://{" + HOSTNAME_REQUEST_PLACEHOLDER + "}:{" + PORT_REQUEST_PLACEHOLDER + "}{" + CONTEXT_PATH + "}/{" + TENANT_PLACEHOLDER + "}/controller/v1/{" + CONTROLLER_ID_PLACEHOLDER + "}/softwaremodules/{" + SOFTWARE_MODULE_ID_PLACEHOLDER + "}/artifacts/{" + ARTIFACT_FILENAME_PLACEHOLDER + "}";
|
||||
|
||||
private final PropertyBasedArtifactUrlResolverProperties urlHandlerProperties;
|
||||
private final String contextPath;
|
||||
|
||||
@SuppressWarnings("java:S3358") // better readable this way
|
||||
public PropertyBasedArtifactUrlResolver(final PropertyBasedArtifactUrlResolverProperties urlHandlerProperties, final String contextPath) {
|
||||
this.urlHandlerProperties = urlHandlerProperties;
|
||||
this.contextPath = ObjectUtils.isEmpty(contextPath) || "/".equals(contextPath)
|
||||
? ""
|
||||
: (contextPath.charAt(0) == '/' ? contextPath : '/' + contextPath); // normalize context path
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ArtifactUrl> getUrls(final DownloadDescriptor downloadDescriptor, final ApiType api) {
|
||||
return getUrls(downloadDescriptor, api, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ArtifactUrl> getUrls(final DownloadDescriptor downloadDescriptor, final ApiType api, final URI requestUri) {
|
||||
return urlHandlerProperties.getProtocols().values().stream()
|
||||
.filter(urlProtocol -> urlProtocol.isEnabled() && urlProtocol.getSupports().contains(api))
|
||||
.map(urlProtocol -> new ArtifactUrl(
|
||||
urlProtocol.getProtocol().toUpperCase(), urlProtocol.getRel(),
|
||||
generateUrl(urlProtocol, downloadDescriptor, requestUri)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String generateUrl(final UrlProtocol protocol, final DownloadDescriptor placeholder, final URI requestUri) {
|
||||
final Set<Entry<String, String>> entrySet = getReplaceMap(protocol, placeholder, requestUri).entrySet();
|
||||
String urlPattern = protocol.getRef();
|
||||
for (final Entry<String, String> entry : entrySet) {
|
||||
if (List.of(PORT_PLACEHOLDER, PORT_REQUEST_PLACEHOLDER).contains(entry.getKey())) {
|
||||
urlPattern = urlPattern.replace(":{" + entry.getKey() + "}",
|
||||
ObjectUtils.isEmpty(entry.getValue()) ? "" : (":" + entry.getValue()));
|
||||
} else {
|
||||
if (entry.getValue() != null) {
|
||||
urlPattern = urlPattern.replace("{" + entry.getKey() + "}", entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
return urlPattern;
|
||||
}
|
||||
|
||||
private Map<String, String> getReplaceMap(final UrlProtocol protocol, final DownloadDescriptor placeholder, final URI requestUri) {
|
||||
final Map<String, String> replaceMap = new HashMap<>();
|
||||
|
||||
replaceMap.put(PROTOCOL_PLACEHOLDER, protocol.getProtocol());
|
||||
replaceMap.put(PROTOCOL_REQUEST_PLACEHOLDER, Optional.ofNullable(requestUri).map(URI::getScheme).orElseGet(protocol::getProtocol));
|
||||
|
||||
replaceMap.put(HOSTNAME_PLACEHOLDER, protocol.getHostname());
|
||||
replaceMap.put(IP_PLACEHOLDER, protocol.getIp());
|
||||
replaceMap.put(HOSTNAME_REQUEST_PLACEHOLDER, Optional.ofNullable(requestUri).map(URI::getHost).orElseGet(protocol::getHostname));
|
||||
replaceMap.put(HOSTNAME_WITH_DOMAIN_REQUEST_PLACEHOLDER, computeHostWithRequestDomain(protocol, requestUri));
|
||||
|
||||
replaceMap.put(PORT_PLACEHOLDER, getPort(protocol));
|
||||
replaceMap.put(
|
||||
PORT_REQUEST_PLACEHOLDER,
|
||||
Optional.ofNullable(requestUri)
|
||||
.map(URI::getPort)
|
||||
.map(port -> port > 0 ? String.valueOf(port) : "")
|
||||
.orElseGet(() -> getPort(protocol)));
|
||||
|
||||
replaceMap.put(CONTEXT_PATH, contextPath);
|
||||
|
||||
replaceMap.put(TENANT_PLACEHOLDER, placeholder.tenant());
|
||||
replaceMap.put(CONTROLLER_ID_PLACEHOLDER, placeholder.controllerId());
|
||||
replaceMap.put(SOFTWARE_MODULE_ID_PLACEHOLDER, String.valueOf(placeholder.softwareModuleId()));
|
||||
replaceMap.put(ARTIFACT_FILENAME_PLACEHOLDER, URLEncoder.encode(placeholder.filename(), StandardCharsets.UTF_8));
|
||||
replaceMap.put(ARTIFACT_SHA1_PLACEHOLDER, placeholder.sha1());
|
||||
return replaceMap;
|
||||
}
|
||||
|
||||
private static String getPort(final UrlProtocol protocol) {
|
||||
return ObjectUtils.isEmpty(protocol.getPort()) ? null : String.valueOf(protocol.getPort());
|
||||
}
|
||||
|
||||
private static String computeHostWithRequestDomain(final UrlProtocol protocol, final URI requestUri) {
|
||||
if (requestUri == null) {
|
||||
return protocol.getHostname();
|
||||
}
|
||||
if (!protocol.getHostname().contains(".")) {
|
||||
return protocol.getHostname();
|
||||
}
|
||||
|
||||
final List<String> domainElements = Arrays.asList(StringUtils.delimitedListToStringArray(requestUri.getHost(), "."));
|
||||
if (domainElements.isEmpty()) {
|
||||
return protocol.getHostname();
|
||||
}
|
||||
|
||||
final String domain = StringUtils.collectionToDelimitedString(domainElements.subList(1, domainElements.size()), ".");
|
||||
return StringUtils.delimitedListToStringArray(protocol.getHostname(), ".")[0].trim() + "." + domain;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.urlhandler;
|
||||
package org.eclipse.hawkbit.artifact.urlresolver;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -19,14 +19,13 @@ import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Artifact handler properties class for holding all supported protocols with
|
||||
* host, ip, port and download pattern.
|
||||
* Artifact handler properties class for holding all supported protocols with host, ip, port and download pattern.
|
||||
*
|
||||
* @see PropertyBasedArtifactUrlHandler
|
||||
* @see PropertyBasedArtifactUrlResolver
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties("hawkbit.artifact.url")
|
||||
public class ArtifactUrlHandlerProperties {
|
||||
public class PropertyBasedArtifactUrlResolverProperties {
|
||||
|
||||
/**
|
||||
* Rel as key and complete protocol as value.
|
||||
@@ -53,9 +52,9 @@ public class ArtifactUrlHandlerProperties {
|
||||
|
||||
/**
|
||||
* Hypermedia ref pattern for this protocol. Supported placeholders are the properties
|
||||
* supported by {@link PropertyBasedArtifactUrlHandler}.
|
||||
* supported by {@link PropertyBasedArtifactUrlResolver}.
|
||||
*/
|
||||
private String ref = PropertyBasedArtifactUrlHandler.DEFAULT_URL_PROTOCOL_REF;
|
||||
private String ref = PropertyBasedArtifactUrlResolver.DEFAULT_URL_PROTOCOL_REF;
|
||||
|
||||
/**
|
||||
* Protocol name placeholder that can be used in ref pattern.
|
||||
@@ -70,8 +69,6 @@ public class ArtifactUrlHandlerProperties {
|
||||
/**
|
||||
* IP address placeholder that can be used in ref pattern.
|
||||
*/
|
||||
// Exception squid:S1313 - default only, can be configured
|
||||
@SuppressWarnings("squid:S1313")
|
||||
private String ip = "127.0.0.1";
|
||||
|
||||
/**
|
||||
@@ -82,9 +79,9 @@ public class ArtifactUrlHandlerProperties {
|
||||
/**
|
||||
* Support for the following hawkBit API.
|
||||
*/
|
||||
private List<ApiType> supports = Arrays.asList(ApiType.DDI, ApiType.DMF, ApiType.MGMT);
|
||||
private List<ArtifactUrlResolver.ApiType> supports = Arrays.asList(ArtifactUrlResolver.ApiType.DDI, ArtifactUrlResolver.ApiType.DMF, ArtifactUrlResolver.ApiType.MGMT);
|
||||
|
||||
public void setSupports(final List<ApiType> supports) {
|
||||
public void setSupports(final List<ArtifactUrlResolver.ApiType> supports) {
|
||||
this.supports = Collections.unmodifiableList(supports);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.model;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Interface definition for artifact binary.
|
||||
*/
|
||||
public interface DbArtifact {
|
||||
|
||||
/**
|
||||
* @return ID of the artifact
|
||||
*/
|
||||
String getArtifactId();
|
||||
|
||||
/**
|
||||
* @return hashes of the artifact
|
||||
*/
|
||||
DbArtifactHash getHashes();
|
||||
|
||||
/**
|
||||
* @return size of the artifact in bytes
|
||||
*/
|
||||
long getSize();
|
||||
|
||||
/**
|
||||
* @return content-type if known by the repository or <code>null</code>
|
||||
*/
|
||||
String getContentType();
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
InputStream getFileInputStream();
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Database representation of artifact hash.
|
||||
*/
|
||||
@Data
|
||||
public class DbArtifactHash {
|
||||
|
||||
private final String sha1;
|
||||
private final String md5;
|
||||
private final String sha256;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param sha1 the sha1 hash
|
||||
* @param md5 the md5 hash
|
||||
* @param sha256 the sha256 hash
|
||||
*/
|
||||
public DbArtifactHash(final String sha1, final String md5, final String sha256) {
|
||||
this.sha1 = sha1;
|
||||
this.md5 = md5;
|
||||
this.sha256 = sha256;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.urlhandler;
|
||||
|
||||
/**
|
||||
* hawkBit API type.
|
||||
*/
|
||||
public enum ApiType {
|
||||
|
||||
/**
|
||||
* Support for Device Management Federation API.
|
||||
*/
|
||||
DMF,
|
||||
|
||||
/**
|
||||
* Support for Direct Device Integration API.
|
||||
*/
|
||||
DDI,
|
||||
|
||||
/**
|
||||
* Support for Management API.
|
||||
*/
|
||||
MGMT
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.urlhandler;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Container for a generated Artifact URL.
|
||||
*/
|
||||
@Data
|
||||
public class ArtifactUrl {
|
||||
|
||||
private final String protocol;
|
||||
private final String rel;
|
||||
private final String ref;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param protocol string, e.g. ftp, http, https
|
||||
* @param rel hypermedia value
|
||||
* @param ref hypermedia value
|
||||
*/
|
||||
public ArtifactUrl(final String protocol, final String rel, final String ref) {
|
||||
this.protocol = protocol;
|
||||
this.rel = rel;
|
||||
this.ref = ref;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.urlhandler;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface declaration of the {@link ArtifactUrlHandler} which generates the URLs to specific artifacts.
|
||||
*/
|
||||
public interface ArtifactUrlHandler {
|
||||
|
||||
/**
|
||||
* Returns a generated download URL for a given artifact parameters for a specific protocol.
|
||||
*
|
||||
* @param placeholder data for URL generation
|
||||
* @param api given protocol that URL needs to support
|
||||
* @return a URL for the given artifact parameters in a given protocol
|
||||
*/
|
||||
List<ArtifactUrl> getUrls(URLPlaceholder placeholder, ApiType api);
|
||||
|
||||
/**
|
||||
* Returns a generated download URL for a given artifact parameters for a
|
||||
* specific protocol.
|
||||
*
|
||||
* @param placeholder data for URL generation
|
||||
* @param api given protocol that URL needs to support
|
||||
* @param requestUri of the request that allows the handler to align the generated URL to the original request.
|
||||
* @return a URL for the given artifact parameters in a given protocol
|
||||
*/
|
||||
List<ArtifactUrl> getUrls(URLPlaceholder placeholder, ApiType api, URI requestUri);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.urlhandler;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Utility class for Base10 to Base62 conversion and vice versa. Base62 has the benefit of being shorter in ASCII representation than Base10.
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
final class Base62Util {
|
||||
|
||||
private static final String BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
private static final int BASE62_BASE = BASE62_ALPHABET.length();
|
||||
|
||||
/**
|
||||
* @param l number
|
||||
* @return converted number into Base62 ASCII string
|
||||
*/
|
||||
static String fromBase10(final long l) {
|
||||
if (l == 0) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
long temp = l;
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
|
||||
while (temp > 0) {
|
||||
temp = fromBase10(temp, sb);
|
||||
}
|
||||
return sb.reverse().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param base62 number
|
||||
* @return converted number into Base10
|
||||
*/
|
||||
static Long toBase10(final String base62) {
|
||||
return toBase10(new StringBuilder(base62).reverse().toString().toCharArray());
|
||||
}
|
||||
|
||||
static Long fromBase10(final long l, final StringBuilder sb) {
|
||||
final int rem = (int) (l % BASE62_BASE);
|
||||
sb.append(BASE62_ALPHABET.charAt(rem));
|
||||
return l / BASE62_BASE;
|
||||
}
|
||||
|
||||
private static Long toBase10(final char[] chars) {
|
||||
long base10 = 0L;
|
||||
for (int i = chars.length - 1; i >= 0; i--) {
|
||||
base10 += toBase10(BASE62_ALPHABET.indexOf(chars[i]), i);
|
||||
}
|
||||
return base10;
|
||||
}
|
||||
|
||||
private static int toBase10(final int n, final int pow) {
|
||||
return n * (int) Math.pow(BASE62_BASE, pow);
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.urlhandler;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.hawkbit.repository.artifact.urlhandler.ArtifactUrlHandlerProperties.UrlProtocol;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Implementation for ArtifactUrlHandler for creating urls to download resource based on patterns configured by
|
||||
* {@link ArtifactUrlHandlerProperties}.
|
||||
*
|
||||
* This mechanism can be used to generate links to arbitrary file hosting infrastructure. However, the hawkBit update server
|
||||
* supports hosting files as well in the following {@link UrlProtocol#getRef()} patterns:
|
||||
*
|
||||
* Default:
|
||||
* {protocol}://{hostname}:{port}{contextPath}/{tenant}/controller/v1/{controllerId}
|
||||
* /softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}
|
||||
*
|
||||
* Default (MD5SUM files):
|
||||
* {protocol}://{hostname}:{port}{contextPath}/{tenant}/controller/v1/{controllerId}/
|
||||
* softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}.MD5SUM
|
||||
*/
|
||||
public class PropertyBasedArtifactUrlHandler implements ArtifactUrlHandler {
|
||||
|
||||
private static final String PROTOCOL_PLACEHOLDER = "protocol";
|
||||
private static final String HOSTNAME_PLACEHOLDER = "hostname";
|
||||
private static final String IP_PLACEHOLDER = "ip";
|
||||
private static final String PORT_PLACEHOLDER = "port";
|
||||
private static final String CONTEXT_PATH = "contextPath";
|
||||
private static final String CONTROLLER_ID_PLACEHOLDER = "controllerId";
|
||||
private static final String TARGET_ID_BASE10_PLACEHOLDER = "targetId";
|
||||
private static final String TARGET_ID_BASE62_PLACEHOLDER = "targetIdBase62";
|
||||
private static final String HOSTNAME_REQUEST_PLACEHOLDER = "hostnameRequest";
|
||||
private static final String PORT_REQUEST_PLACEHOLDER = "portRequest";
|
||||
private static final String PROTOCOL_REQUEST_PLACEHOLDER = "protocolRequest";
|
||||
private static final String HOSTNAME_WITH_DOMAIN_REQUEST_PLACEHOLDER = "domainRequest";
|
||||
private static final String ARTIFACT_FILENAME_PLACEHOLDER = "artifactFileName";
|
||||
private static final String ARTIFACT_SHA1_PLACEHOLDER = "artifactSHA1";
|
||||
private static final String ARTIFACT_ID_BASE10_PLACEHOLDER = "artifactId";
|
||||
private static final String ARTIFACT_ID_BASE62_PLACEHOLDER = "artifactIdBase62";
|
||||
private static final String TENANT_PLACEHOLDER = "tenant";
|
||||
private static final String TENANT_ID_BASE10_PLACEHOLDER = "tenantId";
|
||||
private static final String TENANT_ID_BASE62_PLACEHOLDER = "tenantIdBase62";
|
||||
private static final String SOFTWARE_MODULE_ID_BASE10_PLACEHOLDER = "softwareModuleId";
|
||||
private static final String SOFTWARE_MODULE_ID_BASE62_PLACEHOLDER = "softwareModuleIdBase62";
|
||||
static final String DEFAULT_URL_PROTOCOL_REF = "{" + PROTOCOL_PLACEHOLDER + "}://{" + HOSTNAME_PLACEHOLDER + "}:{" + PORT_PLACEHOLDER + "}{" + CONTEXT_PATH + "}/{" + TENANT_PLACEHOLDER + "}/controller/v1/{" + CONTROLLER_ID_PLACEHOLDER + "}/softwaremodules/{" + SOFTWARE_MODULE_ID_BASE10_PLACEHOLDER + "}/artifacts/{" + ARTIFACT_FILENAME_PLACEHOLDER + "}";
|
||||
|
||||
private final ArtifactUrlHandlerProperties urlHandlerProperties;
|
||||
private final String contextPath;
|
||||
|
||||
/**
|
||||
* @param urlHandlerProperties for URL generation configuration
|
||||
*/
|
||||
public PropertyBasedArtifactUrlHandler(final ArtifactUrlHandlerProperties urlHandlerProperties, final String contextPath) {
|
||||
this.urlHandlerProperties = urlHandlerProperties;
|
||||
this.contextPath = contextPath == null || "/".equals(contextPath) ? "" : contextPath; // normalize
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ArtifactUrl> getUrls(final URLPlaceholder placeholder, final ApiType api) {
|
||||
return getUrls(placeholder, api, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ArtifactUrl> getUrls(final URLPlaceholder placeholder, final ApiType api, final URI requestUri) {
|
||||
return urlHandlerProperties.getProtocols().values().stream()
|
||||
.filter(urlProtocol -> urlProtocol.getSupports().contains(api) && urlProtocol.isEnabled())
|
||||
.map(urlProtocol -> new ArtifactUrl(urlProtocol.getProtocol().toUpperCase(), urlProtocol.getRel(),
|
||||
generateUrl(urlProtocol, placeholder, requestUri)))
|
||||
.toList();
|
||||
|
||||
}
|
||||
|
||||
private static String getRequestPort(final UrlProtocol protocol, final URI requestUri) {
|
||||
if (requestUri == null) {
|
||||
return getPort(protocol);
|
||||
}
|
||||
// if port undefined then default protocol port is used
|
||||
return requestUri.getPort() > 0 ? String.valueOf(requestUri.getPort()) : "";
|
||||
}
|
||||
|
||||
private static String getRequestHost(final UrlProtocol protocol, final URI requestUri) {
|
||||
if (requestUri == null) {
|
||||
return protocol.getHostname();
|
||||
}
|
||||
|
||||
return Optional.ofNullable(requestUri.getHost()).orElse(protocol.getHostname());
|
||||
}
|
||||
|
||||
private static String getRequestProtocol(final UrlProtocol protocol, final URI requestUri) {
|
||||
if (requestUri == null) {
|
||||
return protocol.getProtocol();
|
||||
}
|
||||
|
||||
return Optional.ofNullable(requestUri.getScheme()).orElse(protocol.getProtocol());
|
||||
}
|
||||
|
||||
private static String getPort(final UrlProtocol protocol) {
|
||||
return ObjectUtils.isEmpty(protocol.getPort()) ? null : String.valueOf(protocol.getPort());
|
||||
}
|
||||
|
||||
private static String computeHostWithRequestDomain(final UrlProtocol protocol, final URI requestUri) {
|
||||
|
||||
if (requestUri == null) {
|
||||
return protocol.getHostname();
|
||||
}
|
||||
|
||||
if (!protocol.getHostname().contains(".")) {
|
||||
return protocol.getHostname();
|
||||
}
|
||||
|
||||
final String host = StringUtils.delimitedListToStringArray(protocol.getHostname(), ".")[0].trim();
|
||||
|
||||
final List<String> domainElements = Arrays
|
||||
.asList(StringUtils.delimitedListToStringArray(requestUri.getHost(), "."));
|
||||
final String domain = StringUtils.collectionToDelimitedString(domainElements.subList(1, domainElements.size()),
|
||||
".");
|
||||
|
||||
if (ObjectUtils.isEmpty(domain)) {
|
||||
return protocol.getHostname();
|
||||
}
|
||||
|
||||
return host + "." + domain;
|
||||
}
|
||||
|
||||
private String generateUrl(final UrlProtocol protocol, final URLPlaceholder placeholder,
|
||||
final URI requestUri) {
|
||||
final Set<Entry<String, String>> entrySet = getReplaceMap(protocol, placeholder, requestUri).entrySet();
|
||||
|
||||
String urlPattern = protocol.getRef();
|
||||
|
||||
for (final Entry<String, String> entry : entrySet) {
|
||||
if (List.of(PORT_PLACEHOLDER, PORT_REQUEST_PLACEHOLDER).contains(entry.getKey())) {
|
||||
urlPattern = urlPattern.replace(":{" + entry.getKey() + "}",
|
||||
ObjectUtils.isEmpty(entry.getValue()) ? "" : (":" + entry.getValue()));
|
||||
} else {
|
||||
if (entry.getValue() != null) {
|
||||
urlPattern = urlPattern.replace("{" + entry.getKey() + "}", entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return urlPattern;
|
||||
}
|
||||
|
||||
private Map<String, String> getReplaceMap(final UrlProtocol protocol, final URLPlaceholder placeholder,
|
||||
final URI requestUri) {
|
||||
final Map<String, String> replaceMap = new HashMap<>();
|
||||
replaceMap.put(IP_PLACEHOLDER, protocol.getIp());
|
||||
|
||||
replaceMap.put(HOSTNAME_PLACEHOLDER, protocol.getHostname());
|
||||
|
||||
replaceMap.put(HOSTNAME_REQUEST_PLACEHOLDER, getRequestHost(protocol, requestUri));
|
||||
replaceMap.put(PORT_REQUEST_PLACEHOLDER, getRequestPort(protocol, requestUri));
|
||||
replaceMap.put(HOSTNAME_WITH_DOMAIN_REQUEST_PLACEHOLDER, computeHostWithRequestDomain(protocol, requestUri));
|
||||
replaceMap.put(PROTOCOL_REQUEST_PLACEHOLDER, getRequestProtocol(protocol, requestUri));
|
||||
|
||||
replaceMap.put(CONTEXT_PATH, contextPath);
|
||||
|
||||
replaceMap.put(ARTIFACT_FILENAME_PLACEHOLDER,
|
||||
URLEncoder.encode(placeholder.getSoftwareData().getFilename(), StandardCharsets.UTF_8));
|
||||
|
||||
replaceMap.put(ARTIFACT_SHA1_PLACEHOLDER, placeholder.getSoftwareData().getSha1Hash());
|
||||
replaceMap.put(PROTOCOL_PLACEHOLDER, protocol.getProtocol());
|
||||
replaceMap.put(PORT_PLACEHOLDER, getPort(protocol));
|
||||
replaceMap.put(TENANT_PLACEHOLDER, placeholder.getTenant());
|
||||
replaceMap.put(TENANT_ID_BASE10_PLACEHOLDER, String.valueOf(placeholder.getTenantId()));
|
||||
replaceMap.put(TENANT_ID_BASE62_PLACEHOLDER, Base62Util.fromBase10(placeholder.getTenantId()));
|
||||
replaceMap.put(CONTROLLER_ID_PLACEHOLDER, placeholder.getControllerId());
|
||||
replaceMap.put(TARGET_ID_BASE10_PLACEHOLDER, String.valueOf(placeholder.getTargetId()));
|
||||
if (placeholder.getTargetId() != null) {
|
||||
replaceMap.put(TARGET_ID_BASE62_PLACEHOLDER, Base62Util.fromBase10(placeholder.getTargetId()));
|
||||
}
|
||||
replaceMap.put(ARTIFACT_ID_BASE62_PLACEHOLDER,
|
||||
Base62Util.fromBase10(placeholder.getSoftwareData().getArtifactId()));
|
||||
replaceMap.put(ARTIFACT_ID_BASE10_PLACEHOLDER, String.valueOf(placeholder.getSoftwareData().getArtifactId()));
|
||||
replaceMap.put(SOFTWARE_MODULE_ID_BASE10_PLACEHOLDER,
|
||||
String.valueOf(placeholder.getSoftwareData().getSoftwareModuleId()));
|
||||
replaceMap.put(SOFTWARE_MODULE_ID_BASE62_PLACEHOLDER,
|
||||
Base62Util.fromBase10(placeholder.getSoftwareData().getSoftwareModuleId()));
|
||||
return replaceMap;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.urlhandler;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Container for variables available to the {@link ArtifactUrlHandler}.
|
||||
*/
|
||||
@Data
|
||||
public class URLPlaceholder {
|
||||
|
||||
private final String tenant;
|
||||
private final Long tenantId;
|
||||
private final String controllerId;
|
||||
private final Long targetId;
|
||||
private final SoftwareData softwareData;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param tenant of the client
|
||||
* @param tenantId of teh tenant
|
||||
* @param controllerId of the target
|
||||
* @param targetId of the target
|
||||
* @param softwareData information about the artifact and software module that can be accessed by the URL.
|
||||
*/
|
||||
public URLPlaceholder(final String tenant, final Long tenantId, final String controllerId, final Long targetId,
|
||||
final SoftwareData softwareData) {
|
||||
this.tenant = tenant;
|
||||
this.tenantId = tenantId;
|
||||
this.controllerId = controllerId;
|
||||
this.targetId = targetId;
|
||||
this.softwareData = softwareData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the artifact and software module that can be accessed
|
||||
* by the URL.
|
||||
*/
|
||||
@Data
|
||||
public static class SoftwareData {
|
||||
|
||||
private Long softwareModuleId;
|
||||
private String filename;
|
||||
private Long artifactId;
|
||||
private String sha1Hash;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param softwareModuleId of the module the artifact belongs to
|
||||
* @param filename of the artifact
|
||||
* @param artifactId of the artifact
|
||||
* @param sha1Hash of the artifact
|
||||
*/
|
||||
public SoftwareData(final Long softwareModuleId, final String filename, final Long artifactId, final String sha1Hash) {
|
||||
this.softwareModuleId = softwareModuleId;
|
||||
this.filename = filename;
|
||||
this.artifactId = artifactId;
|
||||
this.sha1Hash = sha1Hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,12 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.encryption;
|
||||
package org.eclipse.hawkbit.artifact.encryption;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import org.eclipse.hawkbit.repository.artifact.exception.ArtifactEncryptionUnsupportedException;
|
||||
import org.eclipse.hawkbit.artifact.exception.ArtifactEncryptionUnsupportedException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.urlhandler;
|
||||
package org.eclipse.hawkbit.artifact.urlresolver;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -15,8 +15,8 @@ import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.hawkbit.repository.artifact.urlhandler.ArtifactUrlHandlerProperties.UrlProtocol;
|
||||
import org.eclipse.hawkbit.repository.artifact.urlhandler.URLPlaceholder.SoftwareData;
|
||||
import org.eclipse.hawkbit.artifact.urlresolver.ArtifactUrlResolver.DownloadDescriptor;
|
||||
import org.eclipse.hawkbit.artifact.urlresolver.PropertyBasedArtifactUrlResolverProperties.UrlProtocol;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -29,55 +29,44 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
* Story: Test to generate the artifact download URL
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PropertyBasedArtifactUrlHandlerTest {
|
||||
class PropertyBasedArtifactUrlResolverTest {
|
||||
|
||||
private static final String TEST_PROTO = "coap";
|
||||
private static final String TEST_PROTO = "https";
|
||||
private static final String TEST_REL = "download-udp";
|
||||
|
||||
private static final long TENANT_ID = 456789L;
|
||||
private static final String CONTROLLER_ID = "Test";
|
||||
private static final String FILENAME_DECODE = "test123!§$%&";
|
||||
private static final String FILENAME_ENCODE = "test123%21%C2%A7%24%25%26";
|
||||
private static final long SOFTWARE_MODULE_ID = 87654L;
|
||||
private static final long TARGET_ID = 3474366L;
|
||||
private static final String TARGET_ID_BASE62 = "EZqA";
|
||||
private static final String SHA1HASH = "test12345";
|
||||
private static final long ARTIFACT_ID = 1345678L;
|
||||
private static final String ARTIFACT_ID_BASE62 = "5e4U";
|
||||
private static final String TENANT = "TEST_TENANT";
|
||||
private static final String CONTROLLER_ID = "Test";
|
||||
private static final long SOFTWARE_MODULE_ID = 87654L;
|
||||
private static final String FILENAME = "test123!§$%&";
|
||||
private static final String FILENAME_ENCODED = "test123%21%C2%A7%24%25%26";
|
||||
private static final String SHA1 = "0123456789012345678901234567890123456789";
|
||||
|
||||
private static final String HTTP_LOCALHOST = "http://localhost:8080/";
|
||||
private static final URLPlaceholder placeHolder = new URLPlaceholder(
|
||||
TENANT, TENANT_ID, CONTROLLER_ID, TARGET_ID,
|
||||
new SoftwareData(SOFTWARE_MODULE_ID, FILENAME_DECODE, ARTIFACT_ID, SHA1HASH));
|
||||
private ArtifactUrlHandler urlHandlerUnderTest;
|
||||
private ArtifactUrlHandlerProperties properties;
|
||||
private static final DownloadDescriptor DOWNLOAD_DESCRIPTOR = new DownloadDescriptor(TENANT, CONTROLLER_ID, SOFTWARE_MODULE_ID, FILENAME,
|
||||
SHA1);
|
||||
|
||||
private ArtifactUrlResolver urlHandlerUnderTest;
|
||||
private PropertyBasedArtifactUrlResolverProperties properties;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
properties = new ArtifactUrlHandlerProperties();
|
||||
urlHandlerUnderTest = new PropertyBasedArtifactUrlHandler(properties, "");
|
||||
properties = new PropertyBasedArtifactUrlResolverProperties();
|
||||
urlHandlerUnderTest = new PropertyBasedArtifactUrlResolver(properties, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the generation of http download url.
|
||||
*/
|
||||
@Test
|
||||
void urlGenerationWithDefaultConfiguration() {
|
||||
properties.getProtocols().put("download-http", new UrlProtocol());
|
||||
|
||||
final List<ArtifactUrl> ddiUrls = urlHandlerUnderTest.getUrls(placeHolder, ApiType.DDI);
|
||||
final List<ArtifactUrl> ddiUrls = urlHandlerUnderTest.getUrls(DOWNLOAD_DESCRIPTOR, ArtifactUrlResolver.ApiType.DDI);
|
||||
assertThat(ddiUrls).containsExactly(
|
||||
new ArtifactUrl(
|
||||
"http".toUpperCase(), "download-http",
|
||||
HTTP_LOCALHOST + TENANT + "/controller/v1/" +
|
||||
CONTROLLER_ID + "/softwaremodules/" + SOFTWARE_MODULE_ID + "/artifacts/" + FILENAME_ENCODE))
|
||||
.isEqualTo(urlHandlerUnderTest.getUrls(placeHolder, ApiType.DMF));
|
||||
new ArtifactUrl(
|
||||
"http".toUpperCase(), "download-http",
|
||||
HTTP_LOCALHOST + TENANT + "/controller/v1/" +
|
||||
CONTROLLER_ID + "/softwaremodules/" + SOFTWARE_MODULE_ID + "/artifacts/" + FILENAME_ENCODED))
|
||||
.isEqualTo(urlHandlerUnderTest.getUrls(DOWNLOAD_DESCRIPTOR, ArtifactUrlResolver.ApiType.DMF));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the generation of custom download url with a CoAP example that supports DMF only.
|
||||
*/
|
||||
@Test
|
||||
void urlGenerationWithCustomConfiguration() {
|
||||
final UrlProtocol proto = new UrlProtocol();
|
||||
@@ -85,41 +74,17 @@ class PropertyBasedArtifactUrlHandlerTest {
|
||||
proto.setPort(5683);
|
||||
proto.setProtocol(TEST_PROTO);
|
||||
proto.setRel(TEST_REL);
|
||||
proto.setSupports(List.of(ApiType.DMF));
|
||||
proto.setRef("{protocol}://{ip}:{port}/fw/{tenant}/{controllerId}/sha1/{artifactSHA1}");
|
||||
proto.setSupports(List.of(ArtifactUrlResolver.ApiType.DMF));
|
||||
proto.setRef("{protocol}://{ip}:{port}/fw/{tenant}/{controllerId}/sha1/{artifactFileName}");
|
||||
properties.getProtocols().put(TEST_PROTO, proto);
|
||||
|
||||
assertThat(urlHandlerUnderTest.getUrls(placeHolder, ApiType.DDI)).isEmpty();
|
||||
assertThat(urlHandlerUnderTest.getUrls(placeHolder, ApiType.DMF)).containsExactly(
|
||||
assertThat(urlHandlerUnderTest.getUrls(DOWNLOAD_DESCRIPTOR, ArtifactUrlResolver.ApiType.DDI)).isEmpty();
|
||||
assertThat(urlHandlerUnderTest.getUrls(DOWNLOAD_DESCRIPTOR, ArtifactUrlResolver.ApiType.DMF)).containsExactly(
|
||||
new ArtifactUrl(
|
||||
TEST_PROTO.toUpperCase(), TEST_REL,
|
||||
"coap://127.0.0.1:5683/fw/" + TENANT + "/" + CONTROLLER_ID + "/sha1/" + SHA1HASH));
|
||||
TEST_PROTO + "://127.0.0.1:5683/fw/" + TENANT + "/" + CONTROLLER_ID + "/sha1/" + FILENAME_ENCODED));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the generation of custom download url using Base62 references with a CoAP example that supports DMF only.
|
||||
*/
|
||||
@Test
|
||||
void urlGenerationWithCustomShortConfiguration() {
|
||||
final UrlProtocol proto = new UrlProtocol();
|
||||
proto.setIp("127.0.0.1");
|
||||
proto.setPort(5683);
|
||||
proto.setProtocol(TEST_PROTO);
|
||||
proto.setRel(TEST_REL);
|
||||
proto.setSupports(List.of(ApiType.DMF));
|
||||
proto.setRef("{protocol}://{ip}:{port}/fws/{tenant}/{targetIdBase62}/{artifactIdBase62}");
|
||||
properties.getProtocols().put("ftp", proto);
|
||||
|
||||
assertThat(urlHandlerUnderTest.getUrls(placeHolder, ApiType.DDI)).isEmpty();
|
||||
assertThat(urlHandlerUnderTest.getUrls(placeHolder, ApiType.DMF)).containsExactly(
|
||||
new ArtifactUrl(
|
||||
TEST_PROTO.toUpperCase(), TEST_REL,
|
||||
TEST_PROTO + "://127.0.0.1:5683/fws/" + TENANT + "/" + TARGET_ID_BASE62 + "/" + ARTIFACT_ID_BASE62));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the full qualified host of the statically defined hostname is replaced with the host of the request.
|
||||
*/
|
||||
@Test
|
||||
void urlGenerationWithHostFromRequest() throws URISyntaxException {
|
||||
final String testHost = "ddi.host.com";
|
||||
@@ -129,14 +94,15 @@ class PropertyBasedArtifactUrlHandlerTest {
|
||||
proto.setPort(5683);
|
||||
proto.setProtocol(TEST_PROTO);
|
||||
proto.setRel(TEST_REL);
|
||||
proto.setSupports(List.of(ApiType.DDI));
|
||||
proto.setRef("{protocol}://{hostnameRequest}:{port}/fws/{tenant}/{targetIdBase62}/{artifactIdBase62}");
|
||||
proto.setSupports(List.of(ArtifactUrlResolver.ApiType.DDI));
|
||||
proto.setRef("{protocol}://{hostnameRequest}:{port}/fws/{tenant}/{controllerId}/{artifactFileName}");
|
||||
properties.getProtocols().put("ftp", proto);
|
||||
|
||||
assertThat(urlHandlerUnderTest.getUrls(placeHolder, ApiType.DDI, new URI("https://" + testHost))).containsExactly(
|
||||
assertThat(urlHandlerUnderTest.getUrls(DOWNLOAD_DESCRIPTOR, ArtifactUrlResolver.ApiType.DDI,
|
||||
new URI("https://" + testHost))).containsExactly(
|
||||
new ArtifactUrl(
|
||||
TEST_PROTO.toUpperCase(), TEST_REL,
|
||||
TEST_PROTO + "://" + testHost + ":5683/fws/" + TENANT + "/" + TARGET_ID_BASE62 + "/" + ARTIFACT_ID_BASE62));
|
||||
TEST_PROTO + "://" + testHost + ":5683/fws/" + TENANT + "/" + CONTROLLER_ID + "/" + FILENAME_ENCODED));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,13 +113,14 @@ class PropertyBasedArtifactUrlHandlerTest {
|
||||
final String testHost = "ddi.host.com";
|
||||
|
||||
final UrlProtocol proto = new UrlProtocol();
|
||||
proto.setRef("{protocolRequest}://{hostname}:{port}/fws/{tenant}/{targetIdBase62}/{artifactIdBase62}");
|
||||
proto.setRef("{protocolRequest}://{hostname}:{port}/fws/{tenant}/{controllerId}/{artifactFileName}");
|
||||
properties.getProtocols().put("download-http", proto);
|
||||
|
||||
assertThat(urlHandlerUnderTest.getUrls(placeHolder, ApiType.DDI, new URI("https://" + testHost))).containsExactly(
|
||||
assertThat(urlHandlerUnderTest.getUrls(DOWNLOAD_DESCRIPTOR, ArtifactUrlResolver.ApiType.DDI,
|
||||
new URI("https://" + testHost))).containsExactly(
|
||||
new ArtifactUrl(
|
||||
"http".toUpperCase(), "download-http",
|
||||
"https://localhost:8080/fws/" + TENANT + "/" + TARGET_ID_BASE62 + "/" + ARTIFACT_ID_BASE62));
|
||||
"https://localhost:8080/fws/" + TENANT + "/" + CONTROLLER_ID + "/" + FILENAME_ENCODED));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,21 +129,23 @@ class PropertyBasedArtifactUrlHandlerTest {
|
||||
@Test
|
||||
void urlGenerationWithPortFromRequest() throws URISyntaxException {
|
||||
final UrlProtocol proto = new UrlProtocol();
|
||||
proto.setRef("{protocol}://{hostname}:{portRequest}/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}");
|
||||
proto.setRef(
|
||||
"{protocol}://{hostname}:{portRequest}/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}");
|
||||
|
||||
properties.getProtocols().put("download-http", proto);
|
||||
|
||||
assertThat(urlHandlerUnderTest.getUrls(placeHolder, ApiType.DDI, new URI("http://anotherHost.com:8083"))).containsExactly(
|
||||
assertThat(urlHandlerUnderTest.getUrls(DOWNLOAD_DESCRIPTOR, ArtifactUrlResolver.ApiType.DDI,
|
||||
new URI("http://anotherHost.com:8083"))).containsExactly(
|
||||
new ArtifactUrl(
|
||||
"http".toUpperCase(), "download-http",
|
||||
"http://localhost:8083/" + TENANT + "/controller/v1/" +
|
||||
CONTROLLER_ID + "/softwaremodules/" + SOFTWARE_MODULE_ID + "/artifacts/" + FILENAME_ENCODE));
|
||||
CONTROLLER_ID + "/softwaremodules/" + SOFTWARE_MODULE_ID + "/artifacts/" + FILENAME_ENCODED));
|
||||
|
||||
assertThat(urlHandlerUnderTest.getUrls(placeHolder, ApiType.DMF)).containsExactly(
|
||||
assertThat(urlHandlerUnderTest.getUrls(DOWNLOAD_DESCRIPTOR, ArtifactUrlResolver.ApiType.DMF)).containsExactly(
|
||||
new ArtifactUrl(
|
||||
"http".toUpperCase(), "download-http",
|
||||
"http://localhost:8080/" + TENANT + "/controller/v1/" +
|
||||
CONTROLLER_ID + "/softwaremodules/" + SOFTWARE_MODULE_ID + "/artifacts/" + FILENAME_ENCODE));
|
||||
CONTROLLER_ID + "/softwaremodules/" + SOFTWARE_MODULE_ID + "/artifacts/" + FILENAME_ENCODED));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,17 +155,16 @@ class PropertyBasedArtifactUrlHandlerTest {
|
||||
void urlGenerationWithPortFromRequestForHttps() throws URISyntaxException {
|
||||
final String protocol = "https";
|
||||
final UrlProtocol proto = new UrlProtocol();
|
||||
proto.setRef("{protocolRequest}://{hostnameRequest}:{portRequest}/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}");
|
||||
proto.setRef(
|
||||
"{protocolRequest}://{hostnameRequest}:{portRequest}/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}");
|
||||
proto.setProtocol(protocol);
|
||||
properties.getProtocols().put("download-http", proto);
|
||||
|
||||
final URI uri = new URI(protocol + "://anotherHost.com");
|
||||
assertThat(urlHandlerUnderTest.getUrls(placeHolder, ApiType.DDI, uri)).containsExactly(
|
||||
assertThat(urlHandlerUnderTest.getUrls(DOWNLOAD_DESCRIPTOR, ArtifactUrlResolver.ApiType.DDI, uri)).containsExactly(
|
||||
new ArtifactUrl(
|
||||
protocol.toUpperCase(), "download-http",
|
||||
uri + "/" + TENANT + "/controller/v1/" +
|
||||
CONTROLLER_ID + "/softwaremodules/" + SOFTWARE_MODULE_ID + "/artifacts/" + FILENAME_ENCODE));
|
||||
|
||||
uri + "/" + TENANT + "/controller/v1/" + CONTROLLER_ID + "/softwaremodules/" + SOFTWARE_MODULE_ID + "/artifacts/" + FILENAME_ENCODED));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,20 +174,22 @@ class PropertyBasedArtifactUrlHandlerTest {
|
||||
void urlGenerationWithDomainFromRequest() throws URISyntaxException {
|
||||
final UrlProtocol proto = new UrlProtocol();
|
||||
proto.setHostname("host.bumlux.net");
|
||||
proto.setRef("{protocol}://{domainRequest}/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}");
|
||||
proto.setRef(
|
||||
"{protocol}://{domainRequest}/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}");
|
||||
|
||||
properties.getProtocols().put("download-http", proto);
|
||||
|
||||
assertThat(urlHandlerUnderTest.getUrls(placeHolder, ApiType.DDI, new URI("http://anotherHost.com:8083"))).containsExactly(
|
||||
assertThat(urlHandlerUnderTest.getUrls(DOWNLOAD_DESCRIPTOR, ArtifactUrlResolver.ApiType.DDI,
|
||||
new URI("http://anotherHost.com:8083"))).containsExactly(
|
||||
new ArtifactUrl(
|
||||
"http".toUpperCase(), "download-http",
|
||||
"http://host.com/" + TENANT + "/controller/v1/" +
|
||||
CONTROLLER_ID + "/softwaremodules/" + SOFTWARE_MODULE_ID + "/artifacts/" + FILENAME_ENCODE));
|
||||
CONTROLLER_ID + "/softwaremodules/" + SOFTWARE_MODULE_ID + "/artifacts/" + FILENAME_ENCODED));
|
||||
|
||||
assertThat(urlHandlerUnderTest.getUrls(placeHolder, ApiType.DMF)).containsExactly(
|
||||
assertThat(urlHandlerUnderTest.getUrls(DOWNLOAD_DESCRIPTOR, ArtifactUrlResolver.ApiType.DMF)).containsExactly(
|
||||
new ArtifactUrl(
|
||||
"http".toUpperCase(), "download-http",
|
||||
"http://host.bumlux.net/" + TENANT + "/controller/v1/" +
|
||||
CONTROLLER_ID + "/softwaremodules/" + SOFTWARE_MODULE_ID + "/artifacts/" + FILENAME_ENCODE));
|
||||
CONTROLLER_ID + "/softwaremodules/" + SOFTWARE_MODULE_ID + "/artifacts/" + FILENAME_ENCODED));
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.urlhandler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Feature: Unit Tests - Artifact URL Handler<br/>
|
||||
* Story: Base62 Utility tests
|
||||
*/
|
||||
class Base62UtilTest {
|
||||
|
||||
/**
|
||||
* Convert Base10 numbers to Base62 ASCII strings.
|
||||
*/
|
||||
@Test
|
||||
void fromBase10() {
|
||||
assertThat(Base62Util.fromBase10(0L)).isEqualTo("0");
|
||||
assertThat(Base62Util.fromBase10(11L)).isEqualTo("B");
|
||||
assertThat(Base62Util.fromBase10(36L)).isEqualTo("a");
|
||||
assertThat(Base62Util.fromBase10(999L)).isEqualTo("G7");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Base62 ASCII strings to Base10 numbers.
|
||||
*/
|
||||
@Test
|
||||
void toBase10() {
|
||||
assertThat(Base62Util.toBase10("0")).isZero();
|
||||
assertThat(Base62Util.toBase10("B")).isEqualTo(11);
|
||||
assertThat(Base62Util.toBase10("a")).isEqualTo(36L);
|
||||
assertThat(Base62Util.toBase10("G7")).isEqualTo(999L);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact.urlhandler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Feature: Unit Tests - Artifact URL Handler<br/>
|
||||
* Story: URL placeholder tests
|
||||
*/
|
||||
class URLPlaceholderTest {
|
||||
|
||||
private final URLPlaceholder.SoftwareData softwareData;
|
||||
private final URLPlaceholder placeholder;
|
||||
|
||||
public URLPlaceholderTest() {
|
||||
this.softwareData = new URLPlaceholder.SoftwareData(1L, "file.txt", 123L, "someHash123");
|
||||
this.placeholder = new URLPlaceholder("SuperCorp", 123L, "Super-1", 1L, softwareData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same object should be equal
|
||||
*/
|
||||
@Test
|
||||
// Exception squid:S5785 - JUnit assertTrue/assertFalse should be simplified to the corresponding dedicated assertion
|
||||
// Need to test the equals method and need to bypass magic logic in utility classes
|
||||
@SuppressWarnings({ "squid:S5838" })
|
||||
void sameObjectShouldBeEqual() {
|
||||
assertThat(softwareData.equals(softwareData)).isTrue();
|
||||
assertThat(placeholder.equals(placeholder)).isTrue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Different object should not be equal
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings({ "squid:S5838" })
|
||||
void differentObjectShouldNotBeEqual() {
|
||||
final URLPlaceholder.SoftwareData softwareData2 = new URLPlaceholder.SoftwareData(2L, "file.txt", 123L, "someHash123");
|
||||
final URLPlaceholder placeholder2 = new URLPlaceholder("SuperCorp", 123L, "Super-2", 2L, softwareData2);
|
||||
final URLPlaceholder placeholderWithOtherSoftwareData = new URLPlaceholder(placeholder.getTenant(),
|
||||
placeholder.getTenantId(), placeholder.getControllerId(), placeholder.getTargetId(), softwareData2);
|
||||
assertThat(placeholder.equals(placeholder2)).isFalse();
|
||||
assertThat(placeholder2.equals(placeholder)).isFalse();
|
||||
assertThat(softwareData.equals(softwareData2)).isFalse();
|
||||
assertThat(softwareData2.equals(softwareData)).isFalse();
|
||||
assertThat(placeholder.equals(placeholderWithOtherSoftwareData)).isFalse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Different objects with same properties should be equal
|
||||
*/
|
||||
@Test
|
||||
void differentObjectsWithSamePropertiesShouldBeEqual() {
|
||||
final URLPlaceholder placeholderWithSameProperties = new URLPlaceholder(placeholder.getTenant(), placeholder.getTenantId(),
|
||||
placeholder.getControllerId(), placeholder.getTargetId(), softwareData);
|
||||
assertThat(placeholder).isEqualTo(placeholderWithSameProperties);
|
||||
assertThat(placeholderWithSameProperties).isEqualTo(placeholder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should not equal null
|
||||
*/
|
||||
@Test
|
||||
// Exception squid:S5785 - JUnit assertTrue/assertFalse should be simplified to
|
||||
// the corresponding dedicated assertion
|
||||
// Need to test the equals method and need to bypass magic logic in utility
|
||||
// classes
|
||||
@SuppressWarnings({ "squid:S5838" })
|
||||
void shouldNotEqualNull() {
|
||||
assertThat(placeholder.equals(null)).isFalse();
|
||||
assertThat(softwareData.equals(null)).isFalse();
|
||||
}
|
||||
|
||||
/**
|
||||
* HashCode should not change
|
||||
*/
|
||||
@Test
|
||||
void hashCodeShouldNotChange() {
|
||||
final URLPlaceholder placeholderWithSameProperties = new URLPlaceholder(placeholder.getTenant(), placeholder.getTenantId(),
|
||||
placeholder.getControllerId(), placeholder.getTargetId(), softwareData);
|
||||
assertThat(placeholder).hasSameHashCodeAs(placeholder).hasSameHashCodeAs(placeholderWithSameProperties);
|
||||
}
|
||||
}
|
||||
8
hawkbit-artifact/hawkbit-artifact-fs/README.md
Normal file
8
hawkbit-artifact/hawkbit-artifact-fs/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
Eclipse.IoT hawkBit - Artifact Repository File System
|
||||
===
|
||||
This module contains the implementation of [ArtifactStorage](../hawkbit-artifact-api/src/main/java/org/eclipse/hawkbit/artifact/ArtifactStorage.java) based on the file-system.
|
||||
It's a very convenient and easy implementation of storing the artifact binaries into the file-system based on the SHA-1 hash naming.
|
||||
|
||||
Due to the limit of many file-systems of files within one directory, the files
|
||||
are stored in different sub-directories based on the last four digits of the
|
||||
SHA1-hash `/basepath/[two digit sha1]/[two digit sha1/sha1-hash-filename]`.
|
||||
@@ -18,7 +18,7 @@
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hawkbit-artifact-repository-filesystem</artifactId>
|
||||
<artifactId>hawkbit-artifact-fs</artifactId>
|
||||
<name>hawkBit :: Artifact :: Filesystem Artifact Repository</name>
|
||||
|
||||
<dependencies>
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact;
|
||||
package org.eclipse.hawkbit.artifact.fs;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -17,7 +17,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties("org.eclipse.hawkbit.repository.file")
|
||||
public class ArtifactFilesystemProperties {
|
||||
public class FileArtifactProperties {
|
||||
|
||||
/**
|
||||
* The base-path of the directory to store the artifacts.
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.artifact.fs;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.eclipse.hawkbit.artifact.AbstractArtifactStorage;
|
||||
import org.eclipse.hawkbit.artifact.ArtifactStorage;
|
||||
import org.eclipse.hawkbit.artifact.exception.ArtifactBinaryNotFoundException;
|
||||
import org.eclipse.hawkbit.artifact.exception.ArtifactStoreException;
|
||||
import org.eclipse.hawkbit.artifact.model.ArtifactHashes;
|
||||
import org.eclipse.hawkbit.artifact.model.StoredArtifactInfo;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link ArtifactStorage} to store artifacts on the file-system. The files are stored by their SHA1 hash of the
|
||||
* artifact binary. Duplicate files with the same SHA1 hash will only be stored once.
|
||||
* <p/>
|
||||
* All files are stored flat in one base directory configured in the {@link FileArtifactProperties#getPath()}.
|
||||
* <p/>
|
||||
* Due to the limit of many file-systems of files within one directory, the files are stored in different subdirectories based on the last four
|
||||
* digits of the SHA1-hash {@code (/basepath/[two digit sha1]/[two digit sha1])}.
|
||||
*/
|
||||
@Validated
|
||||
public class FileArtifactStorage extends AbstractArtifactStorage {
|
||||
|
||||
private final FileArtifactProperties artifactResourceProperties;
|
||||
|
||||
public FileArtifactStorage(final FileArtifactProperties artifactResourceProperties) {
|
||||
this.artifactResourceProperties = artifactResourceProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteBySha1(final String tenant, final String sha1) {
|
||||
FileUtils.deleteQuietly(getFile(tenant, sha1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getBySha1(final String tenant, final String sha1) {
|
||||
final File file = getFile(tenant, sha1);
|
||||
if (!file.exists()) {
|
||||
throw new ArtifactBinaryNotFoundException(sha1);
|
||||
}
|
||||
try {
|
||||
return new BufferedInputStream(new FileInputStream(file));
|
||||
} catch (final FileNotFoundException e) {
|
||||
throw new ArtifactBinaryNotFoundException(sha1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByTenant(final String tenant) {
|
||||
FileUtils.deleteQuietly(Paths.get(artifactResourceProperties.getPath(), sanitizeTenant(tenant)).toFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsBySha1(final String tenant, final String sha1) {
|
||||
return getFile(tenant, sha1).exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StoredArtifactInfo store(final String tenant, final ArtifactHashes base16Hashes, final String contentType, final String tempFile)
|
||||
throws IOException {
|
||||
final File file = new File(tempFile);
|
||||
final File fileSHA1Naming = getFile(tenant, base16Hashes.sha1());
|
||||
if (fileSHA1Naming.exists()) {
|
||||
FileUtils.deleteQuietly(file);
|
||||
} else {
|
||||
Files.move(file.toPath(), fileSHA1Naming.toPath());
|
||||
}
|
||||
|
||||
return new StoredArtifactInfo(contentType, fileSHA1Naming.length(), base16Hashes);
|
||||
}
|
||||
|
||||
private File getFile(final String tenant, final String sha1) {
|
||||
// ensure that the sha1 is not a path traversal attack
|
||||
if (sha1.indexOf('/') >= 0 || sha1.indexOf('\\') >= 0) {
|
||||
throw new IllegalArgumentException("Invalid SHA-1 hash: " + sha1);
|
||||
}
|
||||
|
||||
final File artifactDirectory = getSha1DirectoryPath(tenant, sha1).toFile();
|
||||
if (!artifactDirectory.isDirectory()) {
|
||||
if (artifactDirectory.isFile()) {
|
||||
throw new ArtifactStoreException(artifactDirectory + " is a file, but a directory is required.");
|
||||
} else if (!artifactDirectory.mkdirs()) {
|
||||
throw new ArtifactStoreException("Fail to create directories: " + artifactDirectory);
|
||||
}
|
||||
}
|
||||
return new File(artifactDirectory, sha1);
|
||||
}
|
||||
|
||||
private Path getSha1DirectoryPath(final String tenant, final String sha1) {
|
||||
final int length = sha1.length();
|
||||
final String folder1 = sha1.substring(length - 4, length - 2);
|
||||
final String folder2 = sha1.substring(length - 2, length);
|
||||
return Paths.get(artifactResourceProperties.getPath(), sanitizeTenant(tenant), folder1, folder2);
|
||||
}
|
||||
}
|
||||
@@ -7,30 +7,30 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.autoconfigure.repository.artifact.filesystem;
|
||||
package org.eclipse.hawkbit.autoconfigure.artifact.fs;
|
||||
|
||||
import org.eclipse.hawkbit.repository.artifact.ArtifactFilesystemProperties;
|
||||
import org.eclipse.hawkbit.repository.artifact.ArtifactFilesystemRepository;
|
||||
import org.eclipse.hawkbit.repository.artifact.ArtifactRepository;
|
||||
import org.eclipse.hawkbit.artifact.ArtifactStorage;
|
||||
import org.eclipse.hawkbit.artifact.fs.FileArtifactProperties;
|
||||
import org.eclipse.hawkbit.artifact.fs.FileArtifactStorage;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Configuration for the {@link ArtifactFilesystemRepository}.
|
||||
* Configuration for the {@link FileArtifactStorage}.
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(ArtifactFilesystemProperties.class)
|
||||
public class ArtifactFilesystemConfiguration {
|
||||
@EnableConfigurationProperties(FileArtifactProperties.class)
|
||||
public class FileArtifactStorageConfiguration {
|
||||
|
||||
/**
|
||||
* @param artifactFilesystemProperties the artifact file system properties
|
||||
* @return Default {@link ArtifactRepository} implementation.
|
||||
* @return Default {@link ArtifactStorage} implementation.
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ArtifactRepository artifactRepository(final ArtifactFilesystemProperties artifactFilesystemProperties) {
|
||||
return new ArtifactFilesystemRepository(artifactFilesystemProperties);
|
||||
public ArtifactStorage artifactRepository(final FileArtifactProperties artifactFilesystemProperties) {
|
||||
return new FileArtifactStorage(artifactFilesystemProperties);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.eclipse.hawkbit.autoconfigure.artifact.fs.FileArtifactStorageConfiguration
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact;
|
||||
package org.eclipse.hawkbit.artifact.fs;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@@ -21,8 +21,9 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.eclipse.hawkbit.repository.artifact.exception.ArtifactBinaryNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.artifact.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.artifact.AbstractArtifactStorage;
|
||||
import org.eclipse.hawkbit.artifact.exception.ArtifactBinaryNotFoundException;
|
||||
import org.eclipse.hawkbit.artifact.model.StoredArtifactInfo;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -32,19 +33,19 @@ import org.junit.jupiter.api.Test;
|
||||
* Story: Test storing artifact binaries in the file-system
|
||||
*/
|
||||
@Slf4j
|
||||
class ArtifactFilesystemRepositoryTest {
|
||||
class FileArtifactStorageTest {
|
||||
|
||||
private static final String TENANT = "test_tenant";
|
||||
|
||||
private static ArtifactFilesystemProperties artifactResourceProperties;
|
||||
private static ArtifactFilesystemRepository artifactFilesystemRepository;
|
||||
private static FileArtifactProperties artifactResourceProperties;
|
||||
private static FileArtifactStorage artifactFilesystemRepository;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
artifactResourceProperties = new ArtifactFilesystemProperties();
|
||||
artifactResourceProperties.setPath(AbstractArtifactRepository.createTempFile(true).toString());
|
||||
artifactResourceProperties = new FileArtifactProperties();
|
||||
artifactResourceProperties.setPath(AbstractArtifactStorage.createTempFile(true).toString());
|
||||
|
||||
artifactFilesystemRepository = new ArtifactFilesystemRepository(artifactResourceProperties);
|
||||
artifactFilesystemRepository = new FileArtifactStorage(artifactResourceProperties);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
@@ -64,10 +65,10 @@ class ArtifactFilesystemRepositoryTest {
|
||||
@Test
|
||||
void storeSuccessfully() throws IOException {
|
||||
final byte[] fileContent = randomBytes();
|
||||
final AbstractDbArtifact artifact = storeRandomArtifact(fileContent);
|
||||
final StoredArtifactInfo artifact = storeRandomArtifact(fileContent);
|
||||
|
||||
final byte[] readContent = new byte[fileContent.length];
|
||||
IOUtils.read(artifact.getFileInputStream(), readContent);
|
||||
IOUtils.read(artifactFilesystemRepository.getBySha1(TENANT, artifact.getHashes().sha1()), readContent);
|
||||
assertThat(readContent).isEqualTo(fileContent);
|
||||
}
|
||||
|
||||
@@ -77,9 +78,9 @@ class ArtifactFilesystemRepositoryTest {
|
||||
@Test
|
||||
void getStoredArtifactBasedOnSHA1Hash() throws IOException {
|
||||
final byte[] fileContent = randomBytes();
|
||||
final AbstractDbArtifact artifact = storeRandomArtifact(fileContent);
|
||||
final StoredArtifactInfo artifact = storeRandomArtifact(fileContent);
|
||||
|
||||
assertThat(artifactFilesystemRepository.getBySha1(TENANT, artifact.getHashes().getSha1())).isNotNull();
|
||||
assertThat(artifactFilesystemRepository.getBySha1(TENANT, artifact.getHashes().sha1())).isNotNull();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,10 +88,10 @@ class ArtifactFilesystemRepositoryTest {
|
||||
*/
|
||||
@Test
|
||||
void deleteStoredArtifactBySHA1Hash() throws IOException {
|
||||
final AbstractDbArtifact artifact = storeRandomArtifact(randomBytes());
|
||||
artifactFilesystemRepository.deleteBySha1(TENANT, artifact.getHashes().getSha1());
|
||||
final StoredArtifactInfo artifact = storeRandomArtifact(randomBytes());
|
||||
artifactFilesystemRepository.deleteBySha1(TENANT, artifact.getHashes().sha1());
|
||||
|
||||
final String sha1Hash = artifact.getHashes().getSha1();
|
||||
final String sha1Hash = artifact.getHashes().sha1();
|
||||
assertThatExceptionOfType(ArtifactBinaryNotFoundException.class)
|
||||
.isThrownBy(() -> artifactFilesystemRepository.getBySha1(TENANT, sha1Hash));
|
||||
}
|
||||
@@ -100,10 +101,10 @@ class ArtifactFilesystemRepositoryTest {
|
||||
*/
|
||||
@Test
|
||||
void deleteStoredArtifactOfTenant() throws IOException {
|
||||
final AbstractDbArtifact artifact = storeRandomArtifact(randomBytes());
|
||||
final StoredArtifactInfo artifact = storeRandomArtifact(randomBytes());
|
||||
artifactFilesystemRepository.deleteByTenant(TENANT);
|
||||
|
||||
final String sha1Hash = artifact.getHashes().getSha1();
|
||||
final String sha1Hash = artifact.getHashes().sha1();
|
||||
assertThatExceptionOfType(ArtifactBinaryNotFoundException.class)
|
||||
.isThrownBy(() -> artifactFilesystemRepository.getBySha1(TENANT, sha1Hash));
|
||||
}
|
||||
@@ -119,9 +120,9 @@ class ArtifactFilesystemRepositoryTest {
|
||||
Assertions.fail("did not expect an exception while deleting a file which does not exists");
|
||||
}
|
||||
|
||||
final AbstractDbArtifact artifact = storeRandomArtifact(randomBytes());
|
||||
final StoredArtifactInfo artifact = storeRandomArtifact(randomBytes());
|
||||
try {
|
||||
artifactFilesystemRepository.deleteBySha1("tenantWhichDoesNotExist", artifact.getHashes().getSha1());
|
||||
artifactFilesystemRepository.deleteBySha1("tenantWhichDoesNotExist", artifact.getHashes().sha1());
|
||||
} catch (final Exception e) {
|
||||
Assertions.fail("did not expect an exception while deleting a file which does not exists");
|
||||
}
|
||||
@@ -133,7 +134,7 @@ class ArtifactFilesystemRepositoryTest {
|
||||
return randomBytes;
|
||||
}
|
||||
|
||||
private AbstractDbArtifact storeRandomArtifact(final byte[] fileContent) throws IOException {
|
||||
private StoredArtifactInfo storeRandomArtifact(final byte[] fileContent) throws IOException {
|
||||
try (final ByteArrayInputStream inputStream = new ByteArrayInputStream(fileContent)) {
|
||||
return artifactFilesystemRepository.store(TENANT, inputStream, "filename.tmp", "application/txt", null);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
# Eclipse.IoT hawkBit - Artifact Repository File System
|
||||
|
||||
This module contains the implementation of the `ArtifactRepository` based on the file-system.
|
||||
It's a very convenient and easy implementation of storing the artifact binaries into the file-system based on the SHA-1
|
||||
hash naming.
|
||||
|
||||
Due the limit of many file-systems of files within one directory, the files
|
||||
are stored in different sub-directories based on the last four digits of the
|
||||
SHA1-hash `/basepath/[two digit sha1]/[two digit sha1/sha1-hash-filename]`.
|
||||
|
||||
# Compile
|
||||
|
||||
#### Build hawkbit-artifact-repository-filesystem
|
||||
|
||||
```
|
||||
$ cd hawkbit/hawkbit-artifact-repository-filesystem
|
||||
$ mvn clean install
|
||||
```
|
||||
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact;
|
||||
|
||||
/**
|
||||
* Exception thrown in case that the artifact could not be read.
|
||||
*/
|
||||
public class ArtifactFileNotFoundException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Creates the Exception from its cause
|
||||
*
|
||||
* @param cause the original exception
|
||||
*/
|
||||
public ArtifactFileNotFoundException(final Exception cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.eclipse.hawkbit.repository.artifact.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.repository.artifact.model.DbArtifactHash;
|
||||
|
||||
/**
|
||||
* {@link AbstractDbArtifact} implementation which dynamically creates a {@link FileInputStream} on calling {@link #getFileInputStream()}.
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ArtifactFilesystem extends AbstractDbArtifact {
|
||||
|
||||
private final File file;
|
||||
|
||||
public ArtifactFilesystem(
|
||||
@NotNull final File file, @NotNull final String artifactId,
|
||||
@NotNull final DbArtifactHash hashes, final Long size,
|
||||
final String contentType) {
|
||||
super(artifactId, hashes, size, contentType);
|
||||
this.file = Objects.requireNonNull(file, "Artifact file may not be null");
|
||||
}
|
||||
|
||||
// suppress warning, this InputStream needs to be closed by the caller, this
|
||||
// cannot be closed in this method
|
||||
@SuppressWarnings("squid:S2095")
|
||||
@Override
|
||||
public InputStream getFileInputStream() {
|
||||
try {
|
||||
return new BufferedInputStream(new FileInputStream(file));
|
||||
} catch (final FileNotFoundException e) {
|
||||
throw new ArtifactFileNotFoundException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.eclipse.hawkbit.repository.artifact.exception.ArtifactBinaryNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.artifact.model.AbstractDbArtifact;
|
||||
import org.eclipse.hawkbit.repository.artifact.model.DbArtifactHash;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link ArtifactRepository} to store artifacts on the
|
||||
* file-system. The files are stored by their SHA1 hash of the artifact binary.
|
||||
* Duplicate files with the same SHA1 hash will only be stored once.
|
||||
*
|
||||
* All files are stored flat in one base directory configured in the
|
||||
* {@link ArtifactFilesystemProperties#getPath()}.
|
||||
*
|
||||
* Due to the limit of many file-systems of files within one directory, the files
|
||||
* are stored in different subdirectories based on the last four digits of the
|
||||
* SHA1-hash {@code (/basepath/[two digit sha1]/[two digit sha1])}.
|
||||
*/
|
||||
@Validated
|
||||
public class ArtifactFilesystemRepository extends AbstractArtifactRepository {
|
||||
|
||||
private final ArtifactFilesystemProperties artifactResourceProperties;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param artifactResourceProperties the properties which holds the necessary configuration for the file-system repository
|
||||
*/
|
||||
public ArtifactFilesystemRepository(final ArtifactFilesystemProperties artifactResourceProperties) {
|
||||
this.artifactResourceProperties = artifactResourceProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteBySha1(final String tenant, final String sha1Hash) {
|
||||
FileUtils.deleteQuietly(getFile(tenant, sha1Hash));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractDbArtifact getBySha1(final String tenant, final String sha1) {
|
||||
final File file = getFile(tenant, sha1);
|
||||
if (!file.exists()) {
|
||||
throw new ArtifactBinaryNotFoundException(sha1);
|
||||
}
|
||||
|
||||
return new ArtifactFilesystem(file, sha1, new DbArtifactHash(sha1, null, null), file.length(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByTenant(final String tenant) {
|
||||
FileUtils.deleteQuietly(Paths.get(artifactResourceProperties.getPath(), sanitizeTenant(tenant)).toFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsBySha1(final String tenant, final String sha1) {
|
||||
return getFile(tenant, sha1).exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractDbArtifact store(final String tenant, final DbArtifactHash base16Hashes, final String contentType, final String tempFile)
|
||||
throws IOException {
|
||||
final File file = new File(tempFile);
|
||||
return renameFileToSHA1Naming(
|
||||
tenant, file,
|
||||
new ArtifactFilesystem(file, base16Hashes.getSha1(), base16Hashes, file.length(), contentType));
|
||||
}
|
||||
|
||||
private ArtifactFilesystem renameFileToSHA1Naming(final String tenant, final File file,
|
||||
final AbstractDbArtifact artifact) throws IOException {
|
||||
final File fileSHA1Naming = getFile(tenant, artifact.getHashes().getSha1());
|
||||
if (fileSHA1Naming.exists()) {
|
||||
FileUtils.deleteQuietly(file);
|
||||
} else {
|
||||
Files.move(file.toPath(), fileSHA1Naming.toPath());
|
||||
}
|
||||
|
||||
return new ArtifactFilesystem(
|
||||
fileSHA1Naming, artifact.getArtifactId(), artifact.getHashes(), artifact.getSize(), artifact.getContentType());
|
||||
}
|
||||
|
||||
private File getFile(final String tenant, final String sha1) {
|
||||
// ensure that the sha1 is not a path traversal attack
|
||||
if (sha1.indexOf('/') >= 0 || sha1.indexOf('\\') >= 0) {
|
||||
throw new IllegalArgumentException("Invalid sha1 hash: " + sha1);
|
||||
}
|
||||
|
||||
final File aritfactDirectory = getSha1DirectoryPath(tenant, sha1).toFile();
|
||||
aritfactDirectory.mkdirs();
|
||||
return new File(aritfactDirectory, sha1);
|
||||
}
|
||||
|
||||
private Path getSha1DirectoryPath(final String tenant, final String sha1) {
|
||||
final int length = sha1.length();
|
||||
final String folder1 = sha1.substring(length - 4, length - 2);
|
||||
final String folder2 = sha1.substring(length - 2, length);
|
||||
return Paths.get(artifactResourceProperties.getPath(), sanitizeTenant(tenant), folder1, folder2);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
org.eclipse.hawkbit.autoconfigure.repository.artifact.filesystem.ArtifactFilesystemConfiguration
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.artifact;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.eclipse.hawkbit.repository.artifact.model.DbArtifactHash;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Feature: Unit Tests - Artifact File System Repository<br/>
|
||||
* Story: Test storing artifact binaries in the file-system
|
||||
*/
|
||||
class ArtifactFilesystemTest {
|
||||
|
||||
/**
|
||||
* Verifies that an exception is thrown on opening an InputStream when file does not exists
|
||||
*/
|
||||
@Test
|
||||
void getInputStreamOfNonExistingFileThrowsException() {
|
||||
final File file = new File("fileWhichTotalDoesNotExists");
|
||||
final ArtifactFilesystem underTest = new ArtifactFilesystem(
|
||||
file, "fileWhichTotalDoesNotExists",
|
||||
new DbArtifactHash("1", "2", "3"), 0L, null);
|
||||
assertThatThrownBy(underTest::getFileInputStream)
|
||||
.isInstanceOf(ArtifactFileNotFoundException.class)
|
||||
.hasCauseInstanceOf(FileNotFoundException.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that an InputStream can be opened if file exists
|
||||
*/
|
||||
@Test
|
||||
void getInputStreamOfExistingFile() throws IOException {
|
||||
final ArtifactFilesystem underTest = new ArtifactFilesystem(
|
||||
AbstractArtifactRepository.createTempFile(false), ArtifactFilesystemTest.class.getSimpleName(),
|
||||
new DbArtifactHash("1", "2", "3"), 0L, null);
|
||||
assertThat(IOUtils.read(underTest.getFileInputStream(), new byte[16])).isZero();
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,6 @@
|
||||
|
||||
<modules>
|
||||
<module>hawkbit-artifact-api</module>
|
||||
<module>hawkbit-artifact-repository-filesystem</module>
|
||||
<module>hawkbit-artifact-fs</module>
|
||||
</modules>
|
||||
</project>
|
||||
@@ -9,9 +9,9 @@
|
||||
*/
|
||||
package org.eclipse.hawkbit.autoconfigure.artifact;
|
||||
|
||||
import org.eclipse.hawkbit.repository.artifact.urlhandler.ArtifactUrlHandler;
|
||||
import org.eclipse.hawkbit.repository.artifact.urlhandler.ArtifactUrlHandlerProperties;
|
||||
import org.eclipse.hawkbit.repository.artifact.urlhandler.PropertyBasedArtifactUrlHandler;
|
||||
import org.eclipse.hawkbit.artifact.urlresolver.ArtifactUrlResolver;
|
||||
import org.eclipse.hawkbit.artifact.urlresolver.PropertyBasedArtifactUrlResolver;
|
||||
import org.eclipse.hawkbit.artifact.urlresolver.PropertyBasedArtifactUrlResolverProperties;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
@@ -20,22 +20,18 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
|
||||
/**
|
||||
* Autoconfiguration for {@link ArtifactUrlHandler} based on a properties.
|
||||
* Autoconfiguration for {@link ArtifactUrlResolver} based on a properties.
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties({ ArtifactUrlHandlerProperties.class })
|
||||
@EnableConfigurationProperties({ PropertyBasedArtifactUrlResolverProperties.class })
|
||||
@PropertySource("classpath:/hawkbit-artifactdl-defaults.properties")
|
||||
public class ArtifactUrlHandlerAutoConfiguration {
|
||||
public class PropertyBasedArtifactUrlResolverAutoConfiguration {
|
||||
|
||||
/**
|
||||
* @param urlHandlerProperties for bean configuration
|
||||
* @return PropertyBasedArtifactUrlHandler bean
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ArtifactUrlHandler.class)
|
||||
PropertyBasedArtifactUrlHandler propertyBasedArtifactUrlHandler(
|
||||
final ArtifactUrlHandlerProperties urlHandlerProperties,
|
||||
@ConditionalOnMissingBean(ArtifactUrlResolver.class)
|
||||
PropertyBasedArtifactUrlResolver propertyBasedArtifactUrlHandler(
|
||||
final PropertyBasedArtifactUrlResolverProperties urlHandlerProperties,
|
||||
@Value("${server.servlet.context-path:}") final String contextPath) {
|
||||
return new PropertyBasedArtifactUrlHandler(urlHandlerProperties, contextPath);
|
||||
return new PropertyBasedArtifactUrlResolver(urlHandlerProperties, contextPath);
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
package org.eclipse.hawkbit.autoconfigure.cache;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.eclipse.hawkbit.cache.TenancyCacheManager;
|
||||
import org.eclipse.hawkbit.cache.TenantAwareCacheManager;
|
||||
import org.eclipse.hawkbit.tenancy.cache.TenantCacheManager;
|
||||
import org.eclipse.hawkbit.tenancy.cache.TenantAwareCacheManager;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
@@ -27,7 +27,7 @@ import org.springframework.context.annotation.Primary;
|
||||
* A configuration for configuring the spring {@link CacheManager} for specific multi-tenancy caching. The caches between
|
||||
* tenants must not interfere each other.
|
||||
* <p/>
|
||||
* This is done by providing a special {@link TenancyCacheManager} which generates a cache name included the current tenant.
|
||||
* This is done by providing a special {@link TenantCacheManager} which generates a cache name included the current tenant.
|
||||
*/
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
@@ -39,9 +39,8 @@ public class CacheAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Primary
|
||||
TenancyCacheManager cacheManager(
|
||||
@Qualifier("directCacheManager") final CacheManager directCacheManager,
|
||||
final TenantAware tenantAware) {
|
||||
TenantCacheManager cacheManager(
|
||||
@Qualifier("directCacheManager") final CacheManager directCacheManager, final TenantAware tenantAware) {
|
||||
return new TenantAwareCacheManager(directCacheManager, tenantAware);
|
||||
}
|
||||
|
||||
@@ -63,9 +62,7 @@ public class CacheAutoConfiguration {
|
||||
final CaffeineCacheManager cacheManager = new CaffeineCacheManager();
|
||||
|
||||
if (cacheProperties.getTtl() > 0) {
|
||||
final Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder()
|
||||
.expireAfterWrite(cacheProperties.getTtl(), cacheProperties.getTtlUnit());
|
||||
cacheManager.setCaffeine(cacheBuilder);
|
||||
cacheManager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(cacheProperties.getTtl(), cacheProperties.getTtlUnit()));
|
||||
}
|
||||
|
||||
return cacheManager;
|
||||
|
||||
@@ -19,7 +19,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
* After lifetime the data gets reloaded out of the database.
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties("hawkbit.cache.global")
|
||||
@ConfigurationProperties("hawkbit.cache")
|
||||
public class CacheProperties {
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* Autoconfiguration for the events..
|
||||
* Autoconfiguration for the events.
|
||||
*/
|
||||
@Configuration
|
||||
@Import(EventPublisherConfiguration.class)
|
||||
|
||||
@@ -38,13 +38,13 @@ import org.springframework.security.concurrent.DelegatingSecurityContextSchedule
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(AsyncConfigurerThreadPoolProperties.class)
|
||||
@EnableConfigurationProperties(ExecutorProperties.class)
|
||||
public class ExecutorAutoConfiguration {
|
||||
|
||||
private final AsyncConfigurerThreadPoolProperties asyncConfigurerProperties;
|
||||
private final ExecutorProperties executorProperties;
|
||||
|
||||
public ExecutorAutoConfiguration(final AsyncConfigurerThreadPoolProperties asyncConfigurerProperties) {
|
||||
this.asyncConfigurerProperties = asyncConfigurerProperties;
|
||||
public ExecutorAutoConfiguration(final ExecutorProperties executorProperties) {
|
||||
this.executorProperties = executorProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,7 +72,7 @@ public class ExecutorAutoConfiguration {
|
||||
@ConditionalOnMissingBean
|
||||
public ScheduledExecutorService scheduledExecutorService() {
|
||||
return new DelegatingSecurityContextScheduledExecutorService(Executors.newScheduledThreadPool(
|
||||
asyncConfigurerProperties.getSchedulerThreads(), threadFactory("central-scheduled-executor-pool-%d")));
|
||||
executorProperties.getSchedulerThreads(), threadFactory("central-scheduled-executor-pool-%d")));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,9 +97,9 @@ public class ExecutorAutoConfiguration {
|
||||
* @return central ThreadPoolExecutor for general purpose multithreaded operations. Tries an orderly shutdown when destroyed.
|
||||
*/
|
||||
private ThreadPoolExecutor threadPoolExecutor() {
|
||||
final BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(asyncConfigurerProperties.getQueueSize());
|
||||
return new ThreadPoolExecutor(asyncConfigurerProperties.getCoreThreads(),
|
||||
asyncConfigurerProperties.getMaxThreads(), asyncConfigurerProperties.getIdleTimeout(),
|
||||
final BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(executorProperties.getQueueSize());
|
||||
return new ThreadPoolExecutor(executorProperties.getCoreThreads(),
|
||||
executorProperties.getMaxThreads(), executorProperties.getIdleTimeout(),
|
||||
TimeUnit.MILLISECONDS, blockingQueue,
|
||||
threadFactory("central-executor-pool-%d"),
|
||||
new PoolSizeExceededPolicy());
|
||||
|
||||
@@ -16,8 +16,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
* Properties for the async configurer.
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties("hawkbit.threadpool")
|
||||
public class AsyncConfigurerThreadPoolProperties {
|
||||
@ConfigurationProperties("hawkbit.executor")
|
||||
public class ExecutorProperties {
|
||||
|
||||
/**
|
||||
* Max queue size for central event executor.
|
||||
@@ -25,11 +25,12 @@ import org.springframework.security.config.annotation.authentication.configurati
|
||||
@Configuration
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@EnableConfigurationProperties({ TenantAwareUserProperties.class })
|
||||
public class InMemoryUserManagementAutoConfiguration extends GlobalAuthenticationConfigurerAdapter {
|
||||
public class StaticUserManagementAutoConfiguration extends GlobalAuthenticationConfigurerAdapter {
|
||||
|
||||
private final StaticAuthenticationProvider authenticationProvider;
|
||||
|
||||
InMemoryUserManagementAutoConfiguration(final SecurityProperties securityProperties,
|
||||
StaticUserManagementAutoConfiguration(
|
||||
final SecurityProperties securityProperties,
|
||||
final TenantAwareUserProperties tenantAwareUserProperties) {
|
||||
authenticationProvider = new StaticAuthenticationProvider(tenantAwareUserProperties, securityProperties);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
org.eclipse.hawkbit.autoconfigure.artifact.ArtifactUrlHandlerAutoConfiguration
|
||||
org.eclipse.hawkbit.autoconfigure.artifact.PropertyBasedArtifactUrlResolverAutoConfiguration
|
||||
org.eclipse.hawkbit.autoconfigure.cache.CacheAutoConfiguration
|
||||
org.eclipse.hawkbit.autoconfigure.repository.event.EventPublisherAutoConfiguration
|
||||
org.eclipse.hawkbit.autoconfigure.repository.JpaRepositoryAutoConfiguration
|
||||
org.eclipse.hawkbit.autoconfigure.scheduling.AsyncConfigurerAutoConfiguration
|
||||
org.eclipse.hawkbit.autoconfigure.scheduling.ExecutorAutoConfiguration
|
||||
org.eclipse.hawkbit.autoconfigure.security.SecurityAutoConfiguration
|
||||
org.eclipse.hawkbit.autoconfigure.security.InMemoryUserManagementAutoConfiguration
|
||||
org.eclipse.hawkbit.autoconfigure.security.StaticUserManagementAutoConfiguration
|
||||
|
||||
@@ -22,11 +22,4 @@ hawkbit.artifact.url.protocols.md5sum-http.hostname=${hawkbit.artifact.url.proto
|
||||
hawkbit.artifact.url.protocols.md5sum-http.ip=${hawkbit.artifact.url.protocols.download-http.ip}
|
||||
hawkbit.artifact.url.protocols.md5sum-http.port=${hawkbit.artifact.url.protocols.download-http.port}
|
||||
hawkbit.artifact.url.protocols.md5sum-http.supports=DDI
|
||||
hawkbit.artifact.url.protocols.md5sum-http.ref=${hawkbit.artifact.url.protocols.download-http.ref}.MD5SUM
|
||||
hawkbit.artifact.url.protocols.download-cdn-http.rel=download
|
||||
hawkbit.artifact.url.protocols.download-cdn-http.hostname=localhost
|
||||
hawkbit.artifact.url.protocols.download-cdn-http.ip=127.0.0.1
|
||||
hawkbit.artifact.url.protocols.download-cdn-http.protocol=http
|
||||
hawkbit.artifact.url.protocols.download-cdn-http.port=8080
|
||||
hawkbit.artifact.url.protocols.download-cdn-http.supports=MGMT
|
||||
hawkbit.artifact.url.protocols.download-cdn-http.ref={protocolRequest}://{hostnameRequest}:{portRequest}/rest/v1/softwaremodules/{softwareModuleId}/artifacts/{artifactFileName}
|
||||
hawkbit.artifact.url.protocols.md5sum-http.ref=${hawkbit.artifact.url.protocols.download-http.ref}.MD5SUM
|
||||
@@ -1,3 +1,3 @@
|
||||
# hawkBit Core
|
||||
|
||||
Various internal interfaces and utility classes.
|
||||
Various internal / core interfaces and utility classes.
|
||||
@@ -28,73 +28,28 @@ public abstract class AbstractServerRtException extends RuntimeException {
|
||||
private final SpServerError error;
|
||||
private final transient Map<String, Object> info;
|
||||
|
||||
/**
|
||||
* Parameterized constructor.
|
||||
*
|
||||
* @param error detail
|
||||
*/
|
||||
protected AbstractServerRtException(final SpServerError error) {
|
||||
super(error.getMessage());
|
||||
this.error = error;
|
||||
this.info = null;
|
||||
this(error, error.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameterized constructor.
|
||||
*
|
||||
* @param message custom error message
|
||||
* @param error detail
|
||||
*/
|
||||
protected AbstractServerRtException(final String message, final SpServerError error) {
|
||||
this(message, error, (Map<String, Object>) null);
|
||||
protected AbstractServerRtException(final SpServerError error, final String message) {
|
||||
this(error, message, (Map<String, Object>) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameterized constructor.
|
||||
*
|
||||
* @param message custom error message
|
||||
* @param error detail
|
||||
*/
|
||||
protected AbstractServerRtException(final String message, final SpServerError error, final Map<String, Object> info) {
|
||||
super(message);
|
||||
this.error = error;
|
||||
this.info = info;
|
||||
protected AbstractServerRtException(final SpServerError error, final String message, final Map<String, Object> info) {
|
||||
this(error, message, info, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameterized constructor.
|
||||
*
|
||||
* @param message custom error message
|
||||
* @param error detail
|
||||
* @param cause of the exception
|
||||
*/
|
||||
protected AbstractServerRtException(final String message, final SpServerError error, final Throwable cause) {
|
||||
super(message, cause);
|
||||
this.error = error;
|
||||
this.info = null;
|
||||
protected AbstractServerRtException(final SpServerError error, final String message, final Throwable cause) {
|
||||
this(error, message, null, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameterized constructor.
|
||||
*
|
||||
* @param error detail
|
||||
* @param cause of the exception
|
||||
*/
|
||||
protected AbstractServerRtException(final SpServerError error, final Throwable cause) {
|
||||
super(error.getMessage(), cause);
|
||||
this.error = error;
|
||||
this.info = null;
|
||||
this(error, error.getMessage(), null, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameterized constructor.
|
||||
*
|
||||
* @param message custom error message
|
||||
* @param error detail
|
||||
* @param cause of the exception
|
||||
*/
|
||||
protected AbstractServerRtException(
|
||||
final String message, final SpServerError error, final Throwable cause, final Map<String, Object> info) {
|
||||
final SpServerError error, final String message, final Map<String, Object> info, final Throwable cause) {
|
||||
super(message, cause);
|
||||
this.error = error;
|
||||
this.info = info;
|
||||
|
||||
@@ -86,7 +86,7 @@ public interface TenantAware {
|
||||
if (context.getAuthentication() != null) {
|
||||
final Object principal = context.getAuthentication().getPrincipal();
|
||||
if (context.getAuthentication().getDetails() instanceof TenantAwareAuthenticationDetails tenantAwareAuthenticationDetails) {
|
||||
return tenantAwareAuthenticationDetails.getTenant();
|
||||
return tenantAwareAuthenticationDetails.tenant();
|
||||
} else if (principal instanceof TenantAwareUser tenantAwareUser) {
|
||||
return tenantAwareUser.getTenant();
|
||||
}
|
||||
|
||||
@@ -12,31 +12,14 @@ package org.eclipse.hawkbit.tenancy;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
|
||||
/**
|
||||
* An authentication details object {@link AbstractAuthenticationToken#getDetails()} which is stored in the
|
||||
* spring security authentication token details to transport the principal and tenant in the security context session.
|
||||
*/
|
||||
@Getter
|
||||
@ToString
|
||||
public class TenantAwareAuthenticationDetails implements Serializable {
|
||||
public record TenantAwareAuthenticationDetails(String tenant, boolean controller) implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String tenant;
|
||||
private final boolean controller;
|
||||
|
||||
/**
|
||||
* @param tenant the current tenant
|
||||
* @param controller boolean flag to indicate if this authenticated token is a controller authentication. {@code true} in case of
|
||||
* authenticated controller otherwise {@code false}
|
||||
*/
|
||||
public TenantAwareAuthenticationDetails(final String tenant, final boolean controller) {
|
||||
this.tenant = tenant;
|
||||
this.controller = controller;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware.TenantResolver;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
|
||||
@@ -59,8 +60,9 @@ public class TenantMetricsConfiguration {
|
||||
public DefaultServerRequestObservationConvention serverRequestObservationConvention(final TenantResolver tenantResolver) {
|
||||
return new DefaultServerRequestObservationConvention() {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public KeyValues getLowCardinalityKeyValues(final ServerRequestObservationContext context) {
|
||||
public KeyValues getLowCardinalityKeyValues(@NonNull final ServerRequestObservationContext context) {
|
||||
// Make sure that KeyValues entries are already sorted by name for better performance
|
||||
return KeyValues.of(exception(context), method(context), outcome(context), status(context), tenant(), uri(context));
|
||||
}
|
||||
|
||||
@@ -7,14 +7,17 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.cache;
|
||||
package org.eclipse.hawkbit.tenancy.cache;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
|
||||
import lombok.NonNull;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link CacheManager} delegator which wraps the {@link CacheManager#getCache(String)} and {@link CacheManager#getCacheNames()}
|
||||
@@ -22,7 +25,7 @@ import org.springframework.cache.CacheManager;
|
||||
* <p/>
|
||||
* Additionally, it also provides functionality to retrieve all caches overall tenants at once, for monitoring and system access.
|
||||
*/
|
||||
public class TenantAwareCacheManager implements TenancyCacheManager {
|
||||
public class TenantAwareCacheManager implements TenantCacheManager {
|
||||
|
||||
private static final String TENANT_CACHE_DELIMITER = "|";
|
||||
|
||||
@@ -40,8 +43,9 @@ public class TenantAwareCacheManager implements TenancyCacheManager {
|
||||
this.tenantAware = tenantAware;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Cache getCache(final String name) {
|
||||
public Cache getCache(@NonNull final String name) {
|
||||
final String currentTenant = tenantAware.getCurrentTenant();
|
||||
if (isTenantInvalid(currentTenant)) {
|
||||
return null;
|
||||
@@ -50,6 +54,7 @@ public class TenantAwareCacheManager implements TenancyCacheManager {
|
||||
return delegate.getCache(buildKey(currentTenant.toUpperCase(), name));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Collection<String> getCacheNames() {
|
||||
final String currentTenant = tenantAware.getCurrentTenant();
|
||||
@@ -67,7 +72,8 @@ public class TenantAwareCacheManager implements TenancyCacheManager {
|
||||
|
||||
@Override
|
||||
public void evictCaches(final String tenant) {
|
||||
getCacheNames(tenant).forEach(cacheName -> delegate.getCache(buildKey(tenant, cacheName)).clear());
|
||||
getCacheNames(tenant).forEach(
|
||||
cacheName -> Optional.ofNullable(delegate.getCache(buildKey(tenant, cacheName))).ifPresent(Cache::clear));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.cache;
|
||||
package org.eclipse.hawkbit.tenancy.cache;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
@@ -15,7 +15,7 @@ import org.springframework.cache.CacheManager;
|
||||
/**
|
||||
* A cache interface which handles multi tenancy.
|
||||
*/
|
||||
public interface TenancyCacheManager extends CacheManager {
|
||||
public interface TenantCacheManager extends CacheManager {
|
||||
|
||||
/**
|
||||
* A direct-access for retrieving the cache without including the current tenant key. This is necessary e.g. for retrieving caches not for
|
||||
@@ -71,7 +71,7 @@ public class ObjectCopyUtil {
|
||||
final ToSetter toSetter = toSetter(toClass, setterName, fieldName, fromMethod.getReturnType());
|
||||
if (toSetter == null && toGetter == null) {
|
||||
// we allow toSetter to be null, but in that case the toGetter must not be null and the
|
||||
// from value shall always match the to value (without setting it)
|
||||
// 'from' value shall always match the to value (without setting it)
|
||||
throw new IllegalStateException("Setter counterpart for " + fromMethod + " is not found in " + toClass.getName());
|
||||
}
|
||||
propertySetters.add(new PropertyCopyFunction(fromMethod, toSetter, toGetter));
|
||||
@@ -284,50 +284,41 @@ public class ObjectCopyUtil {
|
||||
private record ToSetter(BiConsumer<Object, Object> toSetter, int order) {}
|
||||
|
||||
@SuppressWarnings("java:S1210") // java:S1210 - return 0 only when default equals return equals, assume equal hashCodes for equal objects
|
||||
private static class PropertyCopyFunction implements CopyFunction, Comparable<PropertyCopyFunction> {
|
||||
|
||||
private final Method fromMethod;
|
||||
private final ToSetter toSetter;
|
||||
private final UnaryOperator<Object> toGetter;
|
||||
|
||||
private PropertyCopyFunction(final Method fromMethod, final ToSetter toSetter, final UnaryOperator<Object> toGetter) {
|
||||
this.fromMethod = fromMethod;
|
||||
this.toGetter = toGetter;
|
||||
this.toSetter = toSetter;
|
||||
}
|
||||
private record PropertyCopyFunction(Method fromMethod, ToSetter toSetter, UnaryOperator<Object> toGetter)
|
||||
implements CopyFunction, Comparable<PropertyCopyFunction> {
|
||||
|
||||
public boolean apply(final Object from, final Object to, final boolean setNullValues, final UnaryOperator<Object> propertyProcessor) {
|
||||
final Object value;
|
||||
try {
|
||||
value = fromMethod.invoke(from);
|
||||
} catch (final IllegalAccessException e) {
|
||||
throw new IllegalStateException("Failed to get source value", e);
|
||||
} catch (final InvocationTargetException e) {
|
||||
throw new IllegalStateException("Failed to get source value", e.getTargetException() == null ? e : e.getTargetException());
|
||||
}
|
||||
if (value == null && !setNullValues) { // if !setNullValues null means no change
|
||||
return false;
|
||||
}
|
||||
if (toGetter != null) {
|
||||
final Object currentValue = toGetter.apply(to);
|
||||
if (Objects.equals(value, currentValue)) {
|
||||
return false; // no change
|
||||
final Object value;
|
||||
try {
|
||||
value = fromMethod.invoke(from);
|
||||
} catch (final IllegalAccessException e) {
|
||||
throw new IllegalStateException("Failed to get source value", e);
|
||||
} catch (final InvocationTargetException e) {
|
||||
throw new IllegalStateException("Failed to get source value", e.getTargetException() == null ? e : e.getTargetException());
|
||||
}
|
||||
if (value == null && !setNullValues) { // if !setNullValues null means no change
|
||||
return false;
|
||||
}
|
||||
if (toGetter != null) {
|
||||
final Object currentValue = toGetter.apply(to);
|
||||
if (Objects.equals(value, currentValue)) {
|
||||
return false; // no change
|
||||
}
|
||||
}
|
||||
if (toSetter == null) {
|
||||
throw new IllegalStateException(
|
||||
"Setter counterpart for " + fromMethod + " is not found in " + to.getClass().getName() +
|
||||
" and the 'from' value is not equal to the 'to' value");
|
||||
}
|
||||
toSetter.toSetter().accept(to, propertyProcessor.apply(value));
|
||||
return true;
|
||||
}
|
||||
if (toSetter == null) {
|
||||
throw new IllegalStateException(
|
||||
"Setter counterpart for " + fromMethod + " is not found in " + to.getClass().getName() +
|
||||
" and the 'from' value is not equal to the 'to' value");
|
||||
}
|
||||
toSetter.toSetter().accept(to, propertyProcessor.apply(value));
|
||||
return true;
|
||||
}
|
||||
|
||||
public int compareTo(final PropertyCopyFunction other) {
|
||||
final int orderCompare = Integer.compare(this.toSetter.order(), other.toSetter.order());
|
||||
return orderCompare == 0 ? Integer.compare(this.hashCode(), other.hashCode()) : orderCompare;
|
||||
public int compareTo(final PropertyCopyFunction other) {
|
||||
final int orderCompare = Integer.compare(this.toSetter.order(), other.toSetter.order());
|
||||
return orderCompare == 0 ? Integer.compare(this.hashCode(), other.hashCode()) : orderCompare;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// functional interface to apply the copy operation
|
||||
private interface CopyFunction {
|
||||
|
||||
@@ -18,7 +18,7 @@ import io.github.classgraph.ClassInfo;
|
||||
import io.github.classgraph.ScanResult;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class FileNameFieldsTest {
|
||||
class RsqlQueryFieldsTest {
|
||||
|
||||
/**
|
||||
* Verifies that fields classes are correctly implemented
|
||||
@@ -35,9 +35,7 @@ class FileNameFieldsTest {
|
||||
.map(clazz -> (Class<? extends RsqlQueryField>) clazz)
|
||||
.toList();
|
||||
assertThat(matchingClasses).isNotEmpty();
|
||||
matchingClasses.forEach(providerClass -> {
|
||||
assertThat(providerClass.getEnumConstants()).isNotEmpty();
|
||||
});
|
||||
matchingClasses.forEach(providerClass -> assertThat(providerClass.getEnumConstants()).isNotEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,9 +47,7 @@ public class DdiActionHistory {
|
||||
* @param messages is a list of messages retrieved from action history.
|
||||
*/
|
||||
@JsonCreator
|
||||
public DdiActionHistory(
|
||||
@JsonProperty("status") final String status,
|
||||
@JsonProperty("messages") final List<String> messages) {
|
||||
public DdiActionHistory(@JsonProperty("status") final String status, @JsonProperty("messages") final List<String> messages) {
|
||||
this.status = status;
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@@ -27,16 +28,10 @@ public class DdiActivateAutoConfirmation {
|
||||
"confirming future actions", example = "exampleRemark")
|
||||
private final String remark;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param initiator can be null
|
||||
* @param remark can be null
|
||||
*/
|
||||
@JsonCreator
|
||||
public DdiActivateAutoConfirmation(
|
||||
@JsonProperty(value = "initiator") final String initiator,
|
||||
@JsonProperty(value = "remark") final String remark) {
|
||||
@Nullable @JsonProperty(value = "initiator") final String initiator,
|
||||
@Nullable @JsonProperty(value = "remark") final String remark) {
|
||||
this.initiator = initiator;
|
||||
this.remark = remark;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.springframework.hateoas.RepresentationModel;
|
||||
* **download** - HTTPs Download resource for artifacts. The resource supports partial download as specified by RFC7233 (range requests). Keep in mind that the target needs to have the artifact assigned in order to be granted permission to download.
|
||||
* **md5sum** - HTTPs Download resource for MD5SUM file is an optional auto generated artifact that is especially useful for Linux based devices on order to check artifact consistency after download by using the md5sum command line tool. The MD5 and SHA1 are in addition available as metadata in the deployment command itself.
|
||||
* **download-http** - HTTP Download resource for artifacts. The resource supports partial download as specified by RFC7233 (range requests). Keep in mind that the target needs to have the artifact assigned in order to be granted permission to download. (note: anonymous download needs to be enabled on the service account for non-TLS access)
|
||||
* **md5sum-http** - HTTP Download resource for MD5SUM file is an optional auto generated artifact that is especially useful for Linux based devices on order to check artifact consistency after download by using the md5sum command line tool. The MD5 and SHA1 are in addition available as metadata in the deployment command itself. (note: anonymous download needs to be enabled on the service account for non-TLS access)
|
||||
* **md5sum-http** - HTTP Download resource for MD5SUM file is an optional auto generated artifact that is especially useful for Linux based devices on order to check artifact consistency after download by using the md5sum command line tool. The MD5 and SHA1 are in addition available as metadata in the deployment command itself. (note: anonymous download needs to be enabled on the service account for non-TLS access)
|
||||
""", example = """
|
||||
{
|
||||
"filename" : "binaryFile",
|
||||
@@ -47,10 +47,10 @@ import org.springframework.hateoas.RepresentationModel;
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/40/filename/binaryFile"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/40/filename/binaryFile"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/40/filename/binaryFile"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/40/filename/binaryFile.MD5SUM"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/40/filename/binaryFile.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/40/filename/binaryFile.MD5SUM"
|
||||
|
||||
@@ -28,11 +28,6 @@ public class DdiConfig {
|
||||
|
||||
private final DdiPolling polling;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param polling configuration of the polling for the target
|
||||
*/
|
||||
@JsonCreator
|
||||
public DdiConfig(@JsonProperty("polling") final DdiPolling polling) {
|
||||
this.polling = polling;
|
||||
|
||||
@@ -57,8 +57,7 @@ public class DdiConfirmationBase extends RepresentationModel<DdiConfirmationBase
|
||||
private final DdiAutoConfirmationState autoConfirm;
|
||||
|
||||
@JsonCreator
|
||||
public DdiConfirmationBase(
|
||||
@JsonProperty(value = "autoConfirm", required = true) final DdiAutoConfirmationState autoConfirm) {
|
||||
public DdiConfirmationBase(@JsonProperty(value = "autoConfirm", required = true) final DdiAutoConfirmationState autoConfirm) {
|
||||
this.autoConfirm = autoConfirm;
|
||||
}
|
||||
}
|
||||
@@ -58,10 +58,10 @@ import org.springframework.hateoas.RepresentationModel;
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/binary.tgz"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/binary.tgz"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/binary.tgz"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/binary.tgz.MD5SUM"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/binary.tgz.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/binary.tgz.MD5SUM"
|
||||
@@ -80,10 +80,10 @@ import org.springframework.hateoas.RepresentationModel;
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/file.signature"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/file.signature"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/file.signature"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/file.signature.MD5SUM"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/file.signature.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/file.signature.MD5SUM"
|
||||
@@ -107,10 +107,10 @@ import org.springframework.hateoas.RepresentationModel;
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/binary.tgz"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/binary.tgz"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/binary.tgz"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/binary.tgz.MD5SUM"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/binary.tgz.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/binary.tgz.MD5SUM"
|
||||
@@ -129,10 +129,10 @@ import org.springframework.hateoas.RepresentationModel;
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/file.signature"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/file.signature"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/file.signature"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/file.signature.MD5SUM"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/file.signature.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/file.signature.MD5SUM"
|
||||
@@ -156,10 +156,10 @@ import org.springframework.hateoas.RepresentationModel;
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/binary.tgz"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/binary.tgz"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/binary.tgz"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/binary.tgz.MD5SUM"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/binary.tgz.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/binary.tgz.MD5SUM"
|
||||
@@ -178,10 +178,10 @@ import org.springframework.hateoas.RepresentationModel;
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/file.signature"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/file.signature"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/file.signature"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/file.signature.MD5SUM"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/file.signature.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/file.signature.MD5SUM"
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.springframework.hateoas.RepresentationModel;
|
||||
**_links**: Actions that the server has for the target
|
||||
* **deploymentBase** - Detailed deployment operation
|
||||
* **installedBase** - Detailed operation of last successfully finished action
|
||||
* **configData** - Link which is provided whenever the provisioning target or device is supposed to push its configuration data (aka. "controller attributes") to the server. Only shown for the initial configuration, after a successful update action, or if requested explicitly (e.g. via the management UI)
|
||||
* **configData** - Link which is provided whenever the provisioning target or device is supposed to push its configuration data (aka. "controller attributes") to the server. Only shown for the initial configuration, after a successful update action, or if requested explicitly (e.g. via the management UI)
|
||||
""",
|
||||
example = """
|
||||
{
|
||||
|
||||
@@ -100,7 +100,7 @@ public class DdiDeployment {
|
||||
*/
|
||||
FORCED("forced");
|
||||
|
||||
private String name;
|
||||
private final String name;
|
||||
|
||||
HandlingType(final String name) {
|
||||
this.name = name;
|
||||
@@ -128,7 +128,7 @@ public class DdiDeployment {
|
||||
*/
|
||||
UNAVAILABLE("unavailable");
|
||||
|
||||
private String status;
|
||||
private final String status;
|
||||
|
||||
DdiMaintenanceWindowStatus(final String status) {
|
||||
this.status = status;
|
||||
|
||||
@@ -54,10 +54,10 @@ import org.springframework.hateoas.RepresentationModel;
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/binary.tgz"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/binary.tgz"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/binary.tgz"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/binary.tgz.MD5SUM"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/binary.tgz.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/binary.tgz.MD5SUM"
|
||||
@@ -76,10 +76,10 @@ import org.springframework.hateoas.RepresentationModel;
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/file.signature"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/file.signature"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/file.signature"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/file.signature.MD5SUM"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/file.signature.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/file.signature.MD5SUM"
|
||||
@@ -103,10 +103,10 @@ import org.springframework.hateoas.RepresentationModel;
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/binary.tgz"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/binary.tgz"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/binary.tgz"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/binary.tgz.MD5SUM"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/binary.tgz.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/binary.tgz.MD5SUM"
|
||||
@@ -125,10 +125,10 @@ import org.springframework.hateoas.RepresentationModel;
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/file.signature"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/file.signature"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/file.signature"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/file.signature.MD5SUM"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/file.signature.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/file.signature.MD5SUM"
|
||||
@@ -152,10 +152,10 @@ import org.springframework.hateoas.RepresentationModel;
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/binary.tgz"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/binary.tgz"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/binary.tgz"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/binary.tgz.MD5SUM"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/binary.tgz.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/binary.tgz.MD5SUM"
|
||||
@@ -174,10 +174,10 @@ import org.springframework.hateoas.RepresentationModel;
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/file.signature"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/file.signature"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/file.signature"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/file.signature.MD5SUM"
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/file.signature.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/file.signature.MD5SUM"
|
||||
|
||||
@@ -67,7 +67,7 @@ public class DdiResult {
|
||||
*/
|
||||
NONE("none");
|
||||
|
||||
private String name;
|
||||
private final String name;
|
||||
|
||||
FinalResult(final String name) {
|
||||
this.name = name;
|
||||
|
||||
@@ -13,8 +13,7 @@ package org.eclipse.hawkbit.ddi.json.model;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
/**
|
||||
* Enumerates the supported update modes. Each mode represents an attribute
|
||||
* update strategy.
|
||||
* Enumerates the supported update modes. Each mode represents an attribute update strategy.
|
||||
*
|
||||
* @see DdiConfigData
|
||||
*/
|
||||
|
||||
@@ -100,7 +100,7 @@ public interface DdiRootControllerRestApi {
|
||||
retrieve actions that need to be executed. Those are provided as a list of links to give more detailed
|
||||
information about the action. Links are only available for initial configuration, open actions, or the latest
|
||||
installed action, respectively. The resource supports Etag based modification checks in order to save traffic.
|
||||
|
||||
|
||||
Note: deployments have to be confirmed in order to move on to the next action. Cancellations have to be
|
||||
confirmed or rejected.""")
|
||||
@ApiResponses(value = {
|
||||
@@ -214,7 +214,7 @@ public interface DdiRootControllerRestApi {
|
||||
* the eTag needs to be re-generated
|
||||
* @param actionHistoryMessageCount specifies the number of messages to be returned from action history. Regardless of the passed value,
|
||||
* in order to restrict resource utilization by controllers, maximum number of
|
||||
* messages that are retrieved from database is limited by {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}.
|
||||
* messages that are retrieved from database is limited by RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT.
|
||||
* actionHistoryMessageCount less than zero: retrieves the maximum allowed number of action status messages from history;
|
||||
* actionHistoryMessageCount equal to zero: does not retrieve any message;
|
||||
* actionHistoryMessageCount greater than zero: retrieves the specified number of messages, limited by maximum allowed number.
|
||||
@@ -222,7 +222,7 @@ public interface DdiRootControllerRestApi {
|
||||
*/
|
||||
@Operation(summary = "Resource for software module (Deployment Base)", description = """
|
||||
Core resource for deployment operations. Contains all information necessary in order to execute the operation.
|
||||
|
||||
|
||||
Keep in mind that the provided download links for the artifacts are generated dynamically by the update server.
|
||||
Host, port and path and not guaranteed to be similar to the provided examples below but will be defined at
|
||||
runtime.
|
||||
@@ -230,15 +230,14 @@ public interface DdiRootControllerRestApi {
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = """
|
||||
Successfully retrieved
|
||||
|
||||
|
||||
In case a device provides state information on the feedback channel and won’t store it locally,
|
||||
a query for, e.q, the last 10 messages, could be used which will include the previously provided by the
|
||||
device,
|
||||
feedback.
|
||||
|
||||
device, feedback.
|
||||
|
||||
In addition to the straight forward approach to inform the device to download and install the software
|
||||
in one transaction hawkBit supports the separation of download and installation into separate steps.
|
||||
|
||||
|
||||
This feature is called Maintenance Window where the device is informed to download the software first
|
||||
and then when it enters a defined (maintenance) window the installation triggers follows as usual.
|
||||
"""),
|
||||
@@ -373,7 +372,7 @@ public interface DdiRootControllerRestApi {
|
||||
*/
|
||||
@Operation(summary = "Cancel an action", description = """
|
||||
The Hawkbit server might cancel an operation, e.g. an unfinished update has a successor. It is up to the
|
||||
provisioning target to decide to accept the cancelation or reject it.""")
|
||||
provisioning target to decide to accept the cancellation or reject it.""")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
|
||||
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
|
||||
@@ -450,7 +449,7 @@ public interface DdiRootControllerRestApi {
|
||||
* @param actionId of the {@link DdiDeploymentBase} that matches to installed action.
|
||||
* @param actionHistoryMessageCount specifies the number of messages to be returned from action
|
||||
* history. Regardless of the passed value, in order to restrict resource utilization by controllers, maximum number of
|
||||
* messages that are retrieved from database is limited by {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}.
|
||||
* messages that are retrieved from database is limited by RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT.
|
||||
* actionHistoryMessageCount less than zero: retrieves the maximum allowed number of action status messages from history;
|
||||
* actionHistoryMessageCount equal to zero: does not retrieve any message;
|
||||
* actionHistoryMessageCount greater than zero: retrieves the specified number of messages, limited by maximum allowed number.
|
||||
@@ -461,7 +460,7 @@ public interface DdiRootControllerRestApi {
|
||||
the already finished action, for example in case a re-installation is necessary. The response will be of
|
||||
the same format as the deploymentBase operation, providing the previous action that has been finished
|
||||
successfully. As the action is already finished, no further feedback is expected.
|
||||
|
||||
|
||||
Keep in mind that the provided download links for the artifacts are generated dynamically by the update server.
|
||||
Host, port and path are not guaranteed to be similar to the provided examples below but will be defined at
|
||||
runtime.
|
||||
@@ -470,7 +469,7 @@ public interface DdiRootControllerRestApi {
|
||||
@ApiResponse(responseCode = "200", description = """
|
||||
The response body includes the detailed operation for the already finished action in the same format as
|
||||
for the deploymentBase operation.
|
||||
|
||||
|
||||
In this case the (optional) query for the last 10 messages, previously provided by the device, are included.
|
||||
"""),
|
||||
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
|
||||
@@ -546,7 +545,7 @@ public interface DdiRootControllerRestApi {
|
||||
* needs to be re-generated
|
||||
* @param actionHistoryMessageCount specifies the number of messages to be returned from action history. Regardless of the passed value,
|
||||
* in order to restrict resource utilization by controllers, maximum number of messages that are retrieved from database is limited
|
||||
* by {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}.
|
||||
* by RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT.
|
||||
* actionHistoryMessageCount less than zero: retrieves the maximum allowed number of action status messages from history;
|
||||
* actionHistoryMessageCount equal to zero: does not retrieve any message;
|
||||
* actionHistoryMessageCount greater than zero: retrieves the specified number of messages, limited by maximum allowed number.
|
||||
@@ -556,7 +555,7 @@ public interface DdiRootControllerRestApi {
|
||||
Resource to receive information about a pending confirmation. The response will be of the same format as the
|
||||
deploymentBase operation. The controller should provide feedback about the confirmation first, before
|
||||
processing the deployment.
|
||||
|
||||
|
||||
Keep in mind that the provided download links for the artifacts are generated dynamically by the update server.
|
||||
Host, port and path are not guaranteed to be similar to the provided examples below but will be defined at
|
||||
runtime.
|
||||
|
||||
@@ -49,7 +49,7 @@ class DdiActionHistoryTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the correct deserialization of a model with a additional unknown property
|
||||
* Verify the correct deserialization of a model with an additional unknown property
|
||||
*/
|
||||
@Test
|
||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
|
||||
@@ -50,7 +50,7 @@ class DdiArtifactHashTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the correct deserialization of a model with a additional unknown property
|
||||
* Verify the correct deserialization of a model with an additional unknown property
|
||||
*/
|
||||
@Test
|
||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
|
||||
@@ -53,7 +53,7 @@ class DdiArtifactTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the correct deserialization of a model with a additional unknown property
|
||||
* Verify the correct deserialization of a model with an additional unknown property
|
||||
*/
|
||||
@Test
|
||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
|
||||
@@ -47,7 +47,7 @@ class DdiCancelActionToStopTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the correct deserialization of a model with a additional unknown property
|
||||
* Verify the correct deserialization of a model with an additional unknown property
|
||||
*/
|
||||
@Test
|
||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
|
||||
@@ -48,7 +48,7 @@ class DdiCancelTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the correct deserialization of a model with a additional unknown property
|
||||
* Verify the correct deserialization of a model with an additional unknown property
|
||||
*/
|
||||
@Test
|
||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
|
||||
@@ -54,7 +54,7 @@ class DdiChunkTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the correct deserialization of a model with a additional unknown property
|
||||
* Verify the correct deserialization of a model with an additional unknown property
|
||||
*/
|
||||
@Test
|
||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
|
||||
@@ -46,7 +46,7 @@ class DdiConfigTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the correct deserialization of a model with a additional unknown property
|
||||
* Verify the correct deserialization of a model with an additional unknown property
|
||||
*/
|
||||
@Test
|
||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
|
||||
@@ -64,7 +64,7 @@ class DdiConfirmationBaseTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the correct deserialization of a model with a additional unknown property
|
||||
* Verify the correct deserialization of a model with an additional unknown property
|
||||
*/
|
||||
@Test
|
||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
|
||||
@@ -47,7 +47,7 @@ class DdiControllerBaseTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the correct deserialization of a model with a additional unknown property
|
||||
* Verify the correct deserialization of a model with an additional unknown property
|
||||
*/
|
||||
@Test
|
||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
|
||||
@@ -58,7 +58,7 @@ class DdiDeploymentBaseTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the correct deserialization of a model with a additional unknown property
|
||||
* Verify the correct deserialization of a model with an additional unknown property
|
||||
*/
|
||||
@Test
|
||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
|
||||
@@ -52,7 +52,7 @@ class DdiDeploymentTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the correct deserialization of a model with a additional unknown property
|
||||
* Verify the correct deserialization of a model with an additional unknown property
|
||||
*/
|
||||
@Test
|
||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
|
||||
@@ -48,7 +48,7 @@ class DdiMetadataTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the correct deserialization of a model with a additional unknown property
|
||||
* Verify the correct deserialization of a model with an additional unknown property
|
||||
*/
|
||||
@Test
|
||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
|
||||
@@ -45,7 +45,7 @@ class DdiPollingTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the correct deserialization of a model with a additional unknown property
|
||||
* Verify the correct deserialization of a model with an additional unknown property
|
||||
*/
|
||||
@Test
|
||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
|
||||
@@ -46,7 +46,7 @@ class DdiProgressTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the correct deserialization of a model with a additional unknown property
|
||||
* Verify the correct deserialization of a model with an additional unknown property
|
||||
*/
|
||||
@Test
|
||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
|
||||
@@ -49,7 +49,7 @@ class DdiResultTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the correct deserialization of a model with a additional unknown property
|
||||
* Verify the correct deserialization of a model with an additional unknown property
|
||||
*/
|
||||
@Test
|
||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
|
||||
@@ -55,7 +55,7 @@ class DdiStatusTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the correct deserialization of a model with a additional unknown property
|
||||
* Verify the correct deserialization of a model with an additional unknown property
|
||||
*/
|
||||
@Test
|
||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user