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 e4b9620c5d80032da48676ddee61df2e5241e275..0feb30644af3bbca7cd805b7e905d71fdc39e5d3 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 4eee5ec45290fe4ca2bc8e3dfe433d55ad38c55b..af78907ccd84e1af2f6087595c27a392b51cd1f9 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 f4b0078f47282cd65be076a86f85df27351ce027..c1df3527a73a29b2a302829d7d66fdba6ded7a56 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 33191d25025d37bc79730b712355c1ae7fa59f5a..35be1904daad07e281252e7492d04ddaffdcf162 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 b47e6be34f1b9ca1b2044e225f820a7c89fb6cfa..49f60cee2bf34aa7a4d379d4b12829c8b6b6bdee 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 fc7758eae9a93233b2f07b64d89e196c5bd0f002..408855a5a241d81483c041168db44fcc0a497782 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 95bd94a066f4616691ba5036aeac21c9046ba3c9..d6c2323bdd6cab5df0bc31b11aaa9576cd0678ef 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 50c551818b7e85b28e6891ef8f60f66c5c1c2310..b7eb941fb56bc17ca645ec40db37bbcaf28aa846 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 7775c355e9de9b612db54af4ed69045e9e48bdfc..f0f8c4f9371770acc7bf60f0b9b291c62c5d064b 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 90fc23194beed185c86b27bbd7d51eb3515f4276..dcc9146cd4a73774250c4ed2bf5c62a45f0ec2b3 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 a6d140f26fb2e558b51a5e4877881857f1637d63..e1c428e5318a10ad8823fec87347629680263fe4 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 2a4754ba2138dfd034049955e7e76f971e3ce820..6a37522175e8d52ededa6233fe9a08121740ab0c 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 d4d08f454cd6832a508d932f152294a0cd050a6e..0d21ff574b88548eb8067f65b2b617377bba56f9 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 d30e4bf195d5615dc7c25e4960fafecd23fddc29..64daf37e3bad68eede73d702c032511b9b7a43ae 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 663e052129cd09d9cc46d0ae09ea4f19fdf2ae89..48a51df1121dc4f22f595bbd4d779dfabd5ec7b3 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 567ed6a1fde045e649c02e955e4c19401b72aac1..bc9426ea543f312fa878f1614a1a0a79ed625bdc 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 1278e00da40e8abe90c844122e34ea58fe41deda..965b4339a467b44dc900c25ed4fe4574c8b76f95 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()