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
|
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.
|
||||||
each release (and milestone) holding 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
|
```shell
|
||||||
mvn clean install -PcheckLicense -DskipTests \
|
$ cd .. && mvn license-tool:license-check -Ddash.fail=false -PcheckLicense
|
||||||
--projects '!org.eclipse.hawkbit:hawkbit-repository-test,!org.eclipse.hawkbit:hawkbit-dmf-rabbitmq-test'
|
```
|
||||||
|
|
||||||
|
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)
|
- name: Check dependency licenses with dash tool (and open issues to Dash IP lab, doesn't fail)
|
||||||
if: ${{ inputs.open_tickets }}
|
if: ${{ inputs.open_tickets }}
|
||||||
run: |
|
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=$(git status --short | wc -l)
|
||||||
CHANGED_FILES_COUNT=${CHANGED_FILES_COUNT//[[:space:]]/}
|
CHANGED_FILES_COUNT=${CHANGED_FILES_COUNT//[[:space:]]/}
|
||||||
echo "Number of changed files: ${CHANGED_FILES_COUNT}"
|
echo "Number of changed files: ${CHANGED_FILES_COUNT}"
|
||||||
|
|||||||
@@ -1,24 +1,33 @@
|
|||||||
hawkBit Docker
|
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
|
## Build
|
||||||
Start the hawkBit Update Server as a single container (requires Docker to be installed and all dependencies to be available)
|
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
|
```bash
|
||||||
$ docker run -d -p 8080:8080 hawkbit/hawkbit-update-server:latest
|
$ docker run -d -p 8080:8080 hawkbit/hawkbit-update-server:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## B: Docker Compose
|
### B: Docker Compose
|
||||||
Start the hawkBit Update Server together with an MySQL and RabbitMQ instance as containers (Requires Docker Compose to be installed)
|
_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
|
```bash
|
||||||
$ docker compose -f mysql/docker-compose-monolith-mysql.yml up
|
$ docker compose -f mysql/docker-compose-monolith-mysql.yml up
|
||||||
```
|
```
|
||||||
You could, also start it in different flavours, with UI or in microservices mode.
|
With the upper command CTRL+C shuts down all services. Add '-d' at the end to start all into detached mode:
|
||||||
|
|
||||||
Note: Whit the upper command CTRL+C shuts down all services. Add '-d' at the end to start all into detached mode:
|
|
||||||
```bash
|
```bash
|
||||||
$ docker compose -f mysql/docker-compose-monolith-mysql.yml up -d
|
$ 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
|
$ docker compose -f mysql/docker-compose-monolith-mysql.yml down
|
||||||
```
|
```
|
||||||
|
|
||||||
# Access
|
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:
|
||||||
| Service / Container | URL | Login | A | B | C |
|
```bash
|
||||||
|--------------------------|--------------------------------------------------|-------------|----------|----------|----------|
|
$ docker compose -f postgres/docker-compose-micro-services-with-simple-ui-postgres.yml up
|
||||||
| 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
|
### Access
|
||||||
You can override application.properties by setting an environment variable SPRING_APPLICATION_JSON for hawkbit container.
|
| 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
|
```yaml
|
||||||
hawkbit:
|
hawkbit:
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ Docker image could be build, for example, with (fixed version 0.4.1 is just an e
|
|||||||
```shell
|
```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
|
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:
|
or just by:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker build --build-arg HAWKBIT_VERSION=0.4.1 -t hawkbit_update_server:0.4.1 .
|
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
|
hawkBit Artifact API
|
||||||
|
===
|
||||||
Various internal interfaces artifact API classes.
|
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
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.artifact;
|
package org.eclipse.hawkbit.artifact;
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -22,17 +22,17 @@ import java.security.NoSuchAlgorithmException;
|
|||||||
import java.util.HexFormat;
|
import java.util.HexFormat;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.eclipse.hawkbit.repository.artifact.exception.ArtifactStoreException;
|
import org.eclipse.hawkbit.artifact.exception.ArtifactStoreException;
|
||||||
import org.eclipse.hawkbit.repository.artifact.exception.HashNotMatchException;
|
import org.eclipse.hawkbit.artifact.exception.HashNotMatchException;
|
||||||
import org.eclipse.hawkbit.repository.artifact.model.AbstractDbArtifact;
|
import org.eclipse.hawkbit.artifact.model.ArtifactHashes;
|
||||||
import org.eclipse.hawkbit.repository.artifact.model.DbArtifactHash;
|
import org.eclipse.hawkbit.artifact.model.StoredArtifactInfo;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract utility class for ArtifactRepository implementations with common functionality, e.g. computation of hashes.
|
* Abstract utility class for ArtifactRepository implementations with common functionality, e.g. computation of hashes.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@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_PREFIX = "tmp";
|
||||||
private static final String TEMP_FILE_SUFFIX = "artifactrepo";
|
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
|
// suppress warning, of not strong enough hashing algorithm, SHA-1 and MD5 is not used security related
|
||||||
@SuppressWarnings("squid:S2070")
|
@SuppressWarnings("squid:S2070")
|
||||||
@Override
|
@Override
|
||||||
public AbstractDbArtifact store(
|
public StoredArtifactInfo store(
|
||||||
final String tenant, final InputStream content, final String filename, final String contentType,
|
final String tenant, final InputStream content, final String filename, final String contentType,
|
||||||
final DbArtifactHash providedHashes) {
|
final ArtifactHashes providedHashes) {
|
||||||
final MessageDigest mdSHA1;
|
final MessageDigest mdSHA1;
|
||||||
final MessageDigest mdMD5;
|
final MessageDigest mdMD5;
|
||||||
final MessageDigest mdSHA256;
|
final MessageDigest mdSHA256;
|
||||||
@@ -60,19 +60,19 @@ public abstract class AbstractArtifactRepository implements ArtifactRepository {
|
|||||||
|
|
||||||
final HexFormat hexFormat = HexFormat.of().withLowerCase();
|
final HexFormat hexFormat = HexFormat.of().withLowerCase();
|
||||||
|
|
||||||
final String sha1Hash16 = hexFormat.formatHex(mdSHA1.digest());
|
final String sha1Hash = hexFormat.formatHex(mdSHA1.digest());
|
||||||
final String md5Hash16 = hexFormat.formatHex(mdMD5.digest());
|
final String md5Hash = hexFormat.formatHex(mdMD5.digest());
|
||||||
final String sha256Hash16 = hexFormat.formatHex(mdSHA256.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
|
// 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
|
// 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) {
|
} catch (final IOException e) {
|
||||||
throw new ArtifactStoreException(e.getMessage(), e);
|
throw new ArtifactStoreException(e.getMessage(), e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -104,13 +104,13 @@ public abstract class AbstractArtifactRepository implements ArtifactRepository {
|
|||||||
return file.getPath();
|
return file.getPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract AbstractDbArtifact store(final String tenant, final DbArtifactHash base16Hashes,
|
protected abstract StoredArtifactInfo store(
|
||||||
final String contentType, final String tempFile) throws IOException;
|
final String tenant, final ArtifactHashes base16Hashes, final String contentType, final String tempFile) throws IOException;
|
||||||
|
|
||||||
// java:S1066 - more readable with separate "if" statements
|
// java:S1066 - more readable with separate "if" statements
|
||||||
// java:S4042 - delete reason is not needed
|
// java:S4042 - delete reason is not needed
|
||||||
@SuppressWarnings({ "java:S1066", "java:S4042" })
|
@SuppressWarnings({ "java:S1066", "java:S4042" })
|
||||||
static File createTempFile(final boolean directory) {
|
public static File createTempFile(final boolean directory) {
|
||||||
try {
|
try {
|
||||||
final File file = (directory
|
final File file = (directory
|
||||||
? Files.createTempDirectory(TEMP_FILE_PREFIX)
|
? Files.createTempDirectory(TEMP_FILE_PREFIX)
|
||||||
@@ -138,22 +138,22 @@ public abstract class AbstractArtifactRepository implements ArtifactRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void checkHashes(final DbArtifactHash providedHashes,
|
private static void checkHashes(
|
||||||
final String sha1Hash16, final String md5Hash16, final String sha256Hash16) {
|
final ArtifactHashes providedHashes, final String sha1Hash16, final String md5Hash16, final String sha256Hash16) {
|
||||||
if (providedHashes == null) {
|
if (providedHashes == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (areHashesNotMatching(providedHashes.getSha1(), sha1Hash16)) {
|
if (areHashesNotMatching(providedHashes.sha1(), sha1Hash16)) {
|
||||||
throw new HashNotMatchException("The given sha1 hash " + providedHashes.getSha1() +
|
throw new HashNotMatchException("The given sha1 hash " + providedHashes.sha1() +
|
||||||
" does not match the calculated sha1 hash " + sha1Hash16, HashNotMatchException.SHA1);
|
" does not match the calculated sha1 hash " + sha1Hash16, HashNotMatchException.SHA1);
|
||||||
}
|
}
|
||||||
if (areHashesNotMatching(providedHashes.getMd5(), md5Hash16)) {
|
if (areHashesNotMatching(providedHashes.md5(), md5Hash16)) {
|
||||||
throw new HashNotMatchException("The given md5 hash " + providedHashes.getMd5() +
|
throw new HashNotMatchException("The given md5 hash " + providedHashes.md5() +
|
||||||
" does not match the calculated md5 hash " + md5Hash16, HashNotMatchException.MD5);
|
" does not match the calculated md5 hash " + md5Hash16, HashNotMatchException.MD5);
|
||||||
}
|
}
|
||||||
if (areHashesNotMatching(providedHashes.getSha256(), sha256Hash16)) {
|
if (areHashesNotMatching(providedHashes.sha256(), sha256Hash16)) {
|
||||||
throw new HashNotMatchException("The given sha256 hash " + providedHashes.getSha256() +
|
throw new HashNotMatchException("The given sha256 hash " + providedHashes.sha256() +
|
||||||
" does not match the calculated sha256 hash " + sha256Hash16, HashNotMatchException.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);
|
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) {
|
private String checkEmpty(final String value, final String fallback) {
|
||||||
return ObjectUtils.isEmpty(value) ? fallback : value;
|
return ObjectUtils.isEmpty(value) ? fallback : value;
|
||||||
}
|
}
|
||||||
@@ -7,22 +7,23 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.artifact;
|
package org.eclipse.hawkbit.artifact;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
import org.eclipse.hawkbit.repository.artifact.exception.ArtifactStoreException;
|
import org.eclipse.hawkbit.artifact.exception.ArtifactBinaryNotFoundException;
|
||||||
import org.eclipse.hawkbit.repository.artifact.exception.HashNotMatchException;
|
import org.eclipse.hawkbit.artifact.exception.ArtifactStoreException;
|
||||||
import org.eclipse.hawkbit.repository.artifact.model.AbstractDbArtifact;
|
import org.eclipse.hawkbit.artifact.exception.HashNotMatchException;
|
||||||
import org.eclipse.hawkbit.repository.artifact.model.DbArtifactHash;
|
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.
|
* 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 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
|
* @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,
|
@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 tenant the tenant to store the artifact
|
||||||
* @param sha1Hash the sha1-hash of the file to lookup.
|
* @param sha1Hash the sha1-hash of the file to lookup.
|
||||||
* @return The artifact file object or {@code null} if no file exists.
|
* @return The artifact file object or {@code null} if no file exists.
|
||||||
* @throws UnsupportedOperationException if implementation does not support the operation
|
* @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
|
* Checks if an artifact exists for a given tenant by its sha1 hash
|
||||||
@@ -7,13 +7,13 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
* 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.io.InputStream;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.eclipse.hawkbit.repository.artifact.exception.ArtifactEncryptionFailedException;
|
import org.eclipse.hawkbit.artifact.exception.ArtifactEncryptionFailedException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface definition for artifact encryption.
|
* Interface definition for artifact encryption.
|
||||||
@@ -23,7 +23,7 @@ public interface ArtifactEncryption {
|
|||||||
/**
|
/**
|
||||||
* Defines the required secret keys for particular encryption algorithm.
|
* Defines the required secret keys for particular encryption algorithm.
|
||||||
*
|
*
|
||||||
* @return list of required secret keys
|
* @return set of required secret keys
|
||||||
*/
|
*/
|
||||||
Set<String> requiredSecretKeys();
|
Set<String> requiredSecretKeys();
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.artifact.encryption;
|
package org.eclipse.hawkbit.artifact.encryption;
|
||||||
|
|
||||||
import java.util.Optional;
|
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
|
* Interface definition for artifact encryption secrets store. It maintains secret key/value pairs
|
||||||
* identified by id (e.g. software module id)
|
* 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.
|
* 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);
|
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.
|
* 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
|
* @param secretKey key of the secret
|
||||||
*/
|
*/
|
||||||
Optional<String> getSecret(final long id, final String secretKey);
|
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
|
* 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.io.InputStream;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -17,12 +17,11 @@ import java.util.Set;
|
|||||||
|
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.NoArgsConstructor;
|
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;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service responsible for encryption operations. Should be registered as a bean in order its autowired dependencies
|
* Service responsible for encryption operations. Should be registered as a bean in order its autowired dependencies to be injected.
|
||||||
* to be injected.
|
|
||||||
*/
|
*/
|
||||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
@SuppressWarnings("java:S6548") // singleton holder ensures static access to spring resources in some places
|
@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 static final ArtifactEncryptionService SINGLETON = new ArtifactEncryptionService();
|
||||||
|
|
||||||
private ArtifactEncryption artifactEncryption;
|
private ArtifactEncryption artifactEncryption;
|
||||||
private ArtifactEncryptionSecretsStore artifactEncryptionSecretsStore;
|
private ArtifactEncryptionSecretsStorage artifactEncryptionSecretsStore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the artifact encryption service singleton instance
|
* @return the artifact encryption service singleton instance
|
||||||
@@ -46,7 +45,7 @@ public final class ArtifactEncryptionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Autowired(required = false) // spring setter injection
|
@Autowired(required = false) // spring setter injection
|
||||||
public void setArtifactEncryptionSecretsStore(final ArtifactEncryptionSecretsStore artifactEncryptionSecretsStore) {
|
public void setArtifactEncryptionSecretsStore(final ArtifactEncryptionSecretsStorage artifactEncryptionSecretsStore) {
|
||||||
this.artifactEncryptionSecretsStore = artifactEncryptionSecretsStore;
|
this.artifactEncryptionSecretsStore = artifactEncryptionSecretsStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +110,7 @@ public final class ArtifactEncryptionService {
|
|||||||
throw new ArtifactEncryptionUnsupportedException("Artifact decryption is not supported.");
|
throw new ArtifactEncryptionUnsupportedException("Artifact decryption is not supported.");
|
||||||
}
|
}
|
||||||
|
|
||||||
final var secrets = getEncryptionSecrets(id);
|
final Map<String, String> secrets = getEncryptionSecrets(id);
|
||||||
try {
|
try {
|
||||||
return artifactEncryption.decryptStream(secrets, encryptedArtifactStream);
|
return artifactEncryption.decryptStream(secrets, encryptedArtifactStream);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.artifact.exception;
|
package org.eclipse.hawkbit.artifact.exception;
|
||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
|
|
||||||
@@ -28,29 +28,7 @@ public class ArtifactBinaryNoLongerExistsException extends AbstractServerRtExcep
|
|||||||
|
|
||||||
private static final SpServerError THIS_ERROR = SpServerError.SP_ARTIFACT_BINARY_DELETED;
|
private static final SpServerError THIS_ERROR = SpServerError.SP_ARTIFACT_BINARY_DELETED;
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new ArtifactBinaryGoneException error.
|
|
||||||
*/
|
|
||||||
public ArtifactBinaryNoLongerExistsException() {
|
public ArtifactBinaryNoLongerExistsException() {
|
||||||
super(THIS_ERROR);
|
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
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.artifact.exception;
|
package org.eclipse.hawkbit.artifact.exception;
|
||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
|
|
||||||
@@ -23,25 +23,7 @@ public final class ArtifactBinaryNotFoundException extends AbstractServerRtExcep
|
|||||||
@Serial
|
@Serial
|
||||||
private static final long serialVersionUID = 1L;
|
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) {
|
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
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.artifact.exception;
|
package org.eclipse.hawkbit.artifact.exception;
|
||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
|
|
||||||
@@ -26,25 +26,7 @@ public final class ArtifactDeleteFailedException extends AbstractServerRtExcepti
|
|||||||
@Serial
|
@Serial
|
||||||
private static final long serialVersionUID = 1L;
|
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) {
|
public ArtifactDeleteFailedException(final Throwable cause) {
|
||||||
super(SpServerError.SP_ARTIFACT_DELETE_FAILED, 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
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.artifact.exception;
|
package org.eclipse.hawkbit.artifact.exception;
|
||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
|
|
||||||
@@ -35,16 +35,8 @@ public final class ArtifactEncryptionFailedException extends AbstractServerRtExc
|
|||||||
@Getter
|
@Getter
|
||||||
private final EncryptionOperation encryptionOperation;
|
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) {
|
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;
|
this.encryptionOperation = encryptionOperation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.artifact.exception;
|
package org.eclipse.hawkbit.artifact.exception;
|
||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
|
|
||||||
@@ -22,17 +22,7 @@ public final class ArtifactEncryptionUnsupportedException extends AbstractServer
|
|||||||
@Serial
|
@Serial
|
||||||
private static final long serialVersionUID = 1L;
|
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) {
|
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
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.artifact.exception;
|
package org.eclipse.hawkbit.artifact.exception;
|
||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
|
|
||||||
@@ -19,12 +19,10 @@ public class ArtifactStoreException extends RuntimeException {
|
|||||||
@Serial
|
@Serial
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
/**
|
public ArtifactStoreException(final String message) {
|
||||||
* Constructs a ArtifactStoreException with message and cause.
|
this(message, null);
|
||||||
*
|
}
|
||||||
* @param message the message of the exception
|
|
||||||
* @param cause of the exception
|
|
||||||
*/
|
|
||||||
public ArtifactStoreException(final String message, final Throwable cause) {
|
public ArtifactStoreException(final String message, final Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.artifact.exception;
|
package org.eclipse.hawkbit.artifact.exception;
|
||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
|
|
||||||
@@ -23,34 +23,7 @@ public final class ArtifactUploadFailedException extends AbstractServerRtExcepti
|
|||||||
@Serial
|
@Serial
|
||||||
private static final long serialVersionUID = 1L;
|
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) {
|
public ArtifactUploadFailedException(final Throwable cause) {
|
||||||
super(SpServerError.SP_ARTIFACT_UPLOAD_FAILED, 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
|
* This program and the accompanying materials are made
|
||||||
* available under the terms of the Eclipse Public License 2.0
|
* available under the terms of the Eclipse Public License 2.0
|
||||||
@@ -7,9 +7,7 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.exception;
|
package org.eclipse.hawkbit.artifact.exception;
|
||||||
|
|
||||||
import static org.eclipse.hawkbit.repository.SizeConversionHelper.byteValueToReadableString;
|
|
||||||
|
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.ToString;
|
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 String MAX_ARTIFACT_SIZE_EXCEEDED = "Maximum artifact size (%s) exceeded.";
|
||||||
private static final SpServerError errorType = SpServerError.SP_FILE_SIZE_QUOTA_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;
|
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) {
|
public FileSizeQuotaExceededException(final long exceededQuotaValue) {
|
||||||
super(createQuotaErrorMessage(exceededQuotaValue), errorType);
|
super(errorType, createQuotaErrorMessage(exceededQuotaValue));
|
||||||
this.exceededQuotaValue = 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) {
|
private static String createQuotaErrorMessage(final long exceededQuotaValue) {
|
||||||
return String.format(MAX_ARTIFACT_SIZE_EXCEEDED, byteValueToReadableString(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
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.artifact.exception;
|
package org.eclipse.hawkbit.artifact.exception;
|
||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
|
|
||||||
@@ -28,12 +28,6 @@ public class HashNotMatchException extends RuntimeException {
|
|||||||
|
|
||||||
private final String hashFunction;
|
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) {
|
public HashNotMatchException(final String message, final String hashFunction) {
|
||||||
super(message);
|
super(message);
|
||||||
this.hashFunction = hashFunction;
|
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
|
* This program and the accompanying materials are made
|
||||||
* available under the terms of the Eclipse Public License 2.0
|
* available under the terms of the Eclipse Public License 2.0
|
||||||
@@ -7,9 +7,7 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.exception;
|
package org.eclipse.hawkbit.artifact.exception;
|
||||||
|
|
||||||
import static org.eclipse.hawkbit.repository.SizeConversionHelper.byteValueToReadableString;
|
|
||||||
|
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
@@ -28,26 +26,12 @@ public class StorageQuotaExceededException extends AbstractServerRtException {
|
|||||||
|
|
||||||
private final long exceededQuotaValue;
|
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) {
|
public StorageQuotaExceededException(final long exceededQuotaValue) {
|
||||||
super(createQuotaErrorMessage(exceededQuotaValue), errorType);
|
super(errorType, createQuotaErrorMessage(exceededQuotaValue));
|
||||||
this.exceededQuotaValue = 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) {
|
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
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.artifact.model;
|
package org.eclipse.hawkbit.artifact.model;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database representation of artifact.
|
* Info for an imported artifact binary.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
public abstract class AbstractDbArtifact implements DbArtifact {
|
public class StoredArtifactInfo {
|
||||||
|
|
||||||
private final String artifactId;
|
|
||||||
private final long size;
|
|
||||||
private final String contentType;
|
private final String contentType;
|
||||||
|
private final long size;
|
||||||
|
private final ArtifactHashes hashes;
|
||||||
|
|
||||||
private DbArtifactHash hashes;
|
public StoredArtifactInfo(final String contentType, final long size, final ArtifactHashes 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");
|
|
||||||
this.hashes = Objects.requireNonNull(hashes, "Hashes cannot be null");
|
this.hashes = Objects.requireNonNull(hashes, "Hashes cannot be null");
|
||||||
this.size = size;
|
|
||||||
this.contentType = contentType;
|
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
|
* 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.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -19,14 +19,13 @@ import lombok.Data;
|
|||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Artifact handler properties class for holding all supported protocols with
|
* Artifact handler properties class for holding all supported protocols with host, ip, port and download pattern.
|
||||||
* host, ip, port and download pattern.
|
|
||||||
*
|
*
|
||||||
* @see PropertyBasedArtifactUrlHandler
|
* @see PropertyBasedArtifactUrlResolver
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@ConfigurationProperties("hawkbit.artifact.url")
|
@ConfigurationProperties("hawkbit.artifact.url")
|
||||||
public class ArtifactUrlHandlerProperties {
|
public class PropertyBasedArtifactUrlResolverProperties {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rel as key and complete protocol as value.
|
* 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
|
* 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.
|
* 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.
|
* 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";
|
private String ip = "127.0.0.1";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,9 +79,9 @@ public class ArtifactUrlHandlerProperties {
|
|||||||
/**
|
/**
|
||||||
* Support for the following hawkBit API.
|
* 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);
|
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
|
* 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.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
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;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
* 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;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@@ -15,8 +15,8 @@ import java.net.URI;
|
|||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.eclipse.hawkbit.repository.artifact.urlhandler.ArtifactUrlHandlerProperties.UrlProtocol;
|
import org.eclipse.hawkbit.artifact.urlresolver.ArtifactUrlResolver.DownloadDescriptor;
|
||||||
import org.eclipse.hawkbit.repository.artifact.urlhandler.URLPlaceholder.SoftwareData;
|
import org.eclipse.hawkbit.artifact.urlresolver.PropertyBasedArtifactUrlResolverProperties.UrlProtocol;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
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
|
* Story: Test to generate the artifact download URL
|
||||||
*/
|
*/
|
||||||
@ExtendWith(MockitoExtension.class)
|
@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 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 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 String HTTP_LOCALHOST = "http://localhost:8080/";
|
||||||
private static final URLPlaceholder placeHolder = new URLPlaceholder(
|
private static final DownloadDescriptor DOWNLOAD_DESCRIPTOR = new DownloadDescriptor(TENANT, CONTROLLER_ID, SOFTWARE_MODULE_ID, FILENAME,
|
||||||
TENANT, TENANT_ID, CONTROLLER_ID, TARGET_ID,
|
SHA1);
|
||||||
new SoftwareData(SOFTWARE_MODULE_ID, FILENAME_DECODE, ARTIFACT_ID, SHA1HASH));
|
|
||||||
private ArtifactUrlHandler urlHandlerUnderTest;
|
private ArtifactUrlResolver urlHandlerUnderTest;
|
||||||
private ArtifactUrlHandlerProperties properties;
|
private PropertyBasedArtifactUrlResolverProperties properties;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setup() {
|
void setup() {
|
||||||
properties = new ArtifactUrlHandlerProperties();
|
properties = new PropertyBasedArtifactUrlResolverProperties();
|
||||||
urlHandlerUnderTest = new PropertyBasedArtifactUrlHandler(properties, "");
|
urlHandlerUnderTest = new PropertyBasedArtifactUrlResolver(properties, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests the generation of http download url.
|
|
||||||
*/
|
|
||||||
@Test
|
@Test
|
||||||
void urlGenerationWithDefaultConfiguration() {
|
void urlGenerationWithDefaultConfiguration() {
|
||||||
properties.getProtocols().put("download-http", new UrlProtocol());
|
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(
|
assertThat(ddiUrls).containsExactly(
|
||||||
new ArtifactUrl(
|
new ArtifactUrl(
|
||||||
"http".toUpperCase(), "download-http",
|
"http".toUpperCase(), "download-http",
|
||||||
HTTP_LOCALHOST + TENANT + "/controller/v1/" +
|
HTTP_LOCALHOST + TENANT + "/controller/v1/" +
|
||||||
CONTROLLER_ID + "/softwaremodules/" + SOFTWARE_MODULE_ID + "/artifacts/" + FILENAME_ENCODE))
|
CONTROLLER_ID + "/softwaremodules/" + SOFTWARE_MODULE_ID + "/artifacts/" + FILENAME_ENCODED))
|
||||||
.isEqualTo(urlHandlerUnderTest.getUrls(placeHolder, ApiType.DMF));
|
.isEqualTo(urlHandlerUnderTest.getUrls(DOWNLOAD_DESCRIPTOR, ArtifactUrlResolver.ApiType.DMF));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests the generation of custom download url with a CoAP example that supports DMF only.
|
|
||||||
*/
|
|
||||||
@Test
|
@Test
|
||||||
void urlGenerationWithCustomConfiguration() {
|
void urlGenerationWithCustomConfiguration() {
|
||||||
final UrlProtocol proto = new UrlProtocol();
|
final UrlProtocol proto = new UrlProtocol();
|
||||||
@@ -85,41 +74,17 @@ class PropertyBasedArtifactUrlHandlerTest {
|
|||||||
proto.setPort(5683);
|
proto.setPort(5683);
|
||||||
proto.setProtocol(TEST_PROTO);
|
proto.setProtocol(TEST_PROTO);
|
||||||
proto.setRel(TEST_REL);
|
proto.setRel(TEST_REL);
|
||||||
proto.setSupports(List.of(ApiType.DMF));
|
proto.setSupports(List.of(ArtifactUrlResolver.ApiType.DMF));
|
||||||
proto.setRef("{protocol}://{ip}:{port}/fw/{tenant}/{controllerId}/sha1/{artifactSHA1}");
|
proto.setRef("{protocol}://{ip}:{port}/fw/{tenant}/{controllerId}/sha1/{artifactFileName}");
|
||||||
properties.getProtocols().put(TEST_PROTO, proto);
|
properties.getProtocols().put(TEST_PROTO, proto);
|
||||||
|
|
||||||
assertThat(urlHandlerUnderTest.getUrls(placeHolder, ApiType.DDI)).isEmpty();
|
assertThat(urlHandlerUnderTest.getUrls(DOWNLOAD_DESCRIPTOR, ArtifactUrlResolver.ApiType.DDI)).isEmpty();
|
||||||
assertThat(urlHandlerUnderTest.getUrls(placeHolder, ApiType.DMF)).containsExactly(
|
assertThat(urlHandlerUnderTest.getUrls(DOWNLOAD_DESCRIPTOR, ArtifactUrlResolver.ApiType.DMF)).containsExactly(
|
||||||
new ArtifactUrl(
|
new ArtifactUrl(
|
||||||
TEST_PROTO.toUpperCase(), TEST_REL,
|
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
|
@Test
|
||||||
void urlGenerationWithHostFromRequest() throws URISyntaxException {
|
void urlGenerationWithHostFromRequest() throws URISyntaxException {
|
||||||
final String testHost = "ddi.host.com";
|
final String testHost = "ddi.host.com";
|
||||||
@@ -129,14 +94,15 @@ class PropertyBasedArtifactUrlHandlerTest {
|
|||||||
proto.setPort(5683);
|
proto.setPort(5683);
|
||||||
proto.setProtocol(TEST_PROTO);
|
proto.setProtocol(TEST_PROTO);
|
||||||
proto.setRel(TEST_REL);
|
proto.setRel(TEST_REL);
|
||||||
proto.setSupports(List.of(ApiType.DDI));
|
proto.setSupports(List.of(ArtifactUrlResolver.ApiType.DDI));
|
||||||
proto.setRef("{protocol}://{hostnameRequest}:{port}/fws/{tenant}/{targetIdBase62}/{artifactIdBase62}");
|
proto.setRef("{protocol}://{hostnameRequest}:{port}/fws/{tenant}/{controllerId}/{artifactFileName}");
|
||||||
properties.getProtocols().put("ftp", proto);
|
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(
|
new ArtifactUrl(
|
||||||
TEST_PROTO.toUpperCase(), TEST_REL,
|
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 String testHost = "ddi.host.com";
|
||||||
|
|
||||||
final UrlProtocol proto = new UrlProtocol();
|
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);
|
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(
|
new ArtifactUrl(
|
||||||
"http".toUpperCase(), "download-http",
|
"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
|
@Test
|
||||||
void urlGenerationWithPortFromRequest() throws URISyntaxException {
|
void urlGenerationWithPortFromRequest() throws URISyntaxException {
|
||||||
final UrlProtocol proto = new UrlProtocol();
|
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);
|
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(
|
new ArtifactUrl(
|
||||||
"http".toUpperCase(), "download-http",
|
"http".toUpperCase(), "download-http",
|
||||||
"http://localhost:8083/" + TENANT + "/controller/v1/" +
|
"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(
|
new ArtifactUrl(
|
||||||
"http".toUpperCase(), "download-http",
|
"http".toUpperCase(), "download-http",
|
||||||
"http://localhost:8080/" + TENANT + "/controller/v1/" +
|
"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 {
|
void urlGenerationWithPortFromRequestForHttps() throws URISyntaxException {
|
||||||
final String protocol = "https";
|
final String protocol = "https";
|
||||||
final UrlProtocol proto = new UrlProtocol();
|
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);
|
proto.setProtocol(protocol);
|
||||||
properties.getProtocols().put("download-http", proto);
|
properties.getProtocols().put("download-http", proto);
|
||||||
|
|
||||||
final URI uri = new URI(protocol + "://anotherHost.com");
|
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(
|
new ArtifactUrl(
|
||||||
protocol.toUpperCase(), "download-http",
|
protocol.toUpperCase(), "download-http",
|
||||||
uri + "/" + TENANT + "/controller/v1/" +
|
uri + "/" + TENANT + "/controller/v1/" + CONTROLLER_ID + "/softwaremodules/" + SOFTWARE_MODULE_ID + "/artifacts/" + FILENAME_ENCODED));
|
||||||
CONTROLLER_ID + "/softwaremodules/" + SOFTWARE_MODULE_ID + "/artifacts/" + FILENAME_ENCODE));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,20 +174,22 @@ class PropertyBasedArtifactUrlHandlerTest {
|
|||||||
void urlGenerationWithDomainFromRequest() throws URISyntaxException {
|
void urlGenerationWithDomainFromRequest() throws URISyntaxException {
|
||||||
final UrlProtocol proto = new UrlProtocol();
|
final UrlProtocol proto = new UrlProtocol();
|
||||||
proto.setHostname("host.bumlux.net");
|
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);
|
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(
|
new ArtifactUrl(
|
||||||
"http".toUpperCase(), "download-http",
|
"http".toUpperCase(), "download-http",
|
||||||
"http://host.com/" + TENANT + "/controller/v1/" +
|
"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(
|
new ArtifactUrl(
|
||||||
"http".toUpperCase(), "download-http",
|
"http".toUpperCase(), "download-http",
|
||||||
"http://host.bumlux.net/" + TENANT + "/controller/v1/" +
|
"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>
|
<version>${revision}</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>hawkbit-artifact-repository-filesystem</artifactId>
|
<artifactId>hawkbit-artifact-fs</artifactId>
|
||||||
<name>hawkBit :: Artifact :: Filesystem Artifact Repository</name>
|
<name>hawkBit :: Artifact :: Filesystem Artifact Repository</name>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.artifact;
|
package org.eclipse.hawkbit.artifact.fs;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
@@ -17,7 +17,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
|||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@ConfigurationProperties("org.eclipse.hawkbit.repository.file")
|
@ConfigurationProperties("org.eclipse.hawkbit.repository.file")
|
||||||
public class ArtifactFilesystemProperties {
|
public class FileArtifactProperties {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base-path of the directory to store the artifacts.
|
* 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
|
* 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.artifact.ArtifactStorage;
|
||||||
import org.eclipse.hawkbit.repository.artifact.ArtifactFilesystemRepository;
|
import org.eclipse.hawkbit.artifact.fs.FileArtifactProperties;
|
||||||
import org.eclipse.hawkbit.repository.artifact.ArtifactRepository;
|
import org.eclipse.hawkbit.artifact.fs.FileArtifactStorage;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for the {@link ArtifactFilesystemRepository}.
|
* Configuration for the {@link FileArtifactStorage}.
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableConfigurationProperties(ArtifactFilesystemProperties.class)
|
@EnableConfigurationProperties(FileArtifactProperties.class)
|
||||||
public class ArtifactFilesystemConfiguration {
|
public class FileArtifactStorageConfiguration {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param artifactFilesystemProperties the artifact file system properties
|
* @param artifactFilesystemProperties the artifact file system properties
|
||||||
* @return Default {@link ArtifactRepository} implementation.
|
* @return Default {@link ArtifactStorage} implementation.
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
public ArtifactRepository artifactRepository(final ArtifactFilesystemProperties artifactFilesystemProperties) {
|
public ArtifactStorage artifactRepository(final FileArtifactProperties artifactFilesystemProperties) {
|
||||||
return new ArtifactFilesystemRepository(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
|
* 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.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
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.FileUtils;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.assertj.core.api.Assertions;
|
import org.assertj.core.api.Assertions;
|
||||||
import org.eclipse.hawkbit.repository.artifact.exception.ArtifactBinaryNotFoundException;
|
import org.eclipse.hawkbit.artifact.AbstractArtifactStorage;
|
||||||
import org.eclipse.hawkbit.repository.artifact.model.AbstractDbArtifact;
|
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.AfterAll;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.Test;
|
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
|
* Story: Test storing artifact binaries in the file-system
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
class ArtifactFilesystemRepositoryTest {
|
class FileArtifactStorageTest {
|
||||||
|
|
||||||
private static final String TENANT = "test_tenant";
|
private static final String TENANT = "test_tenant";
|
||||||
|
|
||||||
private static ArtifactFilesystemProperties artifactResourceProperties;
|
private static FileArtifactProperties artifactResourceProperties;
|
||||||
private static ArtifactFilesystemRepository artifactFilesystemRepository;
|
private static FileArtifactStorage artifactFilesystemRepository;
|
||||||
|
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
static void setup() {
|
static void setup() {
|
||||||
artifactResourceProperties = new ArtifactFilesystemProperties();
|
artifactResourceProperties = new FileArtifactProperties();
|
||||||
artifactResourceProperties.setPath(AbstractArtifactRepository.createTempFile(true).toString());
|
artifactResourceProperties.setPath(AbstractArtifactStorage.createTempFile(true).toString());
|
||||||
|
|
||||||
artifactFilesystemRepository = new ArtifactFilesystemRepository(artifactResourceProperties);
|
artifactFilesystemRepository = new FileArtifactStorage(artifactResourceProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterAll
|
@AfterAll
|
||||||
@@ -64,10 +65,10 @@ class ArtifactFilesystemRepositoryTest {
|
|||||||
@Test
|
@Test
|
||||||
void storeSuccessfully() throws IOException {
|
void storeSuccessfully() throws IOException {
|
||||||
final byte[] fileContent = randomBytes();
|
final byte[] fileContent = randomBytes();
|
||||||
final AbstractDbArtifact artifact = storeRandomArtifact(fileContent);
|
final StoredArtifactInfo artifact = storeRandomArtifact(fileContent);
|
||||||
|
|
||||||
final byte[] readContent = new byte[fileContent.length];
|
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);
|
assertThat(readContent).isEqualTo(fileContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,9 +78,9 @@ class ArtifactFilesystemRepositoryTest {
|
|||||||
@Test
|
@Test
|
||||||
void getStoredArtifactBasedOnSHA1Hash() throws IOException {
|
void getStoredArtifactBasedOnSHA1Hash() throws IOException {
|
||||||
final byte[] fileContent = randomBytes();
|
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
|
@Test
|
||||||
void deleteStoredArtifactBySHA1Hash() throws IOException {
|
void deleteStoredArtifactBySHA1Hash() throws IOException {
|
||||||
final AbstractDbArtifact artifact = storeRandomArtifact(randomBytes());
|
final StoredArtifactInfo artifact = storeRandomArtifact(randomBytes());
|
||||||
artifactFilesystemRepository.deleteBySha1(TENANT, artifact.getHashes().getSha1());
|
artifactFilesystemRepository.deleteBySha1(TENANT, artifact.getHashes().sha1());
|
||||||
|
|
||||||
final String sha1Hash = artifact.getHashes().getSha1();
|
final String sha1Hash = artifact.getHashes().sha1();
|
||||||
assertThatExceptionOfType(ArtifactBinaryNotFoundException.class)
|
assertThatExceptionOfType(ArtifactBinaryNotFoundException.class)
|
||||||
.isThrownBy(() -> artifactFilesystemRepository.getBySha1(TENANT, sha1Hash));
|
.isThrownBy(() -> artifactFilesystemRepository.getBySha1(TENANT, sha1Hash));
|
||||||
}
|
}
|
||||||
@@ -100,10 +101,10 @@ class ArtifactFilesystemRepositoryTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void deleteStoredArtifactOfTenant() throws IOException {
|
void deleteStoredArtifactOfTenant() throws IOException {
|
||||||
final AbstractDbArtifact artifact = storeRandomArtifact(randomBytes());
|
final StoredArtifactInfo artifact = storeRandomArtifact(randomBytes());
|
||||||
artifactFilesystemRepository.deleteByTenant(TENANT);
|
artifactFilesystemRepository.deleteByTenant(TENANT);
|
||||||
|
|
||||||
final String sha1Hash = artifact.getHashes().getSha1();
|
final String sha1Hash = artifact.getHashes().sha1();
|
||||||
assertThatExceptionOfType(ArtifactBinaryNotFoundException.class)
|
assertThatExceptionOfType(ArtifactBinaryNotFoundException.class)
|
||||||
.isThrownBy(() -> artifactFilesystemRepository.getBySha1(TENANT, sha1Hash));
|
.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");
|
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 {
|
try {
|
||||||
artifactFilesystemRepository.deleteBySha1("tenantWhichDoesNotExist", artifact.getHashes().getSha1());
|
artifactFilesystemRepository.deleteBySha1("tenantWhichDoesNotExist", artifact.getHashes().sha1());
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
Assertions.fail("did not expect an exception while deleting a file which does not exists");
|
Assertions.fail("did not expect an exception while deleting a file which does not exists");
|
||||||
}
|
}
|
||||||
@@ -133,7 +134,7 @@ class ArtifactFilesystemRepositoryTest {
|
|||||||
return randomBytes;
|
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)) {
|
try (final ByteArrayInputStream inputStream = new ByteArrayInputStream(fileContent)) {
|
||||||
return artifactFilesystemRepository.store(TENANT, inputStream, "filename.tmp", "application/txt", null);
|
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>
|
<modules>
|
||||||
<module>hawkbit-artifact-api</module>
|
<module>hawkbit-artifact-api</module>
|
||||||
<module>hawkbit-artifact-repository-filesystem</module>
|
<module>hawkbit-artifact-fs</module>
|
||||||
</modules>
|
</modules>
|
||||||
</project>
|
</project>
|
||||||
@@ -9,9 +9,9 @@
|
|||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.autoconfigure.artifact;
|
package org.eclipse.hawkbit.autoconfigure.artifact;
|
||||||
|
|
||||||
import org.eclipse.hawkbit.repository.artifact.urlhandler.ArtifactUrlHandler;
|
import org.eclipse.hawkbit.artifact.urlresolver.ArtifactUrlResolver;
|
||||||
import org.eclipse.hawkbit.repository.artifact.urlhandler.ArtifactUrlHandlerProperties;
|
import org.eclipse.hawkbit.artifact.urlresolver.PropertyBasedArtifactUrlResolver;
|
||||||
import org.eclipse.hawkbit.repository.artifact.urlhandler.PropertyBasedArtifactUrlHandler;
|
import org.eclipse.hawkbit.artifact.urlresolver.PropertyBasedArtifactUrlResolverProperties;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
@@ -20,22 +20,18 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.context.annotation.PropertySource;
|
import org.springframework.context.annotation.PropertySource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Autoconfiguration for {@link ArtifactUrlHandler} based on a properties.
|
* Autoconfiguration for {@link ArtifactUrlResolver} based on a properties.
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableConfigurationProperties({ ArtifactUrlHandlerProperties.class })
|
@EnableConfigurationProperties({ PropertyBasedArtifactUrlResolverProperties.class })
|
||||||
@PropertySource("classpath:/hawkbit-artifactdl-defaults.properties")
|
@PropertySource("classpath:/hawkbit-artifactdl-defaults.properties")
|
||||||
public class ArtifactUrlHandlerAutoConfiguration {
|
public class PropertyBasedArtifactUrlResolverAutoConfiguration {
|
||||||
|
|
||||||
/**
|
|
||||||
* @param urlHandlerProperties for bean configuration
|
|
||||||
* @return PropertyBasedArtifactUrlHandler bean
|
|
||||||
*/
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean(ArtifactUrlHandler.class)
|
@ConditionalOnMissingBean(ArtifactUrlResolver.class)
|
||||||
PropertyBasedArtifactUrlHandler propertyBasedArtifactUrlHandler(
|
PropertyBasedArtifactUrlResolver propertyBasedArtifactUrlHandler(
|
||||||
final ArtifactUrlHandlerProperties urlHandlerProperties,
|
final PropertyBasedArtifactUrlResolverProperties urlHandlerProperties,
|
||||||
@Value("${server.servlet.context-path:}") final String contextPath) {
|
@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;
|
package org.eclipse.hawkbit.autoconfigure.cache;
|
||||||
|
|
||||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
import org.eclipse.hawkbit.cache.TenancyCacheManager;
|
import org.eclipse.hawkbit.tenancy.cache.TenantCacheManager;
|
||||||
import org.eclipse.hawkbit.cache.TenantAwareCacheManager;
|
import org.eclipse.hawkbit.tenancy.cache.TenantAwareCacheManager;
|
||||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
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
|
* A configuration for configuring the spring {@link CacheManager} for specific multi-tenancy caching. The caches between
|
||||||
* tenants must not interfere each other.
|
* tenants must not interfere each other.
|
||||||
* <p/>
|
* <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
|
@Configuration
|
||||||
@EnableCaching
|
@EnableCaching
|
||||||
@@ -39,9 +39,8 @@ public class CacheAutoConfiguration {
|
|||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
@Primary
|
@Primary
|
||||||
TenancyCacheManager cacheManager(
|
TenantCacheManager cacheManager(
|
||||||
@Qualifier("directCacheManager") final CacheManager directCacheManager,
|
@Qualifier("directCacheManager") final CacheManager directCacheManager, final TenantAware tenantAware) {
|
||||||
final TenantAware tenantAware) {
|
|
||||||
return new TenantAwareCacheManager(directCacheManager, tenantAware);
|
return new TenantAwareCacheManager(directCacheManager, tenantAware);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,9 +62,7 @@ public class CacheAutoConfiguration {
|
|||||||
final CaffeineCacheManager cacheManager = new CaffeineCacheManager();
|
final CaffeineCacheManager cacheManager = new CaffeineCacheManager();
|
||||||
|
|
||||||
if (cacheProperties.getTtl() > 0) {
|
if (cacheProperties.getTtl() > 0) {
|
||||||
final Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder()
|
cacheManager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(cacheProperties.getTtl(), cacheProperties.getTtlUnit()));
|
||||||
.expireAfterWrite(cacheProperties.getTtl(), cacheProperties.getTtlUnit());
|
|
||||||
cacheManager.setCaffeine(cacheBuilder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cacheManager;
|
return cacheManager;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
|||||||
* After lifetime the data gets reloaded out of the database.
|
* After lifetime the data gets reloaded out of the database.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@ConfigurationProperties("hawkbit.cache.global")
|
@ConfigurationProperties("hawkbit.cache")
|
||||||
public class CacheProperties {
|
public class CacheProperties {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Autoconfiguration for the events..
|
* Autoconfiguration for the events.
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@Import(EventPublisherConfiguration.class)
|
@Import(EventPublisherConfiguration.class)
|
||||||
|
|||||||
@@ -38,13 +38,13 @@ import org.springframework.security.concurrent.DelegatingSecurityContextSchedule
|
|||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableConfigurationProperties(AsyncConfigurerThreadPoolProperties.class)
|
@EnableConfigurationProperties(ExecutorProperties.class)
|
||||||
public class ExecutorAutoConfiguration {
|
public class ExecutorAutoConfiguration {
|
||||||
|
|
||||||
private final AsyncConfigurerThreadPoolProperties asyncConfigurerProperties;
|
private final ExecutorProperties executorProperties;
|
||||||
|
|
||||||
public ExecutorAutoConfiguration(final AsyncConfigurerThreadPoolProperties asyncConfigurerProperties) {
|
public ExecutorAutoConfiguration(final ExecutorProperties executorProperties) {
|
||||||
this.asyncConfigurerProperties = asyncConfigurerProperties;
|
this.executorProperties = executorProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,7 +72,7 @@ public class ExecutorAutoConfiguration {
|
|||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
public ScheduledExecutorService scheduledExecutorService() {
|
public ScheduledExecutorService scheduledExecutorService() {
|
||||||
return new DelegatingSecurityContextScheduledExecutorService(Executors.newScheduledThreadPool(
|
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.
|
* @return central ThreadPoolExecutor for general purpose multithreaded operations. Tries an orderly shutdown when destroyed.
|
||||||
*/
|
*/
|
||||||
private ThreadPoolExecutor threadPoolExecutor() {
|
private ThreadPoolExecutor threadPoolExecutor() {
|
||||||
final BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(asyncConfigurerProperties.getQueueSize());
|
final BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(executorProperties.getQueueSize());
|
||||||
return new ThreadPoolExecutor(asyncConfigurerProperties.getCoreThreads(),
|
return new ThreadPoolExecutor(executorProperties.getCoreThreads(),
|
||||||
asyncConfigurerProperties.getMaxThreads(), asyncConfigurerProperties.getIdleTimeout(),
|
executorProperties.getMaxThreads(), executorProperties.getIdleTimeout(),
|
||||||
TimeUnit.MILLISECONDS, blockingQueue,
|
TimeUnit.MILLISECONDS, blockingQueue,
|
||||||
threadFactory("central-executor-pool-%d"),
|
threadFactory("central-executor-pool-%d"),
|
||||||
new PoolSizeExceededPolicy());
|
new PoolSizeExceededPolicy());
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
|||||||
* Properties for the async configurer.
|
* Properties for the async configurer.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@ConfigurationProperties("hawkbit.threadpool")
|
@ConfigurationProperties("hawkbit.executor")
|
||||||
public class AsyncConfigurerThreadPoolProperties {
|
public class ExecutorProperties {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Max queue size for central event executor.
|
* Max queue size for central event executor.
|
||||||
@@ -25,11 +25,12 @@ import org.springframework.security.config.annotation.authentication.configurati
|
|||||||
@Configuration
|
@Configuration
|
||||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||||
@EnableConfigurationProperties({ TenantAwareUserProperties.class })
|
@EnableConfigurationProperties({ TenantAwareUserProperties.class })
|
||||||
public class InMemoryUserManagementAutoConfiguration extends GlobalAuthenticationConfigurerAdapter {
|
public class StaticUserManagementAutoConfiguration extends GlobalAuthenticationConfigurerAdapter {
|
||||||
|
|
||||||
private final StaticAuthenticationProvider authenticationProvider;
|
private final StaticAuthenticationProvider authenticationProvider;
|
||||||
|
|
||||||
InMemoryUserManagementAutoConfiguration(final SecurityProperties securityProperties,
|
StaticUserManagementAutoConfiguration(
|
||||||
|
final SecurityProperties securityProperties,
|
||||||
final TenantAwareUserProperties tenantAwareUserProperties) {
|
final TenantAwareUserProperties tenantAwareUserProperties) {
|
||||||
authenticationProvider = new StaticAuthenticationProvider(tenantAwareUserProperties, securityProperties);
|
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.cache.CacheAutoConfiguration
|
||||||
org.eclipse.hawkbit.autoconfigure.repository.event.EventPublisherAutoConfiguration
|
org.eclipse.hawkbit.autoconfigure.repository.event.EventPublisherAutoConfiguration
|
||||||
org.eclipse.hawkbit.autoconfigure.repository.JpaRepositoryAutoConfiguration
|
org.eclipse.hawkbit.autoconfigure.repository.JpaRepositoryAutoConfiguration
|
||||||
org.eclipse.hawkbit.autoconfigure.scheduling.AsyncConfigurerAutoConfiguration
|
org.eclipse.hawkbit.autoconfigure.scheduling.AsyncConfigurerAutoConfiguration
|
||||||
org.eclipse.hawkbit.autoconfigure.scheduling.ExecutorAutoConfiguration
|
org.eclipse.hawkbit.autoconfigure.scheduling.ExecutorAutoConfiguration
|
||||||
org.eclipse.hawkbit.autoconfigure.security.SecurityAutoConfiguration
|
org.eclipse.hawkbit.autoconfigure.security.SecurityAutoConfiguration
|
||||||
org.eclipse.hawkbit.autoconfigure.security.InMemoryUserManagementAutoConfiguration
|
org.eclipse.hawkbit.autoconfigure.security.StaticUserManagementAutoConfiguration
|
||||||
|
|||||||
@@ -23,10 +23,3 @@ hawkbit.artifact.url.protocols.md5sum-http.ip=${hawkbit.artifact.url.protocols.d
|
|||||||
hawkbit.artifact.url.protocols.md5sum-http.port=${hawkbit.artifact.url.protocols.download-http.port}
|
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.supports=DDI
|
||||||
hawkbit.artifact.url.protocols.md5sum-http.ref=${hawkbit.artifact.url.protocols.download-http.ref}.MD5SUM
|
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}
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
# hawkBit Core
|
# 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 SpServerError error;
|
||||||
private final transient Map<String, Object> info;
|
private final transient Map<String, Object> info;
|
||||||
|
|
||||||
/**
|
|
||||||
* Parameterized constructor.
|
|
||||||
*
|
|
||||||
* @param error detail
|
|
||||||
*/
|
|
||||||
protected AbstractServerRtException(final SpServerError error) {
|
protected AbstractServerRtException(final SpServerError error) {
|
||||||
super(error.getMessage());
|
this(error, error.getMessage());
|
||||||
this.error = error;
|
|
||||||
this.info = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected AbstractServerRtException(final SpServerError error, final String message) {
|
||||||
* Parameterized constructor.
|
this(error, message, (Map<String, Object>) null);
|
||||||
*
|
|
||||||
* @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, final Map<String, Object> info) {
|
||||||
* Parameterized constructor.
|
this(error, message, info, null);
|
||||||
*
|
|
||||||
* @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 Throwable cause) {
|
||||||
* Parameterized constructor.
|
this(error, message, null, cause);
|
||||||
*
|
|
||||||
* @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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parameterized constructor.
|
|
||||||
*
|
|
||||||
* @param error detail
|
|
||||||
* @param cause of the exception
|
|
||||||
*/
|
|
||||||
protected AbstractServerRtException(final SpServerError error, final Throwable cause) {
|
protected AbstractServerRtException(final SpServerError error, final Throwable cause) {
|
||||||
super(error.getMessage(), cause);
|
this(error, error.getMessage(), null, cause);
|
||||||
this.error = error;
|
|
||||||
this.info = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parameterized constructor.
|
|
||||||
*
|
|
||||||
* @param message custom error message
|
|
||||||
* @param error detail
|
|
||||||
* @param cause of the exception
|
|
||||||
*/
|
|
||||||
protected AbstractServerRtException(
|
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);
|
super(message, cause);
|
||||||
this.error = error;
|
this.error = error;
|
||||||
this.info = info;
|
this.info = info;
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ public interface TenantAware {
|
|||||||
if (context.getAuthentication() != null) {
|
if (context.getAuthentication() != null) {
|
||||||
final Object principal = context.getAuthentication().getPrincipal();
|
final Object principal = context.getAuthentication().getPrincipal();
|
||||||
if (context.getAuthentication().getDetails() instanceof TenantAwareAuthenticationDetails tenantAwareAuthenticationDetails) {
|
if (context.getAuthentication().getDetails() instanceof TenantAwareAuthenticationDetails tenantAwareAuthenticationDetails) {
|
||||||
return tenantAwareAuthenticationDetails.getTenant();
|
return tenantAwareAuthenticationDetails.tenant();
|
||||||
} else if (principal instanceof TenantAwareUser tenantAwareUser) {
|
} else if (principal instanceof TenantAwareUser tenantAwareUser) {
|
||||||
return tenantAwareUser.getTenant();
|
return tenantAwareUser.getTenant();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,31 +12,14 @@ package org.eclipse.hawkbit.tenancy;
|
|||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An authentication details object {@link AbstractAuthenticationToken#getDetails()} which is stored in the
|
* 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.
|
* spring security authentication token details to transport the principal and tenant in the security context session.
|
||||||
*/
|
*/
|
||||||
@Getter
|
public record TenantAwareAuthenticationDetails(String tenant, boolean controller) implements Serializable {
|
||||||
@ToString
|
|
||||||
public class TenantAwareAuthenticationDetails implements Serializable {
|
|
||||||
|
|
||||||
@Serial
|
@Serial
|
||||||
private static final long serialVersionUID = 1L;
|
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 io.micrometer.observation.ObservationRegistry;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.NonNull;
|
||||||
import org.eclipse.hawkbit.tenancy.TenantAware.TenantResolver;
|
import org.eclipse.hawkbit.tenancy.TenantAware.TenantResolver;
|
||||||
import org.springframework.beans.factory.ObjectProvider;
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
|
import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
|
||||||
@@ -59,8 +60,9 @@ public class TenantMetricsConfiguration {
|
|||||||
public DefaultServerRequestObservationConvention serverRequestObservationConvention(final TenantResolver tenantResolver) {
|
public DefaultServerRequestObservationConvention serverRequestObservationConvention(final TenantResolver tenantResolver) {
|
||||||
return new DefaultServerRequestObservationConvention() {
|
return new DefaultServerRequestObservationConvention() {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@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
|
// 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));
|
return KeyValues.of(exception(context), method(context), outcome(context), status(context), tenant(), uri(context));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,17 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.cache;
|
package org.eclipse.hawkbit.tenancy.cache;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||||
import org.springframework.cache.Cache;
|
import org.springframework.cache.Cache;
|
||||||
import org.springframework.cache.CacheManager;
|
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()}
|
* 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/>
|
* <p/>
|
||||||
* Additionally, it also provides functionality to retrieve all caches overall tenants at once, for monitoring and system access.
|
* 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 = "|";
|
private static final String TENANT_CACHE_DELIMITER = "|";
|
||||||
|
|
||||||
@@ -40,8 +43,9 @@ public class TenantAwareCacheManager implements TenancyCacheManager {
|
|||||||
this.tenantAware = tenantAware;
|
this.tenantAware = tenantAware;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Cache getCache(final String name) {
|
public Cache getCache(@NonNull final String name) {
|
||||||
final String currentTenant = tenantAware.getCurrentTenant();
|
final String currentTenant = tenantAware.getCurrentTenant();
|
||||||
if (isTenantInvalid(currentTenant)) {
|
if (isTenantInvalid(currentTenant)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -50,6 +54,7 @@ public class TenantAwareCacheManager implements TenancyCacheManager {
|
|||||||
return delegate.getCache(buildKey(currentTenant.toUpperCase(), name));
|
return delegate.getCache(buildKey(currentTenant.toUpperCase(), name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Collection<String> getCacheNames() {
|
public Collection<String> getCacheNames() {
|
||||||
final String currentTenant = tenantAware.getCurrentTenant();
|
final String currentTenant = tenantAware.getCurrentTenant();
|
||||||
@@ -67,7 +72,8 @@ public class TenantAwareCacheManager implements TenancyCacheManager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void evictCaches(final String tenant) {
|
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
|
* 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.Cache;
|
||||||
import org.springframework.cache.CacheManager;
|
import org.springframework.cache.CacheManager;
|
||||||
@@ -15,7 +15,7 @@ import org.springframework.cache.CacheManager;
|
|||||||
/**
|
/**
|
||||||
* A cache interface which handles multi tenancy.
|
* 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
|
* 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());
|
final ToSetter toSetter = toSetter(toClass, setterName, fieldName, fromMethod.getReturnType());
|
||||||
if (toSetter == null && toGetter == null) {
|
if (toSetter == null && toGetter == null) {
|
||||||
// we allow toSetter to be null, but in that case the toGetter must not be null and the
|
// 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());
|
throw new IllegalStateException("Setter counterpart for " + fromMethod + " is not found in " + toClass.getName());
|
||||||
}
|
}
|
||||||
propertySetters.add(new PropertyCopyFunction(fromMethod, toSetter, toGetter));
|
propertySetters.add(new PropertyCopyFunction(fromMethod, toSetter, toGetter));
|
||||||
@@ -284,17 +284,8 @@ public class ObjectCopyUtil {
|
|||||||
private record ToSetter(BiConsumer<Object, Object> toSetter, int order) {}
|
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
|
@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 record PropertyCopyFunction(Method fromMethod, ToSetter toSetter, UnaryOperator<Object> toGetter)
|
||||||
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean apply(final Object from, final Object to, final boolean setNullValues, final UnaryOperator<Object> propertyProcessor) {
|
public boolean apply(final Object from, final Object to, final boolean setNullValues, final UnaryOperator<Object> propertyProcessor) {
|
||||||
final Object value;
|
final Object value;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import io.github.classgraph.ClassInfo;
|
|||||||
import io.github.classgraph.ScanResult;
|
import io.github.classgraph.ScanResult;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
class FileNameFieldsTest {
|
class RsqlQueryFieldsTest {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies that fields classes are correctly implemented
|
* Verifies that fields classes are correctly implemented
|
||||||
@@ -35,9 +35,7 @@ class FileNameFieldsTest {
|
|||||||
.map(clazz -> (Class<? extends RsqlQueryField>) clazz)
|
.map(clazz -> (Class<? extends RsqlQueryField>) clazz)
|
||||||
.toList();
|
.toList();
|
||||||
assertThat(matchingClasses).isNotEmpty();
|
assertThat(matchingClasses).isNotEmpty();
|
||||||
matchingClasses.forEach(providerClass -> {
|
matchingClasses.forEach(providerClass -> assertThat(providerClass.getEnumConstants()).isNotEmpty());
|
||||||
assertThat(providerClass.getEnumConstants()).isNotEmpty();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,9 +47,7 @@ public class DdiActionHistory {
|
|||||||
* @param messages is a list of messages retrieved from action history.
|
* @param messages is a list of messages retrieved from action history.
|
||||||
*/
|
*/
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
public DdiActionHistory(
|
public DdiActionHistory(@JsonProperty("status") final String status, @JsonProperty("messages") final List<String> messages) {
|
||||||
@JsonProperty("status") final String status,
|
|
||||||
@JsonProperty("messages") final List<String> messages) {
|
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.messages = messages;
|
this.messages = messages;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
|||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
@@ -27,16 +28,10 @@ public class DdiActivateAutoConfirmation {
|
|||||||
"confirming future actions", example = "exampleRemark")
|
"confirming future actions", example = "exampleRemark")
|
||||||
private final String remark;
|
private final String remark;
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
*
|
|
||||||
* @param initiator can be null
|
|
||||||
* @param remark can be null
|
|
||||||
*/
|
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
public DdiActivateAutoConfirmation(
|
public DdiActivateAutoConfirmation(
|
||||||
@JsonProperty(value = "initiator") final String initiator,
|
@Nullable @JsonProperty(value = "initiator") final String initiator,
|
||||||
@JsonProperty(value = "remark") final String remark) {
|
@Nullable @JsonProperty(value = "remark") final String remark) {
|
||||||
this.initiator = initiator;
|
this.initiator = initiator;
|
||||||
this.remark = remark;
|
this.remark = remark;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/40/filename/binaryFile"
|
||||||
},
|
},
|
||||||
"download-http" : {
|
"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" : {
|
"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" : {
|
"md5sum" : {
|
||||||
"href" : "https://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"
|
||||||
|
|||||||
@@ -28,11 +28,6 @@ public class DdiConfig {
|
|||||||
|
|
||||||
private final DdiPolling polling;
|
private final DdiPolling polling;
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
*
|
|
||||||
* @param polling configuration of the polling for the target
|
|
||||||
*/
|
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
public DdiConfig(@JsonProperty("polling") final DdiPolling polling) {
|
public DdiConfig(@JsonProperty("polling") final DdiPolling polling) {
|
||||||
this.polling = polling;
|
this.polling = polling;
|
||||||
|
|||||||
@@ -57,8 +57,7 @@ public class DdiConfirmationBase extends RepresentationModel<DdiConfirmationBase
|
|||||||
private final DdiAutoConfirmationState autoConfirm;
|
private final DdiAutoConfirmationState autoConfirm;
|
||||||
|
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
public DdiConfirmationBase(
|
public DdiConfirmationBase(@JsonProperty(value = "autoConfirm", required = true) final DdiAutoConfirmationState autoConfirm) {
|
||||||
@JsonProperty(value = "autoConfirm", required = true) final DdiAutoConfirmationState autoConfirm) {
|
|
||||||
this.autoConfirm = 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"
|
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/binary.tgz"
|
||||||
},
|
},
|
||||||
"download-http" : {
|
"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" : {
|
"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" : {
|
"md5sum" : {
|
||||||
"href" : "https://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"
|
||||||
@@ -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"
|
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/file.signature"
|
||||||
},
|
},
|
||||||
"download-http" : {
|
"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" : {
|
"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" : {
|
"md5sum" : {
|
||||||
"href" : "https://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"
|
||||||
@@ -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"
|
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/binary.tgz"
|
||||||
},
|
},
|
||||||
"download-http" : {
|
"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" : {
|
"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" : {
|
"md5sum" : {
|
||||||
"href" : "https://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"
|
||||||
@@ -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"
|
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/file.signature"
|
||||||
},
|
},
|
||||||
"download-http" : {
|
"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" : {
|
"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" : {
|
"md5sum" : {
|
||||||
"href" : "https://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"
|
||||||
@@ -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"
|
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/binary.tgz"
|
||||||
},
|
},
|
||||||
"download-http" : {
|
"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" : {
|
"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" : {
|
"md5sum" : {
|
||||||
"href" : "https://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"
|
||||||
@@ -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"
|
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/file.signature"
|
||||||
},
|
},
|
||||||
"download-http" : {
|
"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" : {
|
"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" : {
|
"md5sum" : {
|
||||||
"href" : "https://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"
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ public class DdiDeployment {
|
|||||||
*/
|
*/
|
||||||
FORCED("forced");
|
FORCED("forced");
|
||||||
|
|
||||||
private String name;
|
private final String name;
|
||||||
|
|
||||||
HandlingType(final String name) {
|
HandlingType(final String name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -128,7 +128,7 @@ public class DdiDeployment {
|
|||||||
*/
|
*/
|
||||||
UNAVAILABLE("unavailable");
|
UNAVAILABLE("unavailable");
|
||||||
|
|
||||||
private String status;
|
private final String status;
|
||||||
|
|
||||||
DdiMaintenanceWindowStatus(final String status) {
|
DdiMaintenanceWindowStatus(final String status) {
|
||||||
this.status = 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"
|
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/binary.tgz"
|
||||||
},
|
},
|
||||||
"download-http" : {
|
"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" : {
|
"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" : {
|
"md5sum" : {
|
||||||
"href" : "https://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"
|
||||||
@@ -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"
|
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/file.signature"
|
||||||
},
|
},
|
||||||
"download-http" : {
|
"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" : {
|
"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" : {
|
"md5sum" : {
|
||||||
"href" : "https://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"
|
||||||
@@ -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"
|
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/binary.tgz"
|
||||||
},
|
},
|
||||||
"download-http" : {
|
"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" : {
|
"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" : {
|
"md5sum" : {
|
||||||
"href" : "https://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"
|
||||||
@@ -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"
|
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/file.signature"
|
||||||
},
|
},
|
||||||
"download-http" : {
|
"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" : {
|
"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" : {
|
"md5sum" : {
|
||||||
"href" : "https://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"
|
||||||
@@ -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"
|
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/binary.tgz"
|
||||||
},
|
},
|
||||||
"download-http" : {
|
"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" : {
|
"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" : {
|
"md5sum" : {
|
||||||
"href" : "https://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"
|
||||||
@@ -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"
|
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/file.signature"
|
||||||
},
|
},
|
||||||
"download-http" : {
|
"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" : {
|
"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" : {
|
"md5sum" : {
|
||||||
"href" : "https://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"
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ public class DdiResult {
|
|||||||
*/
|
*/
|
||||||
NONE("none");
|
NONE("none");
|
||||||
|
|
||||||
private String name;
|
private final String name;
|
||||||
|
|
||||||
FinalResult(final String name) {
|
FinalResult(final String name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ package org.eclipse.hawkbit.ddi.json.model;
|
|||||||
import com.fasterxml.jackson.annotation.JsonValue;
|
import com.fasterxml.jackson.annotation.JsonValue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enumerates the supported update modes. Each mode represents an attribute
|
* Enumerates the supported update modes. Each mode represents an attribute update strategy.
|
||||||
* update strategy.
|
|
||||||
*
|
*
|
||||||
* @see DdiConfigData
|
* @see DdiConfigData
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ public interface DdiRootControllerRestApi {
|
|||||||
* the eTag needs to be re-generated
|
* 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,
|
* @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
|
* 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 less than zero: retrieves the maximum allowed number of action status messages from history;
|
||||||
* actionHistoryMessageCount equal to zero: does not retrieve any message;
|
* actionHistoryMessageCount equal to zero: does not retrieve any message;
|
||||||
* actionHistoryMessageCount greater than zero: retrieves the specified number of messages, limited by maximum allowed number.
|
* actionHistoryMessageCount greater than zero: retrieves the specified number of messages, limited by maximum allowed number.
|
||||||
@@ -233,8 +233,7 @@ public interface DdiRootControllerRestApi {
|
|||||||
|
|
||||||
In case a device provides state information on the feedback channel and won’t store it locally,
|
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
|
a query for, e.q, the last 10 messages, could be used which will include the previously provided by the
|
||||||
device,
|
device, feedback.
|
||||||
feedback.
|
|
||||||
|
|
||||||
In addition to the straight forward approach to inform the device to download and install the software
|
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.
|
in one transaction hawkBit supports the separation of download and installation into separate steps.
|
||||||
@@ -373,7 +372,7 @@ public interface DdiRootControllerRestApi {
|
|||||||
*/
|
*/
|
||||||
@Operation(summary = "Cancel an action", description = """
|
@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
|
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 = {
|
@ApiResponses(value = {
|
||||||
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
|
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
|
||||||
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters",
|
@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 actionId of the {@link DdiDeploymentBase} that matches to installed action.
|
||||||
* @param actionHistoryMessageCount specifies the number of messages to be returned from 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
|
* 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 less than zero: retrieves the maximum allowed number of action status messages from history;
|
||||||
* actionHistoryMessageCount equal to zero: does not retrieve any message;
|
* actionHistoryMessageCount equal to zero: does not retrieve any message;
|
||||||
* actionHistoryMessageCount greater than zero: retrieves the specified number of messages, limited by maximum allowed number.
|
* actionHistoryMessageCount greater than zero: retrieves the specified number of messages, limited by maximum allowed number.
|
||||||
@@ -546,7 +545,7 @@ public interface DdiRootControllerRestApi {
|
|||||||
* needs to be re-generated
|
* needs to be re-generated
|
||||||
* @param actionHistoryMessageCount specifies the number of messages to be returned from action history. Regardless of the passed value,
|
* @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
|
* 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 less than zero: retrieves the maximum allowed number of action status messages from history;
|
||||||
* actionHistoryMessageCount equal to zero: does not retrieve any message;
|
* actionHistoryMessageCount equal to zero: does not retrieve any message;
|
||||||
* actionHistoryMessageCount greater than zero: retrieves the specified number of messages, limited by maximum allowed number.
|
* actionHistoryMessageCount greater than zero: retrieves the specified number of messages, limited by maximum allowed number.
|
||||||
|
|||||||
@@ -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
|
@Test
|
||||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
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
|
@Test
|
||||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
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
|
@Test
|
||||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
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
|
@Test
|
||||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
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
|
@Test
|
||||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
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
|
@Test
|
||||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
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
|
@Test
|
||||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
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
|
@Test
|
||||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
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
|
@Test
|
||||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
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
|
@Test
|
||||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
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
|
@Test
|
||||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
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
|
@Test
|
||||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
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
|
@Test
|
||||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
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
|
@Test
|
||||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
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
|
@Test
|
||||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
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
|
@Test
|
||||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||||
|
|||||||
@@ -14,10 +14,8 @@ import java.util.Map;
|
|||||||
|
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import org.eclipse.hawkbit.repository.artifact.urlhandler.ApiType;
|
import org.eclipse.hawkbit.artifact.urlresolver.ArtifactUrlResolver;
|
||||||
import org.eclipse.hawkbit.repository.artifact.urlhandler.ArtifactUrlHandler;
|
import org.eclipse.hawkbit.artifact.urlresolver.ArtifactUrlResolver.DownloadDescriptor;
|
||||||
import org.eclipse.hawkbit.repository.artifact.urlhandler.URLPlaceholder;
|
|
||||||
import org.eclipse.hawkbit.repository.artifact.urlhandler.URLPlaceholder.SoftwareData;
|
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiArtifact;
|
import org.eclipse.hawkbit.ddi.json.model.DdiArtifact;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiArtifactHash;
|
import org.eclipse.hawkbit.ddi.json.model.DdiArtifactHash;
|
||||||
import org.eclipse.hawkbit.ddi.json.model.DdiAutoConfirmationState;
|
import org.eclipse.hawkbit.ddi.json.model.DdiAutoConfirmationState;
|
||||||
@@ -121,6 +119,7 @@ public final class DataConversionHelper {
|
|||||||
result.add(WebMvcLinkBuilder
|
result.add(WebMvcLinkBuilder
|
||||||
.linkTo(WebMvcLinkBuilder
|
.linkTo(WebMvcLinkBuilder
|
||||||
.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||||
|
// doesn't really call the putConfigData with null, just create the link
|
||||||
.putConfigData(null, tenantAware.getCurrentTenant(), target.getControllerId()))
|
.putConfigData(null, tenantAware.getCurrentTenant(), target.getControllerId()))
|
||||||
.withRel(DdiRestConstants.CONFIG_DATA_ACTION).expand());
|
.withRel(DdiRestConstants.CONFIG_DATA_ACTION).expand());
|
||||||
}
|
}
|
||||||
@@ -130,7 +129,7 @@ public final class DataConversionHelper {
|
|||||||
|
|
||||||
static List<DdiChunk> createChunks(
|
static List<DdiChunk> createChunks(
|
||||||
final Target target, final Action uAction,
|
final Target target, final Action uAction,
|
||||||
final ArtifactUrlHandler artifactUrlHandler, final SystemManagement systemManagement,
|
final ArtifactUrlResolver artifactUrlHandler, final SystemManagement systemManagement,
|
||||||
final HttpRequest request, final ControllerManagement controllerManagement) {
|
final HttpRequest request, final ControllerManagement controllerManagement) {
|
||||||
final Map<Long, Map<String, String>> metadata = controllerManagement
|
final Map<Long, Map<String, String>> metadata = controllerManagement
|
||||||
.findTargetVisibleMetaDataBySoftwareModuleId(uAction.getDistributionSet().getModules().stream()
|
.findTargetVisibleMetaDataBySoftwareModuleId(uAction.getDistributionSet().getModules().stream()
|
||||||
@@ -146,7 +145,7 @@ public final class DataConversionHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static List<DdiArtifact> createArtifacts(final Target target, final SoftwareModule module,
|
static List<DdiArtifact> createArtifacts(final Target target, final SoftwareModule module,
|
||||||
final ArtifactUrlHandler artifactUrlHandler, final SystemManagement systemManagement,
|
final ArtifactUrlResolver artifactUrlHandler, final SystemManagement systemManagement,
|
||||||
final HttpRequest request) {
|
final HttpRequest request) {
|
||||||
|
|
||||||
return new ResponseList<>(module.getArtifacts().stream()
|
return new ResponseList<>(module.getArtifacts().stream()
|
||||||
@@ -171,7 +170,7 @@ public final class DataConversionHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static DdiArtifact createArtifact(
|
private static DdiArtifact createArtifact(
|
||||||
final Target target, final ArtifactUrlHandler artifactUrlHandler,
|
final Target target, final ArtifactUrlResolver artifactUrlHandler,
|
||||||
final Artifact artifact, final SystemManagement systemManagement, final HttpRequest request) {
|
final Artifact artifact, final SystemManagement systemManagement, final HttpRequest request) {
|
||||||
final DdiArtifact file = new DdiArtifact(
|
final DdiArtifact file = new DdiArtifact(
|
||||||
artifact.getFilename(),
|
artifact.getFilename(),
|
||||||
@@ -180,13 +179,11 @@ public final class DataConversionHelper {
|
|||||||
|
|
||||||
final TenantMetaData tenantMetadata = systemManagement.getTenantMetadataWithoutDetails();
|
final TenantMetaData tenantMetadata = systemManagement.getTenantMetadataWithoutDetails();
|
||||||
artifactUrlHandler
|
artifactUrlHandler
|
||||||
.getUrls(new URLPlaceholder(
|
.getUrls(new DownloadDescriptor(
|
||||||
tenantMetadata.getTenant(), tenantMetadata.getId(), target.getControllerId(), target.getId(),
|
tenantMetadata.getTenant(), target.getControllerId(),
|
||||||
new SoftwareData(
|
artifact.getSoftwareModule().getId(), artifact.getFilename(), artifact.getSha1Hash()),
|
||||||
artifact.getSoftwareModule().getId(), artifact.getFilename(), artifact.getId(),
|
ArtifactUrlResolver.ApiType.DDI, request.getURI())
|
||||||
artifact.getSha1Hash())),
|
.forEach(entry -> file.add(Link.of(entry.ref()).withRel(entry.rel()).expand()));
|
||||||
ApiType.DDI, request.getURI())
|
|
||||||
.forEach(entry -> file.add(Link.of(entry.getRef()).withRel(entry.getRel()).expand()));
|
|
||||||
|
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user