diff --git a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java index 002956924d..fa60ea3033 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java @@ -17,9 +17,11 @@ package org.thingsboard.server.controller; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.springframework.core.io.ByteArrayResource; +import org.hibernate.engine.jdbc.BlobProxy; +import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -39,15 +41,20 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.OtaPackageId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.cache.ota.service.FileCacheService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; -import java.nio.ByteBuffer; +import javax.transaction.Transactional; +import java.io.InputStream; +import java.sql.Blob; +import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.OTA_PACKAGE_CHECKSUM_ALGORITHM_ALLOWABLE_VALUES; @@ -69,32 +76,35 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI @Slf4j @RestController @TbCoreComponent +@RequiredArgsConstructor @RequestMapping("/api") public class OtaPackageController extends BaseController { public static final String OTA_PACKAGE_ID = "otaPackageId"; public static final String CHECKSUM_ALGORITHM = "checksumAlgorithm"; + private final FileCacheService fileCacheService; + @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 - public ResponseEntity downloadOtaPackage(@ApiParam(value = OTA_PACKAGE_ID_PARAM_DESCRIPTION) + @Transactional + public ResponseEntity downloadOtaPackage(@ApiParam(value = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); try { OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); - OtaPackage otaPackage = checkOtaPackageId(otaPackageId, Operation.READ); - +// OtaPackage otaPackage = checkOtaPackageId(otaPackageId, Operation.READ); + OtaPackage otaPackage = otaPackageService.findOtaPackageById(new TenantId(UUID.fromString("b04858a0-b646-11ec-8d4d-f5c10326c05e")), otaPackageId); if (otaPackage.hasUrl()) { return ResponseEntity.badRequest().build(); } - - ByteArrayResource resource = new ByteArrayResource(otaPackage.getData().array()); + InputStreamResource resource = fileCacheService.getOtaResourceById(new TenantId(UUID.fromString("b04858a0-b646-11ec-8d4d-f5c10326c05e")), otaPackageId); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + otaPackage.getFileName()) .header("x-filename", otaPackage.getFileName()) - .contentLength(resource.contentLength()) + .contentLength(otaPackage.getDataSize()) .contentType(parseMediaType(otaPackage.getContentType())) .body(resource); } catch (Exception e) { @@ -184,32 +194,12 @@ public class OtaPackageController extends BaseController { checkParameter(CHECKSUM_ALGORITHM, checksumAlgorithmStr); try { OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); - OtaPackageInfo info = checkOtaPackageInfoId(otaPackageId, Operation.READ); - - OtaPackage otaPackage = new OtaPackage(otaPackageId); - otaPackage.setCreatedTime(info.getCreatedTime()); - otaPackage.setTenantId(getTenantId()); - otaPackage.setDeviceProfileId(info.getDeviceProfileId()); - otaPackage.setType(info.getType()); - otaPackage.setTitle(info.getTitle()); - otaPackage.setVersion(info.getVersion()); - otaPackage.setTag(info.getTag()); - otaPackage.setAdditionalInfo(info.getAdditionalInfo()); - ChecksumAlgorithm checksumAlgorithm = ChecksumAlgorithm.valueOf(checksumAlgorithmStr.toUpperCase()); - byte[] bytes = file.getBytes(); if (StringUtils.isEmpty(checksum)) { - checksum = otaPackageService.generateChecksum(checksumAlgorithm, ByteBuffer.wrap(bytes)); + checksum = otaPackageService.generateChecksum(checksumAlgorithm, file.getInputStream()); } - - otaPackage.setChecksumAlgorithm(checksumAlgorithm); - otaPackage.setChecksum(checksum); - otaPackage.setFileName(file.getOriginalFilename()); - otaPackage.setContentType(file.getContentType()); - otaPackage.setData(ByteBuffer.wrap(bytes)); - otaPackage.setDataSize((long) bytes.length); - OtaPackageInfo savedOtaPackage = otaPackageService.saveOtaPackage(otaPackage); + OtaPackageInfo savedOtaPackage = saveOtaPackageWithData(otaPackageId, file, checksum, checksumAlgorithm); logEntityAction(savedOtaPackage.getId(), savedOtaPackage, null, ActionType.UPDATED, null); return savedOtaPackage; } catch (Exception e) { @@ -218,6 +208,32 @@ public class OtaPackageController extends BaseController { } } + private OtaPackage saveOtaPackageWithData(OtaPackageId otaPackageId, MultipartFile file, String checksum, ChecksumAlgorithm checksumAlgorithm) throws ThingsboardException { + OtaPackageInfo info = checkOtaPackageInfoId(otaPackageId, Operation.READ); + OtaPackage otaPackage = new OtaPackage(otaPackageId); + otaPackage.setCreatedTime(info.getCreatedTime()); + otaPackage.setTenantId(info.getTenantId()); + otaPackage.setDeviceProfileId(info.getDeviceProfileId()); + otaPackage.setType(info.getType()); + otaPackage.setTitle(info.getTitle()); + otaPackage.setVersion(info.getVersion()); + otaPackage.setTag(info.getTag()); + otaPackage.setAdditionalInfo(info.getAdditionalInfo()); + otaPackage.setChecksumAlgorithm(checksumAlgorithm); + otaPackage.setChecksum(checksum); + try(InputStream inputStream = file.getInputStream()) { + otaPackage.setFileName(file.getOriginalFilename()); + otaPackage.setContentType(file.getContentType()); + otaPackage.setDataSize(file.getSize()); + Blob blob = BlobProxy.generateProxy(inputStream, file.getSize()); + otaPackage.setData(blob); + return otaPackageService.saveOtaPackage(otaPackage); + } catch (Exception e){ + log.error("Failed to save ota package {} with data {}",otaPackageId, file.getName(), e); + return null; + } + } + @ApiOperation(value = "Get OTA Package Infos (getOtaPackages)", notes = "Returns a page of OTA Package Info objects owned by tenant. " + PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java index b8c0656bd2..821ecd2b35 100644 --- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java @@ -22,11 +22,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.HasName; -import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.*; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; @@ -159,9 +155,15 @@ public class EntityActionService { } ObjectNode entityNode; if (entity != null) { - entityNode = json.valueToTree(entity); - if (entityId.getEntityType() == EntityType.DASHBOARD) { - entityNode.put("configuration", ""); + if (entity instanceof OtaPackage) { + OtaPackage otaPackage = (OtaPackage) entity; + otaPackage.setData(null); + entityNode = json.valueToTree(otaPackage); //todo + } else { + entityNode = json.valueToTree(entity); + if (entityId.getEntityType() == EntityType.DASHBOARD) { + entityNode.put("configuration", ""); + } } } else { entityNode = json.createObjectNode(); diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 5fda933234..ae6e0a6462 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -99,6 +99,7 @@ import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.resource.TbResourceService; +import java.sql.SQLException; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -596,7 +597,11 @@ public class DefaultTransportApiService implements TransportApiService { builder.setContentType(otaPackageInfo.getContentType()); if (!otaPackageDataCache.has(otaPackageId.toString())) { OtaPackage otaPackage = otaPackageService.findOtaPackageById(tenantId, otaPackageId); - otaPackageDataCache.put(otaPackageId.toString(), otaPackage.getData().array()); + try { + otaPackageDataCache.put(otaPackageId.toString(), otaPackage.getData().getBytes(100L, 0));//todo + } catch (SQLException e) { + log.error("ERROR", e); // todo + } } } } diff --git a/common/cache/pom.xml b/common/cache/pom.xml index 351a23ea4c..4b87f6a78e 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -40,6 +40,10 @@ org.thingsboard.common data + + org.thingsboard.common + dao-api + org.springframework.boot spring-boot-autoconfigure diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/ota/service/FileCacheService.java b/common/cache/src/main/java/org/thingsboard/server/cache/ota/service/FileCacheService.java new file mode 100644 index 0000000000..dd9f1eb049 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/ota/service/FileCacheService.java @@ -0,0 +1,163 @@ +/** + * Copyright © 2016-2022 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.cache.ota.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.springframework.core.io.InputStreamResource; +import org.springframework.stereotype.Component; +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.dao.ota.OtaPackageService; + +import javax.annotation.PostConstruct; +import java.io.*; +import java.sql.Blob; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.*; +import java.util.stream.Collectors; + +@Slf4j +@RequiredArgsConstructor +@Component +public class FileCacheService { + private final static String PATH = "files/%s"; + private final static String FILE_NAME_TEMPLATE = "%s.tmp"; + private final static long TEMPORARY_FILE_INACTIVITY_TIME = 300; + private final ConcurrentMap files = new ConcurrentHashMap<>(); + private final ConcurrentMap lastActivityTimes = new ConcurrentHashMap<>(); + private final OtaPackageService otaPackageService; + + private void deleteUnusedTemporaryFiles() { + long currentTime = System.currentTimeMillis(); + List toBeDeleted = lastActivityTimes.entrySet() + .stream() + .filter(entry -> currentTime - entry.getValue() > TEMPORARY_FILE_INACTIVITY_TIME) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + try { + toBeDeleted.forEach(otaId -> { + deleteFile(otaId.getId().toString()); + lastActivityTimes.remove(otaId); + }); + }catch (Exception e){ + log.error(",,", e); + } + log.info("Deleted {} unused temporary files", toBeDeleted.size()); + } + + private void deleteFile(String otaId) { + if (exist(otaId)) { + String fileName = String.format(FILE_NAME_TEMPLATE, otaId); + File file = new File(String.format(PATH, fileName)); + file.delete(); + } + } + + private void cleanDirectoryWithTemporaryFiles() { + File directory = new File(String.format(PATH, "")); + if (directory.isDirectory()) { + try { + FileUtils.cleanDirectory(directory); + log.info("Directory with temporary files cleaned"); + } catch (IOException e) { + log.error("Failed to clean directory with temporary files", e); + throw new RuntimeException(e); + } + } + } + + + @PostConstruct + private void createScheduledTaskToClear() { + cleanDirectoryWithTemporaryFiles(); + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + scheduler.scheduleAtFixedRate(this::deleteUnusedTemporaryFiles, 3, 3, TimeUnit.MINUTES); + } + + public InputStreamResource getOtaResourceById(TenantId tenantId, OtaPackageId otaPackageId) { + try { + String fileName = String.format(FILE_NAME_TEMPLATE, otaPackageId.getId().toString()); + if (exist(fileName)) { + lastActivityTimes.put(otaPackageId, System.currentTimeMillis()); + return new InputStreamResource(new FileInputStream(String.format(PATH, fileName))); + } + InputStreamResource resource = load(tenantId, otaPackageId); + lastActivityTimes.put(otaPackageId, System.currentTimeMillis()); + files.remove(otaPackageId); + return resource; + } catch (FileNotFoundException e) { + log.error("Failed to find resource for otaPackage {}", otaPackageId, e); + throw new RuntimeException(e); + } + } + + private InputStreamResource load(TenantId tenantId, OtaPackageId otaPackageId) throws FileNotFoundException { + CountDownLatch latch = files.computeIfAbsent(otaPackageId, ota -> processFileSaving(tenantId, ota)); + String fileName = String.format(FILE_NAME_TEMPLATE, otaPackageId.getId().toString()); + if (latch.getCount() == 0) { + return new InputStreamResource(new FileInputStream(String.format(PATH, fileName))); + } else { + try { + latch.await(); + return new InputStreamResource(new FileInputStream(String.format(PATH, fileName))); + } catch (InterruptedException e) { + log.error("Failed to save file {} to file system", fileName, e); + throw new RuntimeException(e); + } + } + } + + private CountDownLatch processFileSaving(TenantId tenantId, OtaPackageId otaPackageId) { + CountDownLatch latch = new CountDownLatch(1); + OtaPackage otaPackage = otaPackageService.findOtaPackageById(tenantId, otaPackageId); + if (otaPackage == null) { + latch.countDown(); + log.error("Can't find otaPackage to download file {}", otaPackage); + throw new RuntimeException("No such OtaPackageId"); + } + try { + Blob data = otaPackage.getData(); + InputStream binaryStream = data.getBinaryStream(); + String fileName = String.format(FILE_NAME_TEMPLATE, otaPackageId.getId().toString()); + saveAsSystemFile(fileName, binaryStream); + latch.countDown(); + return latch; + } catch (SQLException e) { + log.error("Failed to get binary data from otaPackage {}", otaPackageId, e); + throw new RuntimeException("Failed to get binary data from database"); + } + } + + private void saveAsSystemFile(String fileName, InputStream inputStream) { + try { + File file = new File(String.format(PATH, fileName)); + FileUtils.copyInputStreamToFile(inputStream, file); + } catch (IOException e) { + log.error("Failed to copy stream to system file {}", fileName, e); + throw new RuntimeException("Failed to save file"); + } + } + + private boolean exist(String name) { + File file = new File(String.format(PATH, name)); + return file.exists(); + } +} \ No newline at end of file diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java index c643ae4797..8e8038cf32 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import java.io.InputStream; import java.nio.ByteBuffer; public interface OtaPackageService { @@ -51,4 +52,5 @@ public interface OtaPackageService { void deleteOtaPackagesByTenantId(TenantId tenantId); long sumDataSizeByTenantId(TenantId tenantId); + String generateChecksum(ChecksumAlgorithm checksumAlgorithm, InputStream fileData); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java index 3a17aefab8..b4957443e9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java @@ -22,6 +22,7 @@ import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.OtaPackageId; import java.nio.ByteBuffer; +import java.sql.Blob; @ApiModel @Data @@ -31,7 +32,7 @@ public class OtaPackage extends OtaPackageInfo { private static final long serialVersionUID = 3091601761339422546L; @ApiModelProperty(position = 16, value = "OTA Package data.", readOnly = true) - private transient ByteBuffer data; + private transient Blob data; public OtaPackage() { super(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java index 9d61cf52f1..f83b768e41 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java @@ -38,6 +38,7 @@ import javax.persistence.Enumerated; import javax.persistence.Lob; import javax.persistence.Table; import java.nio.ByteBuffer; +import java.sql.Blob; import java.util.UUID; import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_CHECKSUM_ALGORITHM_COLUMN; @@ -100,7 +101,7 @@ public class OtaPackageEntity extends BaseSqlEntity implements Searc @Lob @Column(name = OTA_PACKAGE_DATA_COLUMN, columnDefinition = "BINARY") - private byte[] data; + private Blob data; @Column(name = OTA_PACKAGE_DATA_SIZE_COLUMN) private Long dataSize; @@ -132,7 +133,7 @@ public class OtaPackageEntity extends BaseSqlEntity implements Searc this.contentType = otaPackage.getContentType(); this.checksumAlgorithm = otaPackage.getChecksumAlgorithm(); this.checksum = otaPackage.getChecksum(); - this.data = otaPackage.getData().array(); + this.data = otaPackage.getData(); this.dataSize = otaPackage.getDataSize(); this.additionalInfo = otaPackage.getAdditionalInfo(); } @@ -166,7 +167,7 @@ public class OtaPackageEntity extends BaseSqlEntity implements Searc otaPackage.setChecksum(checksum); otaPackage.setDataSize(dataSize); if (data != null) { - otaPackage.setData(ByteBuffer.wrap(data)); + otaPackage.setData(data); otaPackage.setHasData(true); } otaPackage.setAdditionalInfo(additionalInfo); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index ecf8d68267..6a1c14a639 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -20,6 +20,8 @@ import com.google.common.hash.Hashing; import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.hibernate.engine.jdbc.BlobProxy; import org.hibernate.exception.ConstraintViolationException; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; @@ -40,10 +42,15 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; +import java.io.*; import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.sql.SQLException; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.UUID; import static org.thingsboard.server.common.data.CacheConstants.OTA_PACKAGE_CACHE; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -56,6 +63,8 @@ public class BaseOtaPackageService implements OtaPackageService { public static final String INCORRECT_OTA_PACKAGE_ID = "Incorrect otaPackageId "; public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + private static final int ONE_MEGA_BYTE = 1_000_000; + private final OtaPackageDao otaPackageDao; private final OtaPackageInfoDao otaPackageInfoDao; private final CacheManager cacheManager; @@ -91,15 +100,28 @@ public class BaseOtaPackageService implements OtaPackageService { @Override public OtaPackage saveOtaPackage(OtaPackage otaPackage) { log.trace("Executing saveOtaPackage [{}]", otaPackage); - otaPackageValidator.validate(otaPackage, OtaPackageInfo::getTenantId); try { + File tempFile = saveDataToTemporaryFile(otaPackage.getData().getBinaryStream()); + otaPackage.setData(BlobProxy.generateProxy(new FileInputStream(tempFile), otaPackage.getDataSize())); + try { + otaPackageValidator.validate(otaPackage, OtaPackageInfo::getTenantId); + }catch (RuntimeException e){ + deleteTempFile(tempFile); + throw e; + } + otaPackage.setData(BlobProxy.generateProxy(new FileInputStream(tempFile), otaPackage.getDataSize())); OtaPackageId otaPackageId = otaPackage.getId(); if (otaPackageId != null) { Cache cache = cacheManager.getCache(OTA_PACKAGE_CACHE); cache.evict(toOtaPackageInfoKey(otaPackageId)); otaPackageDataCache.evict(otaPackageId.toString()); } - return otaPackageDao.save(otaPackage.getTenantId(), otaPackage); + OtaPackage save = otaPackageDao.save(otaPackage.getTenantId(), otaPackage); + deleteTempFile(tempFile); + return save; + } catch (FileNotFoundException | SQLException e){ + log.error("Failed to validate ota package {}",otaPackage.getId(), e); + throw new RuntimeException(e); } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("ota_package_tenant_title_version_unq_key")) { @@ -110,6 +132,30 @@ public class BaseOtaPackageService implements OtaPackageService { } } + + public void deleteTempFile(File file) { + try { + if(file.exists()) { + FileUtils.delete(file); + log.info("System file {} was deleted", file.getName()); + } + } catch (IOException e) { + log.error("Failed to delete file {}", file.getName(), e); + } + } + + public File saveDataToTemporaryFile(InputStream inputStream){ + File path = new File("files/"); + try { + File tempFile = File.createTempFile(UUID.randomUUID().toString(), ".tmp", path); + FileUtils.copyInputStreamToFile(inputStream, tempFile); + return tempFile; + }catch (IOException e){ + log.error("Failed to create temp file", e); + throw new RuntimeException("Failed to create temp file for input stream"); + } + } + @Override public String generateChecksum(ChecksumAlgorithm checksumAlgorithm, ByteBuffer data) { if (data == null || !data.hasArray() || data.array().length == 0) { @@ -244,4 +290,33 @@ public class BaseOtaPackageService implements OtaPackageService { return Collections.singletonList(otaPackageId); } + public String generateChecksum(ChecksumAlgorithm checksumAlgorithm, InputStream fileData) { + try { + MessageDigest md = MessageDigest.getInstance(checksumAlgorithm.name()); + return checksum(fileData, md); + } catch (NoSuchAlgorithmException e) { + log.error("No such checksum algorithm {}", checksumAlgorithm, e); + throw new RuntimeException(e); + } catch (IOException e) { + log.error("Failed to calculate checksum", e); + throw new RuntimeException(e); + } catch (Exception e) { + log.error("ff", e); + throw new RuntimeException(e); + } + } + + private String checksum(InputStream inputStream, MessageDigest md) throws IOException { + byte[] buffer = new byte[ONE_MEGA_BYTE]; + int count = 0; + while ((count = inputStream.read(buffer)) != -1) { + md.update(buffer, 0, count); + } + StringBuilder result = new StringBuilder(); + for (byte b : md.digest()) { + result.append(String.format("%02x", b)); + } + return result.toString(); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/OtaPackageDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/OtaPackageDataValidator.java index 8b261ad226..449d005ec1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/OtaPackageDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/OtaPackageDataValidator.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.service.validator; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; @@ -27,9 +28,14 @@ import org.thingsboard.server.dao.ota.OtaPackageDao; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; + +import java.sql.SQLException; + + import static org.thingsboard.server.common.data.EntityType.OTA_PACKAGE; @Component +@Slf4j public class OtaPackageDataValidator extends BaseOtaPackageDataValidator { @Autowired @@ -70,13 +76,14 @@ public class OtaPackageDataValidator extends BaseOtaPackageDataValidator