huridocs/uwazi

View on GitHub
app/api/files/uploadMiddleware.ts

Summary

Maintainability
A
1 hr
Test Coverage
A
95%
import { generateFileName } from 'api/files';
import { tenants } from 'api/tenants';
import { NextFunction, Request, Response } from 'express';
import multer from 'multer';
import path from 'path';
import { FileType } from 'shared/types/fileType';
import { legacyLogger } from 'api/log';
// eslint-disable-next-line node/no-restricted-import
import { createReadStream } from 'fs';
import { storage } from './storage';

type multerCallback = (error: Error | null, destination: string) => void;

const defaultStorage = multer.diskStorage({
  filename(_req, file: Express.Multer.File, cb: multerCallback) {
    cb(null, generateFileName(file));
  },
});

const processOriginalFileName = (req: Request) => {
  if (req.body.originalname) {
    return req.body.originalname;
  }

  legacyLogger.debug(
    `[${
      tenants.current().name
      // eslint-disable-next-line max-len
    }] Deprecation warning: providing the filename in the multipart header is deprecated and will stop working in the future. Include an 'originalname' field in the body instead.`
  );

  return req.file?.originalname;
};

const singleUpload =
  (type?: FileType['type'], tmpStorage: multer.StorageEngine = defaultStorage) =>
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      await new Promise<void>((resolve, reject) => {
        multer({ storage: tmpStorage }).single('file')(req, res, err => {
          if (!err) resolve();
          reject(err);
        });
      });
      if (req.file) {
        req.file.originalname = processOriginalFileName(req);
      }
      if (type && req.file) {
        await storage.storeFile(
          req.file.filename,
          createReadStream(path.join(req.file.destination, req.file.filename)),
          type
        );
      }
      next();
    } catch (e) {
      next(e);
    }
  };

const getFieldAndIndex = (fieldname: string) => {
  const fieldAndIndexPattern = /([a-zA-Z0-9]+)\[([0-9]+)\]/g;
  const groups = fieldAndIndexPattern.exec(fieldname);
  return groups && { field: groups[1], index: parseInt(groups[2], 10) };
};

const applyFilesOriginalnames = (req: Request) => {
  if (req.files) {
    (req.files as Express.Multer.File[]).forEach(file => {
      const fileField = getFieldAndIndex(file.fieldname);
      if (fileField) {
        const originalnameInBody = req.body[`${fileField.field}_originalname`]?.[fileField.index];
        if (originalnameInBody) {
          // eslint-disable-next-line no-param-reassign
          file.originalname = originalnameInBody;
        } else {
          legacyLogger.error(
            `[${
              tenants.current().name
              // eslint-disable-next-line max-len
            }] Deprecation warning: relying on the filename in the multipart header is deprecated and will stop working in the future. Include an '${
              fileField.field
            }_originalname[${fileField.index}]' field in the body instead.`
          );
        }
      }
    });
  }
};

const multipleUpload = async (req: Request, res: Response, next: NextFunction) => {
  try {
    await new Promise<void>((resolve, reject) => {
      multer({ storage: defaultStorage }).any()(req, res, err => {
        if (err) reject(err);
        resolve();
      });
    });
    applyFilesOriginalnames(req);
    next();
  } catch (e) {
    next(e);
  }
};

/**
 * accepts a single file and stores it based on type
 * @param type is optional, when undefined the file will be stored on the os tmp default dir
 */
const uploadMiddleware = (type?: FileType['type']) => singleUpload(type, defaultStorage);

/**
 * accepts multiple files and places them in req.files array
 * files will not be stored on disk and will be on a buffer on each element of the array.
 */
uploadMiddleware.multiple = () => multipleUpload;

uploadMiddleware.customStorage = (tmpStorage: multer.StorageEngine, type?: FileType['type']) =>
  singleUpload(type, tmpStorage);

export { uploadMiddleware };