src/api/v2/projects/projectfiles/projectfiles.controller.tsmodified
import crypto from "node:crypto";
import fs from "node:fs/promises";
import { Request, Response } from "express";
import { v4 } from "uuid";
import * as blobService from "../../../../services/azure.blob.service";
import * as service from "../../../../services/projectfiles.service";
import { LoggerFactory } from "../../../../util/logger";
import { createApiErrorResponse } from "../../../../util/errorFormatter";
import { getLoggedInUsername } from "../../../../util/username";
import {
  generateTokenisedBlobDownloadUrl,
  getFullyQualifiedBlobStoragePath,
} from "../../../../util/azure.util";

const logger = LoggerFactory("ProjectFilesController");

export const getFiles = async (req: Request, res: Response) => {
  const { projectId } = req.params;

  try {
    const rows = await service.getProjectFiles(projectId);
    return res.status(200).json(
      rows.map((row) => ({
        fileReferenceId: row.FileReferenceId,
        fileName: row.FileName,
        fileExtension: row.FileExtension,
        xyzDisplayName: row.XyzDisplayName,
        fileSizeBytes: Number(row.FileSizeBytes),
        fileHash: row.FileHash,
        createdBy: row.CreatedBy,
        insertedOn: row.InsertedOn,
        lastModifiedBy: row.LastModifiedBy,
        lastModifiedOn: row.LastModifiedOn,
      })),
    );
  } catch (err) {
    logger.error("Error fetching project files.", err);
    return createApiErrorResponse(err, res);
  }
};

export const getBiggestFile = async (req: Request, res: Response) => {
  const { projectId } = req.params;

  try {
    const row = await service.getBiggestProjectFile(projectId);
    if (!row) {
      return res.status(200).json(null);
    }
    return res.status(200).json({
      fileReferenceId: row.FileReferenceId,
      fileName: row.FileName,
      fileExtension: row.FileExtension,
      xyzDisplayName: row.XyzDisplayName,
      fileSizeBytes: Number(row.FileSizeBytes),
      fileHash: row.FileHash,
      createdBy: row.CreatedBy,
      insertedOn: row.InsertedOn,
      lastModifiedBy: row.LastModifiedBy,
      lastModifiedOn: row.LastModifiedOn,
    });
  } catch (err) {
    logger.error("Error fetching biggest project file.", err);
    return createApiErrorResponse(err, res);
  }
};

export const getFile = async (req: Request, res: Response) => {
  const { projectId, fileReferenceId } = req.params;

  try {
    const row = await service.getProjectFile(projectId, fileReferenceId);
    const downloadUrl = await generateTokenisedBlobDownloadUrl(
      row.CloudStoragePath as string,
    );
    return res.status(200).json({
      fileReferenceId: row.FileReferenceId,
      fileName: row.FileName,
      fileExtension: row.FileExtension,
      xyzDisplayName: row.XyzDisplayName,
      fileSizeBytes: Number(row.FileSizeBytes),
      fileHash: row.FileHash,
      createdBy: row.CreatedBy,
      insertedOn: row.InsertedOn,
      lastModifiedBy: row.LastModifiedBy,
      lastModifiedOn: row.LastModifiedOn,
      downloadUrl,
    });
  } catch (err) {
    logger.error("Error fetching project file.", err);
    return createApiErrorResponse(err, res);
  }
};

export const uploadFile = async (req: Request, res: Response) => {
  if (!req.file) {
    return res
      .status(400)
      .json({
        code: "InvalidRequest",
        message: "No file provided for upload.",
      });
  }

  const { projectId } = req.params;
  const chunkIndex = Number.parseInt(req.body.chunkIndex ?? "0");
  const totalChunks = Number.parseInt(req.body.totalChunks ?? "1");
  const fileReferenceId = chunkIndex === 0 ? v4() : req.body.fileReferenceId;

  const { fileName, xyzDisplayName } = req.body;
  const fileExtension = fileName.split(".").pop()?.toLowerCase() ?? "";
  const cloudStoragePath = `RawData/files/ProjectId=${projectId}/${fileReferenceId}`;

  logger.info(
    `Uploading project file chunk ${chunkIndex + 1} of ${totalChunks} for fileReferenceId: ${fileReferenceId}, projectId: ${projectId}`,
  );

  try {
    const chunkBuffer = await fs.readFile(req.file.path);
    const chunkHash = crypto.createHash("sha256").update(chunkBuffer).digest("hex");

    await blobService.uploadChunk(cloudStoragePath, req.file, chunkIndex, totalChunks);
    await service.accumulateFileHash(fileReferenceId, chunkHash, chunkIndex === 0);

    if (chunkIndex === 0) {
      await service.createProjectFile(
        projectId,
        fileReferenceId,
        fileName,
        fileExtension,
        getFullyQualifiedBlobStoragePath(cloudStoragePath),
        getLoggedInUsername(),
        xyzDisplayName
      );
    }

    if (chunkIndex + 1 === totalChunks) {
      const fileSizeBytes = chunkIndex === 0 ? req.file.size : 0;
      const fileHash = await service.popFileHash(fileReferenceId);
      await service.updateProjectFileSize(
        projectId,
        fileReferenceId,
        fileSizeBytes,
        fileHash,
      );
      return res.status(201).json({
        fileReferenceId,
        message: "Project file uploaded successfully",
      });
    }

    return res.status(200).json({
      fileReferenceId,
      message: "Project file chunk uploaded successfully",
    });
  } catch (err) {
    logger.error("Error uploading project file.", err);
    return createApiErrorResponse(err, res);
  }
};