/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */

#include "FileSystemDataManager.h"

#include "ErrorList.h"
#include "FileSystemDatabaseManager.h"
#include "FileSystemDatabaseManagerVersion001.h"
#include "FileSystemDatabaseManagerVersion002.h"
#include "FileSystemFileManager.h"
#include "FileSystemHashSource.h"
#include "FileSystemParentTypes.h"
#include "ResultStatement.h"
#include "SchemaVersion001.h"
#include "SchemaVersion002.h"
#include "fs/FileSystemConstants.h"
#include "mozIStorageService.h"
#include "mozStorageCID.h"
#include "mozilla/Result.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/dom/FileSystemLog.h"
#include "mozilla/dom/FileSystemManagerParent.h"
#include "mozilla/dom/QMResult.h"
#include "mozilla/dom/quota/ClientImpl.h"
#include "mozilla/dom/quota/DirectoryLock.h"
#include "mozilla/dom/quota/QuotaCommon.h"
#include "mozilla/dom/quota/QuotaManager.h"
#include "mozilla/dom/quota/ResultExtensions.h"
#include "mozilla/dom/quota/UsageInfo.h"
#include "mozilla/ipc/BackgroundParent.h"
#include "nsBaseHashtable.h"
#include "nsCOMPtr.h"
#include "nsHashKeys.h"
#include "nsIFile.h"
#include "nsIFileURL.h"
#include "nsNetCID.h"
#include "nsServiceManagerUtils.h"
#include "nsString.h"
#include "nsThreadUtils.h"

namespace mozilla::dom::fs::data {

namespace {

// nsCStringHashKey with disabled memmove
class nsCStringHashKeyDM : public nsCStringHashKey {
 public:
  explicit nsCStringHashKeyDM(const nsCStringHashKey::KeyTypePointer aKey)
      : nsCStringHashKey(aKey) {}
  enum { ALLOW_MEMMOVE = false };
};

// When CheckedUnsafePtr's checking is enabled, it's necessary to ensure that
// the hashtable uses the copy constructor instead of memmove for moving entries
// since memmove will break CheckedUnsafePtr in a memory-corrupting way.
using FileSystemDataManagerHashKey =
    std::conditional<DiagnosticAssertEnabled::value, nsCStringHashKeyDM,
                     nsCStringHashKey>::type;

// Raw (but checked when the diagnostic assert is enabled) references as we
// don't want to keep FileSystemDataManager objects alive forever. When a
// FileSystemDataManager is destroyed it calls RemoveFileSystemDataManager
// to clear itself.
using FileSystemDataManagerHashtable =
    nsBaseHashtable<FileSystemDataManagerHashKey,
                    NotNull<CheckedUnsafePtr<FileSystemDataManager>>,
                    MovingNotNull<CheckedUnsafePtr<FileSystemDataManager>>>;

// This hashtable isn't protected by any mutex but it is only ever touched on
// the PBackground thread.
StaticAutoPtr<FileSystemDataManagerHashtable> gDataManagers;

RefPtr<FileSystemDataManager> GetFileSystemDataManager(const Origin& aOrigin) {
  ::mozilla::ipc::AssertIsOnBackgroundThread();

  if (gDataManagers) {
    auto maybeDataManager = gDataManagers->MaybeGet(aOrigin);
    if (maybeDataManager) {
      RefPtr<FileSystemDataManager> result(
          std::move(*maybeDataManager).unwrapBasePtr());
      return result;
    }
  }

  return nullptr;
}

void AddFileSystemDataManager(
    const Origin& aOrigin, const RefPtr<FileSystemDataManager>& aDataManager) {
  ::mozilla::ipc::AssertIsOnBackgroundThread();
  MOZ_ASSERT(!quota::QuotaManager::IsShuttingDown());

  if (!gDataManagers) {
    gDataManagers = new FileSystemDataManagerHashtable();
  }

  MOZ_ASSERT(!gDataManagers->Contains(aOrigin));
  gDataManagers->InsertOrUpdate(aOrigin,
                                WrapMovingNotNullUnchecked(aDataManager));
}

void RemoveFileSystemDataManager(const Origin& aOrigin) {
  ::mozilla::ipc::AssertIsOnBackgroundThread();

  MOZ_ASSERT(gDataManagers);
  const DebugOnly<bool> removed = gDataManagers->Remove(aOrigin);
  MOZ_ASSERT(removed);

  if (!gDataManagers->Count()) {
    gDataManagers = nullptr;
  }
}

Result<ResultConnection, QMResult> GetStorageConnection(
    const quota::OriginMetadata& aOriginMetadata,
    const int64_t aDirectoryLockId) {
  MOZ_ASSERT(aDirectoryLockId >= 0);

  // Ensure that storage is initialized and file system folder exists!
  QM_TRY_INSPECT(const auto& dbFileUrl,
                 GetDatabaseFileURL(aOriginMetadata, aDirectoryLockId));

  QM_TRY_INSPECT(
      const auto& storageService,
      QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_GET_TYPED(
          nsCOMPtr<mozIStorageService>, MOZ_SELECT_OVERLOAD(do_GetService),
          MOZ_STORAGE_SERVICE_CONTRACTID)));

  QM_TRY_UNWRAP(auto connection,
                QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
                    nsCOMPtr<mozIStorageConnection>, storageService,
                    OpenDatabaseWithFileURL, dbFileUrl, ""_ns,
                    mozIStorageService::CONNECTION_DEFAULT)));

  ResultConnection result(connection);

  return result;
}

}  // namespace

Result<EntryId, QMResult> GetRootHandle(const Origin& origin) {
  MOZ_ASSERT(!origin.IsEmpty());

  return FileSystemHashSource::GenerateHash(origin, kRootString);
}

Result<EntryId, QMResult> GetEntryHandle(
    const FileSystemChildMetadata& aHandle) {
  MOZ_ASSERT(!aHandle.parentId().IsEmpty());

  return FileSystemHashSource::GenerateHash(aHandle.parentId(),
                                            aHandle.childName());
}

FileSystemDataManager::FileSystemDataManager(
    const quota::OriginMetadata& aOriginMetadata,
    RefPtr<quota::QuotaManager> aQuotaManager,
    MovingNotNull<nsCOMPtr<nsIEventTarget>> aIOTarget,
    MovingNotNull<RefPtr<TaskQueue>> aIOTaskQueue)
    : mOriginMetadata(aOriginMetadata),
      mQuotaManager(std::move(aQuotaManager)),
      mBackgroundTarget(WrapNotNull(GetCurrentSerialEventTarget())),
      mIOTarget(std::move(aIOTarget)),
      mIOTaskQueue(std::move(aIOTaskQueue)),
      mRegCount(0),
      mVersion(0),
      mState(State::Initial) {}

FileSystemDataManager::~FileSystemDataManager() {
  NS_ASSERT_OWNINGTHREAD(FileSystemDataManager);
  MOZ_ASSERT(mState == State::Closed);
  MOZ_ASSERT(!mDatabaseManager);
}

RefPtr<FileSystemDataManager::CreatePromise>
FileSystemDataManager::GetOrCreateFileSystemDataManager(
    const quota::OriginMetadata& aOriginMetadata) {
  if (quota::QuotaManager::IsShuttingDown()) {
    return CreatePromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
  }

  if (RefPtr<FileSystemDataManager> dataManager =
          GetFileSystemDataManager(aOriginMetadata.mOrigin)) {
    if (dataManager->IsOpening()) {
      // We have to wait for the open to be finished before resolving the
      // promise. The manager can't close itself in the meantime because we
      // add a new registration in the lambda capture list.
      return dataManager->OnOpen()->Then(
          GetCurrentSerialEventTarget(), __func__,
          [dataManager = Registered<FileSystemDataManager>(dataManager)](
              const BoolPromise::ResolveOrRejectValue& aValue) {
            if (aValue.IsReject()) {
              return CreatePromise::CreateAndReject(aValue.RejectValue(),
                                                    __func__);
            }
            return CreatePromise::CreateAndResolve(dataManager, __func__);
          });
    }

    if (dataManager->IsClosing()) {
      // First, we need to wait for the close to be finished. After that the
      // manager is closed and it can't be opened again. The only option is
      // to create a new manager and open it. However, all this stuff is
      // asynchronous, so it can happen that something else called
      // `GetOrCreateFileSystemManager` in the meantime. For that reason, we
      // shouldn't try to create a new manager and open it here, a "recursive"
      // call to `GetOrCreateFileSystemManager` will handle any new situation.
      return dataManager->OnClose()->Then(
          GetCurrentSerialEventTarget(), __func__,
          [aOriginMetadata](const BoolPromise::ResolveOrRejectValue&) {
            return GetOrCreateFileSystemDataManager(aOriginMetadata);
          });
    }

    return CreatePromise::CreateAndResolve(
        Registered<FileSystemDataManager>(std::move(dataManager)), __func__);
  }

  RefPtr<quota::QuotaManager> quotaManager = quota::QuotaManager::Get();
  MOZ_ASSERT(quotaManager);

  QM_TRY_UNWRAP(auto streamTransportService,
                MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIEventTarget>,
                                        MOZ_SELECT_OVERLOAD(do_GetService),
                                        NS_STREAMTRANSPORTSERVICE_CONTRACTID),
                CreatePromise::CreateAndReject(NS_ERROR_FAILURE, __func__));

  nsCString taskQueueName("OPFS "_ns + aOriginMetadata.mOrigin);

  RefPtr<TaskQueue> ioTaskQueue =
      TaskQueue::Create(do_AddRef(streamTransportService), taskQueueName.get());

  auto dataManager = MakeRefPtr<FileSystemDataManager>(
      aOriginMetadata, std::move(quotaManager),
      WrapMovingNotNull(streamTransportService),
      WrapMovingNotNull(ioTaskQueue));

  AddFileSystemDataManager(aOriginMetadata.mOrigin, dataManager);

  return dataManager->BeginOpen()->Then(
      GetCurrentSerialEventTarget(), __func__,
      [dataManager = Registered<FileSystemDataManager>(dataManager)](
          const BoolPromise::ResolveOrRejectValue& aValue) {
        if (aValue.IsReject()) {
          return CreatePromise::CreateAndReject(aValue.RejectValue(), __func__);
        }

        return CreatePromise::CreateAndResolve(dataManager, __func__);
      });
}

// static
void FileSystemDataManager::AbortOperationsForLocks(
    const quota::Client::DirectoryLockIdTable& aDirectoryLockIds) {
  ::mozilla::ipc::AssertIsOnBackgroundThread();

  // XXX Share the iteration code with `InitiateShutdown`, for example by
  // creating a helper function which would take a predicate function.

  if (!gDataManagers) {
    return;
  }

  for (const auto& dataManager : gDataManagers->Values()) {
    // Check if the Manager holds an acquired DirectoryLock. Origin clearing
    // can't be blocked by this Manager if there is no acquired DirectoryLock.
    // If there is an acquired DirectoryLock, check if the table contains the
    // lock for the Manager.
    if (quota::Client::IsLockForObjectAcquiredAndContainedInLockTable(
            *dataManager, aDirectoryLockIds)) {
      dataManager->RequestAllowToClose();
    }
  }
}

// static
void FileSystemDataManager::InitiateShutdown() {
  ::mozilla::ipc::AssertIsOnBackgroundThread();

  if (!gDataManagers) {
    return;
  }

  for (const auto& dataManager : gDataManagers->Values()) {
    dataManager->RequestAllowToClose();
  }
}

// static
bool FileSystemDataManager::IsShutdownCompleted() {
  ::mozilla::ipc::AssertIsOnBackgroundThread();

  return !gDataManagers;
}

void FileSystemDataManager::AssertIsOnIOTarget() const {
  DebugOnly<bool> current = false;
  MOZ_ASSERT(NS_SUCCEEDED(mIOTarget->IsOnCurrentThread(&current)));
  MOZ_ASSERT(current);
}

void FileSystemDataManager::Register() { mRegCount++; }

void FileSystemDataManager::Unregister() {
  MOZ_ASSERT(mRegCount > 0);

  mRegCount--;

  if (IsInactive()) {
    BeginClose();
  }
}

void FileSystemDataManager::RegisterActor(
    NotNull<FileSystemManagerParent*> aActor) {
  MOZ_ASSERT(!mBackgroundThreadAccessible.Access()->mActors.Contains(aActor));

  mBackgroundThreadAccessible.Access()->mActors.Insert(aActor);
}

void FileSystemDataManager::UnregisterActor(
    NotNull<FileSystemManagerParent*> aActor) {
  MOZ_ASSERT(mBackgroundThreadAccessible.Access()->mActors.Contains(aActor));

  mBackgroundThreadAccessible.Access()->mActors.Remove(aActor);

  if (IsInactive()) {
    BeginClose();
  }
}

void FileSystemDataManager::RegisterAccessHandle(
    NotNull<FileSystemAccessHandle*> aAccessHandle) {
  MOZ_ASSERT(!mBackgroundThreadAccessible.Access()->mAccessHandles.Contains(
      aAccessHandle));

  mBackgroundThreadAccessible.Access()->mAccessHandles.Insert(aAccessHandle);
}

void FileSystemDataManager::UnregisterAccessHandle(
    NotNull<FileSystemAccessHandle*> aAccessHandle) {
  MOZ_ASSERT(mBackgroundThreadAccessible.Access()->mAccessHandles.Contains(
      aAccessHandle));

  mBackgroundThreadAccessible.Access()->mAccessHandles.Remove(aAccessHandle);

  if (IsInactive()) {
    BeginClose();
  }
}

RefPtr<BoolPromise> FileSystemDataManager::OnOpen() {
  MOZ_ASSERT(mState == State::Opening);

  return mOpenPromiseHolder.Ensure(__func__);
}

RefPtr<BoolPromise> FileSystemDataManager::OnClose() {
  MOZ_ASSERT(mState == State::Closing);

  return mClosePromiseHolder.Ensure(__func__);
}

// Note: Input can be temporary or main file id
Result<bool, QMResult> FileSystemDataManager::IsLocked(
    const FileId& aFileId) const {
  auto checkIfEntryIdIsLocked = [this, &aFileId]() -> Result<bool, QMResult> {
    QM_TRY_INSPECT(const EntryId& entryId,
                   mDatabaseManager->GetEntryId(aFileId));

    return IsLocked(entryId);
  };

  auto valueToSome = [](auto aValue) { return Some(std::move(aValue)); };

  QM_TRY_UNWRAP(Maybe<bool> maybeLocked,
                QM_OR_ELSE_LOG_VERBOSE_IF(
                    // Expression.
                    (checkIfEntryIdIsLocked().map(valueToSome)),
                    // Predicate.
                    IsSpecificError<NS_ERROR_DOM_NOT_FOUND_ERR>,
                    // Fallback.
                    ([](const auto&) -> Result<Maybe<bool>, QMResult> {
                      return Some(false);  // Non-existent files are not locked.
                    })));

  if (!maybeLocked) {
    // If the metadata is inaccessible, we block modifications.
    return true;
  }

  return *maybeLocked;
}

Result<bool, QMResult> FileSystemDataManager::IsLocked(
    const EntryId& aEntryId) const {
  return mExclusiveLocks.Contains(aEntryId) || mSharedLocks.Contains(aEntryId);
}

Result<FileId, QMResult> FileSystemDataManager::LockExclusive(
    const EntryId& aEntryId) {
  QM_TRY_UNWRAP(const bool isLocked, IsLocked(aEntryId));
  if (isLocked) {
    return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR));
  }

  QM_TRY_INSPECT(const FileId& fileId,
                 mDatabaseManager->EnsureFileId(aEntryId));

  // If the file has been removed, we should get a file not found error.
  // Otherwise, if usage tracking cannot be started because file size is not
  // known and attempts to read it are failing, lock is denied to freeze the
  // quota usage until the (external) blocker is gone or the file is removed.
  QM_TRY(QM_TO_RESULT(mDatabaseManager->BeginUsageTracking(fileId)));

  LOG_VERBOSE(("ExclusiveLock"));
  mExclusiveLocks.Insert(aEntryId);

  return fileId;
}

// TODO: Improve reporting of failures, see bug 1840811.
void FileSystemDataManager::UnlockExclusive(const EntryId& aEntryId) {
  MOZ_ASSERT(mExclusiveLocks.Contains(aEntryId));

  LOG_VERBOSE(("ExclusiveUnlock"));
  mExclusiveLocks.Remove(aEntryId);

  QM_TRY_INSPECT(const FileId& fileId, mDatabaseManager->GetFileId(aEntryId),
                 QM_VOID);

  // On error, usage tracking remains on to prevent writes until usage is
  // updated successfully.
  QM_TRY(MOZ_TO_RESULT(mDatabaseManager->UpdateUsage(fileId)), QM_VOID);
  QM_TRY(MOZ_TO_RESULT(mDatabaseManager->EndUsageTracking(fileId)), QM_VOID);
}

Result<FileId, QMResult> FileSystemDataManager::LockShared(
    const EntryId& aEntryId) {
  if (mExclusiveLocks.Contains(aEntryId)) {
    return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR));
  }

  auto& count = mSharedLocks.LookupOrInsert(aEntryId);
  if (!(1u + CheckedUint32(count)).isValid()) {  // don't make the count invalid
    return Err(QMResult(NS_ERROR_UNEXPECTED));
  }

  QM_TRY_INSPECT(const FileId& fileId,
                 mDatabaseManager->EnsureTemporaryFileId(aEntryId));

  // If the file has been removed, we should get a file not found error.
  // Otherwise, if usage tracking cannot be started because file size is not
  // known and attempts to read it are failing, lock is denied to freeze the
  // quota usage until the (external) blocker is gone or the file is removed.
  QM_TRY(QM_TO_RESULT(mDatabaseManager->BeginUsageTracking(fileId)));

  ++count;
  LOG_VERBOSE(("SharedLock %u", count));

  return fileId;
}

// TODO: Improve reporting of failures, see bug 1840811.
void FileSystemDataManager::UnlockShared(const EntryId& aEntryId,
                                         const FileId& aFileId, bool aAbort) {
  MOZ_ASSERT(!mExclusiveLocks.Contains(aEntryId));
  MOZ_ASSERT(mSharedLocks.Contains(aEntryId));

  auto entry = mSharedLocks.Lookup(aEntryId);
  MOZ_ASSERT(entry);

  MOZ_ASSERT(entry.Data() > 0);
  --entry.Data();

  LOG_VERBOSE(("SharedUnlock %u", *entry));

  if (0u == entry.Data()) {
    entry.Remove();
  }

  // On error, usage tracking remains on to prevent writes until usage is
  // updated successfully.
  QM_TRY(MOZ_TO_RESULT(mDatabaseManager->UpdateUsage(aFileId)), QM_VOID);
  QM_TRY(MOZ_TO_RESULT(mDatabaseManager->EndUsageTracking(aFileId)), QM_VOID);
  QM_TRY(
      MOZ_TO_RESULT(mDatabaseManager->MergeFileId(aEntryId, aFileId, aAbort)),
      QM_VOID);
}

FileMode FileSystemDataManager::GetMode(bool aKeepData) const {
  if (1 == mVersion) {
    return FileMode::EXCLUSIVE;
  }

  return aKeepData ? FileMode::SHARED_FROM_COPY : FileMode::SHARED_FROM_EMPTY;
}

bool FileSystemDataManager::IsInactive() const {
  auto data = mBackgroundThreadAccessible.Access();
  return !mRegCount && !data->mActors.Count() && !data->mAccessHandles.Count();
}

void FileSystemDataManager::RequestAllowToClose() {
  for (const auto& actor : mBackgroundThreadAccessible.Access()->mActors) {
    actor->RequestAllowToClose();
  }
}

RefPtr<BoolPromise> FileSystemDataManager::BeginOpen() {
  MOZ_ASSERT(mQuotaManager);
  MOZ_ASSERT(mState == State::Initial);

  mState = State::Opening;

  RefPtr<quota::ClientDirectoryLock> directoryLock =
      mQuotaManager->CreateDirectoryLock(
          quota::PERSISTENCE_TYPE_DEFAULT, mOriginMetadata,
          mozilla::dom::quota::Client::FILESYSTEM,
          /* aExclusive */ false);

  directoryLock->Acquire()
      ->Then(GetCurrentSerialEventTarget(), __func__,
             [self = RefPtr<FileSystemDataManager>(this),
              directoryLock = directoryLock](
                 const BoolPromise::ResolveOrRejectValue& value) mutable {
               if (value.IsReject()) {
                 return BoolPromise::CreateAndReject(value.RejectValue(),
                                                     __func__);
               }

               self->mDirectoryLock = std::move(directoryLock);

               return BoolPromise::CreateAndResolve(true, __func__);
             })
      ->Then(mQuotaManager->IOThread(), __func__,
             [self = RefPtr<FileSystemDataManager>(this)](
                 const BoolPromise::ResolveOrRejectValue& value) {
               if (value.IsReject()) {
                 return BoolPromise::CreateAndReject(value.RejectValue(),
                                                     __func__);
               }

               QM_TRY(MOZ_TO_RESULT(
                          EnsureFileSystemDirectory(self->mOriginMetadata)),
                      CreateAndRejectBoolPromise);

               return BoolPromise::CreateAndResolve(true, __func__);
             })
      ->Then(
          MutableIOTaskQueuePtr(), __func__,
          [self = RefPtr<FileSystemDataManager>(this)](
              const BoolPromise::ResolveOrRejectValue& value) {
            if (value.IsReject()) {
              return BoolPromise::CreateAndReject(value.RejectValue(),
                                                  __func__);
            }

            QM_TRY_UNWRAP(
                auto connection,
                fs::data::GetStorageConnection(self->mOriginMetadata,
                                               self->mDirectoryLock->Id()),
                CreateAndRejectBoolPromiseFromQMResult);

            QM_TRY_UNWRAP(
                self->mVersion,
                QM_OR_ELSE_WARN_IF(
                    // Expression.
                    SchemaVersion002::InitializeConnection(
                        connection, self->mOriginMetadata.mOrigin),
                    // Predicate.
                    ([](const auto&) { return true; }),
                    // Fallback.
                    ([&self, &connection](const auto&) {
                      QM_TRY_RETURN(SchemaVersion001::InitializeConnection(
                          connection, self->mOriginMetadata.mOrigin));
                    })),
                CreateAndRejectBoolPromiseFromQMResult);

            QM_TRY_UNWRAP(UniquePtr<FileSystemFileManager> fmPtr,
                          FileSystemFileManager::CreateFileSystemFileManager(
                              self->mOriginMetadata),
                          CreateAndRejectBoolPromiseFromQMResult);

            QM_TRY_UNWRAP(
                EntryId rootId,
                fs::data::GetRootHandle(self->mOriginMetadata.mOrigin),
                CreateAndRejectBoolPromiseFromQMResult);

            switch (self->mVersion) {
              case 1: {
                self->mDatabaseManager =
                    MakeUnique<FileSystemDatabaseManagerVersion001>(
                        self, std::move(connection), std::move(fmPtr), rootId);
                break;
              }

              case 2: {
                self->mDatabaseManager =
                    MakeUnique<FileSystemDatabaseManagerVersion002>(
                        self, std::move(connection), std::move(fmPtr), rootId);
                break;
              }

              default:
                break;
            }

            return BoolPromise::CreateAndResolve(true, __func__);
          })
      ->Then(GetCurrentSerialEventTarget(), __func__,
             [self = RefPtr<FileSystemDataManager>(this)](
                 const BoolPromise::ResolveOrRejectValue& value) {
               if (value.IsReject()) {
                 self->mState = State::Initial;

                 self->mOpenPromiseHolder.RejectIfExists(value.RejectValue(),
                                                         __func__);

               } else {
                 self->mState = State::Open;

                 self->mOpenPromiseHolder.ResolveIfExists(true, __func__);
               }
             });

  return OnOpen();
}

RefPtr<BoolPromise> FileSystemDataManager::BeginClose() {
  MOZ_ASSERT(mState != State::Closing && mState != State::Closed);
  MOZ_ASSERT(IsInactive());

  mState = State::Closing;

  InvokeAsync(MutableIOTaskQueuePtr(), __func__,
              [self = RefPtr<FileSystemDataManager>(this)]() {
                if (self->mDatabaseManager) {
                  self->mDatabaseManager->Close();
                  self->mDatabaseManager = nullptr;
                }

                return BoolPromise::CreateAndResolve(true, __func__);
              })
      ->Then(MutableBackgroundTargetPtr(), __func__,
             [self = RefPtr<FileSystemDataManager>(this)](
                 const BoolPromise::ResolveOrRejectValue&) {
               return self->mIOTaskQueue->BeginShutdown();
             })
      ->Then(MutableBackgroundTargetPtr(), __func__,
             [self = RefPtr<FileSystemDataManager>(this)](
                 const ShutdownPromise::ResolveOrRejectValue&) {
               self->mDirectoryLock = nullptr;

               RemoveFileSystemDataManager(self->mOriginMetadata.mOrigin);

               self->mState = State::Closed;

               self->mClosePromiseHolder.ResolveIfExists(true, __func__);
             });

  return OnClose();
}

}  // namespace mozilla::dom::fs::data
