Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

load bar for large files #2559

Merged
merged 7 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion shanoir-ng-front/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@
"src/assets"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css",
"src/styles.css"
],
"scripts": []
Expand Down
11 changes: 10 additions & 1 deletion shanoir-ng-front/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { StudyService } from './studies/shared/study.service';
import { TreeService } from './studies/study/tree.service';
import { UserService } from './users/shared/user.service';
import { ServiceLocator } from './utils/locator.service';
import { Observable } from 'rxjs';
import { NotificationsService } from './shared/notifications/notifications.service';


@Component({
Expand All @@ -51,7 +53,8 @@ export class AppComponent {
protected router: Router,
private studyService: StudyService,
private userService: UserService,
public treeService: TreeService) {
public treeService: TreeService,
private notificationsService: NotificationsService) {

ServiceLocator.rootViewContainerRef = this.viewContainerRef;
}
Expand All @@ -70,6 +73,12 @@ export class AppComponent {
this.windowService.width = event.target.innerWidth;
}

@HostListener('window:beforeunload')
canDeactivate(): boolean {
return !this.notificationsService.hasOnGoingDownloads();
}


toggleMenu(open: boolean) {
this.menuOpen = open;
}
Expand Down
2 changes: 2 additions & 0 deletions shanoir-ng-front/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ import { LoginGuard } from "./shared/roles/login-guard";
import { AccessRequestService } from './users/access-request/access-request.service';
import { AccessRequestListComponent } from './users/access-request/access-request-list.component';
import { MassDownloadService } from './shared/mass-download/mass-download.service';
import { SingleDownloadService } from './shared/mass-download/single-download.service';
import { ClipboardModule } from '@angular/cdk/clipboard';
import { TaskStatusComponent } from './async-tasks/status/task-status.component';
import { DatasetCopyDialogComponent } from "./shared/components/dataset-copy-dialog/dataset-copy-dialog.component";
Expand Down Expand Up @@ -548,6 +549,7 @@ import { DoubleAwesomeComponent } from './shared/double-awesome/double-awesome.c
QualityCardService,
QualityCardDTOService,
MassDownloadService,
SingleDownloadService,
SessionService,
ShanoirEventService,
TreeService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html
*/
import { HttpClient, HttpResponse } from '@angular/common/http';
import { Injectable, OnDestroy } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

import { TaskState, TaskStatus } from 'src/app/async-tasks/task.model';
import { TaskState } from 'src/app/async-tasks/task.model';
import { SingleDownloadService } from 'src/app/shared/mass-download/single-download.service';
import { EntityService } from '../../shared/components/entity/entity.abstract.service';
import { Page, Pageable } from '../../shared/components/table/pageable.model';
import * as AppUtils from '../../utils/app.utils';
Expand All @@ -30,7 +31,7 @@ export class ExaminationService extends EntityService<Examination> {

API_URL = AppUtils.BACKEND_API_EXAMINATION_URL;

constructor(protected http: HttpClient) {
constructor(protected http: HttpClient, private downloadService: SingleDownloadService) {
super(http)
}
protected examinationDtoService: ExaminationDTOService = ServiceLocator.injector.get(ExaminationDTOService);
Expand Down Expand Up @@ -82,11 +83,7 @@ export class ExaminationService extends EntityService<Examination> {

downloadFile(fileName: string, examId: number, state?: TaskState): Observable<TaskState> {
const endpoint: string = this.API_URL + '/extra-data-download/' + examId + "/" + fileName + "/";
return AppUtils.downloadWithStatusGET(endpoint, null, state);
}

private downloadIntoBrowser(response: HttpResponse<Blob>){
AppUtils.browserDownloadFileFromResponse(response);
return this.downloadService.downloadSingleFile(endpoint, null, state);
}

public stringify(entity: Examination) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,20 @@
* along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html
*/

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

import { SingleDownloadService } from 'src/app/shared/mass-download/single-download.service';
import { EntityService } from '../../../../shared/components/entity/entity.abstract.service';
import { ExtraData } from './extradata.model';
import * as PreclinicalUtils from '../../../utils/preclinical.utils';
import * as AppUtils from '../../../../utils/app.utils';
import { HttpClient, HttpResponse } from '@angular/common/http';
import { ExtraData } from './extradata.model';

@Injectable()
export class ExtraDataService extends EntityService<ExtraData>{

API_URL = PreclinicalUtils.PRECLINICAL_API_EXAMINATION_URL;

constructor(protected http: HttpClient) {
constructor(protected http: HttpClient, private downloadService: SingleDownloadService) {
super(http)
}

Expand All @@ -39,7 +38,8 @@ export class ExtraDataService extends EntityService<ExtraData>{
.then(entities => entities?.map((entity) => this.toRealObject(entity)) || []);
}

getExtraData(id:string): Promise<ExtraData>{
getExtraData(id:string): Promise<ExtraData> {
console.log('prout')
return this.http.get<ExtraData>(PreclinicalUtils.PRECLINICAL_API_EXAMINATION_URL+"/"+id)
.toPromise()
.then((entity) => this.toRealObject(entity));
Expand All @@ -48,16 +48,7 @@ export class ExtraDataService extends EntityService<ExtraData>{

downloadFile(examId: number): Promise<void> {
const endpoint = this.API_URL + '/extradata/download/' + examId;
return this.http.get(endpoint, { observe: 'response', responseType: 'blob' }
).toPromise().then(
response => {
this.downloadIntoBrowser(response);
}
);
}

private downloadIntoBrowser(response: HttpResponse<Blob>){
AppUtils.browserDownloadFileFromResponse(response);
return this.downloadService.downloadSingleFile(endpoint).toPromise().then(() => null);
}

createExtraData(datatype:string,extradata: any): Promise<any> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@
import { formatDate } from '@angular/common';
import { HttpResponse } from '@angular/common/http';
import { ComponentRef, Injectable } from '@angular/core';
import { AngularDeviceInformationService } from 'angular-device-information';
import { Observable, race, Subscription } from 'rxjs';
import { last, map, take } from 'rxjs/operators';
import { Task, TaskState } from 'src/app/async-tasks/task.model';
import { Dataset } from 'src/app/datasets/shared/dataset.model';
import { DatasetService, Format } from 'src/app/datasets/shared/dataset.service';
import { getSizeStr, StrictUnion } from 'src/app/utils/app.utils';
import { ServiceLocator } from 'src/app/utils/locator.service';
import { SuperPromise } from 'src/app/utils/super-promise';
import { ConfirmDialogService } from '../components/confirm-dialog/confirm-dialog.service';
import { ConsoleService } from '../console/console.service';
import { ShanoirError } from '../models/error.model';
import { NotificationsService } from '../notifications/notifications.service';
import { SessionService } from '../services/session.service';
import { DownloadSetupAltComponent } from './download-setup-alt/download-setup-alt.component';
import { DownloadSetupComponent } from './download-setup/download-setup.component';
import { Queue } from './queue.model';
import { SessionService } from '../services/session.service';
import { ShanoirError } from '../models/error.model';
import { StrictUnion, getSizeStr } from 'src/app/utils/app.utils';
import { AngularDeviceInformationService } from 'angular-device-information';
import { Observable, race, Subscription } from 'rxjs';

declare var JSZip: any;

Expand Down Expand Up @@ -619,7 +619,7 @@ export class MassDownloadService {
this.consoleService.log('error', 'Can\'t parse the status from the recorded message', [e, task?.report]);
return null;
}
}
}
}

export class DownloadSetup {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Shanoir NG - Import, manage and share neuroimaging data
* Copyright (C) 2009-2019 Inria - https://www.inria.fr/
* Contact us on https://project.inria.fr/shanoir/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html
*/

import { HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Task, TaskState } from 'src/app/async-tasks/task.model';
import * as AppUtils from '../../utils/app.utils';
import { NotificationsService } from '../notifications/notifications.service';
import { SessionService } from '../services/session.service';


@Injectable()
export class SingleDownloadService {

constructor(
private notificationService: NotificationsService,
private sessionService: SessionService) {
}

/**
* Handles large files. Download a single file, displaying a loading bar in the side menu if the dl takes more than 5s.
*
* It is impossible to nicely use the browser dl for large files.
* It's because the browser need a dl through a <a href> to do so, and we can't as we have an auth token to put in a http header.
* But using js makes the browser dl the file silently, then copying the blob to the dl dir with the nice browser display.
* So with large files, the silent step is confusing for users. Here we will help her/him by displaying the progress.
*
* @param url
* @param params
* @param state
* @param totalSize total size of the file, if known
* @returns
*/
downloadSingleFile(url: string, params?: HttpParams, state?: TaskState): Observable<TaskState> {
let obs: Observable<TaskState> = AppUtils.downloadWithStatusGET(url, params, state);

let task: Task = new Task();
task.id = Date.now();
task.creationDate = new Date();
task.lastUpdate = task.creationDate;
task.message = 'Downloading ' + (url + '').split('/').pop();
task.progress = 0;
task.status = 2;
task.eventType = 'downloadFile.event';
task.sessionId = this.sessionService.sessionId;

let startTs: number = Date.now();
obs.subscribe(event => {
let ts: number = Date.now();
if (ts - startTs > 5000) {
if (event.progress) {
task.progress = event.progress;
}
task.status = event.status;
if (task.status == 1) task.progress = 1;
this.notificationService.pushLocalTask(task);
}
});

return obs;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ export class NotificationsService {
newTask.creationDate = new Date(task.creationDate as string);
newTask.lastUpdate = new Date(task.lastUpdate as string);
return newTask;
})?.filter(task => { // remove single files downloads that have been interrupted or are over
return task.eventType != 'downloadFile.event' || task.sessionId == this.sessionService.sessionId;
});
}
this.localTasks = storageTasks;
Expand Down Expand Up @@ -247,4 +249,10 @@ export class NotificationsService {
this.tasksInProgress.forEach(task => total += task.progress);
return total/this.tasksInProgress.length;
}

hasOnGoingDownloads(): boolean {
return !!this.tasksInProgress.find(task => {
return ['downloadDataset.event', 'downloadFile.event'].includes(task.eventType);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@
<ng-template [ngSwitchCase]="'downloadDataset.event'">
<i class="fa-solid fa-download"></i>
</ng-template>
<ng-template [ngSwitchCase]="'downloadFile.event'">
<i class="fa-solid fa-download"></i>
</ng-template>
<ng-template [ngSwitchCase]="'importDataset.event'">
<i class="fa-solid fa-upload"></i>
</ng-template>
Expand Down
17 changes: 11 additions & 6 deletions shanoir-ng-front/src/app/studies/shared/study.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
*/
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { Observable } from 'rxjs';
import { Observable, Subscription } from 'rxjs';

import { TaskState } from 'src/app/async-tasks/task.model';
import { SingleDownloadService } from 'src/app/shared/mass-download/single-download.service';
import { Tag } from 'src/app/tags/tag.model';
import { DataUserAgreement } from '../../dua/shared/dua.model';
import { EntityService } from '../../shared/components/entity/entity.abstract.service';
Expand Down Expand Up @@ -46,8 +46,9 @@ export class StudyService extends EntityService<Study> implements OnDestroy {
fileUploads: Map<number, Promise<void>> = new Map(); // current uploads
private studyVolumesCache: Map<number, StudyStorageVolumeDTO> = new Map();

constructor(protected http: HttpClient, private keycloakService: KeycloakService, private studyDTOService: StudyDTOService) {
super(http)
constructor(protected http: HttpClient, private keycloakService: KeycloakService, private studyDTOService: StudyDTOService,
private downloadService: SingleDownloadService) {
super(http);
}

getEntityInstance() { return new Study(); }
Expand Down Expand Up @@ -172,12 +173,16 @@ export class StudyService extends EntityService<Study> implements OnDestroy {

downloadProtocolFile(fileName: string, studyId: number, state?: TaskState) {
const endpoint = this.API_URL + '/protocol-file-download/' + studyId + "/" + fileName + "/";
return AppUtils.downloadWithStatusGET(endpoint, null, state);
return this.downloadService.downloadSingleFile(endpoint, null, state);
}

buildProtocolFileUrl(fileName: string, studyId: number): string {
return this.API_URL + '/protocol-file-download/' + studyId + "/" + fileName;
}

downloadDuaFile(fileName: string, studyId: number, state?: TaskState) {
const endpoint = this.API_URL + '/dua-download/' + studyId + "/" + fileName + "/";
return AppUtils.downloadWithStatusGET(endpoint, null, state);
return this.downloadService.downloadSingleFile(endpoint, null, state);
}

downloadDuaBlob(fileName: string, studyId: number): Promise<Blob> {
Expand Down
4 changes: 4 additions & 0 deletions shanoir-ng-front/src/app/studies/study/study.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,10 @@ export class StudyComponent extends EntityComponent<Study> {
this.studyService.downloadProtocolFile(file, this.study.id, this.pdfDownloadState);
}

public builFileUrl(file): string {
return this.studyService.buildProtocolFileUrl(file, this.study.id);
}

public attachNewFile(event: any) {
let fileToAdd = event.target.files[0];
this.protocolFiles.push(fileToAdd);
Expand Down
Loading
Loading