mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-02-21 02:07:02 +00:00
Export: Fixes and debugging, Migration: show install step first (#1697)
* 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
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
42
js/backup.js
42
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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user