All files / apps/file-download/src/app/downloads downloads.service.ts

100% Statements 73/73
62.5% Branches 5/8
100% Functions 2/2
100% Lines 73/73

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 741x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 3x 1x 1x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 9x 9x 9x 9x 9x 9x 9x 9x 9x 1x  
import { type Readable } from 'node:stream';
 
import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
 
import { DataExport, Download } from '@amalia/core/models';
import { assert } from '@amalia/ext/typescript';
import { DataExportStatus, DownloadType } from '@amalia/reporting/exports/shared';
import { CloudStorageDownloadService } from '@amalia/vendors/google-cloud/cloud-storage';
 
import { configuration } from '../../configuration';
 
const FIVE_MINUTES_IN_MS = 5 * 60 * 1000;
 
const DATA_EXPORT_FOLDER = 'data-exports';
@Injectable()
export class DownloadsService {
  public constructor(
    @InjectRepository(Download)
    private readonly downloadRepository: Repository<Download>,
    @InjectRepository(DataExport)
    private readonly dataExportRepository: Repository<DataExport>,
    private readonly cloudStorageDownloadService: CloudStorageDownloadService,
  ) {}
 
  public async streamFile(token: string): Promise<{
    stream: Readable;
    filenameWithoutExtension: string;
    extension: string;
    contentType: string;
  }> {
    const download = await this.downloadRepository.findOneBy({ token });
 
    assert(download, new NotFoundException('Token not found'));
 
    // Validity check
    const now = Date.now();
    const createdAt = download.createdAt.getTime();
    const isExpired = Math.abs(now - createdAt) > FIVE_MINUTES_IN_MS;
    assert(!isExpired, new BadRequestException('Token is expired. Please retry your download'));
 
    assert(
      (download.type as string) === DownloadType.DATA_EXPORT,
      new BadRequestException(`Unknown type ${download.type}`),
    );
 
    const dataExport = await this.dataExportRepository.findOne({
      where: { id: download.dataId },
      relations: ['company', 'creator'],
    });
 
    assert(dataExport, new NotFoundException('The data export was not found and could not be downloaded'));
    assert(dataExport.status === DataExportStatus.SUCCESS, new ConflictException('This export was not successful'));
 
    // Get the file from cloudStorage
    const stream = this.cloudStorageDownloadService.streamFile(
      dataExport.company,
      DATA_EXPORT_FOLDER,
      `${dataExport.id}.${dataExport.properties.extension}`,
      configuration.gcp.storage.api,
    );
 
    assert(stream?.pipe, new NotFoundException('File not retrieved'));
 
    return {
      stream,
      filenameWithoutExtension: dataExport.properties.filename ?? 'export',
      extension: dataExport.properties.extension,
      contentType: dataExport.properties.contentType,
    };
  }
}