diff --git a/src/app/item-page/simple/item-page.component.html b/src/app/item-page/simple/item-page.component.html
index 8d2c9048344..362a819e57c 100644
--- a/src/app/item-page/simple/item-page.component.html
+++ b/src/app/item-page/simple/item-page.component.html
@@ -10,29 +10,31 @@
-
-
{{'item.page.files.head' | translate}}
-
diff --git a/src/app/item-page/simple/item-page.component.spec.ts b/src/app/item-page/simple/item-page.component.spec.ts
index 8c0bb70cdda..049a8b7494b 100644
--- a/src/app/item-page/simple/item-page.component.spec.ts
+++ b/src/app/item-page/simple/item-page.component.spec.ts
@@ -31,10 +31,16 @@ import { MetadataBitstreamDataService } from 'src/app/core/data/metadata-bitstre
import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock';
import { ConfigurationProperty } from '../../core/shared/configuration-property.model';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
+import { MetadataValue } from '../../core/shared/metadata.models';
const mockItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
- metadata: [],
+ metadata: {
+ 'local.has.files': [Object.assign(new MetadataValue(), {
+ value: 'yes',
+ language: undefined
+ })]
+ },
relationships: createRelationshipsObservable()
});
@@ -207,4 +213,16 @@ describe('ItemPageComponent', () => {
});
});
+ describe('when the item has the file', () => {
+ it('should display license and files section', waitForAsync(async () => {
+ comp.itemRD$ = createSuccessfulRemoteDataObject$(mockItem);
+ fixture.detectChanges();
+
+ void fixture.whenStable().then(() => {
+ const objectLoader = fixture.debugElement.query(By.css('ds-clarin-license-info'));
+ expect(objectLoader.nativeElement).toBeDefined();
+ });
+ }));
+ });
+
});
diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts
index cff0161d254..99abb4b0493 100644
--- a/src/app/item-page/simple/item-page.component.ts
+++ b/src/app/item-page/simple/item-page.component.ts
@@ -18,7 +18,7 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
import { redirectOn4xx } from '../../core/shared/authorized.operators';
import { RegistryService } from 'src/app/core/registry/registry.service';
import { MetadataBitstream } from 'src/app/core/metadata/metadata-bitstream.model';
-import { Observable} from 'rxjs';
+import { BehaviorSubject, Observable } from 'rxjs';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
/**
@@ -103,6 +103,11 @@ export class ItemPageComponent implements OnInit {
canShowCurlDownload = false;
+ /**
+ * True if the item has files, false otherwise.
+ */
+ hasFiles: BehaviorSubject = new BehaviorSubject(false);
+
constructor(
protected route: ActivatedRoute,
private router: Router,
@@ -127,7 +132,7 @@ export class ItemPageComponent implements OnInit {
map((item) => getItemPageRoute(item))
);
- this.showTombstone();
+ this.processItem();
this.registryService
.getMetadataBitstream(this.itemHandle, 'ORIGINAL,TEXT,THUMBNAIL')
@@ -139,6 +144,14 @@ export class ItemPageComponent implements OnInit {
});
}
+ /**
+ * Check if the item has files and assign the result into the `hasFiles` variable.
+ * */
+ private checkIfItemHasFiles(item: Item) {
+ const hasFilesMetadata = item.metadata?.['local.has.files']?.[0]?.value;
+ this.hasFiles.next(hasFilesMetadata !== 'no');
+ }
+
sumFileSizes() {
const sizeUnits = {
B: 1,
@@ -167,7 +180,10 @@ export class ItemPageComponent implements OnInit {
this.totalFileSizes = totalBytes.toFixed(2) + ' ' + finalUnit;
}
- showTombstone() {
+ /**
+ * Process the tombstone of the Item and check if it has files or not.
+ */
+ processItem() {
// if the item is withdrawn
let isWithdrawn = false;
// metadata value from `dc.relation.isreplacedby`
@@ -181,6 +197,9 @@ export class ItemPageComponent implements OnInit {
this.itemHandle = item.handle;
isWithdrawn = item.isWithdrawn;
isReplaced = item.metadata['dc.relation.isreplacedby']?.[0]?.value;
+
+ // check if the item has files
+ this.checkIfItemHasFiles(item);
});
// do not show tombstone for non withdrawn items
diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts
index ed61fab1d62..c85b5166c37 100644
--- a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts
+++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts
@@ -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', () => {
@@ -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: [
@@ -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();
diff --git a/src/app/shared/upload/uploader/uploader.component.spec.ts b/src/app/shared/upload/uploader/uploader.component.spec.ts
index 8ea23c8acbd..90762875ca1 100644
--- a/src/app/shared/upload/uploader/uploader.component.spec.ts
+++ b/src/app/shared/upload/uploader/uploader.component.spec.ts
@@ -14,6 +14,8 @@ 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', () => {
@@ -21,6 +23,10 @@ describe('Chips component', () => {
let testFixture: ComponentFixture;
let html;
+ const configurationServiceSpy = jasmine.createSpyObj('configurationService', {
+ findByPropertyName: of({}),
+ });
+
// waitForAsync beforeEach
beforeEach(waitForAsync(() => {
@@ -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]
});
diff --git a/src/app/shared/upload/uploader/uploader.component.ts b/src/app/shared/upload/uploader/uploader.component.ts
index 14b1ca9b94f..c8963847e92 100644
--- a/src/app/shared/upload/uploader/uploader.component.ts
+++ b/src/app/shared/upload/uploader/uploader.component.ts
@@ -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';
@@ -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',
@@ -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
) {
}
@@ -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;
}
@@ -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 {
+ 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 {
+ return this.configurationService.findByPropertyName(MAX_UPLOAD_FILE_SIZE_CFG_PROPERTY).pipe(
+ getFirstCompletedRemoteData(),
+ map((propertyRD: RemoteData) => {
+ return propertyRD.hasSucceeded ? propertyRD.payload.values : [];
+ })
+ );
+ }
+
}
diff --git a/src/app/submission/form/submission-upload-files/submission-upload-files.component.html b/src/app/submission/form/submission-upload-files/submission-upload-files.component.html
index dfad8c422ec..cf916fb413b 100644
--- a/src/app/submission/form/submission-upload-files/submission-upload-files.component.html
+++ b/src/app/submission/form/submission-upload-files/submission-upload-files.component.html
@@ -5,4 +5,4 @@
[onBeforeUpload]="onBeforeUpload"
[uploadFilesOptions]="uploadFilesOptions"
(onCompleteItem)="onCompleteItem($event)"
- (onUploadError)="onUploadError()">
+ (onUploadError)="onUploadError($event)">
diff --git a/src/app/submission/form/submission-upload-files/submission-upload-files.component.ts b/src/app/submission/form/submission-upload-files/submission-upload-files.component.ts
index 721a6c108b6..d16500a8640 100644
--- a/src/app/submission/form/submission-upload-files/submission-upload-files.component.ts
+++ b/src/app/submission/form/submission-upload-files/submission-upload-files.component.ts
@@ -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);
}
/**
diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5
index 5749e515832..0ec92262265 100644
--- a/src/assets/i18n/en.json5
+++ b/src/assets/i18n/en.json5
@@ -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",