Interactive cleanup of orphaned attachments
This commit is contained in:
parent
e33bcd80b7
commit
854c946cc7
9 changed files with 504 additions and 1510 deletions
|
@ -62,7 +62,7 @@ export class SystemTraySettingCache {
|
|||
|
||||
const value =
|
||||
fastValue ??
|
||||
(await this.sql.sqlCall('getItemById', ['system-tray-setting']))?.value;
|
||||
(await this.sql.sqlCall('getItemById', 'system-tray-setting'))?.value;
|
||||
|
||||
if (value !== undefined) {
|
||||
result = parseSystemTraySetting(value);
|
||||
|
|
|
@ -4,11 +4,22 @@
|
|||
import { ipcMain } from 'electron';
|
||||
import * as rimraf from 'rimraf';
|
||||
import {
|
||||
getAllAttachments,
|
||||
getPath,
|
||||
getStickersPath,
|
||||
getTempPath,
|
||||
getDraftPath,
|
||||
deleteAll as deleteAllAttachments,
|
||||
deleteAllBadges,
|
||||
getAllStickers,
|
||||
deleteAllStickers,
|
||||
getAllDraftAttachments,
|
||||
deleteAllDraftAttachments,
|
||||
} from './attachments';
|
||||
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';
|
||||
|
||||
let initialized = false;
|
||||
|
||||
|
@ -18,12 +29,140 @@ const ERASE_TEMP_KEY = 'erase-temp';
|
|||
const ERASE_DRAFTS_KEY = 'erase-drafts';
|
||||
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
||||
|
||||
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));
|
||||
|
||||
{
|
||||
const attachments: ReadonlyArray<string> = await sql.sqlCall(
|
||||
'getKnownConversationAttachments'
|
||||
);
|
||||
|
||||
for (const known of attachments) {
|
||||
orphanedAttachments.delete(known);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
try {
|
||||
do {
|
||||
let attachments: ReadonlyArray<string>;
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
({ attachments, cursor } = await sql.sqlCall(
|
||||
'getKnownMessageAttachments',
|
||||
cursor
|
||||
));
|
||||
|
||||
for (const known of attachments) {
|
||||
orphanedAttachments.delete(known);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
export function initialize({
|
||||
configDir,
|
||||
cleanupOrphanedAttachments,
|
||||
sql,
|
||||
}: {
|
||||
configDir: string;
|
||||
cleanupOrphanedAttachments: () => Promise<void>;
|
||||
sql: MainSQL;
|
||||
}): void {
|
||||
if (initialized) {
|
||||
throw new Error('initialze: Already initialized!');
|
||||
|
@ -35,58 +174,15 @@ export function initialize({
|
|||
const tempDir = getTempPath(configDir);
|
||||
const draftDir = getDraftPath(configDir);
|
||||
|
||||
ipcMain.on(ERASE_TEMP_KEY, event => {
|
||||
try {
|
||||
rimraf.sync(tempDir);
|
||||
event.sender.send(`${ERASE_TEMP_KEY}-done`);
|
||||
} catch (error) {
|
||||
const errorForDisplay = error && error.stack ? error.stack : error;
|
||||
console.log(`erase temp error: ${errorForDisplay}`);
|
||||
event.sender.send(`${ERASE_TEMP_KEY}-done`, error);
|
||||
}
|
||||
});
|
||||
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));
|
||||
|
||||
ipcMain.on(ERASE_ATTACHMENTS_KEY, event => {
|
||||
try {
|
||||
rimraf.sync(attachmentsDir);
|
||||
event.sender.send(`${ERASE_ATTACHMENTS_KEY}-done`);
|
||||
} catch (error) {
|
||||
const errorForDisplay = error && error.stack ? error.stack : error;
|
||||
console.log(`erase attachments error: ${errorForDisplay}`);
|
||||
event.sender.send(`${ERASE_ATTACHMENTS_KEY}-done`, error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on(ERASE_STICKERS_KEY, event => {
|
||||
try {
|
||||
rimraf.sync(stickersDir);
|
||||
event.sender.send(`${ERASE_STICKERS_KEY}-done`);
|
||||
} catch (error) {
|
||||
const errorForDisplay = error && error.stack ? error.stack : error;
|
||||
console.log(`erase stickers error: ${errorForDisplay}`);
|
||||
event.sender.send(`${ERASE_STICKERS_KEY}-done`, error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on(ERASE_DRAFTS_KEY, event => {
|
||||
try {
|
||||
rimraf.sync(draftDir);
|
||||
event.sender.send(`${ERASE_DRAFTS_KEY}-done`);
|
||||
} catch (error) {
|
||||
const errorForDisplay = error && error.stack ? error.stack : error;
|
||||
console.log(`erase drafts error: ${errorForDisplay}`);
|
||||
event.sender.send(`${ERASE_DRAFTS_KEY}-done`, error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on(CLEANUP_ORPHANED_ATTACHMENTS_KEY, async event => {
|
||||
try {
|
||||
await cleanupOrphanedAttachments();
|
||||
event.sender.send(`${CLEANUP_ORPHANED_ATTACHMENTS_KEY}-done`);
|
||||
} catch (error) {
|
||||
const errorForDisplay = error && error.stack ? error.stack : error;
|
||||
console.log(`cleanup orphaned attachments error: ${errorForDisplay}`);
|
||||
event.sender.send(`${CLEANUP_ORPHANED_ATTACHMENTS_KEY}-done`, error);
|
||||
}
|
||||
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`);
|
||||
});
|
||||
}
|
||||
|
|
77
app/main.ts
77
app/main.ts
|
@ -224,17 +224,17 @@ let sqlInitTimeEnd = 0;
|
|||
const sql = new MainSQL();
|
||||
const heicConverter = getHeicConverter();
|
||||
|
||||
async function getSpellCheckSetting() {
|
||||
async function getSpellCheckSetting(): Promise<boolean> {
|
||||
const fastValue = ephemeralConfig.get('spell-check');
|
||||
if (fastValue !== undefined) {
|
||||
if (typeof fastValue === 'boolean') {
|
||||
getLogger().info('got fast spellcheck setting', fastValue);
|
||||
return fastValue;
|
||||
}
|
||||
|
||||
const json = await sql.sqlCall('getItemById', ['spell-check']);
|
||||
const json = await sql.sqlCall('getItemById', 'spell-check');
|
||||
|
||||
// Default to `true` if setting doesn't exist yet
|
||||
const slowValue = json ? json.value : true;
|
||||
const slowValue = typeof json?.value === 'boolean' ? json.value : true;
|
||||
|
||||
ephemeralConfig.set('spell-check', slowValue);
|
||||
|
||||
|
@ -260,7 +260,7 @@ async function getThemeSetting({
|
|||
return 'system';
|
||||
}
|
||||
|
||||
const json = await sql.sqlCall('getItemById', ['theme-setting']);
|
||||
const json = await sql.sqlCall('getItemById', 'theme-setting');
|
||||
|
||||
// Default to `system` if setting doesn't exist or is invalid
|
||||
const setting: unknown = json?.value;
|
||||
|
@ -928,7 +928,7 @@ async function createWindow() {
|
|||
}
|
||||
|
||||
// Renderer asks if we are done with the database
|
||||
ipc.on('database-ready', async event => {
|
||||
ipc.handle('database-ready', async () => {
|
||||
if (!sqlInitPromise) {
|
||||
getLogger().error('database-ready requested, but sqlInitPromise is falsey');
|
||||
return;
|
||||
|
@ -944,7 +944,6 @@ ipc.on('database-ready', async event => {
|
|||
}
|
||||
|
||||
getLogger().info('sending `database-ready`');
|
||||
event.sender.send('database-ready');
|
||||
});
|
||||
|
||||
ipc.on('show-window', () => {
|
||||
|
@ -1259,8 +1258,8 @@ async function showSettingsWindow() {
|
|||
|
||||
async function getIsLinked() {
|
||||
try {
|
||||
const number = await sql.sqlCall('getItemById', ['number_id']);
|
||||
const password = await sql.sqlCall('getItemById', ['password']);
|
||||
const number = await sql.sqlCall('getItemById', 'number_id');
|
||||
const password = await sql.sqlCall('getItemById', 'password');
|
||||
return Boolean(number && password);
|
||||
} catch (e) {
|
||||
return false;
|
||||
|
@ -1651,12 +1650,10 @@ app.on('ready', async () => {
|
|||
|
||||
// Update both stores
|
||||
ephemeralConfig.set('system-tray-setting', newValue);
|
||||
await sql.sqlCall('createOrUpdateItem', [
|
||||
{
|
||||
id: 'system-tray-setting',
|
||||
value: newValue,
|
||||
},
|
||||
]);
|
||||
await sql.sqlCall('createOrUpdateItem', {
|
||||
id: 'system-tray-setting',
|
||||
value: newValue,
|
||||
});
|
||||
|
||||
if (OS.isWindows()) {
|
||||
getLogger().info('app.ready: enabling open at login');
|
||||
|
@ -1806,8 +1803,8 @@ app.on('ready', async () => {
|
|||
// Initialize IPC channels before creating the window
|
||||
|
||||
attachmentChannel.initialize({
|
||||
sql,
|
||||
configDir: userDataPath,
|
||||
cleanupOrphanedAttachments,
|
||||
});
|
||||
sqlChannels.initialize(sql);
|
||||
PowerChannel.initialize({
|
||||
|
@ -1835,10 +1832,10 @@ app.on('ready', async () => {
|
|||
|
||||
try {
|
||||
const IDB_KEY = 'indexeddb-delete-needed';
|
||||
const item = await sql.sqlCall('getItemById', [IDB_KEY]);
|
||||
const item = await sql.sqlCall('getItemById', IDB_KEY);
|
||||
if (item && item.value) {
|
||||
await sql.sqlCall('removeIndexedDBFiles', []);
|
||||
await sql.sqlCall('removeItemById', [IDB_KEY]);
|
||||
await sql.sqlCall('removeIndexedDBFiles');
|
||||
await sql.sqlCall('removeItemById', IDB_KEY);
|
||||
}
|
||||
} catch (err) {
|
||||
getLogger().error(
|
||||
|
@ -1847,43 +1844,6 @@ app.on('ready', async () => {
|
|||
);
|
||||
}
|
||||
|
||||
async function cleanupOrphanedAttachments() {
|
||||
const allAttachments = await attachments.getAllAttachments(userDataPath);
|
||||
const orphanedAttachments = await sql.sqlCall('removeKnownAttachments', [
|
||||
allAttachments,
|
||||
]);
|
||||
await attachments.deleteAll({
|
||||
userDataPath,
|
||||
attachments: orphanedAttachments,
|
||||
});
|
||||
|
||||
await attachments.deleteAllBadges({
|
||||
userDataPath,
|
||||
pathsToKeep: await sql.sqlCall('getAllBadgeImageFileLocalPaths', []),
|
||||
});
|
||||
|
||||
const allStickers = await attachments.getAllStickers(userDataPath);
|
||||
const orphanedStickers = await sql.sqlCall('removeKnownStickers', [
|
||||
allStickers,
|
||||
]);
|
||||
await attachments.deleteAllStickers({
|
||||
userDataPath,
|
||||
stickers: orphanedStickers,
|
||||
});
|
||||
|
||||
const allDraftAttachments = await attachments.getAllDraftAttachments(
|
||||
userDataPath
|
||||
);
|
||||
const orphanedDraftAttachments = await sql.sqlCall(
|
||||
'removeKnownDraftAttachments',
|
||||
[allDraftAttachments]
|
||||
);
|
||||
await attachments.deleteAllDraftAttachments({
|
||||
userDataPath,
|
||||
attachments: orphanedDraftAttachments,
|
||||
});
|
||||
}
|
||||
|
||||
ready = true;
|
||||
|
||||
setupMenu();
|
||||
|
@ -2320,10 +2280,7 @@ ipc.on('install-sticker-pack', (_event, packId, packKeyHex) => {
|
|||
}
|
||||
});
|
||||
|
||||
ipc.on('ensure-file-permissions', async event => {
|
||||
await ensureFilePermissions();
|
||||
event.reply('ensure-file-permissions-done');
|
||||
});
|
||||
ipc.handle('ensure-file-permissions', () => ensureFilePermissions());
|
||||
|
||||
/**
|
||||
* Ensure files in the user's data directory have the proper permissions.
|
||||
|
|
|
@ -3,21 +3,18 @@
|
|||
|
||||
import { ipcMain } from 'electron';
|
||||
|
||||
import type { MainSQL } from '../ts/sql/main';
|
||||
import { remove as removeUserConfig } from './user_config';
|
||||
import { remove as removeEphemeralConfig } from './ephemeral_config';
|
||||
|
||||
type SQLType = {
|
||||
sqlCall(callName: string, args: ReadonlyArray<unknown>): unknown;
|
||||
};
|
||||
|
||||
let sql: SQLType | undefined;
|
||||
let sql: Pick<MainSQL, 'sqlCall'> | undefined;
|
||||
|
||||
let initialized = false;
|
||||
|
||||
const SQL_CHANNEL_KEY = 'sql-channel';
|
||||
const ERASE_SQL_KEY = 'erase-sql-key';
|
||||
|
||||
export function initialize(mainSQL: SQLType): void {
|
||||
export function initialize(mainSQL: Pick<MainSQL, 'sqlCall'>): void {
|
||||
if (initialized) {
|
||||
throw new Error('sqlChannels: already initialized!');
|
||||
}
|
||||
|
@ -25,33 +22,15 @@ export function initialize(mainSQL: SQLType): void {
|
|||
|
||||
sql = mainSQL;
|
||||
|
||||
ipcMain.on(SQL_CHANNEL_KEY, async (event, jobId, callName, ...args) => {
|
||||
try {
|
||||
if (!sql) {
|
||||
throw new Error(`${SQL_CHANNEL_KEY}: Not yet initialized!`);
|
||||
}
|
||||
const result = await sql.sqlCall(callName, args);
|
||||
event.sender.send(`${SQL_CHANNEL_KEY}-done`, jobId, null, result);
|
||||
} catch (error) {
|
||||
const errorForDisplay = error && error.stack ? error.stack : error;
|
||||
console.log(
|
||||
`sql channel error with call ${callName}: ${errorForDisplay}`
|
||||
);
|
||||
if (!event.sender.isDestroyed()) {
|
||||
event.sender.send(`${SQL_CHANNEL_KEY}-done`, jobId, errorForDisplay);
|
||||
}
|
||||
ipcMain.handle(SQL_CHANNEL_KEY, (_event, callName, ...args) => {
|
||||
if (!sql) {
|
||||
throw new Error(`${SQL_CHANNEL_KEY}: Not yet initialized!`);
|
||||
}
|
||||
return sql.sqlCall(callName, ...args);
|
||||
});
|
||||
|
||||
ipcMain.on(ERASE_SQL_KEY, async event => {
|
||||
try {
|
||||
removeUserConfig();
|
||||
removeEphemeralConfig();
|
||||
event.sender.send(`${ERASE_SQL_KEY}-done`);
|
||||
} catch (error) {
|
||||
const errorForDisplay = error && error.stack ? error.stack : error;
|
||||
console.log(`sql-erase error: ${errorForDisplay}`);
|
||||
event.sender.send(`${ERASE_SQL_KEY}-done`, error);
|
||||
}
|
||||
ipcMain.handle(ERASE_SQL_KEY, () => {
|
||||
removeUserConfig();
|
||||
removeEphemeralConfig();
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue