Unique image resource key based on index

This commit is contained in:
ViacheslavKlimov 2023-11-15 16:10:18 +02:00
parent 2b9eab0150
commit e7aabe80a4
10 changed files with 102 additions and 29 deletions

View File

@ -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<ByteArrayResource> downloadIfChanged(String etag, TbResourceInfo imageInfo, ImageDescriptor imageDescriptor,
Supplier<byte[]> dataSupplier) {
if (etag != null) {
if (etag.equals(imageInfo.getEtag())) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
.eTag(etag)
.build();
}
private ResponseEntity<ByteArrayResource> downloadIfChanged(String actualEtag, ImageDescriptor imageDescriptor,
String fileName, Supplier<byte[]> 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));
}

View File

@ -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);

View File

@ -25,5 +25,6 @@ public class ImageDescriptor {
private int width;
private int height;
private long size;
private String etag;
private ImageDescriptor previewDescriptor;
}

View File

@ -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;
}
}

View File

@ -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<ImageDescriptor, byte[]> 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<ImageDescriptor, byte[]> 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));

View File

@ -67,11 +67,19 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
if (doValidate) {
resourceValidator.validate(resource, TbResourceInfo::getTenantId);
}
TenantId tenantId = resource.getTenantId();
TbResourceId resourceId = resource.getId();
if (resource.getData() != null) {
resource.setEtag(calculateEtag(resource.getData()));
}
return doSaveResource(resource);
}
@Override
public TbResource saveResource(TbResource resource) {
return saveResource(resource, true);
}
protected TbResource doSaveResource(TbResource resource) {
TenantId tenantId = resource.getTenantId();
try {
TbResource saved;
if (resource.getData() != null) {
@ -80,12 +88,12 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
TbResourceInfo resourceInfo = saveResourceInfo(resource);
saved = new TbResource(resourceInfo);
}
publishEvictEvent(new ResourceInfoEvictEvent(tenantId, resourceId));
publishEvictEvent(new ResourceInfoEvictEvent(tenantId, resource.getId()));
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(saved.getTenantId())
.entityId(saved.getId()).added(resourceId == null).build());
.entityId(saved.getId()).added(resource.getId() == null).build());
return saved;
} catch (Exception t) {
publishEvictEvent(new ResourceInfoEvictEvent(tenantId, resourceId));
publishEvictEvent(new ResourceInfoEvictEvent(tenantId, resource.getId()));
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("resource_unq_key")) {
String field = ResourceType.LWM2M_MODEL.equals(resource.getResourceType()) ? "resourceKey" : "fileName";
@ -96,11 +104,6 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
}
}
@Override
public TbResource saveResource(TbResource resource) {
return saveResource(resource, true);
}
private TbResourceInfo saveResourceInfo(TbResource resource) {
return resourceInfoDao.save(resource.getTenantId(), new TbResourceInfo(resource));
}

View File

@ -33,6 +33,10 @@ public interface TbResourceInfoDao extends Dao<TbResourceInfo> {
TbResourceInfo findByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey);
boolean existsByTenantIdAndResourceTypeAndResourceKey(TenantId tenantId, ResourceType resourceType, String resourceKey);
List<String> findKeysByTenantIdAndResourceTypeAndResourceKeyStartingWith(TenantId tenantId, ResourceType resourceType, String resourceKeyQuery);
List<TbResourceInfo> findByTenantIdAndEtagAndKeyStartingWith(TenantId tenantId, String etag, String query);
}

View File

@ -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<TbResource> {
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!");
}

View File

@ -82,6 +82,16 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao<TbResourceInfoEntity, T
return DaoUtil.getData(resourceInfoRepository.findByTenantIdAndResourceTypeAndResourceKey(tenantId.getId(), resourceType.name(), resourceKey));
}
@Override
public boolean existsByTenantIdAndResourceTypeAndResourceKey(TenantId tenantId, ResourceType resourceType, String resourceKey) {
return resourceInfoRepository.existsByTenantIdAndResourceTypeAndResourceKey(tenantId.getId(), resourceType.name(), resourceKey);
}
@Override
public List<String> findKeysByTenantIdAndResourceTypeAndResourceKeyStartingWith(TenantId tenantId, ResourceType resourceType, String resourceKeyQuery) {
return resourceInfoRepository.findKeysByTenantIdAndResourceTypeAndResourceKeyStartingWith(tenantId.getId(), resourceType.name(), resourceKeyQuery);
}
@Override
public List<TbResourceInfo> findByTenantIdAndEtagAndKeyStartingWith(TenantId tenantId, String etag, String query) {
return DaoUtil.convertDataList(resourceInfoRepository.findByTenantIdAndHashCodeAndResourceKeyStartingWith(tenantId.getId(), etag, query));

View File

@ -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<TbResourceInfoEn
TbResourceInfoEntity findByTenantIdAndResourceTypeAndResourceKey(UUID tenantId, String resourceType, String resourceKey);
boolean existsByTenantIdAndResourceTypeAndResourceKey(UUID tenantId, String resourceType, String resourceKey);
@Query(value = "SELECT r.resource_key FROM resource r WHERE r.tenant_id = :tenantId AND r.resource_type = :resourceType " +
"AND starts_with(r.resource_key, :resourceKeyStartsWith)", nativeQuery = true)
List<String> findKeysByTenantIdAndResourceTypeAndResourceKeyStartingWith(@Param("tenantId") UUID tenantId,
@Param("resourceType") String resourceType,
@Param("resourceKeyStartsWith") String resourceKeyStartsWith);
List<TbResourceInfoEntity> findByTenantIdAndHashCodeAndResourceKeyStartingWith(UUID tenantId, String hashCode, String query);
}