Skip to content

Commit 691ca71

Browse files
customer/uk-it-6 (#429)
* ufal/fe-oversized-file-upload-message (#424) * 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 * ufal/fe-item-view-license-box (#427) * Do not show licenses if the Item doesn't have any file. --------- Co-authored-by: Jozef Misutka <332350+vidiecan@users.noreply.github.com>
1 parent 6b6219e commit 691ca71

9 files changed

Lines changed: 147 additions & 32 deletions

File tree

src/app/item-page/simple/item-page.component.html

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,31 @@
1010
<ds-view-tracker [object]="item"></ds-view-tracker>
1111
<ds-listable-object-component-loader *ngIf="!item.isWithdrawn || (isAdmin$|async)" [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
1212
<ds-item-versions class="mt-2" [item]="item" [displayActions]="false"></ds-item-versions>
13-
<ds-clarin-license-info class="mt-3 d-block" [item]="item"></ds-clarin-license-info>
14-
<h6><i class="fa fa-paperclip">&nbsp;</i>{{'item.page.files.head' | translate}}</h6>
15-
<div class="pb-3">
16-
<span class="pr-1">
17-
<a class="btn btn-download" (click)="setCommandline()" style="text-decoration: none"
18-
*ngIf="canShowCurlDownload">
19-
<i class="fa fa-download fa-3x" style="display: block">&nbsp;</i>
20-
{{'item.page.download.button.command.line' | translate}}
21-
</a>
22-
</span>
23-
<div id="command-div" *ngIf="isCommandLineVisible">
24-
<button class="repo-copy-btn pull-right" data-clipboard-target="#command-div"></button>
25-
<pre style="background-color: #d9edf7; color: #3a87ad">{{ command }}</pre>
13+
<div *ngIf="(hasFiles | async) === true">
14+
<ds-clarin-license-info class="mt-3 d-block" [item]="item"></ds-clarin-license-info>
15+
<h6><i class="fa fa-paperclip">&nbsp;</i>{{'item.page.files.head' | translate}}</h6>
16+
<div class="pb-3">
17+
<span class="pr-1">
18+
<a class="btn btn-download" (click)="setCommandline()" style="text-decoration: none"
19+
*ngIf="canShowCurlDownload">
20+
<i class="fa fa-download fa-3x" style="display: block">&nbsp;</i>
21+
{{'item.page.download.button.command.line' | translate}}
22+
</a>
23+
</span>
24+
<div id="command-div" *ngIf="isCommandLineVisible">
25+
<button class="repo-copy-btn pull-right" data-clipboard-target="#command-div"></button>
26+
<pre style="background-color: #d9edf7; color: #3a87ad">{{ command }}</pre>
27+
</div>
28+
<span>
29+
<a *ngIf="canDownloadAllFiles" class="btn btn-download" id="download-all-button" (click)="downloadFiles()"
30+
style="visibility: visible">
31+
<i style="display: block" class="fa fa-download fa-3x">&nbsp;</i>
32+
{{'item.page.download.button.all.files.zip' | translate}} ({{ totalFileSizes }})
33+
</a>
34+
</span>
2635
</div>
27-
<span>
28-
<a *ngIf="canDownloadAllFiles" class="btn btn-download" id="download-all-button" (click)="downloadFiles()"
29-
style="visibility: visible">
30-
<i style="display: block" class="fa fa-download fa-3x">&nbsp;</i>
31-
{{'item.page.download.button.all.files.zip' | translate}} ({{ totalFileSizes }})
32-
</a>
33-
</span>
36+
<ds-preview-section [item]="item"></ds-preview-section>
3437
</div>
35-
<ds-preview-section [item]="item"></ds-preview-section>
3638
</div>
3739
</div>
3840
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>

src/app/item-page/simple/item-page.component.spec.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,16 @@ import { MetadataBitstreamDataService } from 'src/app/core/data/metadata-bitstre
3131
import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock';
3232
import { ConfigurationProperty } from '../../core/shared/configuration-property.model';
3333
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
34+
import { MetadataValue } from '../../core/shared/metadata.models';
3435

3536
const mockItem: Item = Object.assign(new Item(), {
3637
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
37-
metadata: [],
38+
metadata: {
39+
'local.has.files': [Object.assign(new MetadataValue(), {
40+
value: 'yes',
41+
language: undefined
42+
})]
43+
},
3844
relationships: createRelationshipsObservable()
3945
});
4046

@@ -207,4 +213,16 @@ describe('ItemPageComponent', () => {
207213
});
208214
});
209215

216+
describe('when the item has the file', () => {
217+
it('should display license and files section', waitForAsync(async () => {
218+
comp.itemRD$ = createSuccessfulRemoteDataObject$(mockItem);
219+
fixture.detectChanges();
220+
221+
void fixture.whenStable().then(() => {
222+
const objectLoader = fixture.debugElement.query(By.css('ds-clarin-license-info'));
223+
expect(objectLoader.nativeElement).toBeDefined();
224+
});
225+
}));
226+
});
227+
210228
});

src/app/item-page/simple/item-page.component.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
1818
import { redirectOn4xx } from '../../core/shared/authorized.operators';
1919
import { RegistryService } from 'src/app/core/registry/registry.service';
2020
import { MetadataBitstream } from 'src/app/core/metadata/metadata-bitstream.model';
21-
import { Observable} from 'rxjs';
21+
import { BehaviorSubject, Observable } from 'rxjs';
2222
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
2323

2424
/**
@@ -103,6 +103,11 @@ export class ItemPageComponent implements OnInit {
103103

104104
canShowCurlDownload = false;
105105

106+
/**
107+
* True if the item has files, false otherwise.
108+
*/
109+
hasFiles: BehaviorSubject<boolean> = new BehaviorSubject(false);
110+
106111
constructor(
107112
protected route: ActivatedRoute,
108113
private router: Router,
@@ -127,7 +132,7 @@ export class ItemPageComponent implements OnInit {
127132
map((item) => getItemPageRoute(item))
128133
);
129134

130-
this.showTombstone();
135+
this.processItem();
131136

132137
this.registryService
133138
.getMetadataBitstream(this.itemHandle, 'ORIGINAL,TEXT,THUMBNAIL')
@@ -139,6 +144,14 @@ export class ItemPageComponent implements OnInit {
139144
});
140145
}
141146

147+
/**
148+
* Check if the item has files and assign the result into the `hasFiles` variable.
149+
* */
150+
private checkIfItemHasFiles(item: Item) {
151+
const hasFilesMetadata = item.metadata?.['local.has.files']?.[0]?.value;
152+
this.hasFiles.next(hasFilesMetadata !== 'no');
153+
}
154+
142155
sumFileSizes() {
143156
const sizeUnits = {
144157
B: 1,
@@ -167,7 +180,10 @@ export class ItemPageComponent implements OnInit {
167180
this.totalFileSizes = totalBytes.toFixed(2) + ' ' + finalUnit;
168181
}
169182

170-
showTombstone() {
183+
/**
184+
* Process the tombstone of the Item and check if it has files or not.
185+
*/
186+
processItem() {
171187
// if the item is withdrawn
172188
let isWithdrawn = false;
173189
// metadata value from `dc.relation.isreplacedby`
@@ -181,6 +197,9 @@ export class ItemPageComponent implements OnInit {
181197
this.itemHandle = item.handle;
182198
isWithdrawn = item.isWithdrawn;
183199
isReplaced = item.metadata['dc.relation.isreplacedby']?.[0]?.value;
200+
201+
// check if the item has files
202+
this.checkIfItemHasFiles(item);
184203
});
185204

186205
// do not show tombstone for non withdrawn items

src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock';
2626
import { HttpXsrfTokenExtractorMock } from '../../shared/mocks/http-xsrf-token-extractor.mock';
2727
import { getMockEntityTypeService } from './my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.spec';
2828
import { EntityTypeDataService } from '../../core/data/entity-type-data.service';
29+
import { of } from 'rxjs';
30+
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
2931

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

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

40+
const configurationServiceSpy = jasmine.createSpyObj('configurationService', {
41+
findByPropertyName: of({}),
42+
});
43+
3844
beforeEach(waitForAsync(() => {
3945
TestBed.configureTestingModule({
4046
imports: [
@@ -64,6 +70,7 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
6470
{ provide: CookieService, useValue: new CookieServiceMock() },
6571
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
6672
{ provide: EntityTypeDataService, useValue: getMockEntityTypeService() },
73+
{ provide: ConfigurationDataService, useValue: configurationServiceSpy },
6774
],
6875
schemas: [NO_ERRORS_SCHEMA]
6976
}).compileComponents();

src/app/shared/upload/uploader/uploader.component.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,19 @@ import { HttpXsrfTokenExtractor } from '@angular/common/http';
1414
import { CookieService } from '../../../core/services/cookie.service';
1515
import { CookieServiceMock } from '../../mocks/cookie.service.mock';
1616
import { HttpXsrfTokenExtractorMock } from '../../mocks/http-xsrf-token-extractor.mock';
17+
import { of } from 'rxjs';
18+
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
1719

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

2022
let testComp: TestComponent;
2123
let testFixture: ComponentFixture<TestComponent>;
2224
let html;
2325

26+
const configurationServiceSpy = jasmine.createSpyObj('configurationService', {
27+
findByPropertyName: of({}),
28+
});
29+
2430
// waitForAsync beforeEach
2531
beforeEach(waitForAsync(() => {
2632

@@ -40,6 +46,7 @@ describe('Chips component', () => {
4046
DragService,
4147
{ provide: HttpXsrfTokenExtractor, useValue: new HttpXsrfTokenExtractorMock('mock-token') },
4248
{ provide: CookieService, useValue: new CookieServiceMock() },
49+
{ provide: ConfigurationDataService, useValue: configurationServiceSpy },
4350
],
4451
schemas: [CUSTOM_ELEMENTS_SCHEMA]
4552
});

src/app/shared/upload/uploader/uploader.component.ts

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostListener, Input, Output, ViewEncapsulation, } from '@angular/core';
22

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

@@ -12,7 +12,14 @@ import { HttpXsrfTokenExtractor } from '@angular/common/http';
1212
import { XSRF_COOKIE, XSRF_REQUEST_HEADER, XSRF_RESPONSE_HEADER } from '../../../core/xsrf/xsrf.interceptor';
1313
import { CookieService } from '../../../core/services/cookie.service';
1414
import { DragService } from '../../../core/drag.service';
15+
import {ConfigurationDataService} from '../../../core/data/configuration-data.service';
16+
import { map } from 'rxjs/operators';
17+
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
18+
import { RemoteData } from '../../../core/data/remote-data';
19+
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
20+
import { TranslateService } from '@ngx-translate/core';
1521

22+
export const MAX_UPLOAD_FILE_SIZE_CFG_PROPERTY = 'spring.servlet.multipart.max-file-size';
1623
@Component({
1724
selector: 'ds-uploader',
1825
templateUrl: 'uploader.component.html',
@@ -90,7 +97,9 @@ export class UploaderComponent {
9097
private scrollToService: ScrollToService,
9198
private dragService: DragService,
9299
private tokenExtractor: HttpXsrfTokenExtractor,
93-
private cookieService: CookieService
100+
private cookieService: CookieService,
101+
private configurationService: ConfigurationDataService,
102+
private translate: TranslateService
94103
) {
95104
}
96105

@@ -129,7 +138,20 @@ export class UploaderComponent {
129138
if (isUndefined(this.onBeforeUpload)) {
130139
this.onBeforeUpload = () => {return;};
131140
}
132-
this.uploader.onBeforeUploadItem = (item) => {
141+
this.uploader.onBeforeUploadItem = async (item) => {
142+
// Check if the file size is within the maximum upload size
143+
const canUpload = await this.checkFileSizeLimit(item);
144+
// If the file size is too large, emit an error and cancel all uploads
145+
if (!canUpload) {
146+
this.onUploadError.emit({
147+
item: item,
148+
response: this.translate.instant('submission.sections.upload.upload-failed.size-limit-exceeded'),
149+
status: 400,
150+
headers: {}
151+
});
152+
this.uploader.cancelAll();
153+
return;
154+
}
133155
if (item.url !== this.uploader.options.url) {
134156
item.url = this.uploader.options.url;
135157
}
@@ -225,4 +247,38 @@ export class UploaderComponent {
225247
this.cookieService.set(XSRF_COOKIE, token);
226248
}
227249

250+
// Check if the file size is within the maximum upload size
251+
private async checkFileSizeLimit(item: FileItem): Promise<boolean> {
252+
const maxFileUploadSize = await firstValueFrom(this.getMaxFileUploadSizeFromCfg());
253+
if (maxFileUploadSize) {
254+
const maxSizeInGigabytes = parseInt(maxFileUploadSize?.[0], 10);
255+
const maxSizeInBytes = this.gigabytesToBytes(maxSizeInGigabytes);
256+
// If maxSizeInBytes is -1, it means the value in the config is invalid. The file won't be uploaded and the user
257+
// will see error messages in the UI.
258+
if (maxSizeInBytes === -1) {
259+
return false;
260+
}
261+
return item?.file?.size <= maxSizeInBytes;
262+
}
263+
return false;
264+
}
265+
266+
// Convert gigabytes to bytes
267+
private gigabytesToBytes(gigabytes: number): number {
268+
if (typeof gigabytes !== 'number' || isNaN(gigabytes) || !isFinite(gigabytes) || gigabytes < 0) {
269+
return -1;
270+
}
271+
return gigabytes * Math.pow(2, 30); // 2^30 bytes in a gigabyte
272+
}
273+
274+
// Get the maximum file upload size from the configuration
275+
public getMaxFileUploadSizeFromCfg(): Observable<string[]> {
276+
return this.configurationService.findByPropertyName(MAX_UPLOAD_FILE_SIZE_CFG_PROPERTY).pipe(
277+
getFirstCompletedRemoteData(),
278+
map((propertyRD: RemoteData<ConfigurationProperty>) => {
279+
return propertyRD.hasSucceeded ? propertyRD.payload.values : [];
280+
})
281+
);
282+
}
283+
228284
}

src/app/submission/form/submission-upload-files/submission-upload-files.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
[onBeforeUpload]="onBeforeUpload"
66
[uploadFilesOptions]="uploadFilesOptions"
77
(onCompleteItem)="onCompleteItem($event)"
8-
(onUploadError)="onUploadError()"></ds-uploader>
8+
(onUploadError)="onUploadError($event)"></ds-uploader>

src/app/submission/form/submission-upload-files/submission-upload-files.component.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,12 @@ export class SubmissionUploadFilesComponent implements OnChanges {
155155
/**
156156
* Show error notification on upload fails
157157
*/
158-
public onUploadError() {
159-
this.notificationsService.error(null, this.translate.get('submission.sections.upload.upload-failed'));
158+
public onUploadError(event: any) {
159+
const errorMessageUploadLimit = this.translate.instant('submission.sections.upload.upload-failed.size-limit-exceeded');
160+
const defaultErrorMessage = this.translate.instant('submission.sections.upload.upload-failed');
161+
const errorMessage = event?.response === errorMessageUploadLimit ? errorMessageUploadLimit : defaultErrorMessage;
162+
163+
this.notificationsService.error(null, errorMessage);
160164
}
161165

162166
/**

src/assets/i18n/en.json5

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5081,6 +5081,8 @@
50815081

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

5084+
"submission.sections.upload.upload-failed.size-limit-exceeded": "File size exceeds the maximum upload size",
5085+
50845086
"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.",
50855087

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

0 commit comments

Comments
 (0)