Bug 1286798 - Part 26: Implement a lazy data migration from old webappsstore.sqlite; r=asuth,janv
authorJan Varga <jan.varga@gmail.com>
Thu, 29 Nov 2018 21:48:38 +0100
changeset 508024 2620df4a91da32a75123274b2f603e45e78ca3df
parent 508023 8c37e877e231ee830f377b2945291b1b747293d3
child 508025 e75a175ddf880c7d3f0434cda155a0d57876f85b
push id1905
push userffxbld-merge
push dateMon, 21 Jan 2019 12:33:13 +0000
treeherdermozilla-release@c2fca1944d8c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersasuth, janv
bugs1286798
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1286798 - Part 26: Implement a lazy data migration from old webappsstore.sqlite; r=asuth,janv This patch was enhanced by asuth to bind attached database path. Places has shown that argument binding is necessary. (Profiles may include usernames in their path which can have cool emoji and such.)
dom/localstorage/ActorsParent.cpp
dom/localstorage/LSObject.cpp
dom/localstorage/test/unit/archive_profile.zip
dom/localstorage/test/unit/head.js
dom/localstorage/test/unit/migration_profile.zip
dom/localstorage/test/unit/test_archive.js
dom/localstorage/test/unit/test_migration.js
dom/localstorage/test/unit/xpcshell.ini
dom/quota/ActorsParent.cpp
dom/quota/QuotaManager.h
dom/quota/test/unit/createLocalStorage_profile.zip
dom/quota/test/unit/head.js
dom/quota/test/unit/test_createLocalStorage.js
dom/quota/test/unit/xpcshell.ini
dom/storage/moz.build
--- a/dom/localstorage/ActorsParent.cpp
+++ b/dom/localstorage/ActorsParent.cpp
@@ -15,16 +15,18 @@
 #include "mozilla/Preferences.h"
 #include "mozilla/Unused.h"
 #include "mozilla/dom/PBackgroundLSDatabaseParent.h"
 #include "mozilla/dom/PBackgroundLSObjectParent.h"
 #include "mozilla/dom/PBackgroundLSObserverParent.h"
 #include "mozilla/dom/PBackgroundLSRequestParent.h"
 #include "mozilla/dom/PBackgroundLSSharedTypes.h"
 #include "mozilla/dom/PBackgroundLSSimpleRequestParent.h"
+#include "mozilla/dom/StorageDBUpdater.h"
+#include "mozilla/dom/StorageUtils.h"
 #include "mozilla/dom/quota/QuotaManager.h"
 #include "mozilla/dom/quota/QuotaObject.h"
 #include "mozilla/dom/quota/UsageInfo.h"
 #include "mozilla/ipc/BackgroundParent.h"
 #include "mozilla/ipc/PBackgroundParent.h"
 #include "mozilla/ipc/PBackgroundSharedTypes.h"
 #include "nsClassHashtable.h"
 #include "nsDataHashtable.h"
@@ -44,20 +46,22 @@
 #if defined(MOZ_WIDGET_ANDROID)
 #define LS_MOBILE
 #endif
 
 namespace mozilla {
 namespace dom {
 
 using namespace mozilla::dom::quota;
+using namespace mozilla::dom::StorageUtils;
 using namespace mozilla::ipc;
 
 namespace {
 
+class ArchivedOriginInfo;
 class Connection;
 class ConnectionThread;
 class Database;
 class PrepareDatastoreOp;
 class PreparedDatastore;
 
 /*******************************************************************************
  * Constants
@@ -112,16 +116,18 @@ const uint32_t kAutoCommitTimeoutMs = 50
 
 const char kPrivateBrowsingObserverTopic[] = "last-pb-context-exited";
 
 const uint32_t kDefaultOriginLimitKB = 5 * 1024;
 const char kDefaultQuotaPref[] = "dom.storage.default_quota";
 
 const uint32_t kPreparedDatastoreTimeoutMs = 20000;
 
+#define LS_ARCHIVE_FILE_NAME "ls-archive.sqlite"
+
 bool
 IsOnConnectionThread();
 
 void
 AssertIsOnConnectionThread();
 
 /*******************************************************************************
  * SQLite functions
@@ -460,16 +466,168 @@ GetStorageConnection(const nsAString& aD
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   connection.forget(aConnection);
   return NS_OK;
 }
 
+nsresult
+GetArchiveFile(const nsAString& aStoragePath,
+               nsIFile** aArchiveFile)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(!aStoragePath.IsEmpty());
+  MOZ_ASSERT(aArchiveFile);
+
+  nsCOMPtr<nsIFile> archiveFile;
+  nsresult rv = NS_NewLocalFile(aStoragePath,
+                                false,
+                                getter_AddRefs(archiveFile));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = archiveFile->Append(NS_LITERAL_STRING(LS_ARCHIVE_FILE_NAME));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  archiveFile.forget(aArchiveFile);
+  return NS_OK;
+}
+
+nsresult
+CreateArchiveStorageConnection(const nsAString& aStoragePath,
+                               mozIStorageConnection** aConnection)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(!aStoragePath.IsEmpty());
+  MOZ_ASSERT(aConnection);
+
+  nsCOMPtr<nsIFile> archiveFile;
+  nsresult rv = GetArchiveFile(aStoragePath, getter_AddRefs(archiveFile));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  // QuotaManager ensures this file always exists.
+  DebugOnly<bool> exists;
+  MOZ_ASSERT(NS_SUCCEEDED(archiveFile->Exists(&exists)));
+  MOZ_ASSERT(exists);
+
+  bool isDirectory;
+  rv = archiveFile->IsDirectory(&isDirectory);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (isDirectory) {
+    LS_WARNING("ls-archive is not a file!");
+    *aConnection = nullptr;
+    return NS_OK;
+  }
+
+  nsCOMPtr<mozIStorageService> ss =
+    do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCOMPtr<mozIStorageConnection> connection;
+  rv = ss->OpenUnsharedDatabase(archiveFile, getter_AddRefs(connection));
+  if (rv == NS_ERROR_FILE_CORRUPTED) {
+    // Don't throw an error, leave a corrupted ls-archive database as it is.
+    *aConnection = nullptr;
+    return NS_OK;
+  }
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = StorageDBUpdater::Update(connection);
+  if (NS_FAILED(rv)) {
+    // Don't throw an error, leave a non-updateable ls-archive database as
+    // it is.
+    *aConnection = nullptr;
+    return NS_OK;
+  }
+
+  connection.forget(aConnection);
+  return NS_OK;
+}
+
+nsresult
+AttachArchiveDatabase(const nsAString& aStoragePath,
+                      mozIStorageConnection* aConnection)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(!aStoragePath.IsEmpty());
+  MOZ_ASSERT(aConnection);
+  nsCOMPtr<nsIFile> archiveFile;
+
+  nsresult rv = GetArchiveFile(aStoragePath, getter_AddRefs(archiveFile));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+#ifdef DEBUG
+  bool exists;
+  rv = archiveFile->Exists(&exists);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  MOZ_ASSERT(exists);
+#endif
+
+  nsString path;
+  rv = archiveFile->GetPath(path);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCOMPtr<mozIStorageStatement> stmt;
+  rv = aConnection->CreateStatement(
+    NS_LITERAL_CSTRING("ATTACH DATABASE :path AS archive;"),
+    getter_AddRefs(stmt));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("path"), path);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = stmt->Execute();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+DetachArchiveDatabase(mozIStorageConnection* aConnection)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(aConnection);
+
+  nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "DETACH DATABASE archive"
+  ));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
 /*******************************************************************************
  * Non-actor class declarations
  ******************************************************************************/
 
 class DatastoreOperationBase
   : public Runnable
 {
   nsCOMPtr<nsIEventTarget> mOwningEventTarget;
@@ -1419,16 +1577,17 @@ class PrepareDatastoreOp
     AfterNesting
   };
 
   nsCOMPtr<nsIEventTarget> mMainEventTarget;
   RefPtr<PrepareDatastoreOp> mDelayedOp;
   RefPtr<DirectoryLock> mDirectoryLock;
   RefPtr<Connection> mConnection;
   RefPtr<Datastore> mDatastore;
+  nsAutoPtr<ArchivedOriginInfo> mArchivedOriginInfo;
   LoadDataOp* mLoadDataOp;
   nsDataHashtable<nsStringHashKey, nsString> mValues;
   const LSRequestPrepareDatastoreParams mParams;
   nsCString mSuffix;
   nsCString mGroup;
   nsCString mMainThreadOrigin;
   nsCString mOrigin;
   nsString mDatabaseFilePath;
@@ -1507,22 +1666,26 @@ private:
   nsresult
   DatabaseWork();
 
   nsresult
   DatabaseNotAvailable();
 
   nsresult
   EnsureDirectoryEntry(nsIFile* aEntry,
+                       bool aCreateIfNotExists,
                        bool aDirectory,
                        bool* aAlreadyExisted = nullptr);
 
   nsresult
   VerifyDatabaseInformation(mozIStorageConnection* aConnection);
 
+  already_AddRefed<QuotaObject>
+  GetQuotaObject();
+
   nsresult
   BeginLoadData();
 
   nsresult
   NestedRun() override;
 
   void
   GetResponse(LSRequestResponse& aResponse) override;
@@ -1662,16 +1825,54 @@ private:
   void
   GetResponse(LSSimpleRequestResponse& aResponse) override;
 };
 
 /*******************************************************************************
  * Other class declarations
  ******************************************************************************/
 
+class ArchivedOriginInfo
+{
+  nsCString mOriginSuffix;
+  nsCString mOriginNoSuffix;
+
+public:
+  static ArchivedOriginInfo*
+  Create(nsIPrincipal* aPrincipal);
+
+  const nsCString&
+  OriginSuffix() const
+  {
+    return mOriginSuffix;
+  }
+
+  const nsCString&
+  OriginNoSuffix() const
+  {
+    return mOriginNoSuffix;
+  }
+
+  const nsCString
+  Origin() const
+  {
+    return mOriginSuffix + NS_LITERAL_CSTRING(":") + mOriginNoSuffix;
+  }
+
+  nsresult
+  BindToStatement(mozIStorageStatement* aStatement) const;
+
+private:
+  ArchivedOriginInfo(const nsACString& aOriginSuffix,
+                     const nsACString& aOriginNoSuffix)
+    : mOriginSuffix(aOriginSuffix)
+    , mOriginNoSuffix(aOriginNoSuffix)
+  { }
+};
+
 class QuotaClient final
   : public mozilla::dom::quota::Client
 {
   class ClearPrivateBrowsingRunnable;
   class Observer;
 
   static QuotaClient* sInstance;
   static bool sObserversRegistered;
@@ -1835,16 +2036,20 @@ StaticAutoPtr<ObserverHashtable> gObserv
 
 Atomic<uint32_t, Relaxed> gOriginLimitKB(kDefaultOriginLimitKB);
 
 typedef nsDataHashtable<nsCStringHashKey, int64_t> UsageHashtable;
 
 // Can only be touched on the Quota Manager I/O thread.
 StaticAutoPtr<UsageHashtable> gUsages;
 
+typedef nsTHashtable<nsCStringHashKey> ArchivedOriginHashtable;
+
+StaticAutoPtr<ArchivedOriginHashtable> gArchivedOrigins;
+
 bool
 IsOnConnectionThread()
 {
   MOZ_ASSERT(gConnectionThread);
   return gConnectionThread->IsOnConnectionThread();
 }
 
 void
@@ -1862,16 +2067,127 @@ InitUsageForOrigin(const nsACString& aOr
   if (!gUsages) {
     gUsages = new UsageHashtable();
   }
 
   MOZ_ASSERT(!gUsages->Contains(aOrigin));
   gUsages->Put(aOrigin, aUsage);
 }
 
+nsresult
+LoadArchivedOrigins()
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(!gArchivedOrigins);
+
+  QuotaManager* quotaManager = QuotaManager::Get();
+  MOZ_ASSERT(quotaManager);
+
+  // Ensure that the webappsstore.sqlite is moved to new place.
+  nsresult rv = quotaManager->EnsureStorageIsInitialized();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCOMPtr<mozIStorageConnection> connection;
+  rv = CreateArchiveStorageConnection(quotaManager->GetStoragePath(),
+                                      getter_AddRefs(connection));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (!connection) {
+    gArchivedOrigins = new ArchivedOriginHashtable();
+    return NS_OK;
+  }
+
+  nsCOMPtr<mozIStorageStatement> stmt;
+  rv = connection->CreateStatement(NS_LITERAL_CSTRING(
+    "SELECT DISTINCT originAttributes || ':' || originKey "
+      "FROM webappsstore2;"
+  ), getter_AddRefs(stmt));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsAutoPtr<ArchivedOriginHashtable> archivedOrigins(
+    new ArchivedOriginHashtable());
+
+  bool hasResult;
+  while (NS_SUCCEEDED(rv = stmt->ExecuteStep(&hasResult)) && hasResult) {
+    nsCString origin;
+    rv = stmt->GetUTF8String(0, origin);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    archivedOrigins->PutEntry(origin);
+  }
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  gArchivedOrigins = archivedOrigins.forget();
+  return NS_OK;
+}
+
+nsresult
+GetUsage(mozIStorageConnection* aConnection,
+         ArchivedOriginInfo* aArchivedOriginInfo,
+         int64_t* aUsage)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(aConnection);
+  MOZ_ASSERT(aUsage);
+
+  nsresult rv;
+
+  nsCOMPtr<mozIStorageStatement> stmt;
+  if (aArchivedOriginInfo) {
+    rv = aConnection->CreateStatement(NS_LITERAL_CSTRING(
+      "SELECT sum(length(key) + length(value)) "
+      "FROM webappsstore2 "
+      "WHERE originKey = :originKey "
+      "AND originAttributes = :originAttributes;"
+    ), getter_AddRefs(stmt));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = aArchivedOriginInfo->BindToStatement(stmt);
+  } else {
+    rv = aConnection->CreateStatement(NS_LITERAL_CSTRING(
+      "SELECT sum(length(key) + length(value)) "
+      "FROM data"
+    ), getter_AddRefs(stmt));
+  }
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  bool hasResult;
+  rv = stmt->ExecuteStep(&hasResult);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (NS_WARN_IF(!hasResult)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  int64_t usage;
+  rv = stmt->GetInt64(0, &usage);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  *aUsage = usage;
+  return NS_OK;
+}
+
 } // namespace
 
 /*******************************************************************************
  * Exported functions
  ******************************************************************************/
 
 PBackgroundLSObjectParent*
 AllocPBackgroundLSObjectParent(const PrincipalInfo& aPrincipalInfo,
@@ -3858,16 +4174,21 @@ PrepareDatastoreOp::Open()
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
 
     rv = principal->GetPrivateBrowsingId(&mPrivateBrowsingId);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
+
+    mArchivedOriginInfo = ArchivedOriginInfo::Create(principal);
+    if (NS_WARN_IF(!mArchivedOriginInfo)) {
+      return NS_ERROR_FAILURE;
+    }
   }
 
   // This service has to be started on the main thread currently.
   nsCOMPtr<mozIStorageService> ss;
   if (NS_WARN_IF(!(ss = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID)))) {
     return NS_ERROR_FAILURE;
   }
 
@@ -4094,76 +4415,95 @@ PrepareDatastoreOp::SendToIOThread()
 
   return NS_OK;
 }
 
 nsresult
 PrepareDatastoreOp::DatabaseWork()
 {
   AssertIsOnIOThread();
+  MOZ_ASSERT(mArchivedOriginInfo);
   MOZ_ASSERT(mState == State::Nesting);
   MOZ_ASSERT(mNestedState == NestedState::DatabaseWorkOpen);
 
   if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) ||
       !MayProceedOnNonOwningThread()) {
     return NS_ERROR_FAILURE;
   }
 
   QuotaManager* quotaManager = QuotaManager::Get();
   MOZ_ASSERT(quotaManager);
 
+  nsresult rv;
+
+  if (!gArchivedOrigins) {
+    rv = LoadArchivedOrigins();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+    MOZ_ASSERT(gArchivedOrigins);
+  }
+
+  bool hasDataForMigration =
+    gArchivedOrigins->GetEntry(mArchivedOriginInfo->Origin());
+
+  bool createIfNotExists = mParams.createIfNotExists() || hasDataForMigration;
+
   nsCOMPtr<nsIFile> directoryEntry;
-  nsresult rv =
-    quotaManager->EnsureOriginIsInitialized(PERSISTENCE_TYPE_DEFAULT,
-                                            mSuffix,
-                                            mGroup,
-                                            mOrigin,
-                                            mParams.createIfNotExists(),
-                                            getter_AddRefs(directoryEntry));
+  rv = quotaManager->EnsureOriginIsInitialized(PERSISTENCE_TYPE_DEFAULT,
+                                               mSuffix,
+                                               mGroup,
+                                               mOrigin,
+                                               createIfNotExists,
+                                               getter_AddRefs(directoryEntry));
   if (rv == NS_ERROR_NOT_AVAILABLE) {
     return DatabaseNotAvailable();
   }
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   rv = directoryEntry->Append(NS_LITERAL_STRING(LS_DIRECTORY_NAME));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
-  rv = EnsureDirectoryEntry(directoryEntry, /* aIsDirectory */ true);
+  rv = EnsureDirectoryEntry(directoryEntry,
+                            createIfNotExists,
+                            /* aIsDirectory */ true);
   if (rv == NS_ERROR_NOT_AVAILABLE) {
     return DatabaseNotAvailable();
   }
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   rv = directoryEntry->Append(NS_LITERAL_STRING(DATA_FILE_NAME));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   bool alreadyExisted;
   rv = EnsureDirectoryEntry(directoryEntry,
+                            createIfNotExists,
                             /* aIsDirectory */ false,
                             &alreadyExisted);
   if (rv == NS_ERROR_NOT_AVAILABLE) {
     return DatabaseNotAvailable();
   }
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   if (alreadyExisted) {
     MOZ_ASSERT(gUsages);
     MOZ_ASSERT(gUsages->Get(mOrigin, &mUsage));
   } else {
-    InitUsageForOrigin(mOrigin, 0);
+    MOZ_ASSERT(mUsage == 0);
+    InitUsageForOrigin(mOrigin, mUsage);
   }
 
   rv = directoryEntry->GetPath(mDatabaseFilePath);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   nsCOMPtr<mozIStorageConnection> connection;
@@ -4174,16 +4514,103 @@ PrepareDatastoreOp::DatabaseWork()
     return rv;
   }
 
   rv = VerifyDatabaseInformation(connection);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
+  if (hasDataForMigration) {
+    MOZ_ASSERT(mUsage == 0);
+
+    rv = AttachArchiveDatabase(quotaManager->GetStoragePath(), connection);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    int64_t newUsage;
+    rv = GetUsage(connection, mArchivedOriginInfo, &newUsage);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    RefPtr<QuotaObject> quotaObject = GetQuotaObject();
+    MOZ_ASSERT(quotaObject);
+
+    if (!quotaObject->MaybeUpdateSize(newUsage, /* aTruncate */ true)) {
+      return NS_ERROR_FILE_NO_DEVICE_SPACE;
+    }
+
+    mozStorageTransaction transaction(connection, false,
+                                  mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+    nsCOMPtr<mozIStorageStatement> stmt;
+    rv = connection->CreateStatement(NS_LITERAL_CSTRING(
+      "INSERT INTO data (key, value) "
+        "SELECT key, value "
+        "FROM webappsstore2 "
+        "WHERE originKey = :originKey "
+        "AND originAttributes = :originAttributes;"
+
+    ), getter_AddRefs(stmt));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = mArchivedOriginInfo->BindToStatement(stmt);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = stmt->Execute();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = connection->CreateStatement(NS_LITERAL_CSTRING(
+      "DELETE FROM webappsstore2 "
+        "WHERE originKey = :originKey "
+        "AND originAttributes = :originAttributes;"
+    ), getter_AddRefs(stmt));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = mArchivedOriginInfo->BindToStatement(stmt);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = stmt->Execute();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = transaction.Commit();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = DetachArchiveDatabase(connection);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    MOZ_ASSERT(gArchivedOrigins);
+    MOZ_ASSERT(gArchivedOrigins->GetEntry(mArchivedOriginInfo->Origin()));
+    gArchivedOrigins->RemoveEntry(mArchivedOriginInfo->Origin());
+
+    mUsage = newUsage;
+
+    MOZ_ASSERT(gUsages);
+    MOZ_ASSERT(gUsages->Contains(mOrigin));
+    gUsages->Put(mOrigin, newUsage);
+  }
+
   // Must close connection before dispatching otherwise we might race with the
   // connection thread which needs to open the same database.
   MOZ_ALWAYS_SUCCEEDS(connection->Close());
 
   // Must set this before dispatching otherwise we will race with the owning
   // thread.
   mNestedState = NestedState::BeginLoadData;
 
@@ -4214,30 +4641,31 @@ PrepareDatastoreOp::DatabaseNotAvailable
     return rv;
   }
 
   return NS_OK;
 }
 
 nsresult
 PrepareDatastoreOp::EnsureDirectoryEntry(nsIFile* aEntry,
+                                         bool aCreateIfNotExists,
                                          bool aIsDirectory,
                                          bool* aAlreadyExisted)
 {
   AssertIsOnIOThread();
   MOZ_ASSERT(aEntry);
 
   bool exists;
   nsresult rv = aEntry->Exists(&exists);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   if (!exists) {
-    if (!mParams.createIfNotExists()) {
+    if (!aCreateIfNotExists) {
       return NS_ERROR_NOT_AVAILABLE;
     }
 
     if (aIsDirectory) {
       rv = aEntry->Create(nsIFile::DIRECTORY_TYPE, 0755);
       if (NS_WARN_IF(NS_FAILED(rv))) {
         return rv;
       }
@@ -4290,16 +4718,38 @@ PrepareDatastoreOp::VerifyDatabaseInform
 
   if (NS_WARN_IF(!QuotaManager::AreOriginsEqualOnDisk(mOrigin, origin))) {
     return NS_ERROR_FILE_CORRUPTED;
   }
 
   return NS_OK;
 }
 
+already_AddRefed<QuotaObject>
+PrepareDatastoreOp::GetQuotaObject()
+{
+  MOZ_ASSERT(IsOnOwningThread() || IsOnIOThread());
+  MOZ_ASSERT(!mGroup.IsEmpty());
+  MOZ_ASSERT(!mOrigin.IsEmpty());
+  MOZ_ASSERT(!mDatabaseFilePath.IsEmpty());
+
+  QuotaManager* quotaManager = QuotaManager::Get();
+  MOZ_ASSERT(quotaManager);
+
+  RefPtr<QuotaObject> quotaObject =
+    quotaManager->GetQuotaObject(PERSISTENCE_TYPE_DEFAULT,
+                                 mGroup,
+                                 mOrigin,
+                                 mDatabaseFilePath,
+                                 mUsage);
+  MOZ_ASSERT(quotaObject);
+
+  return quotaObject.forget();
+}
+
 nsresult
 PrepareDatastoreOp::BeginLoadData()
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mState == State::Nesting);
   MOZ_ASSERT(mNestedState == NestedState::BeginLoadData);
   MOZ_ASSERT(!mConnection);
 
@@ -4398,26 +4848,17 @@ PrepareDatastoreOp::GetResponse(LSReques
   }
 
   if (!mDatastore) {
     MOZ_ASSERT(mUsage == mDEBUGUsage);
 
     RefPtr<QuotaObject> quotaObject;
 
     if (mPrivateBrowsingId == 0) {
-      MOZ_ASSERT(!mDatabaseFilePath.IsEmpty());
-
-      QuotaManager* quotaManager = QuotaManager::Get();
-      MOZ_ASSERT(quotaManager);
-
-      quotaObject = quotaManager->GetQuotaObject(PERSISTENCE_TYPE_DEFAULT,
-                                                 mGroup,
-                                                 mOrigin,
-                                                 mDatabaseFilePath,
-                                                 mUsage);
+      quotaObject = GetQuotaObject();
       MOZ_ASSERT(quotaObject);
     }
 
     mDatastore = new Datastore(mOrigin,
                                mPrivateBrowsingId,
                                mUsage,
                                mDirectoryLock.forget(),
                                mConnection.forget(),
@@ -4940,16 +5381,59 @@ PreloadedOp::GetResponse(LSSimpleRequest
 
   LSSimpleRequestPreloadedResponse preloadedResponse;
   preloadedResponse.preloaded() = preloaded;
 
   aResponse = preloadedResponse;
 }
 
 /*******************************************************************************
+ * ArchivedOriginInfo
+ ******************************************************************************/
+
+// static
+ArchivedOriginInfo*
+ArchivedOriginInfo::Create(nsIPrincipal* aPrincipal)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aPrincipal);
+
+  nsCString originAttrSuffix;
+  nsCString originKey;
+  nsresult rv = GenerateOriginKey(aPrincipal, originAttrSuffix, originKey);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return nullptr;
+  }
+
+  return new ArchivedOriginInfo(originAttrSuffix, originKey);
+}
+
+nsresult
+ArchivedOriginInfo::BindToStatement(mozIStorageStatement* aStatement) const
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(aStatement);
+
+  nsresult rv =
+    aStatement->BindUTF8StringByName(NS_LITERAL_CSTRING("originKey"),
+                                     mOriginNoSuffix);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = aStatement->BindUTF8StringByName(NS_LITERAL_CSTRING("originAttributes"),
+                                        mOriginSuffix);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+/*******************************************************************************
  * QuotaClient
  ******************************************************************************/
 
 QuotaClient* QuotaClient::sInstance = nullptr;
 bool QuotaClient::sObserversRegistered = false;
 
 QuotaClient::QuotaClient()
   : mShutdownRequested(false)
@@ -5076,37 +5560,18 @@ QuotaClient::InitOrigin(PersistenceType 
     //       For now, get the usage from the database.
 
     nsCOMPtr<mozIStorageConnection> connection;
     rv = CreateStorageConnection(file, aOrigin, getter_AddRefs(connection));
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
 
-    nsCOMPtr<mozIStorageStatement> stmt;
-    rv = connection->CreateStatement(NS_LITERAL_CSTRING(
-      "SELECT sum(length(key) + length(value)) "
-      "FROM data"
-    ), getter_AddRefs(stmt));
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      return rv;
-    }
-
-    bool hasResult;
-    rv = stmt->ExecuteStep(&hasResult);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      return rv;
-    }
-
-    if (NS_WARN_IF(!hasResult)) {
-      return NS_ERROR_FAILURE;
-    }
-
     int64_t usage;
-    rv = stmt->GetInt64(0, &usage);
+    rv = GetUsage(connection, /* aArchivedOriginInfo */ nullptr, &usage);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
 
     InitUsageForOrigin(aOrigin, usage);
 
     aUsageInfo->AppendToDatabaseUsage(uint64_t(usage));
   }
@@ -5208,16 +5673,18 @@ QuotaClient::OnOriginClearCompleted(Pers
 }
 
 void
 QuotaClient::ReleaseIOThreadObjects()
 {
   AssertIsOnIOThread();
 
   gUsages = nullptr;
+
+  gArchivedOrigins = nullptr;
 }
 
 void
 QuotaClient::AbortOperations(const nsACString& aOrigin)
 {
   AssertIsOnBackgroundThread();
 
   // A PrepareDatastoreOp object could already acquire a directory lock for
--- a/dom/localstorage/LSObject.cpp
+++ b/dom/localstorage/LSObject.cpp
@@ -626,17 +626,23 @@ LSObject::DoRequestSynchronously(const L
   // The owning thread is synchronously blocked while the request is
   // asynchronously processed on the DOM File thread.
   nsresult rv = helper->StartAndReturnResponse(aResponse);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   if (aResponse.type() == LSRequestResponse::Tnsresult) {
-    return aResponse.get_nsresult();
+    nsresult errorCode = aResponse.get_nsresult();
+
+    if (errorCode == NS_ERROR_FILE_NO_DEVICE_SPACE) {
+      errorCode = NS_ERROR_DOM_QUOTA_EXCEEDED_ERR;
+    }
+
+    return errorCode;
   }
 
   return NS_OK;
 }
 
 nsresult
 LSObject::EnsureDatabase()
 {
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..71b2d1e5f90de4334169d5ec4c1c385615358a02
GIT binary patch
literal 1162
zc$^FHW@Zs#U|`^2*c4mtv;0t{qA`%iz{J47#~{N{T#{dun4YRvT$qztk{TMq$-sR6
z)|Mz&x93r%72FJrEMFNJ7{L0u`M#Xg@eK7it$)f_C(s}*At@mxr63?60SH)=+}LUw
zI5|Ethq2kt<lQ5%b#a0K6GH&dYA%pv3_9#Td>D`b2f}K7u%#ifVbLIrW@!N2@uol%
z{w?CONCrBbp@ES>63K*|V%@}|<c!QR6n~@vjZ4Ec^6ka5dD5XG3=cN`Uf#6n)S}yh
zv$q8qcXsGDt>(J(!1amN3IT6dkFCev$zIQTQ~12yO4?`M()=r;{s)a82yxF6JGe=q
zc7BQb_dAx)=UJEUtN)V`)oO9NJ2L9%?~D3@n<Z;yj|YmFUVrTMW%~AySIZwAOZzi9
zZOWZJo4=NQ`ka4pRfIhQe(;0eBHtwc&aItyE%w{bwdX#)XNKRh&kwr`%>F3XiMqW^
z3Y-=i*K&TnOx^X@^S(Sf`0?UnM}0f1f6KnB)a_UL_swEoNa=H>#nGWhOG3gI?%BTb
zW&7spkGtNz`?<VVdVAjb^9Cy}UCi7OQmdJf$5(Kv_Q#6yo!);OXa898{mGNLakF>G
zJn!BxbN`Rxugkw|*%z^Q_tt$=kG(40QnmNi-y8Mp_#NNyzxVU%v#+E7)_re}wDX%*
zDb|%aZOYX>=Z-%%d1<qLmFvySt>6AX{aP?X_i|$7Q&%faPuuzX)*N2IbM)fo^WXT~
z9-eO2x%+d`)x5V`oOXXre*7!X`tH5DsP)EtpDRVDh1$=t&G7HC;auJwxr;}<x}5LZ
zH>=3!%}37NPfe?|^Xm4$CVw?zZ)aG3*#5dVr&$R&_Iqt<?&|xwXFe5fI<q;zXY-OZ
z&r0p&mU}+04Anj5w|c9+Oxo#^h_K3ueojH>m+QYaxUqBF?R)!vZqaitoN<2jhBdz)
zzHzJ$dg5sjd*)K=jvEV$H%x!}^L>CfBa<96u1qHZw|_|^h(ynVtdJ~-krO~>;>t}h
zGX)q9I94Of)CZe}HD4iIh?%nxE^KJri)0#>e8vjNXBft_vVpWS10f?&u$URd0{|kw
BoZbKc
--- a/dom/localstorage/test/unit/head.js
+++ b/dom/localstorage/test/unit/head.js
@@ -101,16 +101,91 @@ function resetOrigin(principal, callback
 {
   let request =
     Services.qms.resetStoragesForPrincipal(principal, "default", "ls");
   request.callback = callback;
 
   return request;
 }
 
+function installPackage(packageName)
+{
+  let directoryService = Cc["@mozilla.org/file/directory_service;1"]
+                         .getService(Ci.nsIProperties);
+
+  let currentDir = directoryService.get("CurWorkD", Ci.nsIFile);
+
+  let packageFile = currentDir.clone();
+  packageFile.append(packageName + ".zip");
+
+  let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
+                  .createInstance(Ci.nsIZipReader);
+  zipReader.open(packageFile);
+
+  let entryNames = [];
+  let entries = zipReader.findEntries(null);
+  while (entries.hasMore()) {
+    let entry = entries.getNext();
+    entryNames.push(entry);
+  }
+  entryNames.sort();
+
+  for (let entryName of entryNames) {
+    let zipentry = zipReader.getEntry(entryName);
+
+    let file = getRelativeFile(entryName);
+
+    if (zipentry.isDirectory) {
+      file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+    } else {
+      let istream = zipReader.getInputStream(entryName);
+
+      var ostream = Cc["@mozilla.org/network/file-output-stream;1"]
+                    .createInstance(Ci.nsIFileOutputStream);
+      ostream.init(file, -1, parseInt("0644", 8), 0);
+
+      let bostream = Cc['@mozilla.org/network/buffered-output-stream;1']
+                     .createInstance(Ci.nsIBufferedOutputStream);
+      bostream.init(ostream, 32768);
+
+      bostream.writeFrom(istream, istream.available());
+
+      istream.close();
+      bostream.close();
+    }
+  }
+
+  zipReader.close();
+}
+
+function getProfileDir()
+{
+  let directoryService =
+    Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
+
+  return directoryService.get("ProfD", Ci.nsIFile);
+}
+
+// Given a "/"-delimited path relative to the profile directory,
+// return an nsIFile representing the path.  This does not test
+// for the existence of the file or parent directories.
+// It is safe even on Windows where the directory separator is not "/",
+// but make sure you're not passing in a "\"-delimited path.
+function getRelativeFile(relativePath)
+{
+  let profileDir = getProfileDir();
+
+  let file = profileDir.clone();
+  relativePath.split('/').forEach(function(component) {
+    file.append(component);
+  });
+
+  return file;
+}
+
 function repeatChar(count, ch) {
   if (count == 0) {
     return "";
   }
 
   let result = ch;
   let count2 = count / 2;
 
@@ -118,20 +193,23 @@ function repeatChar(count, ch) {
   while (result.length <= count2) {
     result += result;
   }
 
   // Use substring to hit the precise length target without using extra memory.
   return result + result.substring(0, count - result.length);
 }
 
-function getPrincipal(url)
+function getPrincipal(url, attrs)
 {
   let uri = Services.io.newURI(url);
-  return Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+  if (!attrs) {
+    attrs = {};
+  }
+  return Services.scriptSecurityManager.createCodebasePrincipal(uri, attrs);
 }
 
 function getCurrentPrincipal()
 {
   return Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal);
 }
 
 function getLocalStorage(principal)
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ff1cd89602076ad5bd332b0da22aee4e498c6648
GIT binary patch
literal 1727
zc$^FHW@Zs#U|`^25Xmd|`F!eCohB0lLnjLZ0}q1?Lvm4SVo7RzN|IhyacBr91M`aS
zTcVZ#acKoN10%~<Mg|73?$%((yxRr>dtQrj&z*8)!6j#Xzb2)E#sd!AETzR<H49d5
zJu*#+wJ7yC=a=t#sVDoGx7$j_oa0G%d;Q0VamRM+fOR)3W5Zai92#50CT(CSau7X~
zyxS=1V0gH9E^DyXqO@7hPc2s2>-hB5t(yvS^gk~<E@o>|@B03e|064Zz0WE$L+@Sv
z;c|;*;p=0Sr-J$qZjDo1Jyq`WEzz|HC+J_u-*QNyuGMPq4$n!wUmPS}FzQx$o;w^{
zA17d@*xgZ@Et;9GfB*bs^Rm@9q#Yk`4c#>{E$_OX!QYwd8SX9kn#T1~;G~7B?^C9w
z7ktk#Zo9p9#og6rZpZf2H$UuNBi-Dz`lr0gDjx@-p9Z}%8tR_uJmfyxty$B?XSGb<
z@{z>S2`ZKcqSybDE?={=Oz-agO%s>7&Res@zw*M@JsV{-Chj}h!M!eE+KjndiVmLG
zyTUPP^DKGG8qT*ZN8euV|EcL-pti1F<*VV0vaXx{`{&F(cIIy21BS-kb!_hJj+J*W
zFo{|(zFVGY*&zN`=+PO`9g`U4-K&l*Sa|i|Jol`<wx>S!@#)U->b-j7+KiJI&$C?G
z68<V_;VkBqm&`($(M~slo%XN#E%R7I@0QV}YkM|F?&(|`cmKorx9xL9*m6BZ<6~oE
z_xu)U7P<WI=hs7<<M;xPA9uanrX}qsfA+q(>leA{nICG;J#u{2v-4x0yclDqUqpJK
z-v_@#q6Po|uDpMu!p<rpd$se|$^Tb;+<#lOo*@99+@yfXE#s7Af)S9%08Daxz~ok3
zl3$dVo~l<|n3GwO3QllafQfAjW`g79`*KppGt}d>{wZIbK!db|q=b}|f`EVoAYe>#
zW2<TCY_K`OV>_{ZkHFT&2?9(=Hp>ESev&=exCCe`Ljxm&Fwo}m)TG3Mf?|*ZP^{kx
zw0b9|^=~ga=HE_{U@uVjwPrkZ@fKr?j$ZO5Db<ySmMu{5I=L`OOKQs{l_jBhueM#B
zW3?%E(%TIe-Z%V^so~74;W-zx^U<aiTe2T=U8=J`dF{7N-M{zQH}Bh3+UQMwp>bNh
zP%@Xrp5<%Rhe{Dv$AI<gras6QFMaG48~yE{uWd%{hvLskA76b>m>6!(fFJy0KbQU0
zy8Qb4tLN-x=gX;m){&R*_y0a$UH{wvRp}=kZRU!(&V1@wp89(B!)NadZr#3Fx5>O`
zTlqd2yWV>~F7fYOt_R8QpLav&bWKe`nA*|i)Mu-o9Bp24>aKnL`m*Qm_P&z+cIt<M
z@4BVyMZZ4fev?=Cv7+Mdlo!VgFTVTm^4-+U(@G6bEjwBC_s^!^wR7g}eRnTEEPm<7
zSN#beUq3p2^l%m<e)k`^{bcj8sA2<-!;fvUk7ioc&Ge1hd~?lJd(G3C{{w%||K%Qe
z&9dY3xpM97*L(jO)Rcv({0u9LQu|!=yWqm^6RIyugw9Vs?X$c7N`3LV`r8Rxf4Xh`
zc})H5*0lBFZIg3KlAmphoBBFzXWsU0*WQ#I+Mu&(vWZ;k%cn=q`c2IYpZRU~)#DjD
zT?v6Yb&-Fz{dl?mpCN$&-(zYQmCJYNR^hy@k4$9upHW+TGw0f?`BR>5`M;ueO6pv<
z?9#pB*XGR?_1rgg?zEG8?%2tE(NprYntSB)lUF~EooZiwHmhb*Z(jACwfpzqdeicL
zifh@VV~ch6{@U<YJ;0lhNsbv;SuFu}04P8k8bK`7lA9G$a-*eUWD_y7qXgVr4UPFg
r5%kQ-3dx*!%*4!S$Y#C-iek-ntdMMnVK6HjNFy^4{s+>lSV24hnorEJ
new file mode 100644
--- /dev/null
+++ b/dom/localstorage/test/unit/test_archive.js
@@ -0,0 +1,80 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var testGenerator = testSteps();
+
+function* testSteps()
+{
+  const lsArchiveFile = "storage/ls-archive.sqlite";
+
+  const principalInfo = {
+    url: "http://example.com",
+    attrs: {}
+  };
+
+  function checkStorage()
+  {
+    let principal = getPrincipal(principalInfo.url, principalInfo.attrs);
+    let storage = getLocalStorage(principal);
+    try {
+      storage.open();
+      ok(true, "Did not throw");
+    } catch(ex) {
+      ok(false, "Should not have thrown");
+    }
+  }
+
+  info("Setting pref");
+
+  Services.prefs.setBoolPref("dom.storage.next_gen", true);
+
+  // Profile 1 - Archive file is a directory.
+  info("Clearing");
+
+  clear(continueToNextStepSync);
+  yield undefined;
+
+  let archiveFile = getRelativeFile(lsArchiveFile);
+
+  archiveFile.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+
+  checkStorage();
+
+  // Profile 2 - Corrupted archive file.
+  info("Clearing");
+
+  clear(continueToNextStepSync);
+  yield undefined;
+
+  let ostream = Cc["@mozilla.org/network/file-output-stream;1"]
+                .createInstance(Ci.nsIFileOutputStream);
+  ostream.init(archiveFile, -1, parseInt("0644", 8), 0);
+  ostream.write("foobar", 6);
+  ostream.close();
+
+  checkStorage();
+
+  // Profile 3 - Nonupdateable archive file.
+  info("Clearing");
+
+  clear(continueToNextStepSync);
+  yield undefined;
+
+  info("Installing package");
+
+  // The profile contains storage.sqlite and storage/ls-archive.sqlite
+  // storage/ls-archive.sqlite was taken from FF 54 to force an upgrade.
+  // There's just one record in the webappsstore2 table. The record was
+  // modified by renaming the origin attribute userContextId to userContextKey.
+  // This triggers an error during the upgrade.
+  installPackage("archive_profile");
+
+  let fileSize = archiveFile.fileSize;
+  ok(fileSize > 0, "archive file size is greater than zero");
+
+  checkStorage();
+
+  finishTest();
+}
new file mode 100644
--- /dev/null
+++ b/dom/localstorage/test/unit/test_migration.js
@@ -0,0 +1,57 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var testGenerator = testSteps();
+
+function* testSteps()
+{
+  const principalInfos = [
+    { url: "http://localhost", attrs: {} },
+    { url: "http://www.mozilla.org", attrs: {} },
+    { url: "http://example.com", attrs: {} },
+    { url: "http://example.org", attrs: { userContextId: 5 } }
+  ];
+
+  const data = {
+    key: "foo",
+    value: "bar"
+  };
+
+  info("Setting pref");
+
+  Services.prefs.setBoolPref("dom.storage.next_gen", true);
+
+  info("Clearing");
+
+  clear(continueToNextStepSync);
+  yield undefined;
+
+  info("Installing package");
+
+  // The profile contains storage.sqlite and webappsstore.sqlite. The file
+  // create_db.js in the package was run locally, specifically it was
+  // temporarily added to xpcshell.ini and then executed:
+  // mach xpcshell-test --interactive dom/localstorage/test/unit/create_db.js
+  installPackage("migration_profile");
+
+  info("Getting storages");
+
+  let storages = [];
+  for (let i = 0; i < principalInfos.length; i++) {
+    let principalInfo = principalInfos[i];
+    let principal = getPrincipal(principalInfo.url, principalInfo.attrs);
+    let storage = getLocalStorage(principal);
+    storages.push(storage);
+  }
+
+  info("Verifying data");
+
+  for (let i = 0; i < storages.length; i++) {
+    let value = storages[i].getItem(data.key + i);
+    is(value, data.value + i, "Correct value");
+  }
+
+  finishTest();
+}
--- a/dom/localstorage/test/unit/xpcshell.ini
+++ b/dom/localstorage/test/unit/xpcshell.ini
@@ -1,9 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 [DEFAULT]
 head = head.js
+support-files =
+  archive_profile.zip
+  migration_profile.zip
 
+[test_archive.js]
 [test_eviction.js]
 [test_groupLimit.js]
+[test_migration.js]
--- a/dom/quota/ActorsParent.cpp
+++ b/dom/quota/ActorsParent.cpp
@@ -35,16 +35,17 @@
 #include "mozilla/dom/cache/QuotaClient.h"
 #include "mozilla/dom/indexedDB/ActorsParent.h"
 #include "mozilla/dom/localstorage/ActorsParent.h"
 #include "mozilla/dom/quota/PQuotaParent.h"
 #include "mozilla/dom/quota/PQuotaRequestParent.h"
 #include "mozilla/dom/quota/PQuotaUsageRequestParent.h"
 #include "mozilla/dom/simpledb/ActorsParent.h"
 #include "mozilla/dom/StorageActivityService.h"
+#include "mozilla/dom/StorageDBUpdater.h"
 #include "mozilla/ipc/BackgroundParent.h"
 #include "mozilla/ipc/BackgroundUtils.h"
 #include "mozilla/IntegerRange.h"
 #include "mozilla/Mutex.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Services.h"
 #include "mozilla/StaticPtr.h"
 #include "mozilla/TextUtils.h"
@@ -206,16 +207,17 @@ enum AppId {
 // The name of the file that we use to load/save the last access time of an
 // origin.
 // XXX We should get rid of old metadata files at some point, bug 1343576.
 #define METADATA_FILE_NAME ".metadata"
 #define METADATA_TMP_FILE_NAME ".metadata-tmp"
 #define METADATA_V2_FILE_NAME ".metadata-v2"
 #define METADATA_V2_TMP_FILE_NAME ".metadata-v2-tmp"
 
+#define WEB_APPS_STORE_FILE_NAME "webappsstore.sqlite"
 #define LS_ARCHIVE_FILE_NAME "ls-archive.sqlite"
 #define LS_ARCHIVE_TMP_FILE_NAME "ls-archive-tmp.sqlite"
 
 /******************************************************************************
  * SQLite functions
  ******************************************************************************/
 
 int32_t
@@ -259,16 +261,75 @@ CreateTables(mozIStorageConnection* aCon
   rv = aConnection->SetSchemaVersion(kStorageVersion);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   return NS_OK;
 }
 
+nsresult
+CreateWebAppsStoreConnection(nsIFile* aWebAppsStoreFile,
+                             mozIStorageService* aStorageService,
+                             mozIStorageConnection** aConnection)
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(aWebAppsStoreFile);
+  MOZ_ASSERT(aStorageService);
+  MOZ_ASSERT(aConnection);
+
+  // Check if the old database exists at all.
+  bool exists;
+  nsresult rv = aWebAppsStoreFile->Exists(&exists);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (!exists) {
+    // webappsstore.sqlite doesn't exist, return a null connection.
+    *aConnection = nullptr;
+    return NS_OK;
+  }
+
+  bool isDirectory;
+  rv = aWebAppsStoreFile->IsDirectory(&isDirectory);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (isDirectory) {
+    QM_WARNING("webappsstore.sqlite is not a file!");
+    *aConnection = nullptr;
+    return NS_OK;
+  }
+
+  nsCOMPtr<mozIStorageConnection> connection;
+  rv = aStorageService->OpenUnsharedDatabase(aWebAppsStoreFile,
+                                             getter_AddRefs(connection));
+  if (rv == NS_ERROR_FILE_CORRUPTED) {
+    // Don't throw an error, leave a corrupted webappsstore database as it is.
+    *aConnection = nullptr;
+    return NS_OK;
+  }
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = StorageDBUpdater::Update(connection);
+  if (NS_FAILED(rv)) {
+    // Don't throw an error, leave a non-updateable webappsstore database as
+    // it is.
+    *aConnection = nullptr;
+    return NS_OK;
+  }
+
+  connection.forget(aConnection);
+  return NS_OK;
+}
+
 /******************************************************************************
  * Quota manager class declarations
  ******************************************************************************/
 
 } // namespace
 
 class DirectoryLockImpl final
   : public DirectoryLock
@@ -4832,20 +4893,17 @@ QuotaManager::UpgradeStorageFrom2_0To2_1
 
   return NS_OK;
 }
 
 nsresult
 QuotaManager::MaybeRemoveLocalStorageData()
 {
   AssertIsOnIOThread();
-
-  if (CachedNextGenLocalStorageEnabled()) {
-    return NS_OK;
-  }
+  MOZ_ASSERT(!CachedNextGenLocalStorageEnabled());
 
   // Cleanup the tmp file first, if there's any.
   nsCOMPtr<nsIFile> lsArchiveTmpFile;
   nsresult rv = NS_NewLocalFile(mStoragePath,
                                 false,
                                 getter_AddRefs(lsArchiveTmpFile));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
@@ -5032,16 +5090,212 @@ QuotaManager::MaybeRemoveLocalStorageDir
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
   }
 
   return NS_OK;
 }
 
+nsresult
+QuotaManager::MaybeCreateLocalStorageArchive()
+{
+  AssertIsOnIOThread();
+  MOZ_ASSERT(CachedNextGenLocalStorageEnabled());
+
+  // Check if the archive was already successfully created.
+  nsCOMPtr<nsIFile> lsArchiveFile;
+  nsresult rv = NS_NewLocalFile(mStoragePath,
+                                false,
+                                getter_AddRefs(lsArchiveFile));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = lsArchiveFile->Append(NS_LITERAL_STRING(LS_ARCHIVE_FILE_NAME));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  bool exists;
+  rv = lsArchiveFile->Exists(&exists);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (exists) {
+    // ls-archive.sqlite already exists, nothing to create.
+    return NS_OK;
+  }
+
+  // Get the storage service first, we will need it at multiple places.
+  nsCOMPtr<mozIStorageService> ss =
+    do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  // Get the web apps store file.
+  nsCOMPtr<nsIFile> webAppsStoreFile;
+  rv = NS_NewLocalFile(mBasePath, false, getter_AddRefs(webAppsStoreFile));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = webAppsStoreFile->Append(NS_LITERAL_STRING(WEB_APPS_STORE_FILE_NAME));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  // Now check if the web apps store is useable.
+  nsCOMPtr<mozIStorageConnection> connection;
+  rv = CreateWebAppsStoreConnection(webAppsStoreFile,
+                                    ss,
+                                    getter_AddRefs(connection));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (connection) {
+    // Find out the journal mode.
+    nsCOMPtr<mozIStorageStatement> stmt;
+    rv = connection->CreateStatement(NS_LITERAL_CSTRING(
+      "PRAGMA journal_mode;"
+    ), getter_AddRefs(stmt));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    bool hasResult;
+    rv = stmt->ExecuteStep(&hasResult);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    MOZ_ASSERT(hasResult);
+
+    nsCString journalMode;
+    rv = stmt->GetUTF8String(0, journalMode);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = stmt->Finalize();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    if (journalMode.EqualsLiteral("wal")) {
+      // We don't copy the WAL file, so make sure the old database is fully
+      // checkpointed.
+      rv = connection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+        "PRAGMA wal_checkpoint(TRUNCATE);"
+      ));
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+    }
+
+    // Explicitely close the connection before the old database is copied.
+    rv = connection->Close();
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    // Copy the old database. The database is copied from
+    // <profile>/webappsstore.sqlite to
+    // <profile>/storage/ls-archive-tmp.sqlite
+    // We use a "-tmp" postfix since we are not done yet.
+    nsCOMPtr<nsIFile> storageDir;
+    rv = NS_NewLocalFile(mStoragePath, false, getter_AddRefs(storageDir));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = webAppsStoreFile->CopyTo(storageDir,
+                                  NS_LITERAL_STRING(LS_ARCHIVE_TMP_FILE_NAME));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    nsCOMPtr<nsIFile> lsArchiveTmpFile;
+    rv = NS_NewLocalFile(mStoragePath, false, getter_AddRefs(lsArchiveTmpFile));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = lsArchiveTmpFile->Append(NS_LITERAL_STRING(LS_ARCHIVE_TMP_FILE_NAME));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    if (journalMode.EqualsLiteral("wal")) {
+      nsCOMPtr<mozIStorageConnection> lsArchiveTmpConnection;
+      rv = ss->OpenUnsharedDatabase(lsArchiveTmpFile,
+                                    getter_AddRefs(lsArchiveTmpConnection));
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+
+      // The archive will only be used for lazy data migration. There won't be
+      // any concurrent readers and writers that could benefit from Write-Ahead
+      // Logging. So switch to a standard rollback journal. The standard
+      // rollback journal also provides atomicity across multiple attached
+      // databases which is import for the lazy data migration to work safely.
+      rv = lsArchiveTmpConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+        "PRAGMA journal_mode = DELETE;"
+      ));
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+
+      // The connection will be now implicitely closed (it's always safer to
+      // close database connection before we manipulate underlying file)
+    }
+
+    // Finally, rename ls-archive-tmp.sqlite to ls-archive.sqlite
+    rv = lsArchiveTmpFile->MoveTo(nullptr,
+                                  NS_LITERAL_STRING(LS_ARCHIVE_FILE_NAME));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+  } else {
+    // If webappsstore database is not useable, just create an empty archive.
+
+    // Ensure the storage directory actually exists.
+    nsCOMPtr<nsIFile> storageDirectory;
+    rv = NS_NewLocalFile(GetStoragePath(),
+                         false,
+                         getter_AddRefs(storageDirectory));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    bool dummy;
+    rv = EnsureDirectory(storageDirectory, &dummy);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    nsCOMPtr<mozIStorageConnection> lsArchiveConnection;
+    rv = ss->OpenUnsharedDatabase(lsArchiveFile,
+                                  getter_AddRefs(lsArchiveConnection));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = StorageDBUpdater::Update(lsArchiveConnection);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+  }
+
+  return NS_OK;
+}
+
 #ifdef DEBUG
 
 void
 QuotaManager::AssertStorageIsInitialized() const
 {
   AssertIsOnIOThread();
   MOZ_ASSERT(mStorageInitialized);
 }
@@ -5211,17 +5465,21 @@ QuotaManager::EnsureStorageIsInitialized
     }
 
     rv = transaction.Commit();
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
   }
 
-  rv = MaybeRemoveLocalStorageData();
+  if (CachedNextGenLocalStorageEnabled()) {
+    rv = MaybeCreateLocalStorageArchive();
+  } else {
+    rv = MaybeRemoveLocalStorageData();
+  }
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   mStorageInitialized = true;
 
   return NS_OK;
 }
--- a/dom/quota/QuotaManager.h
+++ b/dom/quota/QuotaManager.h
@@ -501,16 +501,19 @@ private:
 
   nsresult
   MaybeRemoveLocalStorageData();
 
   nsresult
   MaybeRemoveLocalStorageDirectories();
 
   nsresult
+  MaybeCreateLocalStorageArchive();
+
+  nsresult
   InitializeRepository(PersistenceType aPersistenceType);
 
   nsresult
   InitializeOrigin(PersistenceType aPersistenceType,
                    const nsACString& aGroup,
                    const nsACString& aOrigin,
                    int64_t aAccessTime,
                    bool aPersisted,
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d5958dbd5946fef2835373ad735f9992dcaf0cbb
GIT binary patch
literal 1006
zc$^FHW@Zs#U|`^2*c4mtv;0t{qA`%iz{J47#~{N{T#{dun4YRvT$qztk{TMq$-sR6
z)|Mz&x93r%72FJrEMFNJ7{L0u`M#Xg@eK7it$)f_C(s}*At@mxr63?60SH)=+}LUw
zI5|Ethq2kt<lQ5%b#a0K6GH&p>O0IoeEu!svq%P7%h15cAPls+JT)n?pr9D!0u=id
z!o#9L7|s5-7tiKNhl(&f*!X*S)2364ZVS%d7G&Jnq1&{Y>&^q$Ct526yj?xE9(yNy
zJ?l;3^L8s~pLt93uZa2|G=3n&JxlE1CWYGhCGOwvSU#U;UB0jWPexR$#p&+IsH49x
z>IZI?td%_;C}Mj3vDcUB+dp0{e{?ME&*ZczclK=lTK4I4{>4=h_6+#J4}Ob$ll(ik
zcHXtvZ$H<b`}Cd}e#<^T>@G0-qg*HI_A)7OT4-F$`Smh&*I&>3^622li;o@k?X3PS
z`>s;AU+LdBi+v%b&y^NOhaN2n317Hp`^uN?o2x(WdiU<<@?PofdF#&`thjVBb4N(6
zW=0-g!KK<CE6R6z|8boCW6Ad?Pv*wW-XZh6d&A8AKZ?ID|FUIY#NORo_f0+as&q@$
z-dlff)U)Gve8d0V&#TYAj{aNsy*<*-Z(5~TSLU=SSNEJd{@CQD&H7cYH#4_>`~UQ7
z!3^EYiIGoTtvEex=kHr{cmdDRi<{4X<8ym>x>@J$&qY`B-fnT){WbaVuRQC!_v)h7
z8}ogx6rC1oKgTx1zsrVmd3WS49`WjOzHi^GBA+)OId?xbt<uh`+y9#U)rh^FVfkVE
z>)xDZCE(cawWYbM@8_QRRJiHP<^Z3~OV&IqwUb-!`Mfez_mto2t@bi$r%NKjDku6m
z1)X26|JvZj&TY5v?fbb!&$)2M`PCcN{CfDtu{!99r$y|UOQ}0<EG*tI{prv50p5&E
za?H3giv-;MC5<2wJ?pSSvJP6ZMK%*N7a+`RXsia3Sn~ucBu`)%%*qCF8xzA?Ag#>|
G;sF5X4}(wu
--- a/dom/quota/test/unit/head.js
+++ b/dom/quota/test/unit/head.js
@@ -218,17 +218,19 @@ function installPackage(packageName)
   entryNames.sort();
 
   for (let entryName of entryNames) {
     let zipentry = zipReader.getEntry(entryName);
 
     let file = getRelativeFile(entryName);
 
     if (zipentry.isDirectory) {
-      file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+      if (!file.exists()) {
+        file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+      }
     } else {
       let istream = zipReader.getInputStream(entryName);
 
       var ostream = Cc["@mozilla.org/network/file-output-stream;1"]
                     .createInstance(Ci.nsIFileOutputStream);
       ostream.init(file, -1, parseInt("0644", 8), 0);
 
       let bostream = Cc['@mozilla.org/network/buffered-output-stream;1']
new file mode 100644
--- /dev/null
+++ b/dom/quota/test/unit/test_createLocalStorage.js
@@ -0,0 +1,161 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function testSteps()
+{
+  const webAppsStoreFile = "webappsstore.sqlite";
+  const lsArchiveFile = "storage/ls-archive.sqlite";
+  const lsArchiveTmpFile = "storage/ls-archive-tmp.sqlite";
+
+  function checkArchiveFileNotExists()
+  {
+    info("Checking archive tmp file");
+
+    let archiveTmpFile = getRelativeFile(lsArchiveTmpFile);
+
+    let exists = archiveTmpFile.exists();
+    ok(!exists, "archive tmp file doesn't exist");
+
+    info("Checking archive file");
+
+    let archiveFile = getRelativeFile(lsArchiveFile);
+
+    exists = archiveFile.exists();
+    ok(!exists, "archive file doesn't exist");
+  }
+
+  function checkArchiveFileExists()
+  {
+    info("Checking archive tmp file");
+
+    let archiveTmpFile = getRelativeFile(lsArchiveTmpFile);
+
+    let exists = archiveTmpFile.exists();
+    ok(!exists, "archive tmp file doesn't exist");
+
+    info("Checking archive file");
+
+    let archiveFile = getRelativeFile(lsArchiveFile);
+
+    exists = archiveFile.exists();
+    ok(exists, "archive file does exist");
+
+    info("Checking archive file size");
+
+    let fileSize = archiveFile.fileSize;
+    ok(fileSize > 0, "archive file size is greater than zero");
+  }
+
+  info("Setting pref");
+
+  SpecialPowers.setBoolPref("dom.storage.next_gen", true);
+
+  // Profile 1 - Nonexistent apps store file.
+  info("Clearing");
+
+  let request = clear();
+  await requestFinished(request);
+
+  let appsStoreFile = getRelativeFile(webAppsStoreFile);
+
+  let exists = appsStoreFile.exists();
+  ok(!exists, "apps store file doesn't exist");
+
+  checkArchiveFileNotExists();
+
+  try {
+    request = init();
+    await requestFinished(request);
+
+    ok(true, "Should not have thrown");
+  } catch(ex) {
+    ok(false, "Should not have thrown");
+  }
+
+  checkArchiveFileExists();
+
+  // Profile 2 - apps store file is a directory.
+  info("Clearing");
+
+  request = clear();
+  await requestFinished(request);
+
+  appsStoreFile.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+
+  checkArchiveFileNotExists();
+
+  try {
+    request = init();
+    await requestFinished(request);
+
+    ok(true, "Should not have thrown");
+  } catch(ex) {
+    ok(false, "Should not have thrown");
+  }
+
+  checkArchiveFileExists();
+
+  appsStoreFile.remove(true);
+
+  // Profile 3 - Corrupted apps store file.
+  info("Clearing");
+
+  request = clear();
+  await requestFinished(request);
+
+  let ostream = Cc["@mozilla.org/network/file-output-stream;1"]
+                .createInstance(Ci.nsIFileOutputStream);
+  ostream.init(appsStoreFile, -1, parseInt("0644", 8), 0);
+  ostream.write("foobar", 6);
+  ostream.close();
+
+  checkArchiveFileNotExists();
+
+  try {
+    request = init();
+    await requestFinished(request);
+
+    ok(true, "Should not have thrown");
+  } catch(ex) {
+    ok(false, "Should not have thrown");
+  }
+
+  checkArchiveFileExists();
+
+  appsStoreFile.remove(false);
+
+  // Profile 4 - Nonupdateable apps store file.
+  info("Clearing");
+
+  request = clear();
+  await requestFinished(request);
+
+  info("Installing package");
+
+  // The profile contains storage.sqlite and webappsstore.sqlite
+  // webappstore.sqlite was taken from FF 54 to force an upgrade.
+  // There's just one record in the webappsstore2 table. The record was
+  // modified by renaming the origin attribute userContextId to userContextKey.
+  // This triggers an error during the upgrade.
+  installPackage("createLocalStorage_profile");
+
+  let fileSize = appsStoreFile.fileSize;
+  ok(fileSize > 0, "apps store file size is greater than zero");
+
+  checkArchiveFileNotExists();
+
+  try {
+    request = init();
+    await requestFinished(request);
+
+    ok(true, "Should not have thrown");
+  } catch(ex) {
+    ok(false, "Should not have thrown");
+  }
+
+  checkArchiveFileExists();
+
+  appsStoreFile.remove(false);
+}
--- a/dom/quota/test/unit/xpcshell.ini
+++ b/dom/quota/test/unit/xpcshell.ini
@@ -1,32 +1,34 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 [DEFAULT]
 head = head.js
 support-files =
   basics_profile.zip
+  createLocalStorage_profile.zip
   defaultStorageUpgrade_profile.zip
   getUsage_profile.zip
   idbSubdirUpgrade1_profile.zip
   idbSubdirUpgrade2_profile.zip
   morgueCleanup_profile.zip
   obsoleteOriginAttributes_profile.zip
   originAttributesUpgrade_profile.zip
   removeAppsUpgrade_profile.zip
   removeLocalStorage1_profile.zip
   removeLocalStorage2_profile.zip
   storagePersistentUpgrade_profile.zip
   tempMetadataCleanup_profile.zip
   version2_1upgrade_profile.zip
 
 [test_basics.js]
 [test_bad_origin_directory.js]
+[test_createLocalStorage.js]
 [test_defaultStorageUpgrade.js]
 [test_getUsage.js]
 [test_idbSubdirUpgrade.js]
 [test_initTemporaryStorage.js]
 [test_morgueCleanup.js]
 [test_obsoleteOriginAttributesUpgrade.js]
 [test_originAttributesUpgrade.js]
 [test_persist.js]
--- a/dom/storage/moz.build
+++ b/dom/storage/moz.build
@@ -9,16 +9,17 @@ with Files("**"):
 
 EXPORTS.mozilla.dom += [
     'LocalStorage.h',
     'LocalStorageManager.h',
     'PartitionedLocalStorage.h',
     'SessionStorageManager.h',
     'Storage.h',
     'StorageActivityService.h',
+    'StorageDBUpdater.h',
     'StorageIPC.h',
     'StorageNotifierService.h',
     'StorageObserver.h',
     'StorageUtils.h',
 ]
 
 UNIFIED_SOURCES += [
     'LocalStorage.cpp',