From 36a89bea918bc894a7254714efcb7a103bd8ae32 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 14 Dec 2023 16:24:36 +0200 Subject: [PATCH 01/12] Public images --- .../main/data/upgrade/3.6.1/schema_update.sql | 30 ++++++++-- .../ThingsboardSecurityConfiguration.java | 2 +- .../server/controller/ImageController.java | 49 +++++++++++---- .../update/DefaultCacheCleanupService.java | 2 + .../queue/DefaultTbCoreConsumerService.java | 15 +++-- .../resource/DefaultTbImageService.java | 60 ++++++++++++------- .../service/resource/TbImageService.java | 2 +- .../server/dao/resource/ImageService.java | 2 + .../server/common/data/ImageExportData.java | 4 ++ .../server/common/data/TbResourceInfo.java | 11 +++- .../common/data/util/ThrowingSupplier.java | 23 +++++++ common/proto/src/main/proto/queue.proto | 7 ++- .../server/dao/model/ModelConstants.java | 2 + .../dao/model/sql/TbResourceEntity.java | 12 ++++ .../dao/model/sql/TbResourceInfoEntity.java | 12 ++++ .../server/dao/resource/BaseImageService.java | 25 +++++++- .../server/dao/resource/ImageCacheKey.java | 32 +++++++++- .../dao/resource/TbResourceInfoDao.java | 5 ++ .../sql/resource/JpaTbResourceInfoDao.java | 10 ++++ .../resource/TbResourceInfoRepository.java | 5 ++ .../resources/sql/schema-entities-idx.sql | 4 ++ .../main/resources/sql/schema-entities.sql | 2 + 22 files changed, 266 insertions(+), 50 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/util/ThrowingSupplier.java diff --git a/application/src/main/data/upgrade/3.6.1/schema_update.sql b/application/src/main/data/upgrade/3.6.1/schema_update.sql index cfe16f7ff8..976c0df296 100644 --- a/application/src/main/data/upgrade/3.6.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.6.1/schema_update.sql @@ -16,6 +16,28 @@ -- RESOURCES UPDATE START +ALTER TABLE resource ADD COLUMN IF NOT EXISTS descriptor varchar; +ALTER TABLE resource ADD COLUMN IF NOT EXISTS preview bytea; +ALTER TABLE resource ADD COLUMN IF NOT EXISTS external_id uuid; +ALTER TABLE resource ADD COLUMN IF NOT EXISTS is_public boolean default true; +ALTER TABLE resource ADD COLUMN IF NOT EXISTS public_key varchar(35) unique; + +CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag); +CREATE INDEX IF NOT EXISTS idx_resource_image_public_key ON resource(public_key) WHERE resource_type = 'IMAGE'; + +CREATE OR REPLACE FUNCTION generate_resource_public_key() +RETURNS text AS $$ +DECLARE + chars text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + result text := ''; +BEGIN + FOR i IN 1..35 LOOP + result := result || substr(chars, floor(random()*62)::int + 1, 1); + END LOOP; + RETURN result; +END; +$$ LANGUAGE plpgsql; + DO $$ BEGIN @@ -24,15 +46,13 @@ $$ ALTER TABLE resource ADD COLUMN data bytea; UPDATE resource SET data = decode(base64_data, 'base64') WHERE base64_data IS NOT NULL; ALTER TABLE resource DROP COLUMN base64_data; + ELSE + UPDATE resource SET public_key = generate_resource_public_key() WHERE resource_type = 'IMAGE' AND public_key IS NULL; END IF; END; $$; -ALTER TABLE resource ADD COLUMN IF NOT EXISTS descriptor varchar; -ALTER TABLE resource ADD COLUMN IF NOT EXISTS preview bytea; -ALTER TABLE resource ADD COLUMN IF NOT EXISTS external_id uuid; - -CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag); +DROP FUNCTION generate_resource_public_key; -- RESOURCES UPDATE END diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 6b29ba0b28..61b99e78e3 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -75,7 +75,7 @@ public class ThingsboardSecurityConfiguration { public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public"; public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; - protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**"}; + protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**", "/api/images/public/**"}; public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; public static final String WS_ENTRY_POINT = "/api/ws/**"; public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code"; 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 815adaca5f..7dc8501188 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -15,8 +15,9 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.core.JsonProcessingException; import io.swagger.annotations.ApiParam; +import lombok.Builder; +import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -50,6 +51,7 @@ 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.common.data.security.Authority; +import org.thingsboard.server.common.data.util.ThrowingSupplier; import org.thingsboard.server.dao.resource.ImageCacheKey; import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.service.validator.ResourceDataValidator; @@ -92,7 +94,8 @@ public class ImageController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PostMapping("/api/image") public TbResourceInfo uploadImage(@RequestPart MultipartFile file, - @RequestPart(required = false) String title) throws Exception { + @RequestPart(required = false) String title, + @RequestPart(required = false) Boolean isPublic) throws Exception { SecurityUser user = getCurrentUser(); TbResource image = new TbResource(); image.setTenantId(user.getTenantId()); @@ -105,6 +108,7 @@ public class ImageController extends BaseController { } else { image.setTitle(file.getOriginalFilename()); } + image.setPublic(isPublic != null ? isPublic : true); image.setResourceType(ResourceType.IMAGE); ImageDescriptor descriptor = new ImageDescriptor(); descriptor.setMediaType(file.getContentType()); @@ -138,6 +142,7 @@ public class ImageController extends BaseController { @RequestBody TbResourceInfo newImageInfo) throws ThingsboardException { TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.WRITE); imageInfo.setTitle(newImageInfo.getTitle()); + imageInfo.setPublic(newImageInfo.isPublic()); return tbImageService.save(imageInfo, getCurrentUser()); } @@ -149,13 +154,28 @@ public class ImageController extends BaseController { return downloadIfChanged(type, key, etag, false); } + @GetMapping(value = "/api/images/public/{publicKey}", produces = "image/*") + public ResponseEntity downloadPublicImage(@PathVariable String publicKey, + @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception { + ImageCacheKey cacheKey = ImageCacheKey.forPublicImage(publicKey); + return downloadIfChanged(cacheKey, etag, () -> imageService.getPublicImageInfoByPublicKey(publicKey)); + } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = IMAGE_URL + "/export") public ImageExportData exportImage(@PathVariable String type, @PathVariable String key) throws Exception { TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ); ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); byte[] data = imageService.getImageData(imageInfo.getTenantId(), imageInfo.getId()); - return new ImageExportData(descriptor.getMediaType(), imageInfo.getFileName(), imageInfo.getTitle(), imageInfo.getResourceKey(), Base64Utils.encodeToString(data)); + return ImageExportData.builder() + .mediaType(descriptor.getMediaType()) + .fileName(imageInfo.getFileName()) + .title(imageInfo.getTitle()) + .resourceKey(imageInfo.getResourceKey()) + .isPublic(imageInfo.isPublic()) + .publicKey(imageInfo.getPublicKey()) + .data(Base64Utils.encodeToString(data)) + .build(); } @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @@ -172,14 +192,15 @@ public class ImageController extends BaseController { } else { image.setTitle(imageData.getFileName()); } - image.setResourceKey(imageData.getResourceKey()); image.setResourceType(ResourceType.IMAGE); + image.setResourceKey(imageData.getResourceKey()); + image.setPublic(imageData.isPublic()); + image.setPublicKey(imageData.getPublicKey()); ImageDescriptor descriptor = new ImageDescriptor(); descriptor.setMediaType(imageData.getMediaType()); image.setDescriptorValue(descriptor); image.setData(Base64Utils.decodeFromString(imageData.getData())); return tbImageService.save(image, user); - } @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @@ -231,24 +252,28 @@ public class ImageController extends BaseController { return (result.isSuccess() ? ResponseEntity.ok() : ResponseEntity.badRequest()).body(result); } - private ResponseEntity downloadIfChanged(String type, String key, String etag, boolean preview) throws ThingsboardException, JsonProcessingException { - ImageCacheKey cacheKey = new ImageCacheKey(getTenantId(type), key, preview); + private ResponseEntity downloadIfChanged(String type, String key, String etag, boolean preview) throws Exception { + ImageCacheKey cacheKey = ImageCacheKey.forImage(getTenantId(type), key, preview); + return downloadIfChanged(cacheKey, etag, () -> checkImageInfo(type, key, Operation.READ)); + } + + private ResponseEntity downloadIfChanged(ImageCacheKey cacheKey, String etag, ThrowingSupplier imageInfoSupplier) throws Exception { if (StringUtils.isNotEmpty(etag)) { etag = StringUtils.remove(etag, '\"'); // etag is wrapped in double quotes due to HTTP specification if (etag.equals(tbImageService.getETag(cacheKey))) { return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build(); } } - TenantId tenantId = getTenantId(); - TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ); + + TbResourceInfo imageInfo = imageInfoSupplier.get(); String fileName = imageInfo.getFileName(); ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); byte[] data; - if (preview) { + if (cacheKey.isPreview()) { descriptor = descriptor.getPreviewDescriptor(); - data = imageService.getImagePreview(tenantId, imageInfo.getId()); + data = imageService.getImagePreview(imageInfo.getTenantId(), imageInfo.getId()); } else { - data = imageService.getImageData(tenantId, imageInfo.getId()); + data = imageService.getImageData(imageInfo.getTenantId(), imageInfo.getId()); } tbImageService.putETag(cacheKey, descriptor.getEtag()); var result = ResponseEntity.ok() diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java index e4e1ddf427..350005536a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java @@ -27,6 +27,7 @@ import org.springframework.stereotype.Service; import java.util.Objects; import java.util.Optional; +import static org.thingsboard.server.common.data.CacheConstants.RESOURCE_INFO_CACHE; import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE; @RequiredArgsConstructor @@ -91,6 +92,7 @@ public class DefaultCacheCleanupService implements CacheCleanupService { case "3.6.1": log.info("Clearing cache to upgrade from version 3.6.1 to 3.6.2"); clearCacheByName(SECURITY_SETTINGS_CACHE); + clearCacheByName(RESOURCE_INFO_CACHE); break; default: //Do nothing, since cache cleanup is optional. diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 5283713126..08d3b2325c 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -44,8 +44,10 @@ import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; +import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequestActorMsg; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.dao.resource.ImageCacheKey; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; @@ -59,6 +61,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmDeleteProto; import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmUpdateProto; import org.thingsboard.server.gen.transport.TransportProtos.TbAttributeDeleteProto; import org.thingsboard.server.gen.transport.TransportProtos.TbAttributeUpdateProto; +import org.thingsboard.server.gen.transport.TransportProtos.TbEntitySubEventProto; import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesDeleteProto; import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesUpdateProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -66,7 +69,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMs import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; -import org.thingsboard.server.gen.transport.TransportProtos.TbEntitySubEventProto; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; @@ -83,10 +85,8 @@ import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.queue.processing.IdMsgPair; -import org.thingsboard.server.dao.resource.ImageCacheKey; import org.thingsboard.server.service.resource.TbImageService; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; -import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequestActorMsg; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.service.state.DeviceStateService; import org.thingsboard.server.service.subscription.SubscriptionManagerService; @@ -560,8 +560,13 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService { + if (cacheKeyProto.hasResourceKey()) { + return ImageCacheKey.forImage(tenantId, cacheKeyProto.getResourceKey()); + } else { + return ImageCacheKey.forPublicImage(cacheKeyProto.getPublicKey()); + } + }).forEach(imageService::evictETags); callback.onSuccess(); } 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 1fbd62f88f..151ef6ce85 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 @@ -38,8 +38,11 @@ import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; @Service @Slf4j @@ -72,8 +75,11 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb } @Override - public void evictETag(ImageCacheKey imageCacheKey) { + public void evictETags(ImageCacheKey imageCacheKey) { etagCache.invalidate(imageCacheKey); + if (imageCacheKey.getPublicKey() == null) { + etagCache.invalidate(imageCacheKey.withPreview(true)); + } } @Override @@ -82,28 +88,32 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb TenantId tenantId = image.getTenantId(); try { var oldEtag = getEtag(image); + TbResourceInfo existingImage = null; if (image.getId() == null && StringUtils.isNotEmpty(image.getResourceKey())) { - var existingImage = imageService.getImageInfoByTenantIdAndKey(tenantId, image.getResourceKey()); + existingImage = imageService.getImageInfoByTenantIdAndKey(tenantId, image.getResourceKey()); if (existingImage != null) { image.setId(existingImage.getId()); } } TbResourceInfo savedImage = imageService.saveImage(image); notificationEntityService.logEntityAction(tenantId, savedImage.getId(), savedImage, actionType, user); + + List toEvict = new ArrayList<>(); if (oldEtag.isPresent()) { var newEtag = getEtag(savedImage); if (newEtag.isPresent() && !oldEtag.get().equals(newEtag.get())) { - evictETag(new ImageCacheKey(image.getTenantId(), image.getResourceKey(), false)); - evictETag(new ImageCacheKey(image.getTenantId(), image.getResourceKey(), true)); - clusterService.broadcastToCore(TransportProtos.ToCoreNotificationMsg.newBuilder() - .setResourceCacheInvalidateMsg(TransportProtos.ResourceCacheInvalidateMsg.newBuilder() - .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) - .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) - .setResourceKey(image.getResourceKey()) - .build()) - .build()); + toEvict.add(ImageCacheKey.forImage(tenantId, image.getResourceKey())); + if (image.isPublic()) { + toEvict.add(ImageCacheKey.forPublicImage(savedImage.getPublicKey())); + } } } + if (existingImage != null && image.isPublic() != existingImage.isPublic()) { + toEvict.add(ImageCacheKey.forPublicImage(image.getPublicKey())); + } + if (!toEvict.isEmpty()) { + evictFromCache(tenantId, toEvict); + } return savedImage; } catch (Exception e) { image.setData(null); @@ -145,15 +155,13 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb TbImageDeleteResult result = imageService.deleteImage(imageInfo, force); if (result.isSuccess()) { notificationEntityService.logEntityAction(tenantId, imageId, imageInfo, ActionType.DELETED, user, imageId.toString()); - evictETag(new ImageCacheKey(tenantId, imageInfo.getResourceKey(), false)); - evictETag(new ImageCacheKey(tenantId, imageInfo.getResourceKey(), true)); - clusterService.broadcastToCore(TransportProtos.ToCoreNotificationMsg.newBuilder() - .setResourceCacheInvalidateMsg(TransportProtos.ResourceCacheInvalidateMsg.newBuilder() - .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) - .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) - .setResourceKey(imageInfo.getResourceKey()) - .build()) - .build()); + + List toEvict = new ArrayList<>(); + toEvict.add(ImageCacheKey.forImage(tenantId, imageInfo.getResourceKey())); + if (imageInfo.isPublic()) { + toEvict.add(ImageCacheKey.forPublicImage(imageInfo.getPublicKey())); + } + evictFromCache(tenantId, toEvict); } return result; } catch (Exception e) { @@ -161,4 +169,16 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb throw e; } } + + private void evictFromCache(TenantId tenantId, List toEvict) { + toEvict.forEach(this::evictETags); + clusterService.broadcastToCore(TransportProtos.ToCoreNotificationMsg.newBuilder() + .setResourceCacheInvalidateMsg(TransportProtos.ResourceCacheInvalidateMsg.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .addAllKeys(toEvict.stream().map(ImageCacheKey::toProto).collect(Collectors.toList())) + .build()) + .build()); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java b/application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java index c29cc6ad3b..15f41d691d 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java @@ -33,6 +33,6 @@ public interface TbImageService { void putETag(ImageCacheKey imageCacheKey, String etag); - void evictETag(ImageCacheKey imageCacheKey); + void evictETags(ImageCacheKey imageCacheKey); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java index 7e5542919b..e689a48c0b 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java @@ -34,6 +34,8 @@ public interface ImageService { TbResourceInfo getImageInfoByTenantIdAndKey(TenantId tenantId, String key); + TbResourceInfo getPublicImageInfoByPublicKey(String publicKey); + PageData getImagesByTenantId(TenantId tenantId, PageLink pageLink); PageData getAllImagesByTenantId(TenantId tenantId, PageLink pageLink); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java index d1b33f202a..d73881b70d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java @@ -16,18 +16,22 @@ package org.thingsboard.server.common.data; import io.swagger.annotations.ApiModel; +import lombok.Builder; import lombok.Data; import lombok.extern.slf4j.Slf4j; @ApiModel @Slf4j @Data +@Builder public class ImageExportData { private final String mediaType; private final String fileName; private final String title; private final String resourceKey; + private final boolean isPublic; + private final String publicKey; private final String data; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java index b8d2267663..559ac43385 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java @@ -51,6 +51,8 @@ public class TbResourceInfo extends BaseData implements HasName, H @Length(fieldName = "resourceKey") @ApiModelProperty(position = 6, value = "Resource key.", example = "19_1.0", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String resourceKey; + private boolean isPublic; + private String publicKey; @ApiModelProperty(position = 7, value = "Resource search text.", example = "19_1.0:binaryappdatacontainer", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String searchText; @@ -79,6 +81,8 @@ public class TbResourceInfo extends BaseData implements HasName, H this.resourceType = resourceInfo.resourceType; this.resourceKey = resourceInfo.resourceKey; this.searchText = resourceInfo.searchText; + this.isPublic = resourceInfo.isPublic; + this.publicKey = resourceInfo.publicKey; this.etag = resourceInfo.etag; this.fileName = resourceInfo.fileName; this.descriptor = resourceInfo.descriptor != null ? resourceInfo.descriptor.deepCopy() : null; @@ -109,7 +113,12 @@ public class TbResourceInfo extends BaseData implements HasName, H @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String getLink() { if (resourceType == ResourceType.IMAGE) { - return "/api/images/" + ((tenantId == null || !tenantId.isSysTenantId()) ? "tenant" : "system") + "/" + resourceKey; // tenantId is null in case of export to git + if (isPublic) { + return "/api/images/public/" + getPublicKey(); + } else { + String type = (tenantId != null && tenantId.isSysTenantId()) ? "system" : "tenant"; // tenantId is null in case of export to git + return "/api/images/" + type + "/" + resourceKey; + } } return null; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/ThrowingSupplier.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/ThrowingSupplier.java new file mode 100644 index 0000000000..0e9a74dd15 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/ThrowingSupplier.java @@ -0,0 +1,23 @@ +/** + * 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; + +@FunctionalInterface +public interface ThrowingSupplier { + + T get() throws Exception; + +} diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 15e08ee905..0c7dc82ee7 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -309,7 +309,12 @@ message CoreStartupMsg { message ResourceCacheInvalidateMsg { int64 tenantIdMSB = 1; int64 tenantIdLSB = 2; - string resourceKey = 3; + repeated ImageCacheKeyProto keys = 3; +} + +message ImageCacheKeyProto { + optional string resourceKey = 1; + optional string publicKey = 2; } message LwM2MRegistrationRequestMsg { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 21cea59ee3..c4f5047354 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -501,6 +501,8 @@ public class ModelConstants { public static final String RESOURCE_ETAG_COLUMN = "etag"; public static final String RESOURCE_DESCRIPTOR_COLUMN = "descriptor"; public static final String RESOURCE_PREVIEW_COLUMN = "preview"; + public static final String RESOURCE_IS_PUBLIC_COLUMN = "is_public"; + public static final String RESOURCE_PUBLIC_KEY_COLUMN = "public_key"; /** * Ota Package constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java index ff63994259..459e137239 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java @@ -37,8 +37,10 @@ import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_DATA_COLU 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_IS_PUBLIC_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_PREVIEW_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_PUBLIC_KEY_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; @@ -83,6 +85,12 @@ public class TbResourceEntity extends BaseSqlEntity { @Column(name = RESOURCE_PREVIEW_COLUMN) private byte[] preview; + @Column(name = RESOURCE_IS_PUBLIC_COLUMN) + private Boolean isPublic; + + @Column(name = RESOURCE_PUBLIC_KEY_COLUMN, unique = true, updatable = false) + private String publicKey; + @Column(name = EXTERNAL_ID_PROPERTY) private UUID externalId; @@ -106,6 +114,8 @@ public class TbResourceEntity extends BaseSqlEntity { this.etag = resource.getEtag(); this.descriptor = resource.getDescriptor(); this.preview = resource.getPreview(); + this.isPublic = resource.isPublic(); + this.publicKey = resource.getPublicKey(); this.externalId = getUuid(resource.getExternalId()); } @@ -123,6 +133,8 @@ public class TbResourceEntity extends BaseSqlEntity { resource.setEtag(etag); resource.setDescriptor(descriptor); resource.setPreview(preview); + resource.setPublic(isPublic == null || isPublic); + resource.setPublicKey(publicKey); resource.setExternalId(getEntityId(externalId, TbResourceId::new)); return resource; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java index 3a693a6301..5e488b6901 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java @@ -37,7 +37,9 @@ import static org.thingsboard.server.dao.model.ModelConstants.EXTERNAL_ID_PROPER 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_IS_PUBLIC_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_PUBLIC_KEY_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; @@ -76,6 +78,12 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen @Column(name = RESOURCE_DESCRIPTOR_COLUMN) private JsonNode descriptor; + @Column(name = RESOURCE_IS_PUBLIC_COLUMN) + private Boolean isPublic; + + @Column(name = RESOURCE_PUBLIC_KEY_COLUMN, unique = true, updatable = false) + private String publicKey; + @Column(name = EXTERNAL_ID_PROPERTY) private UUID externalId; @@ -95,6 +103,8 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen this.etag = resource.getEtag(); this.fileName = resource.getFileName(); this.descriptor = resource.getDescriptor(); + this.isPublic = resource.isPublic(); + this.publicKey = resource.getPublicKey(); this.externalId = getUuid(resource.getExternalId()); } @@ -110,6 +120,8 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen resource.setEtag(etag); resource.setFileName(fileName); resource.setDescriptor(descriptor); + resource.setPublic(isPublic == null || isPublic); + resource.setPublicKey(publicKey); resource.setExternalId(getEntityId(externalId, TbResourceId::new)); return resource; } 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 f7b39166f0..e3b68b82c6 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 @@ -23,6 +23,7 @@ import com.google.common.base.Strings; import lombok.Data; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.tuple.Pair; @@ -149,6 +150,15 @@ public class BaseImageService extends BaseResourceService implements ImageServic image.setDescriptorValue(descriptor); image.setPreview(result.getRight()); + if (image.getId() == null) { + if (StringUtils.isEmpty(image.getPublicKey())) { + image.setPublicKey(generatePublicKey()); + } else { + if (resourceInfoDao.existsByPublicKey(ResourceType.IMAGE, image.getPublicKey())) { + image.setPublicKey(generatePublicKey()); + } + } + } log.debug("[{}] Creating image {} ('{}')", image.getTenantId(), image.getResourceKey(), image.getName()); return new TbResourceInfo(doSaveResource(image)); } @@ -194,6 +204,10 @@ public class BaseImageService extends BaseResourceService implements ImageServic return resourceKey; } + private String generatePublicKey() { + return RandomStringUtils.randomAlphanumeric(35); + } + @Override public TbResourceInfo saveImageInfo(TbResourceInfo imageInfo) { log.trace("Executing saveImageInfo [{}] [{}]", imageInfo.getTenantId(), imageInfo.getId()); @@ -206,6 +220,11 @@ public class BaseImageService extends BaseResourceService implements ImageServic return findResourceInfoByTenantIdAndKey(tenantId, ResourceType.IMAGE, key); } + @Override + public TbResourceInfo getPublicImageInfoByPublicKey(String publicKey) { + return resourceInfoDao.findPublicResourceByPublicKey(ResourceType.IMAGE, publicKey); + } + @Override public PageData getImagesByTenantId(TenantId tenantId, PageLink pageLink) { log.trace("Executing getImagesByTenantId [{}]", tenantId); @@ -602,7 +621,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic try { ImageCacheKey key = getKeyFromUrl(tenantId, url); if (key != null) { - var imageInfo = getImageInfoByTenantIdAndKey(key.getTenantId(), key.getKey()); + var imageInfo = getImageInfoByTenantIdAndKey(key.getTenantId(), key.getResourceKey()); if (imageInfo != null) { byte[] data = key.isPreview() ? getImagePreview(tenantId, imageInfo.getId()) : getImageData(tenantId, imageInfo.getId()); ImageDescriptor descriptor = getImageDescriptor(imageInfo, key.isPreview()); @@ -638,9 +657,9 @@ public class BaseImageService extends BaseResourceService implements ImageServic if (imageTenantId != null) { var parts = url.split("/"); if (parts.length == 5) { - return new ImageCacheKey(imageTenantId, parts[4], false); + return ImageCacheKey.forImage(imageTenantId, parts[4], false); } else if (parts.length == 6 && "preview".equals(parts[5])) { - return new ImageCacheKey(imageTenantId, parts[4], true); + return ImageCacheKey.forImage(imageTenantId, parts[4], true); } } return null; diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java index 7ee2f4a6b4..992f630978 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java @@ -15,14 +15,44 @@ */ package org.thingsboard.server.dao.resource; +import lombok.AccessLevel; import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.With; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.transport.TransportProtos.ImageCacheKeyProto; @Data +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) public class ImageCacheKey { private final TenantId tenantId; - private final String key; + private final String resourceKey; + @With private final boolean preview; + private final String publicKey; + + public static ImageCacheKey forImage(TenantId tenantId, String key, boolean preview) { + return new ImageCacheKey(tenantId, key, preview, null); + } + + public static ImageCacheKey forImage(TenantId tenantId, String key) { + return forImage(tenantId, key, false); + } + + public static ImageCacheKey forPublicImage(String publicKey) { + return new ImageCacheKey(null, null, false, publicKey); + } + + public ImageCacheKeyProto toProto() { + var msg = ImageCacheKeyProto.newBuilder(); + if (resourceKey != null) { + msg.setResourceKey(resourceKey); + } else { + msg.setPublicKey(publicKey); + } + return msg.build(); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java index d18ced319e..44f42391c5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java @@ -41,4 +41,9 @@ public interface TbResourceInfoDao extends Dao { List findByTenantIdAndEtagAndKeyStartingWith(TenantId tenantId, String etag, String query); TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, ResourceType resourceType, String etag); + + boolean existsByPublicKey(ResourceType resourceType, String publicKey); + + TbResourceInfo findPublicResourceByPublicKey(ResourceType resourceType, String publicKey); + } 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 d3123fe38c..501bb6b81c 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 @@ -109,4 +109,14 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao 'READ'; CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag); + +CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag); + +CREATE INDEX IF NOT EXISTS idx_resource_image_public_key ON resource(public_key) WHERE resource_type = 'IMAGE'; diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 35d1d49cda..fdd6eabc39 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -718,6 +718,8 @@ CREATE TABLE IF NOT EXISTS resource ( etag varchar, descriptor varchar, preview bytea, + is_public boolean default true, + public_key varchar(35) unique, external_id uuid, CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_key) ); From a058f865da46c2606eb8896a21f27f22526026b8 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 14 Dec 2023 17:57:41 +0200 Subject: [PATCH 02/12] Rename to publicResourceKey --- .../src/main/data/upgrade/3.6.1/schema_update.sql | 8 ++++---- .../server/controller/ImageController.java | 4 ++-- .../service/queue/DefaultTbCoreConsumerService.java | 2 +- .../service/resource/DefaultTbImageService.java | 8 ++++---- .../server/common/data/ImageExportData.java | 2 +- .../server/common/data/TbResourceInfo.java | 6 +++--- common/proto/src/main/proto/queue.proto | 2 +- .../thingsboard/server/dao/model/ModelConstants.java | 2 +- .../server/dao/model/sql/TbResourceEntity.java | 10 +++++----- .../server/dao/model/sql/TbResourceInfoEntity.java | 10 +++++----- .../server/dao/resource/BaseImageService.java | 12 ++++++------ .../server/dao/resource/ImageCacheKey.java | 4 ++-- .../server/dao/resource/TbResourceInfoDao.java | 4 ++-- .../dao/sql/resource/JpaTbResourceInfoDao.java | 8 ++++---- .../dao/sql/resource/TbResourceInfoRepository.java | 4 ++-- dao/src/main/resources/sql/schema-entities-idx.sql | 2 +- dao/src/main/resources/sql/schema-entities.sql | 2 +- 17 files changed, 45 insertions(+), 45 deletions(-) diff --git a/application/src/main/data/upgrade/3.6.1/schema_update.sql b/application/src/main/data/upgrade/3.6.1/schema_update.sql index 976c0df296..98e78df5a9 100644 --- a/application/src/main/data/upgrade/3.6.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.6.1/schema_update.sql @@ -20,10 +20,10 @@ ALTER TABLE resource ADD COLUMN IF NOT EXISTS descriptor varchar; ALTER TABLE resource ADD COLUMN IF NOT EXISTS preview bytea; ALTER TABLE resource ADD COLUMN IF NOT EXISTS external_id uuid; ALTER TABLE resource ADD COLUMN IF NOT EXISTS is_public boolean default true; -ALTER TABLE resource ADD COLUMN IF NOT EXISTS public_key varchar(35) unique; +ALTER TABLE resource ADD COLUMN IF NOT EXISTS public_resource_key varchar(32) unique; CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag); -CREATE INDEX IF NOT EXISTS idx_resource_image_public_key ON resource(public_key) WHERE resource_type = 'IMAGE'; +CREATE INDEX IF NOT EXISTS idx_resource_type_public_resource_key ON resource(resource_type, public_resource_key); CREATE OR REPLACE FUNCTION generate_resource_public_key() RETURNS text AS $$ @@ -31,7 +31,7 @@ DECLARE chars text := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; result text := ''; BEGIN - FOR i IN 1..35 LOOP + FOR i IN 1..32 LOOP result := result || substr(chars, floor(random()*62)::int + 1, 1); END LOOP; RETURN result; @@ -47,7 +47,7 @@ $$ UPDATE resource SET data = decode(base64_data, 'base64') WHERE base64_data IS NOT NULL; ALTER TABLE resource DROP COLUMN base64_data; ELSE - UPDATE resource SET public_key = generate_resource_public_key() WHERE resource_type = 'IMAGE' AND public_key IS NULL; + UPDATE resource SET public_resource_key = generate_resource_public_key() WHERE resource_type = 'IMAGE' AND public_resource_key IS NULL; END IF; END; $$; 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 7dc8501188..f3f02f5291 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -173,7 +173,7 @@ public class ImageController extends BaseController { .title(imageInfo.getTitle()) .resourceKey(imageInfo.getResourceKey()) .isPublic(imageInfo.isPublic()) - .publicKey(imageInfo.getPublicKey()) + .publicResourceKey(imageInfo.getPublicResourceKey()) .data(Base64Utils.encodeToString(data)) .build(); } @@ -195,7 +195,7 @@ public class ImageController extends BaseController { image.setResourceType(ResourceType.IMAGE); image.setResourceKey(imageData.getResourceKey()); image.setPublic(imageData.isPublic()); - image.setPublicKey(imageData.getPublicKey()); + image.setPublicResourceKey(imageData.getPublicResourceKey()); ImageDescriptor descriptor = new ImageDescriptor(); descriptor.setMediaType(imageData.getMediaType()); image.setDescriptorValue(descriptor); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 08d3b2325c..3df8876b60 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -564,7 +564,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService toEvict = new ArrayList<>(); toEvict.add(ImageCacheKey.forImage(tenantId, imageInfo.getResourceKey())); if (imageInfo.isPublic()) { - toEvict.add(ImageCacheKey.forPublicImage(imageInfo.getPublicKey())); + toEvict.add(ImageCacheKey.forPublicImage(imageInfo.getPublicResourceKey())); } evictFromCache(tenantId, toEvict); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java index d73881b70d..2e6d387dff 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java @@ -31,7 +31,7 @@ public class ImageExportData { private final String title; private final String resourceKey; private final boolean isPublic; - private final String publicKey; + private final String publicResourceKey; private final String data; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java index 559ac43385..96d258907e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java @@ -52,7 +52,7 @@ public class TbResourceInfo extends BaseData implements HasName, H @ApiModelProperty(position = 6, value = "Resource key.", example = "19_1.0", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String resourceKey; private boolean isPublic; - private String publicKey; + private String publicResourceKey; @ApiModelProperty(position = 7, value = "Resource search text.", example = "19_1.0:binaryappdatacontainer", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String searchText; @@ -82,7 +82,7 @@ public class TbResourceInfo extends BaseData implements HasName, H this.resourceKey = resourceInfo.resourceKey; this.searchText = resourceInfo.searchText; this.isPublic = resourceInfo.isPublic; - this.publicKey = resourceInfo.publicKey; + this.publicResourceKey = resourceInfo.publicResourceKey; this.etag = resourceInfo.etag; this.fileName = resourceInfo.fileName; this.descriptor = resourceInfo.descriptor != null ? resourceInfo.descriptor.deepCopy() : null; @@ -114,7 +114,7 @@ public class TbResourceInfo extends BaseData implements HasName, H public String getLink() { if (resourceType == ResourceType.IMAGE) { if (isPublic) { - return "/api/images/public/" + getPublicKey(); + return "/api/images/public/" + getPublicResourceKey(); } else { String type = (tenantId != null && tenantId.isSysTenantId()) ? "system" : "tenant"; // tenantId is null in case of export to git return "/api/images/" + type + "/" + resourceKey; diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 0c7dc82ee7..fea4848654 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -314,7 +314,7 @@ message ResourceCacheInvalidateMsg { message ImageCacheKeyProto { optional string resourceKey = 1; - optional string publicKey = 2; + optional string publicResourceKey = 2; } message LwM2MRegistrationRequestMsg { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index c4f5047354..dbbacb5358 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -502,7 +502,7 @@ public class ModelConstants { public static final String RESOURCE_DESCRIPTOR_COLUMN = "descriptor"; public static final String RESOURCE_PREVIEW_COLUMN = "preview"; public static final String RESOURCE_IS_PUBLIC_COLUMN = "is_public"; - public static final String RESOURCE_PUBLIC_KEY_COLUMN = "public_key"; + public static final String PUBLIC_RESOURCE_KEY_COLUMN = "public_resource_key"; /** * Ota Package constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java index 459e137239..eec109278c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java @@ -40,7 +40,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_IS_PUBLIC_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_PREVIEW_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_PUBLIC_KEY_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.PUBLIC_RESOURCE_KEY_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; @@ -88,8 +88,8 @@ public class TbResourceEntity extends BaseSqlEntity { @Column(name = RESOURCE_IS_PUBLIC_COLUMN) private Boolean isPublic; - @Column(name = RESOURCE_PUBLIC_KEY_COLUMN, unique = true, updatable = false) - private String publicKey; + @Column(name = PUBLIC_RESOURCE_KEY_COLUMN, unique = true, updatable = false) + private String publicResourceKey; @Column(name = EXTERNAL_ID_PROPERTY) private UUID externalId; @@ -115,7 +115,7 @@ public class TbResourceEntity extends BaseSqlEntity { this.descriptor = resource.getDescriptor(); this.preview = resource.getPreview(); this.isPublic = resource.isPublic(); - this.publicKey = resource.getPublicKey(); + this.publicResourceKey = resource.getPublicResourceKey(); this.externalId = getUuid(resource.getExternalId()); } @@ -134,7 +134,7 @@ public class TbResourceEntity extends BaseSqlEntity { resource.setDescriptor(descriptor); resource.setPreview(preview); resource.setPublic(isPublic == null || isPublic); - resource.setPublicKey(publicKey); + resource.setPublicResourceKey(publicResourceKey); resource.setExternalId(getEntityId(externalId, TbResourceId::new)); return resource; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java index 5e488b6901..c7f05d93b4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java @@ -39,7 +39,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_ETAG_COLU import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_IS_PUBLIC_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN; -import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_PUBLIC_KEY_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.PUBLIC_RESOURCE_KEY_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; @@ -81,8 +81,8 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen @Column(name = RESOURCE_IS_PUBLIC_COLUMN) private Boolean isPublic; - @Column(name = RESOURCE_PUBLIC_KEY_COLUMN, unique = true, updatable = false) - private String publicKey; + @Column(name = PUBLIC_RESOURCE_KEY_COLUMN, unique = true, updatable = false) + private String publicResourceKey; @Column(name = EXTERNAL_ID_PROPERTY) private UUID externalId; @@ -104,7 +104,7 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen this.fileName = resource.getFileName(); this.descriptor = resource.getDescriptor(); this.isPublic = resource.isPublic(); - this.publicKey = resource.getPublicKey(); + this.publicResourceKey = resource.getPublicResourceKey(); this.externalId = getUuid(resource.getExternalId()); } @@ -121,7 +121,7 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen resource.setFileName(fileName); resource.setDescriptor(descriptor); resource.setPublic(isPublic == null || isPublic); - resource.setPublicKey(publicKey); + resource.setPublicResourceKey(publicResourceKey); resource.setExternalId(getEntityId(externalId, TbResourceId::new)); return resource; } 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 e3b68b82c6..096073594f 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 @@ -151,11 +151,11 @@ public class BaseImageService extends BaseResourceService implements ImageServic image.setPreview(result.getRight()); if (image.getId() == null) { - if (StringUtils.isEmpty(image.getPublicKey())) { - image.setPublicKey(generatePublicKey()); + if (StringUtils.isEmpty(image.getPublicResourceKey())) { + image.setPublicResourceKey(generatePublicResourceKey()); } else { - if (resourceInfoDao.existsByPublicKey(ResourceType.IMAGE, image.getPublicKey())) { - image.setPublicKey(generatePublicKey()); + if (resourceInfoDao.existsByPublicResourceKey(ResourceType.IMAGE, image.getPublicResourceKey())) { + image.setPublicResourceKey(generatePublicResourceKey()); } } } @@ -204,7 +204,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic return resourceKey; } - private String generatePublicKey() { + private String generatePublicResourceKey() { return RandomStringUtils.randomAlphanumeric(35); } @@ -222,7 +222,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic @Override public TbResourceInfo getPublicImageInfoByPublicKey(String publicKey) { - return resourceInfoDao.findPublicResourceByPublicKey(ResourceType.IMAGE, publicKey); + return resourceInfoDao.findPublicResourceByKey(ResourceType.IMAGE, publicKey); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java index 992f630978..5a1dc108bd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java @@ -31,7 +31,7 @@ public class ImageCacheKey { @With private final boolean preview; - private final String publicKey; + private final String publicResourceKey; public static ImageCacheKey forImage(TenantId tenantId, String key, boolean preview) { return new ImageCacheKey(tenantId, key, preview, null); @@ -50,7 +50,7 @@ public class ImageCacheKey { if (resourceKey != null) { msg.setResourceKey(resourceKey); } else { - msg.setPublicKey(publicKey); + msg.setPublicResourceKey(publicResourceKey); } return msg.build(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java index 44f42391c5..cea4661ee7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java @@ -42,8 +42,8 @@ public interface TbResourceInfoDao extends Dao { TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, ResourceType resourceType, String etag); - boolean existsByPublicKey(ResourceType resourceType, String publicKey); + boolean existsByPublicResourceKey(ResourceType resourceType, String publicResourceKey); - TbResourceInfo findPublicResourceByPublicKey(ResourceType resourceType, String publicKey); + TbResourceInfo findPublicResourceByKey(ResourceType resourceType, String publicResourceKey); } 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 501bb6b81c..a6b0fdba75 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 @@ -111,12 +111,12 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao Date: Fri, 15 Dec 2023 12:48:30 +0200 Subject: [PATCH 03/12] Endpoint for updating image public status --- .../server/controller/ImageController.java | 21 ++++++++++++------- .../server/dao/resource/ImageService.java | 2 +- .../server/dao/resource/BaseImageService.java | 4 ++-- 3 files changed, 17 insertions(+), 10 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 f3f02f5291..16f69a2cb6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -16,8 +16,6 @@ package org.thingsboard.server.controller; import io.swagger.annotations.ApiParam; -import lombok.Builder; -import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -142,7 +140,16 @@ public class ImageController extends BaseController { @RequestBody TbResourceInfo newImageInfo) throws ThingsboardException { TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.WRITE); imageInfo.setTitle(newImageInfo.getTitle()); - imageInfo.setPublic(newImageInfo.isPublic()); + return tbImageService.save(imageInfo, getCurrentUser()); + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PutMapping(IMAGE_URL + "/public/{isPublic}") + public TbResourceInfo updateImagePublicStatus(@PathVariable String type, + @PathVariable String key, + @PathVariable boolean isPublic) throws ThingsboardException { + TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.WRITE); + imageInfo.setPublic(isPublic); return tbImageService.save(imageInfo, getCurrentUser()); } @@ -154,11 +161,11 @@ public class ImageController extends BaseController { return downloadIfChanged(type, key, etag, false); } - @GetMapping(value = "/api/images/public/{publicKey}", produces = "image/*") - public ResponseEntity downloadPublicImage(@PathVariable String publicKey, + @GetMapping(value = "/api/images/public/{publicResourceKey}", produces = "image/*") + public ResponseEntity downloadPublicImage(@PathVariable String publicResourceKey, @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception { - ImageCacheKey cacheKey = ImageCacheKey.forPublicImage(publicKey); - return downloadIfChanged(cacheKey, etag, () -> imageService.getPublicImageInfoByPublicKey(publicKey)); + ImageCacheKey cacheKey = ImageCacheKey.forPublicImage(publicResourceKey); + return downloadIfChanged(cacheKey, etag, () -> imageService.getPublicImageInfoByKey(publicResourceKey)); } @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java index e689a48c0b..bb4f23a31e 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java @@ -34,7 +34,7 @@ public interface ImageService { TbResourceInfo getImageInfoByTenantIdAndKey(TenantId tenantId, String key); - TbResourceInfo getPublicImageInfoByPublicKey(String publicKey); + TbResourceInfo getPublicImageInfoByKey(String publicResourceKey); PageData getImagesByTenantId(TenantId tenantId, PageLink pageLink); 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 096073594f..32bbd4ab6f 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 @@ -221,8 +221,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic } @Override - public TbResourceInfo getPublicImageInfoByPublicKey(String publicKey) { - return resourceInfoDao.findPublicResourceByKey(ResourceType.IMAGE, publicKey); + public TbResourceInfo getPublicImageInfoByKey(String publicResourceKey) { + return resourceInfoDao.findPublicResourceByKey(ResourceType.IMAGE, publicResourceKey); } @Override From c0524f518cb350801da4799e2cecf4aa6079cbd6 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 15 Dec 2023 12:51:08 +0200 Subject: [PATCH 04/12] Public resource key of 32 chars --- .../org/thingsboard/server/dao/resource/BaseImageService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 32bbd4ab6f..fd137b5b53 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 @@ -205,7 +205,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic } private String generatePublicResourceKey() { - return RandomStringUtils.randomAlphanumeric(35); + return RandomStringUtils.randomAlphanumeric(32); } @Override From f265c91b9ba064d5d06b389a49c31e21acf6f38f Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 15 Dec 2023 13:34:09 +0200 Subject: [PATCH 05/12] Update download image method to skip filename headers for public links --- .../org/thingsboard/server/controller/ImageController.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 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 f3f02f5291..e41c48eb84 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -277,11 +277,14 @@ public class ImageController extends BaseController { } tbImageService.putETag(cacheKey, descriptor.getEtag()); var result = ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName) - .header("x-filename", fileName) .header("Content-Type", descriptor.getMediaType()) .contentLength(data.length) .eTag(descriptor.getEtag()); + if (!cacheKey.isPublic()) { + result + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName) + .header("x-filename", fileName); + } if (systemImagesBrowserTtlInMinutes > 0 && imageInfo.getTenantId().isSysTenantId()) { result.cacheControl(CacheControl.maxAge(systemImagesBrowserTtlInMinutes, TimeUnit.MINUTES)); } else if (tenantImagesBrowserTtlInMinutes > 0 && !imageInfo.getTenantId().isSysTenantId()) { From 61aa595b20a7cb2cb2c7bffde622103fb47f33d8 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 15 Dec 2023 14:29:18 +0200 Subject: [PATCH 06/12] Tests for public images --- .../server/controller/ImageController.java | 2 +- .../controller/ImageControllerTest.java | 75 +++++++++++++++---- 2 files changed, 60 insertions(+), 17 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 16f69a2cb6..c66af2c754 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -272,7 +272,7 @@ public class ImageController extends BaseController { } } - TbResourceInfo imageInfo = imageInfoSupplier.get(); + TbResourceInfo imageInfo = checkNotNull(imageInfoSupplier.get()); String fileName = imageInfo.getFileName(); ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); byte[] data; diff --git a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java index 705e974ad2..6caebd7ca6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java @@ -22,8 +22,9 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; -import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; +import org.springframework.mock.web.MockPart; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ImageDescriptor; @@ -64,7 +65,7 @@ public class ImageControllerTest extends AbstractControllerTest { @Test public void testUploadPngImage() throws Exception { String filename = "my_png_image.png"; - TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE); + TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE, false); assertThat(imageInfo.getTitle()).isEqualTo(filename); assertThat(imageInfo.getResourceType()).isEqualTo(ResourceType.IMAGE); @@ -83,7 +84,7 @@ public class ImageControllerTest extends AbstractControllerTest { @Test public void testUploadJpegImage() throws Exception { String filename = "my_jpeg_image.jpg"; - TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE); + TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE, false); ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class); checkJpegImageDescriptor(imageDescriptor); @@ -95,7 +96,7 @@ public class ImageControllerTest extends AbstractControllerTest { @Test public void testUploadSvgImage() throws Exception { String filename = "my_svg_image.svg"; - TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/svg+xml", SVG_IMAGE); + TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/svg+xml", SVG_IMAGE, false); ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class); checkSvgImageDescriptor(imageDescriptor); @@ -107,17 +108,17 @@ public class ImageControllerTest extends AbstractControllerTest { @Test public void testUploadImageWithSameFilename() throws Exception { String filename = "my_jpeg_image.jpg"; - TbResourceInfo imageInfo1 = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE); + TbResourceInfo imageInfo1 = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE, false); assertThat(imageInfo1.getTitle()).isEqualTo(filename); assertThat(imageInfo1.getFileName()).isEqualTo(filename); assertThat(imageInfo1.getResourceKey()).isEqualTo(filename); - TbResourceInfo imageInfo2 = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE); + TbResourceInfo imageInfo2 = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE, false); assertThat(imageInfo2.getTitle()).isEqualTo(filename); assertThat(imageInfo2.getFileName()).isEqualTo(filename); assertThat(imageInfo2.getResourceKey()).isEqualTo("my_jpeg_image_(1).jpg"); - TbResourceInfo imageInfo3 = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE); + TbResourceInfo imageInfo3 = uploadImage(HttpMethod.POST, "/api/image", filename, "image/jpeg", JPEG_IMAGE, false); assertThat(imageInfo3.getTitle()).isEqualTo(filename); assertThat(imageInfo3.getFileName()).isEqualTo(filename); assertThat(imageInfo3.getResourceKey()).isEqualTo("my_jpeg_image_(2).jpg"); @@ -126,11 +127,11 @@ public class ImageControllerTest extends AbstractControllerTest { @Test public void testUpdateImage() throws Exception { String filename = "my_png_image.png"; - TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE); + TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE, false); checkPngImageDescriptor(imageInfo.getDescriptor(ImageDescriptor.class)); String newFilename = "my_jpeg_image.png"; - imageInfo = uploadImage(HttpMethod.PUT, "/api/images/tenant/" + filename, newFilename, "image/jpeg", JPEG_IMAGE); + imageInfo = uploadImage(HttpMethod.PUT, "/api/images/tenant/" + filename, newFilename, "image/jpeg", JPEG_IMAGE, false); assertThat(imageInfo.getTitle()).isEqualTo(filename); assertThat(imageInfo.getResourceKey()).isEqualTo(filename); @@ -146,7 +147,7 @@ public class ImageControllerTest extends AbstractControllerTest { @Test public void testUpdateImageInfo() throws Exception { String filename = "my_png_image.png"; - TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE); + TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE, false); ImageDescriptor imageDescriptor = imageInfo.getDescriptor(ImageDescriptor.class); assertThat(imageInfo.getTitle()).isEqualTo(filename); @@ -165,7 +166,7 @@ public class ImageControllerTest extends AbstractControllerTest { @Test public void testExportImportImage() throws Exception { String filename = "my_png_image.png"; - uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE); + uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE, false); ImageExportData exportData = doGet("/api/images/tenant/" + filename + "/export", ImageExportData.class); assertThat(exportData.getMediaType()).isEqualTo("image/png"); @@ -188,11 +189,11 @@ public class ImageControllerTest extends AbstractControllerTest { public void testGetImages() throws Exception { loginSysAdmin(); String systemImageName = "my_system_png_image.png"; - TbResourceInfo systemImage = uploadImage(HttpMethod.POST, "/api/image", systemImageName, "image/png", PNG_IMAGE); + TbResourceInfo systemImage = uploadImage(HttpMethod.POST, "/api/image", systemImageName, "image/png", PNG_IMAGE, false); loginTenantAdmin(); String tenantImageName = "my_jpeg_image.jpg"; - TbResourceInfo tenantImage = uploadImage(HttpMethod.POST, "/api/image", tenantImageName, "image/jpeg", JPEG_IMAGE); + TbResourceInfo tenantImage = uploadImage(HttpMethod.POST, "/api/image", tenantImageName, "image/jpeg", JPEG_IMAGE, false); List tenantImages = getImages(null, false, 10); assertThat(tenantImages).containsOnly(tenantImage); @@ -206,6 +207,40 @@ public class ImageControllerTest extends AbstractControllerTest { .containsOnly(tenantImage); } + @Test + public void testUploadPublicImage() throws Exception { + String filename = "my_public_image.png"; + TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE, true); + + assertThat(imageInfo.isPublic()).isTrue(); + assertThat(imageInfo.getPublicResourceKey()).hasSize(32); + assertThat(imageInfo.getLink()).isEqualTo("/api/images/public/" + imageInfo.getPublicResourceKey()); + + assertThat(downloadImage("tenant", filename)).containsExactly(PNG_IMAGE); + resetTokens(); + assertThat(downloadPublicImage(imageInfo.getPublicResourceKey())).containsExactly(PNG_IMAGE); + } + + @Test + public void testMakeImagePublic() throws Exception { + String filename = "my_public_image.png"; + TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", filename, "image/png", PNG_IMAGE, false); + assertThat(imageInfo.isPublic()).isFalse(); + String publicKey = imageInfo.getPublicResourceKey(); + assertThat(publicKey).hasSize(32); + doGet("/api/images/public/" + publicKey).andExpect(status().isNotFound()); + + imageInfo.setPublic(true); + imageInfo = doPut("/api/images/tenant/" + filename + "/public/true", imageInfo, TbResourceInfo.class); + resetTokens(); + assertThat(downloadPublicImage(publicKey)).containsExactly(PNG_IMAGE); + + loginTenantAdmin(); + imageInfo.setPublic(false); + doPut("/api/images/tenant/" + filename + "/public/false", imageInfo, TbResourceInfo.class); + doGet("/api/images/public/" + publicKey).andExpect(status().isNotFound()); + } + private void checkPngImageDescriptor(ImageDescriptor imageDescriptor) { assertThat(imageDescriptor.getMediaType()).isEqualTo("image/png"); assertThat(imageDescriptor.getWidth()).isEqualTo(200); @@ -266,10 +301,18 @@ public class ImageControllerTest extends AbstractControllerTest { .andReturn().getResponse().getContentAsByteArray(); } - private TbResourceInfo uploadImage(HttpMethod httpMethod, String url, String filename, String mediaType, byte[] content) throws Exception { + private byte[] downloadPublicImage(String publicKey) throws Exception { + return doGet("/api/images/public/" + publicKey).andExpect(status().isOk()) + .andReturn().getResponse().getContentAsByteArray(); + } + + private TbResourceInfo uploadImage(HttpMethod httpMethod, String url, String filename, String mediaType, byte[] content, boolean isPublic) throws Exception { MockMultipartFile file = new MockMultipartFile("file", filename, mediaType, content); - MockMultipartHttpServletRequestBuilder request = MockMvcRequestBuilders.multipart(httpMethod, url); - request.file(file); + MockPart publicPart = new MockPart("isPublic", String.valueOf(isPublic).getBytes()); + publicPart.getHeaders().setContentType(MediaType.APPLICATION_JSON); + var request = MockMvcRequestBuilders.multipart(httpMethod, url) + .file(file) + .part(publicPart); setJwtToken(request); return readResponse(mockMvc.perform(request).andExpect(status().isOk()), TbResourceInfo.class); } From 3ac71e9ff1ce42e835aac226e9c83341ddd24d6b Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 15 Dec 2023 16:20:25 +0200 Subject: [PATCH 07/12] Improve SVG preview generation --- .../server/dao/util/ImageUtils.java | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java b/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java index 29f7b32fae..146660a489 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java @@ -20,8 +20,10 @@ import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import com.drew.metadata.Tag; import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.With; import lombok.extern.slf4j.Slf4j; import org.apache.batik.anim.dom.SAXSVGDocumentFactory; import org.apache.batik.bridge.BridgeContext; @@ -121,7 +123,7 @@ public class ImageUtils { image.setHeight(bufferedImage.getHeight()); ProcessedImage preview = new ProcessedImage(); - int[] thumbnailDimensions = getThumbnailDimensions(image.getWidth(), image.getHeight(), thumbnailMaxDimension); + int[] thumbnailDimensions = getThumbnailDimensions(image.getWidth(), image.getHeight(), thumbnailMaxDimension, true); preview.setWidth(thumbnailDimensions[0]); preview.setHeight(thumbnailDimensions[1]); @@ -191,8 +193,18 @@ public class ImageUtils { image.setSize(data.length); PNGTranscoder transcoder = new PNGTranscoder(); - transcoder.addTranscodingHint(PNGTranscoder.KEY_MAX_WIDTH, (float) thumbnailMaxDimension); - transcoder.addTranscodingHint(PNGTranscoder.KEY_MAX_HEIGHT, (float) thumbnailMaxDimension); + if (image.getSize() < 10240) { // if SVG is smaller than 10kB (average 250x250 PNG preview size) + return withPreviewAsOriginalImage(image); + } else if (image.getSize() > 102400 && image.getWidth() != 0) { // considering SVG image detailed after 100kB + // increasing preview dimensions + thumbnailMaxDimension = 512; + int[] thumbnailDimensions = getThumbnailDimensions(image.getWidth(), image.getHeight(), thumbnailMaxDimension, false); + transcoder.addTranscodingHint(PNGTranscoder.KEY_WIDTH, (float) thumbnailDimensions[0]); + transcoder.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, (float) thumbnailDimensions[1]); + } else { + 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(); @@ -214,18 +226,16 @@ public class ImageUtils { image.setSize(data.length); image.setWidth(0); image.setHeight(0); - ProcessedImage preview = new ProcessedImage(); - preview.setMediaType(mediaType); - preview.setData(null); - preview.setSize(data.length); - preview.setWidth(0); - preview.setHeight(0); - image.setPreview(preview); - return image; + return withPreviewAsOriginalImage(image); + } + + public static ProcessedImage withPreviewAsOriginalImage(ProcessedImage originalImage) { + originalImage.setPreview(originalImage.withData(null)); + return originalImage; } - private static int[] getThumbnailDimensions(int originalWidth, int originalHeight, int maxDimension) { - if (originalWidth <= maxDimension && originalHeight <= maxDimension) { + private static int[] getThumbnailDimensions(int originalWidth, int originalHeight, int maxDimension, boolean originalIfSmaller) { + if (originalWidth <= maxDimension && originalHeight <= maxDimension && originalIfSmaller) { return new int[]{originalWidth, originalHeight}; } int thumbnailWidth; @@ -242,10 +252,13 @@ public class ImageUtils { } @Data + @AllArgsConstructor + @NoArgsConstructor public static class ProcessedImage { private String mediaType; private int width; private int height; + @With private byte[] data; private long size; private ProcessedImage preview; From c4f7c07fa33e0935b6c7438ed33d1056cd2a27ce Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 15 Dec 2023 16:37:30 +0200 Subject: [PATCH 08/12] Swagger description for image type and key --- .../server/controller/ImageController.java | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 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 c66af2c754..f4923a6c26 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -89,6 +89,10 @@ public class ImageController extends BaseController { private static final String SYSTEM_IMAGE = "system"; private static final String TENANT_IMAGE = "tenant"; + private static final String IMAGE_TYPE_PARAM_DESCRIPTION = "Type of the image: tenant or system"; + private static final String IMAGE_TYPE_PARAM_ALLOWABLE_VALUES = "tenant, system"; + private static final String IMAGE_KEY_PARAM_DESCRIPTION = "Image resource key, for example thermostats_dashboard_background.jpeg"; + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PostMapping("/api/image") public TbResourceInfo uploadImage(@RequestPart MultipartFile file, @@ -117,7 +121,9 @@ public class ImageController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PutMapping(IMAGE_URL) - public TbResourceInfo updateImage(@PathVariable String type, + public TbResourceInfo updateImage(@ApiParam(value = IMAGE_TYPE_PARAM_DESCRIPTION, allowableValues = IMAGE_TYPE_PARAM_ALLOWABLE_VALUES, required = true) + @PathVariable String type, + @ApiParam(value = IMAGE_KEY_PARAM_DESCRIPTION, required = true) @PathVariable String key, @RequestPart MultipartFile file) throws Exception { TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.WRITE); @@ -135,7 +141,9 @@ public class ImageController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PutMapping(IMAGE_URL + "/info") - public TbResourceInfo updateImageInfo(@PathVariable String type, + public TbResourceInfo updateImageInfo(@ApiParam(value = IMAGE_TYPE_PARAM_DESCRIPTION, allowableValues = IMAGE_TYPE_PARAM_ALLOWABLE_VALUES, required = true) + @PathVariable String type, + @ApiParam(value = IMAGE_KEY_PARAM_DESCRIPTION, required = true) @PathVariable String key, @RequestBody TbResourceInfo newImageInfo) throws ThingsboardException { TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.WRITE); @@ -145,7 +153,9 @@ public class ImageController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PutMapping(IMAGE_URL + "/public/{isPublic}") - public TbResourceInfo updateImagePublicStatus(@PathVariable String type, + public TbResourceInfo updateImagePublicStatus(@ApiParam(value = IMAGE_TYPE_PARAM_DESCRIPTION, allowableValues = IMAGE_TYPE_PARAM_ALLOWABLE_VALUES, required = true) + @PathVariable String type, + @ApiParam(value = IMAGE_KEY_PARAM_DESCRIPTION, required = true) @PathVariable String key, @PathVariable boolean isPublic) throws ThingsboardException { TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.WRITE); @@ -155,7 +165,9 @@ public class ImageController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = IMAGE_URL, produces = "image/*") - public ResponseEntity downloadImage(@PathVariable String type, + public ResponseEntity downloadImage(@ApiParam(value = IMAGE_TYPE_PARAM_DESCRIPTION, allowableValues = IMAGE_TYPE_PARAM_ALLOWABLE_VALUES, required = true) + @PathVariable String type, + @ApiParam(value = IMAGE_KEY_PARAM_DESCRIPTION, required = true) @PathVariable String key, @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception { return downloadIfChanged(type, key, etag, false); @@ -170,7 +182,10 @@ public class ImageController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = IMAGE_URL + "/export") - public ImageExportData exportImage(@PathVariable String type, @PathVariable String key) throws Exception { + public ImageExportData exportImage(@ApiParam(value = IMAGE_TYPE_PARAM_DESCRIPTION, allowableValues = IMAGE_TYPE_PARAM_ALLOWABLE_VALUES, required = true) + @PathVariable String type, + @ApiParam(value = IMAGE_KEY_PARAM_DESCRIPTION, required = true) + @PathVariable String key) throws Exception { TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ); ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); byte[] data = imageService.getImageData(imageInfo.getTenantId(), imageInfo.getId()); @@ -212,7 +227,9 @@ public class ImageController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = IMAGE_URL + "/preview", produces = "image/png") - public ResponseEntity downloadImagePreview(@PathVariable String type, + public ResponseEntity downloadImagePreview(@ApiParam(value = IMAGE_TYPE_PARAM_DESCRIPTION, allowableValues = IMAGE_TYPE_PARAM_ALLOWABLE_VALUES, required = true) + @PathVariable String type, + @ApiParam(value = IMAGE_KEY_PARAM_DESCRIPTION, required = true) @PathVariable String key, @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception { return downloadIfChanged(type, key, etag, true); @@ -220,7 +237,9 @@ public class ImageController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(IMAGE_URL + "/info") - public TbResourceInfo getImageInfo(@PathVariable String type, + public TbResourceInfo getImageInfo(@ApiParam(value = IMAGE_TYPE_PARAM_DESCRIPTION, allowableValues = IMAGE_TYPE_PARAM_ALLOWABLE_VALUES, required = true) + @PathVariable String type, + @ApiParam(value = IMAGE_KEY_PARAM_DESCRIPTION, required = true) @PathVariable String key) throws ThingsboardException { return checkImageInfo(type, key, Operation.READ); } @@ -251,7 +270,9 @@ public class ImageController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @DeleteMapping(IMAGE_URL) - public ResponseEntity deleteImage(@PathVariable String type, + public ResponseEntity deleteImage(@ApiParam(value = IMAGE_TYPE_PARAM_DESCRIPTION, allowableValues = IMAGE_TYPE_PARAM_ALLOWABLE_VALUES, required = true) + @PathVariable String type, + @ApiParam(value = IMAGE_KEY_PARAM_DESCRIPTION, required = true) @PathVariable String key, @RequestParam(name = "force", required = false) boolean force) throws ThingsboardException { TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.DELETE); From 562838cfdbd6da0ff565009fb345f65e239d985b Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 15 Dec 2023 17:10:33 +0200 Subject: [PATCH 09/12] UI: Add Embed Image dialog. --- .../server/common/data/TbResourceInfo.java | 16 ++-- .../server/dao/resource/ImageCacheKey.java | 4 + ui-ngx/src/app/core/http/image.service.ts | 10 +- .../image/embed-image-dialog.component.html | 60 ++++++++++++ .../image/embed-image-dialog.component.scss | 71 ++++++++++++++ .../image/embed-image-dialog.component.ts | 94 +++++++++++++++++++ .../image/image-dialog.component.html | 19 ++-- .../image/image-dialog.component.ts | 33 +++++-- .../image/image-gallery.component.html | 22 ++++- .../image/image-gallery.component.ts | 20 ++++ .../src/app/shared/models/resource.models.ts | 7 ++ ui-ngx/src/app/shared/shared.module.ts | 7 +- .../assets/locale/locale.constant-en_US.json | 12 ++- 13 files changed, 343 insertions(+), 32 deletions(-) create mode 100644 ui-ngx/src/app/shared/components/image/embed-image-dialog.component.html create mode 100644 ui-ngx/src/app/shared/components/image/embed-image-dialog.component.scss create mode 100644 ui-ngx/src/app/shared/components/image/embed-image-dialog.component.ts diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java index 96d258907e..6b485488b0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java @@ -113,12 +113,16 @@ public class TbResourceInfo extends BaseData implements HasName, H @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String getLink() { if (resourceType == ResourceType.IMAGE) { - if (isPublic) { - return "/api/images/public/" + getPublicResourceKey(); - } else { - String type = (tenantId != null && tenantId.isSysTenantId()) ? "system" : "tenant"; // tenantId is null in case of export to git - return "/api/images/" + type + "/" + resourceKey; - } + String type = (tenantId != null && tenantId.isSysTenantId()) ? "system" : "tenant"; // tenantId is null in case of export to git + return "/api/images/" + type + "/" + resourceKey; + } + return null; + } + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getPublicLink() { + if (resourceType == ResourceType.IMAGE && isPublic) { + return "/api/images/public/" + getPublicResourceKey(); } return null; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java index 5a1dc108bd..82efa3c434 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java @@ -55,4 +55,8 @@ public class ImageCacheKey { return msg.build(); } + public boolean isPublic() { + return this.publicResourceKey != null; + } + } diff --git a/ui-ngx/src/app/core/http/image.service.ts b/ui-ngx/src/app/core/http/image.service.ts index 3fcba170f4..b839e0975c 100644 --- a/ui-ngx/src/app/core/http/image.service.ts +++ b/ui-ngx/src/app/core/http/image.service.ts @@ -41,13 +41,14 @@ export class ImageService { ) { } - public uploadImage(file: File, title: string, config?: RequestConfig): Observable { + public uploadImage(file: File, title: string, isPublic = true, config?: RequestConfig): Observable { if (!config) { config = {}; } const formData = new FormData(); formData.append('file', file); formData.append('title', title); + formData.append('isPublic', isPublic ? 'true' : 'false'); return this.http.post('/api/image', formData, defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest)); } @@ -69,6 +70,13 @@ export class ImageService { imageInfo, defaultHttpOptionsFromConfig(config)); } + public updateImagePublicStatus(imageInfo: ImageResourceInfo, isPublic: boolean, config?: RequestConfig): Observable { + const type = imageResourceType(imageInfo); + const key = encodeURIComponent(imageInfo.resourceKey); + return this.http.put(`${IMAGES_URL_PREFIX}/${type}/${key}/public/${isPublic}`, + imageInfo, defaultHttpOptionsFromConfig(config)); + } + public getImages(pageLink: PageLink, includeSystemImages = false, config?: RequestConfig): Observable> { return this.http.get>( `${IMAGES_URL_PREFIX}${pageLink.toQuery()}&includeSystemImages=${includeSystemImages}`, diff --git a/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.html b/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.html new file mode 100644 index 0000000000..bd12430e98 --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.html @@ -0,0 +1,60 @@ + + +

{{ 'image.embed-image' | translate }}

+ + +
+ + +
+
+
+ + + +
+ +
{{ 'image.embed-to-html' | translate }}
+
+
+
image.embed-to-html
+
+
+ +
+ +
+
+
+
+
image.embed-to-angular-template
+
+ +
+
diff --git a/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.scss b/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.scss new file mode 100644 index 0000000000..b769de7840 --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.scss @@ -0,0 +1,71 @@ +/** + * 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. + */ +:host { + .mat-mdc-dialog-content { + display: flex; + flex-direction: column; + gap: 16px; + .tb-embed-image-text { + font-size: 14px; + font-weight: 400; + line-height: 20px; + color: rgba(0,0,0,0.54); + letter-spacing: 0.2px; + } + .tb-form-panel-title { + color: rgba(0, 0, 0, 0.87); + } + } +} + +:host ::ng-deep { + .tb-markdown-view { + max-width: 700px; + .tb-embed-image-code { + .code-wrapper { + padding: 0; + pre[class*=language-] { + margin: 0; + padding: 9px 38px 9px 16px; + } + code[class*="language-"], pre[class*="language-"] { + font-size: 12px; + overflow: hidden; + white-space: normal; + word-break: break-word; + } + button.clipboard-btn { + right: 0; + height: 36px; + p, div { + background: transparent; + } + p { + margin: 0; + padding: 6px; + font-size: 14px; + } + div { + top: 0; + padding: 8px; + height: 38px; + width: 38px; + } + } + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.ts b/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.ts new file mode 100644 index 0000000000..b2c1d19e1f --- /dev/null +++ b/ui-ngx/src/app/shared/components/image/embed-image-dialog.component.ts @@ -0,0 +1,94 @@ +/// +/// 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. +/// + +import { ImageResourceInfo } from '@shared/models/resource.models'; +import { Component, Inject, OnInit } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { ImageService } from '@core/http/image.service'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormControl, UntypedFormBuilder } from '@angular/forms'; + +export interface EmbedImageDialogData { + readonly: boolean; + image: ImageResourceInfo; +} + +@Component({ + selector: 'tb-embed-image-dialog', + templateUrl: './embed-image-dialog.component.html', + styleUrls: ['./embed-image-dialog.component.scss'] +}) +export class EmbedImageDialogComponent extends + DialogComponent implements OnInit { + + image = this.data.image; + + readonly = this.data.readonly; + + imageChanged = false; + + publicStatusControl = new FormControl(this.image.public); + + constructor(protected store: Store, + protected router: Router, + private imageService: ImageService, + @Inject(MAT_DIALOG_DATA) private data: EmbedImageDialogData, + public dialogRef: MatDialogRef, + public fb: UntypedFormBuilder) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + if (!this.readonly) { + this.publicStatusControl.valueChanges.subscribe( + (isPublic) => { + this.updateImagePublicStatus(isPublic); + } + ); + } + } + + cancel(): void { + this.dialogRef.close(this.imageChanged ? this.image : null); + } + + embedToHtmlCode(): string { + return '```html\n' + + ''+this.image.title.replace(/' + + '{:copy-code}\n' + + '```'; + } + + embedToAngularTemplateCode(): string { + return '```html\n' + + '' + + '{:copy-code}\n' + + '```'; + } + + private updateImagePublicStatus(isPublic: boolean): void { + this.imageService.updateImagePublicStatus(this.image, isPublic).subscribe( + (image) => { + this.image = image; + this.imageChanged = true; + } + ); + } + +} diff --git a/ui-ngx/src/app/shared/components/image/image-dialog.component.html b/ui-ngx/src/app/shared/components/image/image-dialog.component.html index 05bc4e943c..5341c94ad5 100644 --- a/ui-ngx/src/app/shared/components/image/image-dialog.component.html +++ b/ui-ngx/src/app/shared/components/image/image-dialog.component.html @@ -56,18 +56,6 @@ check - - image.link - - - -
@@ -86,6 +74,13 @@ (click)="exportImage($event)"> mdi:file-export +
+ + +