diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 11760a7d39..0e26d9e869 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -15,12 +15,20 @@ "message": "Loading...", "description": "Message shown on the loading screen before we've loaded any messages" }, + "installComplete": { + "message": "Install is complete", + "description": "Button to click when user has installed the new Signal Desktop" + }, "migrationWarning": { "message": "The Signal Desktop Chrome app has been deprecated. Would you like to migrate to the new Signal Desktop now?", "description": "Warning notification that this version of the app has been deprecated and the user must migrate" }, + "migrateInstallStep": { + "message": "The first step is to install the new Signal Desktop.", + "description": "The first step in the export process; installing the standalone desktop app, to ensure it is available for that platform." + }, "exportInstructions": { - "message": "The first step is to choose a directory to store this application's exported data. It will contain your message history and sensitive cryptographic data, so be sure to save it somewhere private.", + "message": "Now, choose a directory to store this application's exported data. It will contain your message history and sensitive cryptographic data, so be sure to save it somewhere private.", "description": "Description of the export process" }, "migrate": { diff --git a/background.html b/background.html index 5ef417c85e..5f42e88e65 100644 --- a/background.html +++ b/background.html @@ -17,6 +17,9 @@ {{ #installButton }} {{ /installButton }} + {{ #nextButton }} + + {{ /nextButton }} {{ #exportButton }} {{ /exportButton }} diff --git a/js/backup.js b/js/backup.js index 38b10da06f..ab44dd37cc 100644 --- a/js/backup.js +++ b/js/backup.js @@ -3,6 +3,9 @@ window.Whisper = window.Whisper || {}; function stringToBlob(string) { + if (!string || (typeof string !== 'string' && !(string instanceof ArrayBuffer))) { + throw new Error('stringToBlob: provided value is something strange:', string, JSON.stringify(stringify(string))); + } var buffer = dcodeIO.ByteBuffer.wrap(string).toArrayBuffer(); return new Blob([buffer]); } @@ -61,7 +64,9 @@ } function exportNonMessages(idb_db, parent) { - return createFileAndWriter(parent, 'db.json').then(function(writer) { + // We wouldn't want to overwrite another db file. + var exclusive = true; + return createFileAndWriter(parent, 'db.json', exclusive).then(function(writer) { return exportToJsonFile(idb_db, writer); }); } @@ -223,19 +228,19 @@ }); } - function createDirectory(parent, name) { + function createDirectory(parent, name, exclusive) { var sanitized = sanitizeFileName(name); console._log('-- about to create directory', sanitized); return new Promise(function(resolve, reject) { - parent.getDirectory(sanitized, {create: true, exclusive: true}, resolve, reject); + parent.getDirectory(sanitized, {create: true, exclusive: exclusive}, resolve, reject); }); } - function createFileAndWriter(parent, name) { + function createFileAndWriter(parent, name, exclusive) { var sanitized = sanitizeFileName(name); console._log('-- about to create file', sanitized); return new Promise(function(resolve, reject) { - parent.getFile(sanitized, {create: true, exclusive: true}, function(file) { + parent.getFile(sanitized, {create: true, exclusive: exclusive}, function(file) { return file.createWriter(function(writer) { resolve(writer); }, reject); @@ -322,14 +327,21 @@ function writeAttachment(dir, attachment) { var filename = getAttachmentFileName(attachment); - return createFileAndWriter(dir, filename).then(function(writer) { + // If attachments are in messages with the same received_at and the same name, + // then we'll let that overwrite happen. It should be very uncommon. + var exclusive = false; + return createFileAndWriter(dir, filename, exclusive).then(function(writer) { var stream = createOutputStream(writer); return stream.write(attachment.data); }); } function writeAttachments(parentDir, name, messageId, attachments) { - return createDirectory(parentDir, messageId).then(function(dir) { + // We've had a lot of trouble with attachments, likely due to messages with the same + // received_at in the same conversation. So we sacrifice one of the attachments in + // this unusual case. + var exclusive = false; + return createDirectory(parentDir, messageId, exclusive).then(function(dir) { return Promise.all(_.map(attachments, function(attachment) { return writeAttachment(dir, attachment); })); @@ -350,7 +362,9 @@ function exportConversation(idb_db, name, conversation, dir) { console.log('exporting conversation', name); - return createFileAndWriter(dir, 'messages.json').then(function(writer) { + // We wouldn't want to overwrite the contents of a different conversation. + var exclusive = true; + return createFileAndWriter(dir, 'messages.json', exclusive).then(function(writer) { return new Promise(function(resolve, reject) { var transaction = idb_db.transaction('messages', "readwrite"); transaction.onerror = function(e) { @@ -407,6 +421,9 @@ if (attachments && attachments.length) { var process = function() { console._log('-- writing attachments for message', message.id); + if (!message.received_at) { + return Promise.reject(new Error('Message', message.id, 'had no received_at')); + } return writeAttachments(dir, name, messageId, attachments); }; promiseChain = promiseChain.then(process); @@ -496,7 +513,10 @@ var name = getConversationLoggingName(conversation); var process = function() { - return createDirectory(parentDir, dir).then(function(dir) { + // If we have a conversation directory collision, the user will lose the + // contents of the first conversation. So we throw an error. + var exclusive = true; + return createDirectory(parentDir, dir, exclusive).then(function(dir) { return exportConversation(idb_db, name, conversation, dir); }); }; @@ -667,7 +687,9 @@ return openDatabase().then(function(idb_db) { idb = idb_db; var name = 'Signal Export ' + getTimestamp(); - return createDirectory(directoryEntry, name); + // We don't want to overwrite another signal export, so we set exclusive = true + var exclusive = true; + return createDirectory(directoryEntry, name, exclusive); }).then(function(directory) { dir = directory; return exportNonMessages(idb, dir); diff --git a/js/views/migration_view.js b/js/views/migration_view.js index e9d954849b..6219253413 100644 --- a/js/views/migration_view.js +++ b/js/views/migration_view.js @@ -5,7 +5,8 @@ var State = { DISCONNECTING: 1, EXPORTING: 2, - COMPLETE: 3 + COMPLETE: 3, + CHOOSE_DIR: 4, }; Whisper.Migration = { @@ -53,6 +54,7 @@ 'click .export': 'onClickExport', 'click .debug-log': 'onClickDebugLog', 'click .cancel': 'onClickCancel', + 'click .next': 'onClickNext', }, initialize: function() { if (!Whisper.Migration.inProgress()) { @@ -77,6 +79,7 @@ var debugLogButton = i18n('submitDebugLog'); var installButton = i18n('installNewSignal'); var cancelButton; + var nextButton; if (this.error) { // If we've never successfully exported, then we allow user to cancel out @@ -98,22 +101,31 @@ var location = Whisper.Migration.getExportLocation() || i18n('selectedLocation'); message = i18n('exportComplete', location); exportButton = i18n('exportAgain'); + installButton = null; debugLogButton = null; - cancelButton = i18n('cancelMigration'); break; case State.EXPORTING: message = i18n('exporting'); + installButton = null; break; case State.DISCONNECTING: message = i18n('migrationDisconnecting'); installButton = null; break; - default: + case State.CHOOSE_DIR: hideProgress = true; message = i18n('exportInstructions'); exportButton = i18n('export'); debugLogButton = null; installButton = null; + break; + default: + message = i18n('migrateInstallStep'); + hideProgress = true; + debugLogButton = null; + nextButton = i18n('installComplete'); + cancelButton = i18n('cancel'); + break; } return { @@ -123,12 +135,17 @@ debugLogButton: debugLogButton, installButton: installButton, cancelButton: cancelButton, + nextButton: nextButton, }; }, onClickInstall: function() { var url = 'https://support.whispersystems.org/hc/en-us/articles/214507138'; window.open(url, '_blank'); }, + onClickNext: function() { + storage.put('migrationState', State.CHOOSE_DIR); + this.render(); + }, cancel: function() { console.log('Cancelling out of migration workflow after error'); Whisper.Migration.cancel().then(function() {