Browse Source

Merge pull request #13562 from AndriiLandiak/feature/ota-package-vc

Version control support for OTA packages
pull/10790/merge
Viacheslav Klimov 12 months ago
committed by GitHub
parent
commit
f2b8a7ae7b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 49
      application/src/main/data/upgrade/basic/schema_update.sql
  2. 35
      application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java
  3. 1
      application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java
  4. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
  5. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java
  6. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java
  7. 49
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java
  8. 15
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
  9. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java
  10. 13
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java
  11. 76
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java
  12. 29
      application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java
  13. 80
      application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java
  14. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java
  15. 8
      common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java
  16. 12
      common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java
  17. 3
      common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java
  18. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java
  19. 4
      common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java
  20. 3
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java
  21. 41
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java
  22. 3
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java
  23. 3
      common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java
  24. 7
      dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java
  25. 9
      dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java
  26. 57
      dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java
  27. 7
      dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java
  28. 1
      dao/src/main/java/org/thingsboard/server/dao/service/validator/OtaPackageDataValidator.java
  29. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java
  30. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java
  31. 45
      dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java
  32. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java
  33. 12
      dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageRepository.java
  34. 2
      dao/src/main/resources/sql/schema-entities-idx.sql
  35. 4
      dao/src/main/resources/sql/schema-entities.sql
  36. 2
      ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
  37. 23
      ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html
  38. 36
      ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts
  39. 2
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts
  40. 2
      ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts
  41. 23
      ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html
  42. 40
      ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts
  43. 4
      ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts
  44. 2
      ui-ngx/src/app/shared/models/entity-type.models.ts
  45. 4
      ui-ngx/src/app/shared/models/ota-package.models.ts
  46. 6
      ui-ngx/src/app/shared/models/vc.models.ts
  47. 2
      ui-ngx/src/assets/locale/locale.constant-en_US.json

49
application/src/main/data/upgrade/basic/schema_update.sql

@ -14,48 +14,11 @@
-- limitations under the License.
--
-- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS START
-- UPDATE OTA PACKAGE EXTERNAL ID START
UPDATE tenant_profile
SET profile_data = jsonb_set(
profile_data,
'{configuration}',
(
(profile_data -> 'configuration') - 'cassandraQueryTenantRateLimitsConfiguration'
||
COALESCE(
CASE
WHEN profile_data -> 'configuration' ->
'cassandraQueryTenantRateLimitsConfiguration' IS NOT NULL THEN
jsonb_build_object(
'cassandraReadQueryTenantCoreRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration',
'cassandraWriteQueryTenantCoreRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration',
'cassandraReadQueryTenantRuleEngineRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration',
'cassandraWriteQueryTenantRuleEngineRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration'
)
END,
'{}'::jsonb
)
)
)
WHERE profile_data -> 'configuration' ? 'cassandraQueryTenantRateLimitsConfiguration';
ALTER TABLE ota_package
ADD COLUMN IF NOT EXISTS external_id uuid;
ALTER TABLE ota_package
ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id);
-- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS END
-- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS START
UPDATE notification_rule
SET trigger_config = REGEXP_REPLACE(
trigger_config,
'"CASSANDRA_QUERIES"',
'"CASSANDRA_WRITE_QUERIES_CORE","CASSANDRA_READ_QUERIES_CORE","CASSANDRA_WRITE_QUERIES_RULE_ENGINE","CASSANDRA_READ_QUERIES_RULE_ENGINE","CASSANDRA_WRITE_QUERIES_MONOLITH","CASSANDRA_READ_QUERIES_MONOLITH"',
'g'
)
WHERE trigger_type = 'RATE_LIMITS'
AND trigger_config LIKE '%"CASSANDRA_QUERIES"%';
-- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS END
-- UPDATE OTA PACKAGE EXTERNAL ID END

35
application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java

@ -24,13 +24,14 @@ import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.thingsboard.server.common.data.OtaPackage;
@ -49,8 +50,6 @@ import org.thingsboard.server.service.entitiy.ota.TbOtaPackageService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.io.IOException;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.OTA_PACKAGE_DESCRIPTION;
@ -80,8 +79,7 @@ public class OtaPackageController extends BaseController {
@ApiOperation(value = "Download OTA Package (downloadOtaPackage)", notes = "Download OTA Package based on the provided OTA Package Id." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority( 'TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}/download", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/otaPackage/{otaPackageId}/download")
public ResponseEntity<org.springframework.core.io.Resource> downloadOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId);
@ -105,8 +103,7 @@ public class OtaPackageController extends BaseController {
notes = "Fetch the OTA Package Info object based on the provided OTA Package Id. " +
OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/otaPackage/info/{otaPackageId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/otaPackage/info/{otaPackageId}")
public OtaPackageInfo getOtaPackageInfoById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId);
@ -118,8 +115,7 @@ public class OtaPackageController extends BaseController {
notes = "Fetch the OTA Package object based on the provided OTA Package Id. " +
"The server checks that the OTA Package is owned by the same tenant. " + OTA_PACKAGE_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/otaPackage/{otaPackageId}")
public OtaPackage getOtaPackageById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId);
@ -134,10 +130,9 @@ public class OtaPackageController extends BaseController {
"Referencing non-existing OTA Package Id will cause 'Not Found' error. " +
"\n\nOTA Package combination of the title with the version is unique in the scope of tenant. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/otaPackage")
public OtaPackageInfo saveOtaPackageInfo(@Parameter(description = "A JSON value representing the OTA Package.")
@RequestBody SaveOtaPackageInfoRequest otaPackageInfo) throws ThingsboardException {
@RequestBody SaveOtaPackageInfoRequest otaPackageInfo) throws Exception {
otaPackageInfo.setTenantId(getTenantId());
checkEntity(otaPackageInfo.getId(), otaPackageInfo, Resource.OTA_PACKAGE);
@ -148,8 +143,7 @@ public class OtaPackageController extends BaseController {
notes = "Update the OTA Package. Adds the date to the existing OTA Package Info" + TENANT_AUTHORITY_PARAGRAPH,
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(mediaType = MULTIPART_FORM_DATA_VALUE)))
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.POST, consumes = MULTIPART_FORM_DATA_VALUE)
@ResponseBody
@PostMapping(value = "/otaPackage/{otaPackageId}", consumes = MULTIPART_FORM_DATA_VALUE)
public OtaPackageInfo saveOtaPackageData(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId,
@Parameter(description = "OTA Package checksum. For example, '0xd87f7e0c'")
@ -157,7 +151,7 @@ public class OtaPackageController extends BaseController {
@Parameter(description = "OTA Package checksum algorithm.", schema = @Schema(allowableValues = {"MD5", "SHA256", "SHA384", "SHA512", "CRC32", "MURMUR3_32", "MURMUR3_128"}))
@RequestParam(CHECKSUM_ALGORITHM) String checksumAlgorithmStr,
@Parameter(description = "OTA Package data.")
@RequestPart MultipartFile file) throws ThingsboardException, IOException {
@RequestPart MultipartFile file) throws Exception {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId);
checkParameter(CHECKSUM_ALGORITHM, checksumAlgorithmStr);
OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId));
@ -172,8 +166,7 @@ public class OtaPackageController extends BaseController {
notes = "Returns a page of OTA Package Info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/otaPackages", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/otaPackages")
public PageData<OtaPackageInfo> getOtaPackages(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@ -192,8 +185,7 @@ public class OtaPackageController extends BaseController {
notes = "Returns a page of OTA Package Info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/otaPackages/{deviceProfileId}/{type}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/otaPackages/{deviceProfileId}/{type}")
public PageData<OtaPackageInfo> getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceProfileId") String strDeviceProfileId,
@Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"}))
@ -219,8 +211,7 @@ public class OtaPackageController extends BaseController {
notes = "Deletes the OTA Package. Referencing non-existing OTA Package Id will cause an error. " +
"Can't delete the OTA Package if it is referenced by existing devices or device profile." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.DELETE)
@ResponseBody
@DeleteMapping(value = "/otaPackage/{otaPackageId}")
public void deleteOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable("otaPackageId") String strOtaPackageId) throws ThingsboardException {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId);

1
application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java

@ -110,4 +110,5 @@ public class DefaultTbOtaPackageService extends AbstractTbEntityService implemen
throw e;
}
}
}

2
application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java

@ -67,7 +67,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
protected static final List<EntityType> SUPPORTED_ENTITY_TYPES = List.of(
EntityType.CUSTOMER, EntityType.RULE_CHAIN, EntityType.TB_RESOURCE,
EntityType.DASHBOARD, EntityType.ASSET_PROFILE, EntityType.ASSET,
EntityType.DEVICE_PROFILE, EntityType.DEVICE,
EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE, EntityType.DEVICE,
EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE,
EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE
);

2
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java

@ -38,6 +38,8 @@ public class DeviceExportService extends BaseEntityExportService<DeviceId, Devic
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, Device device, DeviceExportData exportData) {
device.setCustomerId(getExternalIdOrElseInternal(ctx, device.getCustomerId()));
device.setDeviceProfileId(getExternalIdOrElseInternal(ctx, device.getDeviceProfileId()));
device.setFirmwareId(getExternalIdOrElseInternal(ctx, device.getFirmwareId()));
device.setSoftwareId(getExternalIdOrElseInternal(ctx, device.getSoftwareId()));
if (ctx.getSettings().isExportCredentials()) {
var credentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(ctx.getTenantId(), device.getId());
credentials.setId(null);

2
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java

@ -34,6 +34,8 @@ public class DeviceProfileExportService extends BaseEntityExportService<DevicePr
deviceProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultDashboardId()));
deviceProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultRuleChainId()));
deviceProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultEdgeRuleChainId()));
deviceProfile.setFirmwareId(getExternalIdOrElseInternal(ctx, deviceProfile.getFirmwareId()));
deviceProfile.setSoftwareId(getExternalIdOrElseInternal(ctx, deviceProfile.getSoftwareId()));
}
@Override

49
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java

@ -0,0 +1,49 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sync.ie.exporting.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.sync.ie.OtaPackageExportData;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
import java.util.Set;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class OtaPackageExportService extends BaseEntityExportService<OtaPackageId, OtaPackage, OtaPackageExportData> {
@Override
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, OtaPackage otaPackage, OtaPackageExportData exportData) {
otaPackage.setDeviceProfileId(getExternalIdOrElseInternal(ctx, otaPackage.getDeviceProfileId()));
}
@Override
protected OtaPackageExportData newExportData() {
return new OtaPackageExportData();
}
@Override
public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.OTA_PACKAGE);
}
}

15
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java

@ -71,7 +71,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@ -148,6 +147,7 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
public CompareResult(boolean updateNeeded) {
this.updateNeeded = updateNeeded;
}
}
protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) {
@ -203,7 +203,6 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
protected abstract E saveOrUpdate(EntitiesImportCtx ctx, E entity, D exportData, IdProvider idProvider, CompareResult compareResult);
protected void processAfterSaved(EntitiesImportCtx ctx, EntityImportResult<E> importResult, D exportData, IdProvider idProvider) throws ThingsboardException {
E savedEntity = importResult.getSavedEntity();
E oldEntity = importResult.getOldEntity();
@ -405,7 +404,9 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
}
public <ID extends EntityId> ID getInternalId(ID externalId, boolean throwExceptionIfNotFound) {
if (externalId == null || externalId.isNullUid()) return null;
if (externalId == null || externalId.isNullUid()) {
return null;
}
if (EntityType.TENANT.equals(externalId.getEntityType())) {
return (ID) ctx.getTenantId();
@ -432,7 +433,9 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
}
public Optional<EntityId> getInternalIdByUuid(UUID externalUuid, boolean fetchAllUUIDs, Set<EntityType> hints) {
if (externalUuid.equals(EntityId.NULL_UUID)) return Optional.empty();
if (externalUuid.equals(EntityId.NULL_UUID)) {
return Optional.empty();
}
for (EntityType entityType : EntityType.values()) {
Optional<EntityId> externalId = buildEntityId(entityType, externalUuid);
@ -483,10 +486,6 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
}
protected <T extends EntityId, O> T getOldEntityField(O oldEntity, Function<O, T> getter) {
return oldEntity == null ? null : getter.apply(oldEntity);
}
protected void replaceIdsRecursively(EntitiesImportCtx ctx, IdProvider idProvider, JsonNode json,
Set<String> skippedRootFields, Pattern includedFieldsPattern,
LinkedHashSet<EntityType> hints) {

4
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java

@ -44,8 +44,8 @@ public class DeviceImportService extends BaseEntityImportService<DeviceId, Devic
@Override
protected Device prepare(EntitiesImportCtx ctx, Device device, Device old, DeviceExportData exportData, IdProvider idProvider) {
device.setDeviceProfileId(idProvider.getInternalId(device.getDeviceProfileId()));
device.setFirmwareId(getOldEntityField(old, Device::getFirmwareId));
device.setSoftwareId(getOldEntityField(old, Device::getSoftwareId));
device.setFirmwareId(idProvider.getInternalId(device.getFirmwareId()));
device.setSoftwareId(idProvider.getInternalId(device.getSoftwareId()));
return device;
}

13
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java

@ -45,15 +45,20 @@ public class DeviceProfileImportService extends BaseEntityImportService<DevicePr
deviceProfile.setDefaultRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultRuleChainId()));
deviceProfile.setDefaultEdgeRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultEdgeRuleChainId()));
deviceProfile.setDefaultDashboardId(idProvider.getInternalId(deviceProfile.getDefaultDashboardId()));
deviceProfile.setFirmwareId(getOldEntityField(old, DeviceProfile::getFirmwareId));
deviceProfile.setSoftwareId(getOldEntityField(old, DeviceProfile::getSoftwareId));
deviceProfile.setFirmwareId(idProvider.getInternalId(deviceProfile.getFirmwareId(), false));
deviceProfile.setSoftwareId(idProvider.getInternalId(deviceProfile.getSoftwareId(), false));
return deviceProfile;
}
@Override
protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, EntityExportData<DeviceProfile> exportData, IdProvider idProvider, CompareResult compareResult) {
boolean toUpdate = ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds();
if (toUpdate) {
deviceProfile.setFirmwareId(idProvider.getInternalId(deviceProfile.getFirmwareId()));
deviceProfile.setSoftwareId(idProvider.getInternalId(deviceProfile.getSoftwareId()));
}
DeviceProfile saved = deviceProfileService.saveDeviceProfile(deviceProfile);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
if (toUpdate) {
importCalculatedFields(ctx, saved, exportData, idProvider);
}
return saved;
@ -73,8 +78,6 @@ public class DeviceProfileImportService extends BaseEntityImportService<DevicePr
@Override
protected void cleanupForComparison(DeviceProfile deviceProfile) {
super.cleanupForComparison(deviceProfile);
deviceProfile.setFirmwareId(null);
deviceProfile.setSoftwareId(null);
}
@Override

76
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java

@ -0,0 +1,76 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sync.ie.importing.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.ie.OtaPackageExportData;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class OtaPackageImportService extends BaseEntityImportService<OtaPackageId, OtaPackage, OtaPackageExportData> {
private final OtaPackageService otaPackageService;
@Override
protected void setOwner(TenantId tenantId, OtaPackage otaPackage, IdProvider idProvider) {
otaPackage.setTenantId(tenantId);
}
@Override
protected OtaPackage prepare(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackage oldOtaPackage, OtaPackageExportData exportData, IdProvider idProvider) {
otaPackage.setDeviceProfileId(idProvider.getInternalId(otaPackage.getDeviceProfileId()));
return otaPackage;
}
@Override
protected OtaPackage findExistingEntity(EntitiesImportCtx ctx, OtaPackage otaPackage, IdProvider idProvider) {
OtaPackage existingOtaPackage = super.findExistingEntity(ctx, otaPackage, idProvider);
if (existingOtaPackage == null && ctx.isFindExistingByName()) {
existingOtaPackage = otaPackageService.findOtaPackageByTenantIdAndTitleAndVersion(ctx.getTenantId(), otaPackage.getTitle(), otaPackage.getVersion());
}
return existingOtaPackage;
}
@Override
protected OtaPackage deepCopy(OtaPackage otaPackage) {
return new OtaPackage(otaPackage);
}
@Override
protected OtaPackage saveOrUpdate(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackageExportData exportData, IdProvider idProvider, CompareResult compareResult) {
if (otaPackage.hasUrl()) {
OtaPackageInfo info = new OtaPackageInfo(otaPackage);
return new OtaPackage(otaPackageService.saveOtaPackageInfo(info, info.hasUrl()));
}
return otaPackageService.saveOtaPackage(otaPackage);
}
@Override
public EntityType getEntityType() {
return EntityType.OTA_PACKAGE;
}
}

29
application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java

@ -66,6 +66,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
@ -203,11 +204,12 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
AssetProfile assetProfile = createAssetProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Asset profile 1");
Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1");
DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1");
Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1");
OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1", firmware.getId(), null);
CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), asset.getId());
Map<EntityType, EntityExportData> entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(),
ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId())
ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId(), firmware.getId())
.map(entityId -> {
try {
return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder()
@ -275,12 +277,17 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
verify(tbClusterService).sendNotificationMsgToEdge(any(), any(), eq(importedDeviceProfile.getId()), any(), any(), eq(EdgeEventActionType.ADDED), any());
verify(otaPackageStateService).update(eq(importedDeviceProfile), eq(false), eq(false));
OtaPackage importedFirmware = (OtaPackage) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.OTA_PACKAGE)).getSavedEntity();
verify(entityActionService).logEntityAction(any(), eq(importedFirmware.getId()), eq(importedFirmware),
any(), eq(ActionType.ADDED), isNull());
Device importedDevice = (Device) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE)).getSavedEntity();
verify(entityActionService).logEntityAction(any(), eq(importedDevice.getId()), eq(importedDevice),
any(), eq(ActionType.ADDED), isNull());
verify(tbClusterService).onDeviceUpdated(eq(importedDevice), isNull());
importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE));
verify(tbClusterService, Mockito.never()).onDeviceUpdated(eq(importedDevice), eq(importedDevice));
assertThat(importedDevice.getFirmwareId()).isEqualTo(importedFirmware.getId());
// calculated field of imported device:
List<CalculatedField> calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId());
@ -318,14 +325,15 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
assetProfile = assetProfileService.saveAssetProfile(assetProfile);
DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1");
Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1");
OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1", firmware.getId(), null);
EntityView entityView = createEntityView(tenantId1, customer.getId(), device.getId(), "Entity view 1");
CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), device.getId());
Map<EntityId, EntityId> ids = new HashMap<>();
for (EntityId entityId : List.of(customer.getId(), ruleChain.getId(), dashboard.getId(), assetProfile.getId(), asset.getId(),
deviceProfile.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) {
deviceProfile.getId(), firmware.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) {
EntityExportData exportData = exportEntity(getSecurityUser(tenantAdmin1), entityId);
EntityImportResult importResult = importEntity(getSecurityUser(tenantAdmin2), exportData, EntityImportSettings.builder()
.saveCredentials(false)
@ -359,12 +367,17 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
assertThat(exportedDeviceProfile.getDefaultRuleChainId()).isEqualTo(ruleChain.getId());
assertThat(exportedDeviceProfile.getDefaultDashboardId()).isEqualTo(dashboard.getId());
EntityExportData<Device> entityExportData = exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId()));
OtaPackage exportedFirmware = (OtaPackage) exportEntity(tenantAdmin2, (OtaPackageId) ids.get(firmware.getId())).getEntity();
assertThat(exportedFirmware.getDeviceProfileId()).isEqualTo(exportedDeviceProfile.getId());
assertThat(exportedFirmware.getId()).isEqualTo(firmware.getId());
EntityExportData<Device> entityExportData = exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId()));
Device exportedDevice = entityExportData.getEntity();
assertThat(exportedDevice.getCustomerId()).isEqualTo(customer.getId());
assertThat(exportedDevice.getDeviceProfileId()).isEqualTo(deviceProfile.getId());
assertThat(exportedDevice.getFirmwareId()).isEqualTo(firmware.getId());
List<CalculatedField> calculatedFields = ((DeviceExportData) entityExportData).getCalculatedFields();
List<CalculatedField> calculatedFields = entityExportData.getCalculatedFields();
assertThat(calculatedFields.size()).isOne();
CalculatedField field = calculatedFields.get(0);
assertThat(field.getName()).isEqualTo(calculatedField.getName());
@ -380,13 +393,15 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
deviceProfileService.saveDeviceProfile(importedDeviceProfile);
}
protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name) {
protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name, OtaPackageId firmwareId, OtaPackageId softwareId) {
Device device = new Device();
device.setTenantId(tenantId);
device.setCustomerId(customerId);
device.setName(name);
device.setLabel("lbl");
device.setDeviceProfileId(deviceProfileId);
device.setFirmwareId(firmwareId);
device.setSoftwareId(softwareId);
DeviceData deviceData = new DeviceData();
deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration());
device.setDeviceData(deviceData);

80
application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java

@ -116,8 +116,8 @@ import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.controller.TbResourceControllerTest.TEST_DATA;
import static org.thingsboard.server.controller.TbResourceControllerTest.JS_TEST_FILE_NAME;
import static org.thingsboard.server.controller.TbResourceControllerTest.TEST_DATA;
@DaoSqlTest
public class VersionControlTest extends AbstractControllerTest {
@ -262,19 +262,24 @@ public class VersionControlTest extends AbstractControllerTest {
}
@Test
public void testDeviceVc_withProfile_betweenTenants() throws Exception {
public void testDeviceVc_withProfileAndOtaPackage_betweenTenants() throws Exception {
DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile of tenant 1");
createVersion("profiles", EntityType.DEVICE_PROFILE);
Device device = createDevice(null, deviceProfile.getId(), "Device of tenant 1", "test1");
String versionId = createVersion("devices", EntityType.DEVICE);
OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE);
Device device = createDevice(null, deviceProfile.getId(), "Device of tenant 1", "test1", newDevice -> {
newDevice.setFirmwareId(firmware.getId());
newDevice.setSoftwareId(software.getId());
});
String versionId = createVersion("devices with ota", EntityType.DEVICE, EntityType.OTA_PACKAGE);
DeviceCredentials deviceCredentials = findDeviceCredentials(device.getId());
DeviceCredentials newCredentials = new DeviceCredentials(deviceCredentials);
newCredentials.setCredentialsId("new access token"); // updating access token to avoid constraint errors on import
doPost("/api/device/credentials", newCredentials, DeviceCredentials.class);
assertThat(listVersions()).extracting(EntityVersion::getName).containsExactly("devices", "profiles");
assertThat(listVersions()).extracting(EntityVersion::getName).containsExactly("devices with ota", "profiles");
loginTenant2();
Map<EntityType, EntityTypeLoadResult> result = loadVersion(versionId, EntityType.DEVICE, EntityType.DEVICE_PROFILE);
Map<EntityType, EntityTypeLoadResult> result = loadVersion(versionId, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE);
assertThat(result.get(EntityType.DEVICE).getCreated()).isEqualTo(1);
assertThat(result.get(EntityType.DEVICE_PROFILE).getCreated()).isEqualTo(1);
@ -293,6 +298,13 @@ public class VersionControlTest extends AbstractControllerTest {
assertThat(importedCredentials.getCredentialsId()).isEqualTo(deviceCredentials.getCredentialsId());
assertThat(importedCredentials.getCredentialsValue()).isEqualTo(deviceCredentials.getCredentialsValue());
assertThat(importedCredentials.getCredentialsType()).isEqualTo(deviceCredentials.getCredentialsType());
OtaPackage importedFirmwareOta = findOtaPackage(firmware.getTitle());
OtaPackage importedSoftwareOta = findOtaPackage(software.getTitle());
checkImportedEntity(tenantId1, firmware, tenantId2, importedFirmwareOta);
checkImportedOtaPackageData(firmware, importedFirmwareOta);
checkImportedEntity(tenantId1, software, tenantId2, importedSoftwareOta);
checkImportedOtaPackageData(software, importedSoftwareOta);
}
@Test
@ -653,6 +665,57 @@ public class VersionControlTest extends AbstractControllerTest {
assertThat(importedCalculatedField.getType()).isEqualTo(calculatedField.getType());
}
@Test
public void testOtaPackageVc_sameTenant() throws Exception {
DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile v1.0");
OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE);
String versionId = createVersion("ota packages", EntityType.OTA_PACKAGE);
OtaPackage firmwareOta = findOtaPackage(firmware.getTitle());
OtaPackage softwareOta = findOtaPackage(software.getTitle());
loadVersion(versionId, EntityType.OTA_PACKAGE);
OtaPackage importedFirmwareOta = findOtaPackage(firmwareOta.getTitle());
OtaPackage importedSoftwareOta = findOtaPackage(softwareOta.getTitle());
checkImportedEntity(tenantId1, firmwareOta, tenantId1, importedFirmwareOta);
checkImportedOtaPackageData(firmwareOta, importedFirmwareOta);
checkImportedEntity(tenantId1, softwareOta, tenantId1, importedSoftwareOta);
checkImportedOtaPackageData(softwareOta, importedSoftwareOta);
}
@Test
public void testOtaPackageVcWithProfile_betweenTenants() throws Exception {
DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile v1.0");
OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE);
deviceProfile.setFirmwareId(firmware.getId());
deviceProfile.setSoftwareId(software.getId());
deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
String versionId = createVersion("ota packages", EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE);
loginTenant2();
loadVersion(versionId, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE);
DeviceProfile importedProfile = findDeviceProfile(deviceProfile.getName());
OtaPackage importedFirmwareOta = findOtaPackage(firmware.getTitle());
OtaPackage importedSoftwareOta = findOtaPackage(software.getTitle());
checkImportedEntity(tenantId1, deviceProfile, tenantId2, importedProfile);
checkImportedDeviceProfileData(deviceProfile, importedProfile);
checkImportedEntity(tenantId1, firmware, tenantId2, importedFirmwareOta);
checkImportedOtaPackageData(firmware, importedFirmwareOta);
checkImportedEntity(tenantId1, software, tenantId2, importedSoftwareOta);
checkImportedOtaPackageData(software, importedSoftwareOta);
assertThat(importedProfile.getFirmwareId()).isEqualTo(importedFirmwareOta.getId());
assertThat(importedProfile.getSoftwareId()).isEqualTo(importedSoftwareOta.getId());
}
protected void checkImportedOtaPackageData(OtaPackage otaPackage, OtaPackage importedOtaPackage) {
assertThat(importedOtaPackage.getName()).isEqualTo(otaPackage.getName());
assertThat(importedOtaPackage.getTag()).isEqualTo(otaPackage.getTag());
assertThat(importedOtaPackage.getType()).isEqualTo(otaPackage.getType());
assertThat(importedOtaPackage.getFileName()).isEqualTo(otaPackage.getFileName());
}
@Test
public void testResourceVc_sameTenant() throws Exception {
TbResourceInfo resourceInfo = createResource("Test resource");
@ -923,6 +986,7 @@ public class VersionControlTest extends AbstractControllerTest {
otaPackage.setDeviceProfileId(deviceProfileId);
otaPackage.setType(type);
otaPackage.setTitle("My " + type);
otaPackage.setTag("My " + type);
otaPackage.setVersion("v1.0");
otaPackage.setFileName("filename.txt");
otaPackage.setContentType("text/plain");
@ -933,6 +997,10 @@ public class VersionControlTest extends AbstractControllerTest {
return otaPackageService.saveOtaPackage(otaPackage);
}
private OtaPackage findOtaPackage(String title) throws Exception {
return doGetTypedWithPageLink("/api/otaPackages?", new TypeReference<PageData<OtaPackage>>() {}, new PageLink(100, 0, title)).getData().get(0);
}
protected Dashboard createDashboard(CustomerId customerId, String name) {
Dashboard dashboard = new Dashboard();
dashboard.setTitle(name);

3
common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java

@ -41,6 +41,8 @@ public interface OtaPackageService extends EntityDaoService {
OtaPackageInfo findOtaPackageInfoById(TenantId tenantId, OtaPackageId otaPackageId);
OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version);
ListenableFuture<OtaPackageInfo> findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId);
PageData<OtaPackageInfo> findTenantOtaPackagesByTenantId(TenantId tenantId, PageLink pageLink);
@ -52,4 +54,5 @@ public interface OtaPackageService extends EntityDaoService {
void deleteOtaPackagesByTenantId(TenantId tenantId);
long sumDataSizeByTenantId(TenantId tenantId);
}

8
common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java

@ -20,6 +20,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.OtaPackageId;
import java.io.Serial;
import java.nio.ByteBuffer;
@Schema
@ -27,6 +28,7 @@ import java.nio.ByteBuffer;
@EqualsAndHashCode(callSuper = true)
public class OtaPackage extends OtaPackageInfo {
@Serial
private static final long serialVersionUID = 3091601761339422546L;
@Schema(description = "OTA Package data.", accessMode = Schema.AccessMode.READ_ONLY)
@ -44,4 +46,10 @@ public class OtaPackage extends OtaPackageInfo {
super(otaPackage);
this.data = otaPackage.getData();
}
public OtaPackage(OtaPackageInfo otaPackageInfo) {
super(otaPackageInfo);
this.data = null;
}
}

12
common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -29,12 +30,15 @@ import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial;
@Schema
@Slf4j
@Data
@EqualsAndHashCode(callSuper = true)
public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> implements HasName, HasTenantId, HasTitle {
public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> implements HasName, HasTenantId, HasTitle, ExportableEntity<OtaPackageId> {
@Serial
private static final long serialVersionUID = 3168391583570815419L;
@Schema(description = "JSON object with Tenant Id. Tenant Id of the ota package can't be changed.", accessMode = Schema.AccessMode.READ_ONLY)
@ -77,6 +81,8 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> imp
@Schema(description = "OTA Package data size.", example = "8", accessMode = Schema.AccessMode.READ_ONLY)
private Long dataSize;
private OtaPackageId externalId;
public OtaPackageInfo() {
super();
}
@ -100,6 +106,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> imp
this.checksumAlgorithm = otaPackageInfo.getChecksumAlgorithm();
this.checksum = otaPackageInfo.getChecksum();
this.dataSize = otaPackageInfo.getDataSize();
this.externalId = otaPackageInfo.getExternalId();
}
@Schema(description = "JSON object with the ota package Id. " +
@ -118,7 +125,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> imp
}
@Override
@JsonIgnore
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public String getName() {
return title;
}
@ -133,4 +140,5 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> imp
public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo();
}
}

3
common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java

@ -23,6 +23,7 @@ import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.TbResourceId;
import java.io.Serial;
import java.util.Base64;
import java.util.Optional;
@ -31,6 +32,7 @@ import java.util.Optional;
@EqualsAndHashCode(callSuper = true)
public class TbResource extends TbResourceInfo {
@Serial
private static final long serialVersionUID = 7379609705527272306L;
private byte[] data;
@ -88,4 +90,5 @@ public class TbResource extends TbResourceInfo {
public String toString() {
return super.toString();
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java

@ -20,10 +20,12 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.io.Serial;
import java.util.UUID;
public class OtaPackageId extends UUIDBased implements EntityId {
@Serial
private static final long serialVersionUID = 1L;
@JsonCreator

4
common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
@ -58,7 +59,8 @@ import java.lang.annotation.Target;
@Type(name = "NOTIFICATION_TEMPLATE", value = NotificationTemplate.class),
@Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class),
@Type(name = "NOTIFICATION_RULE", value = NotificationRule.class),
@Type(name = "TB_RESOURCE", value = TbResource.class)
@Type(name = "TB_RESOURCE", value = TbResource.class),
@Type(name = "OTA_PACKAGE", value = OtaPackage.class)
})
@JsonIgnoreProperties(value = {"tenantId", "createdTime", "version"}, ignoreUnknown = true)
public @interface JsonTbEntity {

3
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java

@ -41,7 +41,8 @@ import java.util.Map;
@Type(name = "DEVICE", value = DeviceExportData.class),
@Type(name = "RULE_CHAIN", value = RuleChainExportData.class),
@Type(name = "WIDGET_TYPE", value = WidgetTypeExportData.class),
@Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class)
@Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class),
@Type(name = "OTA_PACKAGE", value = OtaPackageExportData.class)
})
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data

41
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.sync.ie;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.OtaPackage;
@EqualsAndHashCode(callSuper = true)
public class OtaPackageExportData extends EntityExportData<OtaPackage> {
/*
* OtaPackage is not a versioned entity; its 'version' field is part of the domain model (not used for optimistic locking)
* We override both methods to ensure 'version' is not ignored during (de)serialization.
*/
@JsonIgnoreProperties(value = {"tenantId", "createdTime"}, ignoreUnknown = true)
@Override
public OtaPackage getEntity() {
return super.getEntity();
}
@JsonIgnoreProperties(value = {"tenantId", "createdTime"}, ignoreUnknown = true)
@Override
public void setEntity(OtaPackage entity) {
super.setEntity(entity);
}
}

3
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java

@ -18,10 +18,13 @@ package org.thingsboard.server.common.data.sync.vc.request.create;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
@EqualsAndHashCode(callSuper = true)
@Data
public class AutoVersionCreateConfig extends VersionCreateConfig {
@Serial
private static final long serialVersionUID = 8245450889383315551L;
private String branch;

3
common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java

@ -62,9 +62,6 @@ import java.util.function.BiFunction;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
/**
* Created by Valerii Sosliuk on 5/12/2017.
*/
@Slf4j
public class JacksonUtil {

7
dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java

@ -38,6 +38,7 @@ import org.thingsboard.server.dao.util.mapping.JsonConverter;
import java.nio.ByteBuffer;
import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.EXTERNAL_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_CHECKSUM_ALGORITHM_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_CHECKSUM_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_CONTENT_TYPE_COLUMN;
@ -105,6 +106,9 @@ public class OtaPackageEntity extends BaseSqlEntity<OtaPackage> {
@Column(name = ModelConstants.OTA_PACKAGE_ADDITIONAL_INFO_COLUMN)
private JsonNode additionalInfo;
@Column(name = EXTERNAL_ID_PROPERTY)
private UUID externalId;
public OtaPackageEntity() {
super();
}
@ -128,6 +132,7 @@ public class OtaPackageEntity extends BaseSqlEntity<OtaPackage> {
this.data = otaPackage.getData().array();
this.dataSize = otaPackage.getDataSize();
this.additionalInfo = otaPackage.getAdditionalInfo();
this.externalId = getUuid(otaPackage.getExternalId());
}
@Override
@ -153,6 +158,8 @@ public class OtaPackageEntity extends BaseSqlEntity<OtaPackage> {
otaPackage.setHasData(true);
}
otaPackage.setAdditionalInfo(additionalInfo);
otaPackage.setExternalId(getEntityId(externalId, OtaPackageId::new));
return otaPackage;
}
}

9
dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java

@ -100,6 +100,9 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> {
@Column(name = ModelConstants.OTA_PACKAGE_ADDITIONAL_INFO_COLUMN)
private JsonNode additionalInfo;
@Column(name = ModelConstants.EXTERNAL_ID_PROPERTY)
private UUID externalId;
@Transient
private boolean hasData;
@ -125,11 +128,12 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> {
this.checksum = otaPackageInfo.getChecksum();
this.dataSize = otaPackageInfo.getDataSize();
this.additionalInfo = otaPackageInfo.getAdditionalInfo();
this.externalId = getUuid(otaPackageInfo.getExternalId());
}
public OtaPackageInfoEntity(UUID id, long createdTime, UUID tenantId, UUID deviceProfileId, OtaPackageType type, String title, String version, String tag,
String url, String fileName, String contentType, ChecksumAlgorithm checksumAlgorithm, String checksum, Long dataSize,
Object additionalInfo, boolean hasData) {
Object additionalInfo, UUID externalId, boolean hasData) {
this.id = id;
this.createdTime = createdTime;
this.tenantId = tenantId;
@ -146,6 +150,7 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> {
this.dataSize = dataSize;
this.hasData = hasData;
this.additionalInfo = JacksonUtil.convertValue(additionalInfo, JsonNode.class);
this.externalId = externalId;
}
@Override
@ -168,6 +173,8 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> {
otaPackageInfo.setDataSize(dataSize);
otaPackageInfo.setAdditionalInfo(additionalInfo);
otaPackageInfo.setHasData(hasData);
otaPackageInfo.setExternalId(getEntityId(externalId, OtaPackageId::new));
return otaPackageInfo;
}
}

57
dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java

@ -54,6 +54,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@Slf4j
@RequiredArgsConstructor
public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackageCacheKey, OtaPackageInfo, OtaPackageCacheEvictEvent> implements OtaPackageService {
public static final String INCORRECT_OTA_PACKAGE_ID = "Incorrect otaPackageId ";
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
@ -73,7 +74,7 @@ public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackag
@Override
public OtaPackageInfo saveOtaPackageInfo(OtaPackageInfo otaPackageInfo, boolean isUrl) {
log.trace("Executing saveOtaPackageInfo [{}]", otaPackageInfo);
if (isUrl && (StringUtils.isEmpty(otaPackageInfo.getUrl()) || otaPackageInfo.getUrl().trim().length() == 0)) {
if (isUrl && StringUtils.isBlank(otaPackageInfo.getUrl())) {
throw new DataValidationException("Ota package URL should be specified!");
}
otaPackageInfoValidator.validate(otaPackageInfo, OtaPackageInfo::getTenantId);
@ -90,12 +91,10 @@ public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackag
if (otaPackageId != null) {
handleEvictEvent(new OtaPackageCacheEvictEvent(otaPackageId));
}
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("ota_package_tenant_title_version_unq_key")) {
throw new DataValidationException("OtaPackage with such title and version already exists!");
} else {
throw t;
}
checkConstraintViolation(t,
"ota_package_tenant_title_version_unq_key", "OtaPackage with such title and version already exists!",
"ota_package_external_id_unq_key", "OtaPackage with such external id already exists!");
throw t;
}
}
@ -116,12 +115,10 @@ public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackag
if (otaPackageId != null) {
handleEvictEvent(new OtaPackageCacheEvictEvent(otaPackageId));
}
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("ota_package_tenant_title_version_unq_key")) {
throw new DataValidationException("OtaPackage with such title and version already exists!");
} else {
throw t;
}
checkConstraintViolation(t,
"ota_package_tenant_title_version_unq_key", "OtaPackage with such title and version already exists!",
"ota_package_external_id_unq_key", "OtaPackage with such external id already exists!");
throw t;
}
}
@ -136,24 +133,16 @@ public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackag
@SuppressWarnings("deprecation")
private HashFunction getHashFunction(ChecksumAlgorithm checksumAlgorithm) {
switch (checksumAlgorithm) {
case MD5:
return Hashing.md5();
case SHA256:
return Hashing.sha256();
case SHA384:
return Hashing.sha384();
case SHA512:
return Hashing.sha512();
case CRC32:
return Hashing.crc32();
case MURMUR3_32:
return Hashing.murmur3_32();
case MURMUR3_128:
return Hashing.murmur3_128();
default:
throw new DataValidationException("Unknown checksum algorithm!");
}
return switch (checksumAlgorithm) {
case MD5 -> Hashing.md5();
case SHA256 -> Hashing.sha256();
case SHA384 -> Hashing.sha384();
case SHA512 -> Hashing.sha512();
case CRC32 -> Hashing.crc32();
case MURMUR3_32 -> Hashing.murmur3_32();
case MURMUR3_128 -> Hashing.murmur3_128();
default -> throw new DataValidationException("Unknown checksum algorithm!");
};
}
@Override
@ -171,6 +160,12 @@ public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackag
() -> otaPackageInfoDao.findById(tenantId, otaPackageId.getId()), true);
}
@Override
public OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version) {
log.trace("Executing findOtaPackageByTenantIdAndTitle [{}] [{}] [{}]", tenantId, title, version);
return otaPackageDao.findOtaPackageByTenantIdAndTitleAndVersion(tenantId, title, version);
}
@Override
public ListenableFuture<OtaPackageInfo> findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId) {
log.trace("Executing findOtaPackageInfoByIdAsync [{}]", otaPackageId);

7
dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java

@ -16,12 +16,17 @@
package org.thingsboard.server.dao.ota;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ExportableEntityDao;
import org.thingsboard.server.dao.TenantEntityWithDataDao;
public interface OtaPackageDao extends Dao<OtaPackage>, TenantEntityWithDataDao {
public interface OtaPackageDao extends Dao<OtaPackage>, TenantEntityWithDataDao, ExportableEntityDao<OtaPackageId, OtaPackage> {
Long sumDataSizeByTenantId(TenantId tenantId);
OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version);
}

1
dao/src/main/java/org/thingsboard/server/dao/service/validator/OtaPackageDataValidator.java

@ -103,4 +103,5 @@ public class OtaPackageDataValidator extends BaseOtaPackageDataValidator<OtaPack
}
return otaPackageOld;
}
}

3
dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java

@ -40,9 +40,6 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* @author Valerii Sosliuk
*/
@Slf4j
@SqlDao
public abstract class JpaAbstractDao<E extends BaseEntity<D>, D>

3
dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java

@ -48,9 +48,6 @@ import java.util.UUID;
import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityInfosToDto;
/**
* Created by Valerii Sosliuk on 5/19/2017.
*/
@Component
@SqlDao
@Slf4j

45
dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java

@ -22,6 +22,7 @@ import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -43,24 +44,52 @@ public class JpaOtaPackageDao extends JpaAbstractDao<OtaPackageEntity, OtaPackag
private OtaPackageRepository otaPackageRepository;
@Override
protected Class<OtaPackageEntity> getEntityClass() {
return OtaPackageEntity.class;
public Long sumDataSizeByTenantId(TenantId tenantId) {
return otaPackageRepository.sumDataSizeByTenantId(tenantId.getId());
}
@Transactional
@Override
protected JpaRepository<OtaPackageEntity, UUID> getRepository() {
return otaPackageRepository;
public OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version) {
return DaoUtil.getData(otaPackageRepository.findByTenantIdAndTitleAndVersion(tenantId.getId(), title, version));
}
@Transactional
@Override
public Long sumDataSizeByTenantId(TenantId tenantId) {
return otaPackageRepository.sumDataSizeByTenantId(tenantId.getId());
public PageData<OtaPackage> findAllByTenantId(TenantId tenantId, PageLink pageLink) {
return DaoUtil.toPageData(otaPackageRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink)));
}
@Transactional
@Override
public PageData<OtaPackage> findAllByTenantId(TenantId tenantId, PageLink pageLink) {
return DaoUtil.toPageData(otaPackageRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink)));
public PageData<OtaPackage> findByTenantId(UUID tenantId, PageLink pageLink) {
return findAllByTenantId(TenantId.fromUUID(tenantId), pageLink);
}
@Override
public PageData<OtaPackageId> findIdsByTenantId(UUID tenantId, PageLink pageLink) {
return DaoUtil.pageToPageData(otaPackageRepository.findIdsByTenantId(tenantId, DaoUtil.toPageable(pageLink)).map(OtaPackageId::new));
}
@Transactional
@Override
public OtaPackage findByTenantIdAndExternalId(UUID tenantId, UUID externalId) {
return DaoUtil.getData(otaPackageRepository.findByTenantIdAndExternalId(tenantId, externalId));
}
@Override
public OtaPackageId getExternalIdByInternal(OtaPackageId internalId) {
return DaoUtil.toEntityId(otaPackageRepository.getExternalIdById(internalId.getId()), OtaPackageId::new);
}
@Override
protected Class<OtaPackageEntity> getEntityClass() {
return OtaPackageEntity.class;
}
@Override
protected JpaRepository<OtaPackageEntity, UUID> getRepository() {
return otaPackageRepository;
}
@Override

7
dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java

@ -26,14 +26,15 @@ import org.thingsboard.server.dao.model.sql.OtaPackageInfoEntity;
import java.util.UUID;
public interface OtaPackageInfoRepository extends JpaRepository<OtaPackageInfoEntity, UUID> {
@Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE " +
@Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, f.externalId, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE " +
"f.tenantId = :tenantId " +
"AND (:searchText IS NULL OR ilike(f.title, CONCAT('%', :searchText, '%')) = true)")
Page<OtaPackageInfoEntity> findAllByTenantId(@Param("tenantId") UUID tenantId,
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, true) FROM OtaPackageEntity f WHERE " +
@Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, f.externalId, true) FROM OtaPackageEntity f WHERE " +
"f.tenantId = :tenantId " +
"AND f.deviceProfileId = :deviceProfileId " +
"AND f.type = :type " +
@ -45,7 +46,7 @@ public interface OtaPackageInfoRepository extends JpaRepository<OtaPackageInfoEn
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE f.id = :id")
@Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, f.externalId, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE f.id = :id")
OtaPackageInfoEntity findOtaPackageInfoById(@Param("id") UUID id);
@Query(value = "SELECT exists(SELECT * " +

12
dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageRepository.java

@ -20,15 +20,25 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.dao.ExportableEntityRepository;
import org.thingsboard.server.dao.model.sql.OtaPackageEntity;
import java.util.UUID;
public interface OtaPackageRepository extends JpaRepository<OtaPackageEntity, UUID> {
public interface OtaPackageRepository extends JpaRepository<OtaPackageEntity, UUID>, ExportableEntityRepository<OtaPackageEntity> {
@Query(value = "SELECT COALESCE(SUM(ota.data_size), 0) FROM ota_package ota WHERE ota.tenant_id = :tenantId AND ota.data IS NOT NULL", nativeQuery = true)
Long sumDataSizeByTenantId(@Param("tenantId") UUID tenantId);
Page<OtaPackageEntity> findByTenantId(UUID tenantId, Pageable pageable);
OtaPackageEntity findByTenantIdAndTitleAndVersion(UUID tenantId, String title, String version);
@Query("SELECT externalId FROM OtaPackageEntity WHERE id = :id")
UUID getExternalIdById(@Param("id") UUID id);
@Query("SELECT r.id FROM OtaPackageEntity r WHERE r.tenantId = :tenantId")
Page<UUID> findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable);
}

2
dao/src/main/resources/sql/schema-entities-idx.sql

@ -91,6 +91,8 @@ CREATE INDEX IF NOT EXISTS idx_widgets_bundle_external_id ON widgets_bundle(tena
CREATE INDEX IF NOT EXISTS idx_rule_node_external_id ON rule_node(rule_chain_id, external_id);
CREATE INDEX IF NOT EXISTS idx_ota_package_external_id ON ota_package(tenant_id, external_id);
CREATE INDEX IF NOT EXISTS idx_rule_node_type_id_configuration_version ON rule_node(type, id, configuration_version);
CREATE INDEX IF NOT EXISTS idx_api_usage_state_entity_id ON api_usage_state(entity_id);

4
dao/src/main/resources/sql/schema-entities.sql

@ -216,7 +216,9 @@ CREATE TABLE IF NOT EXISTS ota_package (
data oid,
data_size bigint,
additional_info varchar,
CONSTRAINT ota_package_tenant_title_version_unq_key UNIQUE (tenant_id, title, version)
external_id uuid,
CONSTRAINT ota_package_tenant_title_version_unq_key UNIQUE (tenant_id, title, version),
CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id)
);
CREATE TABLE IF NOT EXISTS queue (

2
ui-ngx/src/app/modules/home/pages/admin/admin.module.ts

@ -38,6 +38,7 @@ import { JsLibraryTableHeaderComponent } from '@home/pages/admin/resource/js-lib
import { JsResourceComponent } from '@home/pages/admin/resource/js-resource.component';
import { NgxFlowModule } from '@flowjs/ngx-flow';
import { TrendzSettingsComponent } from '@home/pages/admin/trendz-settings.component';
import { ResourceLibraryTabsComponent } from '@home/pages/admin/resource/resource-library-tabs.component';
@NgModule({
declarations:
@ -50,6 +51,7 @@ import { TrendzSettingsComponent } from '@home/pages/admin/trendz-settings.compo
HomeSettingsComponent,
ResourcesLibraryComponent,
ResourceTabsComponent,
ResourceLibraryTabsComponent,
ResourcesTableHeaderComponent,
JsResourceComponent,
JsLibraryTableHeaderComponent,

23
ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html

@ -0,0 +1,23 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-tab *ngIf="entity && entity.tenantId.id !== NULL_UUID && authUser.authority === authorities.TENANT_ADMIN && !isEdit"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [entityName]="entity.name" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

36
ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts

@ -0,0 +1,36 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component';
import { Resource } from '@shared/models/resource.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { NULL_UUID } from '@shared/models/id/has-uuid';
@Component({
selector: 'tb-resource-library-tabs',
templateUrl: './resource-library-tabs.component.html',
styleUrls: []
})
export class ResourceLibraryTabsComponent extends EntityTabsComponent<Resource> {
readonly NULL_UUID = NULL_UUID;
constructor(protected store: Store<AppState>) {
super(store);
}
}

2
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts

@ -37,6 +37,7 @@ import { PageLink } from '@shared/models/page/page-link';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { map } from 'rxjs/operators';
import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component';
import { ResourceLibraryTabsComponent } from '@home/pages/admin/resource/resource-library-tabs.component';
@Injectable()
export class ResourcesLibraryTableConfigResolver {
@ -55,6 +56,7 @@ export class ResourcesLibraryTableConfigResolver {
this.config.entityTranslations = entityTypeTranslations.get(EntityType.TB_RESOURCE);
this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE);
this.config.headerComponent = ResourcesTableHeaderComponent;
this.config.entityTabsComponent = ResourceLibraryTabsComponent;
this.config.entityTitle = (resource) => resource ?
resource.title : '';

2
ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts

@ -36,6 +36,7 @@ import { PageLink } from '@shared/models/page/page-link';
import { OtaUpdateComponent } from '@home/pages/ota-update/ota-update.component';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { FileSizePipe } from '@shared/pipe/file-size.pipe';
import { OtaUpdateTabsComponent } from '@home/pages/ota-update/ota-update-tabs.component';
@Injectable()
export class OtaUpdateTableConfigResolve {
@ -50,6 +51,7 @@ export class OtaUpdateTableConfigResolve {
private fileSize: FileSizePipe) {
this.config.entityType = EntityType.OTA_PACKAGE;
this.config.entityComponent = OtaUpdateComponent;
this.config.entityTabsComponent = OtaUpdateTabsComponent;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.OTA_PACKAGE);
this.config.entityResources = entityTypeResources.get(EntityType.OTA_PACKAGE);

23
ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html

@ -0,0 +1,23 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-tab *ngIf="isTenantOtaUpdate() && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [entityName]="entity.title" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

40
ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts

@ -0,0 +1,40 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { OtaPackage } from '@shared/models/ota-package.models';
@Component({
selector: 'tb-ota-update-tabs',
templateUrl: './ota-update-tabs.component.html',
styleUrls: []
})
export class OtaUpdateTabsComponent extends EntityTabsComponent<OtaPackage> {
constructor(protected store: Store<AppState>) {
super(store);
}
isTenantOtaUpdate() {
return this.entity && this.entity.tenantId.id !== NULL_UUID;
}
}

4
ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts

@ -20,10 +20,12 @@ import { SharedModule } from '@shared/shared.module';
import { HomeComponentsModule } from '@home/components/home-components.module';
import { OtaUpdateRoutingModule } from '@home/pages/ota-update/ota-update-routing.module';
import { OtaUpdateComponent } from '@home/pages/ota-update/ota-update.component';
import { OtaUpdateTabsComponent } from '@home/pages/ota-update/ota-update-tabs.component';
@NgModule({
declarations: [
OtaUpdateComponent
OtaUpdateComponent,
OtaUpdateTabsComponent
],
imports: [
CommonModule,

2
ui-ngx/src/app/shared/models/entity-type.models.ts

@ -346,6 +346,8 @@ export const entityTypeTranslations = new Map<EntityType | AliasEntityType, Enti
EntityType.OTA_PACKAGE,
{
type: 'entity.type-ota-package',
typePlural: 'entity.type-ota-packages',
list: 'entity.list-of-ota-packages',
details: 'ota-update.ota-update-details',
add: 'ota-update.add',
noEntities: 'ota-update.no-packages-text',

4
ui-ngx/src/app/shared/models/ota-package.models.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { BaseData } from '@shared/models/base-data';
import { BaseData, ExportableEntity } from '@shared/models/base-data';
import { TenantId } from '@shared/models/id/tenant-id';
import { OtaPackageId } from '@shared/models/id/ota-package-id';
import { DeviceProfileId } from '@shared/models/id/device-profile-id';
@ -86,7 +86,7 @@ export interface OtaPagesIds {
softwareId?: OtaPackageId;
}
export interface OtaPackageInfo extends BaseData<OtaPackageId>, HasTenantId {
export interface OtaPackageInfo extends Omit<BaseData<OtaPackageId>, 'label'>, HasTenantId, ExportableEntity<OtaPackageId> {
tenantId?: TenantId;
type: OtaUpdateType;
deviceProfileId?: DeviceProfileId;

6
ui-ngx/src/app/shared/models/vc.models.ts

@ -33,16 +33,18 @@ export const exportableEntityTypes: Array<EntityType> = [
EntityType.WIDGET_TYPE,
EntityType.WIDGETS_BUNDLE,
EntityType.TB_RESOURCE,
EntityType.OTA_PACKAGE,
EntityType.NOTIFICATION_TEMPLATE,
EntityType.NOTIFICATION_TARGET,
EntityType.NOTIFICATION_RULE
];
export const entityTypesWithoutRelatedData: Set<EntityType | AliasEntityType> = new Set([
export const entityTypesWithoutRelatedData = new Set<EntityType | AliasEntityType>([
EntityType.NOTIFICATION_TEMPLATE,
EntityType.NOTIFICATION_TARGET,
EntityType.NOTIFICATION_RULE,
EntityType.TB_RESOURCE
EntityType.TB_RESOURCE,
EntityType.OTA_PACKAGE,
]);
export interface VersionCreateConfig {

2
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -2584,6 +2584,8 @@
"type-tb-resources": "Resources",
"list-of-tb-resources": "{ count, plural, =1 {One resource} other {List of # resources} }",
"type-ota-package": "OTA package",
"type-ota-packages": "OTA packages",
"list-of-ota-packages": "{ count, plural, =1 {One OTA package} other {List of # OTA packages} }",
"type-rpc": "RPC",
"type-queue": "Queue",
"type-queue-stats": "Queue statistics",

Loading…
Cancel
Save