File

src/app/features/file/file.service.ts

Description

This service allow handles the logic for files/attachments. Files can be uploaded, shown and removed.

Index

Properties
Methods

Constructor

Protected constructor(entityMapper: EntityMapperService, entities: EntityRegistry, syncState: SyncStateSubject)
Parameters :
Name Type Optional
entityMapper EntityMapperService No
entities EntityRegistry No
syncState SyncStateSubject No

Properties

Protected dialog
Type : MatDialog
Default value : inject(MatDialog)
Protected httpClient
Type : HttpClient
Default value : inject(HttpClient, { optional: true, })
Protected snackbar
Type : MatSnackBar
Default value : inject(MatSnackBar)

Methods

Protected Abstract getShowFileUrl
getShowFileUrl(entity: Entity, property: string)

Template method to be request a file, so that it can be used in the default showFile implementation

Parameters :
Name Type Optional
entity Entity No
property string No
Returns : string
Abstract loadFile
loadFile(entity: Entity, property: string)

Loads the file and returns it as a blob.

Parameters :
Name Type Optional
entity Entity No
property string No
Returns : Observable<SafeUrl>
Abstract removeAllFiles
removeAllFiles(entity: Entity)

Removes all files linked with an entity

Parameters :
Name Type Optional
entity Entity No
Returns : Observable<any>
Abstract removeFile
removeFile(entity: Entity, property: string)

Removes the file

Parameters :
Name Type Optional Description
entity Entity No
property string No

of the entity which points to a file

Returns : Observable<any>
Protected reportProgress
reportProgress(message: string, obs: Observable)
Parameters :
Name Type Optional
message string No
obs Observable<HttpEvent | any> No
Returns : void
showFile
showFile(entity: Entity, property: string)

If a file is available, downloads this file and shows it in a new tab.

Parameters :
Name Type Optional Description
entity Entity No
property string No

where a file previously has been uploaded

Returns : void
Abstract uploadFile
uploadFile(file: File, entity: Entity, property: string)

Uploads the file

Parameters :
Name Type Optional Description
file File No

to be uploaded

entity Entity No
property string No

where the information about the file should be stored

Returns : Observable<any>
import { Entity, EntityConstructor } from "../../core/entity/model/entity";
import { Observable } from "rxjs";
import { EntityMapperService } from "../../core/entity/entity-mapper/entity-mapper.service";
import { EntityRegistry } from "../../core/entity/database-entity.decorator";
import { filter, map, shareReplay } from "rxjs/operators";
import { Logging } from "../../core/logging/logging.service";
import { SafeUrl } from "@angular/platform-browser";
import { FileDatatype } from "./file.datatype";
import { waitForChangeTo } from "../../core/session/session-states/session-utils";
import { SyncState } from "../../core/session/session-states/sync-state.enum";
import { SyncStateSubject } from "../../core/session/session-type";
import {
  HttpClient,
  HttpEvent,
  HttpEventType,
  HttpProgressEvent,
  HttpResponse,
} from "@angular/common/http";
import { ProgressComponent } from "./progress/progress.component";
import { MatSnackBar } from "@angular/material/snack-bar";
import { inject, Optional } from "@angular/core";
import { ShowFileComponent } from "./show-file/show-file.component";
import { MatDialog } from "@angular/material/dialog";

/**
 * This service allow handles the logic for files/attachments.
 * Files can be uploaded, shown and removed.
 */
export abstract class FileService {
  protected snackbar: MatSnackBar = inject(MatSnackBar);
  protected dialog: MatDialog = inject(MatDialog);
  protected httpClient: HttpClient = inject(HttpClient, {
    optional: true,
  });

  protected constructor(
    protected entityMapper: EntityMapperService,
    protected entities: EntityRegistry,
    protected syncState: SyncStateSubject,
  ) {
    // TODO maybe registration is too late (only when component is rendered)
    this.syncState
      // Only start listening to changes once the initial sync has been completed
      .pipe(waitForChangeTo(SyncState.COMPLETED))
      .subscribe(() => this.deleteFilesOfDeletedEntities());
  }

  private deleteFilesOfDeletedEntities() {
    const entitiesWithFiles = this.getEntitiesWithFileDataType();
    entitiesWithFiles.forEach((entity) => {
      this.entityMapper
        .receiveUpdates(entity)
        .pipe(filter(({ type }) => type === "remove"))
        .subscribe(({ entity, type }) => {
          this.removeAllFiles(entity).subscribe({
            next: () => Logging.debug(`deleted all files of ${entity}`),
            error: (err) =>
              Logging.debug(`no files found for ${entity}: ${err}`),
          });
        });
    });
  }

  private getEntitiesWithFileDataType() {
    const entitiesWithFiles: EntityConstructor[] = [];
    for (const entity of this.entities.values()) {
      if (
        this.entityHasFileProperty(entity) &&
        !entitiesWithFiles.includes(entity)
      ) {
        entitiesWithFiles.push(entity);
      }
    }
    return entitiesWithFiles;
  }

  private entityHasFileProperty(entity: EntityConstructor): boolean {
    for (const prop of entity.schema.values()) {
      if (prop.dataType === FileDatatype.dataType) {
        return true;
      }
    }
    return false;
  }

  /**
   * Removes the file
   * @param entity
   * @param property of the entity which points to a file
   */
  abstract removeFile(entity: Entity, property: string): Observable<any>;

  /**
   * Removes all files linked with an entity
   * @param entity
   */
  abstract removeAllFiles(entity: Entity): Observable<any>;

  /**
   * If a file is available, downloads this file and shows it in a new tab.
   * @param entity
   * @param property where a file previously has been uploaded
   */
  showFile(entity: Entity, property: string): void {
    const obs = this.httpClient
      .get(this.getShowFileUrl(entity, property), {
        responseType: "blob",
        reportProgress: true,
        observe: "events",
        headers: { "ngsw-bypass": "" },
      })
      .pipe(shareReplay());
    this.reportProgress($localize`Loading "${entity[property]}"`, obs);
    obs
      .pipe(filter((e) => e.type === HttpEventType.Response))
      .subscribe((e: HttpResponse<Blob>) => {
        const fileURL = URL.createObjectURL(e.body);
        const win = window.open(fileURL, "_blank");
        if (!win || win.closed || typeof win.closed == "undefined") {
          // When it takes more than a few (2-5) seconds to open the file, the browser might block the popup
          this.dialog.open(ShowFileComponent, { data: fileURL });
        }
      });
  }
  /**
   * Template method to be request a file, so that it can be used in the default showFile implementation
   */
  protected abstract getShowFileUrl(entity: Entity, property: string): string;

  /**
   * Loads the file and returns it as a blob.
   * @param entity
   * @param property
   */
  abstract loadFile(entity: Entity, property: string): Observable<SafeUrl>;

  /**
   * Uploads the file
   * @param file to be uploaded
   * @param entity
   * @param property where the information about the file should be stored
   */
  abstract uploadFile(
    file: File,
    entity: Entity,
    property: string,
  ): Observable<any>;

  protected reportProgress(
    message: string,
    obs: Observable<HttpEvent<any> | any>,
  ) {
    const progress = obs.pipe(
      filter(
        (e) =>
          e.type === HttpEventType.DownloadProgress ||
          e.type === HttpEventType.UploadProgress,
      ),
      map((e: HttpProgressEvent) => Math.round(100 * (e.loaded / e.total))),
    );
    const ref = this.snackbar.openFromComponent(ProgressComponent, {
      data: { message, progress },
    });
    progress.subscribe({
      complete: () => ref.dismiss(),
      error: () => ref.dismiss(),
    });
  }
}

results matching ""

    No results matching ""