Image descriptor, image preview, refactoring
This commit is contained in:
parent
d70ce27253
commit
19968b5c10
@ -27,6 +27,8 @@ $$
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
ALTER TABLE resource ADD COLUMN IF NOT EXISTS media_type varchar(255);
|
||||
ALTER TABLE resource
|
||||
ADD COLUMN IF NOT EXISTS descriptor varchar,
|
||||
ADD COLUMN IF NOT EXISTS preview bytea;
|
||||
|
||||
-- RESOURCES UPDATE END
|
||||
|
||||
@ -32,8 +32,13 @@ import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.server.common.data.ImageDescriptor;
|
||||
import org.thingsboard.server.common.data.ResourceType;
|
||||
import org.thingsboard.server.common.data.TbResource;
|
||||
import org.thingsboard.server.common.data.TbResourceInfo;
|
||||
import org.thingsboard.server.common.data.exception.ThingsboardException;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
@ -43,7 +48,9 @@ import org.thingsboard.server.common.data.security.Authority;
|
||||
import org.thingsboard.server.dao.resource.ImageService;
|
||||
import org.thingsboard.server.queue.util.TbCoreComponent;
|
||||
import org.thingsboard.server.service.resource.TbImageService;
|
||||
import org.thingsboard.server.service.security.model.SecurityUser;
|
||||
import org.thingsboard.server.service.security.permission.Operation;
|
||||
import org.thingsboard.server.service.security.permission.Resource;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@ -68,52 +75,75 @@ public class ImageController extends BaseController {
|
||||
private static final String SYSTEM_IMAGE = "system";
|
||||
private static final String TENANT_IMAGE = "tenant";
|
||||
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
|
||||
@PostMapping("/api/image")
|
||||
public TbResourceInfo uploadImage(MultipartFile file) {
|
||||
// imageService.saveImage()
|
||||
return null;
|
||||
public TbResourceInfo uploadImage(@RequestPart MultipartFile file) throws Exception {
|
||||
SecurityUser user = getCurrentUser();
|
||||
TbResource image = new TbResource();
|
||||
image.setTenantId(user.getTenantId());
|
||||
accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.CREATE, null, image);
|
||||
|
||||
image.setFileName(file.getOriginalFilename());
|
||||
image.setTitle(file.getOriginalFilename());
|
||||
image.setResourceType(ResourceType.IMAGE);
|
||||
ImageDescriptor descriptor = new ImageDescriptor();
|
||||
descriptor.setMediaType(file.getContentType());
|
||||
image.setDescriptor(JacksonUtil.valueToTree(descriptor));
|
||||
image.setData(file.getBytes());
|
||||
return tbImageService.save(image, user);
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
|
||||
@PutMapping(IMAGE_URL)
|
||||
public TbResourceInfo updateImage(MultipartFile file) {
|
||||
return null;
|
||||
public TbResourceInfo updateImage(@PathVariable String type,
|
||||
@PathVariable String key,
|
||||
@RequestPart MultipartFile file) throws Exception {
|
||||
TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.WRITE);
|
||||
ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class);
|
||||
|
||||
TbResource image = new TbResource(imageInfo);
|
||||
image.setData(file.getBytes());
|
||||
descriptor.setMediaType(file.getContentType());
|
||||
return tbImageService.save(image, getCurrentUser());
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
|
||||
@PutMapping(IMAGE_URL + "/info")
|
||||
public TbResourceInfo updateImageInfo(@RequestBody TbResourceInfo imageInfo) {
|
||||
return null;
|
||||
public TbResourceInfo updateImageInfo(@PathVariable String type,
|
||||
@PathVariable String key,
|
||||
@RequestBody TbResourceInfo newImageInfo) throws ThingsboardException {
|
||||
TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.WRITE);
|
||||
imageInfo.setTitle(newImageInfo.getTitle());
|
||||
return tbImageService.save(imageInfo, getCurrentUser());
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||
@GetMapping(value = IMAGE_URL, produces = "image/*")
|
||||
public ResponseEntity<ByteArrayResource> downloadImage(@PathVariable String type,
|
||||
@PathVariable String key,
|
||||
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
|
||||
TenantId tenantId = getTenantId(type);
|
||||
TbResourceInfo imageInfo = imageService.getImageInfoByTenantIdAndKey(tenantId, key);
|
||||
return downloadIfChanged(etag, imageInfo, () -> imageService.getImageData(tenantId, imageInfo.getId()), imageInfo.getMediaType());
|
||||
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception {
|
||||
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()));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||
@GetMapping(value = IMAGE_URL + "/preview", produces = "image/png")
|
||||
public ResponseEntity<ByteArrayResource> downloadImagePreview(@PathVariable String type,
|
||||
@PathVariable String key,
|
||||
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
|
||||
TenantId tenantId = getTenantId(type);
|
||||
TbResourceInfo imageInfo = imageService.getImageInfoByTenantIdAndKey(tenantId, key);
|
||||
return downloadIfChanged(etag, imageInfo, () -> imageService.getImagePreview(tenantId, imageInfo.getId()), imageInfo.getMediaType());
|
||||
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception {
|
||||
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()));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||
@GetMapping(IMAGE_URL + "/info")
|
||||
public TbResourceInfo getImageInfo(@PathVariable String type,
|
||||
@PathVariable String key) throws ThingsboardException {
|
||||
TenantId tenantId = getTenantId(type);
|
||||
TbResourceInfo imageInfo = imageService.getImageInfoByTenantIdAndKey(tenantId, key);
|
||||
return checkEntity(getCurrentUser(), imageInfo, Operation.READ);
|
||||
return checkImageInfo(type, key, Operation.READ);
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
|
||||
@ -128,6 +158,7 @@ public class ImageController extends BaseController {
|
||||
@RequestParam(required = false) String sortProperty,
|
||||
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
|
||||
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
|
||||
// PE: generic permission
|
||||
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
|
||||
TenantId tenantId = getTenantId();
|
||||
if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN) {
|
||||
@ -141,17 +172,14 @@ public class ImageController extends BaseController {
|
||||
@DeleteMapping(IMAGE_URL)
|
||||
public void deleteImage(@PathVariable String type,
|
||||
@PathVariable String key) throws ThingsboardException {
|
||||
TenantId tenantId = getTenantId(type);
|
||||
TbResourceInfo imageInfo = imageService.getImageInfoByTenantIdAndKey(tenantId, key);
|
||||
checkEntity(getCurrentUser(), imageInfo, Operation.DELETE);
|
||||
TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.DELETE);
|
||||
tbImageService.delete(imageInfo, getCurrentUser());
|
||||
}
|
||||
|
||||
private ResponseEntity<ByteArrayResource> downloadIfChanged(String etag, TbResourceInfo resourceInfo,
|
||||
Supplier<byte[]> dataSupplier, String mediaType) throws ThingsboardException {
|
||||
checkEntity(getCurrentUser(), resourceInfo, Operation.READ);
|
||||
private ResponseEntity<ByteArrayResource> downloadIfChanged(String etag, TbResourceInfo imageInfo, ImageDescriptor imageDescriptor,
|
||||
Supplier<byte[]> dataSupplier) {
|
||||
if (etag != null) {
|
||||
if (etag.equals(resourceInfo.getEtag())) {
|
||||
if (etag.equals(imageInfo.getEtag())) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
|
||||
.eTag(etag)
|
||||
.build();
|
||||
@ -160,15 +188,22 @@ public class ImageController extends BaseController {
|
||||
|
||||
byte[] data = dataSupplier.get();
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + resourceInfo.getFileName())
|
||||
.header("x-filename", resourceInfo.getFileName())
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + imageInfo.getFileName())
|
||||
.header("x-filename", imageInfo.getFileName())
|
||||
.contentLength(data.length)
|
||||
.header("Content-Type", mediaType)
|
||||
.header("Content-Type", imageDescriptor.getMediaType())
|
||||
.cacheControl(CacheControl.noCache())
|
||||
.eTag(resourceInfo.getEtag())
|
||||
.eTag(imageInfo.getEtag())
|
||||
.body(new ByteArrayResource(data));
|
||||
}
|
||||
|
||||
private TbResourceInfo checkImageInfo(String imageType, String key, Operation operation) throws ThingsboardException {
|
||||
TenantId tenantId = getTenantId(imageType);
|
||||
TbResourceInfo imageInfo = imageService.getImageInfoByTenantIdAndKey(tenantId, key);
|
||||
checkEntity(getCurrentUser(), checkNotNull(imageInfo), operation);
|
||||
return imageInfo;
|
||||
}
|
||||
|
||||
private TenantId getTenantId(String imageType) throws ThingsboardException {
|
||||
TenantId tenantId;
|
||||
if (imageType.equals(TENANT_IMAGE)) {
|
||||
|
||||
@ -54,11 +54,10 @@ import org.thingsboard.server.service.security.permission.Operation;
|
||||
import org.thingsboard.server.service.security.permission.Resource;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
|
||||
import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION;
|
||||
@ -110,17 +109,6 @@ public class TbResourceController extends BaseController {
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
@ApiOperation(value = "Download Image (downloadImageIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
|
||||
@GetMapping(value = "/images/{resourceKey}", produces = "image/*")
|
||||
public ResponseEntity<ByteArrayResource> downloadImageIfChanged(@PathVariable("resourceKey") String resourceKey,
|
||||
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
|
||||
TenantId tenantId = getTenantId();
|
||||
return downloadResourceIfChanged(ResourceType.IMAGE, etag,
|
||||
() -> resourceService.findResourceInfoByTenantIdAndKey(tenantId, ResourceType.IMAGE, resourceKey),
|
||||
() -> resourceService.findResourceByTenantIdAndKey(tenantId, ResourceType.IMAGE, resourceKey));
|
||||
}
|
||||
|
||||
@ApiOperation(value = "Download LWM2M Resource (downloadLwm2mResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
|
||||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
|
||||
@GetMapping(value = "/resource/lwm2m/{resourceId}/download", produces = "application/xml")
|
||||
@ -291,19 +279,9 @@ public class TbResourceController extends BaseController {
|
||||
|
||||
private ResponseEntity<ByteArrayResource> downloadResourceIfChanged(ResourceType resourceType, String strResourceId, String etag) throws ThingsboardException {
|
||||
checkParameter(RESOURCE_ID, strResourceId);
|
||||
TenantId tenantId = getTenantId();
|
||||
TbResourceId resourceId = new TbResourceId(toUUID(strResourceId));
|
||||
return downloadResourceIfChanged(resourceType, etag,
|
||||
() -> resourceService.findResourceInfoById(tenantId, resourceId),
|
||||
() -> resourceService.findResourceById(tenantId, resourceId));
|
||||
}
|
||||
|
||||
private ResponseEntity<ByteArrayResource> downloadResourceIfChanged(ResourceType resourceType, String etag,
|
||||
Supplier<TbResourceInfo> resourceInfoSupplier,
|
||||
Supplier<TbResource> resourceSupplier) throws ThingsboardException {
|
||||
if (etag != null) {
|
||||
TbResourceInfo tbResourceInfo = resourceInfoSupplier.get();
|
||||
checkEntity(getCurrentUser(), tbResourceInfo, Operation.READ);
|
||||
TbResourceInfo tbResourceInfo = checkResourceInfoId(resourceId, Operation.READ);
|
||||
if (etag.equals(tbResourceInfo.getEtag())) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
|
||||
.eTag(tbResourceInfo.getEtag())
|
||||
@ -311,16 +289,14 @@ public class TbResourceController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: rate limits
|
||||
TbResource tbResource = resourceSupplier.get();
|
||||
checkEntity(getCurrentUser(), tbResource, Operation.READ);
|
||||
TbResource tbResource = checkResourceId(resourceId, Operation.READ);
|
||||
|
||||
ByteArrayResource resource = new ByteArrayResource(tbResource.getData());
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + tbResource.getFileName())
|
||||
.header("x-filename", tbResource.getFileName())
|
||||
.contentLength(resource.contentLength())
|
||||
.header("Content-Type", tbResource.getMediaType() != null ? tbResource.getMediaType() : resourceType.getDefaultMediaType())
|
||||
.header("Content-Type", resourceType.getMediaType())
|
||||
.cacheControl(CacheControl.noCache())
|
||||
.eTag(tbResource.getEtag())
|
||||
.body(resource);
|
||||
|
||||
@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.ResourceType;
|
||||
import org.thingsboard.server.common.data.TbResource;
|
||||
import org.thingsboard.server.common.data.TbResourceInfo;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.util.ImageUtils;
|
||||
import org.thingsboard.server.dao.util.ImageUtils;
|
||||
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
|
||||
import org.thingsboard.server.common.data.widget.WidgetsBundle;
|
||||
import org.thingsboard.server.dao.resource.ImageService;
|
||||
@ -38,7 +38,6 @@ import org.thingsboard.server.dao.resource.ImageService;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@ -199,41 +198,44 @@ public class ImagesUpdater {
|
||||
return new ImageSaveResult(imageLink, !imageLink.equals(data));
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private String saveImage(TenantId tenantId, String name, String key, byte[] imageData, String mediaType,
|
||||
String existingImageQuery) {
|
||||
TbResourceInfo resourceInfo = imageService.getImageInfoByTenantIdAndKey(tenantId, key);
|
||||
if (resourceInfo == null && !tenantId.isSysTenantId() && existingImageQuery != null) {
|
||||
TbResourceInfo imageInfo = imageService.getImageInfoByTenantIdAndKey(tenantId, key);
|
||||
if (imageInfo == null && !tenantId.isSysTenantId() && existingImageQuery != null) {
|
||||
// TODO: need to search among tenant images too (custom widgets)
|
||||
// List<TbResourceInfo> existingSystemImages = imageService.findByTenantIdAndDataAndKeyStartingWith(TenantId.SYS_TENANT_ID, imageData, existingImageQuery);
|
||||
// if (!existingSystemImages.isEmpty()) {
|
||||
// resourceInfo = existingSystemImages.get(0);
|
||||
// imageInfo = existingSystemImages.get(0);
|
||||
// if (existingSystemImages.size() > 1) {
|
||||
// log.warn("Found more than one system image resources for key {}", existingImageQuery);
|
||||
// }
|
||||
// String link = imageService.getImageLink(resourceInfo);
|
||||
// String link = imageService.getImageLink(imageInfo);
|
||||
// log.info("Using system image {} for {}", link, key);
|
||||
// return link;
|
||||
// }
|
||||
}
|
||||
TbResource resource;
|
||||
if (resourceInfo == null) {
|
||||
resource = new TbResource();
|
||||
resource.setTenantId(tenantId);
|
||||
resource.setResourceType(ResourceType.IMAGE);
|
||||
resource.setResourceKey(key);
|
||||
TbResource image;
|
||||
if (imageInfo == null) {
|
||||
image = new TbResource();
|
||||
image.setTenantId(tenantId);
|
||||
image.setResourceType(ResourceType.IMAGE);
|
||||
image.setResourceKey(key);
|
||||
} else if (tenantId.isSysTenantId()) {
|
||||
resource = new TbResource(resourceInfo);
|
||||
image = new TbResource(imageInfo);
|
||||
} else {
|
||||
return imageService.getImageLink(resourceInfo);
|
||||
return imageService.getImageLink(imageInfo);
|
||||
}
|
||||
resource.setTitle(name);
|
||||
resource.setFileName(key);
|
||||
resource.setMediaType(mediaType);
|
||||
resource.setData(imageData);
|
||||
resource = imageService.saveImage(resource);
|
||||
log.info("[{}] {} image '{}' ({})", tenantId, resourceInfo == null ? "Created" : "Updated",
|
||||
resource.getTitle(), resource.getResourceKey());
|
||||
return imageService.getImageLink(resourceInfo);
|
||||
image.setTitle(name);
|
||||
image.setFileName(key);
|
||||
image.setDescriptor(JacksonUtil.newObjectNode()
|
||||
.put("mediaType", mediaType));
|
||||
image.setData(imageData);
|
||||
|
||||
TbResourceInfo savedImage = imageService.saveImage(image);
|
||||
log.info("[{}] {} image '{}' ({})", tenantId, imageInfo == null ? "Created" : "Updated",
|
||||
image.getTitle(), image.getResourceKey());
|
||||
return imageService.getImageLink(savedImage);
|
||||
}
|
||||
|
||||
private String getText(JsonNode jsonNode, String field) {
|
||||
|
||||
@ -24,7 +24,7 @@ import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.server.common.data.util.MediaTypeUtils;
|
||||
import org.thingsboard.server.dao.util.ImageUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@ -17,29 +17,65 @@ package org.thingsboard.server.service.resource;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.thingsboard.server.common.data.EntityType;
|
||||
import org.thingsboard.server.common.data.TbResource;
|
||||
import org.thingsboard.server.common.data.TbResourceInfo;
|
||||
import org.thingsboard.server.common.data.User;
|
||||
import org.thingsboard.server.common.data.audit.ActionType;
|
||||
import org.thingsboard.server.common.data.id.TbResourceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.dao.resource.ImageService;
|
||||
import org.thingsboard.server.queue.util.TbCoreComponent;
|
||||
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
|
||||
|
||||
@Service
|
||||
@TbCoreComponent
|
||||
@RequiredArgsConstructor
|
||||
public class DefaultTbImageService implements TbImageService {
|
||||
public class DefaultTbImageService extends AbstractTbEntityService implements TbImageService {
|
||||
|
||||
private final ImageService imageService;
|
||||
|
||||
@Override
|
||||
public TbResource save(TbResourceInfo imageInfo, MultipartFile imageFile, User user) {
|
||||
return null;
|
||||
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);
|
||||
return savedImage;
|
||||
} catch (Exception e) {
|
||||
image.setData(null);
|
||||
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE), image, actionType, user, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TbResourceInfo save(TbResourceInfo imageInfo, User user) {
|
||||
TenantId tenantId = imageInfo.getTenantId();
|
||||
TbResourceId imageId = imageInfo.getId();
|
||||
try {
|
||||
imageInfo = imageService.saveImageInfo(imageInfo);
|
||||
notificationEntityService.logEntityAction(tenantId, imageId, imageInfo, ActionType.UPDATED, user);
|
||||
return imageInfo;
|
||||
} catch (Exception e) {
|
||||
notificationEntityService.logEntityAction(tenantId, imageId, imageInfo, ActionType.UPDATED, user, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(TbResourceInfo imageInfo, User user) {
|
||||
|
||||
TenantId tenantId = imageInfo.getTenantId();
|
||||
TbResourceId imageId = imageInfo.getId();
|
||||
try {
|
||||
imageService.deleteImage(tenantId, imageId);
|
||||
notificationEntityService.logEntityAction(tenantId, imageId, imageInfo, ActionType.DELETED, user, imageId.toString());
|
||||
} catch (Exception e) {
|
||||
notificationEntityService.logEntityAction(tenantId, imageId, ActionType.DELETED, user, e, imageId.toString());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -15,14 +15,15 @@
|
||||
*/
|
||||
package org.thingsboard.server.service.resource;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.thingsboard.server.common.data.TbResource;
|
||||
import org.thingsboard.server.common.data.TbResourceInfo;
|
||||
import org.thingsboard.server.common.data.User;
|
||||
|
||||
public interface TbImageService {
|
||||
|
||||
TbResource save(TbResourceInfo imageInfo, MultipartFile imageFile, User user);
|
||||
TbResourceInfo save(TbResource image, User user) throws Exception;
|
||||
|
||||
TbResourceInfo save(TbResourceInfo imageInfo, User user);
|
||||
|
||||
void delete(TbResourceInfo imageInfo, User user);
|
||||
|
||||
|
||||
@ -620,10 +620,9 @@ public class TbResourceControllerTest extends AbstractControllerTest {
|
||||
@Test
|
||||
public void testUpdateResourceData_nonUpdatableResourceType() throws Exception {
|
||||
TbResource resource = new TbResource();
|
||||
resource.setResourceType(ResourceType.IMAGE);
|
||||
resource.setResourceType(ResourceType.PKCS_12);
|
||||
resource.setTitle("My resource");
|
||||
resource.setFileName("image.png");
|
||||
resource.setMediaType("image/png");
|
||||
resource.setFileName("3.pks");
|
||||
resource.setBase64Data(TEST_DATA);
|
||||
TbResource savedResource = save(resource);
|
||||
resource.setEtag(savedResource.getEtag());
|
||||
@ -638,7 +637,6 @@ public class TbResourceControllerTest extends AbstractControllerTest {
|
||||
savedResource = doPost("/api/resource", savedResource, TbResource.class);
|
||||
assertThat(savedResource.getTitle()).isEqualTo("Updated resource");
|
||||
assertThat(savedResource.getFileName()).isEqualTo(resource.getFileName());
|
||||
assertThat(savedResource.getMediaType()).isEqualTo(resource.getMediaType());
|
||||
assertThat(savedResource.getEtag()).isEqualTo(resource.getEtag());
|
||||
assertThat(download(savedResource.getId())).asBase64Encoded().isEqualTo(TEST_DATA);
|
||||
}
|
||||
@ -665,54 +663,6 @@ public class TbResourceControllerTest extends AbstractControllerTest {
|
||||
assertThat(download(savedResource.getId())).asBase64Encoded().isEqualTo(newData);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveImage_systemLevel() throws Exception {
|
||||
loginSysAdmin();
|
||||
TbResource resource = new TbResource();
|
||||
resource.setResourceType(ResourceType.IMAGE);
|
||||
resource.setTitle("System image");
|
||||
resource.setFileName("image.png");
|
||||
resource.setMediaType("image/png");
|
||||
resource.setBase64Data(TEST_DATA);
|
||||
|
||||
TbResource savedResource = save(resource);
|
||||
|
||||
loginTenantAdmin();
|
||||
MockHttpServletResponse imageResponse = doGet(getImageLink(savedResource)).andExpect(status().isOk())
|
||||
.andReturn().getResponse();
|
||||
assertThat(imageResponse.getContentAsByteArray())
|
||||
.isEqualTo(download(savedResource.getId()))
|
||||
.isEqualTo(Base64.getDecoder().decode(TEST_DATA));
|
||||
assertThat(imageResponse.getContentType()).isEqualTo("image/png");
|
||||
|
||||
loginSysAdmin();
|
||||
doDelete("/api/resource/" + savedResource.getId()).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveImage_tenantLevel() throws Exception {
|
||||
loginTenantAdmin();
|
||||
TbResource resource = new TbResource();
|
||||
resource.setResourceType(ResourceType.IMAGE);
|
||||
resource.setTitle("My image");
|
||||
resource.setFileName("image.jpg");
|
||||
resource.setMediaType("image/jpeg");
|
||||
resource.setBase64Data(TEST_DATA);
|
||||
|
||||
TbResource savedResource = save(resource);
|
||||
String imageLink = getImageLink(savedResource);
|
||||
|
||||
MockHttpServletResponse imageResponse = doGet(imageLink).andExpect(status().isOk())
|
||||
.andReturn().getResponse();
|
||||
assertThat(imageResponse.getContentAsByteArray())
|
||||
.isEqualTo(download(savedResource.getId()))
|
||||
.isEqualTo(Base64.getDecoder().decode(TEST_DATA));
|
||||
assertThat(imageResponse.getContentType()).isEqualTo("image/jpeg");
|
||||
|
||||
loginDifferentTenant();
|
||||
doGet(imageLink).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
private TbResource save(TbResource tbResource) throws Exception {
|
||||
return doPostWithTypedResponse("/api/resource", tbResource, new TypeReference<>() {
|
||||
});
|
||||
|
||||
@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.page.PageLink;
|
||||
|
||||
public interface ImageService {
|
||||
|
||||
TbResource saveImage(TbResource image);
|
||||
TbResourceInfo saveImage(TbResource image) throws Exception;
|
||||
|
||||
TbResourceInfo saveImageInfo(TbResourceInfo imageInfo);
|
||||
|
||||
@ -38,6 +38,8 @@ public interface ImageService {
|
||||
|
||||
byte[] getImagePreview(TenantId tenantId, TbResourceId imageId);
|
||||
|
||||
void deleteImage(TenantId tenantId, TbResourceId imageId);
|
||||
|
||||
String getImageLink(TbResourceInfo imageInfo);
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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.common.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ImageDescriptor {
|
||||
private String mediaType;
|
||||
private int width;
|
||||
private int height;
|
||||
private long size;
|
||||
private ImageDescriptor previewDescriptor;
|
||||
}
|
||||
@ -27,7 +27,7 @@ public enum ResourceType {
|
||||
IMAGE(null, true, false);
|
||||
|
||||
@Getter
|
||||
private final String defaultMediaType;
|
||||
private final String mediaType;
|
||||
@Getter
|
||||
private final boolean customerAccess;
|
||||
@Getter
|
||||
|
||||
@ -37,6 +37,9 @@ public class TbResource extends TbResourceInfo {
|
||||
@JsonIgnore
|
||||
private byte[] data;
|
||||
|
||||
@JsonIgnore
|
||||
private byte[] preview;
|
||||
|
||||
public TbResource() {
|
||||
super();
|
||||
}
|
||||
@ -53,6 +56,7 @@ public class TbResource extends TbResourceInfo {
|
||||
super(resource);
|
||||
this.base64Data = resource.base64Data;
|
||||
this.data = resource.data;
|
||||
this.preview = resource.preview;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
|
||||
@ -16,6 +16,9 @@
|
||||
package org.thingsboard.server.common.data;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
@ -55,9 +58,8 @@ public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, H
|
||||
@Length(fieldName = "file name")
|
||||
@ApiModelProperty(position = 9, value = "Resource file name.", example = "19.xml", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
|
||||
private String fileName;
|
||||
@ApiModelProperty(position = 10, value = "Resource media type.", example = "image/png", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
|
||||
private String mediaType;
|
||||
private ObjectNode descriptor;
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
private JsonNode descriptor;
|
||||
|
||||
public TbResourceInfo() {
|
||||
super();
|
||||
@ -76,7 +78,6 @@ public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, H
|
||||
this.searchText = resourceInfo.searchText;
|
||||
this.etag = resourceInfo.etag;
|
||||
this.fileName = resourceInfo.fileName;
|
||||
this.mediaType = resourceInfo.mediaType;
|
||||
this.descriptor = resourceInfo.descriptor;
|
||||
}
|
||||
|
||||
@ -106,4 +107,9 @@ public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, H
|
||||
return searchText != null ? searchText : title;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public <T> T getDescriptor(Class<T> type) throws JsonProcessingException {
|
||||
return descriptor != null ? mapper.treeToValue(descriptor, type) : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,96 +0,0 @@
|
||||
/**
|
||||
* 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.common.data.util;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class ImageUtils {
|
||||
|
||||
private static final Map<String, String> mediaTypeMappings = Map.of(
|
||||
"jpeg", "jpg",
|
||||
"svg+xml", "svg"
|
||||
);
|
||||
|
||||
public static String mediaTypeToFileExtension(String mimeType) {
|
||||
String subtype = MimeTypeUtils.parseMimeType(mimeType).getSubtype();
|
||||
return mediaTypeMappings.getOrDefault(subtype, subtype);
|
||||
}
|
||||
|
||||
public static String fileExtensionToMediaType(String type, String extension) {
|
||||
String subtype = mediaTypeMappings.entrySet().stream()
|
||||
.filter(mapping -> mapping.getValue().equals(extension))
|
||||
.map(Map.Entry::getKey).findFirst().orElse(extension);
|
||||
return new MimeType(type, subtype).toString();
|
||||
}
|
||||
|
||||
public static ImageInfo processImage(byte[] imageData) throws IOException {
|
||||
BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageData));
|
||||
ImageThumbnail thumbnail = getImageThumbnail(image, 250);
|
||||
return new ImageInfo(image.getWidth(), image.getHeight(), thumbnail);
|
||||
}
|
||||
|
||||
private static ImageThumbnail getImageThumbnail(BufferedImage originalImage, int maxDimension) throws IOException {
|
||||
int originalWidth = originalImage.getWidth();
|
||||
int originalHeight = originalImage.getHeight();
|
||||
int thumbnailWidth;
|
||||
int thumbnailHeight;
|
||||
if (originalWidth <= maxDimension && originalHeight <= maxDimension) {
|
||||
thumbnailWidth = originalWidth;
|
||||
thumbnailHeight = originalHeight;
|
||||
} else {
|
||||
double aspectRatio = (double) originalWidth / originalHeight;
|
||||
if (originalWidth > originalHeight) {
|
||||
thumbnailWidth = maxDimension;
|
||||
thumbnailHeight = (int) (maxDimension / aspectRatio);
|
||||
} else {
|
||||
thumbnailWidth = (int) (maxDimension * aspectRatio);
|
||||
thumbnailHeight = maxDimension;
|
||||
}
|
||||
}
|
||||
BufferedImage thumbnail = new BufferedImage(thumbnailWidth, thumbnailHeight, BufferedImage.TYPE_INT_RGB);
|
||||
thumbnail.getGraphics().drawImage(originalImage, 0, 0, thumbnailWidth, thumbnailHeight, null);
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
ImageIO.write(thumbnail, "ignored", os);
|
||||
return new ImageThumbnail(thumbnail.getWidth(), thumbnail.getHeight(), os.toByteArray());
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ImageInfo {
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final ImageThumbnail thumbnail;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ImageThumbnail {
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final byte[] data;
|
||||
}
|
||||
|
||||
}
|
||||
@ -234,6 +234,14 @@
|
||||
<groupId>io.hypersistence</groupId>
|
||||
<artifactId>hypersistence-utils-hibernate-55</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.xmlgraphics</groupId>
|
||||
<artifactId>batik-transcoder</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.xmlgraphics</groupId>
|
||||
<artifactId>batik-codec</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
@ -499,7 +499,8 @@ public class ModelConstants {
|
||||
public static final String RESOURCE_FILE_NAME_COLUMN = "file_name";
|
||||
public static final String RESOURCE_DATA_COLUMN = "data";
|
||||
public static final String RESOURCE_ETAG_COLUMN = "etag";
|
||||
public static final String RESOURCE_MEDIA_TYPE_COLUMN = "media_type";
|
||||
public static final String RESOURCE_DESCRIPTOR_COLUMN = "descriptor";
|
||||
public static final String RESOURCE_PREVIEW_COLUMN = "preview";
|
||||
|
||||
/**
|
||||
* Ota Package constants.
|
||||
|
||||
@ -15,14 +15,17 @@
|
||||
*/
|
||||
package org.thingsboard.server.dao.model.sql;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.hibernate.annotations.Type;
|
||||
import org.hibernate.annotations.TypeDef;
|
||||
import org.thingsboard.server.common.data.ResourceType;
|
||||
import org.thingsboard.server.common.data.TbResource;
|
||||
import org.thingsboard.server.common.data.id.TbResourceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.dao.model.BaseEntity;
|
||||
import org.thingsboard.server.dao.model.BaseSqlEntity;
|
||||
import org.thingsboard.server.dao.util.mapping.JsonStringType;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
@ -30,10 +33,11 @@ import javax.persistence.Table;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_DATA_COLUMN;
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_DESCRIPTOR_COLUMN;
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_ETAG_COLUMN;
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME_COLUMN;
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN;
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_MEDIA_TYPE_COLUMN;
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_PREVIEW_COLUMN;
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME;
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN;
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TITLE_COLUMN;
|
||||
@ -43,8 +47,9 @@ import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPER
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Entity
|
||||
@TypeDef(name = "json", typeClass = JsonStringType.class)
|
||||
@Table(name = RESOURCE_TABLE_NAME)
|
||||
public class TbResourceEntity extends BaseSqlEntity<TbResource> implements BaseEntity<TbResource> {
|
||||
public class TbResourceEntity extends BaseSqlEntity<TbResource> {
|
||||
|
||||
@Column(name = RESOURCE_TENANT_ID_COLUMN, columnDefinition = "uuid")
|
||||
private UUID tenantId;
|
||||
@ -70,8 +75,12 @@ public class TbResourceEntity extends BaseSqlEntity<TbResource> implements BaseE
|
||||
@Column(name = RESOURCE_ETAG_COLUMN)
|
||||
private String etag;
|
||||
|
||||
@Column(name = RESOURCE_MEDIA_TYPE_COLUMN)
|
||||
private String mediaType;
|
||||
@Type(type = "json")
|
||||
@Column(name = RESOURCE_DESCRIPTOR_COLUMN)
|
||||
private JsonNode descriptor;
|
||||
|
||||
@Column(name = RESOURCE_PREVIEW_COLUMN)
|
||||
private byte[] preview;
|
||||
|
||||
public TbResourceEntity() {
|
||||
}
|
||||
@ -91,7 +100,8 @@ public class TbResourceEntity extends BaseSqlEntity<TbResource> implements BaseE
|
||||
this.fileName = resource.getFileName();
|
||||
this.data = resource.getData();
|
||||
this.etag = resource.getEtag();
|
||||
this.mediaType = resource.getMediaType();
|
||||
this.descriptor = resource.getDescriptor();
|
||||
this.preview = resource.getPreview();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -106,7 +116,8 @@ public class TbResourceEntity extends BaseSqlEntity<TbResource> implements BaseE
|
||||
resource.setFileName(fileName);
|
||||
resource.setData(data);
|
||||
resource.setEtag(etag);
|
||||
resource.setMediaType(mediaType);
|
||||
resource.setDescriptor(descriptor);
|
||||
resource.setPreview(preview);
|
||||
return resource;
|
||||
}
|
||||
|
||||
|
||||
@ -15,24 +15,28 @@
|
||||
*/
|
||||
package org.thingsboard.server.dao.model.sql;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.hibernate.annotations.Type;
|
||||
import org.hibernate.annotations.TypeDef;
|
||||
import org.thingsboard.server.common.data.ResourceType;
|
||||
import org.thingsboard.server.common.data.TbResourceInfo;
|
||||
import org.thingsboard.server.common.data.id.TbResourceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.dao.model.BaseEntity;
|
||||
import org.thingsboard.server.dao.model.BaseSqlEntity;
|
||||
import org.thingsboard.server.dao.util.mapping.JsonStringType;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Table;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_DESCRIPTOR_COLUMN;
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_ETAG_COLUMN;
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME_COLUMN;
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN;
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_MEDIA_TYPE_COLUMN;
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME;
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN;
|
||||
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TITLE_COLUMN;
|
||||
@ -42,6 +46,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPER
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Entity
|
||||
@TypeDef(name = "json", typeClass = JsonStringType.class)
|
||||
@Table(name = RESOURCE_TABLE_NAME)
|
||||
public class TbResourceInfoEntity extends BaseSqlEntity<TbResourceInfo> implements BaseEntity<TbResourceInfo> {
|
||||
|
||||
@ -66,8 +71,9 @@ public class TbResourceInfoEntity extends BaseSqlEntity<TbResourceInfo> implemen
|
||||
@Column(name = RESOURCE_FILE_NAME_COLUMN)
|
||||
private String fileName;
|
||||
|
||||
@Column(name = RESOURCE_MEDIA_TYPE_COLUMN)
|
||||
private String mediaType;
|
||||
@Type(type = "json")
|
||||
@Column(name = RESOURCE_DESCRIPTOR_COLUMN)
|
||||
private JsonNode descriptor;
|
||||
|
||||
public TbResourceInfoEntity() {
|
||||
}
|
||||
@ -84,7 +90,7 @@ public class TbResourceInfoEntity extends BaseSqlEntity<TbResourceInfo> implemen
|
||||
this.searchText = resource.getSearchText();
|
||||
this.hashCode = resource.getEtag();
|
||||
this.fileName = resource.getFileName();
|
||||
this.mediaType = resource.getMediaType();
|
||||
this.descriptor = resource.getDescriptor();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -98,7 +104,7 @@ public class TbResourceInfoEntity extends BaseSqlEntity<TbResourceInfo> implemen
|
||||
resource.setSearchText(searchText);
|
||||
resource.setEtag(hashCode);
|
||||
resource.setFileName(fileName);
|
||||
resource.setMediaType(mediaType);
|
||||
resource.setDescriptor(descriptor);
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,11 @@
|
||||
*/
|
||||
package org.thingsboard.server.dao.resource;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.server.common.data.ImageDescriptor;
|
||||
import org.thingsboard.server.common.data.ResourceType;
|
||||
import org.thingsboard.server.common.data.TbResource;
|
||||
import org.thingsboard.server.common.data.TbResourceInfo;
|
||||
@ -25,10 +29,13 @@ import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.page.PageData;
|
||||
import org.thingsboard.server.common.data.page.PageLink;
|
||||
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.Set;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class BaseImageService extends BaseResourceService implements ImageService {
|
||||
|
||||
public BaseImageService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, ResourceDataValidator resourceValidator) {
|
||||
@ -36,18 +43,39 @@ public class BaseImageService extends BaseResourceService implements ImageServic
|
||||
}
|
||||
|
||||
@Override
|
||||
public TbResource saveImage(TbResource image) {
|
||||
public TbResourceInfo saveImage(TbResource image) throws Exception {
|
||||
resourceValidator.validate(image, TbResourceInfo::getTenantId);
|
||||
if (image.getData() != null) {
|
||||
|
||||
}
|
||||
// generate preview, etc.
|
||||
return saveResource(image, false);
|
||||
ImageDescriptor descriptor = image.getDescriptor(ImageDescriptor.class);
|
||||
Pair<ImageDescriptor, byte[]> result = processImage(image.getData(), descriptor);
|
||||
image.setDescriptor(JacksonUtil.valueToTree(result.getLeft()));
|
||||
image.setPreview(result.getRight());
|
||||
|
||||
image = saveResource(image, false);
|
||||
return new TbResourceInfo(image);
|
||||
}
|
||||
|
||||
private Pair<ImageDescriptor, byte[]> processImage(byte[] data, ImageDescriptor descriptor) throws Exception {
|
||||
ProcessedImage image = ImageUtils.processImage(data, descriptor.getMediaType(), 250);
|
||||
ProcessedImage preview = image.getPreview();
|
||||
|
||||
descriptor.setWidth(image.getWidth());
|
||||
descriptor.setHeight(image.getHeight());
|
||||
descriptor.setSize(image.getSize());
|
||||
|
||||
ImageDescriptor previewDescriptor = new ImageDescriptor();
|
||||
previewDescriptor.setWidth(preview.getWidth());
|
||||
previewDescriptor.setHeight(preview.getHeight());
|
||||
previewDescriptor.setMediaType(preview.getMediaType());
|
||||
previewDescriptor.setSize(preview.getSize());
|
||||
descriptor.setPreviewDescriptor(previewDescriptor);
|
||||
|
||||
return Pair.of(descriptor, preview.getData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TbResourceInfo saveImageInfo(TbResourceInfo imageInfo) {
|
||||
return null;
|
||||
return saveResource(new TbResource(imageInfo));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -80,7 +108,12 @@ public class BaseImageService extends BaseResourceService implements ImageServic
|
||||
|
||||
@Override
|
||||
public byte[] getImagePreview(TenantId tenantId, TbResourceId imageId) {
|
||||
return new byte[0];
|
||||
return resourceDao.getResourcePreview(tenantId, imageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteImage(TenantId tenantId, TbResourceId imageId) {
|
||||
deleteResource(tenantId, imageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -15,12 +15,12 @@
|
||||
*/
|
||||
package org.thingsboard.server.dao.resource;
|
||||
|
||||
import com.datastax.oss.driver.api.core.uuid.Uuids;
|
||||
import com.google.common.hash.Hashing;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.hibernate.exception.ConstraintViolationException;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey;
|
||||
@ -46,7 +46,6 @@ import org.thingsboard.server.dao.service.validator.ResourceDataValidator;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID;
|
||||
import static org.thingsboard.server.dao.service.Validator.validateId;
|
||||
@ -54,6 +53,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
|
||||
@Service("TbResourceDaoService")
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
@Primary
|
||||
public class BaseResourceService extends AbstractCachedEntityService<ResourceInfoCacheKey, TbResourceInfo, ResourceInfoEvictEvent> implements ResourceService {
|
||||
|
||||
public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId ";
|
||||
|
||||
@ -43,4 +43,6 @@ public interface TbResourceDao extends Dao<TbResource>, TenantEntityWithDataDao
|
||||
|
||||
byte[] getResourceData(TenantId tenantId, TbResourceId resourceId);
|
||||
|
||||
byte[] getResourcePreview(TenantId tenantId, TbResourceId resourceId);
|
||||
|
||||
}
|
||||
|
||||
@ -17,8 +17,6 @@ package org.thingsboard.server.dao.service.validator;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.http.InvalidMediaTypeException;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thingsboard.server.common.data.StringUtils;
|
||||
import org.thingsboard.server.common.data.TbResource;
|
||||
@ -94,19 +92,6 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
|
||||
long maxSumResourcesDataInBytes = profileConfiguration.getMaxResourcesInBytes();
|
||||
validateMaxSumDataSizePerTenant(tenantId, resourceDao, maxSumResourcesDataInBytes, resource.getData().length, TB_RESOURCE);
|
||||
}
|
||||
if (resource.getResourceType().getDefaultMediaType() != null) {
|
||||
resource.setMediaType(resource.getResourceType().getDefaultMediaType());
|
||||
} else {
|
||||
if (resource.getMediaType() == null) {
|
||||
throw new DataValidationException("Media type is required");
|
||||
} else {
|
||||
try {
|
||||
MediaType.parseMediaType(resource.getMediaType());
|
||||
} catch (InvalidMediaTypeException e) {
|
||||
throw new DataValidationException("Invalid media type", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (StringUtils.isEmpty(resource.getFileName())) {
|
||||
throw new DataValidationException("Resource file name should be specified!");
|
||||
}
|
||||
|
||||
@ -99,6 +99,11 @@ public class JpaTbResourceDao extends JpaAbstractDao<TbResourceEntity, TbResourc
|
||||
return resourceRepository.getDataById(resourceId.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getResourcePreview(TenantId tenantId, TbResourceId resourceId) {
|
||||
return resourceRepository.getPreviewById(resourceId.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long sumDataSizeByTenantId(TenantId tenantId) {
|
||||
return resourceRepository.sumDataSizeByTenantId(tenantId.getId());
|
||||
|
||||
@ -37,19 +37,19 @@ public interface TbResourceInfoRepository extends JpaRepository<TbResourceInfoEn
|
||||
"WHERE sr.tenantId = :tenantId " +
|
||||
"AND tr.resourceType = sr.resourceType " +
|
||||
"AND tr.resourceKey = sr.resourceKey)))" +
|
||||
"AND (:resourceTypes IS NULL OR tr.resourceType IN :resourceTypes)")
|
||||
"AND tr.resourceType IN :resourceTypes")
|
||||
Page<TbResourceInfoEntity> findAllTenantResourcesByTenantId(@Param("tenantId") UUID tenantId,
|
||||
@Param("systemAdminId") UUID sysadminId,
|
||||
@Param("resourceTypes") Collection<String> resourceTypes,
|
||||
@Param("resourceTypes") List<String> resourceTypes,
|
||||
@Param("searchText") String searchText,
|
||||
Pageable pageable);
|
||||
|
||||
@Query("SELECT ri FROM TbResourceInfoEntity ri WHERE " +
|
||||
"ri.tenantId = :tenantId " +
|
||||
"AND (:resourceTypes IS NULL OR ri.resourceType IN :resourceTypes)" +
|
||||
"AND ri.resourceType IN :resourceTypes " +
|
||||
"AND (:searchText IS NULL OR ilike(ri.title, CONCAT('%', :searchText, '%')) = true)")
|
||||
Page<TbResourceInfoEntity> findTenantResourcesByTenantId(@Param("tenantId") UUID tenantId,
|
||||
@Param("resourceTypes") Collection<String> resourceTypes,
|
||||
@Param("resourceTypes") List<String> resourceTypes,
|
||||
@Param("searchText") String searchText,
|
||||
Pageable pageable);
|
||||
|
||||
|
||||
@ -84,4 +84,7 @@ public interface TbResourceRepository extends JpaRepository<TbResourceEntity, UU
|
||||
@Query("SELECT r.data FROM TbResourceEntity r WHERE r.id = :id")
|
||||
byte[] getDataById(@Param("id") UUID id);
|
||||
|
||||
@Query(value = "SELECT COALESCE(preview, data) FROM resource WHERE id = :id", nativeQuery = true)
|
||||
byte[] getPreviewById(@Param("id") UUID id);
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 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.dao.util;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.batik.transcoder.TranscoderInput;
|
||||
import org.apache.batik.transcoder.TranscoderOutput;
|
||||
import org.apache.batik.transcoder.image.PNGTranscoder;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Map;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class ImageUtils {
|
||||
|
||||
private static final Map<String, String> mediaTypeMappings = Map.of(
|
||||
"jpeg", "jpg",
|
||||
"svg+xml", "svg"
|
||||
);
|
||||
|
||||
public static String mediaTypeToFileExtension(String mimeType) {
|
||||
String subtype = MimeTypeUtils.parseMimeType(mimeType).getSubtype();
|
||||
return mediaTypeMappings.getOrDefault(subtype, subtype);
|
||||
}
|
||||
|
||||
public static String fileExtensionToMediaType(String type, String extension) {
|
||||
String subtype = mediaTypeMappings.entrySet().stream()
|
||||
.filter(mapping -> mapping.getValue().equals(extension))
|
||||
.map(Map.Entry::getKey).findFirst().orElse(extension);
|
||||
return new MimeType(type, subtype).toString();
|
||||
}
|
||||
|
||||
public static ProcessedImage processImage(byte[] data, String mediaType, int thumbnailMaxDimension) throws Exception {
|
||||
if (mediaTypeToFileExtension(mediaType).equals("svg")) {
|
||||
return processSvgImage(data, thumbnailMaxDimension);
|
||||
}
|
||||
|
||||
BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(data));
|
||||
ProcessedImage image = new ProcessedImage();
|
||||
image.setMediaType(mediaType);
|
||||
image.setWidth(bufferedImage.getWidth());
|
||||
image.setHeight(bufferedImage.getHeight());
|
||||
image.setData(data);
|
||||
image.setSize(data.length);
|
||||
|
||||
ProcessedImage preview = new ProcessedImage();
|
||||
int[] thumbnailDimensions = getThumbnailDimensions(image.getWidth(), image.getHeight(), thumbnailMaxDimension);
|
||||
preview.setWidth(thumbnailDimensions[0]);
|
||||
preview.setHeight(thumbnailDimensions[1]);
|
||||
|
||||
if (preview.getWidth() == image.getWidth() && preview.getHeight() == image.getHeight()) {
|
||||
if (mediaType.equals("image/png")) {
|
||||
preview.setMediaType(mediaType);
|
||||
preview.setData(null);
|
||||
preview.setSize(data.length);
|
||||
image.setPreview(preview);
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
BufferedImage thumbnail = new BufferedImage(preview.getWidth(), preview.getHeight(), BufferedImage.TYPE_INT_RGB);
|
||||
thumbnail.getGraphics().drawImage(bufferedImage, 0, 0, preview.getWidth(), preview.getHeight(), null);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ImageIO.write(thumbnail, "png", out);
|
||||
|
||||
preview.setMediaType("image/png");
|
||||
preview.setData(out.toByteArray());
|
||||
preview.setSize(preview.getData().length);
|
||||
image.setPreview(preview);
|
||||
return image;
|
||||
}
|
||||
|
||||
public static ProcessedImage processSvgImage(byte[] data, int thumbnailMaxDimension) throws Exception {
|
||||
ProcessedImage image = new ProcessedImage();
|
||||
image.setWidth(0);
|
||||
image.setHeight(0);
|
||||
image.setData(data);
|
||||
image.setSize(data.length);
|
||||
|
||||
PNGTranscoder transcoder = new PNGTranscoder();
|
||||
transcoder.addTranscodingHint(PNGTranscoder.KEY_MAX_WIDTH, (float) thumbnailMaxDimension);
|
||||
transcoder.addTranscodingHint(PNGTranscoder.KEY_MAX_HEIGHT, (float) thumbnailMaxDimension);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
transcoder.transcode(new TranscoderInput(new ByteArrayInputStream(data)), new TranscoderOutput(out));
|
||||
byte[] pngThumbnail = out.toByteArray();
|
||||
|
||||
ProcessedImage preview = new ProcessedImage();
|
||||
preview.setWidth(thumbnailMaxDimension);
|
||||
preview.setHeight(thumbnailMaxDimension);
|
||||
preview.setMediaType("image/png");
|
||||
preview.setData(pngThumbnail);
|
||||
preview.setSize(pngThumbnail.length);
|
||||
image.setPreview(preview);
|
||||
return image;
|
||||
}
|
||||
|
||||
private static int[] getThumbnailDimensions(int originalWidth, int originalHeight, int maxDimension) {
|
||||
if (originalWidth <= maxDimension && originalHeight <= maxDimension) {
|
||||
return new int[]{originalWidth, originalHeight};
|
||||
}
|
||||
int thumbnailWidth;
|
||||
int thumbnailHeight;
|
||||
double aspectRatio = (double) originalWidth / originalHeight;
|
||||
if (originalWidth > originalHeight) {
|
||||
thumbnailWidth = maxDimension;
|
||||
thumbnailHeight = (int) (maxDimension / aspectRatio);
|
||||
} else {
|
||||
thumbnailWidth = (int) (maxDimension * aspectRatio);
|
||||
thumbnailHeight = maxDimension;
|
||||
}
|
||||
return new int[]{thumbnailWidth, thumbnailHeight};
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ProcessedImage {
|
||||
private String mediaType;
|
||||
private int width;
|
||||
private int height;
|
||||
private byte[] data;
|
||||
private long size;
|
||||
private ProcessedImage preview;
|
||||
}
|
||||
|
||||
}
|
||||
@ -716,7 +716,8 @@ CREATE TABLE IF NOT EXISTS resource (
|
||||
file_name varchar(255) NOT NULL,
|
||||
data bytea,
|
||||
etag varchar,
|
||||
media_type varchar(255),
|
||||
descriptor varchar,
|
||||
preview bytea,
|
||||
CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_key)
|
||||
);
|
||||
|
||||
|
||||
11
pom.xml
11
pom.xml
@ -153,6 +153,7 @@
|
||||
<slack-api.version>1.12.1</slack-api.version>
|
||||
<oshi.version>6.4.2</oshi.version>
|
||||
<google-oauth-client.version>1.34.1</google-oauth-client.version>
|
||||
<apache-xmlgraphics.version>1.14</apache-xmlgraphics.version>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
@ -2029,6 +2030,16 @@
|
||||
<artifactId>google-oauth-client</artifactId>
|
||||
<version>${google-oauth-client.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.xmlgraphics</groupId>
|
||||
<artifactId>batik-transcoder</artifactId>
|
||||
<version>${apache-xmlgraphics.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.xmlgraphics</groupId>
|
||||
<artifactId>batik-codec</artifactId>
|
||||
<version>${apache-xmlgraphics.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@ -61,8 +61,6 @@ export interface ResourceInfo extends Omit<BaseData<TbResourceId>, 'name' | 'lab
|
||||
title?: string;
|
||||
resourceType: ResourceType;
|
||||
fileName: string;
|
||||
mediaType: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export interface Resource extends ResourceInfo {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user