Skip to content

Commit

Permalink
ufal/fe-oversized-file-upload-message (#424)
Browse files Browse the repository at this point in the history
* If the file exceeds the upload max file size the uploading will be stopped before starting and the user will see proper error message.

* Fixed unit tests - added configurationDataService
  • Loading branch information
milanmajchrak authored Dec 15, 2023
1 parent 1d4c3a5 commit ee0e746
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock';
import { HttpXsrfTokenExtractorMock } from '../../shared/mocks/http-xsrf-token-extractor.mock';
import { getMockEntityTypeService } from './my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.spec';
import { EntityTypeDataService } from '../../core/data/entity-type-data.service';
import { of } from 'rxjs';
import { ConfigurationDataService } from '../../core/data/configuration-data.service';

describe('MyDSpaceNewSubmissionComponent test', () => {

Expand All @@ -35,6 +37,10 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
uploadAll: jasmine.createSpy('uploadAll').and.stub()
});

const configurationServiceSpy = jasmine.createSpyObj('configurationService', {
findByPropertyName: of({}),
});

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
Expand Down Expand Up @@ -64,6 +70,7 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
{ provide: CookieService, useValue: new CookieServiceMock() },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
{ provide: EntityTypeDataService, useValue: getMockEntityTypeService() },
{ provide: ConfigurationDataService, useValue: configurationServiceSpy },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
Expand Down
7 changes: 7 additions & 0 deletions src/app/shared/upload/uploader/uploader.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ import { HttpXsrfTokenExtractor } from '@angular/common/http';
import { CookieService } from '../../../core/services/cookie.service';
import { CookieServiceMock } from '../../mocks/cookie.service.mock';
import { HttpXsrfTokenExtractorMock } from '../../mocks/http-xsrf-token-extractor.mock';
import { of } from 'rxjs';
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';

describe('Chips component', () => {

let testComp: TestComponent;
let testFixture: ComponentFixture<TestComponent>;
let html;

const configurationServiceSpy = jasmine.createSpyObj('configurationService', {
findByPropertyName: of({}),
});

// waitForAsync beforeEach
beforeEach(waitForAsync(() => {

Expand All @@ -40,6 +46,7 @@ describe('Chips component', () => {
DragService,
{ provide: HttpXsrfTokenExtractor, useValue: new HttpXsrfTokenExtractorMock('mock-token') },
{ provide: CookieService, useValue: new CookieServiceMock() },
{ provide: ConfigurationDataService, useValue: configurationServiceSpy },
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
Expand Down
64 changes: 60 additions & 4 deletions src/app/shared/upload/uploader/uploader.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostListener, Input, Output, ViewEncapsulation, } from '@angular/core';

import { of as observableOf } from 'rxjs';
import { FileUploader } from 'ng2-file-upload';
import { firstValueFrom, Observable, of as observableOf } from 'rxjs';
import { FileItem, FileUploader } from 'ng2-file-upload';
import uniqueId from 'lodash/uniqueId';
import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to';

Expand All @@ -12,7 +12,14 @@ import { HttpXsrfTokenExtractor } from '@angular/common/http';
import { XSRF_COOKIE, XSRF_REQUEST_HEADER, XSRF_RESPONSE_HEADER } from '../../../core/xsrf/xsrf.interceptor';
import { CookieService } from '../../../core/services/cookie.service';
import { DragService } from '../../../core/drag.service';
import {ConfigurationDataService} from '../../../core/data/configuration-data.service';
import { map } from 'rxjs/operators';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { RemoteData } from '../../../core/data/remote-data';
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
import { TranslateService } from '@ngx-translate/core';

export const MAX_UPLOAD_FILE_SIZE_CFG_PROPERTY = 'spring.servlet.multipart.max-file-size';
@Component({
selector: 'ds-uploader',
templateUrl: 'uploader.component.html',
Expand Down Expand Up @@ -90,7 +97,9 @@ export class UploaderComponent {
private scrollToService: ScrollToService,
private dragService: DragService,
private tokenExtractor: HttpXsrfTokenExtractor,
private cookieService: CookieService
private cookieService: CookieService,
private configurationService: ConfigurationDataService,
private translate: TranslateService
) {
}

Expand Down Expand Up @@ -129,7 +138,20 @@ export class UploaderComponent {
if (isUndefined(this.onBeforeUpload)) {
this.onBeforeUpload = () => {return;};
}
this.uploader.onBeforeUploadItem = (item) => {
this.uploader.onBeforeUploadItem = async (item) => {
// Check if the file size is within the maximum upload size
const canUpload = await this.checkFileSizeLimit(item);
// If the file size is too large, emit an error and cancel all uploads
if (!canUpload) {
this.onUploadError.emit({
item: item,
response: this.translate.instant('submission.sections.upload.upload-failed.size-limit-exceeded'),
status: 400,
headers: {}
});
this.uploader.cancelAll();
return;
}
if (item.url !== this.uploader.options.url) {
item.url = this.uploader.options.url;
}
Expand Down Expand Up @@ -225,4 +247,38 @@ export class UploaderComponent {
this.cookieService.set(XSRF_COOKIE, token);
}

// Check if the file size is within the maximum upload size
private async checkFileSizeLimit(item: FileItem): Promise<boolean> {
const maxFileUploadSize = await firstValueFrom(this.getMaxFileUploadSizeFromCfg());
if (maxFileUploadSize) {
const maxSizeInGigabytes = parseInt(maxFileUploadSize?.[0], 10);
const maxSizeInBytes = this.gigabytesToBytes(maxSizeInGigabytes);
// If maxSizeInBytes is -1, it means the value in the config is invalid. The file won't be uploaded and the user
// will see error messages in the UI.
if (maxSizeInBytes === -1) {
return false;
}
return item?.file?.size <= maxSizeInBytes;
}
return false;
}

// Convert gigabytes to bytes
private gigabytesToBytes(gigabytes: number): number {
if (typeof gigabytes !== 'number' || isNaN(gigabytes) || !isFinite(gigabytes) || gigabytes < 0) {
return -1;
}
return gigabytes * Math.pow(2, 30); // 2^30 bytes in a gigabyte
}

// Get the maximum file upload size from the configuration
public getMaxFileUploadSizeFromCfg(): Observable<string[]> {
return this.configurationService.findByPropertyName(MAX_UPLOAD_FILE_SIZE_CFG_PROPERTY).pipe(
getFirstCompletedRemoteData(),
map((propertyRD: RemoteData<ConfigurationProperty>) => {
return propertyRD.hasSucceeded ? propertyRD.payload.values : [];
})
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
[onBeforeUpload]="onBeforeUpload"
[uploadFilesOptions]="uploadFilesOptions"
(onCompleteItem)="onCompleteItem($event)"
(onUploadError)="onUploadError()"></ds-uploader>
(onUploadError)="onUploadError($event)"></ds-uploader>
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,12 @@ export class SubmissionUploadFilesComponent implements OnChanges {
/**
* Show error notification on upload fails
*/
public onUploadError() {
this.notificationsService.error(null, this.translate.get('submission.sections.upload.upload-failed'));
public onUploadError(event: any) {
const errorMessageUploadLimit = this.translate.instant('submission.sections.upload.upload-failed.size-limit-exceeded');
const defaultErrorMessage = this.translate.instant('submission.sections.upload.upload-failed');
const errorMessage = event?.response === errorMessageUploadLimit ? errorMessageUploadLimit : defaultErrorMessage;

this.notificationsService.error(null, errorMessage);
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/assets/i18n/cs.json5
Original file line number Diff line number Diff line change
Expand Up @@ -7500,6 +7500,9 @@
// "submission.sections.upload.upload-successful": "Upload successful",
"submission.sections.upload.upload-successful" : "Úspěšné nahrání",

// "submission.sections.upload.upload-failed.size-limit-exceeded": "File size exceeds the maximum upload size",
"submission.sections.upload.upload-failed.size-limit-exceeded": "Soubor přesahuje maximální povolenou velikost",

// "submission.sections.accesses.form.discoverable-description": "When checked, this item will be discoverable in search/browse. When unchecked, the item will only be available via a direct link and will never appear in search/browse.",
"submission.sections.accesses.form.discoverable-description" : "Pokud je tato položka zaškrtnuta, bude ji možné hledat ve vyhledávání/prohlížení. Pokud není zaškrtnuta, bude položka dostupná pouze prostřednictvím přímého odkazu a nikdy se nezobrazí ve vyhledávání/prohlížení.",

Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/en.json5
Original file line number Diff line number Diff line change
Expand Up @@ -5081,6 +5081,8 @@

"submission.sections.upload.upload-successful": "Upload successful",

"submission.sections.upload.upload-failed.size-limit-exceeded": "File size exceeds the maximum upload size",

"submission.sections.accesses.form.discoverable-description": "When checked, this item will be discoverable in search/browse. When unchecked, the item will only be available via a direct link and will never appear in search/browse.",

"submission.sections.accesses.form.discoverable-label": "Discoverable",
Expand Down

0 comments on commit ee0e746

Please sign in to comment.