Browse Source

Merge pull request #14205 from AndriiLandiak/resource-upload

Fixed error when uploading resources approximately larger than 15 MB
pull/14469/head
Viacheslav Klimov 6 months ago
committed by GitHub
parent
commit
dea0bd83a6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 70
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  2. 2
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  3. 2
      application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java
  4. 4
      application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java
  5. 69
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  6. 2
      common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java
  7. 5
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  8. 9
      dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java
  9. 50
      ui-ngx/src/app/core/http/resource.service.ts
  10. 5
      ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts
  11. 2
      ui-ngx/src/app/modules/home/components/resources/resources-library.component.html
  12. 2
      ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts
  13. 19
      ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts
  14. 2
      ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html
  15. 8
      ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts
  16. 26
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts
  17. 52
      ui-ngx/src/app/shared/components/file-input.component.ts
  18. 2
      ui-ngx/src/app/shared/models/resource.models.ts

70
application/src/main/java/org/thingsboard/server/controller/TbResourceController.java

@ -32,12 +32,16 @@ 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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
@ -215,6 +219,7 @@ public class TbResourceController extends BaseController {
"\n\nResource combination of the title with the key is unique in the scope of tenant. " +
"Remove 'id', 'tenantId' from the request body example (below) to create new Resource entity." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@Deprecated // resource should be save or update with an upload request
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PostMapping(value = "/resource")
public TbResourceInfo saveResource(@Parameter(description = "A JSON value representing the Resource.")
@ -224,6 +229,71 @@ public class TbResourceController extends BaseController {
return tbResourceService.save(resource, getCurrentUser());
}
@ApiOperation(value = "Upload Resource via Multipart File (uploadResource)",
notes = "Create the Resource using multipart file upload. " +
"\n\nResource combination of the title with the key is unique in the scope of tenant. " +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PostMapping(value = "/resource/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public TbResourceInfo uploadResource(@Parameter(description = "Resource title.", example = "Title")
@RequestPart(name = "title", required = false) String title,
@Parameter(description = "Resource type.")
@RequestPart(name = "resourceType") String resourceTypeStr,
@Parameter(description = "Resource descriptor (JSON).")
@RequestPart(name = "descriptor", required = false) String descriptor,
@Parameter(description = "Resource sub type.")
@RequestPart(name = "resourceSubType", required = false) String resourceSubTypeStr,
@Parameter(description = "Resource file.")
@RequestPart MultipartFile file) throws Exception {
TbResource resource = new TbResource();
resource.setTenantId(getTenantId());
resource.setTitle(StringUtils.isNotEmpty(title) ? title : file.getOriginalFilename());
ResourceType resourceType = ResourceType.valueOf(resourceTypeStr);
resource.setResourceType(resourceType);
if (StringUtils.isNotEmpty(descriptor)) {
resource.setDescriptor(JacksonUtil.toJsonNode(descriptor));
} else {
String mediaType = resourceType.getMediaType() != null ? resourceType.getMediaType() : file.getContentType();
resource.setDescriptor(JacksonUtil.newObjectNode().put("mediaType", mediaType));
}
if (StringUtils.isNotEmpty(resourceSubTypeStr)) {
resource.setResourceSubType(ResourceSubType.valueOf(resourceSubTypeStr));
}
resource.setFileName(file.getOriginalFilename());
resource.setData(file.getBytes());
checkEntity(resource.getId(), resource, Resource.TB_RESOURCE);
return tbResourceService.save(resource, getCurrentUser());
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PutMapping(value = "/resource/{id}/data", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public TbResourceInfo updateResourceData(@Parameter(description = "Unique identifier of the Resource to update", required = true)
@PathVariable UUID id,
@Parameter(description = "Resource file.")
@RequestPart MultipartFile file) throws Exception {
TbResourceId tbResourceId = new TbResourceId(id);
TbResource resource = checkResourceId(tbResourceId, Operation.WRITE);
resource.setFileName(file.getOriginalFilename());
resource.setData(file.getBytes());
return tbResourceService.save(resource, getCurrentUser());
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PutMapping("/resource/{id}/info")
public TbResourceInfo updateResourceInfo(@Parameter(description = "Unique identifier of the Resource to update", required = true)
@PathVariable UUID id,
@Parameter(description = "A JSON value representing the Resource Info.")
@RequestBody TbResourceInfo resourceInfo) throws Exception {
TbResourceId tbResourceId = new TbResourceId(id);
checkResourceInfoId(tbResourceId, Operation.WRITE);
resourceInfo.setId(tbResourceId);
TbResource resource = new TbResource(resourceInfo);
return tbResourceService.save(resource, getCurrentUser());
}
@ApiOperation(value = "Get Resource Infos (getResources)",
notes = "Returns a page of Resource Info objects owned by tenant or sysadmin. " +
PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)

2
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java

@ -75,7 +75,7 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
ActionType actionType = resource.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = resource.getTenantId();
try {
if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType())) {
if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType()) && resource.getId() == null) {
toLwm2mResource(resource);
} else if (resource.getResourceKey() == null) {
resource.setResourceKey(resource.getFileName());

2
application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java

@ -19,8 +19,8 @@ import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceDeleteResult;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;

4
application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java

@ -73,7 +73,7 @@ public class LwM2mObjectModelUtils {
try {
List<ObjectModel> objectModels =
ddfFileParser.parse(new ByteArrayInputStream(resource.getData()), resource.getSearchText());
if (objectModels.size() == 0) {
if (objectModels.isEmpty()) {
return null;
} else {
ObjectModel obj = objectModels.get(0);
@ -95,7 +95,7 @@ public class LwM2mObjectModelUtils {
resources.add(lwM2MResourceObserve);
}
});
if (isSave || resources.size() > 0) {
if (isSave || !resources.isEmpty()) {
instance.setResources(resources.toArray(LwM2mResourceObserve[]::new));
lwM2mObject.setInstances(new LwM2mInstance[]{instance});
return lwM2mObject;

69
application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java

@ -25,11 +25,12 @@ import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockPart;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType;
@ -47,15 +48,14 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
@ -64,7 +64,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@DaoSqlTest
public class TbResourceControllerTest extends AbstractControllerTest {
private IdComparator<TbResourceInfo> idComparator = new IdComparator<>();
private final IdComparator<TbResourceInfo> idComparator = new IdComparator<>();
private static final String DEFAULT_FILE_NAME = "test.jks";
private static final String DEFAULT_FILE_NAME_2 = "test2.jks";
@ -126,13 +126,10 @@ public class TbResourceControllerTest extends AbstractControllerTest {
Assert.assertEquals(DEFAULT_FILE_NAME, savedResource.getResourceKey());
Assert.assertArrayEquals(resource.getData(), download(savedResource.getId()));
TbResource foundResource = doGet("/api/resource/" + savedResource.getId().getId().toString(), TbResource.class);
foundResource.setTitle("My new resource");
foundResource.setData(null);
savedResource = save(foundResource);
Assert.assertEquals(foundResource.getTitle(), savedResource.getTitle());
String resourceTitle = "My new resource";
savedResource.setTitle(resourceTitle);
savedResource = doPut("/api/resource/" + savedResource.getUuidId() + "/info", savedResource, TbResourceInfo.class);
assertThat(savedResource.getTitle()).isEqualTo(resourceTitle);
testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(savedResource, savedResource.getId(), savedResource.getId(),
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
@ -501,8 +498,8 @@ public class TbResourceControllerTest extends AbstractControllerTest {
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.ADDED, cntEntity, cntEntity, cntEntity);
Collections.sort(resources, idComparator);
Collections.sort(loadedResources, idComparator);
resources.sort(idComparator);
loadedResources.sort(idComparator);
Assert.assertEquals(resources, loadedResources);
}
@ -549,8 +546,8 @@ public class TbResourceControllerTest extends AbstractControllerTest {
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED,
jksCntEntity + lwm2mCntEntity, jksCntEntity + lwm2mCntEntity, jksCntEntity + lwm2mCntEntity);
Collections.sort(resources, idComparator);
Collections.sort(loadedResources, idComparator);
resources.sort(idComparator);
loadedResources.sort(idComparator);
Assert.assertEquals(resources, loadedResources);
}
@ -581,8 +578,8 @@ public class TbResourceControllerTest extends AbstractControllerTest {
}
} while (pageData.hasNext());
Collections.sort(resources, idComparator);
Collections.sort(loadedResources, idComparator);
resources.sort(idComparator);
loadedResources.sort(idComparator);
Assert.assertEquals(resources, loadedResources);
@ -654,8 +651,8 @@ public class TbResourceControllerTest extends AbstractControllerTest {
}
} while (pageData.hasNext());
Collections.sort(jksResources, idComparator);
Collections.sort(loadedResources, idComparator);
jksResources.sort(idComparator);
loadedResources.sort(idComparator);
Assert.assertEquals(jksResources, loadedResources);
@ -736,8 +733,8 @@ public class TbResourceControllerTest extends AbstractControllerTest {
}
} while (pageData.hasNext());
Collections.sort(expectedResources, idComparator);
Collections.sort(loadedResources, idComparator);
expectedResources.sort(idComparator);
loadedResources.sort(idComparator);
Assert.assertEquals(expectedResources, loadedResources);
@ -770,7 +767,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
MockHttpServletResponse response = resultActions.andReturn().getResponse();
String eTag = response.getHeader("ETag");
Assert.assertNotNull(eTag);
Assert.assertEquals(Base64.getEncoder().encodeToString(response.getContentAsByteArray()), TEST_DATA);
Assert.assertEquals(TEST_DATA, Base64.getEncoder().encodeToString(response.getContentAsByteArray()));
//download with if-none-match header
HttpHeaders headers = new HttpHeaders();
@ -814,7 +811,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
MockHttpServletResponse response = resultActions.andReturn().getResponse();
String eTag = response.getHeader("ETag");
Assert.assertNotNull(eTag);
Assert.assertEquals(Base64.getEncoder().encodeToString(response.getContentAsByteArray()), TEST_DATA);
Assert.assertEquals(TEST_DATA, Base64.getEncoder().encodeToString(response.getContentAsByteArray()));
//download with if-none-match header
HttpHeaders headers = new HttpHeaders();
@ -859,10 +856,10 @@ public class TbResourceControllerTest extends AbstractControllerTest {
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("can't be updated")));
foundResource.setData(null);
foundResource.setTitle("Updated resource");
savedResource = doPost("/api/resource", foundResource, TbResource.class);
assertThat(savedResource.getTitle()).isEqualTo("Updated resource");
String resourceTitle = "Updated resource";
savedResource.setTitle(resourceTitle);
savedResource = doPut("/api/resource/" + savedResource.getUuidId() + "/info", savedResource, TbResourceInfo.class);
assertThat(savedResource.getTitle()).isEqualTo(resourceTitle);
assertThat(savedResource.getFileName()).isEqualTo(resource.getFileName());
assertThat(savedResource.getEtag()).isEqualTo(resource.getEtag());
assertThat(download(savedResource.getId())).asBase64Encoded().isEqualTo(TEST_DATA);
@ -923,8 +920,20 @@ public class TbResourceControllerTest extends AbstractControllerTest {
}
private TbResourceInfo save(TbResource tbResource) throws Exception {
return doPostWithTypedResponse("/api/resource", tbResource, new TypeReference<>() {
});
byte[] data = tbResource.getData() != null ? tbResource.getData() : tbResource.getEncodedData() != null ? Base64.getDecoder().decode(tbResource.getEncodedData()) : null;
List<MockPart> parts = new ArrayList<>();
parts.add(new MockPart("resourceType", tbResource.getResourceType().name().getBytes()));
if (tbResource.getTitle() != null) {
parts.add(new MockPart("title", tbResource.getTitle().getBytes()));
}
if (tbResource.getDescriptor() != null) {
parts.add(new MockPart("descriptor", tbResource.getDescriptor().toString().getBytes()));
}
if (tbResource.getResourceSubType() != null) {
parts.add(new MockPart("resourceSubType", tbResource.getResourceSubType().name().getBytes()));
}
return uploadResource(HttpMethod.POST, "/api/resource/upload", tbResource.getFileName(), tbResource.getResourceType().getMediaType(), data, parts);
}
private TbResourceInfo findResourceInfo(TbResourceId id) throws Exception {
@ -949,7 +958,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
for (String model : models) {
String fileName = model + ".xml";
byte[] bytes = IOUtils.toByteArray(getClass().getClassLoader().getResourceAsStream("lwm2m/" + fileName));
byte[] bytes = IOUtils.toByteArray(Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream("lwm2m/" + fileName)));
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.LWM2M_MODEL);

2
common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial;
import java.util.function.UnaryOperator;
@Schema
@ -36,6 +37,7 @@ import java.util.function.UnaryOperator;
@EqualsAndHashCode(callSuper = true)
public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, HasTenantId, ExportableEntity<TbResourceId> {
@Serial
private static final long serialVersionUID = 7282664529021651736L;
@Schema(description = "JSON object with Tenant Id. Tenant Id of the resource can't be changed.", accessMode = Schema.AccessMode.READ_ONLY)

5
dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java

@ -91,7 +91,9 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@Primary
public class BaseResourceService extends AbstractCachedEntityService<ResourceInfoCacheKey, TbResourceInfo, ResourceInfoEvictEvent> implements ResourceService {
public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId ";
protected static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId ";
protected static final int MAX_ENTITIES_TO_FIND = 10;
protected final TbResourceDao resourceDao;
protected final TbResourceInfoDao resourceInfoDao;
protected final ResourceDataValidator resourceValidator;
@ -100,7 +102,6 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
protected final RuleChainDao ruleChainDao;
private final Map<EntityType, ResourceContainerDao<?>> resourceLinkContainerDaoMap = new HashMap<>();
private final Map<EntityType, ResourceContainerDao<?>> generalResourceContainerDaoMap = new HashMap<>();
protected static final int MAX_ENTITIES_TO_FIND = 10;
@PostConstruct
public void init() {

9
dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java

@ -17,9 +17,11 @@ package org.thingsboard.server.dao.service.validator;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
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;
@ -54,8 +56,8 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
@Override
protected TbResource validateUpdate(TenantId tenantId, TbResource resource) {
if (resource.getData() != null && !resource.getResourceType().isUpdatable() &&
tenantId != null && !tenantId.isSysTenantId()) {
if ((resource.getData() != null && !resource.getResourceType().isUpdatable() && tenantId != null && !tenantId.isSysTenantId())
|| resource.getResourceType().equals(ResourceType.LWM2M_MODEL)) {
throw new DataValidationException("This type of resource can't be updated");
}
return resource;
@ -81,7 +83,7 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
if (StringUtils.isEmpty(resource.getFileName())) {
throw new DataValidationException("Resource file name should be specified!");
}
if (StringUtils.containsAny(resource.getFileName(), "/", "\\")) {
if (Strings.CS.containsAny(resource.getFileName(), "/", "\\")) {
throw new DataValidationException("File name contains forbidden symbols");
}
if (StringUtils.isEmpty(resource.getResourceKey())) {
@ -104,4 +106,5 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
validateMaxSumDataSizePerTenant(tenantId, resourceDao, maxSumResourcesDataInBytes, dataSize, TB_RESOURCE);
}
}
}

50
ui-ngx/src/app/core/http/resource.service.ts

@ -17,7 +17,7 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { defaultHttpOptionsFromConfig, defaultHttpUploadOptions, RequestConfig } from '@core/http/http-utils';
import { forkJoin, Observable, of } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { Resource, ResourceInfo, ResourceSubType, ResourceType, TBResourceScope } from '@shared/models/resource.models';
@ -90,6 +90,54 @@ export class ResourceService {
return this.http.post<Resource>('/api/resource', resource, defaultHttpOptionsFromConfig(config));
}
public uploadResources(resources: Resource[], config?: RequestConfig): Observable<Resource[]> {
let partSize = 100;
partSize = resources.length > partSize ? partSize : resources.length;
const resourceObservables: Observable<Resource>[] = [];
for (let i = 0; i < partSize; i++) {
resourceObservables.push(this.uploadResource(resources[i], config).pipe(catchError(() => of({} as Resource))));
}
return forkJoin(resourceObservables).pipe(
mergeMap((resource) => {
resources.splice(0, partSize);
if (resources.length) {
return this.uploadResources(resources, config);
} else {
return of(resource);
}
})
);
}
public uploadResource(resource: Resource, config?: RequestConfig): Observable<Resource> {
if (!config) {
config = {};
}
const formData = new FormData();
formData.append('file', resource.data);
formData.append('title', resource.title);
formData.append('resourceType', resource.resourceType);
if (resource.resourceSubType) {
formData.append('resourceSubType', resource.resourceSubType);
}
return this.http.post<Resource>('/api/resource/upload', formData,
defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest));
}
public updatedResourceInfo(resourceId: string, updatedResources: Partial<Omit<Resource, 'data'>>, config?: RequestConfig): Observable<Resource> {
return this.http.put<Resource>(`/api/resource/${resourceId}/info`, updatedResources, defaultHttpOptionsFromConfig(config));
}
public updatedResourceData(resourceId: string, data: File, config?: RequestConfig): Observable<Resource> {
if (!config) {
config = {};
}
const formData = new FormData();
formData.append('file', data);
return this.http.put<Resource>(`/api/resource/${resourceId}/data`, formData,
defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest));
}
public deleteResource(resourceId: string, force = false, config?: RequestConfig) {
return this.http.delete(`/api/resource/${resourceId}?force=${force}`, defaultHttpOptionsFromConfig(config));
}

5
ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts

@ -98,18 +98,17 @@ export class ResourcesDialogComponent extends DialogComponent<ResourcesDialogCom
resources.push({
resourceType: resource.resourceType,
data,
fileName: resource.fileName[index],
title: resource.title
});
});
this.resourceService.saveResources(resources, {resendRequest: true}).pipe(
this.resourceService.uploadResources(resources, {resendRequest: true}).pipe(
map((response) => response[0])
).subscribe(result => this.dialogRef.close(result));
} else {
if (resource.resourceType !== ResourceType.GENERAL) {
delete resource.descriptor;
}
this.resourceService.saveResource(resource).subscribe(result => this.dialogRef.close(result));
this.resourceService.uploadResource(resource).subscribe(result => this.dialogRef.close(result));
}
}
}

2
ui-ngx/src/app/modules/home/components/resources/resources-library.component.html

@ -70,7 +70,7 @@
<tb-file-input formControlName="data"
required
label="{{ (entityForm.get('resourceType').value === resourceType.LWM2M_MODEL ? 'resource.resource-files' : 'resource.resource-file') | translate }}"
[readAsBinary]="true"
[workFromFileObj]="true"
[maxSizeByte]="maxResourceSize"
[allowedExtensions]="getAllowedExtensions()"
[contentConvertFunction]="convertToBase64File"

2
ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts

@ -89,7 +89,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
return this.fb.group({
title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]],
resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, Validators.required],
fileName: [entity ? entity.fileName : null, Validators.required],
fileName: [entity ? entity.fileName : null],
data: [entity ? entity.data : null, this.isAdd ? [Validators.required] : []],
descriptor: this.fb.group({
mediaType: ['']

19
ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts

@ -32,7 +32,7 @@ import {
ResourceType,
toResourceDeleteResult
} from '@shared/models/resource.models';
import { EntityType, entityTypeResources } from '@shared/models/entity-type.models';
import { EntityType } from '@shared/models/entity-type.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { DatePipe } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
@ -47,7 +47,7 @@ import { JsLibraryTableHeaderComponent } from '@home/pages/admin/resource/js-lib
import { JsResourceComponent } from '@home/pages/admin/resource/js-resource.component';
import { catchError, map, switchMap } from 'rxjs/operators';
import { ResourceTabsComponent } from '@home/pages/admin/resource/resource-tabs.component';
import { forkJoin, of } from 'rxjs';
import { forkJoin, Observable, of } from 'rxjs';
import { parseHttpErrorMessage } from '@core/utils';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { MatDialog } from '@angular/material/dialog';
@ -118,9 +118,20 @@ export class JsLibraryTableConfigResolver {
return this.resourceService.getResourceInfoById(id.id)
}
};
this.config.saveEntity = resource => {
this.config.saveEntity = (resource: Resource, originalResource: Resource) => {
resource.resourceType = ResourceType.JS_MODULE;
let saveObservable = this.resourceService.saveResource(resource);
let saveObservable: Observable<Resource>;
if (!originalResource) {
saveObservable = this.resourceService.uploadResource(resource);
} else {
const { data, ...resourceInfo } = resource;
saveObservable = this.resourceService.updatedResourceInfo(resource.id.id, resourceInfo);
if (data) {
saveObservable = saveObservable.pipe(
switchMap(() => this.resourceService.updatedResourceData(resource.id.id, data))
)
}
}
if (resource.resourceSubType === ResourceSubType.MODULE) {
saveObservable = saveObservable.pipe(
switchMap((saved) => this.resourceService.getResource(saved.id.id))

2
ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html

@ -70,7 +70,7 @@
formControlName="data"
[required]="isAdd"
label="{{ 'javascript.resource-file' | translate }}"
[readAsBinary]="true"
[workFromFileObj]="true"
[maxSizeByte]="maxResourceSize"
[allowedExtensions]="getAllowedExtensions()"
[contentConvertFunction]="convertToBase64File"

8
ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts

@ -82,7 +82,7 @@ export class JsResourceComponent extends EntityComponent<Resource> implements On
return this.fb.group({
title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]],
resourceSubType: [entity?.resourceSubType ? entity.resourceSubType : ResourceSubType.EXTENSION, Validators.required],
fileName: [entity ? entity.fileName : null, Validators.required],
fileName: [entity ? entity.fileName : null],
data: [entity ? entity.data : null, this.isAdd ? [Validators.required] : []],
content: [entity?.data?.length ? base64toString(entity.data) : '', Validators.required]
});
@ -110,7 +110,9 @@ export class JsResourceComponent extends EntityComponent<Resource> implements On
if (!formValue.fileName) {
formValue.fileName = formValue.title + '.js';
}
formValue.data = stringToBase64((formValue as any).content);
formValue.data = new File([(formValue as any).content], formValue.fileName, {
type: 'text/javascript'
});
delete (formValue as any).content;
}
return super.prepareFormValue(formValue);
@ -125,7 +127,7 @@ export class JsResourceComponent extends EntityComponent<Resource> implements On
}
convertToBase64File(data: string): string {
return window.btoa(data);
return stringToBase64(data);
}
onResourceIdCopied(): void {

26
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts

@ -24,7 +24,8 @@ import {
import { Router } from '@angular/router';
import {
Resource,
ResourceInfo, ResourceInfoWithReferences,
ResourceInfo,
ResourceInfoWithReferences,
ResourceType,
ResourceTypeTranslationMap,
toResourceDeleteResult
@ -41,10 +42,10 @@ import { Authority } from '@shared/models/authority.enum';
import { ResourcesLibraryComponent } from '@home/components/resources/resources-library.component';
import { PageLink } from '@shared/models/page/page-link';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { catchError, map } from 'rxjs/operators';
import { catchError, map, switchMap } from 'rxjs/operators';
import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component';
import { ResourceLibraryTabsComponent } from '@home/pages/admin/resource/resource-library-tabs.component';
import { forkJoin, of } from "rxjs";
import { forkJoin, Observable, of } from "rxjs";
import {
ResourcesInUseDialogComponent,
ResourcesInUseDialogData
@ -114,27 +115,36 @@ export class ResourcesLibraryTableConfigResolver {
this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink, this.config.componentsData.resourceType);
this.config.loadEntity = id => this.resourceService.getResourceInfoById(id.id);
this.config.saveEntity = resource => this.saveResource(resource);
this.config.saveEntity = (resource, originalResource) => this.saveResource(resource, originalResource);
this.config.onEntityAction = action => this.onResourceAction(action);
}
saveResource(resource) {
saveResource(resource: Resource & {data?: File | File[]}, originalResource: Resource) {
if (Array.isArray(resource.data)) {
const resources = [];
resource.data.forEach((data, index) => {
resources.push({
resourceType: resource.resourceType,
data,
fileName: resource.fileName[index],
title: resource.title
});
});
return this.resourceService.saveResources(resources, {resendRequest: true}).pipe(
return this.resourceService.uploadResources(resources, {resendRequest: true}).pipe(
map((response) => response[0])
);
} else if (!originalResource) {
return this.resourceService.uploadResource(resource);
} else {
return this.resourceService.saveResource(resource);
const { data, ...resourceInfo } = resource;
let saveObservable: Observable<Resource>;
saveObservable = this.resourceService.updatedResourceInfo(resource.id.id, resourceInfo);
if (data) {
saveObservable = saveObservable.pipe(
switchMap(() => this.resourceService.updatedResourceData(resource.id.id, data))
)
}
return saveObservable;
}
}

52
ui-ngx/src/app/shared/components/file-input.component.ts

@ -180,17 +180,18 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
if (readers.length) {
Promise.all(readers).then((files) => {
files = files.filter(file => file.fileContent != null || file.files != null);
if (files.length === 1) {
this.fileContent = files[0].fileContent;
this.fileName = files[0].fileName;
this.files = files[0].files;
this.mediaType = files[0].mediaType;
const validResults = files.filter(file => file.fileContent != null || file.files != null);
if (validResults.length === 1) {
this.fileContent = validResults[0].fileContent;
this.fileName = validResults[0].fileName;
this.files = validResults[0].files;
this.mediaType = validResults[0].mediaType;
this.updateModel();
} else if (files.length > 1) {
this.fileContent = files.map(content => content.fileContent);
this.fileName = files.map(content => content.fileName);
this.files = files.map(content => content.files);
} else if (validResults.length > 1) {
this.fileContent = validResults.map(content => content.fileContent);
this.fileName = validResults.map(content => content.fileName);
this.files = validResults.map(content => content.files);
this.updateModel();
}
});
@ -204,29 +205,32 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
private readerAsFile(file: flowjs.FlowFile): Promise<any> {
return new Promise((resolve) => {
if (this.workFromFileObj) {
resolve({
fileContent: null,
fileName: file.name,
files: file.file,
mediaType: file.file.type || null
});
return;
}
const reader = new FileReader();
reader.onload = () => {
let fileName = null;
let fileContent = null;
let files = null;
let mediaType = null;
if (reader.readyState === reader.DONE) {
if (!this.workFromFileObj) {
fileContent = reader.result;
if (fileContent && fileContent.length > 0) {
if (this.contentConvertFunction) {
fileContent = this.contentConvertFunction(fileContent);
}
fileName = fileContent ? file.name : null;
mediaType = file?.file?.type || null;
fileContent = reader.result;
if (fileContent && fileContent.length > 0) {
if (this.contentConvertFunction) {
fileContent = this.contentConvertFunction(fileContent);
}
} else if (file.name || file.file){
files = file.file;
fileName = file.name;
mediaType = file.file.type || null;
fileName = fileContent ? file.name : null;
mediaType = file?.file?.type || null;
}
}
resolve({fileContent, fileName, files, mediaType});
resolve({fileContent, fileName, files: null, mediaType});
};
reader.onerror = () => {
resolve({fileContent: null, fileName: null, files: null, mediaType: null});

2
ui-ngx/src/app/shared/models/resource.models.ts

@ -89,7 +89,7 @@ export interface TbResourceInfo<D> extends Omit<BaseData<TbResourceId>, 'name' |
export type ResourceInfo = TbResourceInfo<any>;
export interface Resource extends ResourceInfo {
data?: string;
data?: any;
name?: string;
}

Loading…
Cancel
Save