284 lines
11 KiB
TypeScript
284 lines
11 KiB
TypeScript
import { ApplicationRef, Injectable, OnDestroy } from '@angular/core';
|
|
import { FileUploadModel } from '../model/file-upload.model';
|
|
import { HttpErrorResponse, HttpEventType, HttpStatusCode } from '@angular/common/http';
|
|
import { interval, Subject, Subscription } from 'rxjs';
|
|
import { ConfigService } from '@services/config.service';
|
|
import { TranslateService } from '@ngx-translate/core';
|
|
import { IFileUploadResult, OverwriteFileOption, OverwriteFileOptions } from '@red/domain';
|
|
import { isAcceptedFileType, isCsv, isDocument, isZip } from '@utils/file-drop-utils';
|
|
import { ErrorMessageService, GenericService, Toaster } from '@iqser/common-ui';
|
|
import { FilesMapService } from '@services/files/files-map.service';
|
|
import { switchMap, tap, throttleTime } from 'rxjs/operators';
|
|
import { FilesService } from '@services/files/files.service';
|
|
import { UploadDownloadDialogService } from './upload-download-dialog.service';
|
|
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
|
import { HeadersConfiguration } from '@iqser/common-ui/lib/utils';
|
|
import { LicenseService } from '@services/license.service';
|
|
import { LicenseFeatures } from '../../admin/screens/license/utils/constants';
|
|
import { UserPreferenceService } from '@users/user-preference.service';
|
|
|
|
export interface ActiveUpload {
|
|
subscription: Subscription;
|
|
fileUploadModel: FileUploadModel;
|
|
}
|
|
|
|
@Injectable({ providedIn: 'root' })
|
|
export class FileUploadService extends GenericService<IFileUploadResult> implements OnDestroy {
|
|
static readonly MAX_PARALLEL_UPLOADS = 5;
|
|
#pendingUploads: FileUploadModel[] = [];
|
|
#activeUploads: ActiveUpload[] = [];
|
|
readonly #fetchFiles$ = new Subject<string>();
|
|
readonly #subscriptions = new Subscription();
|
|
protected readonly _defaultModelPath = 'upload';
|
|
files: FileUploadModel[] = [];
|
|
groupedFiles: { [key: string]: FileUploadModel[] } = {};
|
|
activeUploads = 0;
|
|
|
|
constructor(
|
|
private readonly _filesService: FilesService,
|
|
private readonly _filesMapService: FilesMapService,
|
|
private readonly _applicationRef: ApplicationRef,
|
|
private readonly _translateService: TranslateService,
|
|
private readonly _configService: ConfigService,
|
|
private readonly _dialogService: UploadDownloadDialogService,
|
|
private readonly _errorMessageService: ErrorMessageService,
|
|
private readonly _licenseService: LicenseService,
|
|
private readonly _toaster: Toaster,
|
|
private readonly _userPreferenceService: UserPreferenceService,
|
|
) {
|
|
super();
|
|
const fileFetch$ = this.#fetchFiles$.pipe(
|
|
throttleTime(250),
|
|
switchMap(dossierId => this._filesService.loadAll(dossierId)),
|
|
);
|
|
this.#subscriptions.add(fileFetch$.subscribe());
|
|
const interval$ = interval(2500).pipe(tap(() => this._handleUploads()));
|
|
this.#subscriptions.add(interval$.subscribe());
|
|
}
|
|
|
|
get activeDossierKeys() {
|
|
return Object.keys(this.groupedFiles).filter(dossierId => this.groupedFiles[dossierId].length > 0);
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this.#subscriptions.unsubscribe();
|
|
}
|
|
|
|
scheduleUpload(item: FileUploadModel) {
|
|
if (!item.sizeError && !item.typeError) {
|
|
item.progress = 0;
|
|
item.completed = false;
|
|
item.error = null;
|
|
this.#pendingUploads.push(item);
|
|
}
|
|
}
|
|
|
|
async uploadFiles(files: FileUploadModel[], dossierId: string): Promise<number> {
|
|
const maxSizeMB = this._configService.values.MAX_FILE_SIZE_MB;
|
|
const maxSizeBytes = maxSizeMB * 1024 * 1024;
|
|
const dossierFiles = this._filesMapService.get(dossierId);
|
|
const supportMsOfficeFormats = this._licenseService.getFeature(LicenseFeatures.SUPPORT_MS_OFFICE_FORMATS)?.value as boolean;
|
|
let option: OverwriteFileOption | 'undefined' = this._userPreferenceService.getOverwriteFileOption();
|
|
for (let idx = 0; idx < files.length; ++idx) {
|
|
const file = files[idx];
|
|
let currentOption = option;
|
|
|
|
if (isZip(file)) {
|
|
if (dossierFiles.length > 0) {
|
|
const res = await this._dialogService.openOverwriteFileDialog(null);
|
|
if (res.cancel) {
|
|
return;
|
|
}
|
|
if (res.option === OverwriteFileOptions.PARTIAL_OVERWRITE) {
|
|
file.keepManualRedactions = true;
|
|
}
|
|
if (res.option === OverwriteFileOptions.SKIP) {
|
|
files = [];
|
|
}
|
|
}
|
|
} else if (dossierFiles.find(pf => pf.filename === file.file.name)) {
|
|
if (option === 'undefined') {
|
|
const res = await this._dialogService.openOverwriteFileDialog(file.file.name);
|
|
if (res.cancel) {
|
|
return;
|
|
}
|
|
|
|
if (res.rememberChoice) {
|
|
await this._userPreferenceService.saveOverwriteFileOption(res.option);
|
|
}
|
|
|
|
currentOption = res.option;
|
|
option = res.applyToAllFiles ? currentOption : undefined;
|
|
}
|
|
|
|
if (currentOption === OverwriteFileOptions.PARTIAL_OVERWRITE) {
|
|
file.keepManualRedactions = true;
|
|
}
|
|
|
|
if (currentOption === OverwriteFileOptions.SKIP) {
|
|
files.splice(idx, 1);
|
|
--idx;
|
|
continue;
|
|
}
|
|
}
|
|
if (!isAcceptedFileType(file, supportMsOfficeFormats)) {
|
|
file.completed = true;
|
|
file.error = {
|
|
message: this._translateService.instant('upload-status.error.file-type'),
|
|
};
|
|
file.typeError = true;
|
|
} else if (file.size > maxSizeBytes) {
|
|
file.completed = true;
|
|
file.error = {
|
|
message: this._translateService.instant('upload-status.error.file-size', {
|
|
size: maxSizeMB,
|
|
}),
|
|
};
|
|
file.sizeError = true;
|
|
}
|
|
}
|
|
|
|
this.files.push(...files);
|
|
files.forEach(newFile => {
|
|
this._addFileToGroup(newFile);
|
|
this.scheduleUpload(newFile);
|
|
});
|
|
return files.length;
|
|
}
|
|
|
|
filterFiles() {
|
|
for (const file of this.files) {
|
|
if (file.completed && !file.error) {
|
|
this.removeFile(file);
|
|
}
|
|
}
|
|
}
|
|
|
|
stopAllUploads() {
|
|
this.files = [];
|
|
this.groupedFiles = {};
|
|
}
|
|
|
|
removeFile(item: FileUploadModel) {
|
|
this._removeUpload(item);
|
|
|
|
const index = this.files.indexOf(item);
|
|
if (index > -1) {
|
|
this._removeFileFromGroup(item);
|
|
this.#fetchFiles$.next(item.dossierId);
|
|
this.files.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
uploadFileForm(dossierId: string, keepManualRedactions = false, file?: Blob) {
|
|
const formParams = new FormData();
|
|
|
|
formParams.append('keepManualRedactions', keepManualRedactions.toString());
|
|
|
|
if (file !== undefined) {
|
|
formParams.append('file', file);
|
|
}
|
|
|
|
const headers = HeadersConfiguration.getHeaders({ contentType: false }).append('ngsw-bypass', 'true');
|
|
|
|
return this._http.post<IFileUploadResult>(`/${this._serviceName}/${this._defaultModelPath}/${dossierId}`, formParams, {
|
|
headers,
|
|
observe: 'events',
|
|
reportProgress: true,
|
|
});
|
|
}
|
|
|
|
private _addFileToGroup(file: FileUploadModel) {
|
|
if (!this.groupedFiles[file.dossierId]) {
|
|
this.groupedFiles[file.dossierId] = [];
|
|
}
|
|
this.groupedFiles[file.dossierId].push(file);
|
|
}
|
|
|
|
private _removeFileFromGroup(file: FileUploadModel) {
|
|
const index = this.groupedFiles[file.dossierId].indexOf(file);
|
|
this.groupedFiles[file.dossierId].splice(index, 1);
|
|
}
|
|
|
|
private _handleUploads() {
|
|
if (this.#activeUploads.length < FileUploadService.MAX_PARALLEL_UPLOADS && this.#pendingUploads.length > 0) {
|
|
let cnt = FileUploadService.MAX_PARALLEL_UPLOADS - this.#activeUploads.length;
|
|
while (cnt > 0 && this.#pendingUploads.length > 0) {
|
|
// Only schedule CSVs when no other file types in queue.
|
|
// CSVs are sorted at the end of `_pendingUploads`.
|
|
if (isCsv(this.#pendingUploads[0]) && this.#activeUploads.filter(upload => !isCsv(upload.fileUploadModel)).length > 0) {
|
|
return;
|
|
}
|
|
|
|
cnt--;
|
|
const poppedFileUploadModel = this.#pendingUploads.shift();
|
|
const subscription = this._createSubscription(poppedFileUploadModel);
|
|
this.#activeUploads.push({
|
|
subscription: subscription,
|
|
fileUploadModel: poppedFileUploadModel,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
private _createSubscription(uploadFile: FileUploadModel) {
|
|
this.activeUploads++;
|
|
const obs = this.uploadFileForm(uploadFile.dossierId, uploadFile.keepManualRedactions, uploadFile.file);
|
|
return obs.subscribe({
|
|
next: event => {
|
|
if (event.type === HttpEventType.UploadProgress) {
|
|
uploadFile.progress = Math.round((event.loaded / (event.total || event.loaded)) * 100);
|
|
this._applicationRef.tick();
|
|
}
|
|
if (event.type === HttpEventType.Response) {
|
|
if (event.status < 300) {
|
|
uploadFile.progress = 100;
|
|
uploadFile.completed = true;
|
|
if (isCsv(uploadFile)) {
|
|
this._toaster.success(_('file-upload.type.csv'));
|
|
}
|
|
} else {
|
|
uploadFile.completed = true;
|
|
uploadFile.error = {
|
|
message: this._translateService.instant('upload-status.error.generic'),
|
|
};
|
|
}
|
|
this._removeUpload(uploadFile);
|
|
}
|
|
},
|
|
error: (err: unknown) => {
|
|
uploadFile.completed = true;
|
|
uploadFile.error = {
|
|
// Extract error message
|
|
message: this._errorMessageService.getMessage(err as HttpErrorResponse, 'upload-status.error.generic'),
|
|
};
|
|
this._removeUpload(uploadFile);
|
|
if (
|
|
uploadFile.retryCount < 5 &&
|
|
![HttpStatusCode.BadRequest, HttpStatusCode.Conflict].includes((err as HttpErrorResponse).status)
|
|
) {
|
|
uploadFile.retryCount += 1;
|
|
this.scheduleUpload(uploadFile);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
private _removeUpload(fileUploadModel: FileUploadModel) {
|
|
const index = this.#activeUploads.findIndex(au => au.fileUploadModel === fileUploadModel);
|
|
if (index > -1) {
|
|
const subscription = this.#activeUploads[index].subscription;
|
|
if (subscription) {
|
|
try {
|
|
subscription.unsubscribe();
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
this.#activeUploads.splice(index, 1);
|
|
this.activeUploads--;
|
|
}
|
|
}
|
|
}
|