Browse Source

UI: Add API to upload large file resources

pull/14205/head
Vladyslav_Prykhodko 9 months ago
parent
commit
0ed9deb186
  1. 50
      ui-ngx/src/app/core/http/resource.service.ts
  2. 5
      ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts
  3. 2
      ui-ngx/src/app/modules/home/components/resources/resources-library.component.html
  4. 2
      ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts
  5. 19
      ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts
  6. 2
      ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html
  7. 14
      ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts
  8. 26
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts
  9. 52
      ui-ngx/src/app/shared/components/file-input.component.ts
  10. 2
      ui-ngx/src/app/shared/models/resource.models.ts

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"

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

@ -32,7 +32,7 @@ import {
} from '@shared/models/resource.models';
import { startWith, takeUntil } from 'rxjs/operators';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { isDefinedAndNotNull } from '@core/utils';
import { base64toString, isDefinedAndNotNull, stringToBase64 } from '@core/utils';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
@Component({
@ -82,15 +82,15 @@ 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 ? window.atob(entity.data) : '', Validators.required]
content: [entity?.data?.length ? base64toString(entity.data) : '', Validators.required]
});
}
updateForm(entity: Resource): void {
this.entityForm.patchValue(entity);
const content = entity.resourceSubType === ResourceSubType.MODULE && entity?.data?.length ? window.atob(entity.data) : '';
const content = entity.resourceSubType === ResourceSubType.MODULE && entity?.data?.length ? base64toString(entity.data) : '';
this.entityForm.get('content').patchValue(content);
}
@ -110,7 +110,9 @@ export class JsResourceComponent extends EntityComponent<Resource> implements On
if (!formValue.fileName) {
formValue.fileName = formValue.title + '.js';
}
formValue.data = window.btoa((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