From a44c5613987d0b81010fdbb0fce6443e998a78c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Tue, 8 Mar 2022 22:05:09 +0100 Subject: [PATCH] WIP => download file actions ngrx --- .../result/datatable-tab.component.html | 4 +- .../result/datatable-tab.component.ts | 2 +- .../result/datatable.component.html | 2 +- .../components/result/datatable.component.ts | 2 +- .../result/download-file-tab.component.html | 2 +- .../components/result/download.component.html | 15 +- .../components/result/download.component.ts | 110 +--------- .../renderer/download-renderer.component.ts | 13 +- .../search/containers/result.component.html | 5 +- .../search/containers/result.component.ts | 23 +- .../store/actions/download-file.actions.ts | 20 +- .../store/effects/download-file.effects.ts | 206 +++++++++++++++++- .../store/models/download-file.model.ts | 8 +- .../store/reducers/download-file.reducer.ts | 39 +++- .../store/services/download-file.service.ts | 27 ++- .../Action/StartTaskCreateResultAction.php | 10 +- tasks/src/anis_tasks/result.py | 3 +- 17 files changed, 339 insertions(+), 152 deletions(-) diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.html b/client/src/app/instance/search/components/result/datatable-tab.component.html index e4b9620c..0feb3064 100644 --- a/client/src/app/instance/search/components/result/datatable-tab.component.html +++ b/client/src/app/instance/search/components/result/datatable-tab.component.html @@ -19,7 +19,7 @@ [dataLength]="dataLength" [sampRegistered]="sampRegistered" (broadcast)="broadcast.emit($event)" - (startsDownloadingFile)="startsDownloadingFile.emit($event)"> + (downloadingFile)="downloadingFile.emit($event)"> </app-datatable-actions> <app-datatable [dataset]="datasetList | datasetByName:datasetSelected" @@ -35,7 +35,7 @@ (retrieveData)="retrieveData.emit($event)" (addSelectedData)="addSelectedData.emit($event)" (deleteSelectedData)="deleteSelectedData.emit($event)" - (startsDownloadingFile)="startsDownloadingFile.emit($event)"> + (downloadingFile)="downloadingFile.emit($event)"> </app-datatable> </accordion-group> </accordion> diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.ts b/client/src/app/instance/search/components/result/datatable-tab.component.ts index 4eee5ec4..af78907c 100644 --- a/client/src/app/instance/search/components/result/datatable-tab.component.ts +++ b/client/src/app/instance/search/components/result/datatable-tab.component.ts @@ -40,5 +40,5 @@ export class DatatableTabComponent { @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter(); @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter(); @Output() broadcast: EventEmitter<string> = new EventEmitter(); - @Output() startsDownloadingFile: EventEmitter<{url: string, filename: string}> = new EventEmitter(); + @Output() downloadingFile: EventEmitter<{url: string, fileId: string, datasetName: string, filename: string}> = new EventEmitter(); } diff --git a/client/src/app/instance/search/components/result/datatable.component.html b/client/src/app/instance/search/components/result/datatable.component.html index f4b0078f..c1df3527 100644 --- a/client/src/app/instance/search/components/result/datatable.component.html +++ b/client/src/app/instance/search/components/result/datatable.component.html @@ -63,7 +63,7 @@ [datasetName]="dataset.name" [datasetPublic]="dataset.public" [config]="getRendererConfig(attribute)" - (startsDownloadingFile)="startsDownloadingFile.emit($event)"> + (downloadingFile)="downloadingFile.emit($event)"> </app-download-renderer> </div> <div *ngSwitchCase="'image'"> diff --git a/client/src/app/instance/search/components/result/datatable.component.ts b/client/src/app/instance/search/components/result/datatable.component.ts index 33191d25..35be1904 100644 --- a/client/src/app/instance/search/components/result/datatable.component.ts +++ b/client/src/app/instance/search/components/result/datatable.component.ts @@ -44,7 +44,7 @@ export class DatatableComponent implements OnInit { @Output() retrieveData: EventEmitter<Pagination> = new EventEmitter(); @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter(); @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter(); - @Output() startsDownloadingFile: EventEmitter<{url: string, filename: string}> = new EventEmitter(); + @Output() downloadingFile: EventEmitter<{url: string, fileId: string, datasetName: string, filename: string}> = new EventEmitter(); public page = 1; public nbItems = 10; diff --git a/client/src/app/instance/search/components/result/download-file-tab.component.html b/client/src/app/instance/search/components/result/download-file-tab.component.html index b47e6be3..49f60cee 100644 --- a/client/src/app/instance/search/components/result/download-file-tab.component.html +++ b/client/src/app/instance/search/components/result/download-file-tab.component.html @@ -3,7 +3,7 @@ Files downloaded : <ul> <li *ngFor="let downloadFile of downloadedFiles"> - {{ downloadFile.name }} : + {{ downloadFile.fileName }} : <progressbar [value]="downloadFile.progress" [type]="getType(downloadFile.progress)" [animate]="true">{{ downloadFile.progress }}%</progressbar> </li> </ul> diff --git a/client/src/app/instance/search/components/result/download.component.html b/client/src/app/instance/search/components/result/download.component.html index fc7758ea..408855a5 100644 --- a/client/src/app/instance/search/components/result/download.component.html +++ b/client/src/app/instance/search/components/result/download.component.html @@ -18,19 +18,19 @@ <p>Download results just here:</p> </div> <div class="col"> - <a *ngIf="getConfigDownloadResultFormat('download_csv')" [href]="getUrl('csv')" (click)="click($event, getUrl('csv'), 'csv')" class="btn btn-outline-primary" title="Download results in CSV format"> + <a *ngIf="getConfigDownloadResultFormat('download_csv')" (click)="downloadResult('csv')" class="btn btn-outline-primary" title="Download results in CSV format"> <span class="fas fa-file-csv"></span> CSV </a> - <a *ngIf="getConfigDownloadResultFormat('download_ascii')" [href]="getUrl('ascii')" (click)="click($event, getUrl('ascii'), 'txt')" class="btn btn-outline-primary" title="Download results in ASCII format"> + <a *ngIf="getConfigDownloadResultFormat('download_ascii')" (click)="downloadResult('ascii')" class="btn btn-outline-primary" title="Download results in ASCII format"> <span class="fas fa-file"></span> ASCII </a> - <a *ngIf="getConfigDownloadResultFormat('download_vo')" [href]="getUrl('votable')" (click)="click($event, getUrl('votable'), 'xml')" class="btn btn-outline-primary" title="Download results in VO format"> + <a *ngIf="getConfigDownloadResultFormat('download_vo')" (click)="downloadResult('votable')" class="btn btn-outline-primary" title="Download results in VO format"> <span class="fas fa-file"></span> VOtable </a> - <button *ngIf="getConfigDownloadResultFormat('download_vo')" [disabled]="!sampRegistered" (click)="broadcastVotable()" class="btn btn-outline-primary" title="Broadcast samp votable"> + <button *ngIf="getConfigDownloadResultFormat('download_vo')" [disabled]="!sampRegistered" (click)="broadcast.emit()" class="btn btn-outline-primary" title="Broadcast samp votable"> <span class="fas fa-broadcast-tower"></span> Broadcast VOtable </button> </div> @@ -41,14 +41,9 @@ <p>Download archive files just here:</p> </div> <div class="col"> - <a *ngIf="!archiveInProgress" [href]="createFilesArchiveUrl()" (click)="createFilesArchive($event, createFilesArchiveUrl())" class="btn btn-outline-primary" title="Download an archive with all files"> + <a (click)="startTaskCreateArchive.emit()" class="btn btn-outline-primary" title="Download an archive with all files"> <span class="fas fa-archive"></span> Files archive </a> - <div *ngIf="archiveInProgress"> - <span class="fas fa-circle-notch fa-spin fa-3x"></span> - <span class="sr-only">Loading...</span> - Please wait archive is under construction... - </div> </div> </div> </div> diff --git a/client/src/app/instance/search/components/result/download.component.ts b/client/src/app/instance/search/components/result/download.component.ts index 95bd94a0..d6c2323b 100644 --- a/client/src/app/instance/search/components/result/download.component.ts +++ b/client/src/app/instance/search/components/result/download.component.ts @@ -7,14 +7,10 @@ * file that was distributed with this source code. */ -import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { interval, Subscription} from 'rxjs'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { Criterion, ConeSearch, criterionToString } from '../../../store/models'; +import { Criterion, ConeSearch } from '../../../store/models'; import { Dataset } from 'src/app/metamodel/models'; -import { getHost } from 'src/app/shared/utils'; -import { AppConfigService } from 'src/app/app-config.service'; /** * @class @@ -24,7 +20,7 @@ import { AppConfigService } from 'src/app/app-config.service'; selector: 'app-download', templateUrl: 'download.component.html' }) -export class DownloadComponent implements OnDestroy { +export class DownloadComponent { @Input() datasetSelected: string; @Input() datasetList: Dataset[]; @Input() criteriaList: Criterion[]; @@ -33,14 +29,8 @@ export class DownloadComponent implements OnDestroy { @Input() dataLength: number; @Input() sampRegistered: boolean; @Output() broadcast: EventEmitter<string> = new EventEmitter(); - @Output() startsDownloadingFile: EventEmitter<{url: string, filename: string}> = new EventEmitter(); - - archiveName = ''; - archiveId =''; - archiveInProgress = false; - archiveIsAvailableSubscription: Subscription - - constructor(private appConfig: AppConfigService, private http: HttpClient) { } + @Output() startTaskCreateResult: EventEmitter<{ format: string, selectedData: boolean }> = new EventEmitter(); + @Output() startTaskCreateArchive: EventEmitter<{}> = new EventEmitter(); /** * Checks if download tab has to be display. @@ -74,92 +64,10 @@ export class DownloadComponent implements OnDestroy { return dataset.config.download[format]; } - /** - * Returns URL to download file for the given format. - * - * @param {string} format - The file format to download. - * - * @return string - */ - getUrl(format: string): string { - let query: string = `${getHost(this.appConfig.apiUrl)}/search/${this.datasetSelected}?a=${this.outputList.join(';')}`; - if (this.criteriaList.length > 0) { - query += `&c=${this.criteriaList.map(criterion => criterionToString(criterion)).join(';')}`; - } - if (this.coneSearch) { - query += `&cs=${this.coneSearch.ra}:${this.coneSearch.dec}:${this.coneSearch.radius}`; - } - query += `&f=${format}`; - return query; - } - - /** - * Allows to download file. - */ - click(event, href, extension): void { - event.preventDefault(); - - const url = href; - const filename = `${this.datasetSelected}.${extension}`; - - this.startsDownloadingFile.emit({ url, filename }); - } - - /** - * Emits event to action to broadcast data. - * - * @fires EventEmitter<string> - */ - broadcastVotable(): void { - this.broadcast.emit(this.getUrl('votable')); - } - - /** - * Returns URL to download archive. - * - * @return boolean - */ - createFilesArchiveUrl(): string { - let query: string = `${getHost(this.appConfig.apiUrl)}/start-task-create-archive/${this.datasetSelected}?a=${this.outputList.join(';')}`; - if (this.criteriaList.length > 0) { - query += `&c=${this.criteriaList.map(criterion => criterionToString(criterion)).join(';')}`; - } - if (this.coneSearch) { - query += `&cs=${this.coneSearch.ra}:${this.coneSearch.dec}:${this.coneSearch.radius}`; - } - return query; - } - - getArchiveUrl() { - return `${getHost(this.appConfig.apiUrl)}/download-archive/${this.datasetSelected}/${this.archiveId}`; - } - - testArchiveIsAvailable() { - const url = `${getHost(this.appConfig.apiUrl)}/is-archive-available/${this.archiveId}`; - this.http.get<{"archive_is_available": boolean}>(url).subscribe(data => { - if (data.archive_is_available) { - this.archiveInProgress = false; - this.archiveIsAvailableSubscription.unsubscribe(); - this.startsDownloadingFile.emit({ url: this.getArchiveUrl(), filename: this.archiveName }) - } + downloadResult(format: string) { + this.startTaskCreateResult.emit({ + format, + selectedData: false }); } - - /** - * Starts the creation of the files archive - */ - createFilesArchive(event, href): void { - event.preventDefault(); - - this.http.get<{"archive_name": string, "archive_id": string}>(href).subscribe(data => { - this.archiveInProgress = true; - this.archiveName = data.archive_name; - this.archiveId = data.archive_id; - this.archiveIsAvailableSubscription = interval(1000).subscribe(() => this.testArchiveIsAvailable()); - }); - } - - ngOnDestroy() { - this.archiveIsAvailableSubscription.unsubscribe(); - } } diff --git a/client/src/app/instance/search/components/result/renderer/download-renderer.component.ts b/client/src/app/instance/search/components/result/renderer/download-renderer.component.ts index 50c55181..b7eb941f 100644 --- a/client/src/app/instance/search/components/result/renderer/download-renderer.component.ts +++ b/client/src/app/instance/search/components/result/renderer/download-renderer.component.ts @@ -27,7 +27,7 @@ export class DownloadRendererComponent { @Input() datasetName: string; @Input() datasetPublic: boolean; @Input() config: DownloadRendererConfig; - @Output() startsDownloadingFile: EventEmitter<{url: string, filename: string}> = new EventEmitter(); + @Output() downloadingFile: EventEmitter<{url: string, fileId: string, datasetName: string, filename: string}> = new EventEmitter(); constructor(private appConfig: AppConfigService) { } @@ -58,6 +58,15 @@ export class DownloadRendererComponent { const url = this.getHref(); const filename = url.substring(url.lastIndexOf('/') + 1); - this.startsDownloadingFile.emit({ url, filename }) + const n = Math.floor(Math.random() * 11); + const k = Math.floor(Math.random() * 1000000); + const m = String.fromCharCode(n) + k; + + this.downloadingFile.emit({ + url, + fileId: m, + datasetName: this.datasetName, + filename + }); } } diff --git a/client/src/app/instance/search/containers/result.component.html b/client/src/app/instance/search/containers/result.component.html index 7775c355..f0f8c4f9 100644 --- a/client/src/app/instance/search/containers/result.component.html +++ b/client/src/app/instance/search/containers/result.component.html @@ -38,7 +38,8 @@ [dataLength]="dataLength | async" [sampRegistered]="sampRegistered | async" (broadcast)="broadcastVotable($event)" - (startsDownloadingFile)="startsDownloadingFile($event)"> + (startTaskCreateResult)="startTaskCreateResult($event)" + (startTaskCreateArchive)="startTaskCreateArchive()"> </app-download> <app-reminder [datasetSelected]="datasetSelected | async" @@ -83,7 +84,7 @@ (addSelectedData)="addSearchData($event)" (deleteSelectedData)="deleteSearchData($event)" (broadcast)="broadcastVotable($event)" - (startsDownloadingFile)="startsDownloadingFile($event)"> + (downloadingFile)="downloadingFile($event)"> </app-datatable-tab> </ng-container> </div> diff --git a/client/src/app/instance/search/containers/result.component.ts b/client/src/app/instance/search/containers/result.component.ts index 90fc2319..dcc9146c 100644 --- a/client/src/app/instance/search/containers/result.component.ts +++ b/client/src/app/instance/search/containers/result.component.ts @@ -127,10 +127,27 @@ export class ResultComponent extends AbstractSearchComponent { /** * Dispatches action to starts downloading file. * - * @param {url: string, filename: string} download - Info about file to download + * @param {url: string, fileId: string, datasetName: string, filename: string} download - Info about file to download */ - startsDownloadingFile(download: {url: string, filename: string}): void { - this.store.dispatch(downloadFileActions.startsDownloadingFile(download)); + downloadingFile(download: {url: string, fileId: string, datasetName: string, filename: string}): void { + this.store.dispatch(downloadFileActions.downloadFile(download)); + } + + /** + * Dispatches action to starts task create result and download + * + * @param string format - Info about result format + */ + startTaskCreateResult(event: { format: string, selectedData: boolean }) { + this.store.dispatch(downloadFileActions.startTaskCreateResult(event)); + } + + /** + * Dispatches action to starts task create archive and download + * + */ + startTaskCreateArchive() { + this.store.dispatch(downloadFileActions.startTaskCreateArchive()); } /** diff --git a/client/src/app/instance/store/actions/download-file.actions.ts b/client/src/app/instance/store/actions/download-file.actions.ts index a6d140f2..e1c428e5 100644 --- a/client/src/app/instance/store/actions/download-file.actions.ts +++ b/client/src/app/instance/store/actions/download-file.actions.ts @@ -9,6 +9,20 @@ import { createAction, props } from '@ngrx/store'; -export const startsDownloadingFile = createAction('[File] Starts Downloading File', props<{ url: string, filename: string }>()); -export const updateDownloadProgress = createAction('[File] Update Download Progress', props<{ progress: number, filename: string }>()); -export const fileDownloaded = createAction('[File] File Downloaded', props<{ filename: string }>()); +export const startTaskCreateResult = createAction('[File] Start Task Create Result', props<{ format: string, selectedData: boolean }>()); +export const startTaskCreateResultSuccess = createAction('[File] Start Task Create Result Success', props<{ fileId: string, datasetName: string, filename: string }>()); +export const startTaskCreateResultFail = createAction('[File] Start Task Create Result Fail'); +export const isResultAvailable = createAction('[File] Is Result Available', props<{ fileId: string, datasetName: string, filename: string }>()); +export const isResultAvailableFail = createAction('[File] Is Result Available Fail'); + +export const startTaskCreateArchive = createAction('[File] Start Task Create Archive'); +export const startTaskCreateArchiveSuccess = createAction('[File] Start Task Create Archive Success', props<{ fileId: string, datasetName: string, filename: string }>()); +export const startTaskCreateArchiveFail = createAction('[File] Start Task Create Archive Fail'); +export const isArchiveAvailable = createAction('[File] Is Archive Available', props<{ fileId: string, datasetName: string, filename: string }>()); +export const isArchiveAvailableFail = createAction('[File] Is Archive Available Fail'); + +export const downloadFile = createAction('[File] Download File', props<{ url: string, fileId: string, datasetName: string, filename: string }>()); + +export const startsDownloadingFile = createAction('[File] Starts Downloading File', props<{ url: string, filename: string, fileId: string }>()); +export const updateDownloadProgress = createAction('[File] Update Download Progress', props<{ progress: number, fileId: string }>()); +export const fileDownloaded = createAction('[File] File Downloaded', props<{ fileId: string }>()); diff --git a/client/src/app/instance/store/effects/download-file.effects.ts b/client/src/app/instance/store/effects/download-file.effects.ts index 2a4754ba..6a375221 100644 --- a/client/src/app/instance/store/effects/download-file.effects.ts +++ b/client/src/app/instance/store/effects/download-file.effects.ts @@ -10,12 +10,18 @@ import { Injectable } from '@angular/core'; import { HttpEventType } from '@angular/common/http'; -import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { Actions, createEffect, ofType, concatLatestFrom } from '@ngrx/effects'; import { Store } from '@ngrx/store'; -import { map, mergeMap } from 'rxjs/operators'; +import { of, timer, mapTo, takeUntil, Subject } from 'rxjs'; +import { map, tap, mergeMap, switchMap, catchError } from 'rxjs/operators'; +import { ToastrService } from 'ngx-toastr'; +import { AppConfigService } from 'src/app/app-config.service'; import { DownloadFileService } from '../services/download-file.service'; import * as downloadFileActions from '../actions/download-file.actions'; +import * as searchSelector from '../selectors/search.selector'; +import * as attributeSelector from 'src/app/metamodel/selectors/attribute.selector'; +import * as coneSearchSelector from '../selectors/cone-search.selector'; /** * @class @@ -23,6 +29,192 @@ import * as downloadFileActions from '../actions/download-file.actions'; */ @Injectable() export class DownloadFileEffects { + startTaskCreateResult$ = createEffect(() => + this.actions$.pipe( + ofType(downloadFileActions.startTaskCreateResult), + concatLatestFrom(() => [ + this.store.select(searchSelector.selectCurrentDataset), + this.store.select(attributeSelector.selectAllAttributes), + this.store.select(searchSelector.selectCriteriaListByRoute), + this.store.select(coneSearchSelector.selectConeSearchByRoute), + this.store.select(searchSelector.selectOutputListByRoute), + this.store.select(searchSelector.selectSelectedData) + ]), + mergeMap(([action, currentDataset, attributeList, criteriaList, coneSearch, outputList, selectedData]) => { + const attributeId = attributeList.find(a => a.search_flag === 'ID'); + let query: string = `${currentDataset}?a=${outputList}`; + if (criteriaList) { + query += `&c=${criteriaList}`; + if (selectedData && action.selectedData) { + query += `;${attributeId.id}::in::${selectedData.join('|')}`; + } + } else if (selectedData && action.selectedData) { + query += `&c=${attributeId.id}::in::${selectedData.join('|')}`; + } + if (coneSearch) { + query += `&cs=${coneSearch}`; + } + query += `&f=${action.format}`; + + return this.downloadFileService.startTaskCreateResult(query) + .pipe( + map((response) => downloadFileActions.startTaskCreateResultSuccess({ + fileId: response.file_id, + filename: response.file_name, + datasetName: currentDataset + })), + catchError(() => of(downloadFileActions.startTaskCreateResultFail())) + ) + }) + ) + ); + + startTaskCreateResultSuccess$ = createEffect(() => + this.actions$.pipe( + ofType(downloadFileActions.startTaskCreateResultSuccess), + tap(action => this.kill$[action.fileId] = new Subject()), + switchMap(action => timer(0, 1000) + .pipe( + mapTo(downloadFileActions.isResultAvailable(action)), + takeUntil(this.kill$[action.fileId]) + ) + ) + ) + ); + + startTaskCreateResultFail$ = createEffect(() => + this.actions$.pipe( + ofType(downloadFileActions.startTaskCreateResultFail), + tap(() => this.toastr.error('The creation of the result file failed', 'Start async task failed')) + ), { dispatch: false} + ); + + isResultAvailable$ = createEffect(() => + this.actions$.pipe( + ofType(downloadFileActions.isResultAvailable), + switchMap(action => this.downloadFileService.isResultAvailable(action.fileId) + .pipe( + map(result => { + if (result.file_is_available) { + this.kill$[action.fileId].next({}); + this.kill$[action.fileId].unsubscribe(); + return downloadFileActions.startsDownloadingFile({ + fileId: action.fileId, + filename: action.filename, + url: `${this.config.apiUrl}/download-result/${action.datasetName}/${action.fileId}` + }); + } else { + return { type: '[No Action] Is Result Available' }; + } + }), + catchError(() => of(downloadFileActions.isResultAvailableFail())) + ) + ) + ) + ); + + isResultAvailableFail$ = createEffect(() => + this.actions$.pipe( + ofType(downloadFileActions.isResultAvailableFail), + tap(() => this.toastr.error('The creation of the file has encountered a problem', 'File result download failed')) + ), { dispatch: false} + ); + + + + startTaskCreateArchive$ = createEffect(() => + this.actions$.pipe( + ofType(downloadFileActions.startTaskCreateArchive), + concatLatestFrom(() => [ + this.store.select(searchSelector.selectCurrentDataset), + this.store.select(searchSelector.selectCriteriaListByRoute), + this.store.select(coneSearchSelector.selectConeSearchByRoute), + this.store.select(searchSelector.selectOutputListByRoute) + ]), + mergeMap(([action, currentDataset, criteriaList, coneSearch, outputList]) => { + let query: string = `${currentDataset}?a=${outputList}`; + if (criteriaList) { + query += `&c=${criteriaList}`; + } + if (coneSearch) { + query += `&cs=${coneSearch}`; + } + + return this.downloadFileService.startTaskCreateArchive(query) + .pipe( + map((response) => downloadFileActions.startTaskCreateArchiveSuccess({ + fileId: response.archive_id, + filename: response.archive_name, + datasetName: currentDataset + })), + catchError(() => of(downloadFileActions.startTaskCreateArchiveFail())) + ) + }) + ) + ); + + startTaskCreateArchiveSuccess$ = createEffect(() => + this.actions$.pipe( + ofType(downloadFileActions.startTaskCreateArchiveSuccess), + tap(action => this.kill$[action.fileId] = new Subject()), + switchMap(action => timer(0, 1000) + .pipe( + mapTo(downloadFileActions.isArchiveAvailable(action)), + takeUntil(this.kill$[action.fileId]) + ) + ) + ) + ); + + startTaskCreateArchiveFail$ = createEffect(() => + this.actions$.pipe( + ofType(downloadFileActions.startTaskCreateArchiveFail), + tap(() => this.toastr.error('The creation of the archive file failed', 'Start async task failed')) + ), { dispatch: false} + ); + + isArchiveAvailable$ = createEffect(() => + this.actions$.pipe( + ofType(downloadFileActions.isArchiveAvailable), + switchMap(action => this.downloadFileService.isArchiveAvailable(action.fileId) + .pipe( + map(result => { + if (result.archive_is_available) { + this.kill$[action.fileId].next({}); + this.kill$[action.fileId].unsubscribe(); + return downloadFileActions.startsDownloadingFile({ + fileId: action.fileId, + filename: action.filename, + url: `${this.config.apiUrl}/download-archive/${action.datasetName}/${action.fileId}` + }); + } else { + return { type: '[No Action] Is Archive Available' }; + } + }), + catchError(() => of(downloadFileActions.isArchiveAvailableFail())) + ) + ) + ) + ); + + isArchiveAvailableFail$ = createEffect(() => + this.actions$.pipe( + ofType(downloadFileActions.isArchiveAvailableFail), + tap(() => this.toastr.error('The creation of the archive has encountered a problem', 'Archive result download failed')) + ), { dispatch: false} + ); + + downloadFile$ = createEffect(() => + this.actions$.pipe( + ofType(downloadFileActions.downloadFile), + map(action => downloadFileActions.startsDownloadingFile({ + url: action.url, + fileId: action.fileId, + filename: action.filename + })) + ) + ); + /** * Calls actions to retrieve object. */ @@ -35,12 +227,12 @@ export class DownloadFileEffects { if (event.type === HttpEventType.DownloadProgress) { this.store.dispatch(downloadFileActions.updateDownloadProgress({ progress: Math.round((100 * event.loaded) / event.total), - filename: action.filename + fileId: action.fileId })); } if (event.type === HttpEventType.Response) { - this.store.dispatch(downloadFileActions.fileDownloaded({ filename: action.filename })); + this.store.dispatch(downloadFileActions.fileDownloaded({ fileId: action.fileId })); this.downloadFileService.saveDownloadedFile(event.body as Blob, action.filename); } }) @@ -49,9 +241,13 @@ export class DownloadFileEffects { ), { dispatch: false } ); + private kill$ = []; + constructor( private actions$: Actions, private downloadFileService: DownloadFileService, - private store: Store<{ }> + private store: Store<{ }>, + private toastr: ToastrService, + private config: AppConfigService ) {} } diff --git a/client/src/app/instance/store/models/download-file.model.ts b/client/src/app/instance/store/models/download-file.model.ts index d4d08f45..0d21ff57 100644 --- a/client/src/app/instance/store/models/download-file.model.ts +++ b/client/src/app/instance/store/models/download-file.model.ts @@ -8,7 +8,9 @@ */ export interface DownloadFile { - name: string, - state: 'PENDING' | 'IN_PROGRESS' | 'DONE' - progress: number + id: string; + datasetName: string; + fileName: string; + state: 'PENDING' | 'IN_PROGRESS' | 'DONE'; + progress: number; } diff --git a/client/src/app/instance/store/reducers/download-file.reducer.ts b/client/src/app/instance/store/reducers/download-file.reducer.ts index d30e4bf1..64daf37e 100644 --- a/client/src/app/instance/store/reducers/download-file.reducer.ts +++ b/client/src/app/instance/store/reducers/download-file.reducer.ts @@ -27,31 +27,52 @@ export const initialState: State = { export const fileReducer = createReducer( initialState, - on(downloadFileActions.startsDownloadingFile, (state, { filename }) => ({ + on(downloadFileActions.startTaskCreateResultSuccess, (state, {fileId, datasetName, filename }) => ({ ...state, downloadedFiles: [...state.downloadedFiles, { - name: filename, + id: fileId, + datasetName, + fileName: filename, state: 'PENDING', progress: 0 }] })), - on(downloadFileActions.updateDownloadProgress, (state, { progress, filename }) => ({ + on(downloadFileActions.startTaskCreateArchiveSuccess, (state, {fileId, datasetName, filename }) => ({ + ...state, + downloadedFiles: [...state.downloadedFiles, { + id: fileId, + datasetName, + fileName: filename, + state: 'PENDING', + progress: 0 + }] + })), + on(downloadFileActions.downloadFile, (state, {fileId, datasetName, filename }) => ({ + ...state, + downloadedFiles: [...state.downloadedFiles, { + id: fileId, + datasetName, + fileName: filename, + state: 'PENDING', + progress: 0 + }] + })), + on(downloadFileActions.updateDownloadProgress, (state, { progress, fileId }) => ({ ...state, downloadedFiles: [ - ...state.downloadedFiles.filter(f => f.name !== filename), + ...state.downloadedFiles.filter(f => f.id !== fileId), { - name: filename, - state: 'IN_PROGRESS', + ...state.downloadedFiles.find(f => f.id === fileId), progress: progress } ] })), - on(downloadFileActions.fileDownloaded, (state, { filename }) => ({ + on(downloadFileActions.fileDownloaded, (state, { fileId }) => ({ ...state, downloadedFiles: [ - ...state.downloadedFiles.filter(f => f.name !== filename), + ...state.downloadedFiles.filter(f => f.id !== fileId), { - name: filename, + ...state.downloadedFiles.find(f => f.id === fileId), state: 'DONE', progress: 100 } diff --git a/client/src/app/instance/store/services/download-file.service.ts b/client/src/app/instance/store/services/download-file.service.ts index 663e0521..48a51df1 100644 --- a/client/src/app/instance/store/services/download-file.service.ts +++ b/client/src/app/instance/store/services/download-file.service.ts @@ -10,9 +10,34 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpRequest } from '@angular/common/http'; +import { AppConfigService } from 'src/app/app-config.service'; + @Injectable({providedIn: 'root'}) export class DownloadFileService { - constructor(private http: HttpClient) { } + constructor(private http: HttpClient, private config: AppConfigService) { } + + /** + * Returns file name and file ID + * + * @param {string} query - The query. + * + * @return Observable<{"file_name": string, "file_id": string}> + */ + startTaskCreateResult(query: string) { + return this.http.get<{"file_name": string, "file_id": string}>(`${this.config.apiUrl}/start-task-create-result/${query}`); + } + + isResultAvailable(id: string) { + return this.http.get<{"file_is_available": boolean}>(`${this.config.apiUrl}/is-result-available/${id}`); + } + + startTaskCreateArchive(query: string) { + return this.http.get<{"archive_name": string, "archive_id": string}>(`${this.config.apiUrl}/start-task-create-archive/${query}`); + } + + isArchiveAvailable(id: string) { + return this.http.get<{"archive_is_available": boolean}>(`${this.config.apiUrl}/is-archive-available/${id}`); + } startsDownloadingFile(url: string) { const request = new HttpRequest( diff --git a/server/src/Action/StartTaskCreateResultAction.php b/server/src/Action/StartTaskCreateResultAction.php index 567ed6a1..bc9426ea 100644 --- a/server/src/Action/StartTaskCreateResultAction.php +++ b/server/src/Action/StartTaskCreateResultAction.php @@ -31,7 +31,7 @@ use PhpAmqpLib\Message\AMQPMessage; * @author François Agneray <francois.agneray@lam.fr> * @package App\Action */ -final class StartTaskCreateArchiveAction extends AbstractAction +final class StartTaskCreateResultAction extends AbstractAction { /** * Contains RabbitMQ connection socket @@ -111,18 +111,18 @@ final class StartTaskCreateArchiveAction extends AbstractAction } // Search extension - if ($queryParams === 'csv') { + if ($queryParams['f'] === 'csv') { $extension = '.csv'; - } else if ($queryParams === 'ascii') { + } else if ($queryParams['f'] === 'ascii') { $extension = '.txt'; - } else if ($queryParams === 'votable') { + } else if ($queryParams['f'] === 'votable') { $extension = '.xml'; } else { $extension = '.json'; } // Create the name of the future archive - $fileName = 'result_' . $dataset->getName() . '_' . (new \DateTime())->format('Y-m-d\TH:i:s') . '.' . $extension; + $fileName = 'result_' . $dataset->getName() . '_' . (new \DateTime())->format('Y-m-d\TH:i:s') . $extension; $fileId = uniqid(); // Publish message in the archive queue diff --git a/tasks/src/anis_tasks/result.py b/tasks/src/anis_tasks/result.py index 1278e00d..965b4339 100644 --- a/tasks/src/anis_tasks/result.py +++ b/tasks/src/anis_tasks/result.py @@ -1,6 +1,5 @@ # Standard library imports import logging, json, os -from zipfile import ZipFile # Local application imports from anis_tasks import utils @@ -20,7 +19,7 @@ def result_handler(ch, method, properties, body): file = open(file_path + ".tmp", "w") # Write data - file.write(data) + file.write(data.text) # close the File file.close() -- GitLab