Files
Signal-Desktop/js/modules/backup.js
Scott Nonnenberg 6d8f4b7b6e Backup: zipped messages.json, flat attachments dir
Backup creates, in a target directory:
  - An attachments folder, with all attachments, each named for their
    parent message's id - a GUID. If there is more than one attachment
    in a given message,  each attachment beyond the first will end with
    '-N', zero-indexed.
  - A file named messages.zip. It contains exactly what went to disk in
    the original export code, but zipped up.

Export is now only 'light,' and in this new messages.zip format.

Import supports both the new format and the old format. If the target
directory has a messages.zip file, we'll treat it as the new format.

Next up: Encrypting attachments and the messages.zip!
2018-03-20 11:53:22 -07:00

1121 lines
30 KiB
JavaScript

/* global Whisper: false */
/* global dcodeIO: 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 tmp = require('tmp');
const decompress = require('decompress');
const pify = require('pify');
const archiver = require('archiver');
const rimraf = require('rimraf');
const electronRemote = require('electron').remote;
const {
dialog,
BrowserWindow,
} = electronRemote;
module.exports = {
getDirectoryForExport,
backupToDirectory,
getDirectoryForImport,
importFromDirectory,
// for testing
sanitizeFileName,
trimFileName,
getExportAttachmentFileName,
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: dcodeIO.ByteBuffer.wrap(val).toString('base64'),
};
} 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] = dcodeIO.ByteBuffer.wrap(val.data, 'base64').toArrayBuffer();
} 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 exportContactAndGroupsToFile(db, parent) {
const writer = await createFileAndWriter(parent, 'db.json');
return exportContactsAndGroups(db, writer);
}
function exportContactsAndGroups(db, fileWriter) {
return new Promise((resolve, reject) => {
let storeNames = db.objectStoreNames;
storeNames = _.without(
storeNames,
'messages',
'items',
'signedPreKeys',
'preKeys',
'identityKeys',
'sessions',
'unprocessed'
);
const exportedStoreNames = [];
if (storeNames.length === 0) {
throw new Error('No stores to export');
}
console.log('Exporting from these stores:', storeNames.join(', '));
const stream = createOutputStream(fileWriter);
stream.write('{');
_.each(storeNames, (storeName) => {
const transaction = db.transaction(storeNames, 'readwrite');
transaction.onerror = () => {
Whisper.Database.handleDOMException(
`exportToJsonFile transaction error (store: ${storeName})`,
transaction.error,
reject
);
};
transaction.oncomplete = () => {
console.log('transaction complete');
};
const store = transaction.objectStore(storeName);
const request = store.openCursor();
let count = 0;
request.onerror = () => {
Whisper.Database.handleDOMException(
`exportToJsonFile request error (store: ${storeNames})`,
request.error,
reject
);
};
request.onsuccess = async (event) => {
if (count === 0) {
console.log('cursor opened');
stream.write(`"${storeName}": [`);
}
const cursor = event.target.result;
if (cursor) {
if (count > 0) {
stream.write(',');
}
// Preventing base64'd images from reaching the disk, making db.json too big
const item = _.omit(
cursor.value,
['avatar', 'profileAvatar']
);
const jsonString = JSON.stringify(stringify(item));
stream.write(jsonString);
cursor.continue();
count += 1;
} else {
// no more
stream.write(']');
console.log('Exported', count, 'items from store', storeName);
exportedStoreNames.push(storeName);
if (exportedStoreNames.length < storeNames.length) {
stream.write(',');
} else {
console.log('Exported all stores');
stream.write('}');
await stream.close();
console.log('Finished writing all stores to disk');
resolve();
}
}
};
});
});
}
async function importNonMessages(db, parent, options) {
const file = 'db.json';
const string = await readFileAsText(parent, file);
return importFromJsonString(db, string, path.join(parent, file), options);
}
function eliminateClientConfigInBackup(data, targetPath) {
const cleaned = _.pick(data, 'conversations', 'groups');
console.log('Writing configuration-free backup file back to disk');
try {
fs.writeFileSync(targetPath, JSON.stringify(cleaned));
} catch (error) {
console.log('Error writing cleaned-up backup to disk: ', error.stack);
}
}
function importFromJsonString(db, jsonString, targetPath, options) {
options = options || {};
_.defaults(options, {
forceLightImport: false,
conversationLookup: {},
groupLookup: {},
});
const {
conversationLookup,
groupLookup,
} = options;
const result = {
fullImport: true,
};
return new Promise((resolve, reject) => {
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;
console.log('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 and groups.
eliminateClientConfigInBackup(importObject, targetPath);
const storeNames = _.keys(importObject);
console.log('Importing to these stores:', storeNames.join(', '));
let finished = false;
const finish = (via) => {
console.log('non-messages import done via', via);
if (finished) {
resolve(result);
}
finished = true;
};
const transaction = db.transaction(storeNames, 'readwrite');
transaction.onerror = () => {
Whisper.Database.handleDOMException(
'importFromJsonString transaction error',
transaction.error,
reject
);
};
transaction.oncomplete = finish.bind(null, 'transaction complete');
_.each(storeNames, (storeName) => {
console.log('Importing items for store', storeName);
if (!importObject[storeName].length) {
delete importObject[storeName];
return;
}
let count = 0;
let skipCount = 0;
const finishStore = () => {
// added all objects for this store
delete importObject[storeName];
console.log(
'Done importing to store',
storeName,
'Total count:',
count,
'Skipped:',
skipCount
);
if (_.keys(importObject).length === 0) {
// added all object stores
console.log('DB import complete');
finish('puts scheduled');
}
};
_.each(importObject[storeName], (toAdd) => {
toAdd = unstringify(toAdd);
const haveConversationAlready =
storeName === 'conversations' &&
conversationLookup[getConversationKey(toAdd)];
const haveGroupAlready =
storeName === 'groups' && groupLookup[getGroupKey(toAdd)];
if (haveConversationAlready || haveGroupAlready) {
skipCount += 1;
count += 1;
return;
}
const request = transaction.objectStore(storeName).put(toAdd, toAdd.id);
request.onsuccess = () => {
count += 1;
if (count === importObject[storeName].length) {
finishStore();
}
};
request.onerror = () => {
Whisper.Database.handleDOMException(
`importFromJsonString request error (store: ${storeName})`,
request.error,
reject
);
};
});
// We have to check here, because we may have skipped every item, resulting
// in no onsuccess callback at all.
if (count === importObject[storeName].length) {
finishStore();
}
});
});
}
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);
});
});
}
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);
}
// Buffer instances are also Uint8Array instances
// https://nodejs.org/docs/latest/api/buffer.html#buffer_buffers_and_typedarray
return resolve(buffer.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.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 readAttachment(dir, attachment, name) {
const anonymousName = sanitizeFileName(name);
const targetPath = path.join(dir, anonymousName);
if (!fs.existsSync(targetPath)) {
console.log(`Warning: attachment ${anonymousName} not found`);
return;
}
attachment.data = await readFileAsArrayBuffer(targetPath);
}
async function writeAttachment(dir, message, index, attachment) {
const filename = getAnonymousAttachmentFileName(message, index);
const target = path.join(dir, filename);
if (fs.existsSync(target)) {
console.log(`Skipping attachment ${filename}; already exists`);
return;
}
const writer = await createFileAndWriter(dir, filename);
const stream = createOutputStream(writer);
stream.write(Buffer.from(attachment.data));
await stream.close();
}
async function writeAttachments(dir, name, message, attachments) {
const promises = _.map(
attachments,
(attachment, index) => writeAttachment(dir, message, index, attachment)
);
try {
await Promise.all(promises);
} catch (error) {
console.log(
'writeAttachments: error exporting conversation',
name,
':',
error && error.stack ? error.stack : error
);
throw error;
}
}
function sanitizeFileName(filename) {
return filename.toString().replace(/[^a-z0-9.,+()'#\- ]/gi, '_');
}
async function exportConversation(db, conversation, options) {
options = options || {};
const {
name,
dir,
attachmentsDir,
} = 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!');
}
console.log('exporting conversation', name);
const writer = await createFileAndWriter(dir, 'messages.json');
return new Promise(async (resolve, reject) => {
const transaction = db.transaction('messages', 'readwrite');
transaction.onerror = () => {
Whisper.Database.handleDOMException(
`exportConversation transaction error (conversation: ${name})`,
transaction.error,
reject
);
};
transaction.oncomplete = () => {
// this doesn't really mean anything - we may have attachment processing to do
};
const store = transaction.objectStore('messages');
const index = store.index('conversation');
const range = window.IDBKeyRange.bound(
[conversation.id, 0],
[conversation.id, Number.MAX_VALUE]
);
let promiseChain = Promise.resolve();
let count = 0;
const request = index.openCursor(range);
const stream = createOutputStream(writer);
stream.write('{"messages":[');
request.onerror = () => {
Whisper.Database.handleDOMException(
`exportConversation request error (conversation: ${name})`,
request.error,
reject
);
};
request.onsuccess = async (event) => {
const cursor = event.target.result;
if (cursor) {
const message = cursor.value;
const { attachments } = message;
// skip message if it is disappearing, no matter the amount of time left
if (message.expireTimer) {
cursor.continue();
return;
}
if (count !== 0) {
stream.write(',');
}
// eliminate attachment data from the JSON, since it will go to disk
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) {
const exportAttachments = () =>
writeAttachments(attachmentsDir, name, message, attachments);
// eslint-disable-next-line more/no-then
promiseChain = promiseChain.then(exportAttachments);
}
count += 1;
cursor.continue();
} else {
try {
await Promise.all([
stream.write(']}'),
promiseChain,
stream.close(),
]);
} catch (error) {
console.log(
'exportConversation: error exporting conversation',
name,
':',
error && error.stack ? error.stack : error
);
reject(error);
return;
}
console.log('done exporting conversation', name);
resolve();
}
};
});
}
// 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 || 'never';
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 || 'never';
if (conversation.type === 'private') {
name += ` (${conversation.id})`;
} else {
name += ` ([REDACTED_GROUP]${conversation.id.slice(-3)})`;
}
return name;
}
function exportConversations(db, options) {
options = options || {};
const {
messagesDir,
attachmentsDir,
} = options;
if (!messagesDir) {
return Promise.reject(new Error('Need a messages directory!'));
}
if (!attachmentsDir) {
return Promise.reject(new Error('Need an attachments directory!'));
}
return new Promise((resolve, reject) => {
const transaction = db.transaction('conversations', 'readwrite');
transaction.onerror = () => {
Whisper.Database.handleDOMException(
'exportConversations transaction error',
transaction.error,
reject
);
};
transaction.oncomplete = () => {
// not really very useful - fires at unexpected times
};
let promiseChain = Promise.resolve();
const store = transaction.objectStore('conversations');
const request = store.openCursor();
request.onerror = () => {
Whisper.Database.handleDOMException(
'exportConversations request error',
request.error,
reject
);
};
request.onsuccess = async (event) => {
const cursor = event.target.result;
if (cursor && cursor.value) {
const conversation = cursor.value;
const dirName = getConversationDirName(conversation);
const name = getConversationLoggingName(conversation);
const process = async () => {
const dir = await createDirectory(messagesDir, dirName);
return exportConversation(db, conversation, {
name,
dir,
attachmentsDir,
});
};
console.log('scheduling export for conversation', name);
// eslint-disable-next-line more/no-then
promiseChain = promiseChain.then(process);
cursor.continue();
} else {
console.log('Done scheduling conversation exports');
try {
await promiseChain;
} catch (error) {
reject(error);
return;
}
resolve();
}
};
});
}
function getDirectory(options) {
return new Promise((resolve, reject) => {
const browserWindow = BrowserWindow.getFocusedWindow();
const dialogOptions = {
title: options.title,
properties: ['openDirectory'],
buttonLabel: options.buttonLabel,
};
dialog.showOpenDialog(browserWindow, dialogOptions, (directory) => {
if (!directory || !directory[0]) {
const error = new Error('Error choosing directory');
error.name = 'ChooseError';
return reject(error);
}
return resolve(directory[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);
});
});
}
function loadAttachments(dir, message, getName) {
const promises = _.map(message.attachments, (attachment, index) => {
const name = getName(message, index, attachment);
return readAttachment(dir, attachment, name);
});
return Promise.all(promises);
}
function saveMessage(db, message) {
return saveAllMessages(db, [message]);
}
function saveAllMessages(db, messages) {
if (!messages.length) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
let finished = false;
const finish = (via) => {
console.log('messages done saving via', via);
if (finished) {
resolve();
}
finished = true;
};
const transaction = db.transaction('messages', 'readwrite');
transaction.onerror = () => {
Whisper.Database.handleDOMException(
'saveAllMessages transaction error',
transaction.error,
reject
);
};
transaction.oncomplete = finish.bind(null, 'transaction complete');
const store = transaction.objectStore('messages');
const { conversationId } = messages[0];
let count = 0;
_.forEach(messages, (message) => {
const request = store.put(message, message.id);
request.onsuccess = () => {
count += 1;
if (count === messages.length) {
console.log(
'Saved',
messages.length,
'messages for conversation',
// Don't know if group or private conversation, so we blindly redact
`[REDACTED]${conversationId.slice(-3)}`
);
finish('puts scheduled');
}
};
request.onerror = () => {
Whisper.Database.handleDOMException(
'saveAllMessages request error',
request.error,
reject
);
};
});
});
}
// 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(db, dir, options) {
options = options || {};
_.defaults(options, { messageLookup: {} });
const {
messageLookup,
attachmentsDir,
} = options;
let conversationId = 'unknown';
let total = 0;
let skipped = 0;
let contents;
try {
contents = await readFileAsText(dir, 'messages.json');
} catch (error) {
console.log(`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;
}
if (message.attachments && message.attachments.length) {
const importMessage = async () => {
const getName = attachmentsDir
? getAnonymousAttachmentFileName
: getExportAttachmentFileName;
const parent = attachmentsDir || path.join(dir, message.received_at.toString());
await loadAttachments(parent, message, getName);
return saveMessage(db, message);
};
// eslint-disable-next-line more/no-then
promiseChain = promiseChain.then(importMessage);
return false;
}
return true;
});
if (messages.length > 0) {
await saveAllMessages(db, messages);
}
await promiseChain;
console.log(
'Finished importing conversation',
conversationId,
'Total:',
total,
'Skipped:',
skipped
);
}
async function importConversations(db, dir, options) {
const contents = await getDirContents(dir);
let promiseChain = Promise.resolve();
_.forEach(contents, (conversationDir) => {
if (!fs.statSync(conversationDir).isDirectory()) {
return;
}
const loadConversation = () => importConversation(db, 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 source = message.source || ourNumber;
if (source === ourNumber) {
return `${source} ${message.timestamp}`;
}
const sourceDevice = message.sourceDevice || 1;
return `${source}.${sourceDevice} ${message.timestamp}`;
}
function loadMessagesLookup(db) {
return assembleLookup(db, 'messages', getMessageKey);
}
function getConversationKey(conversation) {
return conversation.id;
}
function loadConversationLookup(db) {
return assembleLookup(db, 'conversations', getConversationKey);
}
function getGroupKey(group) {
return group.id;
}
function loadGroupsLookup(db) {
return assembleLookup(db, 'groups', getGroupKey);
}
function assembleLookup(db, storeName, keyFunction) {
const lookup = Object.create(null);
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
transaction.onerror = () => {
Whisper.Database.handleDOMException(
`assembleLookup(${storeName}) transaction error`,
transaction.error,
reject
);
};
transaction.oncomplete = () => {
// not really very useful - fires at unexpected times
};
const store = transaction.objectStore(storeName);
const request = store.openCursor();
request.onerror = () => {
Whisper.Database.handleDOMException(
`assembleLookup(${storeName}) request error`,
request.error,
reject
);
};
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor && cursor.value) {
lookup[keyFunction(cursor.value)] = true;
cursor.continue();
} else {
console.log(`Done creating ${storeName} lookup`);
resolve(lookup);
}
};
});
}
function getDirectoryForExport() {
const options = {
title: i18n('exportChooserTitle'),
buttonLabel: i18n('exportButton'),
};
return getDirectory(options);
}
function createZip(zipDir, targetDir) {
return new Promise((resolve, reject) => {
const target = path.join(zipDir, 'messages.zip');
const output = fs.createWriteStream(target);
const archive = archiver('zip', {
cwd: targetDir,
});
output.on('close', () => {
resolve(target);
});
archive.on('warning', (error) => {
console.log(`Archive generation warning: ${error.stack}`);
});
archive.on('error', reject);
archive.pipe(output);
archive.directory(targetDir, '');
archive.finalize();
});
}
function createTempDir() {
return pify(tmp.dir)();
}
function deleteAll(pattern) {
console.log(`Deleting ${pattern}`);
return pify(rimraf)(pattern);
}
async function backupToDirectory(directory) {
let tempDir;
try {
tempDir = await createTempDir();
const db = await Whisper.Database.open();
const attachmentsDir = await createDirectory(directory, 'attachments');
await exportContactAndGroupsToFile(db, tempDir);
await exportConversations(db, {
messagesDir: tempDir,
attachmentsDir,
});
await createZip(directory, tempDir);
// now that we've made the zip file, we can delete the temp messages directory
await deleteAll(tempDir);
tempDir = null;
console.log('done backing up!');
return directory;
} catch (error) {
console.log(
'the backup went wrong:',
error && error.stack ? error.stack : error
);
throw error;
} finally {
if (tempDir) {
await deleteAll(tempDir);
}
}
}
function getDirectoryForImport() {
const options = {
title: i18n('importChooserTitle'),
buttonLabel: i18n('importButton'),
};
return getDirectory(options);
}
async function importFromDirectory(directory, options) {
options = options || {};
try {
const db = await Whisper.Database.open();
const lookups = await Promise.all([
loadMessagesLookup(db),
loadConversationLookup(db),
loadGroupsLookup(db),
]);
const [messageLookup, conversationLookup, groupLookup] = lookups;
options = Object.assign({}, options, {
messageLookup,
conversationLookup,
groupLookup,
});
const zipPath = path.join(directory, 'messages.zip');
if (fs.existsSync(zipPath)) {
// we're in the world of an encrypted, zipped backup
let tempDir;
try {
tempDir = await createTempDir();
const attachmentsDir = path.join(directory, 'attachments');
await decompress(zipPath, tempDir);
options = Object.assign({}, options, {
attachmentsDir,
});
const result = await importNonMessages(db, tempDir, options);
await importConversations(db, tempDir, options);
console.log('done importing from backup!');
return result;
} finally {
if (tempDir) {
await deleteAll(tempDir);
}
}
}
const result = await importNonMessages(db, directory, options);
await importConversations(db, directory, options);
console.log('done importing!');
return result;
} catch (error) {
console.log(
'the import went wrong:',
error && error.stack ? error.stack : error
);
throw error;
}
}