Skip to content

Commit

Permalink
Export: Fixes and debugging, Migration: show install step first (#1697)
Browse files Browse the repository at this point in the history
* Export: Allow for duplicate folders and filenames

This should account for messages with duplicate received_at times. It is
unlikely that their attachment filenames overlap as well, but in that
case the more recent attachment will win. On import, both messages will
end up with the same file.

* Export: Throw informative error if message had no received_at

* Migration: First step is to install the new signal desktop

Once that's installed, we move on to the choose directory step, which
actually does the export. You can cancel out of the first step if you
can't install the new Signal Desktop at the moment.

Also: This removes the cancel button on the 'complete' step, since it
has the potential to very easily cause conflicts between the new Signal
Desktop and the chrome app.

* Refine our duplicate resilience: throw on dupe in some cases

* Migration: Remove later step install buttons; should be complete

* Export: Check type of data destined for disk, throw error
  • Loading branch information
scottnonnenberg authored Nov 3, 2017
1 parent 9585f14 commit adb30e6
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 14 deletions.
10 changes: 9 additions & 1 deletion _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
3 changes: 3 additions & 0 deletions background.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
{{ #installButton }}
<button class='install grey'>{{ installButton }}</button>
{{ /installButton }}
{{ #nextButton }}
<button class='next grey'>{{ nextButton }}</button>
{{ /nextButton }}
{{ #exportButton }}
<button class='export grey'>{{ exportButton }}</button>
{{ /exportButton }}
Expand Down
42 changes: 32 additions & 10 deletions js/backup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand Down Expand Up @@ -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);
});
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}));
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
};
Expand Down Expand Up @@ -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);
Expand Down
23 changes: 20 additions & 3 deletions js/views/migration_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
var State = {
DISCONNECTING: 1,
EXPORTING: 2,
COMPLETE: 3
COMPLETE: 3,
CHOOSE_DIR: 4,
};

Whisper.Migration = {
Expand Down Expand Up @@ -53,6 +54,7 @@
'click .export': 'onClickExport',
'click .debug-log': 'onClickDebugLog',
'click .cancel': 'onClickCancel',
'click .next': 'onClickNext',
},
initialize: function() {
if (!Whisper.Migration.inProgress()) {
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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() {
Expand Down

0 comments on commit adb30e6

Please sign in to comment.