From e7aabe80a43c30e29a77c6d6e15c1bb8f932052d Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 15 Nov 2023 16:10:18 +0200 Subject: [PATCH] Unique image resource key based on index --- .../server/controller/ImageController.java | 26 ++++++------ .../resource/DefaultTbImageService.java | 1 - .../server/common/data/ImageDescriptor.java | 1 + .../thingsboard/common/util/RegexUtils.java | 11 +++++ .../server/dao/resource/BaseImageService.java | 41 +++++++++++++++++-- .../dao/resource/BaseResourceService.java | 23 ++++++----- .../dao/resource/TbResourceInfoDao.java | 4 ++ .../validator/ResourceDataValidator.java | 5 ++- .../sql/resource/JpaTbResourceInfoDao.java | 10 +++++ .../resource/TbResourceInfoRepository.java | 9 +++- 10 files changed, 102 insertions(+), 29 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/ImageController.java b/application/src/main/java/org/thingsboard/server/controller/ImageController.java index 159cd1eb0c..add38ee272 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -125,7 +125,8 @@ public class ImageController extends BaseController { TenantId tenantId = getTenantId(); TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ); ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); - return downloadIfChanged(etag, imageInfo, descriptor, () -> imageService.getImageData(tenantId, imageInfo.getId())); + return downloadIfChanged(etag, descriptor, imageInfo.getFileName(), + () -> imageService.getImageData(tenantId, imageInfo.getId())); } @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @@ -136,7 +137,8 @@ public class ImageController extends BaseController { TenantId tenantId = getTenantId(); TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ); ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); - return downloadIfChanged(etag, imageInfo, descriptor.getPreviewDescriptor(), () -> imageService.getImagePreview(tenantId, imageInfo.getId())); + return downloadIfChanged(etag, descriptor.getPreviewDescriptor(), imageInfo.getFileName(), + () -> imageService.getImagePreview(tenantId, imageInfo.getId())); } @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @@ -176,24 +178,22 @@ public class ImageController extends BaseController { tbImageService.delete(imageInfo, getCurrentUser()); } - private ResponseEntity downloadIfChanged(String etag, TbResourceInfo imageInfo, ImageDescriptor imageDescriptor, - Supplier dataSupplier) { - if (etag != null) { - if (etag.equals(imageInfo.getEtag())) { - return ResponseEntity.status(HttpStatus.NOT_MODIFIED) - .eTag(etag) - .build(); - } + private ResponseEntity downloadIfChanged(String actualEtag, ImageDescriptor imageDescriptor, + String fileName, Supplier dataSupplier) { + if (imageDescriptor.getEtag().equals(actualEtag)) { + return ResponseEntity.status(HttpStatus.NOT_MODIFIED) + .eTag(actualEtag) + .build(); } byte[] data = dataSupplier.get(); return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + imageInfo.getFileName()) - .header("x-filename", imageInfo.getFileName()) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName) + .header("x-filename", fileName) .contentLength(data.length) .header("Content-Type", imageDescriptor.getMediaType()) .cacheControl(CacheControl.noCache()) - .eTag(imageInfo.getEtag()) + .eTag(imageDescriptor.getEtag()) .body(new ByteArrayResource(data)); } diff --git a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java index 188dff20d5..65304c8270 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java @@ -39,7 +39,6 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb public TbResourceInfo save(TbResource image, User user) throws Exception { ActionType actionType = image.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = image.getTenantId(); - image.setResourceKey(image.getFileName()); // TODO: generate unique resource key file_name+idx try { TbResourceInfo savedImage = imageService.saveImage(image); notificationEntityService.logEntityAction(tenantId, savedImage.getId(), savedImage, actionType, user); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ImageDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/ImageDescriptor.java index 2bd7c59a85..367531565f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ImageDescriptor.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ImageDescriptor.java @@ -25,5 +25,6 @@ public class ImageDescriptor { private int width; private int height; private long size; + private String etag; private ImageDescriptor previewDescriptor; } diff --git a/common/util/src/main/java/org/thingsboard/common/util/RegexUtils.java b/common/util/src/main/java/org/thingsboard/common/util/RegexUtils.java index 87d1e11bab..bbeb86efca 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/RegexUtils.java +++ b/common/util/src/main/java/org/thingsboard/common/util/RegexUtils.java @@ -19,6 +19,7 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; import java.util.function.UnaryOperator; +import java.util.regex.Matcher; import java.util.regex.Pattern; @NoArgsConstructor(access = AccessLevel.PRIVATE) @@ -37,4 +38,14 @@ public class RegexUtils { return pattern.matcher(input).matches(); } + public static String getMatch(String input, Pattern pattern, int group) { + Matcher matcher = pattern.matcher(input); + if (matcher.find()) { + try { + return matcher.group(group); + } catch (Exception ignored) {} + } + return null; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java index c046ca1c33..778192b49c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java @@ -16,9 +16,12 @@ package org.thingsboard.server.dao.resource; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.RegexUtils; import org.thingsboard.server.common.data.ImageDescriptor; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; @@ -32,7 +35,9 @@ import org.thingsboard.server.dao.service.validator.ResourceDataValidator; import org.thingsboard.server.dao.util.ImageUtils; import org.thingsboard.server.dao.util.ImageUtils.ProcessedImage; +import java.util.Objects; import java.util.Set; +import java.util.regex.Pattern; @Service @Slf4j @@ -42,17 +47,22 @@ public class BaseImageService extends BaseResourceService implements ImageServic super(resourceDao, resourceInfoDao, resourceValidator); } + @Transactional @Override public TbResourceInfo saveImage(TbResource image) throws Exception { + if (image.getId() == null) { + image.setResourceKey(getUniqueKey(image.getTenantId(), image.getFileName())); + } resourceValidator.validate(image, TbResourceInfo::getTenantId); ImageDescriptor descriptor = image.getDescriptor(ImageDescriptor.class); Pair result = processImage(image.getData(), descriptor); - image.setDescriptor(JacksonUtil.valueToTree(result.getLeft())); + descriptor = result.getLeft(); + image.setEtag(descriptor.getEtag()); + image.setDescriptor(JacksonUtil.valueToTree(descriptor)); image.setPreview(result.getRight()); - image = saveResource(image, false); - return new TbResourceInfo(image); + return new TbResourceInfo(doSaveResource(image)); } private Pair processImage(byte[] data, ImageDescriptor descriptor) throws Exception { @@ -62,17 +72,42 @@ public class BaseImageService extends BaseResourceService implements ImageServic descriptor.setWidth(image.getWidth()); descriptor.setHeight(image.getHeight()); descriptor.setSize(image.getSize()); + descriptor.setEtag(calculateEtag(data)); ImageDescriptor previewDescriptor = new ImageDescriptor(); previewDescriptor.setWidth(preview.getWidth()); previewDescriptor.setHeight(preview.getHeight()); previewDescriptor.setMediaType(preview.getMediaType()); previewDescriptor.setSize(preview.getSize()); + previewDescriptor.setEtag(preview.getData() != null ? calculateEtag(preview.getData()) : descriptor.getEtag()); descriptor.setPreviewDescriptor(previewDescriptor); return Pair.of(descriptor, preview.getData()); } + private String getUniqueKey(TenantId tenantId, String filename) { + if (!resourceInfoDao.existsByTenantIdAndResourceTypeAndResourceKey(tenantId, ResourceType.IMAGE, filename)) { + return filename; + } + + String basename = StringUtils.substringBeforeLast(filename, "."); + String extension = StringUtils.substringAfterLast(filename, "."); + + Pattern similarImagesPattern = Pattern.compile( + Pattern.quote(basename) + "_(\\d+)\\.?" + Pattern.quote(extension) + ); + int maxImageIdx = resourceInfoDao.findKeysByTenantIdAndResourceTypeAndResourceKeyStartingWith( + tenantId, ResourceType.IMAGE, basename + "_").stream() + .map(key -> RegexUtils.getMatch(key, similarImagesPattern, 1)) + .filter(Objects::nonNull).mapToInt(Integer::parseInt) + .max().orElse(0); + String uniqueKey = basename + "_" + (maxImageIdx + 1); + if (!extension.isEmpty()) { + uniqueKey += "." + extension; + } + return uniqueKey; + } + @Override public TbResourceInfo saveImageInfo(TbResourceInfo imageInfo) { return saveResource(new TbResource(imageInfo)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index bb46af22ff..bb881e5dcd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -67,11 +67,19 @@ public class BaseResourceService extends AbstractCachedEntityService { TbResourceInfo findByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey); + boolean existsByTenantIdAndResourceTypeAndResourceKey(TenantId tenantId, ResourceType resourceType, String resourceKey); + + List findKeysByTenantIdAndResourceTypeAndResourceKeyStartingWith(TenantId tenantId, ResourceType resourceType, String resourceKeyQuery); + List findByTenantIdAndEtagAndKeyStartingWith(TenantId tenantId, String etag, String query); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java index affde231f5..37e3f3cb84 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java @@ -15,10 +15,10 @@ */ package org.thingsboard.server.dao.service.validator; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -95,6 +95,9 @@ public class ResourceDataValidator extends DataValidator { if (StringUtils.isEmpty(resource.getFileName())) { throw new DataValidationException("Resource file name should be specified!"); } + if (StringUtils.containsAny(resource.getFileName(), "/", "\\")) { + throw new DataValidationException("File name contains forbidden symbols"); + } if (StringUtils.isEmpty(resource.getResourceKey())) { throw new DataValidationException("Resource key should be specified!"); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java index 62d02233e0..fc7a29af11 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java @@ -82,6 +82,16 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao findKeysByTenantIdAndResourceTypeAndResourceKeyStartingWith(TenantId tenantId, ResourceType resourceType, String resourceKeyQuery) { + return resourceInfoRepository.findKeysByTenantIdAndResourceTypeAndResourceKeyStartingWith(tenantId.getId(), resourceType.name(), resourceKeyQuery); + } + @Override public List findByTenantIdAndEtagAndKeyStartingWith(TenantId tenantId, String etag, String query) { return DaoUtil.convertDataList(resourceInfoRepository.findByTenantIdAndHashCodeAndResourceKeyStartingWith(tenantId.getId(), etag, query)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java index e7881c7c74..9c526ae321 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java @@ -22,7 +22,6 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.TbResourceInfoEntity; -import java.util.Collection; import java.util.List; import java.util.UUID; @@ -55,6 +54,14 @@ public interface TbResourceInfoRepository extends JpaRepository findKeysByTenantIdAndResourceTypeAndResourceKeyStartingWith(@Param("tenantId") UUID tenantId, + @Param("resourceType") String resourceType, + @Param("resourceKeyStartsWith") String resourceKeyStartsWith); + List findByTenantIdAndHashCodeAndResourceKeyStartingWith(UUID tenantId, String hashCode, String query); }