Public images

This commit is contained in:
ViacheslavKlimov 2023-12-14 16:24:36 +02:00
parent f4761519b6
commit 36a89bea91
22 changed files with 266 additions and 50 deletions

View File

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

View File

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

View File

@ -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<ByteArrayResource> 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<ByteArrayResource> downloadIfChanged(String type, String key, String etag, boolean preview) throws ThingsboardException, JsonProcessingException {
ImageCacheKey cacheKey = new ImageCacheKey(getTenantId(type), key, preview);
private ResponseEntity<ByteArrayResource> 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<ByteArrayResource> downloadIfChanged(ImageCacheKey cacheKey, String etag, ThrowingSupplier<TbResourceInfo> 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()

View File

@ -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.

View File

@ -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<ToCore
private void forwardToResourceService(TransportProtos.ResourceCacheInvalidateMsg msg, TbCallback callback) {
var tenantId = new TenantId(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB()));
imageService.evictETag(new ImageCacheKey(tenantId, msg.getResourceKey(), false));
imageService.evictETag(new ImageCacheKey(tenantId, msg.getResourceKey(), true));
msg.getKeysList().stream().map(cacheKeyProto -> {
if (cacheKeyProto.hasResourceKey()) {
return ImageCacheKey.forImage(tenantId, cacheKeyProto.getResourceKey());
} else {
return ImageCacheKey.forPublicImage(cacheKeyProto.getPublicKey());
}
}).forEach(imageService::evictETags);
callback.onSuccess();
}

View File

@ -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<ImageCacheKey> 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<ImageCacheKey> 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<ImageCacheKey> 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());
}
}

View File

@ -33,6 +33,6 @@ public interface TbImageService {
void putETag(ImageCacheKey imageCacheKey, String etag);
void evictETag(ImageCacheKey imageCacheKey);
void evictETags(ImageCacheKey imageCacheKey);
}

View File

@ -34,6 +34,8 @@ public interface ImageService {
TbResourceInfo getImageInfoByTenantIdAndKey(TenantId tenantId, String key);
TbResourceInfo getPublicImageInfoByPublicKey(String publicKey);
PageData<TbResourceInfo> getImagesByTenantId(TenantId tenantId, PageLink pageLink);
PageData<TbResourceInfo> getAllImagesByTenantId(TenantId tenantId, PageLink pageLink);

View File

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

View File

@ -51,6 +51,8 @@ public class TbResourceInfo extends BaseData<TbResourceId> 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<TbResourceId> 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<TbResourceId> 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;
}

View File

@ -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> {
T get() throws Exception;
}

View File

@ -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 {

View File

@ -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.

View File

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

View File

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

View File

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

View File

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

View File

@ -41,4 +41,9 @@ public interface TbResourceInfoDao extends Dao<TbResourceInfo> {
List<TbResourceInfo> 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);
}

View File

@ -109,4 +109,14 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao<TbResourceInfoEntity, T
public TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, ResourceType resourceType, String etag) {
return DaoUtil.getData(resourceInfoRepository.findSystemOrTenantImageByEtag(tenantId.getId(), resourceType.name(), etag));
}
@Override
public boolean existsByPublicKey(ResourceType resourceType, String publicKey) {
return resourceInfoRepository.existsByResourceTypeAndPublicKey(resourceType.name(), publicKey);
}
@Override
public TbResourceInfo findPublicResourceByPublicKey(ResourceType resourceType, String publicKey) {
return DaoUtil.getData(resourceInfoRepository.findByResourceTypeAndPublicKeyAndIsPublicTrue(resourceType.name(), publicKey));
}
}

View File

@ -70,4 +70,9 @@ public interface TbResourceInfoRepository extends JpaRepository<TbResourceInfoEn
TbResourceInfoEntity findSystemOrTenantImageByEtag(@Param("tenantId") UUID tenantId,
@Param("resourceType") String resourceType,
@Param("etag") String etag);
boolean existsByResourceTypeAndPublicKey(String resourceType, String publicKey);
TbResourceInfoEntity findByResourceTypeAndPublicKeyAndIsPublicTrue(String resourceType, String publicKey);
}

View File

@ -125,3 +125,7 @@ CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_created_time ON notific
CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> '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';

View File

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