Resumable attachment downloads
This commit is contained in:
parent
2c92591b59
commit
38f532cdda
22 changed files with 401 additions and 89 deletions
|
@ -18,13 +18,17 @@ import {
|
||||||
import { RangeFinder, DefaultStorage } from '@indutny/range-finder';
|
import { RangeFinder, DefaultStorage } from '@indutny/range-finder';
|
||||||
import {
|
import {
|
||||||
getAllAttachments,
|
getAllAttachments,
|
||||||
|
getAllDownloads,
|
||||||
getAvatarsPath,
|
getAvatarsPath,
|
||||||
getPath,
|
getPath,
|
||||||
getStickersPath,
|
getStickersPath,
|
||||||
getTempPath,
|
getTempPath,
|
||||||
getDraftPath,
|
getDraftPath,
|
||||||
|
getDownloadsPath,
|
||||||
deleteAll as deleteAllAttachments,
|
deleteAll as deleteAllAttachments,
|
||||||
deleteAllBadges,
|
deleteAllBadges,
|
||||||
|
deleteAllDownloads,
|
||||||
|
deleteStaleDownloads,
|
||||||
getAllStickers,
|
getAllStickers,
|
||||||
deleteAllStickers,
|
deleteAllStickers,
|
||||||
getAllDraftAttachments,
|
getAllDraftAttachments,
|
||||||
|
@ -50,6 +54,8 @@ const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
|
||||||
const ERASE_STICKERS_KEY = 'erase-stickers';
|
const ERASE_STICKERS_KEY = 'erase-stickers';
|
||||||
const ERASE_TEMP_KEY = 'erase-temp';
|
const ERASE_TEMP_KEY = 'erase-temp';
|
||||||
const ERASE_DRAFTS_KEY = 'erase-drafts';
|
const ERASE_DRAFTS_KEY = 'erase-drafts';
|
||||||
|
const ERASE_DOWNLOADS_KEY = 'erase-downloads';
|
||||||
|
const CLEANUP_DOWNLOADS_KEY = 'cleanup-downloads';
|
||||||
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
||||||
|
|
||||||
const INTERACTIVITY_DELAY = 50;
|
const INTERACTIVITY_DELAY = 50;
|
||||||
|
@ -189,6 +195,7 @@ const dispositionSchema = z.enum([
|
||||||
|
|
||||||
type DeleteOrphanedAttachmentsOptionsType = Readonly<{
|
type DeleteOrphanedAttachmentsOptionsType = Readonly<{
|
||||||
orphanedAttachments: Set<string>;
|
orphanedAttachments: Set<string>;
|
||||||
|
orphanedDownloads: Set<string>;
|
||||||
sql: MainSQL;
|
sql: MainSQL;
|
||||||
userDataPath: string;
|
userDataPath: string;
|
||||||
}>;
|
}>;
|
||||||
|
@ -235,8 +242,14 @@ async function cleanupOrphanedAttachments({
|
||||||
`${orphanedAttachments.size} attachments on disk`
|
`${orphanedAttachments.size} attachments on disk`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const orphanedDownloads = new Set(await getAllDownloads(userDataPath));
|
||||||
|
console.log(
|
||||||
|
'cleanupOrphanedAttachments: found ' +
|
||||||
|
`${orphanedDownloads.size} downloads on disk`
|
||||||
|
);
|
||||||
|
|
||||||
{
|
{
|
||||||
const attachments: ReadonlyArray<string> = await sql.sqlRead(
|
const attachments: Array<string> = await sql.sqlRead(
|
||||||
'getKnownConversationAttachments'
|
'getKnownConversationAttachments'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -258,6 +271,7 @@ async function cleanupOrphanedAttachments({
|
||||||
// are saved to disk, but not put into any message or conversation model yet.
|
// are saved to disk, but not put into any message or conversation model yet.
|
||||||
deleteOrphanedAttachments({
|
deleteOrphanedAttachments({
|
||||||
orphanedAttachments,
|
orphanedAttachments,
|
||||||
|
orphanedDownloads,
|
||||||
sql,
|
sql,
|
||||||
userDataPath,
|
userDataPath,
|
||||||
});
|
});
|
||||||
|
@ -265,6 +279,7 @@ async function cleanupOrphanedAttachments({
|
||||||
|
|
||||||
function deleteOrphanedAttachments({
|
function deleteOrphanedAttachments({
|
||||||
orphanedAttachments,
|
orphanedAttachments,
|
||||||
|
orphanedDownloads,
|
||||||
sql,
|
sql,
|
||||||
userDataPath,
|
userDataPath,
|
||||||
}: DeleteOrphanedAttachmentsOptionsType): void {
|
}: DeleteOrphanedAttachmentsOptionsType): void {
|
||||||
|
@ -273,17 +288,21 @@ function deleteOrphanedAttachments({
|
||||||
let cursor: MessageAttachmentsCursorType | undefined;
|
let cursor: MessageAttachmentsCursorType | undefined;
|
||||||
let totalFound = 0;
|
let totalFound = 0;
|
||||||
let totalMissing = 0;
|
let totalMissing = 0;
|
||||||
|
let totalDownloadsFound = 0;
|
||||||
|
let totalDownloadsMissing = 0;
|
||||||
try {
|
try {
|
||||||
do {
|
do {
|
||||||
let attachments: ReadonlyArray<string>;
|
let attachments: ReadonlyArray<string>;
|
||||||
|
let downloads: ReadonlyArray<string>;
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
({ attachments, cursor } = await sql.sqlRead(
|
({ attachments, downloads, cursor } = await sql.sqlRead(
|
||||||
'getKnownMessageAttachments',
|
'getKnownMessageAttachments',
|
||||||
cursor
|
cursor
|
||||||
));
|
));
|
||||||
|
|
||||||
totalFound += attachments.length;
|
totalFound += attachments.length;
|
||||||
|
totalDownloadsFound += downloads.length;
|
||||||
|
|
||||||
for (const known of attachments) {
|
for (const known of attachments) {
|
||||||
if (!orphanedAttachments.delete(known)) {
|
if (!orphanedAttachments.delete(known)) {
|
||||||
|
@ -291,6 +310,12 @@ function deleteOrphanedAttachments({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const known of downloads) {
|
||||||
|
if (!orphanedDownloads.delete(known)) {
|
||||||
|
totalDownloadsMissing += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (cursor === undefined) {
|
if (cursor === undefined) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -316,6 +341,16 @@ function deleteOrphanedAttachments({
|
||||||
userDataPath,
|
userDataPath,
|
||||||
attachments: Array.from(orphanedAttachments),
|
attachments: Array.from(orphanedAttachments),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`cleanupOrphanedAttachments: found ${totalDownloadsFound} downloads ` +
|
||||||
|
`(${totalDownloadsMissing} missing) ` +
|
||||||
|
`${orphanedDownloads.size} remain`
|
||||||
|
);
|
||||||
|
await deleteAllDownloads({
|
||||||
|
userDataPath,
|
||||||
|
downloads: Array.from(orphanedDownloads),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runSafe() {
|
async function runSafe() {
|
||||||
|
@ -341,6 +376,7 @@ let attachmentsDir: string | undefined;
|
||||||
let stickersDir: string | undefined;
|
let stickersDir: string | undefined;
|
||||||
let tempDir: string | undefined;
|
let tempDir: string | undefined;
|
||||||
let draftDir: string | undefined;
|
let draftDir: string | undefined;
|
||||||
|
let downloadsDir: string | undefined;
|
||||||
let avatarDataDir: string | undefined;
|
let avatarDataDir: string | undefined;
|
||||||
|
|
||||||
export function initialize({
|
export function initialize({
|
||||||
|
@ -359,6 +395,7 @@ export function initialize({
|
||||||
stickersDir = getStickersPath(configDir);
|
stickersDir = getStickersPath(configDir);
|
||||||
tempDir = getTempPath(configDir);
|
tempDir = getTempPath(configDir);
|
||||||
draftDir = getDraftPath(configDir);
|
draftDir = getDraftPath(configDir);
|
||||||
|
downloadsDir = getDownloadsPath(configDir);
|
||||||
avatarDataDir = getAvatarsPath(configDir);
|
avatarDataDir = getAvatarsPath(configDir);
|
||||||
|
|
||||||
ipcMain.handle(ERASE_TEMP_KEY, () => {
|
ipcMain.handle(ERASE_TEMP_KEY, () => {
|
||||||
|
@ -377,6 +414,10 @@ export function initialize({
|
||||||
strictAssert(draftDir != null, 'not initialized');
|
strictAssert(draftDir != null, 'not initialized');
|
||||||
rimraf.sync(draftDir);
|
rimraf.sync(draftDir);
|
||||||
});
|
});
|
||||||
|
ipcMain.handle(ERASE_DOWNLOADS_KEY, () => {
|
||||||
|
strictAssert(downloadsDir != null, 'not initialized');
|
||||||
|
rimraf.sync(downloadsDir);
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle(CLEANUP_ORPHANED_ATTACHMENTS_KEY, async () => {
|
ipcMain.handle(CLEANUP_ORPHANED_ATTACHMENTS_KEY, async () => {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
@ -385,6 +426,13 @@ export function initialize({
|
||||||
console.log(`cleanupOrphanedAttachments: took ${duration}ms`);
|
console.log(`cleanupOrphanedAttachments: took ${duration}ms`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(CLEANUP_DOWNLOADS_KEY, async () => {
|
||||||
|
const start = Date.now();
|
||||||
|
await deleteStaleDownloads(configDir);
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
console.log(`cleanupDownloads: took ${duration}ms`);
|
||||||
|
});
|
||||||
|
|
||||||
protocol.handle('attachment', handleAttachmentRequest);
|
protocol.handle('attachment', handleAttachmentRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,18 +2,24 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { PassThrough } from 'node:stream';
|
import { PassThrough } from 'node:stream';
|
||||||
|
import { stat } from 'node:fs/promises';
|
||||||
import { join, relative, normalize } from 'path';
|
import { join, relative, normalize } from 'path';
|
||||||
|
import pMap from 'p-map';
|
||||||
import fastGlob from 'fast-glob';
|
import fastGlob from 'fast-glob';
|
||||||
import fse from 'fs-extra';
|
import fse from 'fs-extra';
|
||||||
import { map, isString } from 'lodash';
|
import { map, isString } from 'lodash';
|
||||||
import normalizePath from 'normalize-path';
|
import normalizePath from 'normalize-path';
|
||||||
import { isPathInside } from '../ts/util/isPathInside';
|
import { isPathInside } from '../ts/util/isPathInside';
|
||||||
|
import { DAY } from '../ts/util/durations';
|
||||||
|
import { isOlderThan } from '../ts/util/timestamp';
|
||||||
|
import { isNotNil } from '../ts/util/isNotNil';
|
||||||
import {
|
import {
|
||||||
generateKeys,
|
generateKeys,
|
||||||
decryptAttachmentV2ToSink,
|
decryptAttachmentV2ToSink,
|
||||||
encryptAttachmentV2ToDisk,
|
encryptAttachmentV2ToDisk,
|
||||||
} from '../ts/AttachmentCrypto';
|
} from '../ts/AttachmentCrypto';
|
||||||
import type { LocalAttachmentV2Type } from '../ts/types/Attachment';
|
import type { LocalAttachmentV2Type } from '../ts/types/Attachment';
|
||||||
|
import * as Errors from '../ts/types/errors';
|
||||||
|
|
||||||
const PATH = 'attachments.noindex';
|
const PATH = 'attachments.noindex';
|
||||||
const AVATAR_PATH = 'avatars.noindex';
|
const AVATAR_PATH = 'avatars.noindex';
|
||||||
|
@ -22,9 +28,12 @@ const STICKER_PATH = 'stickers.noindex';
|
||||||
const TEMP_PATH = 'temp';
|
const TEMP_PATH = 'temp';
|
||||||
const UPDATE_CACHE_PATH = 'update-cache';
|
const UPDATE_CACHE_PATH = 'update-cache';
|
||||||
const DRAFT_PATH = 'drafts.noindex';
|
const DRAFT_PATH = 'drafts.noindex';
|
||||||
|
const DOWNLOADS_PATH = 'downloads.noindex';
|
||||||
|
|
||||||
const CACHED_PATHS = new Map<string, string>();
|
const CACHED_PATHS = new Map<string, string>();
|
||||||
|
|
||||||
|
const FS_CONCURRENCY = 100;
|
||||||
|
|
||||||
const createPathGetter =
|
const createPathGetter =
|
||||||
(subpath: string) =>
|
(subpath: string) =>
|
||||||
(userDataPath: string): string => {
|
(userDataPath: string): string => {
|
||||||
|
@ -52,6 +61,7 @@ const createPathGetter =
|
||||||
export const getAvatarsPath = createPathGetter(AVATAR_PATH);
|
export const getAvatarsPath = createPathGetter(AVATAR_PATH);
|
||||||
export const getBadgesPath = createPathGetter(BADGES_PATH);
|
export const getBadgesPath = createPathGetter(BADGES_PATH);
|
||||||
export const getDraftPath = createPathGetter(DRAFT_PATH);
|
export const getDraftPath = createPathGetter(DRAFT_PATH);
|
||||||
|
export const getDownloadsPath = createPathGetter(DOWNLOADS_PATH);
|
||||||
export const getPath = createPathGetter(PATH);
|
export const getPath = createPathGetter(PATH);
|
||||||
export const getStickersPath = createPathGetter(STICKER_PATH);
|
export const getStickersPath = createPathGetter(STICKER_PATH);
|
||||||
export const getTempPath = createPathGetter(TEMP_PATH);
|
export const getTempPath = createPathGetter(TEMP_PATH);
|
||||||
|
@ -88,6 +98,16 @@ export const getAllAttachments = async (
|
||||||
return map(files, file => relative(dir, file));
|
return map(files, file => relative(dir, file));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAllDownloads = async (
|
||||||
|
userDataPath: string
|
||||||
|
): Promise<ReadonlyArray<string>> => {
|
||||||
|
const dir = getDownloadsPath(userDataPath);
|
||||||
|
const pattern = normalizePath(join(dir, '**', '*'));
|
||||||
|
|
||||||
|
const files = await fastGlob(pattern, { onlyFiles: true });
|
||||||
|
return map(files, file => relative(dir, file));
|
||||||
|
};
|
||||||
|
|
||||||
const getAllBadgeImageFiles = async (
|
const getAllBadgeImageFiles = async (
|
||||||
userDataPath: string
|
userDataPath: string
|
||||||
): Promise<ReadonlyArray<string>> => {
|
): Promise<ReadonlyArray<string>> => {
|
||||||
|
@ -123,6 +143,43 @@ export const clearTempPath = (userDataPath: string): Promise<void> => {
|
||||||
return fse.emptyDir(tempPath);
|
return fse.emptyDir(tempPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteStaleDownloads = async (
|
||||||
|
userDataPath: string
|
||||||
|
): Promise<void> => {
|
||||||
|
const dir = getDownloadsPath(userDataPath);
|
||||||
|
const files = await getAllDownloads(userDataPath);
|
||||||
|
|
||||||
|
const result = await pMap(
|
||||||
|
files,
|
||||||
|
async file => {
|
||||||
|
try {
|
||||||
|
const { birthtimeMs } = await stat(join(dir, file));
|
||||||
|
if (isOlderThan(birthtimeMs, DAY)) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// No longer exists
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error(
|
||||||
|
'deleteStaleDownloads: failed to get file stats',
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
{ concurrency: FS_CONCURRENCY }
|
||||||
|
);
|
||||||
|
|
||||||
|
const stale = result.filter(isNotNil);
|
||||||
|
if (stale.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`deleteStaleDownloads: found ${stale.length}`);
|
||||||
|
await deleteAllDownloads({ userDataPath, downloads: stale });
|
||||||
|
};
|
||||||
|
|
||||||
export const deleteAll = async ({
|
export const deleteAll = async ({
|
||||||
userDataPath,
|
userDataPath,
|
||||||
attachments,
|
attachments,
|
||||||
|
@ -132,15 +189,25 @@ export const deleteAll = async ({
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
const deleteFromDisk = createDeleter(getPath(userDataPath));
|
const deleteFromDisk = createDeleter(getPath(userDataPath));
|
||||||
|
|
||||||
for (let index = 0, max = attachments.length; index < max; index += 1) {
|
await pMap(attachments, deleteFromDisk, { concurrency: FS_CONCURRENCY });
|
||||||
const file = attachments[index];
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await deleteFromDisk(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`deleteAll: deleted ${attachments.length} files`);
|
console.log(`deleteAll: deleted ${attachments.length} files`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteAllDownloads = async ({
|
||||||
|
userDataPath,
|
||||||
|
downloads,
|
||||||
|
}: {
|
||||||
|
userDataPath: string;
|
||||||
|
downloads: ReadonlyArray<string>;
|
||||||
|
}): Promise<void> => {
|
||||||
|
const deleteFromDisk = createDeleter(getDownloadsPath(userDataPath));
|
||||||
|
|
||||||
|
await pMap(downloads, deleteFromDisk, { concurrency: FS_CONCURRENCY });
|
||||||
|
|
||||||
|
console.log(`deleteAllDownloads: deleted ${downloads.length} files`);
|
||||||
|
};
|
||||||
|
|
||||||
export const deleteAllStickers = async ({
|
export const deleteAllStickers = async ({
|
||||||
userDataPath,
|
userDataPath,
|
||||||
stickers,
|
stickers,
|
||||||
|
@ -150,11 +217,7 @@ export const deleteAllStickers = async ({
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
const deleteFromDisk = createDeleter(getStickersPath(userDataPath));
|
const deleteFromDisk = createDeleter(getStickersPath(userDataPath));
|
||||||
|
|
||||||
for (let index = 0, max = stickers.length; index < max; index += 1) {
|
await pMap(stickers, deleteFromDisk, { concurrency: FS_CONCURRENCY });
|
||||||
const file = stickers[index];
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await deleteFromDisk(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`deleteAllStickers: deleted ${stickers.length} files`);
|
console.log(`deleteAllStickers: deleted ${stickers.length} files`);
|
||||||
};
|
};
|
||||||
|
@ -189,11 +252,7 @@ export const deleteAllDraftAttachments = async ({
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
const deleteFromDisk = createDeleter(getDraftPath(userDataPath));
|
const deleteFromDisk = createDeleter(getDraftPath(userDataPath));
|
||||||
|
|
||||||
for (let index = 0, max = attachments.length; index < max; index += 1) {
|
await pMap(attachments, deleteFromDisk, { concurrency: FS_CONCURRENCY });
|
||||||
const file = attachments[index];
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await deleteFromDisk(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`deleteAllDraftAttachments: deleted ${attachments.length} files`);
|
console.log(`deleteAllDraftAttachments: deleted ${attachments.length} files`);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2208,6 +2208,15 @@ app.on('ready', async () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await attachments.deleteStaleDownloads(userDataPath);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
'main/ready: Error deleting stale downloads:',
|
||||||
|
Errors.toLogFormat(err)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize IPC channels before creating the window
|
// Initialize IPC channels before creating the window
|
||||||
|
|
||||||
attachmentChannel.initialize({
|
attachmentChannel.initialize({
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
getAvatarsPath,
|
getAvatarsPath,
|
||||||
getBadgesPath,
|
getBadgesPath,
|
||||||
getDraftPath,
|
getDraftPath,
|
||||||
|
getDownloadsPath,
|
||||||
getPath,
|
getPath,
|
||||||
getStickersPath,
|
getStickersPath,
|
||||||
getTempPath,
|
getTempPath,
|
||||||
|
@ -61,6 +62,7 @@ function _createFileHandler({
|
||||||
getAvatarsPath(userDataPath),
|
getAvatarsPath(userDataPath),
|
||||||
getBadgesPath(userDataPath),
|
getBadgesPath(userDataPath),
|
||||||
getDraftPath(userDataPath),
|
getDraftPath(userDataPath),
|
||||||
|
getDownloadsPath(userDataPath),
|
||||||
getPath(userDataPath),
|
getPath(userDataPath),
|
||||||
getStickersPath(userDataPath),
|
getStickersPath(userDataPath),
|
||||||
getTempPath(userDataPath),
|
getTempPath(userDataPath),
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { unlinkSync, createReadStream, createWriteStream } from 'fs';
|
import { createReadStream, createWriteStream } from 'fs';
|
||||||
import { open } from 'fs/promises';
|
import { open, unlink } from 'fs/promises';
|
||||||
import { createCipheriv, createHash, createHmac, randomBytes } from 'crypto';
|
import { createCipheriv, createHash, createHmac, randomBytes } from 'crypto';
|
||||||
import type { Hash } from 'crypto';
|
import type { Hash } from 'crypto';
|
||||||
import { PassThrough, Transform, type Writable, Readable } from 'stream';
|
import { PassThrough, Transform, type Writable, Readable } from 'stream';
|
||||||
|
@ -114,7 +114,7 @@ export async function encryptAttachmentV2ToDisk(
|
||||||
sink: createWriteStream(absoluteTargetPath),
|
sink: createWriteStream(absoluteTargetPath),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
safeUnlinkSync(absoluteTargetPath);
|
await safeUnlink(absoluteTargetPath);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,7 +307,7 @@ export async function decryptAttachmentV2(
|
||||||
`${logId}: Failed to decrypt attachment to disk`,
|
`${logId}: Failed to decrypt attachment to disk`,
|
||||||
Errors.toLogFormat(error)
|
Errors.toLogFormat(error)
|
||||||
);
|
);
|
||||||
safeUnlinkSync(absoluteTargetPath);
|
await safeUnlink(absoluteTargetPath);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
await writeFd?.close();
|
await writeFd?.close();
|
||||||
|
@ -523,7 +523,7 @@ export async function decryptAndReencryptLocally(
|
||||||
`${logId}: Failed to decrypt attachment`,
|
`${logId}: Failed to decrypt attachment`,
|
||||||
Errors.toLogFormat(error)
|
Errors.toLogFormat(error)
|
||||||
);
|
);
|
||||||
safeUnlinkSync(absoluteTargetPath);
|
await safeUnlink(absoluteTargetPath);
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
await writeFd?.close();
|
await writeFd?.close();
|
||||||
|
@ -618,9 +618,9 @@ export function getPlaintextHashForInMemoryAttachment(
|
||||||
* Unlinks a file without throwing an error if it doesn't exist.
|
* Unlinks a file without throwing an error if it doesn't exist.
|
||||||
* Throws an error if it fails to unlink for any other reason.
|
* Throws an error if it fails to unlink for any other reason.
|
||||||
*/
|
*/
|
||||||
export function safeUnlinkSync(filePath: string): void {
|
export async function safeUnlink(filePath: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
unlinkSync(filePath);
|
await unlink(filePath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore if file doesn't exist
|
// Ignore if file doesn't exist
|
||||||
if (error.code !== 'ENOENT') {
|
if (error.code !== 'ENOENT') {
|
||||||
|
|
|
@ -1104,6 +1104,10 @@ export async function startApp(): Promise<void> {
|
||||||
reportLongRunningTasks();
|
reportLongRunningTasks();
|
||||||
}, FIVE_MINUTES);
|
}, FIVE_MINUTES);
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
drop(window.Events.cleanupDownloads());
|
||||||
|
}, DAY);
|
||||||
|
|
||||||
let mainWindowStats = {
|
let mainWindowStats = {
|
||||||
isMaximized: false,
|
isMaximized: false,
|
||||||
isFullScreen: false,
|
isFullScreen: false,
|
||||||
|
|
|
@ -411,7 +411,7 @@ export async function runDownloadAttachmentJobInner({
|
||||||
|
|
||||||
const upgradedAttachment =
|
const upgradedAttachment =
|
||||||
await window.Signal.Migrations.processNewAttachment({
|
await window.Signal.Migrations.processNewAttachment({
|
||||||
...omit(attachment, ['error', 'pending']),
|
...omit(attachment, ['error', 'pending', 'downloadPath']),
|
||||||
...downloaded,
|
...downloaded,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -107,6 +107,7 @@ export async function onDelete(item: DeleteForMeAttributesType): Promise<void> {
|
||||||
item.deleteAttachmentData,
|
item.deleteAttachmentData,
|
||||||
{
|
{
|
||||||
deleteOnDisk: window.Signal.Migrations.deleteAttachmentData,
|
deleteOnDisk: window.Signal.Migrations.deleteAttachmentData,
|
||||||
|
deleteDownloadOnDisk: window.Signal.Migrations.deleteDownloadData,
|
||||||
logId,
|
logId,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
15
ts/signal.ts
15
ts/signal.ts
|
@ -71,6 +71,7 @@ type MigrationsModuleType = {
|
||||||
) => Promise<{ path: string; size: number }>;
|
) => Promise<{ path: string; size: number }>;
|
||||||
deleteAttachmentData: (path: string) => Promise<void>;
|
deleteAttachmentData: (path: string) => Promise<void>;
|
||||||
deleteAvatar: (path: string) => Promise<void>;
|
deleteAvatar: (path: string) => Promise<void>;
|
||||||
|
deleteDownloadData: (path: string) => Promise<void>;
|
||||||
deleteDraftFile: (path: string) => Promise<void>;
|
deleteDraftFile: (path: string) => Promise<void>;
|
||||||
deleteExternalMessageFiles: (
|
deleteExternalMessageFiles: (
|
||||||
attributes: MessageAttributesType
|
attributes: MessageAttributesType
|
||||||
|
@ -81,6 +82,7 @@ type MigrationsModuleType = {
|
||||||
getAbsoluteAttachmentPath: (path: string) => string;
|
getAbsoluteAttachmentPath: (path: string) => string;
|
||||||
getAbsoluteAvatarPath: (src: string) => string;
|
getAbsoluteAvatarPath: (src: string) => string;
|
||||||
getAbsoluteBadgeImageFilePath: (path: string) => string;
|
getAbsoluteBadgeImageFilePath: (path: string) => string;
|
||||||
|
getAbsoluteDownloadsPath: (path: string) => string;
|
||||||
getAbsoluteDraftPath: (path: string) => string;
|
getAbsoluteDraftPath: (path: string) => string;
|
||||||
getAbsoluteStickerPath: (path: string) => string;
|
getAbsoluteStickerPath: (path: string) => string;
|
||||||
getAbsoluteTempPath: (path: string) => string;
|
getAbsoluteTempPath: (path: string) => string;
|
||||||
|
@ -161,6 +163,7 @@ export function initializeMigrations({
|
||||||
createDoesExist,
|
createDoesExist,
|
||||||
getAvatarsPath,
|
getAvatarsPath,
|
||||||
getDraftPath,
|
getDraftPath,
|
||||||
|
getDownloadsPath,
|
||||||
getPath,
|
getPath,
|
||||||
getStickersPath,
|
getStickersPath,
|
||||||
getBadgesPath,
|
getBadgesPath,
|
||||||
|
@ -260,6 +263,10 @@ export function initializeMigrations({
|
||||||
const deleteDraftFile = Attachments.createDeleter(draftPath);
|
const deleteDraftFile = Attachments.createDeleter(draftPath);
|
||||||
const readDraftData = createEncryptedReader(draftPath);
|
const readDraftData = createEncryptedReader(draftPath);
|
||||||
|
|
||||||
|
const downloadsPath = getDownloadsPath(userDataPath);
|
||||||
|
const getAbsoluteDownloadsPath = createAbsolutePathGetter(downloadsPath);
|
||||||
|
const deleteDownloadOnDisk = Attachments.createDeleter(downloadsPath);
|
||||||
|
|
||||||
const avatarsPath = getAvatarsPath(userDataPath);
|
const avatarsPath = getAvatarsPath(userDataPath);
|
||||||
const readAvatarData = createEncryptedReader(avatarsPath);
|
const readAvatarData = createEncryptedReader(avatarsPath);
|
||||||
const getAbsoluteAvatarPath = createAbsolutePathGetter(avatarsPath);
|
const getAbsoluteAvatarPath = createAbsolutePathGetter(avatarsPath);
|
||||||
|
@ -272,9 +279,13 @@ export function initializeMigrations({
|
||||||
copyIntoTempDirectory,
|
copyIntoTempDirectory,
|
||||||
deleteAttachmentData: deleteOnDisk,
|
deleteAttachmentData: deleteOnDisk,
|
||||||
deleteAvatar,
|
deleteAvatar,
|
||||||
|
deleteDownloadData: deleteDownloadOnDisk,
|
||||||
deleteDraftFile,
|
deleteDraftFile,
|
||||||
deleteExternalMessageFiles: MessageType.deleteAllExternalFiles({
|
deleteExternalMessageFiles: MessageType.deleteAllExternalFiles({
|
||||||
deleteAttachmentData: Type.deleteData(deleteOnDisk),
|
deleteAttachmentData: Type.deleteData({
|
||||||
|
deleteOnDisk,
|
||||||
|
deleteDownloadOnDisk,
|
||||||
|
}),
|
||||||
deleteOnDisk,
|
deleteOnDisk,
|
||||||
}),
|
}),
|
||||||
deleteSticker,
|
deleteSticker,
|
||||||
|
@ -283,6 +294,7 @@ export function initializeMigrations({
|
||||||
getAbsoluteAttachmentPath,
|
getAbsoluteAttachmentPath,
|
||||||
getAbsoluteAvatarPath,
|
getAbsoluteAvatarPath,
|
||||||
getAbsoluteBadgeImageFilePath,
|
getAbsoluteBadgeImageFilePath,
|
||||||
|
getAbsoluteDownloadsPath,
|
||||||
getAbsoluteDraftPath,
|
getAbsoluteDraftPath,
|
||||||
getAbsoluteStickerPath,
|
getAbsoluteStickerPath,
|
||||||
getAbsoluteTempPath,
|
getAbsoluteTempPath,
|
||||||
|
@ -357,6 +369,7 @@ type StringGetterType = (basePath: string) => string;
|
||||||
type AttachmentsModuleType = {
|
type AttachmentsModuleType = {
|
||||||
getAvatarsPath: StringGetterType;
|
getAvatarsPath: StringGetterType;
|
||||||
getBadgesPath: StringGetterType;
|
getBadgesPath: StringGetterType;
|
||||||
|
getDownloadsPath: StringGetterType;
|
||||||
getDraftPath: StringGetterType;
|
getDraftPath: StringGetterType;
|
||||||
getPath: StringGetterType;
|
getPath: StringGetterType;
|
||||||
getStickersPath: StringGetterType;
|
getStickersPath: StringGetterType;
|
||||||
|
|
|
@ -70,6 +70,7 @@ const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
|
||||||
const ERASE_STICKERS_KEY = 'erase-stickers';
|
const ERASE_STICKERS_KEY = 'erase-stickers';
|
||||||
const ERASE_TEMP_KEY = 'erase-temp';
|
const ERASE_TEMP_KEY = 'erase-temp';
|
||||||
const ERASE_DRAFTS_KEY = 'erase-drafts';
|
const ERASE_DRAFTS_KEY = 'erase-drafts';
|
||||||
|
const ERASE_DOWNLOADS_KEY = 'erase-downloads';
|
||||||
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
||||||
const ENSURE_FILE_PERMISSIONS = 'ensure-file-permissions';
|
const ENSURE_FILE_PERMISSIONS = 'ensure-file-permissions';
|
||||||
const PAUSE_WRITE_ACCESS = 'pause-sql-writes';
|
const PAUSE_WRITE_ACCESS = 'pause-sql-writes';
|
||||||
|
@ -803,6 +804,7 @@ async function removeOtherData(): Promise<void> {
|
||||||
invokeWithTimeout(ERASE_ATTACHMENTS_KEY),
|
invokeWithTimeout(ERASE_ATTACHMENTS_KEY),
|
||||||
invokeWithTimeout(ERASE_STICKERS_KEY),
|
invokeWithTimeout(ERASE_STICKERS_KEY),
|
||||||
invokeWithTimeout(ERASE_TEMP_KEY),
|
invokeWithTimeout(ERASE_TEMP_KEY),
|
||||||
|
invokeWithTimeout(ERASE_DOWNLOADS_KEY),
|
||||||
invokeWithTimeout(ERASE_DRAFTS_KEY),
|
invokeWithTimeout(ERASE_DRAFTS_KEY),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -406,6 +406,7 @@ export type MessageAttachmentsCursorType = MessageCursorType &
|
||||||
export type GetKnownMessageAttachmentsResultType = Readonly<{
|
export type GetKnownMessageAttachmentsResultType = Readonly<{
|
||||||
cursor: MessageAttachmentsCursorType;
|
cursor: MessageAttachmentsCursorType;
|
||||||
attachments: ReadonlyArray<string>;
|
attachments: ReadonlyArray<string>;
|
||||||
|
downloads: ReadonlyArray<string>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type PageMessagesCursorType = MessageCursorType &
|
export type PageMessagesCursorType = MessageCursorType &
|
||||||
|
|
|
@ -6442,9 +6442,13 @@ function getMessageServerGuidsForSpam(
|
||||||
.all({ conversationId });
|
.all({ conversationId });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExternalFilesForMessage(message: MessageType): Array<string> {
|
function getExternalFilesForMessage(message: MessageType): {
|
||||||
|
externalAttachments: Array<string>;
|
||||||
|
externalDownloads: Array<string>;
|
||||||
|
} {
|
||||||
const { attachments, contact, quote, preview, sticker } = message;
|
const { attachments, contact, quote, preview, sticker } = message;
|
||||||
const files: Array<string> = [];
|
const externalAttachments: Array<string> = [];
|
||||||
|
const externalDownloads: Array<string> = [];
|
||||||
|
|
||||||
forEach(attachments, attachment => {
|
forEach(attachments, attachment => {
|
||||||
const {
|
const {
|
||||||
|
@ -6452,21 +6456,28 @@ function getExternalFilesForMessage(message: MessageType): Array<string> {
|
||||||
thumbnail,
|
thumbnail,
|
||||||
screenshot,
|
screenshot,
|
||||||
thumbnailFromBackup,
|
thumbnailFromBackup,
|
||||||
|
downloadPath,
|
||||||
} = attachment;
|
} = attachment;
|
||||||
if (file) {
|
if (file) {
|
||||||
files.push(file);
|
externalAttachments.push(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// downloadPath is relative to downloads folder and has to be tracked
|
||||||
|
// separately.
|
||||||
|
if (downloadPath) {
|
||||||
|
externalDownloads.push(downloadPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (thumbnail && thumbnail.path) {
|
if (thumbnail && thumbnail.path) {
|
||||||
files.push(thumbnail.path);
|
externalAttachments.push(thumbnail.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (screenshot && screenshot.path) {
|
if (screenshot && screenshot.path) {
|
||||||
files.push(screenshot.path);
|
externalAttachments.push(screenshot.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (thumbnailFromBackup && thumbnailFromBackup.path) {
|
if (thumbnailFromBackup && thumbnailFromBackup.path) {
|
||||||
files.push(thumbnailFromBackup.path);
|
externalAttachments.push(thumbnailFromBackup.path);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -6475,7 +6486,7 @@ function getExternalFilesForMessage(message: MessageType): Array<string> {
|
||||||
const { thumbnail } = attachment;
|
const { thumbnail } = attachment;
|
||||||
|
|
||||||
if (thumbnail && thumbnail.path) {
|
if (thumbnail && thumbnail.path) {
|
||||||
files.push(thumbnail.path);
|
externalAttachments.push(thumbnail.path);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -6485,7 +6496,7 @@ function getExternalFilesForMessage(message: MessageType): Array<string> {
|
||||||
const { avatar } = item;
|
const { avatar } = item;
|
||||||
|
|
||||||
if (avatar && avatar.avatar && avatar.avatar.path) {
|
if (avatar && avatar.avatar && avatar.avatar.path) {
|
||||||
files.push(avatar.avatar.path);
|
externalAttachments.push(avatar.avatar.path);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -6495,20 +6506,20 @@ function getExternalFilesForMessage(message: MessageType): Array<string> {
|
||||||
const { image } = item;
|
const { image } = item;
|
||||||
|
|
||||||
if (image && image.path) {
|
if (image && image.path) {
|
||||||
files.push(image.path);
|
externalAttachments.push(image.path);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sticker && sticker.data && sticker.data.path) {
|
if (sticker && sticker.data && sticker.data.path) {
|
||||||
files.push(sticker.data.path);
|
externalAttachments.push(sticker.data.path);
|
||||||
|
|
||||||
if (sticker.data.thumbnail && sticker.data.thumbnail.path) {
|
if (sticker.data.thumbnail && sticker.data.thumbnail.path) {
|
||||||
files.push(sticker.data.thumbnail.path);
|
externalAttachments.push(sticker.data.thumbnail.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return files;
|
return { externalAttachments, externalDownloads };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExternalFilesForConversation(
|
function getExternalFilesForConversation(
|
||||||
|
@ -6559,17 +6570,20 @@ function getKnownMessageAttachments(
|
||||||
const innerCursor = cursor as MessageCursorType | undefined as
|
const innerCursor = cursor as MessageCursorType | undefined as
|
||||||
| PageMessagesCursorType
|
| PageMessagesCursorType
|
||||||
| undefined;
|
| undefined;
|
||||||
const result = new Set<string>();
|
const attachments = new Set<string>();
|
||||||
|
const downloads = new Set<string>();
|
||||||
|
|
||||||
const { messages, cursor: newCursor } = pageMessages(db, innerCursor);
|
const { messages, cursor: newCursor } = pageMessages(db, innerCursor);
|
||||||
|
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
const externalFiles = getExternalFilesForMessage(message);
|
const { externalAttachments, externalDownloads } =
|
||||||
forEach(externalFiles, file => result.add(file));
|
getExternalFilesForMessage(message);
|
||||||
|
externalAttachments.forEach(file => attachments.add(file));
|
||||||
|
externalDownloads.forEach(file => downloads.add(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attachments: Array.from(result),
|
attachments: Array.from(attachments),
|
||||||
|
downloads: Array.from(downloads),
|
||||||
cursor: newCursor as MessageCursorType as MessageAttachmentsCursorType,
|
cursor: newCursor as MessageCursorType as MessageAttachmentsCursorType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,10 @@ describe('processDataMessage', () => {
|
||||||
timestamp: Long.fromNumber(TIMESTAMP),
|
timestamp: Long.fromNumber(TIMESTAMP),
|
||||||
...message,
|
...message,
|
||||||
},
|
},
|
||||||
TIMESTAMP
|
TIMESTAMP,
|
||||||
|
{
|
||||||
|
_createName: () => 'random-path',
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
it('should process attachments', () => {
|
it('should process attachments', () => {
|
||||||
|
@ -54,7 +57,12 @@ describe('processDataMessage', () => {
|
||||||
attachments: [UNPROCESSED_ATTACHMENT],
|
attachments: [UNPROCESSED_ATTACHMENT],
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepStrictEqual(out.attachments, [PROCESSED_ATTACHMENT]);
|
assert.deepStrictEqual(out.attachments, [
|
||||||
|
{
|
||||||
|
...PROCESSED_ATTACHMENT,
|
||||||
|
downloadPath: 'random-path',
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should process attachments with 0 cdnId', () => {
|
it('should process attachments with 0 cdnId', () => {
|
||||||
|
@ -71,6 +79,7 @@ describe('processDataMessage', () => {
|
||||||
{
|
{
|
||||||
...PROCESSED_ATTACHMENT,
|
...PROCESSED_ATTACHMENT,
|
||||||
cdnId: undefined,
|
cdnId: undefined,
|
||||||
|
downloadPath: 'random-path',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
1
ts/textsecure/Types.d.ts
vendored
1
ts/textsecure/Types.d.ts
vendored
|
@ -119,6 +119,7 @@ export type ProcessedAttachment = {
|
||||||
cdnNumber?: number;
|
cdnNumber?: number;
|
||||||
textAttachment?: Omit<TextAttachmentType, 'preview'>;
|
textAttachment?: Omit<TextAttachmentType, 'preview'>;
|
||||||
backupLocator?: AttachmentType['backupLocator'];
|
backupLocator?: AttachmentType['backupLocator'];
|
||||||
|
downloadPath?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProcessedGroupV2Context = {
|
export type ProcessedGroupV2Context = {
|
||||||
|
|
|
@ -184,7 +184,8 @@ type PromiseAjaxOptionsType = {
|
||||||
| 'jsonwithdetails'
|
| 'jsonwithdetails'
|
||||||
| 'bytes'
|
| 'bytes'
|
||||||
| 'byteswithdetails'
|
| 'byteswithdetails'
|
||||||
| 'stream';
|
| 'stream'
|
||||||
|
| 'streamwithdetails';
|
||||||
serverUrl?: string;
|
serverUrl?: string;
|
||||||
stack?: string;
|
stack?: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
@ -214,6 +215,11 @@ type BytesWithDetailsType = {
|
||||||
contentType: string | null;
|
contentType: string | null;
|
||||||
response: Response;
|
response: Response;
|
||||||
};
|
};
|
||||||
|
type StreamWithDetailsType = {
|
||||||
|
stream: Readable;
|
||||||
|
contentType: string | null;
|
||||||
|
response: Response;
|
||||||
|
};
|
||||||
|
|
||||||
export const multiRecipient200ResponseSchema = z.object({
|
export const multiRecipient200ResponseSchema = z.object({
|
||||||
uuids404: z.array(serviceIdSchema).optional(),
|
uuids404: z.array(serviceIdSchema).optional(),
|
||||||
|
@ -386,7 +392,10 @@ async function _promiseAjax(
|
||||||
options.responseType === 'byteswithdetails'
|
options.responseType === 'byteswithdetails'
|
||||||
) {
|
) {
|
||||||
result = await response.buffer();
|
result = await response.buffer();
|
||||||
} else if (options.responseType === 'stream') {
|
} else if (
|
||||||
|
options.responseType === 'stream' ||
|
||||||
|
options.responseType === 'streamwithdetails'
|
||||||
|
) {
|
||||||
result = response.body;
|
result = response.body;
|
||||||
} else {
|
} else {
|
||||||
result = await response.textConverted();
|
result = await response.textConverted();
|
||||||
|
@ -437,6 +446,24 @@ async function _promiseAjax(
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.responseType === 'streamwithdetails') {
|
||||||
|
log.info(logId, response.status, 'Streaming with details');
|
||||||
|
response.body.on('error', e => {
|
||||||
|
log.info(logId, 'Errored while streaming:', e.message);
|
||||||
|
});
|
||||||
|
response.body.on('end', () => {
|
||||||
|
log.info(logId, response.status, 'Streaming ended');
|
||||||
|
});
|
||||||
|
|
||||||
|
const fullResult: StreamWithDetailsType = {
|
||||||
|
stream: result as Readable,
|
||||||
|
contentType: getContentType(response),
|
||||||
|
response,
|
||||||
|
};
|
||||||
|
|
||||||
|
return fullResult;
|
||||||
|
}
|
||||||
|
|
||||||
log.info(logId, response.status, 'Success');
|
log.info(logId, response.status, 'Success');
|
||||||
|
|
||||||
if (options.responseType === 'byteswithdetails') {
|
if (options.responseType === 'byteswithdetails') {
|
||||||
|
@ -506,6 +533,10 @@ function _outerAjax(
|
||||||
providedUrl: string | null,
|
providedUrl: string | null,
|
||||||
options: PromiseAjaxOptionsType & { responseType?: 'stream' }
|
options: PromiseAjaxOptionsType & { responseType?: 'stream' }
|
||||||
): Promise<Readable>;
|
): Promise<Readable>;
|
||||||
|
function _outerAjax(
|
||||||
|
providedUrl: string | null,
|
||||||
|
options: PromiseAjaxOptionsType & { responseType: 'streamwithdetails' }
|
||||||
|
): Promise<StreamWithDetailsType>;
|
||||||
function _outerAjax(
|
function _outerAjax(
|
||||||
providedUrl: string | null,
|
providedUrl: string | null,
|
||||||
options: PromiseAjaxOptionsType
|
options: PromiseAjaxOptionsType
|
||||||
|
@ -1215,6 +1246,7 @@ export type WebAPIType = {
|
||||||
options?: {
|
options?: {
|
||||||
disableRetries?: boolean;
|
disableRetries?: boolean;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
downloadOffset?: number;
|
||||||
};
|
};
|
||||||
}) => Promise<Readable>;
|
}) => Promise<Readable>;
|
||||||
getAttachment: (args: {
|
getAttachment: (args: {
|
||||||
|
@ -1223,6 +1255,7 @@ export type WebAPIType = {
|
||||||
options?: {
|
options?: {
|
||||||
disableRetries?: boolean;
|
disableRetries?: boolean;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
downloadOffset?: number;
|
||||||
};
|
};
|
||||||
}) => Promise<Readable>;
|
}) => Promise<Readable>;
|
||||||
getAttachmentUploadForm: () => Promise<AttachmentUploadFormResponseType>;
|
getAttachmentUploadForm: () => Promise<AttachmentUploadFormResponseType>;
|
||||||
|
@ -1789,6 +1822,9 @@ export function initialize({
|
||||||
function _ajax(
|
function _ajax(
|
||||||
param: AjaxOptionsType & { responseType: 'stream' }
|
param: AjaxOptionsType & { responseType: 'stream' }
|
||||||
): Promise<Readable>;
|
): Promise<Readable>;
|
||||||
|
function _ajax(
|
||||||
|
param: AjaxOptionsType & { responseType: 'streamwithdetails' }
|
||||||
|
): Promise<StreamWithDetailsType>;
|
||||||
function _ajax(
|
function _ajax(
|
||||||
param: AjaxOptionsType & { responseType: 'json' }
|
param: AjaxOptionsType & { responseType: 'json' }
|
||||||
): Promise<unknown>;
|
): Promise<unknown>;
|
||||||
|
@ -3442,6 +3478,7 @@ export function initialize({
|
||||||
options?: {
|
options?: {
|
||||||
disableRetries?: boolean;
|
disableRetries?: boolean;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
downloadOffset?: number;
|
||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
return _getAttachment({
|
return _getAttachment({
|
||||||
|
@ -3468,6 +3505,7 @@ export function initialize({
|
||||||
options?: {
|
options?: {
|
||||||
disableRetries?: boolean;
|
disableRetries?: boolean;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
downloadOffset?: number;
|
||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
return _getAttachment({
|
return _getAttachment({
|
||||||
|
@ -3482,7 +3520,7 @@ export function initialize({
|
||||||
async function _getAttachment({
|
async function _getAttachment({
|
||||||
cdnPath,
|
cdnPath,
|
||||||
cdnNumber,
|
cdnNumber,
|
||||||
headers,
|
headers = {},
|
||||||
redactor,
|
redactor,
|
||||||
options,
|
options,
|
||||||
}: {
|
}: {
|
||||||
|
@ -3493,12 +3531,13 @@ export function initialize({
|
||||||
options?: {
|
options?: {
|
||||||
disableRetries?: boolean;
|
disableRetries?: boolean;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
downloadOffset?: number;
|
||||||
};
|
};
|
||||||
}): Promise<Readable> {
|
}): Promise<Readable> {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const cdnUrl = cdnUrlObject[cdnNumber] ?? cdnUrlObject['0'];
|
const cdnUrl = cdnUrlObject[cdnNumber] ?? cdnUrlObject['0'];
|
||||||
|
|
||||||
let downloadStream: Readable | undefined;
|
let streamWithDetails: StreamWithDetailsType | undefined;
|
||||||
|
|
||||||
const cancelRequest = () => {
|
const cancelRequest = () => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
|
@ -3508,23 +3547,38 @@ export function initialize({
|
||||||
|
|
||||||
// This is going to the CDN, not the service, so we use _outerAjax
|
// This is going to the CDN, not the service, so we use _outerAjax
|
||||||
try {
|
try {
|
||||||
downloadStream = await _outerAjax(`${cdnUrl}${cdnPath}`, {
|
const targetHeaders = { ...headers };
|
||||||
headers,
|
if (options?.downloadOffset) {
|
||||||
|
targetHeaders.range = `bytes=${options.downloadOffset}-`;
|
||||||
|
}
|
||||||
|
streamWithDetails = await _outerAjax(`${cdnUrl}${cdnPath}`, {
|
||||||
|
headers: targetHeaders,
|
||||||
certificateAuthority,
|
certificateAuthority,
|
||||||
disableRetries: options?.disableRetries,
|
disableRetries: options?.disableRetries,
|
||||||
proxyUrl,
|
proxyUrl,
|
||||||
responseType: 'stream',
|
responseType: 'streamwithdetails',
|
||||||
timeout: options?.timeout ?? DEFAULT_TIMEOUT,
|
timeout: options?.timeout ?? DEFAULT_TIMEOUT,
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
redactUrl: redactor,
|
redactUrl: redactor,
|
||||||
version,
|
version,
|
||||||
abortSignal: abortController.signal,
|
abortSignal: abortController.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (targetHeaders.range != null) {
|
||||||
|
strictAssert(
|
||||||
|
streamWithDetails.response.status === 206,
|
||||||
|
`Expected 206 status code for offset ${options?.downloadOffset}`
|
||||||
|
);
|
||||||
|
strictAssert(
|
||||||
|
!streamWithDetails.contentType?.includes('multipart'),
|
||||||
|
`Expected non-multipart response for ${cdnUrl}${cdnPath}`
|
||||||
|
);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (!downloadStream) {
|
if (!streamWithDetails) {
|
||||||
unregisterInFlightRequest(cancelRequest);
|
unregisterInFlightRequest(cancelRequest);
|
||||||
} else {
|
} else {
|
||||||
downloadStream.on('close', () => {
|
streamWithDetails.stream.on('close', () => {
|
||||||
unregisterInFlightRequest(cancelRequest);
|
unregisterInFlightRequest(cancelRequest);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -3536,7 +3590,7 @@ export function initialize({
|
||||||
abortController,
|
abortController,
|
||||||
});
|
});
|
||||||
|
|
||||||
const combinedStream = downloadStream
|
const combinedStream = streamWithDetails.stream
|
||||||
// We do this manually; pipe() doesn't flow errors through the streams for us
|
// We do this manually; pipe() doesn't flow errors through the streams for us
|
||||||
.on('error', (error: Error) => {
|
.on('error', (error: Error) => {
|
||||||
timeoutStream.emit('error', error);
|
timeoutStream.emit('error', error);
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { createWriteStream } from 'fs';
|
import { createWriteStream } from 'node:fs';
|
||||||
|
import { stat } from 'node:fs/promises';
|
||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
import type { Readable } from 'stream';
|
import type { Readable, Writable } from 'stream';
|
||||||
import { Transform } from 'stream';
|
import { Transform } from 'stream';
|
||||||
import { pipeline } from 'stream/promises';
|
import { pipeline } from 'stream/promises';
|
||||||
import { ensureFile } from 'fs-extra';
|
import { ensureFile } from 'fs-extra';
|
||||||
|
@ -24,7 +25,7 @@ import {
|
||||||
} from '../Crypto';
|
} from '../Crypto';
|
||||||
import {
|
import {
|
||||||
getAttachmentCiphertextLength,
|
getAttachmentCiphertextLength,
|
||||||
safeUnlinkSync,
|
safeUnlink,
|
||||||
splitKeys,
|
splitKeys,
|
||||||
type ReencryptedAttachmentV2,
|
type ReencryptedAttachmentV2,
|
||||||
decryptAndReencryptLocally,
|
decryptAndReencryptLocally,
|
||||||
|
@ -124,6 +125,39 @@ export async function downloadAttachment(
|
||||||
|
|
||||||
let downloadResult: Awaited<ReturnType<typeof downloadToDisk>>;
|
let downloadResult: Awaited<ReturnType<typeof downloadToDisk>>;
|
||||||
|
|
||||||
|
let { downloadPath } = attachment;
|
||||||
|
let downloadOffset = 0;
|
||||||
|
if (downloadPath) {
|
||||||
|
const absoluteDownloadPath =
|
||||||
|
window.Signal.Migrations.getAbsoluteAttachmentPath(downloadPath);
|
||||||
|
try {
|
||||||
|
({ size: downloadOffset } = await stat(absoluteDownloadPath));
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== 'ENOENT') {
|
||||||
|
log.error(
|
||||||
|
'downloadAttachment: Failed to get file size for previous download',
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await safeUnlink(downloadPath);
|
||||||
|
} catch {
|
||||||
|
downloadPath = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start over if we go over the size
|
||||||
|
if (downloadOffset >= size && downloadPath) {
|
||||||
|
log.warn('downloadAttachment: went over, retrying');
|
||||||
|
await safeUnlink(downloadPath);
|
||||||
|
downloadOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadOffset !== 0) {
|
||||||
|
log.info(`${logId}: resuming from ${downloadOffset}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (mediaTier === MediaTier.STANDARD) {
|
if (mediaTier === MediaTier.STANDARD) {
|
||||||
strictAssert(
|
strictAssert(
|
||||||
options.variant !== AttachmentVariant.ThumbnailFromBackup,
|
options.variant !== AttachmentVariant.ThumbnailFromBackup,
|
||||||
|
@ -135,9 +169,17 @@ export async function downloadAttachment(
|
||||||
const downloadStream = await server.getAttachment({
|
const downloadStream = await server.getAttachment({
|
||||||
cdnKey,
|
cdnKey,
|
||||||
cdnNumber,
|
cdnNumber,
|
||||||
options,
|
options: {
|
||||||
|
...options,
|
||||||
|
downloadOffset,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
downloadResult = await downloadToDisk({
|
||||||
|
downloadStream,
|
||||||
|
size,
|
||||||
|
downloadPath,
|
||||||
|
downloadOffset,
|
||||||
});
|
});
|
||||||
downloadResult = await downloadToDisk({ downloadStream, size });
|
|
||||||
} else {
|
} else {
|
||||||
const mediaId =
|
const mediaId =
|
||||||
options.variant === AttachmentVariant.ThumbnailFromBackup
|
options.variant === AttachmentVariant.ThumbnailFromBackup
|
||||||
|
@ -157,10 +199,15 @@ export async function downloadAttachment(
|
||||||
mediaDir,
|
mediaDir,
|
||||||
headers: cdnCredentials.headers,
|
headers: cdnCredentials.headers,
|
||||||
cdnNumber,
|
cdnNumber,
|
||||||
options,
|
options: {
|
||||||
|
...options,
|
||||||
|
downloadOffset,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
downloadResult = await downloadToDisk({
|
downloadResult = await downloadToDisk({
|
||||||
downloadStream,
|
downloadStream,
|
||||||
|
downloadPath,
|
||||||
|
downloadOffset,
|
||||||
size: getAttachmentCiphertextLength(
|
size: getAttachmentCiphertextLength(
|
||||||
options.variant === AttachmentVariant.ThumbnailFromBackup
|
options.variant === AttachmentVariant.ThumbnailFromBackup
|
||||||
? // be generous, accept downloads of up to twice what we expect for thumbnail
|
? // be generous, accept downloads of up to twice what we expect for thumbnail
|
||||||
|
@ -170,10 +217,7 @@ export async function downloadAttachment(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { relativePath: downloadedRelativePath, downloadSize } = downloadResult;
|
const { absolutePath: cipherTextAbsolutePath, downloadSize } = downloadResult;
|
||||||
|
|
||||||
const cipherTextAbsolutePath =
|
|
||||||
window.Signal.Migrations.getAbsoluteAttachmentPath(downloadedRelativePath);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (options.variant) {
|
switch (options.variant) {
|
||||||
|
@ -226,23 +270,42 @@ export async function downloadAttachment(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
safeUnlinkSync(cipherTextAbsolutePath);
|
await safeUnlink(cipherTextAbsolutePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadToDisk({
|
async function downloadToDisk({
|
||||||
downloadStream,
|
downloadStream,
|
||||||
|
downloadPath,
|
||||||
|
downloadOffset = 0,
|
||||||
size,
|
size,
|
||||||
}: {
|
}: {
|
||||||
downloadStream: Readable;
|
downloadStream: Readable;
|
||||||
|
downloadPath?: string;
|
||||||
|
downloadOffset?: number;
|
||||||
size: number;
|
size: number;
|
||||||
}): Promise<{ relativePath: string; downloadSize: number }> {
|
}): Promise<{ absolutePath: string; downloadSize: number }> {
|
||||||
const relativeTargetPath = getRelativePath(createName());
|
const absoluteTargetPath = downloadPath
|
||||||
const absoluteTargetPath =
|
? window.Signal.Migrations.getAbsoluteDownloadsPath(downloadPath)
|
||||||
window.Signal.Migrations.getAbsoluteAttachmentPath(relativeTargetPath);
|
: window.Signal.Migrations.getAbsoluteAttachmentPath(
|
||||||
|
getRelativePath(createName())
|
||||||
|
);
|
||||||
await ensureFile(absoluteTargetPath);
|
await ensureFile(absoluteTargetPath);
|
||||||
const writeStream = createWriteStream(absoluteTargetPath);
|
let writeStream: Writable;
|
||||||
const targetSize = getAttachmentCiphertextLength(size);
|
if (downloadPath) {
|
||||||
|
writeStream = createWriteStream(absoluteTargetPath, {
|
||||||
|
flags: 'a',
|
||||||
|
start: downloadOffset,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
strictAssert(
|
||||||
|
!downloadOffset,
|
||||||
|
'Download cannot be resumed without downloadPath'
|
||||||
|
);
|
||||||
|
writeStream = createWriteStream(absoluteTargetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetSize = getAttachmentCiphertextLength(size) - downloadOffset;
|
||||||
let downloadSize = 0;
|
let downloadSize = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -255,19 +318,23 @@ async function downloadToDisk({
|
||||||
writeStream
|
writeStream
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (downloadPath) {
|
||||||
|
log.warn(`downloadToDisk: stopping at ${downloadSize}`);
|
||||||
|
} else {
|
||||||
try {
|
try {
|
||||||
safeUnlinkSync(absoluteTargetPath);
|
await safeUnlink(absoluteTargetPath);
|
||||||
} catch (cleanupError) {
|
} catch (cleanupError) {
|
||||||
log.error(
|
log.error(
|
||||||
'downloadToDisk: Error while cleaning up',
|
'downloadToDisk: Error while cleaning up',
|
||||||
Errors.toLogFormat(cleanupError)
|
Errors.toLogFormat(cleanupError)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { relativePath: relativeTargetPath, downloadSize };
|
return { absolutePath: absoluteTargetPath, downloadSize };
|
||||||
}
|
}
|
||||||
|
|
||||||
// A simple transform that throws if it sees more than maxBytes on the stream.
|
// A simple transform that throws if it sees more than maxBytes on the stream.
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { filterAndClean } from '../types/BodyRange';
|
||||||
import { isAciString } from '../util/isAciString';
|
import { isAciString } from '../util/isAciString';
|
||||||
import { normalizeAci } from '../util/normalizeAci';
|
import { normalizeAci } from '../util/normalizeAci';
|
||||||
import { bytesToUuid } from '../util/uuidToBytes';
|
import { bytesToUuid } from '../util/uuidToBytes';
|
||||||
|
import { createName } from '../util/attachmentPath';
|
||||||
|
|
||||||
const FLAGS = Proto.DataMessage.Flags;
|
const FLAGS = Proto.DataMessage.Flags;
|
||||||
export const ATTACHMENT_MAX = 32;
|
export const ATTACHMENT_MAX = 32;
|
||||||
|
@ -284,7 +285,10 @@ export function processGiftBadge(
|
||||||
|
|
||||||
export function processDataMessage(
|
export function processDataMessage(
|
||||||
message: Proto.IDataMessage,
|
message: Proto.IDataMessage,
|
||||||
envelopeTimestamp: number
|
envelopeTimestamp: number,
|
||||||
|
|
||||||
|
// Only for testing
|
||||||
|
{ _createName: doCreateName = createName } = {}
|
||||||
): ProcessedDataMessage {
|
): ProcessedDataMessage {
|
||||||
/* eslint-disable no-bitwise */
|
/* eslint-disable no-bitwise */
|
||||||
|
|
||||||
|
@ -309,7 +313,10 @@ export function processDataMessage(
|
||||||
const result: ProcessedDataMessage = {
|
const result: ProcessedDataMessage = {
|
||||||
body: dropNull(message.body),
|
body: dropNull(message.body),
|
||||||
attachments: (message.attachments ?? []).map(
|
attachments: (message.attachments ?? []).map(
|
||||||
(attachment: Proto.IAttachmentPointer) => processAttachment(attachment)
|
(attachment: Proto.IAttachmentPointer) => ({
|
||||||
|
...processAttachment(attachment),
|
||||||
|
downloadPath: doCreateName(),
|
||||||
|
})
|
||||||
),
|
),
|
||||||
groupV2: processGroupV2Context(message.groupV2),
|
groupV2: processGroupV2Context(message.groupV2),
|
||||||
flags: message.flags ?? 0,
|
flags: message.flags ?? 0,
|
||||||
|
|
|
@ -78,6 +78,7 @@ export type AttachmentType = {
|
||||||
cdnNumber?: number;
|
cdnNumber?: number;
|
||||||
cdnId?: string;
|
cdnId?: string;
|
||||||
cdnKey?: string;
|
cdnKey?: string;
|
||||||
|
downloadPath?: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
iv?: string;
|
iv?: string;
|
||||||
data?: Uint8Array;
|
data?: Uint8Array;
|
||||||
|
@ -386,9 +387,13 @@ export function loadData(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteData(
|
export function deleteData({
|
||||||
deleteOnDisk: (path: string) => Promise<void>
|
deleteOnDisk,
|
||||||
): (attachment?: AttachmentType) => Promise<void> {
|
deleteDownloadOnDisk,
|
||||||
|
}: {
|
||||||
|
deleteOnDisk: (path: string) => Promise<void>;
|
||||||
|
deleteDownloadOnDisk: (path: string) => Promise<void>;
|
||||||
|
}): (attachment?: AttachmentType) => Promise<void> {
|
||||||
if (!isFunction(deleteOnDisk)) {
|
if (!isFunction(deleteOnDisk)) {
|
||||||
throw new TypeError('deleteData: deleteOnDisk must be a function');
|
throw new TypeError('deleteData: deleteOnDisk must be a function');
|
||||||
}
|
}
|
||||||
|
@ -398,12 +403,17 @@ export function deleteData(
|
||||||
throw new TypeError('deleteData: attachment is not valid');
|
throw new TypeError('deleteData: attachment is not valid');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { path, thumbnail, screenshot, thumbnailFromBackup } = attachment;
|
const { path, downloadPath, thumbnail, screenshot, thumbnailFromBackup } =
|
||||||
|
attachment;
|
||||||
|
|
||||||
if (isString(path)) {
|
if (isString(path)) {
|
||||||
await deleteOnDisk(path);
|
await deleteOnDisk(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isString(downloadPath)) {
|
||||||
|
await deleteDownloadOnDisk(downloadPath);
|
||||||
|
}
|
||||||
|
|
||||||
if (thumbnail && isString(thumbnail.path)) {
|
if (thumbnail && isString(thumbnail.path)) {
|
||||||
await deleteOnDisk(thumbnail.path);
|
await deleteOnDisk(thumbnail.path);
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,6 +104,7 @@ export type IPCEventsCallbacksType = {
|
||||||
}>;
|
}>;
|
||||||
addCustomColor: (customColor: CustomColorType) => void;
|
addCustomColor: (customColor: CustomColorType) => void;
|
||||||
addDarkOverlay: () => void;
|
addDarkOverlay: () => void;
|
||||||
|
cleanupDownloads: () => Promise<void>;
|
||||||
deleteAllData: () => Promise<void>;
|
deleteAllData: () => Promise<void>;
|
||||||
deleteAllMyStories: () => Promise<void>;
|
deleteAllMyStories: () => Promise<void>;
|
||||||
editCustomColor: (colorId: string, customColor: CustomColorType) => void;
|
editCustomColor: (colorId: string, customColor: CustomColorType) => void;
|
||||||
|
@ -533,6 +534,10 @@ export function createIPCEvents(
|
||||||
showKeyboardShortcuts: () =>
|
showKeyboardShortcuts: () =>
|
||||||
window.reduxActions.globalModals.showShortcutGuideModal(),
|
window.reduxActions.globalModals.showShortcutGuideModal(),
|
||||||
|
|
||||||
|
cleanupDownloads: async () => {
|
||||||
|
await ipcRenderer.invoke('cleanup-downloads');
|
||||||
|
},
|
||||||
|
|
||||||
deleteAllData: async () => {
|
deleteAllData: async () => {
|
||||||
renderClearingDataView();
|
renderClearingDataView();
|
||||||
},
|
},
|
||||||
|
|
|
@ -123,9 +123,11 @@ export async function deleteAttachmentFromMessage(
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
deleteOnDisk,
|
deleteOnDisk,
|
||||||
|
deleteDownloadOnDisk,
|
||||||
logId,
|
logId,
|
||||||
}: {
|
}: {
|
||||||
deleteOnDisk: (path: string) => Promise<void>;
|
deleteOnDisk: (path: string) => Promise<void>;
|
||||||
|
deleteDownloadOnDisk: (path: string) => Promise<void>;
|
||||||
logId: string;
|
logId: string;
|
||||||
}
|
}
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
@ -147,6 +149,7 @@ export async function deleteAttachmentFromMessage(
|
||||||
|
|
||||||
return applyDeleteAttachmentFromMessage(message, deleteAttachmentData, {
|
return applyDeleteAttachmentFromMessage(message, deleteAttachmentData, {
|
||||||
deleteOnDisk,
|
deleteOnDisk,
|
||||||
|
deleteDownloadOnDisk,
|
||||||
logId,
|
logId,
|
||||||
shouldSave: true,
|
shouldSave: true,
|
||||||
});
|
});
|
||||||
|
@ -165,10 +168,12 @@ export async function applyDeleteAttachmentFromMessage(
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
deleteOnDisk,
|
deleteOnDisk,
|
||||||
|
deleteDownloadOnDisk,
|
||||||
shouldSave,
|
shouldSave,
|
||||||
logId,
|
logId,
|
||||||
}: {
|
}: {
|
||||||
deleteOnDisk: (path: string) => Promise<void>;
|
deleteOnDisk: (path: string) => Promise<void>;
|
||||||
|
deleteDownloadOnDisk: (path: string) => Promise<void>;
|
||||||
shouldSave: boolean;
|
shouldSave: boolean;
|
||||||
logId: string;
|
logId: string;
|
||||||
}
|
}
|
||||||
|
@ -206,7 +211,7 @@ export async function applyDeleteAttachmentFromMessage(
|
||||||
if (shouldSave) {
|
if (shouldSave) {
|
||||||
await saveMessage(message.attributes, { ourAci });
|
await saveMessage(message.attributes, { ourAci });
|
||||||
}
|
}
|
||||||
await deleteData(deleteOnDisk)(attachment);
|
await deleteData({ deleteOnDisk, deleteDownloadOnDisk })(attachment);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,6 +92,7 @@ export async function modifyTargetMessage(
|
||||||
logId,
|
logId,
|
||||||
shouldSave: false,
|
shouldSave: false,
|
||||||
deleteOnDisk: window.Signal.Migrations.deleteAttachmentData,
|
deleteOnDisk: window.Signal.Migrations.deleteAttachmentData,
|
||||||
|
deleteDownloadOnDisk: window.Signal.Migrations.deleteDownloadData,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (result) {
|
if (result) {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import type { AttachmentUploadFormResponseType } from '../textsecure/WebAPI';
|
||||||
import {
|
import {
|
||||||
type EncryptedAttachmentV2,
|
type EncryptedAttachmentV2,
|
||||||
encryptAttachmentV2ToDisk,
|
encryptAttachmentV2ToDisk,
|
||||||
safeUnlinkSync,
|
safeUnlink,
|
||||||
type PlaintextSourceType,
|
type PlaintextSourceType,
|
||||||
type HardcodedIVForEncryptionType,
|
type HardcodedIVForEncryptionType,
|
||||||
} from '../AttachmentCrypto';
|
} from '../AttachmentCrypto';
|
||||||
|
@ -117,7 +117,7 @@ export async function encryptAndUploadAttachment({
|
||||||
return { cdnKey: uploadForm.key, cdnNumber: uploadForm.cdn, encrypted };
|
return { cdnKey: uploadForm.key, cdnNumber: uploadForm.cdn, encrypted };
|
||||||
} finally {
|
} finally {
|
||||||
if (absoluteCiphertextPath) {
|
if (absoluteCiphertextPath) {
|
||||||
safeUnlinkSync(absoluteCiphertextPath);
|
await safeUnlink(absoluteCiphertextPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue