File

lib/common/file/file.service.ts

Description

Service for managing files, including uploading, finding, and removing files.

Extends

FileManager

Index

Methods

Constructor

constructor(logger: Logger, fileRep: Repository<FileEntity>, cacheService: CacheService, metadataService: FileMd)
Parameters :
Name Type Optional
logger Logger No
fileRep Repository<FileEntity> No
cacheService CacheService No
metadataService FileMd No

Methods

Private createBasicFindQb
createBasicFindQb()

Private helper method to create a query builder for finding files with their related entities.

Returns : any

A query builder instance for finding files.

Private Async createFileDirectory
createFileDirectory(isPublic: boolean, entityId: string)

Private helper method to create a file directory for the specified public/private status and entity ID.

Parameters :
Name Type Optional Description
isPublic boolean No
  • A boolean flag indicating if the directory is for public files (true) or private files (false).
entityId string No
  • The entity ID to use as the directory name.
Returns : Promise<string>

A promise that resolves to the created directory path as a string.

Private Async createFileEntity
createFileEntity(isPublic: boolean)

Private helper method to create a new FileEntity with the specified public/private status.

Parameters :
Name Type Optional Description
isPublic boolean No
  • A boolean flag indicating if the FileEntity is for a public file (true) or private file (false).
Returns : unknown

A promise that resolves to the created FileEntity.

Async createOrUpdateFile
createOrUpdateFile(file: Buffer, extension: string, isPublic, code?: string, existedEntityId?: number, name?: string)
Inherited from FileManager
Defined in FileManager:66

Uploads a file and saves it to the specified directory (public or private). Also creates a FileEntity and saves the file's metadata in the database.

Parameters :
Name Type Optional Default value Description
file Buffer No
  • Buffer of file.
extension string No ""
  • file extension (based of file name).
isPublic No true
  • A boolean flag indicating if the file should be saved to the public directory (true) or private directory (false).
code string Yes
  • Specific identification code for file entity.
existedEntityId number Yes
  • ID of file entity for patch.
name string Yes
  • name for file entity.

A promise that resolves to the created FileEntity.

Async findByCode
findByCode(code: string)
Inherited from FileManager
Defined in FileManager:148

Finds a file entity by code.

Parameters :
Name Type Optional Description
code string No
  • The code of the file entity.

The found file entity.

Async findFileById
findFileById(id: number, isPublic: boolean)
Inherited from FileManager
Defined in FileManager:131

Private helper method to find a file by its ID and public/private status.

Parameters :
Name Type Optional Default value Description
id number No
  • The ID of the file to find.
isPublic boolean No undefined
  • A boolean flag indicating if the file is public (true) or private (false).
Returns : unknown

A promise that resolves to the found FileEntity.

Async findPrivateById
findPrivateById(id: number)
Inherited from FileManager
Defined in FileManager:168

Finds a private file by its ID.

Parameters :
Name Type Optional Description
id number No
  • The ID of the file to find.

A promise that resolves to the found FileEntity.

Async findPublicById
findPublicById(id: number)
Inherited from FileManager
Defined in FileManager:159

Finds a public file by its ID.

Parameters :
Name Type Optional Description
id number No
  • The ID of the file to find.

A promise that resolves to the found FileEntity.

Async getFilePath
getFilePath(file: File)
Inherited from FileManager
Defined in FileManager:177

Constructs the full file path for the given File object.

Parameters :
Name Type Optional Description
file File No
  • A File object containing the file's metadata.
Returns : Promise<string>

The full file path as a string.

Private Async getPrivateDir
getPrivateDir()

Retrieves the private directory path stored in configuration.

Returns : unknown

A promise that resolves to the normalized private directory path.

Private Async getPublicDir
getPublicDir()

Retrieves the public directory path stored in configuration.

Returns : unknown

A promise that resolves to the normalized public directory path.

Async remove
remove(id: number)
Inherited from FileManager
Defined in FileManager:187

Removes a file by its ID and deletes its corresponding directory.

Parameters :
Name Type Optional Description
id number No
  • The ID of the file to remove.

A promise that resolves to the removed FileEntity.

import {
  BadRequestException,
  Inject,
  Injectable,
  InternalServerErrorException,
  Logger,
  NotFoundException,
} from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { FileEntity } from "./entity/file.entity";
import * as path from "path";
import * as fs from "fs";
import { File } from "./file.types";
import { FilesUtils } from "../../shared/utils/files.utils";
import { FileConfig } from "../../../gen-src/file.config";
import { FileManager, FileMd } from "./file.constants";
import { LOGGER } from "../../shared/modules/log/log.constants";
import { CacheService } from "../../shared/modules/cache/cache.types";
import PRIVATE_DIR = FileConfig.PRIVATE_DIR;
import PUBLIC_DIR = FileConfig.PUBLIC_DIR;
import createDirectoriesIfNotExist = FilesUtils.createDirectoriesIfNotExist;

/**
 * Service for managing files, including uploading, finding, and removing files.
 */
@Injectable()
export class FileService extends FileManager {
  constructor(
    @Inject(LOGGER) protected readonly logger: Logger,
    @InjectRepository(FileEntity)
    private readonly fileRep: Repository<FileEntity>,
    private readonly cacheService: CacheService,
    private readonly metadataService: FileMd,
  ) {
    super();
  }

  /**
   * Uploads a file and saves it to the specified directory (public or private).
   * Also creates a FileEntity and saves the file's metadata in the database.
   * @param file - Buffer of file.
   * @param extension - file extension (based of file name).
   * @param isPublic - A boolean flag indicating if the file should be saved to the public directory (true) or private directory (false).
   * @param code - Specific identification code for file entity.
   * @param existedEntityId - ID of file entity for patch.
   * @param name - name for file entity.
   * @returns A promise that resolves to the created FileEntity.
   */
  async createOrUpdateFile(
    file: Buffer,
    extension = "",
    isPublic = true,
    code?: string,
    existedEntityId?: number,
    name?: string,
  ): Promise<FileEntity> {
    let entity: FileEntity = undefined;
    await this.fileRep.manager.transaction(async (transactionManager) => {
      if (existedEntityId) {
        entity = await this.findFileById(existedEntityId, isPublic);
        if (!entity) {
          throw new BadRequestException(
            `Cannot patch file with ID ${existedEntityId}, because than not exists`,
          );
        }
        const dir = path.join(
          !entity.public
            ? await this.getPrivateDir()
            : await this.getPublicDir(),
          entity.id.toString(),
        );
        await fs.promises
          .rm(dir, { recursive: true, force: true })
          .catch((err) => {
            throw new InternalServerErrorException(
              `Failed to delete directory: ${dir}`,
              err,
            );
          });
      } else {
        entity = await this.createFileEntity(isPublic);
      }
      const outputPath = await this.createFileDirectory(
        entity.public,
        entity.id.toString(),
      );
      const fileName =
        entity.id.toString() + (extension ? `.${extension}` : extension);
      entity.size = file.length;
      entity.path = fileName;
      entity.name = name;
      entity.code = code;
      await fs.promises.writeFile(`${outputPath}/${fileName}`, file);
      if (!existedEntityId) {
        entity.metadata = await this.metadataService.createFileMetadataEntity(
          file,
          `${outputPath}/${fileName}`,
        );
      }
      await transactionManager.save(entity);
    });
    this.logger.log(
      `${!existedEntityId ? `Created` : `Updated`} file with ID ${entity.id}`,
    );
    return entity;
  }

  /**
   * Private helper method to find a file by its ID and public/private status.
   * @param id - The ID of the file to find.
   * @param isPublic - A boolean flag indicating if the file is public (true) or private (false).
   * @returns A promise that resolves to the found FileEntity.
   */
  async findFileById(id: number, isPublic: boolean = undefined) {
    const qb = this.createBasicFindQb().where("file.id = :id", { id });
    if (isPublic !== undefined) {
      qb.andWhere(`file.public = :isPublic`, { isPublic });
    }
    const entity = await qb.getOne();
    if (!entity) {
      throw new NotFoundException(`File with ID ${id} not found`);
    }
    return entity;
  }

  /**
   * Finds a file entity by code.
   * @param code - The code of the file entity.
   * @returns The found file entity.
   */
  async findByCode(code: string): Promise<FileEntity> {
    return await this.createBasicFindQb()
      .where("file.code = :code", { code })
      .getOne();
  }

  /**
   * Finds a public file by its ID.
   * @param id - The ID of the file to find.
   * @returns A promise that resolves to the found FileEntity.
   */
  async findPublicById(id: number): Promise<FileEntity> {
    return this.findFileById(id, true);
  }

  /**
   * Finds a private file by its ID.
   * @param id - The ID of the file to find.
   * @returns A promise that resolves to the found FileEntity.
   */
  async findPrivateById(id: number): Promise<FileEntity> {
    return this.findFileById(id);
  }

  /**
   * Constructs the full file path for the given File object.
   * @param file - A File object containing the file's metadata.
   * @returns The full file path as a string.
   */
  async getFilePath(file: File): Promise<string> {
    const filePath = `${!file.public ? await this.getPrivateDir() : await this.getPublicDir()}/${file.id}/`;
    return filePath + file.path;
  }

  /**
   * Removes a file by its ID and deletes its corresponding directory.
   * @param id - The ID of the file to remove.
   * @returns A promise that resolves to the removed FileEntity.
   */
  async remove(id: number): Promise<FileEntity> {
    const file = await this.findFileById(id);
    const dir = path.join(
      !file.public ? await this.getPrivateDir() : await this.getPublicDir(),
      file.id.toString(),
    );
    await this.fileRep.manager.transaction(async (transactionManager) => {
      if (file.metadata) {
        if (file.metadata.image) {
          await transactionManager.remove(file.metadata.image);
        }
        if (file.metadata.gps) {
          await transactionManager.remove(file.metadata.gps);
        }
        if (file.metadata.audio) {
          await transactionManager.remove(file.metadata.audio);
        }
        if (file.metadata.video) {
          await transactionManager.remove(file.metadata.video);
        }
      }
      await transactionManager.remove(file);
      await fs.promises
        .rm(dir, { recursive: true, force: true })
        .catch((err) => {
          throw new InternalServerErrorException(
            `Failed to delete directory: ${dir}`,
            err,
          );
        });
    });
    this.logger.log(`File with ID ${id} removed`);
    return file;
  }

  /**
   * Private helper method to create a query builder for finding files with their related entities.
   * @returns A query builder instance for finding files.
   */
  private createBasicFindQb() {
    return this.fileRep
      .createQueryBuilder("file")
      .leftJoinAndSelect("file.icon", "icon")
      .leftJoinAndSelect("file.metadata", "metadata")
      .leftJoinAndSelect("metadata.gps", "metaGps")
      .leftJoinAndSelect("metadata.image", "metaImage")
      .leftJoinAndSelect("metadata.audio", "metaAudio")
      .leftJoinAndSelect("metadata.video", "metaVideo")
      .leftJoinAndSelect("icon.name", "iconName")
      .leftJoinAndSelect("iconName.lang", "iconLang")
      .leftJoinAndSelect("icon.files", "iconFiles")
      .leftJoinAndSelect("iconFiles.format", "iconFilesFormat")
      .leftJoinAndSelect("icon.type", "iconType")
      .leftJoinAndSelect("iconType.ext", "iconTypeExt")
      .leftJoinAndSelect("file.preview", "preview")
      .leftJoinAndSelect("preview.name", "previewName")
      .leftJoinAndSelect("previewName.lang", "previewLang")
      .leftJoinAndSelect("preview.files", "previewFiles")
      .leftJoinAndSelect("previewFiles.format", "previewFilesFormat")
      .leftJoinAndSelect("preview.type", "previewType")
      .leftJoinAndSelect("previewType.ext", "previewTypeExt");
  }

  /**
   * Private helper method to create a file directory for the specified public/private status and entity ID.
   * @param isPublic - A boolean flag indicating if the directory is for public files (true) or private files (false).
   * @param entityId - The entity ID to use as the directory name.
   * @returns A promise that resolves to the created directory path as a string.
   */
  private async createFileDirectory(
    isPublic: boolean,
    entityId: string,
  ): Promise<string> {
    const dir = path.join(
      !isPublic ? await this.getPrivateDir() : await this.getPublicDir(),
      entityId,
    );
    await createDirectoriesIfNotExist(dir);
    return dir;
  }

  /**
   * Private helper method to create a new FileEntity with the specified public/private status.
   * @param isPublic - A boolean flag indicating if the FileEntity is for a public file (true) or private file (false).
   * @returns A promise that resolves to the created FileEntity.
   */
  private async createFileEntity(isPublic: boolean) {
    const entity = new FileEntity();
    entity.public = isPublic;
    return this.fileRep.save(entity);
  }

  /**
   * Retrieves the public directory path stored in configuration.
   * @returns A promise that resolves to the normalized public directory path.
   */
  private async getPublicDir() {
    const dir = process.cwd() + (await this.cacheService.get(PUBLIC_DIR));
    return path.normalize(dir);
  }

  /**
   * Retrieves the private directory path stored in configuration.
   * @returns A promise that resolves to the normalized private directory path.
   */
  private async getPrivateDir() {
    const dir = process.cwd() + (await this.cacheService.get(PRIVATE_DIR));
    return path.normalize(dir);
  }
}

results matching ""

    No results matching ""