diff --git a/_locales/en/messages.json b/_locales/en/messages.json index bb5868492..056f45537 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1120,6 +1120,14 @@ "message": "Disconnecting and deleting all data", "description": "Message shown to user when app is disconnected and data deleted" }, + "deleteOldIndexedDBData": { + "message": "You have obsolete data from a prior installation of Signal Desktop. If you choose to continue, it will be deleted and you will start from scratch.", + "description": "Shown if user last ran Signal Desktop before October 2018" + }, + "deleteOldData": { + "message": "Delete Old Data", + "description": "Button to make the delete happen" + }, "notifications": { "message": "Notifications", "description": "Header for notification settings" diff --git a/background.html b/background.html index fc1eb480b..75ea6863a 100644 --- a/background.html +++ b/background.html @@ -344,7 +344,6 @@ - diff --git a/js/background.js b/js/background.js index df1b7fd7e..14694c55e 100644 --- a/js/background.js +++ b/js/background.js @@ -190,10 +190,7 @@ const { IdleDetector, MessageDataMigrator } = Signal.Workflow; const { - mandatoryMessageUpgrade, - migrateAllToSQLCipher, - removeDatabase, - runMigrations, + removeDatabase: removeIndexedDB, doesDatabaseExist, } = Signal.IndexedDB; const { Errors, Message } = window.Signal.Types; @@ -205,11 +202,6 @@ } = window.Signal.Migrations; const { Views } = window.Signal; - // Implicitly used in `indexeddb-backbonejs-adapter`: - // https://github.com/signalapp/Signal-Desktop/blob/4033a9f8137e62ed286170ed5d4941982b1d3a64/components/indexeddb-backbonejs-adapter/backbone-indexeddb.js#L569 - window.onInvalidStateError = error => - window.log.error(error && error.stack ? error.stack : error); - window.log.info('background page reloaded'); window.log.info('environment:', window.getEnvironment()); @@ -267,13 +259,56 @@ const cancelInitializationMessage = Views.Initialization.setMessage(); const version = await window.Signal.Data.getItemById('version'); - let isIndexedDBPresent = false; if (!version) { - isIndexedDBPresent = await doesDatabaseExist(); + const isIndexedDBPresent = await doesDatabaseExist(); if (isIndexedDBPresent) { - window.installStorage(window.legacyStorage); - window.log.info('Start IndexedDB migrations'); - await runMigrations(); + window.log.info('Found IndexedDB database.'); + try { + window.log.info('Confirming deletion of old data with user...'); + + try { + await new Promise((resolve, reject) => { + const dialog = new Whisper.ConfirmationDialogView({ + message: window.i18n('deleteOldIndexedDBData'), + okText: window.i18n('deleteOldData'), + cancelText: window.i18n('quit'), + resolve, + reject, + }); + document.body.append(dialog.el); + dialog.focusCancel(); + }); + } catch (error) { + window.log.info( + 'User chose not to delete old data. Shutting down.', + error && error.stack ? error.stack : error + ); + window.shutdown(); + return; + } + + window.log.info('Deleting all previously-migrated data in SQL...'); + window.log.info('Deleting IndexedDB file...'); + + await Promise.all([ + removeIndexedDB(), + window.Signal.Data.removeAll(), + window.Signal.Data.removeIndexedDBFiles(), + ]); + window.log.info('Done with SQL deletion and IndexedDB file deletion.'); + } catch (error) { + window.log.error( + 'Failed to remove IndexedDB file or remove SQL data:', + error && error.stack ? error.stack : error + ); + } + + // Set a flag to delete IndexedDB on next startup if it wasn't deleted just now. + // We need to use direct data calls, since storage isn't ready yet. + await window.Signal.Data.createOrUpdateItem({ + id: 'indexeddb-delete-needed', + value: true, + }); } } @@ -424,24 +459,6 @@ }, }; - if (isIndexedDBPresent) { - await mandatoryMessageUpgrade({ upgradeMessageSchema }); - await migrateAllToSQLCipher({ writeNewAttachmentData, Views }); - await removeDatabase(); - try { - await window.Signal.Data.removeIndexedDBFiles(); - } catch (error) { - window.log.error( - 'Failed to remove IndexedDB files:', - error && error.stack ? error.stack : error - ); - } - - window.installStorage(window.newStorage); - await window.storage.fetch(); - await storage.put('indexeddb-delete-needed', true); - } - // How long since we were last running? const now = Date.now(); const lastHeartbeat = storage.get('lastHeartbeat'); diff --git a/js/database.js b/js/database.js index b9f66da1f..c78db08c9 100644 --- a/js/database.js +++ b/js/database.js @@ -1,14 +1,9 @@ -/* global _: false */ -/* global Backbone: false */ - /* global Whisper: false */ // eslint-disable-next-line func-names (function() { 'use strict'; - const { getPlaceholderMigrations } = window.Signal.Migrations; - window.Whisper = window.Whisper || {}; window.Whisper.Database = window.Whisper.Database || {}; window.Whisper.Database.id = window.Whisper.Database.id || 'signal'; @@ -23,108 +18,4 @@ ); reject(error || new Error(prefix)); }; - - function clearStores(db, names) { - return new Promise((resolve, reject) => { - const storeNames = names || db.objectStoreNames; - window.log.info('Clearing these indexeddb stores:', storeNames); - const transaction = db.transaction(storeNames, 'readwrite'); - - let finished = false; - const finish = via => { - window.log.info('clearing all stores done via', via); - if (finished) { - resolve(); - } - finished = true; - }; - - transaction.oncomplete = finish.bind(null, 'transaction complete'); - transaction.onerror = () => { - Whisper.Database.handleDOMException( - 'clearStores transaction error', - transaction.error, - reject - ); - }; - - let count = 0; - - // can't use built-in .forEach because db.objectStoreNames is not a plain array - _.forEach(storeNames, storeName => { - const store = transaction.objectStore(storeName); - const request = store.clear(); - - request.onsuccess = () => { - count += 1; - window.log.info('Done clearing store', storeName); - - if (count >= storeNames.length) { - window.log.info('Done clearing indexeddb stores'); - finish('clears complete'); - } - }; - - request.onerror = () => { - Whisper.Database.handleDOMException( - 'clearStores request error', - request.error, - reject - ); - }; - }); - }); - } - - Whisper.Database.open = () => { - const { migrations } = Whisper.Database; - const { version } = migrations[migrations.length - 1]; - const DBOpenRequest = window.indexedDB.open(Whisper.Database.id, version); - - return new Promise((resolve, reject) => { - // these two event handlers act on the IDBDatabase object, - // when the database is opened successfully, or not - DBOpenRequest.onerror = reject; - DBOpenRequest.onsuccess = () => resolve(DBOpenRequest.result); - - // This event handles the event whereby a new version of - // the database needs to be created Either one has not - // been created before, or a new version number has been - // submitted via the window.indexedDB.open line above - DBOpenRequest.onupgradeneeded = reject; - }); - }; - - Whisper.Database.clear = async () => { - const db = await Whisper.Database.open(); - await clearStores(db); - db.close(); - }; - - Whisper.Database.clearStores = async storeNames => { - const db = await Whisper.Database.open(); - await clearStores(db, storeNames); - db.close(); - }; - - Whisper.Database.close = () => window.wrapDeferred(Backbone.sync('closeall')); - - Whisper.Database.drop = () => - new Promise((resolve, reject) => { - const request = window.indexedDB.deleteDatabase(Whisper.Database.id); - - request.onblocked = () => { - reject(new Error('Error deleting database: Blocked.')); - }; - request.onupgradeneeded = () => { - reject(new Error('Error deleting database: Upgrade needed.')); - }; - request.onerror = () => { - reject(new Error('Error deleting database.')); - }; - - request.onsuccess = resolve; - }); - - Whisper.Database.migrations = getPlaceholderMigrations(); })(); diff --git a/js/legacy_storage.js b/js/legacy_storage.js deleted file mode 100644 index 5a1f1e2d8..000000000 --- a/js/legacy_storage.js +++ /dev/null @@ -1,92 +0,0 @@ -/* global Backbone, Whisper */ - -/* eslint-disable more/no-then */ - -// eslint-disable-next-line func-names -(function() { - 'use strict'; - - window.Whisper = window.Whisper || {}; - const Item = Backbone.Model.extend({ - database: Whisper.Database, - storeName: 'items', - }); - const ItemCollection = Backbone.Collection.extend({ - model: Item, - storeName: 'items', - database: Whisper.Database, - }); - - let ready = false; - const items = new ItemCollection(); - items.on('reset', () => { - ready = true; - }); - window.legacyStorage = { - /** *************************** - *** Base Storage Routines *** - **************************** */ - put(key, value) { - if (value === undefined) { - throw new Error('Tried to store undefined'); - } - if (!ready) { - window.log.warn( - 'Called storage.put before storage is ready. key:', - key - ); - } - const item = items.add({ id: key, value }, { merge: true }); - return new Promise((resolve, reject) => { - item.save().then(resolve, reject); - }); - }, - - get(key, defaultValue) { - const item = items.get(`${key}`); - if (!item) { - return defaultValue; - } - return item.get('value'); - }, - - remove(key) { - const item = items.get(`${key}`); - if (item) { - items.remove(item); - return new Promise((resolve, reject) => { - item.destroy().then(resolve, reject); - }); - } - return Promise.resolve(); - }, - - onready(callback) { - if (ready) { - callback(); - } else { - items.on('reset', callback); - } - }, - - fetch() { - return new Promise((resolve, reject) => { - items - .fetch({ reset: true }) - .fail(() => - reject( - new Error( - 'Failed to fetch from storage.' + - ' This may be due to an unexpected database version.' - ) - ) - ) - .always(resolve); - }); - }, - - reset() { - items.reset(); - }, - }; -})(); diff --git a/js/modules/data.d.ts b/js/modules/data.d.ts deleted file mode 100644 index 41e949111..000000000 --- a/js/modules/data.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -export function searchMessages(query: string): Promise>; -export function searchConversations(query: string): Promise>; -export function searchMessagesInConversation( - query: string, - conversationId: string -): Promise>; - -export function updateStickerLastUsed( - packId: string, - stickerId: number, - time: number -): Promise; -export function updateStickerPackStatus( - packId: string, - status: 'known' | 'downloaded' | 'installed' | 'error' | 'pending', - options?: { timestamp: number } -): Promise; - -export function getRecentStickers(): Promise< - Array<{ - id: number; - packId: string; - }> ->; - -export function updateEmojiUsage(shortName: string): Promise; -export function getRecentEmojis( - limit: number -): Promise>; diff --git a/js/modules/database.js b/js/modules/database.js deleted file mode 100644 index 18c3845f7..000000000 --- a/js/modules/database.js +++ /dev/null @@ -1,68 +0,0 @@ -/* global indexedDB */ - -// Module for interacting with IndexedDB without Backbone IndexedDB adapter -// and using promises. Revisit use of `idb` dependency as it might cover -// this functionality. - -const { isObject, isNumber } = require('lodash'); - -exports.open = (name, version, { onUpgradeNeeded } = {}) => { - const request = indexedDB.open(name, version); - return new Promise((resolve, reject) => { - request.onblocked = () => reject(new Error('Database blocked')); - - request.onupgradeneeded = event => { - const hasRequestedSpecificVersion = isNumber(version); - if (!hasRequestedSpecificVersion) { - return; - } - - const { newVersion, oldVersion } = event; - if (onUpgradeNeeded) { - const { transaction } = event.target; - onUpgradeNeeded({ oldVersion, transaction }); - return; - } - - reject( - new Error( - 'Database upgrade required:' + - ` oldVersion: ${oldVersion}, newVersion: ${newVersion}` - ) - ); - }; - - request.onerror = event => reject(event.target.error); - - request.onsuccess = event => { - const connection = event.target.result; - resolve(connection); - }; - }); -}; - -exports.completeTransaction = transaction => - new Promise((resolve, reject) => { - transaction.addEventListener('abort', event => reject(event.target.error)); - transaction.addEventListener('error', event => reject(event.target.error)); - transaction.addEventListener('complete', () => resolve()); - }); - -exports.getVersion = async name => { - const connection = await exports.open(name); - const { version } = connection; - connection.close(); - return version; -}; - -exports.getCount = async ({ store } = {}) => { - if (!isObject(store)) { - throw new TypeError("'store' is required"); - } - - const request = store.count(); - return new Promise((resolve, reject) => { - request.onerror = event => reject(event.target.error); - request.onsuccess = event => resolve(event.target.result); - }); -}; diff --git a/js/modules/deferred_to_promise.d.ts b/js/modules/deferred_to_promise.d.ts deleted file mode 100644 index 67f9ff212..000000000 --- a/js/modules/deferred_to_promise.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function deferredToPromise( - deferred: JQuery.Deferred -): Promise; diff --git a/js/modules/deferred_to_promise.js b/js/modules/deferred_to_promise.js deleted file mode 100644 index d6409b09f..000000000 --- a/js/modules/deferred_to_promise.js +++ /dev/null @@ -1,3 +0,0 @@ -exports.deferredToPromise = deferred => - // eslint-disable-next-line more/no-then - new Promise((resolve, reject) => deferred.then(resolve, reject)); diff --git a/js/modules/indexeddb.js b/js/modules/indexeddb.js index b64e339fb..d2b434f41 100644 --- a/js/modules/indexeddb.js +++ b/js/modules/indexeddb.js @@ -1,146 +1,13 @@ -/* global window, Whisper, textsecure, setTimeout */ - -const { isFunction } = require('lodash'); - -const MessageDataMigrator = require('./messages_data_migrator'); -const { - run, - getLatestVersion, - getDatabase, -} = require('./migrations/migrations'); +/* global window, Whisper, setTimeout */ const MESSAGE_MINIMUM_VERSION = 7; module.exports = { doesDatabaseExist, - mandatoryMessageUpgrade, MESSAGE_MINIMUM_VERSION, - migrateAllToSQLCipher, removeDatabase, - runMigrations, }; -async function runMigrations() { - window.log.info('Run migrations on database with attachment data'); - await run({ - Backbone: window.Backbone, - logger: window.log, - }); - - Whisper.Database.migrations[0].version = getLatestVersion(); -} - -async function mandatoryMessageUpgrade({ upgradeMessageSchema } = {}) { - if (!isFunction(upgradeMessageSchema)) { - throw new Error( - 'mandatoryMessageUpgrade: upgradeMessageSchema must be a function!' - ); - } - - const NUM_MESSAGES_PER_BATCH = 10; - window.log.info( - 'upgradeMessages: Mandatory message schema upgrade started.', - `Target version: ${MESSAGE_MINIMUM_VERSION}` - ); - - let isMigrationWithoutIndexComplete = false; - while (!isMigrationWithoutIndexComplete) { - const database = getDatabase(); - // eslint-disable-next-line no-await-in-loop - const batchWithoutIndex = await MessageDataMigrator.processNextBatchWithoutIndex( - { - databaseName: database.name, - minDatabaseVersion: database.version, - numMessagesPerBatch: NUM_MESSAGES_PER_BATCH, - upgradeMessageSchema, - maxVersion: MESSAGE_MINIMUM_VERSION, - BackboneMessage: Whisper.Message, - saveMessage: window.Signal.Data.saveLegacyMessage, - } - ); - window.log.info( - 'upgradeMessages: upgrade without index', - batchWithoutIndex - ); - isMigrationWithoutIndexComplete = batchWithoutIndex.done; - } - window.log.info('upgradeMessages: upgrade without index complete!'); - - let isMigrationWithIndexComplete = false; - while (!isMigrationWithIndexComplete) { - // eslint-disable-next-line no-await-in-loop - const batchWithIndex = await MessageDataMigrator.processNext({ - BackboneMessage: Whisper.Message, - BackboneMessageCollection: Whisper.MessageCollection, - numMessagesPerBatch: NUM_MESSAGES_PER_BATCH, - upgradeMessageSchema, - getMessagesNeedingUpgrade: - window.Signal.Data.getLegacyMessagesNeedingUpgrade, - saveMessage: window.Signal.Data.saveLegacyMessage, - maxVersion: MESSAGE_MINIMUM_VERSION, - }); - window.log.info('upgradeMessages: upgrade with index', batchWithIndex); - isMigrationWithIndexComplete = batchWithIndex.done; - } - window.log.info('upgradeMessages: upgrade with index complete!'); - - window.log.info('upgradeMessages: Message schema upgrade complete'); -} - -async function migrateAllToSQLCipher({ writeNewAttachmentData, Views } = {}) { - if (!isFunction(writeNewAttachmentData)) { - throw new Error( - 'migrateAllToSQLCipher: writeNewAttachmentData must be a function' - ); - } - if (!Views) { - throw new Error('migrateAllToSQLCipher: Views must be provided!'); - } - - let totalMessages; - const db = await Whisper.Database.open(); - - function showMigrationStatus(current) { - const status = `${current}/${totalMessages}`; - Views.Initialization.setMessage( - window.i18n('migratingToSQLCipher', [status]) - ); - } - - try { - totalMessages = await MessageDataMigrator.getNumMessages({ - connection: db, - }); - } catch (error) { - window.log.error( - 'background.getNumMessages error:', - error && error.stack ? error.stack : error - ); - totalMessages = 0; - } - - if (totalMessages) { - window.log.info(`About to migrate ${totalMessages} messages`); - showMigrationStatus(0); - } else { - window.log.info('About to migrate non-messages'); - } - - await window.Signal.migrateToSQL({ - db, - clearStores: Whisper.Database.clearStores, - handleDOMException: Whisper.Database.handleDOMException, - arrayBufferToString: textsecure.MessageReceiver.arrayBufferToStringBase64, - countCallback: count => { - window.log.info(`Migration: ${count} messages complete`); - showMigrationStatus(count); - }, - writeNewAttachmentData, - }); - - db.close(); -} - async function doesDatabaseExist() { window.log.info('Checking for the existence of IndexedDB data...'); return new Promise((resolve, reject) => { diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 6bde6dc79..85c8cc11f 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -1,18 +1,10 @@ -// Module to upgrade the schema of messages, e.g. migrate attachments to disk. -// `dangerouslyProcessAllWithoutIndex` purposely doesn’t rely on our Backbone -// IndexedDB adapter to prevent automatic migrations. Rather, it uses direct -// IndexedDB access. This includes avoiding usage of `storage` module which uses -// Backbone under the hood. +// Ensures that messages in database are at the right schema. -/* global IDBKeyRange, window */ +/* global window */ -const { isFunction, isNumber, isObject, isString, last } = require('lodash'); +const { isFunction, isNumber } = require('lodash'); -const database = require('./database'); const Message = require('./types/message'); -const settings = require('./settings'); - -const MESSAGES_STORE_NAME = 'messages'; exports.processNext = async ({ BackboneMessage, @@ -96,310 +88,3 @@ exports.processNext = async ({ totalDuration, }; }; - -exports.dangerouslyProcessAllWithoutIndex = async ({ - databaseName, - minDatabaseVersion, - numMessagesPerBatch, - upgradeMessageSchema, - logger, - maxVersion = Message.CURRENT_SCHEMA_VERSION, - saveMessage, - BackboneMessage, -} = {}) => { - if (!isString(databaseName)) { - throw new TypeError("'databaseName' must be a string"); - } - - if (!isNumber(minDatabaseVersion)) { - throw new TypeError("'minDatabaseVersion' must be a number"); - } - - if (!isNumber(numMessagesPerBatch)) { - throw new TypeError("'numMessagesPerBatch' must be a number"); - } - if (!isFunction(upgradeMessageSchema)) { - throw new TypeError("'upgradeMessageSchema' is required"); - } - if (!isFunction(BackboneMessage)) { - throw new TypeError("'upgradeMessageSchema' is required"); - } - if (!isFunction(saveMessage)) { - throw new TypeError("'upgradeMessageSchema' is required"); - } - - const connection = await database.open(databaseName); - const databaseVersion = connection.version; - const isValidDatabaseVersion = databaseVersion >= minDatabaseVersion; - logger.info('Database status', { - databaseVersion, - isValidDatabaseVersion, - minDatabaseVersion, - }); - if (!isValidDatabaseVersion) { - throw new Error( - `Expected database version (${databaseVersion})` + - ` to be at least ${minDatabaseVersion}` - ); - } - - // NOTE: Even if we make this async using `then`, requesting `count` on an - // IndexedDB store blocks all subsequent transactions, so we might as well - // explicitly wait for it here: - const numTotalMessages = await exports.getNumMessages({ connection }); - - const migrationStartTime = Date.now(); - let numCumulativeMessagesProcessed = 0; - // eslint-disable-next-line no-constant-condition - while (true) { - // eslint-disable-next-line no-await-in-loop - const status = await _processBatch({ - connection, - numMessagesPerBatch, - upgradeMessageSchema, - maxVersion, - saveMessage, - BackboneMessage, - }); - if (status.done) { - break; - } - numCumulativeMessagesProcessed += status.numMessagesProcessed; - logger.info( - 'Upgrade message schema:', - Object.assign({}, status, { - numTotalMessages, - numCumulativeMessagesProcessed, - }) - ); - } - - logger.info('Close database connection'); - connection.close(); - - const totalDuration = Date.now() - migrationStartTime; - logger.info('Attachment migration complete:', { - totalDuration, - totalMessagesProcessed: numCumulativeMessagesProcessed, - }); -}; - -exports.processNextBatchWithoutIndex = async ({ - databaseName, - minDatabaseVersion, - numMessagesPerBatch, - upgradeMessageSchema, - maxVersion, - BackboneMessage, - saveMessage, -} = {}) => { - if (!isFunction(upgradeMessageSchema)) { - throw new TypeError("'upgradeMessageSchema' is required"); - } - - const connection = await _getConnection({ databaseName, minDatabaseVersion }); - const batch = await _processBatch({ - connection, - numMessagesPerBatch, - upgradeMessageSchema, - maxVersion, - BackboneMessage, - saveMessage, - }); - return batch; -}; - -// Private API -const _getConnection = async ({ databaseName, minDatabaseVersion }) => { - if (!isString(databaseName)) { - throw new TypeError("'databaseName' must be a string"); - } - - if (!isNumber(minDatabaseVersion)) { - throw new TypeError("'minDatabaseVersion' must be a number"); - } - - const connection = await database.open(databaseName); - const databaseVersion = connection.version; - const isValidDatabaseVersion = databaseVersion >= minDatabaseVersion; - if (!isValidDatabaseVersion) { - throw new Error( - `Expected database version (${databaseVersion})` + - ` to be at least ${minDatabaseVersion}` - ); - } - - return connection; -}; - -const _processBatch = async ({ - connection, - numMessagesPerBatch, - upgradeMessageSchema, - maxVersion, - BackboneMessage, - saveMessage, -} = {}) => { - if (!isObject(connection)) { - throw new TypeError('_processBatch: connection must be a string'); - } - - if (!isFunction(upgradeMessageSchema)) { - throw new TypeError('_processBatch: upgradeMessageSchema is required'); - } - - if (!isNumber(numMessagesPerBatch)) { - throw new TypeError('_processBatch: numMessagesPerBatch is required'); - } - if (!isNumber(maxVersion)) { - throw new TypeError('_processBatch: maxVersion is required'); - } - if (!isFunction(BackboneMessage)) { - throw new TypeError('_processBatch: BackboneMessage is required'); - } - if (!isFunction(saveMessage)) { - throw new TypeError('_processBatch: saveMessage is required'); - } - - const isAttachmentMigrationComplete = await settings.isAttachmentMigrationComplete( - connection - ); - if (isAttachmentMigrationComplete) { - return { - done: true, - }; - } - - const lastProcessedIndex = await settings.getAttachmentMigrationLastProcessedIndex( - connection - ); - - const fetchUnprocessedMessagesStartTime = Date.now(); - let unprocessedMessages; - try { - unprocessedMessages = await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex( - { - connection, - count: numMessagesPerBatch, - lastIndex: lastProcessedIndex, - } - ); - } catch (error) { - window.log.error( - '_processBatch error:', - error && error.stack ? error.stack : error - ); - await settings.markAttachmentMigrationComplete(connection); - await settings.deleteAttachmentMigrationLastProcessedIndex(connection); - return { - done: true, - }; - } - const fetchDuration = Date.now() - fetchUnprocessedMessagesStartTime; - - const upgradeStartTime = Date.now(); - const upgradedMessages = await Promise.all( - unprocessedMessages.map(message => - upgradeMessageSchema(message, { maxVersion }) - ) - ); - const upgradeDuration = Date.now() - upgradeStartTime; - - const saveMessagesStartTime = Date.now(); - const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readwrite'); - const transactionCompletion = database.completeTransaction(transaction); - await Promise.all( - upgradedMessages.map(message => - saveMessage(message, { Message: BackboneMessage }) - ) - ); - await transactionCompletion; - const saveDuration = Date.now() - saveMessagesStartTime; - - const numMessagesProcessed = upgradedMessages.length; - const done = numMessagesProcessed < numMessagesPerBatch; - const lastMessage = last(upgradedMessages); - const newLastProcessedIndex = lastMessage ? lastMessage.id : null; - if (!done) { - await settings.setAttachmentMigrationLastProcessedIndex( - connection, - newLastProcessedIndex - ); - } else { - await settings.markAttachmentMigrationComplete(connection); - await settings.deleteAttachmentMigrationLastProcessedIndex(connection); - } - - const batchTotalDuration = Date.now() - fetchUnprocessedMessagesStartTime; - - return { - batchTotalDuration, - done, - fetchDuration, - lastProcessedIndex, - newLastProcessedIndex, - numMessagesProcessed, - saveDuration, - targetSchemaVersion: Message.CURRENT_SCHEMA_VERSION, - upgradeDuration, - }; -}; - -// NOTE: Named ‘dangerous’ because it is not as efficient as using our -// `messages` `schemaVersion` index: -const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex = ({ - connection, - count, - lastIndex, -} = {}) => { - if (!isObject(connection)) { - throw new TypeError("'connection' is required"); - } - - if (!isNumber(count)) { - throw new TypeError("'count' is required"); - } - - if (lastIndex && !isString(lastIndex)) { - throw new TypeError("'lastIndex' must be a string"); - } - - const hasLastIndex = Boolean(lastIndex); - - const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readonly'); - const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME); - - const excludeLowerBound = true; - const range = hasLastIndex - ? IDBKeyRange.lowerBound(lastIndex, excludeLowerBound) - : undefined; - return new Promise((resolve, reject) => { - const items = []; - const request = messagesStore.openCursor(range); - request.onsuccess = event => { - const cursor = event.target.result; - const hasMoreData = Boolean(cursor); - if (!hasMoreData || items.length === count) { - resolve(items); - return; - } - const item = cursor.value; - items.push(item); - cursor.continue(); - }; - request.onerror = event => reject(event.target.error); - }); -}; - -exports.getNumMessages = async ({ connection } = {}) => { - if (!isObject(connection)) { - throw new TypeError("'connection' is required"); - } - - const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readonly'); - const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME); - const numTotalMessages = await database.getCount({ store: messagesStore }); - await database.completeTransaction(transaction); - - return numTotalMessages; -}; diff --git a/js/modules/migrate_to_sql.js b/js/modules/migrate_to_sql.js deleted file mode 100644 index 1e882270a..000000000 --- a/js/modules/migrate_to_sql.js +++ /dev/null @@ -1,409 +0,0 @@ -/* global window, IDBKeyRange */ - -const { includes, isFunction, isString, last, map } = require('lodash'); -const { - bulkAddSessions, - bulkAddIdentityKeys, - bulkAddPreKeys, - bulkAddSignedPreKeys, - bulkAddItems, - - removeSessionById, - removeIdentityKeyById, - removePreKeyById, - removeSignedPreKeyById, - removeItemById, - - saveMessages, - _removeMessages, - - saveUnprocesseds, - removeUnprocessed, - - saveConversations, - _removeConversations, -} = require('../../ts/sql/Client').default; -const { - getMessageExportLastIndex, - setMessageExportLastIndex, - getMessageExportCount, - setMessageExportCount, - getUnprocessedExportLastIndex, - setUnprocessedExportLastIndex, -} = require('./settings'); -const { migrateConversation } = require('./types/conversation'); - -module.exports = { - migrateToSQL, -}; - -async function migrateToSQL({ - db, - clearStores, - handleDOMException, - countCallback, - arrayBufferToString, - writeNewAttachmentData, -}) { - if (!db) { - throw new Error('Need db for IndexedDB connection!'); - } - if (!isFunction(clearStores)) { - throw new Error('Need clearStores function!'); - } - if (!isFunction(arrayBufferToString)) { - throw new Error('Need arrayBufferToString function!'); - } - if (!isFunction(handleDOMException)) { - throw new Error('Need handleDOMException function!'); - } - - window.log.info('migrateToSQL: start'); - - let [lastIndex, doneSoFar] = await Promise.all([ - getMessageExportLastIndex(db), - getMessageExportCount(db), - ]); - let complete = false; - - while (!complete) { - // eslint-disable-next-line no-await-in-loop - const status = await migrateStoreToSQLite({ - db, - save: saveMessages, - remove: _removeMessages, - storeName: 'messages', - handleDOMException, - lastIndex, - }); - - ({ complete, lastIndex } = status); - - // eslint-disable-next-line no-await-in-loop - await Promise.all([ - setMessageExportCount(db, doneSoFar), - setMessageExportLastIndex(db, lastIndex), - ]); - - const { count } = status; - doneSoFar += count; - if (countCallback) { - countCallback(doneSoFar); - } - } - window.log.info('migrateToSQL: migrate of messages complete'); - try { - await clearStores(['messages']); - } catch (error) { - window.log.warn('Failed to clear messages store'); - } - - lastIndex = await getUnprocessedExportLastIndex(db); - complete = false; - - while (!complete) { - // eslint-disable-next-line no-await-in-loop - const status = await migrateStoreToSQLite({ - db, - save: async array => { - await Promise.all( - map(array, async item => { - // In the new database, we can't store ArrayBuffers, so we turn these two - // fields into strings like MessageReceiver now does before save. - - // Need to set it to version two, since we're using Base64 strings now - // eslint-disable-next-line no-param-reassign - item.version = 2; - - if (item.envelope) { - // eslint-disable-next-line no-param-reassign - item.envelope = arrayBufferToString(item.envelope); - } - if (item.decrypted) { - // eslint-disable-next-line no-param-reassign - item.decrypted = arrayBufferToString(item.decrypted); - } - }) - ); - await saveUnprocesseds(array); - }, - remove: removeUnprocessed, - storeName: 'unprocessed', - handleDOMException, - lastIndex, - }); - - ({ complete, lastIndex } = status); - - // eslint-disable-next-line no-await-in-loop - await setUnprocessedExportLastIndex(db, lastIndex); - } - window.log.info('migrateToSQL: migrate of unprocessed complete'); - try { - await clearStores(['unprocessed']); - } catch (error) { - window.log.warn('Failed to clear unprocessed store'); - } - - complete = false; - lastIndex = null; - - while (!complete) { - // eslint-disable-next-line no-await-in-loop - const status = await migrateStoreToSQLite({ - db, - // eslint-disable-next-line no-loop-func - save: async array => { - const conversations = await Promise.all( - map(array, async conversation => - migrateConversation(conversation, { writeNewAttachmentData }) - ) - ); - - saveConversations(conversations); - }, - remove: _removeConversations, - storeName: 'conversations', - handleDOMException, - lastIndex, - // Because we're doing real-time moves to the filesystem, minimize parallelism - batchSize: 5, - }); - - ({ complete, lastIndex } = status); - } - window.log.info('migrateToSQL: migrate of conversations complete'); - try { - await clearStores(['conversations']); - } catch (error) { - window.log.warn('Failed to clear conversations store'); - } - - complete = false; - lastIndex = null; - - while (!complete) { - // eslint-disable-next-line no-await-in-loop - const status = await migrateStoreToSQLite({ - db, - // eslint-disable-next-line no-loop-func - save: bulkAddSessions, - remove: removeSessionById, - storeName: 'sessions', - handleDOMException, - lastIndex, - batchSize: 10, - }); - - ({ complete, lastIndex } = status); - } - window.log.info('migrateToSQL: migrate of sessions complete'); - try { - await clearStores(['sessions']); - } catch (error) { - window.log.warn('Failed to clear sessions store'); - } - - complete = false; - lastIndex = null; - - while (!complete) { - // eslint-disable-next-line no-await-in-loop - const status = await migrateStoreToSQLite({ - db, - // eslint-disable-next-line no-loop-func - save: bulkAddIdentityKeys, - remove: removeIdentityKeyById, - storeName: 'identityKeys', - handleDOMException, - lastIndex, - batchSize: 10, - }); - - ({ complete, lastIndex } = status); - } - window.log.info('migrateToSQL: migrate of identityKeys complete'); - try { - await clearStores(['identityKeys']); - } catch (error) { - window.log.warn('Failed to clear identityKeys store'); - } - - complete = false; - lastIndex = null; - - while (!complete) { - // eslint-disable-next-line no-await-in-loop - const status = await migrateStoreToSQLite({ - db, - // eslint-disable-next-line no-loop-func - save: bulkAddPreKeys, - remove: removePreKeyById, - storeName: 'preKeys', - handleDOMException, - lastIndex, - batchSize: 10, - }); - - ({ complete, lastIndex } = status); - } - window.log.info('migrateToSQL: migrate of preKeys complete'); - try { - await clearStores(['preKeys']); - } catch (error) { - window.log.warn('Failed to clear preKeys store'); - } - - complete = false; - lastIndex = null; - - while (!complete) { - // eslint-disable-next-line no-await-in-loop - const status = await migrateStoreToSQLite({ - db, - // eslint-disable-next-line no-loop-func - save: bulkAddSignedPreKeys, - remove: removeSignedPreKeyById, - storeName: 'signedPreKeys', - handleDOMException, - lastIndex, - batchSize: 10, - }); - - ({ complete, lastIndex } = status); - } - window.log.info('migrateToSQL: migrate of signedPreKeys complete'); - try { - await clearStores(['signedPreKeys']); - } catch (error) { - window.log.warn('Failed to clear signedPreKeys store'); - } - - complete = false; - lastIndex = null; - - while (!complete) { - // eslint-disable-next-line no-await-in-loop - const status = await migrateStoreToSQLite({ - db, - // eslint-disable-next-line no-loop-func - save: bulkAddItems, - remove: removeItemById, - storeName: 'items', - handleDOMException, - lastIndex, - batchSize: 10, - }); - - ({ complete, lastIndex } = status); - } - window.log.info('migrateToSQL: migrate of items complete'); - // Note: we don't clear the items store because it contains important metadata which, - // if this process fails, will be crucial to going through this process again. - - window.log.info('migrateToSQL: complete'); -} - -async function migrateStoreToSQLite({ - db, - save, - remove, - storeName, - handleDOMException, - lastIndex = null, - batchSize = 50, -}) { - if (!db) { - throw new Error('Need db for IndexedDB connection!'); - } - if (!isFunction(save)) { - throw new Error('Need save function!'); - } - if (!isFunction(remove)) { - throw new Error('Need remove function!'); - } - if (!isString(storeName)) { - throw new Error('Need storeName!'); - } - if (!isFunction(handleDOMException)) { - throw new Error('Need handleDOMException for error handling!'); - } - - if (!includes(db.objectStoreNames, storeName)) { - return { - complete: true, - count: 0, - }; - } - - const queryPromise = new Promise((resolve, reject) => { - const items = []; - const transaction = db.transaction(storeName, 'readonly'); - transaction.onerror = () => { - handleDOMException( - 'migrateToSQLite transaction error', - transaction.error, - reject - ); - }; - transaction.oncomplete = () => {}; - - const store = transaction.objectStore(storeName); - const excludeLowerBound = true; - const range = lastIndex - ? IDBKeyRange.lowerBound(lastIndex, excludeLowerBound) - : undefined; - const request = store.openCursor(range); - request.onerror = () => { - handleDOMException( - 'migrateToSQLite: request error', - request.error, - reject - ); - }; - request.onsuccess = event => { - const cursor = event.target.result; - - if (!cursor || !cursor.value) { - return resolve({ - complete: true, - items, - }); - } - - const item = cursor.value; - items.push(item); - - if (items.length >= batchSize) { - return resolve({ - complete: false, - items, - }); - } - - return cursor.continue(); - }; - }); - - const { items, complete } = await queryPromise; - - if (items.length) { - // Because of the force save and some failed imports, we're going to delete before - // we attempt to insert. - const ids = items.map(item => item.id); - await remove(ids); - - // We need to pass forceSave parameter, because these items already have an - // id key. Normally, this call would be interpreted as an update request. - await save(items, { forceSave: true }); - } - - const lastItem = last(items); - const id = lastItem ? lastItem.id : null; - - return { - complete, - count: items.length, - lastIndex: id, - }; -} diff --git a/js/modules/migrations/18/index.js b/js/modules/migrations/18/index.js deleted file mode 100644 index ef0650a80..000000000 --- a/js/modules/migrations/18/index.js +++ /dev/null @@ -1,17 +0,0 @@ -exports.run = ({ transaction, logger }) => { - const messagesStore = transaction.objectStore('messages'); - - logger.info("Create message attachment metadata index: 'hasAttachments'"); - messagesStore.createIndex( - 'hasAttachments', - ['conversationId', 'hasAttachments', 'received_at'], - { unique: false } - ); - - ['hasVisualMediaAttachments', 'hasFileAttachments'].forEach(name => { - logger.info(`Create message attachment metadata index: '${name}'`); - messagesStore.createIndex(name, ['conversationId', 'received_at', name], { - unique: false, - }); - }); -}; diff --git a/js/modules/migrations/get_placeholder_migrations.js b/js/modules/migrations/get_placeholder_migrations.js deleted file mode 100644 index 3377096ea..000000000 --- a/js/modules/migrations/get_placeholder_migrations.js +++ /dev/null @@ -1,35 +0,0 @@ -/* global window, Whisper */ - -const Migrations = require('./migrations'); - -exports.getPlaceholderMigrations = () => { - const version = Migrations.getLatestVersion(); - - return [ - { - version, - migrate() { - throw new Error( - 'Unexpected invocation of placeholder migration!' + - '\n\nMigrations must explicitly be run upon application startup instead' + - ' of implicitly via Backbone IndexedDB adapter at any time.' - ); - }, - }, - ]; -}; - -exports.getCurrentVersion = () => - new Promise((resolve, reject) => { - const request = window.indexedDB.open(Whisper.Database.id); - - request.onerror = reject; - request.onupgradeneeded = reject; - - request.onsuccess = () => { - const db = request.result; - const { version } = db; - - return resolve(version); - }; - }); diff --git a/js/modules/migrations/migrations.js b/js/modules/migrations/migrations.js deleted file mode 100644 index d3da57759..000000000 --- a/js/modules/migrations/migrations.js +++ /dev/null @@ -1,221 +0,0 @@ -/* global window */ - -const { isString, last } = require('lodash'); - -const { runMigrations } = require('./run_migrations'); -const Migration18 = require('./18'); - -// IMPORTANT: The migrations below are run on a database that may be very large -// due to attachments being directly stored inside the database. Please avoid -// any expensive operations, e.g. modifying all messages / attachments, etc., as -// it may cause out-of-memory errors for users with long histories: -// https://github.com/signalapp/Signal-Desktop/issues/2163 -const migrations = [ - { - version: '12.0', - migrate(transaction, next) { - window.log.info('Migration 12'); - window.log.info('creating object stores'); - const messages = transaction.db.createObjectStore('messages'); - messages.createIndex('conversation', ['conversationId', 'received_at'], { - unique: false, - }); - messages.createIndex('receipt', 'sent_at', { unique: false }); - messages.createIndex('unread', ['conversationId', 'unread'], { - unique: false, - }); - messages.createIndex('expires_at', 'expires_at', { unique: false }); - - const conversations = transaction.db.createObjectStore('conversations'); - conversations.createIndex('inbox', 'active_at', { unique: false }); - conversations.createIndex('group', 'members', { - unique: false, - multiEntry: true, - }); - conversations.createIndex('type', 'type', { - unique: false, - }); - conversations.createIndex('search', 'tokens', { - unique: false, - multiEntry: true, - }); - - transaction.db.createObjectStore('groups'); - - transaction.db.createObjectStore('sessions'); - transaction.db.createObjectStore('identityKeys'); - transaction.db.createObjectStore('preKeys'); - transaction.db.createObjectStore('signedPreKeys'); - transaction.db.createObjectStore('items'); - - window.log.info('creating debug log'); - transaction.db.createObjectStore('debug'); - - next(); - }, - }, - { - version: '13.0', - migrate(transaction, next) { - window.log.info('Migration 13'); - window.log.info('Adding fields to identity keys'); - const identityKeys = transaction.objectStore('identityKeys'); - const request = identityKeys.openCursor(); - const promises = []; - request.onsuccess = event => { - const cursor = event.target.result; - if (cursor) { - const attributes = cursor.value; - attributes.timestamp = 0; - attributes.firstUse = false; - attributes.nonblockingApproval = false; - attributes.verified = 0; - promises.push( - new Promise((resolve, reject) => { - const putRequest = identityKeys.put(attributes, attributes.id); - putRequest.onsuccess = resolve; - putRequest.onerror = error => { - window.log.error(error && error.stack ? error.stack : error); - reject(error); - }; - }) - ); - cursor.continue(); - } else { - // no more results - // eslint-disable-next-line more/no-then - Promise.all(promises).then(() => { - next(); - }); - } - }; - request.onerror = event => { - window.log.error(event); - }; - }, - }, - { - version: '14.0', - migrate(transaction, next) { - window.log.info('Migration 14'); - window.log.info('Adding unprocessed message store'); - const unprocessed = transaction.db.createObjectStore('unprocessed'); - unprocessed.createIndex('received', 'timestamp', { unique: false }); - next(); - }, - }, - { - version: '15.0', - migrate(transaction, next) { - window.log.info('Migration 15'); - window.log.info('Adding messages index for de-duplication'); - const messages = transaction.objectStore('messages'); - messages.createIndex('unique', ['source', 'sourceDevice', 'sent_at'], { - unique: true, - }); - next(); - }, - }, - { - version: '16.0', - migrate(transaction, next) { - window.log.info('Migration 16'); - window.log.info('Dropping log table, since we now log to disk'); - transaction.db.deleteObjectStore('debug'); - next(); - }, - }, - { - version: 17, - async migrate(transaction, next) { - window.log.info('Migration 17'); - - const start = Date.now(); - - const messagesStore = transaction.objectStore('messages'); - window.log.info( - 'Create index from attachment schema version to attachment' - ); - messagesStore.createIndex('schemaVersion', 'schemaVersion', { - unique: false, - }); - - const duration = Date.now() - start; - - window.log.info( - 'Complete migration to database version 17', - `Duration: ${duration}ms` - ); - next(); - }, - }, - { - version: 18, - migrate(transaction, next) { - window.log.info('Migration 18'); - - const start = Date.now(); - Migration18.run({ transaction, logger: window.log }); - const duration = Date.now() - start; - - window.log.info( - 'Complete migration to database version 18', - `Duration: ${duration}ms` - ); - next(); - }, - }, - { - version: 19, - migrate(transaction, next) { - window.log.info('Migration 19'); - - // Empty because we don't want to cause incompatibility with beta users who have - // already run migration 19 when it was object store removal. - - next(); - }, - }, - { - version: 20, - migrate(transaction, next) { - window.log.info('Migration 20'); - - // Empty because we don't want to cause incompatibility with users who have already - // run migration 20 when it was object store removal. - - next(); - }, - }, -]; - -const database = { - id: 'signal', - nolog: true, - migrations, -}; - -exports.run = ({ Backbone, databaseName, logger } = {}) => - runMigrations({ - Backbone, - logger, - database: Object.assign( - {}, - database, - isString(databaseName) ? { id: databaseName } : {} - ), - }); - -exports.getDatabase = () => ({ - name: database.id, - version: exports.getLatestVersion(), -}); - -exports.getLatestVersion = () => { - const lastMigration = last(migrations); - if (!lastMigration) { - return null; - } - - return lastMigration.version; -}; diff --git a/js/modules/migrations/run_migrations.js b/js/modules/migrations/run_migrations.js deleted file mode 100644 index 35a84cc4a..000000000 --- a/js/modules/migrations/run_migrations.js +++ /dev/null @@ -1,79 +0,0 @@ -/* eslint-env browser */ - -const { head, isFunction, isObject, isString, last } = require('lodash'); - -const db = require('../database'); -const { deferredToPromise } = require('../deferred_to_promise'); - -const closeDatabaseConnection = ({ Backbone } = {}) => - deferredToPromise(Backbone.sync('closeall')); - -exports.runMigrations = async ({ Backbone, database, logger } = {}) => { - if ( - !isObject(Backbone) || - !isObject(Backbone.Collection) || - !isFunction(Backbone.Collection.extend) - ) { - throw new TypeError('runMigrations: Backbone is required'); - } - - if ( - !isObject(database) || - !isString(database.id) || - !Array.isArray(database.migrations) - ) { - throw new TypeError('runMigrations: database is required'); - } - if (!isObject(logger)) { - throw new TypeError('runMigrations: logger is required'); - } - - const { - firstVersion: firstMigrationVersion, - lastVersion: lastMigrationVersion, - } = getMigrationVersions(database); - - const databaseVersion = await db.getVersion(database.id); - const isAlreadyUpgraded = databaseVersion >= lastMigrationVersion; - - logger.info('Database status', { - firstMigrationVersion, - lastMigrationVersion, - databaseVersion, - isAlreadyUpgraded, - }); - - if (isAlreadyUpgraded) { - return; - } - - const migrationCollection = new (Backbone.Collection.extend({ - database, - storeName: 'items', - }))(); - - // Note: this legacy migration technique is required to bring old clients with - // data in IndexedDB forward into the new world of SQLCipher only. - await deferredToPromise(migrationCollection.fetch({ limit: 1 })); - - logger.info('Close database connection'); - await closeDatabaseConnection({ Backbone }); -}; - -const getMigrationVersions = database => { - if (!isObject(database) || !Array.isArray(database.migrations)) { - throw new TypeError("'database' is required"); - } - - const firstMigration = head(database.migrations); - const lastMigration = last(database.migrations); - - const firstVersion = firstMigration - ? parseInt(firstMigration.version, 10) - : null; - const lastVersion = lastMigration - ? parseInt(lastMigration.version, 10) - : null; - - return { firstVersion, lastVersion }; -}; diff --git a/js/modules/signal.js b/js/modules/signal.js index 7d42c4a09..4c7b9b2e6 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -4,7 +4,6 @@ const { bindActionCreators } = require('redux'); const Backbone = require('../../ts/backbone'); const Crypto = require('../../ts/Crypto'); const Data = require('../../ts/sql/Client').default; -const Database = require('./database'); const Emojis = require('./emojis'); const EmojiLib = require('../../ts/components/emoji/lib'); const IndexedDB = require('./indexeddb'); @@ -13,7 +12,6 @@ const OS = require('../../ts/OS'); const Stickers = require('./stickers'); const Settings = require('./settings'); const Util = require('../../ts/util'); -const { migrateToSQL } = require('./migrate_to_sql'); const Metadata = require('./metadata/SecretSessionCipher'); const RefreshSenderCertificate = require('./refresh_sender_certificate'); const LinkPreviews = require('./link_previews'); @@ -75,13 +73,6 @@ const userDuck = require('../../ts/state/ducks/user'); const conversationsSelectors = require('../../ts/state/selectors/conversations'); const searchSelectors = require('../../ts/state/selectors/search'); -// Migrations -const { - getPlaceholderMigrations, - getCurrentVersion, -} = require('./migrations/get_placeholder_migrations'); -const { run } = require('./migrations/migrations'); - // Types const AttachmentType = require('./types/attachment'); const VisualAttachment = require('./types/visual_attachment'); @@ -193,8 +184,6 @@ function initializeMigrations({ getAbsoluteDraftPath, getAbsoluteStickerPath, getAbsoluteTempPath, - getPlaceholderMigrations, - getCurrentVersion, loadAttachmentData, loadMessage: MessageType.createAttachmentLoader(loadAttachmentData), loadPreviewData, @@ -205,7 +194,6 @@ function initializeMigrations({ readDraftData, readStickerData, readTempData, - run, saveAttachmentToDisk, processNewAttachment: attachment => MessageType.processNewAttachment(attachment, { @@ -353,13 +341,11 @@ exports.setup = (options = {}) => { Components, Crypto, Data, - Database, Emojis, EmojiLib, IndexedDB, LinkPreviews, Metadata, - migrateToSQL, Migrations, Notifications, OS, diff --git a/js/views/confirmation_dialog_view.js b/js/views/confirmation_dialog_view.js index 02867fe77..0576cea63 100644 --- a/js/views/confirmation_dialog_view.js +++ b/js/views/confirmation_dialog_view.js @@ -58,7 +58,7 @@ cancel() { this.remove(); if (this.reject) { - this.reject(); + this.reject(new Error('User clicked cancel button')); } }, onKeydown(event) { diff --git a/main.js b/main.js index b24ad7c6e..4b6b531ea 100644 --- a/main.js +++ b/main.js @@ -1095,6 +1095,9 @@ ipc.on('restart', () => { app.relaunch(); app.quit(); }); +ipc.on('shutdown', () => { + app.quit(); +}); ipc.on('set-auto-hide-menu-bar', (event, autoHide) => { if (mainWindow) { diff --git a/preload.js b/preload.js index 009e723e7..f791b4ce1 100644 --- a/preload.js +++ b/preload.js @@ -9,8 +9,6 @@ try { const _ = require('lodash'); const { installGetter, installSetter } = require('./preload_utils'); - const { deferredToPromise } = require('./js/modules/deferred_to_promise'); - const { remote } = electron; const { app } = remote; const { nativeTheme } = remote.require('electron'); @@ -66,8 +64,6 @@ try { } }; - window.wrapDeferred = deferredToPromise; - const ipc = electron.ipcRenderer; const localeMessages = ipc.sendSync('locale-data'); @@ -97,6 +93,10 @@ try { window.log.info('restart'); ipc.send('restart'); }; + window.shutdown = () => { + window.log.info('shutdown'); + ipc.send('shutdown'); + }; window.closeAbout = () => ipc.send('close-about'); window.readyForUpdates = () => ipc.send('ready-for-updates'); diff --git a/ts/sql/Client.ts b/ts/sql/Client.ts index 822da7286..4d09d4d22 100644 --- a/ts/sql/Client.ts +++ b/ts/sql/Client.ts @@ -18,7 +18,6 @@ import { import { arrayBufferToBase64, base64ToArrayBuffer } from '../Crypto'; import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message'; import { createBatcher } from '../util/batcher'; -import { v4 as getGuid } from 'uuid'; import { AttachmentDownloadJobType, @@ -208,9 +207,6 @@ const dataInterface: ClientInterface = { cleanupOrphanedAttachments, ensureFilePermissions, - getLegacyMessagesNeedingUpgrade, - saveLegacyMessage, - // Client-side only, and test-only _removeConversations, @@ -1330,97 +1326,3 @@ async function getMessagesWithFileAttachments( limit, }); } - -// Legacy IndexedDB Support - -async function getLegacyMessagesNeedingUpgrade( - limit: number, - { maxVersion = CURRENT_SCHEMA_VERSION }: { maxVersion: number } -): Promise { - const db = await window.Whisper.Database.open(); - try { - return new Promise((resolve, reject) => { - const transaction = db.transaction('messages', 'readonly'); - const messages: Array = []; - - transaction.onerror = () => { - window.Whisper.Database.handleDOMException( - 'getLegacyMessagesNeedingUpgrade transaction error', - transaction.error, - reject - ); - }; - transaction.oncomplete = () => { - resolve(messages); - }; - - const store = transaction.objectStore('messages'); - const index = store.index('schemaVersion'); - const range = IDBKeyRange.upperBound(maxVersion, true); - - const request = index.openCursor(range); - let count = 0; - - request.onsuccess = event => { - // @ts-ignore - const cursor = event.target.result; - - if (cursor) { - count += 1; - messages.push(cursor.value); - - if (count >= limit) { - return; - } - - cursor.continue(); - } - }; - request.onerror = () => { - window.Whisper.Database.handleDOMException( - 'getLegacyMessagesNeedingUpgrade request error', - request.error, - reject - ); - }; - }); - } finally { - db.close(); - } -} - -async function saveLegacyMessage(data: MessageType) { - const db = await window.Whisper.Database.open(); - try { - await new Promise((resolve, reject) => { - const transaction = db.transaction('messages', 'readwrite'); - - transaction.onerror = () => { - window.Whisper.Database.handleDOMException( - 'saveLegacyMessage transaction error', - transaction.error, - reject - ); - }; - transaction.oncomplete = resolve; - - const store = transaction.objectStore('messages'); - - if (!data.id) { - data.id = getGuid(); - } - - const request = store.put(data, data.id); - request.onsuccess = resolve; - request.onerror = () => { - window.Whisper.Database.handleDOMException( - 'saveLegacyMessage request error', - request.error, - reject - ); - }; - }); - } finally { - db.close(); - } -} diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index 193e4226c..b26ef1b9d 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -356,12 +356,6 @@ export type ClientInterface = DataInterface & { cleanupOrphanedAttachments: () => Promise; ensureFilePermissions: () => Promise; - getLegacyMessagesNeedingUpgrade: ( - limit: number, - options: { maxVersion: number } - ) => Promise>; - saveLegacyMessage: (data: MessageType) => Promise; - // Client-side only, and test-only _removeConversations: (ids: Array) => Promise;