Browse Source

Merge pull request #9901 from thingsboard/feature/resource-max-size

Set max allow resource size from tenant profile
pull/9904/head
Igor Kulikov 3 years ago
committed by GitHub
parent
commit
4b64910758
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      application/src/main/java/org/thingsboard/server/controller/ImageController.java
  2. 5
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  3. 14
      application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java
  4. 1
      common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
  5. 1
      ui-ngx/src/app/core/auth/auth.models.ts
  6. 1
      ui-ngx/src/app/core/auth/auth.reducer.ts
  7. 3
      ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html
  8. 2
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html
  9. 3
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts
  10. 1
      ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html
  11. 15
      ui-ngx/src/app/shared/components/file-input.component.html
  12. 37
      ui-ngx/src/app/shared/components/file-input.component.scss
  13. 60
      ui-ngx/src/app/shared/components/file-input.component.ts
  14. 1
      ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html
  15. 3
      ui-ngx/src/app/shared/components/image/upload-image-dialog.component.ts
  16. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json

26
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;
@ -51,7 +49,6 @@ 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.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.util.ThrowingSupplier;
import org.thingsboard.server.dao.resource.ImageCacheKey;
import org.thingsboard.server.dao.resource.ImageService;
@ -284,19 +281,6 @@ public class ImageController extends BaseController {
return (result.isSuccess() ? ResponseEntity.ok() : ResponseEntity.badRequest()).body(result);
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping("/api/image/specs")
public ImageSpecs getImageUploadSpecs() throws ThingsboardException {
SecurityUser user = getCurrentUser();
if (user.isSystemAdmin()) {
return ImageSpecs.DEFAULT;
}
DefaultTenantProfileConfiguration tenantProfileConfig = tenantProfileCache.get(user.getTenantId()).getDefaultProfileConfiguration();
return ImageSpecs.builder()
.maximumSize(tenantProfileConfig.getMaxResourceSize())
.build();
}
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));
@ -359,14 +343,4 @@ public class ImageController extends BaseController {
return tenantId;
}
@Data
@Builder
public static class ImageSpecs {
private final long maximumSize;
public static final ImageSpecs DEFAULT = ImageSpecs.builder()
.maximumSize(0)
.build();
}
}

5
application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
@ -130,6 +131,10 @@ public class SystemInfoController extends BaseController {
}
systemParams.setUserSettings(userSettingsNode);
systemParams.setMaxDatapointsLimit(maxDatapointsLimit);
if (!currentUser.isSystemAdmin()) {
DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration();
systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize());
}
return systemParams;
}

14
application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java

@ -30,10 +30,10 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.ImageDescriptor;
import org.thingsboard.server.common.data.ImageExportData;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.SystemParams;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.controller.ImageController.ImageSpecs;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.sql.resource.TbResourceRepository;
@ -243,24 +243,24 @@ public class ImageControllerTest extends AbstractControllerTest {
@Test
public void testGetImageUploadSpecs() throws Exception {
ImageSpecs specs = doGet("/api/image/specs", ImageSpecs.class);
assertThat(specs.getMaximumSize()).isZero();
SystemParams systemParams = doGet("/api/system/params", SystemParams.class);
assertThat(systemParams.getMaxResourceSize()).isZero();
loginSysAdmin();
updateDefaultTenantProfileConfig(tenantProfileConfig -> {
tenantProfileConfig.setMaxResourceSize(100);
});
loginTenantAdmin();
specs = doGet("/api/image/specs", ImageSpecs.class);
assertThat(specs.getMaximumSize()).isEqualTo(100);
systemParams = doGet("/api/system/params", SystemParams.class);
assertThat(systemParams.getMaxResourceSize()).isEqualTo(100);
loginSysAdmin();
updateDefaultTenantProfileConfig(tenantProfileConfig -> {
tenantProfileConfig.setMaxResourceSize(0);
});
loginTenantAdmin();
specs = doGet("/api/image/specs", ImageSpecs.class);
assertThat(specs.getMaximumSize()).isEqualTo(0);
systemParams = doGet("/api/system/params", SystemParams.class);
assertThat(systemParams.getMaxResourceSize()).isEqualTo(0);
}
private TbResourceInfo updateImagePublicStatus(String filename, boolean isPublic) throws Exception {

1
common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java

@ -30,4 +30,5 @@ public class SystemParams {
boolean persistDeviceStateToTelemetry;
JsonNode userSettings;
long maxDatapointsLimit;
long maxResourceSize;
}

1
ui-ngx/src/app/core/auth/auth.models.ts

@ -25,6 +25,7 @@ export interface SysParamsState {
tbelEnabled: boolean;
persistDeviceStateToTelemetry: boolean;
userSettings: UserSettings;
maxResourceSize: number;
}
export interface SysParams extends SysParamsState {

1
ui-ngx/src/app/core/auth/auth.reducer.ts

@ -29,6 +29,7 @@ const emptyUserAuthState: AuthPayload = {
hasRepository: false,
tbelEnabled: false,
persistDeviceStateToTelemetry: false,
maxResourceSize: 0,
userSettings: initialUserSettings
};

3
ui-ngx/src/app/modules/home/components/vc/repository-settings.component.html

@ -80,8 +80,7 @@
</mat-form-field>
</section>
<section [fxShow]="repositorySettingsForm.get('authMethod').value === repositoryAuthMethod.PRIVATE_KEY" fxLayout="column">
<tb-file-input style="margin-bottom: 16px;"
[existingFileName]="repositorySettingsForm.get('privateKeyFileName').value"
<tb-file-input [existingFileName]="repositorySettingsForm.get('privateKeyFileName').value"
required
formControlName="privateKey"
dropLabel="{{ 'admin.drop-private-key-file-or' | translate }}"

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

@ -69,7 +69,9 @@
<tb-file-input *ngIf="isAdd || (isEdit && entityForm.get('resourceType').value === resourceType.JS_MODULE)"
formControlName="data"
required
label="{{ (entityForm.get('resourceType').value === resourceType.LWM2M_MODEL ? 'resource.resource-files' : 'resource.resource-file') | translate }}"
[readAsBinary]="true"
[maxSizeByte]="maxResourceSize"
[allowedExtensions]="getAllowedExtensions()"
[contentConvertFunction]="convertToBase64File"
[accept]="getAcceptType()"

3
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts

@ -32,6 +32,7 @@ import {
import { filter, startWith, takeUntil } from 'rxjs/operators';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { isDefinedAndNotNull } from '@core/utils';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
@Component({
selector: 'tb-resources-library',
@ -43,6 +44,8 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
readonly resourceTypes: ResourceType[] = Object.values(this.resourceType);
readonly resourceTypesTranslationMap = ResourceTypeTranslationMap;
maxResourceSize = getCurrentAuthState(this.store).maxResourceSize;
private destroy$ = new Subject<void>();
constructor(protected store: Store<AppState>,

1
ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html

@ -120,6 +120,7 @@
<tb-file-input
formControlName="file"
workFromFileObj="true"
label="{{ 'ota-update.package-file' | translate }}"
[required]="!entityForm.get('isURL').value"
dropLabel="{{'ota-update.drop-package-file-or' | translate}}">
</tb-file-input>

15
ui-ngx/src/app/shared/components/file-input.component.html

@ -16,7 +16,7 @@
-->
<div class="tb-container">
<label class="tb-title"
<label class="tb-title" *ngIf="label"
[class.tb-required]="!disabled && required"
[class.pointer-event]="hint"
tb-hint-tooltip-icon="{{ hint }}">{{ label }}
@ -38,7 +38,7 @@
flowDrop
[flow]="flow.flowJs">
<div class="upload-label">
<mat-icon>cloud_upload</mat-icon>
<mat-icon class="tb-mat-32">cloud_upload</mat-icon>
<span>{{ dropLabel }}</span>
<button type="button" mat-button color="primary" class="browse-file">
<label
@ -50,9 +50,10 @@
</div>
</div>
</ng-container>
</div>
<div>
<tb-error *ngIf="!fileName && required && requiredAsError" error="{{ noFileText | translate }}"></tb-error>
<div *ngIf="!fileName && !requiredAsError" translate>{{ noFileText }}</div>
<div *ngIf="fileName">{{ fileName }}</div>
<div class="tb-file-info-container">
<tb-error *ngIf="!fileName && required && requiredAsError" class="tb-file-name" error="{{ noFileText | translate }}"></tb-error>
<div *ngIf="!fileName && !requiredAsError" class="tb-file-name" translate>{{ noFileText }}</div>
<div *ngIf="fileName" class="tb-file-name" [title]="fileName">{{ fileName }}</div>
<div *ngIf="maxSizeByte && !disabled" class="tb-file-hint" translate [translateParams]="{ size: maxSizeByte | fileSize }">dashboard.maximum-upload-file-size</div>
</div>
</div>

37
ui-ngx/src/app/shared/components/file-input.component.scss

@ -21,9 +21,13 @@ $previewSize: 100px !default;
.tb-container {
margin-top: 0;
padding: 0 0 16px;
display: flex;
flex-direction: column;
gap: 8px;
label.tb-title {
display: flex;
padding-bottom: 8px;
padding-bottom: 0;
}
}
@ -78,19 +82,46 @@ $previewSize: 100px !default;
text-align: center;
.mat-icon {
margin-right: 17px;
color: rgba(0,0,0,0.12);
}
}
}
.tb-file-info-container {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 13px;
font-style: normal;
line-height: 16px;
letter-spacing: normal;
}
.tb-file-name {
color: rgba(0, 0, 0, 0.54);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
}
.tb-file-hint {
color: rgba(0, 0, 0, 0.38);
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
}
}
:host ::ng-deep {
button.browse-file {
button.mat-mdc-button.mat-mdc-button-base.browse-file {
padding: 0;
min-width: 0;
height: 24px;
font-size: 16px;
label {
display: block;
cursor: pointer;
padding: 0 16px;
padding: 0 8px;
}
}

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

@ -32,10 +32,12 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subscription } from 'rxjs';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { FlowDirective } from '@flowjs/ngx-flow';
import { TranslateService } from '@ngx-translate/core';
import { UtilsService } from '@core/services/utils.service';
import { DialogService } from '@core/services/dialog.service';
import { FileSizePipe } from '@shared/pipe/file-size.pipe';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-file-input',
@ -73,43 +75,28 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
dropLabel: string;
@Input()
contentConvertFunction: (content: string) => any;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
maxSizeByte: number;
@Input()
set required(value: boolean) {
const newVal = coerceBooleanProperty(value);
if (this.requiredValue !== newVal) {
this.requiredValue = newVal;
}
}
private requiredAsErrorValue: boolean;
contentConvertFunction: (content: string) => any;
get requiredAsError(): boolean {
return this.requiredAsErrorValue;
}
@Input()
@coerceBoolean()
required: boolean;
@Input()
set requiredAsError(value: boolean) {
const newVal = coerceBooleanProperty(value);
if (this.requiredAsErrorValue !== newVal) {
this.requiredAsErrorValue = newVal;
}
}
@coerceBoolean()
requiredAsError: boolean;
@Input()
@coerceBoolean()
disabled: boolean;
@Input()
existingFileName: string;
@Input()
@coerceBoolean()
readAsBinary = false;
@Input()
@ -148,7 +135,9 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
constructor(protected store: Store<AppState>,
private utils: UtilsService,
public translate: TranslateService) {
private translate: TranslateService,
private dialog: DialogService,
private fileSize: FileSizePipe) {
super(store);
}
@ -156,11 +145,24 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
this.autoUploadSubscription = this.flow.events$.subscribe(event => {
if (event.type === 'filesAdded') {
const readers = [];
let showMaxSizeAlert = false;
(event.event[0] as flowjs.FlowFile[]).forEach(file => {
if (this.filterFile(file)) {
readers.push(this.readerAsFile(file));
if (this.checkMaxSize(file)) {
readers.push(this.readerAsFile(file));
} else {
showMaxSizeAlert = true;
}
}
});
if (showMaxSizeAlert) {
this.dialog.alert(
this.translate.instant('dashboard.cannot-upload-file'),
this.translate.instant('dashboard.maximum-upload-file-size', {size: this.fileSize.transform(this.maxSizeByte)})
).subscribe(() => { });
}
if (readers.length) {
Promise.all(readers).then((files) => {
files = files.filter(file => file.fileContent != null || file.files != null);
@ -218,6 +220,10 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
});
}
private checkMaxSize(file: flowjs.FlowFile): boolean {
return !this.maxSizeByte || file.size <= this.maxSizeByte;
}
private filterFile(file: flowjs.FlowFile): boolean {
if (this.allowedExtensions) {
return this.allowedExtensions.split(',').indexOf(file.getExtension()) > -1;

1
ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html

@ -35,6 +35,7 @@
label="{{'image.image-preview' | translate}}"
formControlName="file"
showFileName
[maxSizeByte]="maxResourceSize"
[fileName]="data?.image?.fileName"
(fileNameChanged)="imageFileNameChanged($event)">
</tb-image-input>

3
ui-ngx/src/app/shared/components/image/upload-image-dialog.component.ts

@ -31,6 +31,7 @@ import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { ImageService } from '@core/http/image.service';
import { ImageResourceInfo, imageResourceType } from '@shared/models/resource.models';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
export interface UploadImageDialogData {
image?: ImageResourceInfo;
@ -51,6 +52,8 @@ export class UploadImageDialogComponent extends
submitted = false;
maxResourceSize = getCurrentAuthState(this.store).maxResourceSize;
constructor(protected store: Store<AppState>,
protected router: Router,
private imageService: ImageService,

3
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -3511,6 +3511,7 @@
"ota-update": "OTA update",
"ota-update-details": "OTA update details",
"ota-updates": "OTA updates",
"package-file": "Package file",
"package-type": "Package type",
"packages-repository": "Packages repository",
"search": "Search packages",
@ -3697,6 +3698,8 @@
"no-resource-text": "No resources found",
"open-widgets-bundle": "Open widgets bundle",
"resource": "Resource",
"resource-file": "Resource file",
"resource-files": "Resource files",
"resource-library-details": "Resource details",
"resource-type": "Resource type",
"resources-library": "Resources library",

Loading…
Cancel
Save