huridocs/uwazi

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

Summary

Maintainability
A
0 mins
Test Coverage
A
92%
import path from 'path';
import { Readable } from 'stream';
import mimetypes from 'mime-types';

import ID from 'shared/uniqueID';
// eslint-disable-next-line node/no-restricted-import
import fs, { access } from 'fs/promises';
import { tenants } from 'api/tenants/tenantContext';
import { testingTenants } from 'api/utils/testingTenants';

import { FileType } from '../../shared/types/fileType';
// eslint-disable-next-line node/no-restricted-import
import { createWriteStream } from 'fs';

type FilePath = string;
type pathFunction = (fileName?: string) => FilePath;

const uploadsPath: pathFunction = (fileName = ''): FilePath =>
  path.join(tenants.current().uploadedDocuments, fileName);

const attachmentsPath: pathFunction = (fileName = ''): FilePath =>
  path.join(tenants.current().attachments, fileName);

const customUploadsPath: pathFunction = (fileName = ''): FilePath =>
  path.join(tenants.current().customUploads, fileName);

const temporalFilesPath: pathFunction = (fileName = ''): FilePath => path.join('/tmp', fileName);

const activityLogPath: pathFunction = (fileName = ''): FilePath =>
  path.join(tenants.current().activityLogs, fileName);

async function deleteFile(file: FilePath) {
  try {
    await fs.unlink(file);
  } catch (err) {
    if (err && err.code !== 'ENOENT') {
      throw err;
    }
  }
}

async function deleteFiles(files: FilePath[]) {
  return Promise.all(files.map(async file => deleteFile(file)));
}

const createDirIfNotExists = async (dirPath: string) => {
  try {
    await fs.mkdir(dirPath);
  } catch (e) {
    if (!e.message.match(/file already exists/)) {
      throw e;
    }
  }
};

const testingUploadPaths = async (subPath: string = '') => {
  if (subPath) {
    await createDirIfNotExists(`${__dirname}/specs/uploads/${subPath}`);
    await createDirIfNotExists(`${__dirname}/specs/customUploads/${subPath}`);
  }
  return {
    uploadedDocuments: `${__dirname}/specs/uploads/${subPath}`,
    attachments: `${__dirname}/specs/uploads/${subPath}`,
    customUploads: `${__dirname}/specs/customUploads/${subPath}`,
    activityLogs: `${__dirname}/specs/uploads/${subPath}`,
  };
};

const setupTestUploadedPaths = async (subFolder: string = '') => {
  testingTenants.changeCurrentTenant(await testingUploadPaths(subFolder));
};

const getExtension = (mimetype = '') => {
  const result = mimetypes.extension(mimetype);

  return result === 'jpeg' ? 'jpg' : result;
};

const generateFileName = ({ mimetype = '', originalname = '' }: FileType) => {
  const fileName = `${Date.now()}${ID()}`;

  const extensionFromOriginalName = getExtension(mimetypes.lookup(originalname) || '');
  if (extensionFromOriginalName) return `${fileName}.${extensionFromOriginalName}`;

  const extensionFromMime = getExtension(mimetype);
  if (extensionFromMime) return `${fileName}.${extensionFromMime}`;

  return fileName;
};

/**
 * Create a file from a read stream and save it to one of uwazi filesystem paths
 * @param destination by default this will be uploadsPaths,
 * if you want another one you can pass filesystem destinatations
 * e.g. attachmentsPath()
 *
 */
const fileFromReadStream = async (
  fileName: FilePath,
  readStream: Readable | NodeJS.ReadableStream,
  destination: string | undefined = undefined
): Promise<FilePath> =>
  new Promise((resolve, reject) => {
    const filePath = path.join(destination || uploadsPath(), fileName);
    const writeStream = createWriteStream(filePath);
    readStream
      .pipe(writeStream)
      .on('finish', () => resolve(filePath))
      .on('error', reject);
  });

const streamToString = async (stream: Readable): Promise<string> =>
  new Promise((resolve, reject) => {
    const chunks: any[] = [];
    stream.on('data', chunk => chunks.push(Buffer.from(chunk)));
    stream.on('error', reject);
    stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
  });

const fileExistsOnPath = async (filePath: string): Promise<boolean> => {
  try {
    await access(filePath);
  } catch (err) {
    if (err?.code === 'ENOENT') {
      return false;
    }
    if (err) {
      throw err;
    }
  }
  return true;
};

export {
  setupTestUploadedPaths,
  createDirIfNotExists,
  deleteFiles,
  deleteFile,
  generateFileName,
  fileFromReadStream,
  streamToString,
  customUploadsPath,
  uploadsPath,
  temporalFilesPath,
  attachmentsPath,
  activityLogPath,
  testingUploadPaths,
  fileExistsOnPath,
};

export type { FilePath, pathFunction };