cleanup old blobs from time to time

pull/1554/head
Audric Ackermann 4 years ago
parent ed30be5334
commit 050739b0ab
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -46,7 +46,6 @@ exports.createReader = root => {
if (!isString(relativePath)) {
throw new TypeError("'relativePath' must be a string");
}
console.warn(`readFile: ${relativePath}`);
const absolutePath = path.join(root, relativePath);
const normalized = path.normalize(absolutePath);
if (!normalized.startsWith(root)) {

@ -30,7 +30,7 @@ exports.getImageDimensions = ({ objectUrl, logger }) =>
reject(error);
});
//FIXME image/jpeg is hard coded
DecryptedAttachmentsManager.getDecryptedAttachmentUrl(
DecryptedAttachmentsManager.getDecryptedMediaUrl(
objectUrl,
'image/jpg'
).then(decryptedUrl => {
@ -80,7 +80,7 @@ exports.makeImageThumbnail = ({
reject(error);
});
DecryptedAttachmentsManager.getDecryptedAttachmentUrl(
DecryptedAttachmentsManager.getDecryptedMediaUrl(
objectUrl,
contentType
).then(decryptedUrl => {
@ -117,8 +117,9 @@ exports.makeVideoScreenshot = ({
logger.error('makeVideoScreenshot error', toLogFormat(error));
reject(error);
});
//FIXME image/jpeg is hard coded
DecryptedAttachmentsManager.getDecryptedAttachmentUrl(
DecryptedAttachmentsManager.getDecryptedMediaUrl(
objectUrl,
'image/jpg'
).then(decryptedUrl => {

@ -30,7 +30,6 @@ describe('Attachments', () => {
tempRootDirectory,
'Attachments_createWriterForNew'
);
const outputPath = await Attachments.createWriterForNew(tempDirectory)(
input
);

@ -83,17 +83,17 @@ const AvatarImage = (props: {
export const Avatar = (props: Props) => {
const { avatarPath, size, memberAvatars, name } = props;
const [imageBroken, setImageBroken] = useState(false);
// contentType is not important
const { urlToLoad } = useEncryptedFileFetch(avatarPath || '', '');
const handleImageError = () => {
window.log.warn(
'Avatar: Image failed to load; failing over to placeholder'
'Avatar: Image failed to load; failing over to placeholder',
urlToLoad
);
setImageBroken(true);
};
// contentType is not important
const { urlToLoad } = useEncryptedFileFetch(avatarPath || '', '');
const isClosedGroupAvatar = memberAvatars && memberAvatars.length;
const hasImage = urlToLoad && !imageBroken && !isClosedGroupAvatar;

@ -252,7 +252,13 @@ export const LightboxObject = ({
});
if (isImageTypeSupported) {
return <img style={styles.object} alt={window.i18n('lightboxImageAlt')} src={urlToLoad} />;
return (
<img
style={styles.object}
alt={window.i18n('lightboxImageAlt')}
src={urlToLoad}
/>
);
}
const isVideoTypeSupported = GoogleChrome.isVideoTypeSupported(contentType);

@ -1,14 +1,12 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
import { Avatar, AvatarSize } from '../Avatar';
import { darkTheme, lightTheme } from '../../state/ducks/SessionTheme';
import { SessionToastContainer } from './SessionToastContainer';
import { ConversationType } from '../../state/ducks/conversations';
import { DefaultTheme } from 'styled-components';
import { ConversationController } from '../../session/conversations';
import { UserUtils } from '../../session/utils';
import { syncConfigurationIfNeeded } from '../../session/utils/syncUtils';
import { DAYS } from '../../session/utils/Number';
import { DAYS, MINUTES, SECONDS } from '../../session/utils/Number';
import {
getItemById,
hasSyncedInitialConfigurationItem,
@ -29,6 +27,8 @@ import { getFocusedSection } from '../../state/selectors/section';
import { useInterval } from '../../hooks/useInterval';
import { clearSearch } from '../../state/ducks/search';
import { showLeftPaneSection } from '../../state/ducks/section';
import { cleanUpOldDecryptedMedias } from '../../session/crypto/DecryptedAttachmentsManager';
import { useTimeoutFn } from 'react-use';
// tslint:disable-next-line: no-import-side-effect no-submodule-imports
export enum SectionType {
@ -126,16 +126,20 @@ const showResetSessionIDDialogIfNeeded = async () => {
window.showResetSessionIdDialog();
};
const cleanUpMediasInterval = MINUTES * 30;
/**
* ActionsPanel is the far left banner (not the left pane).
* The panel with buttons to switch between the message/contact/settings/theme views
*/
export const ActionsPanel = () => {
const dispatch = useDispatch();
const [startCleanUpMedia, setStartCleanUpMedia] = useState(false);
const ourPrimaryConversation = useSelector(getOurPrimaryConversation);
// this maxi useEffect is called only once: when the component is mounted.
// For the action panel, it means this is called only one per app start/with a user loggedin
useEffect(() => {
void window.setClockParams();
if (
@ -174,10 +178,27 @@ export const ActionsPanel = () => {
};
// trigger a sync message if needed for our other devices
void syncConfiguration();
}, []);
// wait for cleanUpMediasInterval and then start cleaning up medias
// this would be way easier to just be able to not trigger a call with the setInterval
useEffect(() => {
const timeout = global.setTimeout(
() => setStartCleanUpMedia(true),
cleanUpMediasInterval
);
return () => global.clearTimeout(timeout);
}, []);
useInterval(
() => {
cleanUpOldDecryptedMedias();
},
startCleanUpMedia ? cleanUpMediasInterval : null
);
if (!ourPrimaryConversation) {
window.log.warn('ActionsPanel: ourPrimaryConversation is not set');
return <></>;

@ -34,7 +34,7 @@ import {
getPubkeysInPublicConversation,
} from '../../../data/data';
import autoBind from 'auto-bind';
import { getDecryptedAttachmentUrl } from '../../../session/crypto/DecryptedAttachmentsManager';
import { getDecryptedMediaUrl } from '../../../session/crypto/DecryptedAttachmentsManager';
interface State {
// Message sending progress
@ -940,7 +940,7 @@ export class SessionConversation extends React.Component<Props, State> {
index?: number;
}) {
const { getAbsoluteAttachmentPath } = window.Signal.Migrations;
attachment.url = await getDecryptedAttachmentUrl(
attachment.url = await getDecryptedMediaUrl(
attachment.url,
attachment.contentType
);

@ -21,7 +21,7 @@ import {
getMessagesWithFileAttachments,
getMessagesWithVisualMediaAttachments,
} from '../../../data/data';
import { getDecryptedAttachmentUrl } from '../../../session/crypto/DecryptedAttachmentsManager';
import { getDecryptedMediaUrl } from '../../../session/crypto/DecryptedAttachmentsManager';
interface Props {
id: string;
@ -196,7 +196,7 @@ class SessionRightPanel extends React.Component<Props, State> {
const saveAttachment = async ({ attachment, message }: any = {}) => {
const timestamp = message.received_at;
attachment.url = await getDecryptedAttachmentUrl(
attachment.url = await getDecryptedMediaUrl(
attachment.url,
attachment.contentType
);

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { getDecryptedAttachmentUrl } from '../session/crypto/DecryptedAttachmentsManager';
import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager';
export const useEncryptedFileFetch = (url: string, contentType: string) => {
// tslint:disable-next-line: no-bitwise
@ -8,7 +8,7 @@ export const useEncryptedFileFetch = (url: string, contentType: string) => {
const [loading, setLoading] = useState(true);
async function fetchUrl() {
const decryptedUrl = await getDecryptedAttachmentUrl(url, contentType);
const decryptedUrl = await getDecryptedMediaUrl(url, contentType);
setUrlToLoad(decryptedUrl);
setLoading(false);

@ -1,18 +1,47 @@
/**
* 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 toArrayBuffer from 'to-arraybuffer';
import * as fse from 'fs-extra';
import { decryptAttachmentBuffer } from '../../types/Attachment';
import { HOURS, MINUTES, SECONDS } from '../utils/Number';
// FIXME.
// add a way to clean those from time to time (like every hours?)
// add a way to remove the blob when the attachment file path is removed (message removed?)
const urlToDecryptedBlobMap = new Map<string, string>();
// do not hardcode the password
// a few FIXME image/jpeg is hard coded
const urlToDecryptedBlobMap = new Map<
string,
{ decrypted: string; lastAccessTimestamp: number }
>();
export const getDecryptedAttachmentUrl = async (
export const cleanUpOldDecryptedMedias = () => {
const currentTimestamp = Date.now();
let countCleaned = 0;
let countKept = 0;
window.log.info('Starting cleaning of medias blobs...');
for (const iterator of urlToDecryptedBlobMap) {
// if the last access is older than one hour, revoke the url and remove it.
if (iterator[1].lastAccessTimestamp < currentTimestamp - HOURS * 1) {
URL.revokeObjectURL(iterator[1].decrypted);
urlToDecryptedBlobMap.delete(iterator[0]);
countCleaned++;
} else {
countKept++;
}
}
window.log.info(
`Clean medias blobs: cleaned/kept: ${countCleaned}:${countKept}`
);
};
export const getDecryptedMediaUrl = async (
url: string,
contentType: string
): Promise<string> => {
@ -28,10 +57,18 @@ export const getDecryptedAttachmentUrl = async (
// 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
console.warn('url:', url, ' has:', urlToDecryptedBlobMap.has(url));
if (urlToDecryptedBlobMap.has(url)) {
// refresh the last access timestamp so we keep the one being currently in use
const existingObjUrl = urlToDecryptedBlobMap.get(url)
?.decrypted as string;
urlToDecryptedBlobMap.set(url, {
decrypted: existingObjUrl,
lastAccessTimestamp: Date.now(),
});
// typescript does not realize that the has above makes sure the get is not undefined
return urlToDecryptedBlobMap.get(url) as string;
return existingObjUrl;
} else {
const encryptedFileContent = await fse.readFile(url);
const decryptedContent = await decryptAttachmentBuffer(
@ -41,19 +78,23 @@ export const getDecryptedAttachmentUrl = async (
const arrayBuffer = decryptedContent.buffer;
const { makeObjectUrl } = window.Signal.Types.VisualAttachment;
const obj = makeObjectUrl(arrayBuffer, contentType);
console.warn('makeObjectUrl: ', obj, contentType);
if (!urlToDecryptedBlobMap.has(url)) {
urlToDecryptedBlobMap.set(url, obj);
urlToDecryptedBlobMap.set(url, {
decrypted: obj,
lastAccessTimestamp: Date.now(),
});
}
return obj;
} else {
// failed to decrypt, fallback to url image loading
// it might be a media we received before the update encrypting attachments locally.
return url;
}
}
} else {
// Not sure what we got here. Just return the file.
return url;
}
};

Loading…
Cancel
Save