ImageController draft

This commit is contained in:
ViacheslavKlimov 2023-11-14 12:03:26 +02:00
parent 7489b87fa5
commit 7d4729451a
14 changed files with 382 additions and 39 deletions

View File

@ -0,0 +1,184 @@
/**
* 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.controller;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException;
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.dao.resource.ImageService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.resource.TbImageService;
import org.thingsboard.server.service.security.permission.Operation;
import java.util.function.Supplier;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
@Slf4j
@RestController
@TbCoreComponent
@RequiredArgsConstructor
public class ImageController extends BaseController {
private final ImageService imageService;
private final TbImageService tbImageService;
private static final String IMAGE_URL = "/api/images/{type}/{key}";
private static final String SYSTEM_IMAGE = "system";
private static final String TENANT_IMAGE = "tenant";
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping("/api/image")
public TbResourceInfo uploadImage(MultipartFile file) {
// imageService.saveImage()
return null;
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@PutMapping(IMAGE_URL)
public TbResourceInfo updateImage(MultipartFile file) {
return null;
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@PutMapping(IMAGE_URL + "/info")
public TbResourceInfo updateImageInfo(@RequestBody TbResourceInfo imageInfo) {
return null;
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = IMAGE_URL, produces = "image/*")
public ResponseEntity<ByteArrayResource> downloadImage(@PathVariable String type,
@PathVariable String key,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
TenantId tenantId = getTenantId(type);
TbResourceInfo imageInfo = imageService.getImageInfoByTenantIdAndKey(tenantId, key);
return downloadIfChanged(etag, imageInfo, () -> imageService.getImageData(tenantId, imageInfo.getId()), imageInfo.getMediaType());
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = IMAGE_URL + "/preview", produces = "image/png")
public ResponseEntity<ByteArrayResource> downloadImagePreview(@PathVariable String type,
@PathVariable String key,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
TenantId tenantId = getTenantId(type);
TbResourceInfo imageInfo = imageService.getImageInfoByTenantIdAndKey(tenantId, key);
return downloadIfChanged(etag, imageInfo, () -> imageService.getImagePreview(tenantId, imageInfo.getId()), imageInfo.getMediaType());
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(IMAGE_URL + "/info")
public TbResourceInfo getImageInfo(@PathVariable String type,
@PathVariable String key) throws ThingsboardException {
TenantId tenantId = getTenantId(type);
TbResourceInfo imageInfo = imageService.getImageInfoByTenantIdAndKey(tenantId, key);
return checkEntity(getCurrentUser(), imageInfo, Operation.READ);
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping("/images")
public PageData<TbResourceInfo> getImages(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = RESOURCE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
TenantId tenantId = getTenantId();
if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN) {
return checkNotNull(imageService.getImagesByTenantId(tenantId, pageLink));
} else {
return checkNotNull(imageService.getAllImagesByTenantId(tenantId, pageLink));
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@DeleteMapping(IMAGE_URL)
public void deleteImage(@PathVariable String type,
@PathVariable String key) throws ThingsboardException {
TenantId tenantId = getTenantId(type);
TbResourceInfo imageInfo = imageService.getImageInfoByTenantIdAndKey(tenantId, key);
checkEntity(getCurrentUser(), imageInfo, Operation.DELETE);
tbImageService.delete(imageInfo, getCurrentUser());
}
private ResponseEntity<ByteArrayResource> downloadIfChanged(String etag, TbResourceInfo resourceInfo,
Supplier<byte[]> dataSupplier, String mediaType) throws ThingsboardException {
checkEntity(getCurrentUser(), resourceInfo, Operation.READ);
if (etag != null) {
if (etag.equals(resourceInfo.getEtag())) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
.eTag(etag)
.build();
}
}
byte[] data = dataSupplier.get();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + resourceInfo.getFileName())
.header("x-filename", resourceInfo.getFileName())
.contentLength(data.length)
.header("Content-Type", mediaType)
.cacheControl(CacheControl.noCache())
.eTag(resourceInfo.getEtag())
.body(new ByteArrayResource(data));
}
private TenantId getTenantId(String imageType) throws ThingsboardException {
TenantId tenantId;
if (imageType.equals(TENANT_IMAGE)) {
tenantId = getTenantId();
} else if (imageType.equals(SYSTEM_IMAGE)) {
tenantId = TenantId.SYS_TENANT_ID;
} else {
throw new IllegalArgumentException("Invalid image URL");
}
return tenantId;
}
}

View File

@ -54,7 +54,10 @@ import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.Base64;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
@ -224,9 +227,14 @@ public class TbResourceController extends BaseController {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
TbResourceInfoFilter.TbResourceInfoFilterBuilder filter = TbResourceInfoFilter.builder();
filter.tenantId(getTenantId());
Set<ResourceType> resourceTypes = new HashSet<>();
if (StringUtils.isNotEmpty(resourceType)) {
filter.resourceType(ResourceType.valueOf(resourceType));
resourceTypes.add(ResourceType.valueOf(resourceType));
} else {
Collections.addAll(resourceTypes, ResourceType.values());
resourceTypes.remove(ResourceType.IMAGE);
}
filter.resourceTypes(resourceTypes);
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
return checkNotNull(resourceService.findTenantResourcesByTenantId(filter.build(), pageLink));
} else {

View File

@ -0,0 +1,30 @@
package org.thingsboard.server.service.resource;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.queue.util.TbCoreComponent;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class DefaultTbImageService implements TbImageService {
private final ImageService imageService;
@Override
public TbResource save(TbResourceInfo imageInfo, MultipartFile imageFile, User user) {
return null;
}
@Override
public void delete(TbResourceInfo imageInfo, User user) {
}
}

View File

@ -0,0 +1,14 @@
package org.thingsboard.server.service.resource;
import org.springframework.web.multipart.MultipartFile;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.User;
public interface TbImageService {
TbResource save(TbResourceInfo imageInfo, MultipartFile imageFile, User user);
void delete(TbResourceInfo imageInfo, User user);
}

View File

@ -0,0 +1,28 @@
package org.thingsboard.server.dao.resource;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
public interface ImageService {
TbResourceInfo saveImage(TbResource image);
TbResourceInfo saveImageInfo(TbResourceInfo imageInfo);
TbResourceInfo getImageInfoByTenantIdAndKey(TenantId tenantId, String key);
PageData<TbResourceInfo> getImagesByTenantId(TenantId tenantId, PageLink pageLink);
PageData<TbResourceInfo> getAllImagesByTenantId(TenantId tenantId, PageLink pageLink);
byte[] getImageData(TenantId tenantId, TbResourceId imageId);
byte[] getImagePreview(TenantId tenantId, TbResourceId imageId);
String getImageLink(TbResourceInfo imageInfo);
}

View File

@ -62,6 +62,4 @@ public interface ResourceService extends EntityDaoService {
List<TbResourceInfo> findByTenantIdAndDataAndKeyStartingWith(TenantId tenantId, byte[] data, String query);
String getResourceLink(TbResourceInfo resourceInfo);
}

View File

@ -19,11 +19,13 @@ import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.Set;
@Data
@Builder
public class TbResourceInfoFilter {
private TenantId tenantId;
private ResourceType resourceType;
private Set<ResourceType> resourceTypes;
}

View File

@ -0,0 +1,84 @@
package org.thingsboard.server.dao.resource;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.service.validator.ResourceDataValidator;
import java.util.Set;
@Service
public class BaseImageService extends BaseResourceService implements ImageService {
public BaseImageService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, ResourceDataValidator resourceValidator) {
super(resourceDao, resourceInfoDao, resourceValidator);
}
@Override
public TbResourceInfo saveImage(TbResource image) {
resourceValidator.validate(image, TbResourceInfo::getTenantId);
if (image.getData() != null) {
}
// generate preview, etc.
return saveResource(image, false);
}
@Override
public TbResourceInfo saveImageInfo(TbResourceInfo imageInfo) {
return null;
}
@Override
public TbResourceInfo getImageInfoByTenantIdAndKey(TenantId tenantId, String key) {
return findResourceInfoByTenantIdAndKey(tenantId, ResourceType.IMAGE, key);
}
@Override
public PageData<TbResourceInfo> getImagesByTenantId(TenantId tenantId, PageLink pageLink) {
TbResourceInfoFilter filter = TbResourceInfoFilter.builder()
.tenantId(tenantId)
.resourceTypes(Set.of(ResourceType.IMAGE))
.build();
return findTenantResourcesByTenantId(filter, pageLink);
}
@Override
public PageData<TbResourceInfo> getAllImagesByTenantId(TenantId tenantId, PageLink pageLink) {
TbResourceInfoFilter filter = TbResourceInfoFilter.builder()
.tenantId(tenantId)
.resourceTypes(Set.of(ResourceType.IMAGE))
.build();
return findAllTenantResourcesByTenantId(filter, pageLink);
}
@Override
public byte[] getImageData(TenantId tenantId, TbResourceId imageId) {
return resourceDao.getResourceData(tenantId, imageId);
}
@Override
public byte[] getImagePreview(TenantId tenantId, TbResourceId imageId) {
return new byte[0];
}
@Override
public String getImageLink(TbResourceInfo imageInfo) {
String link = "/api/images/";
if (imageInfo.getTenantId().isSysTenantId()) {
link += "system/";
} else {
link += "tenant/";
}
link += imageInfo.getResourceKey();
return link;
}
}

View File

@ -57,9 +57,9 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
public class BaseResourceService extends AbstractCachedEntityService<ResourceInfoCacheKey, TbResourceInfo, ResourceInfoEvictEvent> implements ResourceService {
public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId ";
private final TbResourceDao resourceDao;
private final TbResourceInfoDao resourceInfoDao;
private final ResourceDataValidator resourceValidator;
protected final TbResourceDao resourceDao;
protected final TbResourceInfoDao resourceInfoDao;
protected final ResourceDataValidator resourceValidator;
@Override
public TbResource saveResource(TbResource resource, boolean doValidate) {
@ -67,20 +67,10 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
if (doValidate) {
resourceValidator.validate(resource, TbResourceInfo::getTenantId);
}
TenantId tenantId = resource.getTenantId();
TbResourceId resourceId = resource.getId();
if (resourceId == null) {
UUID uuid = Uuids.timeBased();
resource.setId(new TbResourceId(uuid));
resource.setCreatedTime(Uuids.unixTimestamp(uuid));
}
if (resource.getData() != null) {
resource.setEtag(calculateEtag(resource.getData()));
if (resource.getResourceType() == ResourceType.IMAGE) {
// FIXME: skip SVG (?)
}
}
try {
TbResource saved;
@ -225,17 +215,7 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
return resourceInfoDao.findByTenantIdAndEtagAndKeyStartingWith(tenantId, etag, query);
}
@Override
public String getResourceLink(TbResourceInfo resourceInfo) {
String link = "/api/images/";
if (resourceInfo.getTenantId().isSysTenantId()) {
link += "system/";
}
link += resourceInfo.getResourceKey();
return link;
}
private String calculateEtag(byte[] data) {
protected String calculateEtag(byte[] data) {
return Hashing.sha256().hashBytes(data).toString();
}

View File

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.resource;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -39,4 +40,7 @@ public interface TbResourceDao extends Dao<TbResource>, TenantEntityWithDataDao
ResourceType resourceType,
String[] objectIds,
String searchText);
byte[] getResourceData(TenantId tenantId, TbResourceId resourceId);
}

View File

@ -21,6 +21,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -31,7 +32,6 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
@Slf4j
@ -94,6 +94,11 @@ public class JpaTbResourceDao extends JpaAbstractDao<TbResourceEntity, TbResourc
resourceType.name(), objectIds));
}
@Override
public byte[] getResourceData(TenantId tenantId, TbResourceId resourceId) {
return resourceRepository.getDataById(resourceId.getId());
}
@Override
public Long sumDataSizeByTenantId(TenantId tenantId) {
return resourceRepository.sumDataSizeByTenantId(tenantId.getId());

View File

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.TbResourceInfoFilter;
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.util.CollectionsUtil;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.TbResourceInfoEntity;
import org.thingsboard.server.dao.resource.TbResourceInfoDao;
@ -34,6 +35,7 @@ import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
@Slf4j
@Component
@ -55,23 +57,22 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao<TbResourceInfoEntity, T
@Override
public PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) {
ResourceType resourceType = filter.getResourceType();
return DaoUtil.toPageData(resourceInfoRepository
.findAllTenantResourcesByTenantId(
filter.getTenantId().getId(),
TenantId.NULL_UUID,
resourceType == null ? null : resourceType.name(),
filter.getTenantId().getId(), TenantId.NULL_UUID,
CollectionsUtil.isNotEmpty(filter.getResourceTypes()) ? filter.getResourceTypes()
.stream().map(Enum::name).collect(Collectors.toList()) : null,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<TbResourceInfo> findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) {
ResourceType resourceType = filter.getResourceType();
return DaoUtil.toPageData(resourceInfoRepository
.findTenantResourcesByTenantId(
filter.getTenantId().getId(),
resourceType == null ? null : resourceType.name(),
CollectionsUtil.isNotEmpty(filter.getResourceTypes()) ? filter.getResourceTypes()
.stream().map(Enum::name).collect(Collectors.toList()) : null,
pageLink.getTextSearch(),
DaoUtil.toPageable(pageLink)));
}

View File

@ -22,6 +22,7 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.dao.model.sql.TbResourceInfoEntity;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
@ -36,19 +37,19 @@ public interface TbResourceInfoRepository extends JpaRepository<TbResourceInfoEn
"WHERE sr.tenantId = :tenantId " +
"AND tr.resourceType = sr.resourceType " +
"AND tr.resourceKey = sr.resourceKey)))" +
"AND (:resourceType IS NULL OR tr.resourceType = :resourceType)")
"AND (:resourceTypes IS NULL OR tr.resourceType IN :resourceTypes)")
Page<TbResourceInfoEntity> findAllTenantResourcesByTenantId(@Param("tenantId") UUID tenantId,
@Param("systemAdminId") UUID sysadminId,
@Param("resourceType") String resourceType,
@Param("resourceTypes") Collection<String> resourceTypes,
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT ri FROM TbResourceInfoEntity ri WHERE " +
"ri.tenantId = :tenantId " +
"AND (:resourceType IS NULL OR ri.resourceType = :resourceType)" +
"AND (:resourceTypes IS NULL OR ri.resourceType IN :resourceTypes)" +
"AND (:searchText IS NULL OR ilike(ri.title, CONCAT('%', :searchText, '%')) = true)")
Page<TbResourceInfoEntity> findTenantResourcesByTenantId(@Param("tenantId") UUID tenantId,
@Param("resourceType") String resourceType,
@Param("resourceTypes") Collection<String> resourceTypes,
@Param("searchText") String searchText,
Pageable pageable);

View File

@ -80,4 +80,8 @@ public interface TbResourceRepository extends JpaRepository<TbResourceEntity, UU
@Query(value = "SELECT COALESCE(SUM(LENGTH(r.data)), 0) FROM resource r WHERE r.tenant_id = :tenantId", nativeQuery = true)
Long sumDataSizeByTenantId(@Param("tenantId") UUID tenantId);
@Query("SELECT r.data FROM TbResourceEntity r WHERE r.id = :id")
byte[] getDataById(@Param("id") UUID id);
}