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() {