Merge branch 'feature/image-resources' of github.com:thingsboard/thingsboard into feature/image-resources

This commit is contained in:
Igor Kulikov 2023-11-17 19:10:15 +02:00
commit 52d2376931
5 changed files with 87 additions and 21 deletions

View File

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -37,6 +38,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.thingsboard.server.common.data.ImageDescriptor; import org.thingsboard.server.common.data.ImageDescriptor;
import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -46,11 +48,13 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.resource.ImageCacheKey;
import org.thingsboard.server.service.resource.TbImageService; import org.thingsboard.server.service.resource.TbImageService;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
import java.time.Duration;
import java.util.function.Supplier; import java.util.function.Supplier;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
@ -122,11 +126,7 @@ public class ImageController extends BaseController {
public ResponseEntity<ByteArrayResource> downloadImage(@PathVariable String type, public ResponseEntity<ByteArrayResource> downloadImage(@PathVariable String type,
@PathVariable String key, @PathVariable String key,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception { @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception {
TenantId tenantId = getTenantId(); return downloadIfChanged(type, key, etag, false);
TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ);
ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class);
return downloadIfChanged(etag, descriptor, imageInfo.getFileName(),
() -> imageService.getImageData(tenantId, imageInfo.getId()));
} }
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@ -134,11 +134,7 @@ public class ImageController extends BaseController {
public ResponseEntity<ByteArrayResource> downloadImagePreview(@PathVariable String type, public ResponseEntity<ByteArrayResource> downloadImagePreview(@PathVariable String type,
@PathVariable String key, @PathVariable String key,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception { @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception {
TenantId tenantId = getTenantId(); return downloadIfChanged(type, key, etag, true);
TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ);
ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class);
return downloadIfChanged(etag, descriptor.getPreviewDescriptor(), imageInfo.getFileName(),
() -> imageService.getImagePreview(tenantId, imageInfo.getId()));
} }
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@ -178,22 +174,33 @@ public class ImageController extends BaseController {
tbImageService.delete(imageInfo, getCurrentUser()); tbImageService.delete(imageInfo, getCurrentUser());
} }
private ResponseEntity<ByteArrayResource> downloadIfChanged(String actualEtag, ImageDescriptor imageDescriptor, private ResponseEntity<ByteArrayResource> downloadIfChanged(String type, String key, String etag, boolean preview) throws ThingsboardException, JsonProcessingException {
String fileName, Supplier<byte[]> dataSupplier) { ImageCacheKey cacheKey = new ImageCacheKey(getTenantId(type), key, preview);
if (imageDescriptor.getEtag().equals(actualEtag)) { if (StringUtils.isNotEmpty(etag)) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED) etag = etag.replaceAll("\"", ""); // TODO: investigate why Spring provides extra quotes.
.eTag(actualEtag) if (etag.equals(tbImageService.getETag(cacheKey))) {
.build(); return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
} }
}
byte[] data = dataSupplier.get(); TenantId tenantId = getTenantId();
TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ);
String fileName = imageInfo.getFileName();
ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class);
byte[] data;
if (preview) {
descriptor = descriptor.getPreviewDescriptor();
data = imageService.getImagePreview(tenantId, imageInfo.getId());
} else {
data = imageService.getImageData(tenantId, imageInfo.getId());
}
tbImageService.putETag(cacheKey, descriptor.getEtag());
return ResponseEntity.ok() return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName)
.header("x-filename", fileName) .header("x-filename", fileName)
.contentLength(data.length) .contentLength(data.length)
.header("Content-Type", imageDescriptor.getMediaType()) .header("Content-Type", descriptor.getMediaType())
.cacheControl(CacheControl.noCache()) .cacheControl(CacheControl.noCache())
.eTag(imageDescriptor.getEtag()) .eTag(descriptor.getEtag())
.body(new ByteArrayResource(data)); .body(new ByteArrayResource(data));
} }

View File

@ -15,7 +15,10 @@
*/ */
package org.thingsboard.server.service.resource; package org.thingsboard.server.service.resource;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
@ -28,12 +31,34 @@ import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import java.util.concurrent.TimeUnit;
@Service @Service
@TbCoreComponent @TbCoreComponent
@RequiredArgsConstructor
public class DefaultTbImageService extends AbstractTbEntityService implements TbImageService { public class DefaultTbImageService extends AbstractTbEntityService implements TbImageService {
private final ImageService imageService; private final ImageService imageService;
private final Cache<ImageCacheKey, String> cache;
public DefaultTbImageService(ImageService imageService,
@Value("${cache.imageETags.timeToLiveInMinutes:120}") int cacheTtl,
@Value("${cache.imageETags.maxSize:200000}") int cacheMaxSize) {
this.imageService = imageService;
this.cache = Caffeine.newBuilder()
.expireAfterAccess(cacheTtl, TimeUnit.MINUTES)
.maximumSize(cacheMaxSize)
.build();
}
@Override
public String getETag(ImageCacheKey imageCacheKey) {
return cache.getIfPresent(imageCacheKey);
}
@Override
public void putETag(ImageCacheKey imageCacheKey, String etag) {
cache.put(imageCacheKey, etag);
}
@Override @Override
public TbResourceInfo save(TbResource image, User user) throws Exception { public TbResourceInfo save(TbResource image, User user) throws Exception {

View File

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2023 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.resource;
import lombok.Data;
import org.thingsboard.server.common.data.id.TenantId;
@Data
public class ImageCacheKey {
private final TenantId tenantId;
private final String key;
private final boolean preview;
}

View File

@ -27,4 +27,7 @@ public interface TbImageService {
void delete(TbResourceInfo imageInfo, User user); void delete(TbResourceInfo imageInfo, User user);
String getETag(ImageCacheKey imageCacheKey);
void putETag(ImageCacheKey imageCacheKey, String etag);
} }

View File

@ -586,6 +586,9 @@ cache:
entityLimits: entityLimits:
timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_LIMITS_TTL:5}" # Entity limits cache TTL timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_LIMITS_TTL:5}" # Entity limits cache TTL
maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled
imageETags:
timeToLiveInMinutes: "${CACHE_SPECS_IMAGE_ETAGS_TTL:44640}" # Image ETags cache TTL
maxSize: "${CACHE_SPECS_IMAGE_ETAGS_MAX_SIZE:1000000}" # 0 means the cache is disabled
# Spring data parameters # Spring data parameters
spring.data.redis.repositories.enabled: false # Disable this because it is not required. spring.data.redis.repositories.enabled: false # Disable this because it is not required.