diff --git a/js/modules/backup.js b/js/modules/backup.js
deleted file mode 100644
index 80d1036298..0000000000
--- a/js/modules/backup.js
+++ /dev/null
@@ -1,1322 +0,0 @@
-// Copyright 2018-2020 Signal Messenger, LLC
-// SPDX-License-Identifier: AGPL-3.0-only
-
-/* global Signal: false */
-/* global Whisper: false */
-/* global _: false */
-/* global textsecure: false */
-/* global i18n: false */
-
-/* eslint-env browser */
-/* eslint-env node */
-
-/* eslint-disable no-param-reassign, guard-for-in */
-
-const fs = require('fs');
-const path = require('path');
-
-const { map, fromPairs } = require('lodash');
-const tar = require('tar');
-const tmp = require('tmp');
-const pify = require('pify');
-const rimraf = require('rimraf');
-const electronRemote = require('electron').remote;
-
-const crypto = require('../../ts/Crypto');
-const { getEnvironment } = require('../../ts/environment');
-
-const { dialog, BrowserWindow } = electronRemote;
-
-module.exports = {
- getDirectoryForExport,
- exportToDirectory,
- getDirectoryForImport,
- importFromDirectory,
- // for testing
- _sanitizeFileName,
- _trimFileName,
- _getExportAttachmentFileName,
- _getAnonymousAttachmentFileName,
- _getConversationDirName,
- _getConversationLoggingName,
-};
-
-function stringify(object) {
- // eslint-disable-next-line no-restricted-syntax
- for (const key in object) {
- const val = object[key];
- if (val instanceof ArrayBuffer) {
- object[key] = {
- type: 'ArrayBuffer',
- encoding: 'base64',
- data: crypto.arrayBufferToBase64(val),
- };
- } else if (val instanceof Object) {
- object[key] = stringify(val);
- }
- }
- return object;
-}
-
-function unstringify(object) {
- if (!(object instanceof Object)) {
- throw new Error('unstringify expects an object');
- }
- // eslint-disable-next-line no-restricted-syntax
- for (const key in object) {
- const val = object[key];
- if (
- val &&
- val.type === 'ArrayBuffer' &&
- val.encoding === 'base64' &&
- typeof val.data === 'string'
- ) {
- object[key] = crypto.base64ToArrayBuffer(val.data);
- } else if (val instanceof Object) {
- object[key] = unstringify(object[key]);
- }
- }
- return object;
-}
-
-function createOutputStream(writer) {
- let wait = Promise.resolve();
- return {
- write(string) {
- // eslint-disable-next-line more/no-then
- wait = wait.then(
- () =>
- new Promise(resolve => {
- if (writer.write(string)) {
- resolve();
- return;
- }
-
- // If write() returns true, we don't need to wait for the drain event
- // https://nodejs.org/dist/latest-v7.x/docs/api/stream.html#stream_class_stream_writable
- writer.once('drain', resolve);
-
- // We don't register for the 'error' event here, only in close(). Otherwise,
- // we'll get "Possible EventEmitter memory leak detected" warnings.
- })
- );
- return wait;
- },
- async close() {
- await wait;
- return new Promise((resolve, reject) => {
- writer.once('finish', resolve);
- writer.once('error', reject);
- writer.end();
- });
- },
- };
-}
-
-async function exportConversationListToFile(parent) {
- const writer = await createFileAndWriter(parent, 'db.json');
- return exportConversationList(writer);
-}
-
-function writeArray(stream, array) {
- stream.write('[');
-
- for (let i = 0, max = array.length; i < max; i += 1) {
- if (i > 0) {
- stream.write(',');
- }
-
- const item = array[i];
-
- // We don't back up avatars; we'll get them in a future contact sync or profile fetch
- const cleaned = _.omit(item, ['avatar', 'profileAvatar']);
-
- stream.write(JSON.stringify(stringify(cleaned)));
- }
-
- stream.write(']');
-}
-
-function getPlainJS(collection) {
- return collection.map(model => model.attributes);
-}
-
-async function exportConversationList(fileWriter) {
- const stream = createOutputStream(fileWriter);
-
- stream.write('{');
-
- stream.write('"conversations": ');
- const conversations = await window.Signal.Data.getAllConversations({
- ConversationCollection: Whisper.ConversationCollection,
- });
- window.log.info(`Exporting ${conversations.length} conversations`);
- writeArray(stream, getPlainJS(conversations));
-
- stream.write('}');
- await stream.close();
-}
-
-async function importNonMessages(parent, options) {
- const file = 'db.json';
- const string = await readFileAsText(parent, file);
- return importFromJsonString(string, path.join(parent, file), options);
-}
-
-function eliminateClientConfigInBackup(data, targetPath) {
- const cleaned = _.pick(data, 'conversations');
- window.log.info('Writing configuration-free backup file back to disk');
- try {
- fs.writeFileSync(targetPath, JSON.stringify(cleaned));
- } catch (error) {
- window.log.error('Error writing cleaned-up backup to disk: ', error.stack);
- }
-}
-
-async function importConversationsFromJSON(conversations, options) {
- const { writeNewAttachmentData } = window.Signal.Migrations;
- const { conversationLookup } = options;
-
- let count = 0;
- let skipCount = 0;
-
- for (let i = 0, max = conversations.length; i < max; i += 1) {
- const toAdd = unstringify(conversations[i]);
- const haveConversationAlready =
- conversationLookup[getConversationKey(toAdd)];
-
- if (haveConversationAlready) {
- skipCount += 1;
- count += 1;
- continue;
- }
-
- count += 1;
- // eslint-disable-next-line no-await-in-loop
- const migrated = await window.Signal.Types.Conversation.migrateConversation(
- toAdd,
- {
- writeNewAttachmentData,
- }
- );
- // eslint-disable-next-line no-await-in-loop
- await window.Signal.Data.saveConversation(migrated, {
- Conversation: Whisper.Conversation,
- });
- }
-
- window.log.info(
- 'Done importing conversations:',
- 'Total count:',
- count,
- 'Skipped:',
- skipCount
- );
-}
-
-async function importFromJsonString(jsonString, targetPath, options) {
- options = options || {};
- _.defaults(options, {
- forceLightImport: false,
- conversationLookup: {},
- });
-
- const result = {
- fullImport: true,
- };
-
- const importObject = JSON.parse(jsonString);
- delete importObject.debug;
-
- if (!importObject.sessions || options.forceLightImport) {
- result.fullImport = false;
-
- delete importObject.items;
- delete importObject.signedPreKeys;
- delete importObject.preKeys;
- delete importObject.identityKeys;
- delete importObject.sessions;
- delete importObject.unprocessed;
-
- window.log.info(
- 'This is a light import; contacts, groups and messages only'
- );
- }
-
- // We mutate the on-disk backup to prevent the user from importing client
- // configuration more than once - that causes lots of encryption errors.
- // This of course preserves the true data: conversations.
- eliminateClientConfigInBackup(importObject, targetPath);
-
- const storeNames = _.keys(importObject);
- window.log.info('Importing to these stores:', storeNames.join(', '));
-
- // Special-case conversations key here, going to SQLCipher
- const { conversations } = importObject;
- const remainingStoreNames = _.without(
- storeNames,
- 'conversations',
- 'unprocessed',
- 'groups' // in old data sets, but no longer included in database schema
- );
- await importConversationsFromJSON(conversations, options);
-
- const SAVE_FUNCTIONS = {
- identityKeys: window.Signal.Data.createOrUpdateIdentityKey,
- items: window.Signal.Data.createOrUpdateItem,
- preKeys: window.Signal.Data.createOrUpdatePreKey,
- sessions: window.Signal.Data.createOrUpdateSession,
- signedPreKeys: window.Signal.Data.createOrUpdateSignedPreKey,
- };
-
- await Promise.all(
- _.map(remainingStoreNames, async storeName => {
- const save = SAVE_FUNCTIONS[storeName];
- if (!_.isFunction(save)) {
- throw new Error(
- `importFromJsonString: Didn't have save function for store ${storeName}`
- );
- }
-
- window.log.info(`Importing items for store ${storeName}`);
- const toImport = importObject[storeName];
-
- if (!toImport || !toImport.length) {
- window.log.info(`No items in ${storeName} store`);
- return;
- }
-
- for (let i = 0, max = toImport.length; i < max; i += 1) {
- const toAdd = unstringify(toImport[i]);
- // eslint-disable-next-line no-await-in-loop
- await save(toAdd);
- }
-
- window.log.info(
- 'Done importing to store',
- storeName,
- 'Total count:',
- toImport.length
- );
- })
- );
-
- window.log.info('DB import complete');
- return result;
-}
-
-function createDirectory(parent, name) {
- return new Promise((resolve, reject) => {
- const sanitized = _sanitizeFileName(name);
- const targetDir = path.join(parent, sanitized);
- if (fs.existsSync(targetDir)) {
- resolve(targetDir);
- return;
- }
-
- fs.mkdir(targetDir, error => {
- if (error) {
- reject(error);
- return;
- }
-
- resolve(targetDir);
- });
- });
-}
-
-function createFileAndWriter(parent, name) {
- return new Promise(resolve => {
- const sanitized = _sanitizeFileName(name);
- const targetPath = path.join(parent, sanitized);
- const options = {
- flags: 'wx',
- };
- return resolve(fs.createWriteStream(targetPath, options));
- });
-}
-
-function readFileAsText(parent, name) {
- return new Promise((resolve, reject) => {
- const targetPath = path.join(parent, name);
- fs.readFile(targetPath, 'utf8', (error, string) => {
- if (error) {
- return reject(error);
- }
-
- return resolve(string);
- });
- });
-}
-
-// Buffer instances are also Uint8Array instances, but they might be a view
-// https://nodejs.org/docs/latest/api/buffer.html#buffer_buffers_and_typedarray
-const toArrayBuffer = nodeBuffer =>
- nodeBuffer.buffer.slice(
- nodeBuffer.byteOffset,
- nodeBuffer.byteOffset + nodeBuffer.byteLength
- );
-
-function readFileAsArrayBuffer(targetPath) {
- return new Promise((resolve, reject) => {
- // omitting the encoding to get a buffer back
- fs.readFile(targetPath, (error, buffer) => {
- if (error) {
- return reject(error);
- }
-
- return resolve(toArrayBuffer(buffer));
- });
- });
-}
-
-function _trimFileName(filename) {
- const components = filename.split('.');
- if (components.length <= 1) {
- return filename.slice(0, 30);
- }
-
- const extension = components[components.length - 1];
- const name = components.slice(0, components.length - 1);
- if (extension.length > 5) {
- return filename.slice(0, 30);
- }
-
- return `${name.join('.').slice(0, 24)}.${extension}`;
-}
-
-function _getExportAttachmentFileName(message, index, attachment) {
- if (attachment.fileName) {
- return _trimFileName(attachment.fileName);
- }
-
- let name = attachment.cdnId || attachment.cdnKey || attachment.id;
-
- if (attachment.contentType) {
- const components = attachment.contentType.split('/');
- name += `.${
- components.length > 1 ? components[1] : attachment.contentType
- }`;
- }
-
- return name;
-}
-
-function _getAnonymousAttachmentFileName(message, index) {
- if (!index) {
- return message.id;
- }
- return `${message.id}-${index}`;
-}
-
-async function readEncryptedAttachment(dir, attachment, name, options) {
- options = options || {};
- const { key } = options;
-
- const sanitizedName = _sanitizeFileName(name);
- const targetPath = path.join(dir, sanitizedName);
-
- if (!fs.existsSync(targetPath)) {
- window.log.warn(`Warning: attachment ${sanitizedName} not found`);
- return;
- }
-
- const data = await readFileAsArrayBuffer(targetPath);
-
- const isEncrypted = !_.isUndefined(key);
-
- if (isEncrypted) {
- attachment.data = await crypto.decryptAttachment(
- key,
- attachment.path,
- data
- );
- } else {
- attachment.data = data;
- }
-}
-
-async function writeQuoteThumbnail(attachment, options) {
- if (!attachment || !attachment.thumbnail || !attachment.thumbnail.path) {
- return;
- }
-
- const { dir, message, index, key, newKey } = options;
- const filename = `${_getAnonymousAttachmentFileName(
- message,
- index
- )}-quote-thumbnail`;
- const target = path.join(dir, filename);
-
- await writeEncryptedAttachment(target, attachment.thumbnail.path, {
- key,
- newKey,
- filename,
- dir,
- });
-}
-
-async function writeQuoteThumbnails(quotedAttachments, options) {
- const { name } = options;
-
- try {
- await Promise.all(
- _.map(quotedAttachments, (attachment, index) =>
- writeQuoteThumbnail(attachment, { ...options, index })
- )
- );
- } catch (error) {
- window.log.error(
- 'writeThumbnails: error exporting conversation',
- name,
- ':',
- error && error.stack ? error.stack : error
- );
- throw error;
- }
-}
-
-async function writeAttachment(attachment, options) {
- if (!_.isString(attachment.path)) {
- throw new Error('writeAttachment: attachment.path was not a string!');
- }
-
- const { dir, message, index, key, newKey } = options;
- const filename = _getAnonymousAttachmentFileName(message, index);
- const target = path.join(dir, filename);
-
- await writeEncryptedAttachment(target, attachment.path, {
- key,
- newKey,
- filename,
- dir,
- });
-
- if (attachment.thumbnail && _.isString(attachment.thumbnail.path)) {
- const thumbnailName = `${_getAnonymousAttachmentFileName(
- message,
- index
- )}-thumbnail`;
- const thumbnailTarget = path.join(dir, thumbnailName);
- await writeEncryptedAttachment(thumbnailTarget, attachment.thumbnail.path, {
- key,
- newKey,
- filename: thumbnailName,
- dir,
- });
- }
-
- if (attachment.screenshot && _.isString(attachment.screenshot.path)) {
- const screenshotName = `${_getAnonymousAttachmentFileName(
- message,
- index
- )}-screenshot`;
- const screenshotTarget = path.join(dir, screenshotName);
- await writeEncryptedAttachment(
- screenshotTarget,
- attachment.screenshot.path,
- {
- key,
- newKey,
- filename: screenshotName,
- dir,
- }
- );
- }
-}
-
-async function writeAttachments(attachments, options) {
- const { name } = options;
-
- const promises = _.map(attachments, (attachment, index) =>
- writeAttachment(attachment, { ...options, index })
- );
- try {
- await Promise.all(promises);
- } catch (error) {
- window.log.error(
- 'writeAttachments: error exporting conversation',
- name,
- ':',
- error && error.stack ? error.stack : error
- );
- throw error;
- }
-}
-
-async function writeAvatar(contact, options) {
- const { avatar } = contact || {};
- if (!avatar || !avatar.avatar || !avatar.avatar.path) {
- return;
- }
-
- const { dir, message, index, key, newKey } = options;
- const name = _getAnonymousAttachmentFileName(message, index);
- const filename = `${name}-contact-avatar`;
- const target = path.join(dir, filename);
-
- await writeEncryptedAttachment(target, avatar.avatar.path, {
- key,
- newKey,
- filename,
- dir,
- });
-}
-
-async function writeContactAvatars(contact, options) {
- const { name } = options;
-
- try {
- await Promise.all(
- _.map(contact, (item, index) => writeAvatar(item, { ...options, index }))
- );
- } catch (error) {
- window.log.error(
- 'writeContactAvatars: error exporting conversation',
- name,
- ':',
- error && error.stack ? error.stack : error
- );
- throw error;
- }
-}
-
-async function writePreviewImage(preview, options) {
- const { image } = preview || {};
- if (!image || !image.path) {
- return;
- }
-
- const { dir, message, index, key, newKey } = options;
- const name = _getAnonymousAttachmentFileName(message, index);
- const filename = `${name}-preview`;
- const target = path.join(dir, filename);
-
- await writeEncryptedAttachment(target, image.path, {
- key,
- newKey,
- filename,
- dir,
- });
-}
-
-async function writePreviews(preview, options) {
- const { name } = options;
-
- try {
- await Promise.all(
- _.map(preview, (item, index) =>
- writePreviewImage(item, { ...options, index })
- )
- );
- } catch (error) {
- window.log.error(
- 'writePreviews: error exporting conversation',
- name,
- ':',
- error && error.stack ? error.stack : error
- );
- throw error;
- }
-}
-
-async function writeEncryptedAttachment(target, source, options = {}) {
- const { key, newKey, filename, dir } = options;
-
- if (fs.existsSync(target)) {
- if (newKey) {
- window.log.info(`Deleting attachment ${filename}; key has changed`);
- fs.unlinkSync(target);
- } else {
- window.log.info(`Skipping attachment ${filename}; already exists`);
- return;
- }
- }
-
- const { readAttachmentData } = Signal.Migrations;
- const data = await readAttachmentData(source);
- const ciphertext = await crypto.encryptAttachment(key, source, data);
-
- const writer = await createFileAndWriter(dir, filename);
- const stream = createOutputStream(writer);
- stream.write(Buffer.from(ciphertext));
- await stream.close();
-}
-
-function _sanitizeFileName(filename) {
- return filename.toString().replace(/[^a-z0-9.,+()'#\- ]/gi, '_');
-}
-
-async function exportConversation(conversation, options = {}) {
- const { name, dir, attachmentsDir, key, newKey } = options;
-
- if (!name) {
- throw new Error('Need a name!');
- }
- if (!dir) {
- throw new Error('Need a target directory!');
- }
- if (!attachmentsDir) {
- throw new Error('Need an attachments directory!');
- }
- if (!key) {
- throw new Error('Need a key to encrypt with!');
- }
-
- window.log.info('exporting conversation', name);
- const writer = await createFileAndWriter(dir, 'messages.json');
- const stream = createOutputStream(writer);
- stream.write('{"messages":[');
-
- const CHUNK_SIZE = 50;
- let count = 0;
- let complete = false;
-
- // We're looping from the most recent to the oldest
- let lastReceivedAt = Number.MAX_VALUE;
- let lastSentAt = Number.MAX_VALUE;
-
- while (!complete) {
- // eslint-disable-next-line no-await-in-loop
- const collection = await window.Signal.Data.getOlderMessagesByConversation(
- conversation.id,
- {
- limit: CHUNK_SIZE,
- receivedAt: lastReceivedAt,
- sentAt: lastSentAt,
- MessageCollection: Whisper.MessageCollection,
- }
- );
- const messages = getPlainJS(collection);
-
- for (let i = 0, max = messages.length; i < max; i += 1) {
- const message = messages[i];
- if (count > 0) {
- stream.write(',');
- }
-
- count += 1;
-
- // skip message if it is disappearing, no matter the amount of time left
- if (message.expireTimer || message.messageTimer || message.isViewOnce) {
- continue;
- }
-
- const { attachments } = message;
- // eliminate attachment data from the JSON, since it will go to disk
- // Note: this is for legacy messages only, which stored attachment data in the db
- message.attachments = _.map(attachments, attachment =>
- _.omit(attachment, ['data'])
- );
- // completely drop any attachments in messages cached in error objects
- // TODO: move to lodash. Sadly, a number of the method signatures have changed!
- message.errors = _.map(message.errors, error => {
- if (error && error.args) {
- error.args = [];
- }
- if (error && error.stack) {
- error.stack = '';
- }
- return error;
- });
-
- const jsonString = JSON.stringify(stringify(message));
- stream.write(jsonString);
-
- if (attachments && attachments.length > 0) {
- // eslint-disable-next-line no-await-in-loop
- await writeAttachments(attachments, {
- dir: attachmentsDir,
- name,
- message,
- key,
- newKey,
- });
- }
-
- const quoteThumbnails = message.quote && message.quote.attachments;
- if (quoteThumbnails && quoteThumbnails.length > 0) {
- // eslint-disable-next-line no-await-in-loop
- await writeQuoteThumbnails(quoteThumbnails, {
- dir: attachmentsDir,
- name,
- message,
- key,
- newKey,
- });
- }
-
- const { contact } = message;
- if (contact && contact.length > 0) {
- // eslint-disable-next-line no-await-in-loop
- await writeContactAvatars(contact, {
- dir: attachmentsDir,
- name,
- message,
- key,
- newKey,
- });
- }
-
- const { preview } = message;
- if (preview && preview.length > 0) {
- // eslint-disable-next-line no-await-in-loop
- await writePreviews(preview, {
- dir: attachmentsDir,
- name,
- message,
- key,
- newKey,
- });
- }
- }
-
- const last = messages.length > 0 ? messages[messages.length - 1] : null;
- if (last) {
- lastReceivedAt = last.received_at;
- lastSentAt = last.sent_at;
- }
-
- if (messages.length < CHUNK_SIZE) {
- complete = true;
- }
- }
-
- stream.write(']}');
- await stream.close();
-}
-
-// Goals for directory names:
-// 1. Human-readable, for easy use and verification by user (names not just ids)
-// 2. Sorted just like the list of conversations in the left-pan (active_at)
-// 3. Disambiguated from other directories (active_at, truncated name, id)
-function _getConversationDirName(conversation) {
- const name = conversation.active_at || 'inactive';
- if (conversation.name) {
- return `${name} (${conversation.name.slice(0, 30)} ${conversation.id})`;
- }
- return `${name} (${conversation.id})`;
-}
-
-// Goals for logging names:
-// 1. Can be associated with files on disk
-// 2. Adequately disambiguated to enable debugging flow of execution
-// 3. Can be shared to the web without privacy concerns (there's no global redaction
-// logic for group ids, so we do it manually here)
-function _getConversationLoggingName(conversation) {
- let name = conversation.active_at || 'inactive';
- if (conversation.type === 'private') {
- name += ` (${conversation.id})`;
- } else {
- name += ` ([REDACTED_GROUP]${conversation.id.slice(-3)})`;
- }
- return name;
-}
-
-async function exportConversations(options) {
- options = options || {};
- const { messagesDir, attachmentsDir, key, newKey } = options;
-
- if (!messagesDir) {
- throw new Error('Need a messages directory!');
- }
- if (!attachmentsDir) {
- throw new Error('Need an attachments directory!');
- }
-
- const collection = await window.Signal.Data.getAllConversations({
- ConversationCollection: Whisper.ConversationCollection,
- });
- const conversations = collection.models;
-
- for (let i = 0, max = conversations.length; i < max; i += 1) {
- const conversation = conversations[i];
- const dirName = _getConversationDirName(conversation);
- const name = _getConversationLoggingName(conversation);
-
- // eslint-disable-next-line no-await-in-loop
- const dir = await createDirectory(messagesDir, dirName);
- // eslint-disable-next-line no-await-in-loop
- await exportConversation(conversation, {
- name,
- dir,
- attachmentsDir,
- key,
- newKey,
- });
- }
-
- window.log.info('Done exporting conversations!');
-}
-
-async function getDirectory(options = {}) {
- const browserWindow = BrowserWindow.getFocusedWindow();
- const dialogOptions = {
- title: options.title,
- properties: ['openDirectory'],
- buttonLabel: options.buttonLabel,
- };
-
- const { canceled, filePaths } = await dialog.showOpenDialog(
- browserWindow,
- dialogOptions
- );
- if (canceled || !filePaths || !filePaths[0]) {
- const error = new Error('Error choosing directory');
- error.name = 'ChooseError';
- throw error;
- }
-
- return filePaths[0];
-}
-
-function getDirContents(dir) {
- return new Promise((resolve, reject) => {
- fs.readdir(dir, (err, files) => {
- if (err) {
- reject(err);
- return;
- }
-
- files = _.map(files, file => path.join(dir, file));
-
- resolve(files);
- });
- });
-}
-
-async function loadAttachments(dir, getName, options) {
- options = options || {};
- const { message } = options;
-
- await Promise.all(
- _.map(message.attachments, async (attachment, index) => {
- const name = getName(message, index, attachment);
-
- await readEncryptedAttachment(dir, attachment, name, options);
-
- if (attachment.thumbnail && _.isString(attachment.thumbnail.path)) {
- const thumbnailName = `${name}-thumbnail`;
- await readEncryptedAttachment(
- dir,
- attachment.thumbnail,
- thumbnailName,
- options
- );
- }
-
- if (attachment.screenshot && _.isString(attachment.screenshot.path)) {
- const screenshotName = `${name}-screenshot`;
- await readEncryptedAttachment(
- dir,
- attachment.screenshot,
- screenshotName,
- options
- );
- }
- })
- );
-
- const quoteAttachments = message.quote && message.quote.attachments;
- await Promise.all(
- _.map(quoteAttachments, (attachment, index) => {
- const thumbnail = attachment && attachment.thumbnail;
- if (!thumbnail) {
- return null;
- }
-
- const name = `${getName(message, index)}-quote-thumbnail`;
- return readEncryptedAttachment(dir, thumbnail, name, options);
- })
- );
-
- const { contact } = message;
- await Promise.all(
- _.map(contact, (item, index) => {
- const avatar = item && item.avatar && item.avatar.avatar;
- if (!avatar) {
- return null;
- }
-
- const name = `${getName(message, index)}-contact-avatar`;
- return readEncryptedAttachment(dir, avatar, name, options);
- })
- );
-
- const { preview } = message;
- await Promise.all(
- _.map(preview, (item, index) => {
- const image = item && item.image;
- if (!image) {
- return null;
- }
-
- const name = `${getName(message, index)}-preview`;
- return readEncryptedAttachment(dir, image, name, options);
- })
- );
-}
-
-function saveMessage(message) {
- return saveAllMessages([message]);
-}
-
-async function saveAllMessages(rawMessages) {
- if (rawMessages.length === 0) {
- return;
- }
-
- try {
- const { writeMessageAttachments, upgradeMessageSchema } = Signal.Migrations;
- const importAndUpgrade = async message =>
- upgradeMessageSchema(await writeMessageAttachments(message));
-
- const messages = await Promise.all(rawMessages.map(importAndUpgrade));
-
- const { conversationId } = messages[0];
-
- await window.Signal.Data.saveMessages(messages, {
- forceSave: true,
- });
-
- window.log.info(
- 'Saved',
- messages.length,
- 'messages for conversation',
- // Don't know if group or private conversation, so we blindly redact
- `[REDACTED]${conversationId.slice(-3)}`
- );
- } catch (error) {
- window.log.error(
- 'saveAllMessages error',
- error && error.message ? error.message : error
- );
- }
-}
-
-// To reduce the memory impact of attachments, we make individual saves to the
-// database for every message with an attachment. We load the attachment for a
-// message, save it, and only then do we move on to the next message. Thus, every
-// message with attachments needs to be removed from our overall message save with the
-// filter() call.
-async function importConversation(dir, options) {
- options = options || {};
- _.defaults(options, { messageLookup: {} });
-
- const { messageLookup, attachmentsDir, key } = options;
-
- let conversationId = 'unknown';
- let total = 0;
- let skipped = 0;
- let contents;
-
- try {
- contents = await readFileAsText(dir, 'messages.json');
- } catch (error) {
- window.log.error(
- `Warning: could not access messages.json in directory: ${dir}`
- );
- }
-
- let promiseChain = Promise.resolve();
-
- const json = JSON.parse(contents);
- if (json.messages && json.messages.length) {
- conversationId = `[REDACTED]${(json.messages[0].conversationId || '').slice(
- -3
- )}`;
- }
- total = json.messages.length;
-
- const messages = _.filter(json.messages, message => {
- message = unstringify(message);
-
- if (messageLookup[getMessageKey(message)]) {
- skipped += 1;
- return false;
- }
-
- const hasAttachments = message.attachments && message.attachments.length;
- const hasQuotedAttachments =
- message.quote &&
- message.quote.attachments &&
- message.quote.attachments.length > 0;
- const hasContacts = message.contact && message.contact.length;
- const hasPreviews = message.preview && message.preview.length;
-
- if (hasAttachments || hasQuotedAttachments || hasContacts || hasPreviews) {
- const importMessage = async () => {
- const getName = attachmentsDir
- ? _getAnonymousAttachmentFileName
- : _getExportAttachmentFileName;
- const parentDir =
- attachmentsDir || path.join(dir, message.received_at.toString());
-
- await loadAttachments(parentDir, getName, {
- message,
- key,
- });
- return saveMessage(message);
- };
-
- // eslint-disable-next-line more/no-then
- promiseChain = promiseChain.then(importMessage);
-
- return false;
- }
-
- return true;
- });
-
- await saveAllMessages(messages);
-
- await promiseChain;
- window.log.info(
- 'Finished importing conversation',
- conversationId,
- 'Total:',
- total,
- 'Skipped:',
- skipped
- );
-}
-
-async function importConversations(dir, options) {
- const contents = await getDirContents(dir);
- let promiseChain = Promise.resolve();
-
- _.forEach(contents, conversationDir => {
- if (!fs.statSync(conversationDir).isDirectory()) {
- return;
- }
-
- const loadConversation = () => importConversation(conversationDir, options);
-
- // eslint-disable-next-line more/no-then
- promiseChain = promiseChain.then(loadConversation);
- });
-
- return promiseChain;
-}
-
-function getMessageKey(message) {
- const ourNumber = textsecure.storage.user.getNumber();
- const ourUuid = textsecure.storage.user.getUuid();
- const source = message.source || ourNumber;
- const sourceUuid = message.sourceUuid || ourUuid;
-
- if (
- (source && source === ourNumber) ||
- (sourceUuid && sourceUuid === ourUuid)
- ) {
- return `${source} ${message.timestamp}`;
- }
-
- const sourceDevice = message.sourceDevice || 1;
- return `${source}.${sourceDevice} ${message.timestamp}`;
-}
-async function loadMessagesLookup() {
- const array = await window.Signal.Data.getAllMessageIds();
- return fromPairs(map(array, item => [getMessageKey(item), true]));
-}
-
-function getConversationKey(conversation) {
- return conversation.id;
-}
-async function loadConversationLookup() {
- const array = await window.Signal.Data.getAllConversationIds();
- return fromPairs(map(array, item => [getConversationKey(item), true]));
-}
-
-function getDirectoryForExport() {
- return getDirectory();
-}
-
-async function compressArchive(file, targetDir) {
- const items = fs.readdirSync(targetDir);
- return tar.c(
- {
- gzip: true,
- file,
- cwd: targetDir,
- },
- items
- );
-}
-
-async function decompressArchive(file, targetDir) {
- return tar.x({
- file,
- cwd: targetDir,
- });
-}
-
-function writeFile(targetPath, contents) {
- return pify(fs.writeFile)(targetPath, contents);
-}
-
-// prettier-ignore
-const UNIQUE_ID = new Uint8Array([
- 1, 3, 4, 5, 6, 7, 8, 11,
- 23, 34, 1, 34, 3, 5, 45, 45,
- 1, 3, 4, 5, 6, 7, 8, 11,
- 23, 34, 1, 34, 3, 5, 45, 45,
-]);
-async function encryptFile(sourcePath, targetPath, options) {
- options = options || {};
-
- const { key } = options;
- if (!key) {
- throw new Error('Need key to do encryption!');
- }
-
- const plaintext = await readFileAsArrayBuffer(sourcePath);
- const ciphertext = await crypto.encryptFile(key, UNIQUE_ID, plaintext);
- return writeFile(targetPath, Buffer.from(ciphertext));
-}
-
-async function decryptFile(sourcePath, targetPath, options) {
- options = options || {};
-
- const { key } = options;
- if (!key) {
- throw new Error('Need key to do encryption!');
- }
-
- const ciphertext = await readFileAsArrayBuffer(sourcePath);
- const plaintext = await crypto.decryptFile(key, UNIQUE_ID, ciphertext);
- return writeFile(targetPath, Buffer.from(plaintext));
-}
-
-function createTempDir() {
- return pify(tmp.dir)();
-}
-
-function deleteAll(pattern) {
- return pify(rimraf)(pattern);
-}
-
-const ARCHIVE_NAME = 'messages.tar.gz';
-
-async function exportToDirectory(directory, options) {
- const env = getEnvironment();
- if (env !== 'test') {
- throw new Error('export is only supported in test mode');
- }
-
- options = options || {};
-
- if (!options.key) {
- throw new Error('Encrypted backup requires a key to encrypt with!');
- }
-
- let stagingDir;
- let encryptionDir;
- try {
- stagingDir = await createTempDir();
- encryptionDir = await createTempDir();
-
- const attachmentsDir = await createDirectory(directory, 'attachments');
-
- await exportConversationListToFile(stagingDir);
- await exportConversations({
- ...options,
- messagesDir: stagingDir,
- attachmentsDir,
- });
-
- const archivePath = path.join(directory, ARCHIVE_NAME);
- await compressArchive(archivePath, stagingDir);
- await encryptFile(archivePath, path.join(directory, ARCHIVE_NAME), options);
-
- window.log.info('done backing up!');
- return directory;
- } catch (error) {
- window.log.error(
- 'The backup went wrong!',
- error && error.stack ? error.stack : error
- );
- throw error;
- } finally {
- if (stagingDir) {
- await deleteAll(stagingDir);
- }
- if (encryptionDir) {
- await deleteAll(encryptionDir);
- }
- }
-}
-
-function getDirectoryForImport() {
- const options = {
- title: i18n('importChooserTitle'),
- };
- return getDirectory(options);
-}
-
-async function importFromDirectory(directory, options) {
- options = options || {};
-
- try {
- const lookups = await Promise.all([
- loadMessagesLookup(),
- loadConversationLookup(),
- ]);
- const [messageLookup, conversationLookup] = lookups;
- options = { ...options, messageLookup, conversationLookup };
-
- const archivePath = path.join(directory, ARCHIVE_NAME);
- if (fs.existsSync(archivePath)) {
- const env = getEnvironment();
- if (env !== 'test') {
- throw new Error('import is only supported in test mode');
- }
-
- // we're in the world of an encrypted, zipped backup
- if (!options.key) {
- throw new Error(
- 'Importing an encrypted backup; decryption key is required!'
- );
- }
-
- let stagingDir;
- let decryptionDir;
- try {
- stagingDir = await createTempDir();
- decryptionDir = await createTempDir();
-
- const attachmentsDir = path.join(directory, 'attachments');
-
- const decryptedArchivePath = path.join(decryptionDir, ARCHIVE_NAME);
- await decryptFile(archivePath, decryptedArchivePath, options);
- await decompressArchive(decryptedArchivePath, stagingDir);
-
- options = { ...options, attachmentsDir };
- const result = await importNonMessages(stagingDir, options);
- await importConversations(stagingDir, { ...options });
-
- window.log.info('Done importing from backup!');
- return result;
- } finally {
- if (stagingDir) {
- await deleteAll(stagingDir);
- }
- if (decryptionDir) {
- await deleteAll(decryptionDir);
- }
- }
- }
-
- const result = await importNonMessages(directory, options);
- await importConversations(directory, options);
-
- window.log.info('Done importing!');
- return result;
- } catch (error) {
- window.log.error(
- 'The import went wrong!',
- error && error.stack ? error.stack : error
- );
- throw error;
- }
-}
diff --git a/preload.js b/preload.js
index 7debc1baf7..2a00f1bd5a 100644
--- a/preload.js
+++ b/preload.js
@@ -590,7 +590,6 @@ try {
require('./ts/background');
// Pulling these in separately since they access filesystem, electron
- window.Signal.Backup = require('./js/modules/backup');
window.Signal.Debug = require('./js/modules/debug');
window.Signal.Logs = require('./js/modules/logs');
diff --git a/test/backup_test.js b/test/backup_test.js
deleted file mode 100644
index ac9a4bb104..0000000000
--- a/test/backup_test.js
+++ /dev/null
@@ -1,666 +0,0 @@
-// Copyright 2017-2020 Signal Messenger, LLC
-// SPDX-License-Identifier: AGPL-3.0-only
-
-/* global Signal, Whisper, textsecure, _ */
-
-/* eslint-disable no-console */
-
-'use strict';
-
-describe('Backup', () => {
- describe('_sanitizeFileName', () => {
- it('leaves a basic string alone', () => {
- const initial = "Hello, how are you #5 ('fine' + great).jpg";
- const expected = initial;
- assert.strictEqual(Signal.Backup._sanitizeFileName(initial), expected);
- });
-
- it('replaces all unknown characters', () => {
- const initial = '!@$%^&*=';
- const expected = '________';
- assert.strictEqual(Signal.Backup._sanitizeFileName(initial), expected);
- });
- });
-
- describe('_trimFileName', () => {
- it('handles a file with no extension', () => {
- const initial = '0123456789012345678901234567890123456789';
- const expected = '012345678901234567890123456789';
- assert.strictEqual(Signal.Backup._trimFileName(initial), expected);
- });
-
- it('handles a file with a long extension', () => {
- const initial =
- '0123456789012345678901234567890123456789.01234567890123456789';
- const expected = '012345678901234567890123456789';
- assert.strictEqual(Signal.Backup._trimFileName(initial), expected);
- });
-
- it('handles a file with a normal extension', () => {
- const initial = '01234567890123456789012345678901234567890123456789.jpg';
- const expected = '012345678901234567890123.jpg';
- assert.strictEqual(Signal.Backup._trimFileName(initial), expected);
- });
- });
-
- describe('_getExportAttachmentFileName', () => {
- it('uses original filename if attachment has one', () => {
- const message = {
- body: 'something',
- };
- const index = 0;
- const attachment = {
- fileName: 'blah.jpg',
- };
- const expected = 'blah.jpg';
-
- const actual = Signal.Backup._getExportAttachmentFileName(
- message,
- index,
- attachment
- );
- assert.strictEqual(actual, expected);
- });
-
- it('uses attachment id if no filename', () => {
- const message = {
- body: 'something',
- };
- const index = 0;
- const attachment = {
- cdnId: '123',
- };
- const expected = '123';
-
- const actual = Signal.Backup._getExportAttachmentFileName(
- message,
- index,
- attachment
- );
- assert.strictEqual(actual, expected);
- });
-
- it('uses attachment id and contentType if available', () => {
- const message = {
- body: 'something',
- };
- const index = 0;
- const attachment = {
- cdnId: '123',
- contentType: 'image/jpeg',
- };
- const expected = '123.jpeg';
-
- const actual = Signal.Backup._getExportAttachmentFileName(
- message,
- index,
- attachment
- );
- assert.strictEqual(actual, expected);
- });
-
- it('handles strange contentType', () => {
- const message = {
- body: 'something',
- };
- const index = 0;
- const attachment = {
- cdnId: '123',
- contentType: 'something',
- };
- const expected = '123.something';
-
- const actual = Signal.Backup._getExportAttachmentFileName(
- message,
- index,
- attachment
- );
- assert.strictEqual(actual, expected);
- });
-
- it('uses CDN key if attachment ID not available', () => {
- const message = {
- body: 'something',
- };
- const index = 0;
- const attachment = {
- cdnKey: 'abc',
- };
- const expected = 'abc';
-
- const actual = Signal.Backup._getExportAttachmentFileName(
- message,
- index,
- attachment
- );
- assert.strictEqual(actual, expected);
- });
-
- it('uses CDN key and contentType if available', () => {
- const message = {
- body: 'something',
- };
- const index = 0;
- const attachment = {
- cdnKey: 'def',
- contentType: 'image/jpeg',
- };
- const expected = 'def.jpeg';
-
- const actual = Signal.Backup._getExportAttachmentFileName(
- message,
- index,
- attachment
- );
- assert.strictEqual(actual, expected);
- });
- });
-
- describe('_getAnonymousAttachmentFileName', () => {
- it('uses message id', () => {
- const message = {
- id: 'id-45',
- body: 'something',
- };
- const index = 0;
- const attachment = {
- fileName: 'blah.jpg',
- };
- const expected = 'id-45';
-
- const actual = Signal.Backup._getAnonymousAttachmentFileName(
- message,
- index,
- attachment
- );
- assert.strictEqual(actual, expected);
- });
-
- it('appends index if it is above zero', () => {
- const message = {
- id: 'id-45',
- body: 'something',
- };
- const index = 1;
- const attachment = {
- fileName: 'blah.jpg',
- };
- const expected = 'id-45-1';
-
- const actual = Signal.Backup._getAnonymousAttachmentFileName(
- message,
- index,
- attachment
- );
- assert.strictEqual(actual, expected);
- });
- });
-
- describe('_getConversationDirName', () => {
- it('uses name if available', () => {
- const conversation = {
- active_at: 123,
- name: '0123456789012345678901234567890123456789',
- id: 'id',
- };
- const expected = '123 (012345678901234567890123456789 id)';
- assert.strictEqual(
- Signal.Backup._getConversationDirName(conversation),
- expected
- );
- });
-
- it('uses just id if name is not available', () => {
- const conversation = {
- active_at: 123,
- id: 'id',
- };
- const expected = '123 (id)';
- assert.strictEqual(
- Signal.Backup._getConversationDirName(conversation),
- expected
- );
- });
-
- it('uses inactive for missing active_at', () => {
- const conversation = {
- name: 'name',
- id: 'id',
- };
- const expected = 'inactive (name id)';
- assert.strictEqual(
- Signal.Backup._getConversationDirName(conversation),
- expected
- );
- });
- });
-
- describe('_getConversationLoggingName', () => {
- it('uses plain id if conversation is private', () => {
- const conversation = {
- active_at: 123,
- id: 'id',
- type: 'private',
- };
- const expected = '123 (id)';
- assert.strictEqual(
- Signal.Backup._getConversationLoggingName(conversation),
- expected
- );
- });
-
- it('uses just id if name is not available', () => {
- const conversation = {
- active_at: 123,
- id: 'groupId',
- type: 'group',
- };
- const expected = '123 ([REDACTED_GROUP]pId)';
- assert.strictEqual(
- Signal.Backup._getConversationLoggingName(conversation),
- expected
- );
- });
-
- it('uses inactive for missing active_at', () => {
- const conversation = {
- id: 'id',
- type: 'private',
- };
- const expected = 'inactive (id)';
- assert.strictEqual(
- Signal.Backup._getConversationLoggingName(conversation),
- expected
- );
- });
- });
-
- describe('end-to-end', () => {
- it('exports then imports to produce the same data we started with', async function thisNeeded() {
- this.timeout(6000);
-
- const {
- attachmentsPath,
- fse,
- fastGlob,
- normalizePath,
- path,
- tmp,
- } = window.test;
- const {
- upgradeMessageSchema,
- loadAttachmentData,
- } = window.Signal.Migrations;
-
- const staticKeyPair = window.Signal.Curve.generateKeyPair();
- const attachmentsPattern = normalizePath(
- path.join(attachmentsPath, '**')
- );
-
- const OUR_NUMBER = '+12025550000';
- const CONTACT_ONE_NUMBER = '+12025550001';
- const CONTACT_TWO_NUMBER = '+12025550002';
-
- const CONVERSATION_ID = 'bdaa7f4f-e9bd-493e-ab0d-8331ad604269';
-
- const getFixture = target =>
- window.Signal.Crypto.typedArrayToArrayBuffer(fse.readFileSync(target));
-
- const FIXTURES = {
- gif: getFixture('fixtures/giphy-7GFfijngKbeNy.gif'),
- mp4: getFixture('fixtures/pixabay-Soap-Bubble-7141.mp4'),
- jpg: getFixture('fixtures/koushik-chowdavarapu-105425-unsplash.jpg'),
- mp3: getFixture('fixtures/incompetech-com-Agnus-Dei-X.mp3'),
- txt: getFixture('fixtures/lorem-ipsum.txt'),
- png: getFixture(
- 'fixtures/freepngs-2cd43b_bed7d1327e88454487397574d87b64dc_mv2.png'
- ),
- };
-
- async function wrappedLoadAttachment(attachment) {
- return _.omit(await loadAttachmentData(attachment), ['path']);
- }
-
- async function clearAllData() {
- await textsecure.storage.protocol.removeAllData();
- await fse.emptyDir(attachmentsPath);
- }
-
- function removeId(model) {
- return _.omit(model, ['id']);
- }
-
- const getUndefinedKeys = object =>
- Object.entries(object)
- .filter(([, value]) => value === undefined)
- .map(([name]) => name);
- const omitUndefinedKeys = object =>
- _.omit(object, getUndefinedKeys(object));
-
- // We want to know which paths have two slashes, since that tells us which files
- // in the attachment fan-out are files vs. directories.
- const TWO_SLASHES = /[^/]*\/[^/]*\/[^/]*/;
- // On windows, attachmentsPath has a normal windows path format (\ separators), but
- // glob returns only /. We normalize to / separators for our manipulations.
- const normalizedBase = attachmentsPath.replace(/\\/g, '/');
- function removeDirs(dirs) {
- return _.filter(dirs, fullDir => {
- const dir = fullDir.replace(normalizedBase, '');
- return TWO_SLASHES.test(dir);
- });
- }
-
- function _mapQuotedAttachments(mapper) {
- return async (message, context) => {
- if (!message.quote) {
- return message;
- }
-
- const wrappedMapper = async attachment => {
- if (!attachment || !attachment.thumbnail) {
- return attachment;
- }
-
- return {
- ...attachment,
- thumbnail: await mapper(attachment.thumbnail, context),
- };
- };
-
- const quotedAttachments =
- (message.quote && message.quote.attachments) || [];
-
- return {
- ...message,
- quote: {
- ...message.quote,
- attachments: await Promise.all(
- quotedAttachments.map(wrappedMapper)
- ),
- },
- };
- };
- }
-
- async function loadAllFilesFromDisk(message) {
- const loadThumbnails = _mapQuotedAttachments(thumbnail => {
- // we want to be bulletproof to thumbnails without data
- if (!thumbnail.path) {
- return thumbnail;
- }
-
- return wrappedLoadAttachment(thumbnail);
- });
-
- return {
- ...(await loadThumbnails(message)),
- contact: await Promise.all(
- (message.contact || []).map(async contact => {
- return contact && contact.avatar && contact.avatar.avatar
- ? {
- ...contact,
- avatar: {
- ...contact.avatar,
- avatar: await wrappedLoadAttachment(
- contact.avatar.avatar
- ),
- },
- }
- : contact;
- })
- ),
- attachments: await Promise.all(
- (message.attachments || []).map(async attachment => {
- await wrappedLoadAttachment(attachment);
-
- if (attachment.thumbnail) {
- await wrappedLoadAttachment(attachment.thumbnail);
- }
-
- if (attachment.screenshot) {
- await wrappedLoadAttachment(attachment.screenshot);
- }
-
- return attachment;
- })
- ),
- preview: await Promise.all(
- (message.preview || []).map(async item => {
- if (item.image) {
- await wrappedLoadAttachment(item.image);
- }
-
- return item;
- })
- ),
- };
- }
-
- let backupDir;
- try {
- // Seven total:
- // - Five from image/video attachments
- // - One from embedded contact avatar
- // - One from embedded quoted attachment thumbnail
- // - One from a link preview image
- const ATTACHMENT_COUNT = 8;
- const MESSAGE_COUNT = 1;
- const CONVERSATION_COUNT = 1;
-
- const messageWithAttachments = {
- conversationId: CONVERSATION_ID,
- body: 'Totally!',
- source: OUR_NUMBER,
- received_at: 1524185933350,
- sent_at: 1524185933350,
- timestamp: 1524185933350,
- errors: [],
- attachments: [
- // Note: generates two more files: screenshot and thumbnail
- {
- contentType: 'video/mp4',
- fileName: 'video.mp4',
- data: FIXTURES.mp4,
- },
- // Note: generates one more file: thumbnail
- {
- contentType: 'image/png',
- fileName: 'landscape.png',
- data: FIXTURES.png,
- },
- ],
- hasAttachments: 1,
- hasVisualMediaAttachments: 1,
- quote: {
- text: "Isn't it cute?",
- author: CONTACT_ONE_NUMBER,
- id: 12345678,
- attachments: [
- {
- contentType: 'audio/mp3',
- fileName: 'song.mp3',
- },
- {
- contentType: 'image/gif',
- fileName: 'avatar.gif',
- thumbnail: {
- contentType: 'image/png',
- data: FIXTURES.gif,
- },
- },
- ],
- },
- contact: [
- {
- name: {
- displayName: 'Someone Somewhere',
- },
- number: [
- {
- value: CONTACT_TWO_NUMBER,
- type: 1,
- },
- ],
- avatar: {
- isProfile: false,
- avatar: {
- contentType: 'image/png',
- data: FIXTURES.png,
- },
- },
- },
- ],
- preview: [
- {
- url: 'https://www.instagram.com/p/BsOGulcndj-/',
- title:
- 'EGG GANG π on Instagram: βLetβs set a world record together and get the most liked post on Instagram. Beating the current world record held by Kylie Jenner (18β¦β',
- image: {
- contentType: 'image/jpeg',
- data: FIXTURES.jpg,
- },
- },
- ],
- };
-
- console.log('Backup test: Clear all data');
- await clearAllData();
-
- console.log('Backup test: Create models, save to db/disk');
- const message = await upgradeMessageSchema(messageWithAttachments);
- console.log({ message });
- await window.Signal.Data.saveMessage(message, {
- Message: Whisper.Message,
- });
-
- const conversation = {
- active_at: 1524185933350,
- color: 'orange',
- expireTimer: 0,
- id: CONVERSATION_ID,
- name: 'Someone Somewhere',
- profileAvatar: {
- contentType: 'image/jpeg',
- data: FIXTURES.jpeg,
- size: 64,
- },
- profileKey: 'BASE64KEY',
- profileName: 'Someone! π€',
- profileSharing: true,
- profileLastFetchedAt: 1524185933350,
- timestamp: 1524185933350,
- type: 'private',
- unreadCount: 0,
- messageCount: 0,
- sentMessageCount: 0,
- verified: 0,
- sealedSender: 0,
- version: 2,
- };
- console.log({ conversation });
- await window.Signal.Data.saveConversation(conversation, {
- Conversation: Whisper.Conversation,
- });
-
- console.log(
- 'Backup test: Ensure that all attachments were saved to disk'
- );
- const attachmentFiles = removeDirs(fastGlob.sync(attachmentsPattern));
- console.log({ attachmentFiles });
- assert.strictEqual(ATTACHMENT_COUNT, attachmentFiles.length);
-
- console.log('Backup test: Export!');
- backupDir = tmp.dirSync().name;
- console.log({ backupDir });
- await Signal.Backup.exportToDirectory(backupDir, {
- key: staticKeyPair.pubKey,
- });
-
- console.log('Backup test: Ensure that messages.tar.gz exists');
- const archivePath = path.join(backupDir, 'messages.tar.gz');
- const messageZipExists = fse.existsSync(archivePath);
- assert.strictEqual(true, messageZipExists);
-
- console.log(
- 'Backup test: Ensure that all attachments made it to backup dir'
- );
- const backupAttachmentPattern = normalizePath(
- path.join(backupDir, 'attachments/*')
- );
- const backupAttachments = fastGlob.sync(backupAttachmentPattern);
- console.log({ backupAttachments });
- assert.strictEqual(ATTACHMENT_COUNT, backupAttachments.length);
-
- console.log('Backup test: Clear all data');
- await clearAllData();
-
- console.log('Backup test: Import!');
- await Signal.Backup.importFromDirectory(backupDir, {
- key: staticKeyPair.privKey,
- });
-
- console.log('Backup test: Check conversations');
- const conversationCollection = await window.Signal.Data.getAllConversations(
- {
- ConversationCollection: Whisper.ConversationCollection,
- }
- );
- assert.strictEqual(conversationCollection.length, CONVERSATION_COUNT);
-
- const conversationFromDB = conversationCollection.at(0).attributes;
- console.log({ conversationFromDB, conversation });
- assert.deepEqual(
- conversationFromDB,
- _.omit(conversation, ['profileAvatar'])
- );
-
- console.log('Backup test: Check messages');
- const messages = await window.Signal.Data._getAllMessages({
- MessageCollection: Whisper.MessageCollection,
- });
- assert.strictEqual(messages.length, MESSAGE_COUNT);
- const messageFromDB = removeId(messages.at(0).attributes);
- const expectedMessage = messageFromDB;
- console.log({ messageFromDB, expectedMessage });
- assert.deepEqual(messageFromDB, expectedMessage);
-
- console.log('Backup test: Ensure that all attachments were imported');
- const recreatedAttachmentFiles = removeDirs(
- fastGlob.sync(attachmentsPattern)
- );
- console.log({ recreatedAttachmentFiles });
- assert.strictEqual(ATTACHMENT_COUNT, recreatedAttachmentFiles.length);
- assert.deepEqual(attachmentFiles, recreatedAttachmentFiles);
-
- console.log(
- 'Backup test: Check that all attachments were successfully imported'
- );
- const messageWithAttachmentsFromDB = await loadAllFilesFromDisk(
- omitUndefinedKeys(messageFromDB)
- );
- const expectedMessageWithAttachments = await loadAllFilesFromDisk(
- omitUndefinedKeys(message)
- );
- console.log({
- messageWithAttachmentsFromDB,
- expectedMessageWithAttachments,
- });
- assert.deepEqual(
- messageWithAttachmentsFromDB,
- expectedMessageWithAttachments
- );
-
- console.log('Backup test: Clear all data');
- await clearAllData();
-
- console.log('Backup test: Complete!');
- } finally {
- if (backupDir) {
- console.log({ backupDir });
- console.log('Deleting', backupDir);
- await fse.remove(backupDir);
- }
- }
- });
- });
-});
diff --git a/test/index.html b/test/index.html
index 40f350b0f9..69c6214038 100644
--- a/test/index.html
+++ b/test/index.html
@@ -372,7 +372,6 @@
-