You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			198 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			198 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			TypeScript
		
	
/* eslint-disable import/no-import-module-exports */
 | 
						|
/* eslint-disable no-async-promise-executor */
 | 
						|
/* eslint-disable @typescript-eslint/no-misused-promises */
 | 
						|
/* eslint-disable no-restricted-syntax */
 | 
						|
/**
 | 
						|
 * This file handles attachments for us.
 | 
						|
 * If the attachment filepath is an encrypted one. It will decrypt it, cache it, and return the blob url to it.
 | 
						|
 * An interval is run from time to time to cleanup old blobs loaded and not needed anymore (based on last access timestamp).
 | 
						|
 *
 | 
						|
 *
 | 
						|
 */
 | 
						|
import path from 'path';
 | 
						|
import { reject } from 'lodash';
 | 
						|
 | 
						|
import * as fse from 'fs-extra';
 | 
						|
 | 
						|
import { DURATION } from '../constants';
 | 
						|
import { makeObjectUrl, urlToBlob } from '../../types/attachments/VisualAttachment';
 | 
						|
import {
 | 
						|
  getAbsoluteAttachmentPath as msgGetAbsoluteAttachmentPath,
 | 
						|
  getAttachmentPath,
 | 
						|
} from '../../types/MessageAttachment';
 | 
						|
import { decryptAttachmentBufferRenderer } from '../../util/local_attachments_encrypter';
 | 
						|
 | 
						|
export const urlToDecryptedBlobMap = new Map<
 | 
						|
  string,
 | 
						|
  { decrypted: string; lastAccessTimestamp: number; forceRetain: boolean }
 | 
						|
>();
 | 
						|
export const urlToDecryptingPromise = new Map<string, Promise<string>>();
 | 
						|
 | 
						|
export const cleanUpOldDecryptedMedias = () => {
 | 
						|
  const currentTimestamp = Date.now();
 | 
						|
  let countCleaned = 0;
 | 
						|
  let countKept = 0;
 | 
						|
  let keptAsAvatars = 0;
 | 
						|
 | 
						|
  window?.log?.info('Starting cleaning of medias blobs...');
 | 
						|
  for (const iterator of urlToDecryptedBlobMap) {
 | 
						|
    if (
 | 
						|
      iterator[1].forceRetain &&
 | 
						|
      iterator[1].lastAccessTimestamp < currentTimestamp - DURATION.DAYS * 7
 | 
						|
    ) {
 | 
						|
      // keep forceRetained items for at most 7 days
 | 
						|
      keptAsAvatars++;
 | 
						|
    } else if (iterator[1].lastAccessTimestamp < currentTimestamp - DURATION.HOURS * 1) {
 | 
						|
      // if the last access is older than one hour, revoke the url and remove it.
 | 
						|
 | 
						|
      URL.revokeObjectURL(iterator[1].decrypted);
 | 
						|
      urlToDecryptedBlobMap.delete(iterator[0]);
 | 
						|
      countCleaned++;
 | 
						|
    } else {
 | 
						|
      countKept++;
 | 
						|
    }
 | 
						|
  }
 | 
						|
  window?.log?.info(
 | 
						|
    `Clean medias blobs: cleaned/kept/keptAsAvatars: ${countCleaned}:${countKept}:${keptAsAvatars}`
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
export const getLocalAttachmentPath = () => {
 | 
						|
  return getAttachmentPath();
 | 
						|
};
 | 
						|
 | 
						|
export const getAbsoluteAttachmentPath = (url: string) => {
 | 
						|
  return msgGetAbsoluteAttachmentPath(url);
 | 
						|
};
 | 
						|
 | 
						|
export const readFileContent = async (url: string) => {
 | 
						|
  return fse.readFile(url);
 | 
						|
};
 | 
						|
 | 
						|
export const getDecryptedMediaUrl = async (
 | 
						|
  url: string,
 | 
						|
  contentType: string,
 | 
						|
  isAvatar: boolean
 | 
						|
): Promise<string> => {
 | 
						|
  if (!url) {
 | 
						|
    return url;
 | 
						|
  }
 | 
						|
  if (url.startsWith('blob:')) {
 | 
						|
    return url;
 | 
						|
  }
 | 
						|
 | 
						|
  const isAbsolute = path.isAbsolute(url);
 | 
						|
 | 
						|
  if (
 | 
						|
    (isAbsolute &&
 | 
						|
      exports.getLocalAttachmentPath &&
 | 
						|
      url.startsWith(exports.getLocalAttachmentPath())) ||
 | 
						|
    fse.pathExistsSync(exports.getAbsoluteAttachmentPath(url))
 | 
						|
  ) {
 | 
						|
    // this is a file encoded by session on our current attachments path.
 | 
						|
    // we consider the file is encrypted.
 | 
						|
    // if it's not, the hook caller has to fallback to setting the img src as an url to the file instead and load it
 | 
						|
    if (urlToDecryptedBlobMap.has(url)) {
 | 
						|
      // refresh the last access timestamp so we keep the one being currently in use
 | 
						|
      const existing = urlToDecryptedBlobMap.get(url);
 | 
						|
      const existingObjUrl = existing?.decrypted as string;
 | 
						|
 | 
						|
      urlToDecryptedBlobMap.set(url, {
 | 
						|
        decrypted: existingObjUrl,
 | 
						|
        lastAccessTimestamp: Date.now(),
 | 
						|
        forceRetain: existing?.forceRetain || false,
 | 
						|
      });
 | 
						|
      // typescript does not realize that the has above makes sure the get is not undefined
 | 
						|
 | 
						|
      return existingObjUrl;
 | 
						|
    }
 | 
						|
 | 
						|
    if (urlToDecryptingPromise.has(url)) {
 | 
						|
      return urlToDecryptingPromise.get(url) as Promise<string>;
 | 
						|
    }
 | 
						|
 | 
						|
    urlToDecryptingPromise.set(
 | 
						|
      url,
 | 
						|
      new Promise(async resolve => {
 | 
						|
        // window.log.debug('about to read and decrypt file :', url, path.isAbsolute(url));
 | 
						|
        try {
 | 
						|
          const absUrl = path.isAbsolute(url) ? url : getAbsoluteAttachmentPath(url);
 | 
						|
          const encryptedFileContent = await readFileContent(absUrl);
 | 
						|
          const decryptedContent = await decryptAttachmentBufferRenderer(
 | 
						|
            encryptedFileContent.buffer
 | 
						|
          );
 | 
						|
          if (decryptedContent?.length) {
 | 
						|
            const arrayBuffer = decryptedContent.buffer;
 | 
						|
            const obj = makeObjectUrl(arrayBuffer, contentType);
 | 
						|
 | 
						|
            if (!urlToDecryptedBlobMap.has(url)) {
 | 
						|
              urlToDecryptedBlobMap.set(url, {
 | 
						|
                decrypted: obj,
 | 
						|
                lastAccessTimestamp: Date.now(),
 | 
						|
                forceRetain: isAvatar,
 | 
						|
              });
 | 
						|
            }
 | 
						|
            // window.log.debug(' file decrypted :', url, ' as ', obj);
 | 
						|
            urlToDecryptingPromise.delete(url);
 | 
						|
            resolve(obj);
 | 
						|
            return;
 | 
						|
          }
 | 
						|
          // failed to decrypt, fallback to url image loading
 | 
						|
          // it might be a media we received before the update encrypting attachments locally.
 | 
						|
          urlToDecryptingPromise.delete(url);
 | 
						|
          window.log.info('error decrypting file :', url);
 | 
						|
          resolve(url);
 | 
						|
        } catch (e) {
 | 
						|
          window.log.warn(e);
 | 
						|
          reject(e.message);
 | 
						|
        }
 | 
						|
      })
 | 
						|
    );
 | 
						|
 | 
						|
    return urlToDecryptingPromise.get(url) as Promise<string>;
 | 
						|
  }
 | 
						|
  // Not sure what we got here. Just return the file.
 | 
						|
 | 
						|
  return url;
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 *
 | 
						|
 * Returns the already decrypted URL or null
 | 
						|
 */
 | 
						|
export const getAlreadyDecryptedMediaUrl = (url: string): string | null => {
 | 
						|
  if (!url) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
  if (url.startsWith('blob:')) {
 | 
						|
    return url;
 | 
						|
  }
 | 
						|
  if (exports.getLocalAttachmentPath() && url.startsWith(exports.getLocalAttachmentPath())) {
 | 
						|
    if (urlToDecryptedBlobMap.has(url)) {
 | 
						|
      const existing = urlToDecryptedBlobMap.get(url);
 | 
						|
 | 
						|
      const existingObjUrl = existing?.decrypted as string;
 | 
						|
      urlToDecryptedBlobMap.set(url, {
 | 
						|
        decrypted: existingObjUrl,
 | 
						|
        lastAccessTimestamp: Date.now(),
 | 
						|
        forceRetain: existing?.forceRetain || false,
 | 
						|
      });
 | 
						|
      return existingObjUrl;
 | 
						|
    }
 | 
						|
  }
 | 
						|
  return null;
 | 
						|
};
 | 
						|
 | 
						|
export const getDecryptedBlob = async (url: string, contentType: string): Promise<Blob> => {
 | 
						|
  const decryptedUrl = await getDecryptedMediaUrl(url, contentType, false);
 | 
						|
  return urlToBlob(decryptedUrl);
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * This function should only be used for testing purpose
 | 
						|
 */
 | 
						|
export const resetDecryptedUrlForTesting = () => {
 | 
						|
  urlToDecryptedBlobMap.clear();
 | 
						|
  urlToDecryptingPromise.clear();
 | 
						|
};
 |