diff --git a/js/backup.js b/js/backup.js index e7d01d99f..ffe33933a 100644 --- a/js/backup.js +++ b/js/backup.js @@ -7,7 +7,7 @@ /* eslint-env node */ -/* eslint-disable no-param-reassign, more/no-then, guard-for-in */ +/* eslint-disable no-param-reassign, guard-for-in */ 'use strict'; @@ -65,6 +65,7 @@ const { 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(); @@ -80,12 +81,13 @@ const { }))); return wait; }, - close() { - return wait.then(() => new Promise(((resolve, reject) => { + async close() { + await wait; + return new Promise(((resolve, reject) => { writer.once('finish', resolve); writer.once('error', reject); writer.end(); - }))); + })); }, }; } @@ -149,7 +151,7 @@ const { reject ); }; - request.onsuccess = (event) => { + request.onsuccess = async (event) => { if (count === 0) { console.log('cursor opened'); stream.write(`"${storeName}": [`); @@ -176,10 +178,9 @@ const { console.log('Exported all stores'); stream.write('}'); - stream.close().then(() => { - console.log('Finished writing all stores to disk'); - resolve(); - }); + await stream.close(); + console.log('Finished writing all stores to disk'); + resolve(); } } }; @@ -417,26 +418,20 @@ const { return name; } - function readAttachment(parent, message, attachment) { - return new Promise(((resolve, reject) => { - const name = getAttachmentFileName(attachment); - const sanitized = sanitizeFileName(name); - const attachmentDir = path.join(parent, message.received_at.toString()); + async function readAttachment(parent, message, attachment) { + const name = getAttachmentFileName(attachment); + const sanitized = sanitizeFileName(name); + const attachmentDir = path.join(parent, message.received_at.toString()); - return readFileAsArrayBuffer(attachmentDir, sanitized).then((contents) => { - attachment.data = contents; - return resolve(); - }, reject); - })); + attachment.data = await readFileAsArrayBuffer(attachmentDir, sanitized); } - function writeAttachment(dir, attachment) { + async function writeAttachment(dir, attachment) { const filename = getAttachmentFileName(attachment); - return createFileAndWriter(dir, filename).then((writer) => { - const stream = createOutputStream(writer); - stream.write(Buffer.from(attachment.data)); - return stream.close(); - }); + const writer = await createFileAndWriter(dir, filename); + const stream = createOutputStream(writer); + stream.write(Buffer.from(attachment.data)); + return stream.close(); } async function writeAttachments(parentDir, name, messageId, attachments) { @@ -459,11 +454,10 @@ const { return filename.toString().replace(/[^a-z0-9.,+()'#\- ]/gi, '_'); } - function exportConversation(db, name, conversation, dir) { + async function exportConversation(db, name, conversation, dir) { console.log('exporting conversation', name); - const writerPromise = createFileAndWriter(dir, 'messages.json'); - - return writerPromise.then(writer => new Promise(((resolve, reject) => { + const writer = await createFileAndWriter(dir, 'messages.json'); + return new Promise(((resolve, reject) => { const transaction = db.transaction('messages', 'readwrite'); transaction.onerror = () => { Whisper.Database.handleDOMException( @@ -497,7 +491,7 @@ const { reject ); }; - request.onsuccess = (event) => { + request.onsuccess = async (event) => { const cursor = event.target.result; if (cursor) { const message = cursor.value; @@ -524,31 +518,35 @@ const { if (attachments && attachments.length) { const process = () => writeAttachments(dir, name, messageId, attachments); + // eslint-disable-next-line more/no-then promiseChain = promiseChain.then(process); } count += 1; cursor.continue(); } else { - stream.write(']}'); - - const promise = stream.close(); - - promiseChain.then(promise).then(() => { - console.log('done exporting conversation', name); - return resolve(); - }, (error) => { + try { + await Promise.all([ + stream.write(']}'), + promiseChain, + stream.close(), + ]); + } catch (error) { console.log( 'exportConversation: error exporting conversation', name, ':', error && error.stack ? error.stack : error ); - return reject(error); - }); + reject(error); + return; + } + + console.log('done exporting conversation', name); + resolve(); } }; - }))); + })); } // Goals for directory names: @@ -602,7 +600,7 @@ const { reject ); }; - request.onsuccess = (event) => { + request.onsuccess = async (event) => { const cursor = event.target.result; if (cursor && cursor.value) { const conversation = cursor.value; @@ -615,11 +613,18 @@ const { }; 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'); - promiseChain.then(resolve, reject); + try { + await promiseChain; + } catch (error) { + reject(error); + return; + } + resolve(); } }; })); @@ -734,7 +739,7 @@ const { // 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. - function importConversation(db, dir, options) { + async function importConversation(db, dir, options) { options = options || {}; _.defaults(options, { messageLookup: {} }); @@ -742,76 +747,77 @@ const { let conversationId = 'unknown'; let total = 0; let skipped = 0; + let contents; - return readFileAsText(dir, 'messages.json').then((contents) => { - 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; + try { + contents = await readFileAsText(dir, 'messages.json'); + } catch (error) { + console.log(`Warning: could not access messages.json in directory: ${dir}`); + } - const messages = _.filter(json.messages, (message) => { - message = unstringify(message); + let promiseChain = Promise.resolve(); - if (messageLookup[getMessageKey(message)]) { - skipped += 1; - return false; - } + const json = JSON.parse(contents); + if (json.messages && json.messages.length) { + conversationId = `[REDACTED]${(json.messages[0].conversationId || '').slice(-3)}`; + } + total = json.messages.length; - if (message.attachments && message.attachments.length) { - const process = async () => { - await loadAttachments(dir, message); - return saveMessage(db, message); - }; + const messages = _.filter(json.messages, (message) => { + message = unstringify(message); - promiseChain = promiseChain.then(process); + if (messageLookup[getMessageKey(message)]) { + skipped += 1; + return false; + } - return false; - } + if (message.attachments && message.attachments.length) { + const process = async () => { + await loadAttachments(dir, message); + return saveMessage(db, message); + }; - return true; - }); + // eslint-disable-next-line more/no-then + promiseChain = promiseChain.then(process); - let promise = Promise.resolve(); - if (messages.length > 0) { - promise = saveAllMessages(db, messages); + return false; } - return promise - .then(() => promiseChain) - .then(() => { - console.log( - 'Finished importing conversation', - conversationId, - 'Total:', - total, - 'Skipped:', - skipped - ); - }); - }, () => { - console.log(`Warning: could not access messages.json in directory: ${dir}`); + return true; }); + + if (messages.length > 0) { + await saveAllMessages(db, messages); + } + + await promiseChain; + console.log( + 'Finished importing conversation', + conversationId, + 'Total:', + total, + 'Skipped:', + skipped + ); } - function importConversations(db, dir, options) { - return getDirContents(dir).then((contents) => { - let promiseChain = Promise.resolve(); + async function importConversations(db, dir, options) { + const contents = await getDirContents(dir); - _.forEach(contents, (conversationDir) => { - if (!fs.statSync(conversationDir).isDirectory()) { - return; - } + let promiseChain = Promise.resolve(); - const process = () => importConversation(db, conversationDir, options); + _.forEach(contents, (conversationDir) => { + if (!fs.statSync(conversationDir).isDirectory()) { + return; + } - promiseChain = promiseChain.then(process); - }); + const process = () => importConversation(db, conversationDir, options); - return promiseChain; + // eslint-disable-next-line more/no-then + promiseChain = promiseChain.then(process); }); + + return promiseChain; } function getMessageKey(message) { @@ -894,28 +900,23 @@ const { }; return getDirectory(options); }, - exportToDirectory(directory, options) { - let dir; - let db; - return Whisper.Database.open().then((openedDb) => { - db = openedDb; - const name = `Signal Export ${getTimestamp()}`; - return createDirectory(directory, name); - }).then((created) => { - dir = created; - return exportNonMessages(db, dir, options); - }).then(() => exportConversations(db, dir)) - .then(() => dir) - .then((targetPath) => { - console.log('done backing up!'); - return targetPath; - }, (error) => { - console.log( - 'the backup went wrong:', - error && error.stack ? error.stack : error - ); - return Promise.reject(error); - }); + async exportToDirectory(directory, options) { + const name = `Signal Export ${getTimestamp()}`; + try { + const db = await Whisper.Database.open(); + const dir = await createDirectory(directory, name); + await exportNonMessages(db, dir, options); + await exportConversations(db, dir); + + console.log('done backing up!'); + return dir; + } catch (error) { + console.log( + 'the backup went wrong:', + error && error.stack ? error.stack : error + ); + throw error; + } }, getDirectoryForImport() { const options = { @@ -924,41 +925,34 @@ const { }; return getDirectory(options); }, - importFromDirectory(directory, options) { + async importFromDirectory(directory, options) { options = options || {}; - let db; - let nonMessageResult; - return Whisper.Database.open().then((createdDb) => { - db = createdDb; - - return Promise.all([ + try { + const db = await Whisper.Database.open(); + const lookups = await Promise.all([ loadMessagesLookup(db), loadConversationLookup(db), loadGroupsLookup(db), ]); - }).then((lookups) => { const [messageLookup, conversationLookup, groupLookup] = lookups; options = Object.assign({}, options, { messageLookup, conversationLookup, groupLookup, }); - }).then(() => importNonMessages(db, directory, options)) - .then((result) => { - nonMessageResult = result; - return importConversations(db, directory, options); - }) - .then(() => { - console.log('done restoring from backup!'); - return nonMessageResult; - }, (error) => { - console.log( - 'the import went wrong:', - error && error.stack ? error.stack : error - ); - return Promise.reject(error); - }); + + const result = await importNonMessages(db, directory, options); + await importConversations(db, directory, options); + console.log('done restoring from backup!'); + return result; + } catch (error) { + console.log( + 'the import went wrong:', + error && error.stack ? error.stack : error + ); + throw error; + } }, // for testing sanitizeFileName,