2020-10-30 20:34:04 +00:00
|
|
|
// Copyright 2018-2020 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2021-06-18 17:04:27 +00:00
|
|
|
import { ipcMain } from 'electron';
|
|
|
|
import * as rimraf from 'rimraf';
|
|
|
|
import {
|
2022-11-17 00:29:15 +00:00
|
|
|
getAllAttachments,
|
2021-06-18 17:04:27 +00:00
|
|
|
getPath,
|
|
|
|
getStickersPath,
|
|
|
|
getTempPath,
|
|
|
|
getDraftPath,
|
2022-11-17 00:29:15 +00:00
|
|
|
deleteAll as deleteAllAttachments,
|
|
|
|
deleteAllBadges,
|
|
|
|
getAllStickers,
|
|
|
|
deleteAllStickers,
|
|
|
|
getAllDraftAttachments,
|
|
|
|
deleteAllDraftAttachments,
|
2022-10-24 20:46:36 +00:00
|
|
|
} from './attachments';
|
2022-11-17 00:29:15 +00:00
|
|
|
import type { MainSQL } from '../ts/sql/main';
|
|
|
|
import type { MessageAttachmentsCursorType } from '../ts/sql/Interface';
|
|
|
|
import * as Errors from '../ts/types/errors';
|
|
|
|
import { sleep } from '../ts/util/sleep';
|
2018-07-27 01:13:56 +00:00
|
|
|
|
|
|
|
let initialized = false;
|
|
|
|
|
|
|
|
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
|
2019-05-16 22:32:11 +00:00
|
|
|
const ERASE_STICKERS_KEY = 'erase-stickers';
|
2019-05-24 01:27:42 +00:00
|
|
|
const ERASE_TEMP_KEY = 'erase-temp';
|
2019-08-07 00:40:25 +00:00
|
|
|
const ERASE_DRAFTS_KEY = 'erase-drafts';
|
2018-08-08 17:00:33 +00:00
|
|
|
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
2018-07-27 01:13:56 +00:00
|
|
|
|
2022-11-17 00:29:15 +00:00
|
|
|
const INTERACTIVITY_DELAY = 50;
|
|
|
|
|
|
|
|
type DeleteOrphanedAttachmentsOptionsType = Readonly<{
|
|
|
|
orphanedAttachments: Set<string>;
|
|
|
|
sql: MainSQL;
|
|
|
|
userDataPath: string;
|
|
|
|
}>;
|
|
|
|
|
|
|
|
type CleanupOrphanedAttachmentsOptionsType = Readonly<{
|
|
|
|
sql: MainSQL;
|
|
|
|
userDataPath: string;
|
|
|
|
}>;
|
|
|
|
|
|
|
|
async function cleanupOrphanedAttachments({
|
|
|
|
sql,
|
|
|
|
userDataPath,
|
|
|
|
}: CleanupOrphanedAttachmentsOptionsType): Promise<void> {
|
|
|
|
await deleteAllBadges({
|
|
|
|
userDataPath,
|
|
|
|
pathsToKeep: await sql.sqlCall('getAllBadgeImageFileLocalPaths'),
|
|
|
|
});
|
|
|
|
|
|
|
|
const allStickers = await getAllStickers(userDataPath);
|
|
|
|
const orphanedStickers = await sql.sqlCall(
|
|
|
|
'removeKnownStickers',
|
|
|
|
allStickers
|
|
|
|
);
|
|
|
|
await deleteAllStickers({
|
|
|
|
userDataPath,
|
|
|
|
stickers: orphanedStickers,
|
|
|
|
});
|
|
|
|
|
|
|
|
const allDraftAttachments = await getAllDraftAttachments(userDataPath);
|
|
|
|
const orphanedDraftAttachments = await sql.sqlCall(
|
|
|
|
'removeKnownDraftAttachments',
|
|
|
|
allDraftAttachments
|
|
|
|
);
|
|
|
|
await deleteAllDraftAttachments({
|
|
|
|
userDataPath,
|
|
|
|
attachments: orphanedDraftAttachments,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Delete orphaned attachments from conversations and messages.
|
|
|
|
|
|
|
|
const orphanedAttachments = new Set(await getAllAttachments(userDataPath));
|
2022-11-17 20:06:19 +00:00
|
|
|
console.log(
|
|
|
|
'cleanupOrphanedAttachments: found ' +
|
|
|
|
`${orphanedAttachments.size} attachments on disk`
|
|
|
|
);
|
2022-11-17 00:29:15 +00:00
|
|
|
|
|
|
|
{
|
|
|
|
const attachments: ReadonlyArray<string> = await sql.sqlCall(
|
|
|
|
'getKnownConversationAttachments'
|
|
|
|
);
|
|
|
|
|
2022-11-17 20:06:19 +00:00
|
|
|
let missing = 0;
|
2022-11-17 00:29:15 +00:00
|
|
|
for (const known of attachments) {
|
2022-11-17 20:06:19 +00:00
|
|
|
if (!orphanedAttachments.delete(known)) {
|
|
|
|
missing += 1;
|
|
|
|
}
|
2022-11-17 00:29:15 +00:00
|
|
|
}
|
2022-11-17 20:06:19 +00:00
|
|
|
|
|
|
|
console.log(
|
|
|
|
`cleanupOrphanedAttachments: found ${attachments.length} conversation ` +
|
|
|
|
`attachments (${missing} missing), ${orphanedAttachments.size} remain`
|
|
|
|
);
|
2022-11-17 00:29:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// This call is intentionally not awaited. We block the app while running
|
|
|
|
// all fetches above to ensure that there are no in-flight attachments that
|
|
|
|
// are saved to disk, but not put into any message or conversation model yet.
|
|
|
|
deleteOrphanedAttachments({
|
|
|
|
orphanedAttachments,
|
|
|
|
sql,
|
|
|
|
userDataPath,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function deleteOrphanedAttachments({
|
|
|
|
orphanedAttachments,
|
|
|
|
sql,
|
|
|
|
userDataPath,
|
|
|
|
}: DeleteOrphanedAttachmentsOptionsType): void {
|
|
|
|
// This function *can* throw.
|
|
|
|
async function runWithPossibleException(): Promise<void> {
|
|
|
|
let cursor: MessageAttachmentsCursorType | undefined;
|
2022-11-17 20:06:19 +00:00
|
|
|
let totalFound = 0;
|
|
|
|
let totalMissing = 0;
|
2022-11-17 00:29:15 +00:00
|
|
|
try {
|
|
|
|
do {
|
|
|
|
let attachments: ReadonlyArray<string>;
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
({ attachments, cursor } = await sql.sqlCall(
|
|
|
|
'getKnownMessageAttachments',
|
|
|
|
cursor
|
|
|
|
));
|
|
|
|
|
2022-11-17 20:06:19 +00:00
|
|
|
totalFound += attachments.length;
|
|
|
|
|
2022-11-17 00:29:15 +00:00
|
|
|
for (const known of attachments) {
|
2022-11-17 20:06:19 +00:00
|
|
|
if (!orphanedAttachments.delete(known)) {
|
|
|
|
totalMissing += 1;
|
|
|
|
}
|
2022-11-17 00:29:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (cursor === undefined) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Let other SQL calls come through. There are hundreds of thousands of
|
|
|
|
// messages in the database and it might take time to go through them all.
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
await sleep(INTERACTIVITY_DELAY);
|
|
|
|
} while (cursor !== undefined && !cursor.done);
|
|
|
|
} finally {
|
|
|
|
if (cursor !== undefined) {
|
|
|
|
await sql.sqlCall('finishGetKnownMessageAttachments', cursor);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-17 20:06:19 +00:00
|
|
|
console.log(
|
|
|
|
`cleanupOrphanedAttachments: found ${totalFound} message ` +
|
|
|
|
`attachments, (${totalMissing} missing) ` +
|
|
|
|
`${orphanedAttachments.size} remain`
|
|
|
|
);
|
|
|
|
|
2022-11-17 00:29:15 +00:00
|
|
|
await deleteAllAttachments({
|
|
|
|
userDataPath,
|
|
|
|
attachments: Array.from(orphanedAttachments),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async function runSafe() {
|
|
|
|
const start = Date.now();
|
|
|
|
try {
|
|
|
|
await runWithPossibleException();
|
|
|
|
} catch (error) {
|
|
|
|
console.error(
|
|
|
|
'deleteOrphanedAttachments: error',
|
|
|
|
Errors.toLogFormat(error)
|
|
|
|
);
|
|
|
|
} finally {
|
|
|
|
const duration = Date.now() - start;
|
|
|
|
console.log(`deleteOrphanedAttachments: took ${duration}ms`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Intentionally not awaiting
|
|
|
|
runSafe();
|
|
|
|
}
|
|
|
|
|
2021-06-18 17:04:27 +00:00
|
|
|
export function initialize({
|
|
|
|
configDir,
|
2022-11-17 00:29:15 +00:00
|
|
|
sql,
|
2021-06-18 17:04:27 +00:00
|
|
|
}: {
|
|
|
|
configDir: string;
|
2022-11-17 00:29:15 +00:00
|
|
|
sql: MainSQL;
|
2021-06-18 17:04:27 +00:00
|
|
|
}): void {
|
2018-07-27 01:13:56 +00:00
|
|
|
if (initialized) {
|
|
|
|
throw new Error('initialze: Already initialized!');
|
|
|
|
}
|
|
|
|
initialized = true;
|
|
|
|
|
2021-06-18 17:04:27 +00:00
|
|
|
const attachmentsDir = getPath(configDir);
|
|
|
|
const stickersDir = getStickersPath(configDir);
|
|
|
|
const tempDir = getTempPath(configDir);
|
|
|
|
const draftDir = getDraftPath(configDir);
|
2019-05-24 01:27:42 +00:00
|
|
|
|
2022-11-17 00:29:15 +00:00
|
|
|
ipcMain.handle(ERASE_TEMP_KEY, () => rimraf.sync(tempDir));
|
|
|
|
ipcMain.handle(ERASE_ATTACHMENTS_KEY, () => rimraf.sync(attachmentsDir));
|
|
|
|
ipcMain.handle(ERASE_STICKERS_KEY, () => rimraf.sync(stickersDir));
|
|
|
|
ipcMain.handle(ERASE_DRAFTS_KEY, () => rimraf.sync(draftDir));
|
2019-08-07 00:40:25 +00:00
|
|
|
|
2022-11-17 00:29:15 +00:00
|
|
|
ipcMain.handle(CLEANUP_ORPHANED_ATTACHMENTS_KEY, async () => {
|
|
|
|
const start = Date.now();
|
|
|
|
await cleanupOrphanedAttachments({ sql, userDataPath: configDir });
|
|
|
|
const duration = Date.now() - start;
|
|
|
|
console.log(`cleanupOrphanedAttachments: took ${duration}ms`);
|
2018-08-08 17:00:33 +00:00
|
|
|
});
|
2018-07-27 01:13:56 +00:00
|
|
|
}
|