summaryrefslogtreecommitdiffstats
path: root/dom/fs/parent/datamodel
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /dom/fs/parent/datamodel
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/fs/parent/datamodel')
-rw-r--r--dom/fs/parent/datamodel/FileSystemDataManager.cpp529
-rw-r--r--dom/fs/parent/datamodel/FileSystemDataManager.h161
-rw-r--r--dom/fs/parent/datamodel/FileSystemDatabaseManager.cpp58
-rw-r--r--dom/fs/parent/datamodel/FileSystemDatabaseManager.h151
-rw-r--r--dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp1002
-rw-r--r--dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h101
-rw-r--r--dom/fs/parent/datamodel/FileSystemFileManager.cpp267
-rw-r--r--dom/fs/parent/datamodel/FileSystemFileManager.h147
-rw-r--r--dom/fs/parent/datamodel/SchemaVersion001.cpp185
-rw-r--r--dom/fs/parent/datamodel/SchemaVersion001.h25
-rw-r--r--dom/fs/parent/datamodel/moz.build26
11 files changed, 2652 insertions, 0 deletions
diff --git a/dom/fs/parent/datamodel/FileSystemDataManager.cpp b/dom/fs/parent/datamodel/FileSystemDataManager.cpp
new file mode 100644
index 0000000000..73b5327d77
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDataManager.cpp
@@ -0,0 +1,529 @@
+/* -*- 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 "FileSystemDatabaseManager.h"
+#include "FileSystemDatabaseManagerVersion001.h"
+#include "FileSystemFileManager.h"
+#include "FileSystemHashSource.h"
+#include "ResultStatement.h"
+#include "SchemaVersion001.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/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 "nsProxyRelease.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 Origin& aOrigin, 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(aOrigin, 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, kRootName);
+}
+
+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,
+ MovingNotNull<nsCOMPtr<nsIEventTarget>> aIOTarget,
+ MovingNotNull<RefPtr<TaskQueue>> aIOTaskQueue)
+ : mOriginMetadata(aOriginMetadata),
+ mBackgroundTarget(WrapNotNull(GetCurrentSerialEventTarget())),
+ mIOTarget(std::move(aIOTarget)),
+ mIOTaskQueue(std::move(aIOTaskQueue)),
+ mRegCount(0),
+ mState(State::Initial) {}
+
+FileSystemDataManager::~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&) {
+ 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__);
+ }
+
+ 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, WrapMovingNotNull(streamTransportService),
+ WrapMovingNotNull(ioTaskQueue));
+
+ AddFileSystemDataManager(aOriginMetadata.mOrigin, dataManager);
+
+ return dataManager->BeginOpen()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [dataManager = Registered<FileSystemDataManager>(dataManager)](
+ const BoolPromise::ResolveOrRejectValue&) {
+ 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();
+ }
+}
+
+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__);
+}
+
+bool FileSystemDataManager::IsLocked(const EntryId& aEntryId) const {
+ return mExclusiveLocks.Contains(aEntryId);
+}
+
+bool FileSystemDataManager::LockExclusive(const EntryId& aEntryId) {
+ if (IsLocked(aEntryId)) {
+ return false;
+ }
+
+ LOG_VERBOSE(("ExclusiveLock"));
+ mExclusiveLocks.Insert(aEntryId);
+
+ return true;
+}
+
+void FileSystemDataManager::UnlockExclusive(const EntryId& aEntryId) {
+ MOZ_ASSERT(mExclusiveLocks.Contains(aEntryId));
+
+ LOG_VERBOSE(("ExclusiveUnlock"));
+ mExclusiveLocks.Remove(aEntryId);
+
+ QM_WARNONLY_TRY(MOZ_TO_RESULT(mDatabaseManager->UpdateUsage(aEntryId)));
+}
+
+bool FileSystemDataManager::LockShared(const EntryId& aEntryId) {
+ return LockExclusive(aEntryId);
+}
+
+void FileSystemDataManager::UnlockShared(const EntryId& aEntryId) {
+ UnlockExclusive(aEntryId);
+}
+
+bool FileSystemDataManager::IsInactive() const {
+ return !mRegCount && !mBackgroundThreadAccessible.Access()->mActors.Count();
+}
+
+void FileSystemDataManager::RequestAllowToClose() {
+ for (const auto& actor : mBackgroundThreadAccessible.Access()->mActors) {
+ actor->RequestAllowToClose();
+ }
+}
+
+RefPtr<BoolPromise> FileSystemDataManager::BeginOpen() {
+ MOZ_ASSERT(mState == State::Initial);
+
+ mState = State::Opening;
+
+ QM_TRY_UNWRAP(const NotNull<RefPtr<quota::QuotaManager>> quotaManager,
+ quota::QuotaManager::GetOrCreate(), CreateAndRejectBoolPromise);
+
+ RefPtr<quota::ClientDirectoryLock> directoryLock =
+ quotaManager->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(quotaManager->IOThread(), __func__,
+ [self = RefPtr<FileSystemDataManager>(this)](
+ const BoolPromise::ResolveOrRejectValue& value) mutable {
+ auto autoProxyReleaseManager = MakeScopeExit([&self] {
+ nsCOMPtr<nsISerialEventTarget> target =
+ self->MutableBackgroundTargetPtr();
+
+ NS_ProxyRelease("ReleaseFileSystemDataManager", target,
+ self.forget());
+ });
+
+ if (value.IsReject()) {
+ return BoolPromise::CreateAndReject(value.RejectValue(),
+ __func__);
+ }
+
+ QM_TRY(MOZ_TO_RESULT(
+ EnsureFileSystemDirectory(self->mOriginMetadata)),
+ CreateAndRejectBoolPromise);
+
+ return BoolPromise::CreateAndResolve(true, __func__);
+ })
+ ->Then(MutableIOTargetPtr(), __func__,
+ [self = RefPtr<FileSystemDataManager>(this)](
+ const BoolPromise::ResolveOrRejectValue& value) mutable {
+ auto autoProxyReleaseManager = MakeScopeExit([&self] {
+ nsCOMPtr<nsISerialEventTarget> target =
+ self->MutableBackgroundTargetPtr();
+
+ NS_ProxyRelease("ReleaseFileSystemDataManager", target,
+ self.forget());
+ });
+
+ if (value.IsReject()) {
+ return BoolPromise::CreateAndReject(value.RejectValue(),
+ __func__);
+ }
+
+ QM_TRY_UNWRAP(
+ auto connection,
+ fs::data::GetStorageConnection(self->mOriginMetadata.mOrigin,
+ self->mDirectoryLock->Id()),
+ CreateAndRejectBoolPromiseFromQMResult);
+
+ QM_TRY_UNWRAP(DatabaseVersion version,
+ SchemaVersion001::InitializeConnection(
+ connection, self->mOriginMetadata.mOrigin),
+ CreateAndRejectBoolPromiseFromQMResult);
+
+ if (1 == version) {
+ QM_TRY_UNWRAP(
+ FileSystemFileManager fmRes,
+ FileSystemFileManager::CreateFileSystemFileManager(
+ self->mOriginMetadata.mOrigin),
+ CreateAndRejectBoolPromiseFromQMResult);
+
+ QM_TRY_UNWRAP(
+ EntryId rootId,
+ fs::data::GetRootHandle(self->mOriginMetadata.mOrigin),
+ CreateAndRejectBoolPromiseFromQMResult);
+
+ self->mDatabaseManager =
+ MakeUnique<FileSystemDatabaseManagerVersion001>(
+ self, std::move(connection),
+ MakeUnique<FileSystemFileManager>(std::move(fmRes)),
+ rootId);
+ }
+
+ 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(MutableIOTargetPtr(), __func__,
+ [self = RefPtr<FileSystemDataManager>(this)]() mutable {
+ auto autoProxyReleaseManager = MakeScopeExit([&self] {
+ nsCOMPtr<nsISerialEventTarget> target =
+ self->MutableBackgroundTargetPtr();
+
+ NS_ProxyRelease("ReleaseFileSystemDataManager", target,
+ self.forget());
+ });
+
+ 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
diff --git a/dom/fs/parent/datamodel/FileSystemDataManager.h b/dom/fs/parent/datamodel/FileSystemDataManager.h
new file mode 100644
index 0000000000..12ebbd2b41
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDataManager.h
@@ -0,0 +1,161 @@
+/* -*- 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/. */
+
+#ifndef DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATAMANAGER_H_
+#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATAMANAGER_H_
+
+#include "ResultConnection.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/ThreadBound.h"
+#include "mozilla/dom/FileSystemHelpers.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/quota/CheckedUnsafePtr.h"
+#include "mozilla/dom/quota/CommonMetadata.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+#include "nsCOMPtr.h"
+#include "nsISupportsUtils.h"
+#include "nsString.h"
+#include "nsTHashSet.h"
+
+namespace mozilla {
+
+template <typename V, typename E>
+class Result;
+
+namespace dom {
+
+class FileSystemManagerParent;
+
+namespace fs {
+class FileSystemChildMetadata;
+} // namespace fs
+
+namespace quota {
+class DirectoryLock;
+} // namespace quota
+
+namespace fs::data {
+
+class FileSystemDatabaseManager;
+
+Result<EntryId, QMResult> GetRootHandle(const Origin& origin);
+
+Result<EntryId, QMResult> GetEntryHandle(
+ const FileSystemChildMetadata& aHandle);
+
+class FileSystemDataManager
+ : public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> {
+ public:
+ enum struct State : uint8_t { Initial = 0, Opening, Open, Closing, Closed };
+
+ FileSystemDataManager(const quota::OriginMetadata& aOriginMetadata,
+ MovingNotNull<nsCOMPtr<nsIEventTarget>> aIOTarget,
+ MovingNotNull<RefPtr<TaskQueue>> aIOTaskQueue);
+
+ // IsExclusive is true because we want to allow the move operations. There's
+ // always just one consumer anyway.
+ using CreatePromise = MozPromise<Registered<FileSystemDataManager>, nsresult,
+ /* IsExclusive */ true>;
+ static RefPtr<CreatePromise> GetOrCreateFileSystemDataManager(
+ const quota::OriginMetadata& aOriginMetadata);
+
+ static void AbortOperationsForLocks(
+ const quota::Client::DirectoryLockIdTable& aDirectoryLockIds);
+
+ static void InitiateShutdown();
+
+ static bool IsShutdownCompleted();
+
+ NS_INLINE_DECL_REFCOUNTING(FileSystemDataManager)
+
+ void AssertIsOnIOTarget() const;
+
+ const quota::OriginMetadata& OriginMetadataRef() const {
+ return mOriginMetadata;
+ }
+
+ nsISerialEventTarget* MutableBackgroundTargetPtr() const {
+ return mBackgroundTarget.get();
+ }
+
+ nsISerialEventTarget* MutableIOTargetPtr() const {
+ return mIOTaskQueue.get();
+ }
+
+ Maybe<quota::DirectoryLock&> MaybeDirectoryLockRef() const {
+ return ToMaybeRef(mDirectoryLock.get());
+ }
+
+ FileSystemDatabaseManager* MutableDatabaseManagerPtr() const {
+ MOZ_ASSERT(mDatabaseManager);
+
+ return mDatabaseManager.get();
+ }
+
+ void Register();
+
+ void Unregister();
+
+ void RegisterActor(NotNull<FileSystemManagerParent*> aActor);
+
+ void UnregisterActor(NotNull<FileSystemManagerParent*> aActor);
+
+ bool IsOpen() const { return mState == State::Open; }
+
+ RefPtr<BoolPromise> OnOpen();
+
+ RefPtr<BoolPromise> OnClose();
+
+ bool IsLocked(const EntryId& aEntryId) const;
+
+ bool LockExclusive(const EntryId& aEntryId);
+
+ void UnlockExclusive(const EntryId& aEntryId);
+
+ bool LockShared(const EntryId& aEntryId);
+
+ void UnlockShared(const EntryId& aEntryId);
+
+ protected:
+ virtual ~FileSystemDataManager();
+
+ bool IsInactive() const;
+
+ bool IsOpening() const { return mState == State::Opening; }
+
+ bool IsClosing() const { return mState == State::Closing; }
+
+ void RequestAllowToClose();
+
+ RefPtr<BoolPromise> BeginOpen();
+
+ RefPtr<BoolPromise> BeginClose();
+
+ // Things touched on background thread only.
+ struct BackgroundThreadAccessible {
+ nsTHashSet<FileSystemManagerParent*> mActors;
+ };
+ ThreadBound<BackgroundThreadAccessible> mBackgroundThreadAccessible;
+
+ const quota::OriginMetadata mOriginMetadata;
+ nsTHashSet<EntryId> mExclusiveLocks;
+ const NotNull<nsCOMPtr<nsISerialEventTarget>> mBackgroundTarget;
+ const NotNull<nsCOMPtr<nsIEventTarget>> mIOTarget;
+ const NotNull<RefPtr<TaskQueue>> mIOTaskQueue;
+ RefPtr<quota::DirectoryLock> mDirectoryLock;
+ UniquePtr<FileSystemDatabaseManager> mDatabaseManager;
+ MozPromiseHolder<BoolPromise> mOpenPromiseHolder;
+ MozPromiseHolder<BoolPromise> mClosePromiseHolder;
+ uint32_t mRegCount;
+ State mState;
+};
+
+} // namespace fs::data
+} // namespace dom
+} // namespace mozilla
+
+#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATAMANAGER_H_
diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManager.cpp b/dom/fs/parent/datamodel/FileSystemDatabaseManager.cpp
new file mode 100644
index 0000000000..7fc37da8d5
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDatabaseManager.cpp
@@ -0,0 +1,58 @@
+/* -*- 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 "FileSystemDatabaseManager.h"
+
+#include "FileSystemDatabaseManagerVersion001.h"
+#include "FileSystemFileManager.h"
+#include "ResultConnection.h"
+#include "ResultStatement.h"
+#include "SchemaVersion001.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+
+namespace mozilla::dom::fs::data {
+
+/* static */
+Result<quota::UsageInfo, QMResult> FileSystemDatabaseManager::GetUsage(
+ const ResultConnection& aConnection, const Origin& aOrigin) {
+ QM_TRY_INSPECT(const auto& databaseFile, GetDatabaseFile(aOrigin));
+
+ // If database is deleted between connection creation and now, error
+ int64_t dbSize = 0;
+ QM_TRY(QM_TO_RESULT(databaseFile->GetFileSize(&dbSize)));
+
+ quota::UsageInfo result(quota::DatabaseUsageType(Some(dbSize)));
+
+ DatabaseVersion version = 0;
+ QM_TRY(QM_TO_RESULT(aConnection->GetSchemaVersion(&version)));
+
+ switch (version) {
+ case 0: {
+ return result;
+ }
+
+ case 1: {
+ QM_TRY_INSPECT(
+ const Usage& fileUsage,
+ FileSystemDatabaseManagerVersion001::GetFileUsage(aConnection));
+
+ // XXX: DatabaseUsage is currently total usage for most forms of storage
+ result += quota::DatabaseUsageType(Some(fileUsage));
+
+ return result;
+ }
+
+ default:
+ break;
+ }
+
+ return Err(QMResult(NS_ERROR_NOT_IMPLEMENTED));
+}
+
+} // namespace mozilla::dom::fs::data
diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManager.h b/dom/fs/parent/datamodel/FileSystemDatabaseManager.h
new file mode 100644
index 0000000000..259cd4d392
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDatabaseManager.h
@@ -0,0 +1,151 @@
+/* -*- 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/. */
+
+#ifndef DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGER_H_
+#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGER_H_
+
+#include "ResultConnection.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+#include "mozilla/dom/quota/UsageInfo.h"
+#include "nsStringFwd.h"
+
+template <class T>
+class nsCOMPtr;
+
+class nsIFile;
+
+namespace mozilla {
+
+template <typename V, typename E>
+class Result;
+
+namespace dom::fs {
+
+class FileSystemChildMetadata;
+class FileSystemEntryMetadata;
+class FileSystemDirectoryListing;
+class FileSystemEntryPair;
+
+namespace data {
+
+class FileSystemDatabaseManager {
+ public:
+ /**
+ * @brief Obtains the current total usage for origin and connection.
+ *
+ * @return Result<quota::UsageInfo, QMResult> On success,
+ * - field UsageInfo::DatabaseUsage contains the sum of current
+ * total database and file usage,
+ * - field UsageInfo::FileUsage is not used and should be equal to Nothing.
+ *
+ * If the disk is inaccessible, various IO related errors may be returned.
+ */
+ static Result<quota::UsageInfo, QMResult> GetUsage(
+ const ResultConnection& aConnection, const Origin& aOrigin);
+
+ /**
+ * @brief Refreshes the stored file size.
+ *
+ * @param aEntry EntryId of the file whose size is refreshed.
+ */
+ virtual nsresult UpdateUsage(const EntryId& aEntry) = 0;
+
+ /**
+ * @brief Returns directory identifier, optionally creating it if it doesn't
+ * exist
+ *
+ * @param aHandle Current directory and filename
+ * @return Result<bool, QMResult> Directory identifier or error
+ */
+ virtual Result<EntryId, QMResult> GetOrCreateDirectory(
+ const FileSystemChildMetadata& aHandle, bool aCreate) = 0;
+
+ /**
+ * @brief Returns file identifier, optionally creating it if it doesn't exist
+ *
+ * @param aHandle Current directory and filename
+ * @return Result<bool, QMResult> File identifier or error
+ */
+ virtual Result<EntryId, QMResult> GetOrCreateFile(
+ const FileSystemChildMetadata& aHandle, bool aCreate) = 0;
+
+ /**
+ * @brief Returns the properties of a file corresponding to a file handle
+ */
+ virtual nsresult GetFile(const EntryId& aEntryId, nsString& aType,
+ TimeStamp& lastModifiedMilliSeconds, Path& aPath,
+ nsCOMPtr<nsIFile>& aFile) const = 0;
+
+ virtual Result<FileSystemDirectoryListing, QMResult> GetDirectoryEntries(
+ const EntryId& aParent, PageNumber aPage) const = 0;
+
+ /**
+ * @brief Removes a directory
+ *
+ * @param aHandle Current directory and filename
+ * @return Result<bool, QMResult> False if file did not exist, otherwise true
+ * or error
+ */
+ virtual Result<bool, QMResult> RemoveDirectory(
+ const FileSystemChildMetadata& aHandle, bool aRecursive) = 0;
+
+ /**
+ * @brief Removes a file
+ *
+ * @param aHandle Current directory and filename
+ * @return Result<bool, QMResult> False if file did not exist, otherwise true
+ * or error
+ */
+ virtual Result<bool, QMResult> RemoveFile(
+ const FileSystemChildMetadata& aHandle) = 0;
+
+ /**
+ * @brief Rename a file/directory
+ *
+ * @param aHandle Source directory or file
+ * @param aNewName New entry name
+ * @return Result<bool, QMResult> False if entry didn't exist, otherwise true
+ * or error
+ */
+ virtual Result<bool, QMResult> RenameEntry(
+ const FileSystemEntryMetadata& aHandle, const Name& aNewName) = 0;
+
+ /**
+ * @brief Move a file/directory
+ *
+ * @param aHandle Source directory or file
+ * @param aNewDesignation Destination directory and entry name
+ * @return Result<bool, QMResult> False if entry didn't exist, otherwise true
+ * or error
+ */
+ virtual Result<bool, QMResult> MoveEntry(
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewDesignation) = 0;
+
+ /**
+ * @brief Tries to connect a parent directory to a file system item with a
+ * path, excluding the parent directory
+ *
+ * @param aHandle Pair of parent directory and child item candidates
+ * @return Result<Path, QMResult> Path or error if no it didn't exists
+ */
+ virtual Result<Path, QMResult> Resolve(
+ const FileSystemEntryPair& aEndpoints) const = 0;
+
+ /**
+ * @brief Close database connection.
+ */
+ virtual void Close() = 0;
+
+ virtual ~FileSystemDatabaseManager() = default;
+};
+
+} // namespace data
+} // namespace dom::fs
+} // namespace mozilla
+
+#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGER_H_
diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp
new file mode 100644
index 0000000000..87105a668b
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp
@@ -0,0 +1,1002 @@
+/* -*- 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 "FileSystemDatabaseManagerVersion001.h"
+
+#include "FileSystemFileManager.h"
+#include "ResultStatement.h"
+#include "mozStorageHelper.h"
+#include "mozilla/dom/FileSystemDataManager.h"
+#include "mozilla/dom/FileSystemHandle.h"
+#include "mozilla/dom/FileSystemLog.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/PFileSystemManager.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+
+namespace mozilla::dom {
+
+using FileSystemEntries = nsTArray<fs::FileSystemEntryMetadata>;
+
+namespace fs::data {
+
+namespace {
+
+Result<bool, QMResult> ApplyEntryExistsQuery(
+ const FileSystemConnection& aConnection, const nsACString& aQuery,
+ const FileSystemChildMetadata& aHandle) {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, aQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aHandle.parentId())));
+ QM_TRY(QM_TO_RESULT(stmt.BindNameByName("name"_ns, aHandle.childName())));
+
+ return stmt.YesOrNoQuery();
+}
+
+Result<bool, QMResult> ApplyEntryExistsQuery(
+ const FileSystemConnection& aConnection, const nsACString& aQuery,
+ const EntryId& aEntry) {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, aQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntry)));
+
+ return stmt.YesOrNoQuery();
+}
+
+Result<bool, QMResult> IsDirectoryEmpty(FileSystemConnection& mConnection,
+ const EntryId& aEntryId) {
+ const nsLiteralCString isDirEmptyQuery =
+ "SELECT EXISTS ("
+ "SELECT 1 FROM Entries WHERE parent = :parent "
+ ");"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, isDirEmptyQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aEntryId)));
+ QM_TRY_UNWRAP(bool childrenExist, stmt.YesOrNoQuery());
+
+ return !childrenExist;
+}
+
+Result<bool, QMResult> DoesDirectoryExist(
+ const FileSystemConnection& mConnection,
+ const FileSystemChildMetadata& aHandle) {
+ MOZ_ASSERT(!aHandle.parentId().IsEmpty());
+
+ const nsCString existsQuery =
+ "SELECT EXISTS "
+ "(SELECT 1 FROM Directories JOIN Entries USING (handle) "
+ "WHERE Directories.name = :name AND Entries.parent = :parent ) "
+ ";"_ns;
+
+ QM_TRY_RETURN(ApplyEntryExistsQuery(mConnection, existsQuery, aHandle));
+}
+
+Result<bool, QMResult> DoesDirectoryExist(
+ const FileSystemConnection& mConnection, const EntryId& aEntry) {
+ MOZ_ASSERT(!aEntry.IsEmpty());
+
+ const nsCString existsQuery =
+ "SELECT EXISTS "
+ "(SELECT 1 FROM Directories WHERE handle = :handle ) "
+ ";"_ns;
+
+ QM_TRY_RETURN(ApplyEntryExistsQuery(mConnection, existsQuery, aEntry));
+}
+
+Result<Path, QMResult> ResolveReversedPath(
+ const FileSystemConnection& aConnection,
+ const FileSystemEntryPair& aEndpoints) {
+ const nsCString pathQuery =
+ "WITH RECURSIVE followPath(handle, parent) AS ( "
+ "SELECT handle, parent "
+ "FROM Entries "
+ "WHERE handle=:entryId "
+ "UNION "
+ "SELECT Entries.handle, Entries.parent FROM followPath, Entries "
+ "WHERE followPath.parent=Entries.handle ) "
+ "SELECT COALESCE(Directories.name, Files.name), handle "
+ "FROM followPath "
+ "LEFT JOIN Directories USING(handle) "
+ "LEFT JOIN Files USING(handle);"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, pathQuery));
+ QM_TRY(
+ QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEndpoints.childId())));
+ QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep());
+
+ Path pathResult;
+ while (moreResults) {
+ QM_TRY_UNWRAP(Name entryName, stmt.GetNameByColumn(/* Column */ 0u));
+ QM_TRY_UNWRAP(EntryId entryId, stmt.GetEntryIdByColumn(/* Column */ 1u));
+
+ if (aEndpoints.parentId() == entryId) {
+ return pathResult;
+ }
+ pathResult.AppendElement(entryName);
+
+ QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep());
+ }
+
+ // Spec wants us to return 'null' for not-an-ancestor case
+ pathResult.Clear();
+ return pathResult;
+}
+
+Result<bool, QMResult> IsAncestor(const FileSystemConnection& aConnection,
+ const FileSystemEntryPair& aEndpoints) {
+ const nsCString pathQuery =
+ "WITH RECURSIVE followPath(handle, parent) AS ( "
+ "SELECT handle, parent "
+ "FROM Entries "
+ "WHERE handle=:entryId "
+ "UNION "
+ "SELECT Entries.handle, Entries.parent FROM followPath, Entries "
+ "WHERE followPath.parent=Entries.handle ) "
+ "SELECT EXISTS "
+ "(SELECT 1 FROM followPath "
+ "WHERE handle=:possibleAncestor ) "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, pathQuery));
+ QM_TRY(
+ QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEndpoints.childId())));
+ QM_TRY(QM_TO_RESULT(
+ stmt.BindEntryIdByName("possibleAncestor"_ns, aEndpoints.parentId())));
+
+ return stmt.YesOrNoQuery();
+}
+
+Result<bool, QMResult> DoesFileExist(const FileSystemConnection& aConnection,
+ const FileSystemChildMetadata& aHandle) {
+ MOZ_ASSERT(!aHandle.parentId().IsEmpty());
+
+ const nsCString existsQuery =
+ "SELECT EXISTS "
+ "(SELECT 1 FROM Files JOIN Entries USING (handle) "
+ "WHERE Files.name = :name AND Entries.parent = :parent ) "
+ ";"_ns;
+
+ QM_TRY_RETURN(ApplyEntryExistsQuery(aConnection, existsQuery, aHandle));
+}
+
+Result<bool, QMResult> DoesFileExist(const FileSystemConnection& aConnection,
+ const EntryId& aEntry) {
+ MOZ_ASSERT(!aEntry.IsEmpty());
+
+ const nsCString existsQuery =
+ "SELECT EXISTS "
+ "(SELECT 1 FROM Files WHERE handle = :handle ) "
+ ";"_ns;
+
+ QM_TRY_RETURN(ApplyEntryExistsQuery(aConnection, existsQuery, aEntry));
+}
+
+Result<EntryId, QMResult> FindParent(const FileSystemConnection& aConnection,
+ const EntryId& aEntryId) {
+ const nsCString aParentQuery =
+ "SELECT handle FROM Entries "
+ "WHERE handle IN ( "
+ "SELECT parent FROM Entries WHERE "
+ "handle = :entryId ) "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, aParentQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEntryId)));
+ QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep());
+
+ if (!moreResults) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ QM_TRY_UNWRAP(EntryId parentId, stmt.GetEntryIdByColumn(/* Column */ 0u));
+ return parentId;
+}
+
+nsresult GetFileAttributes(const FileSystemConnection& aConnection,
+ const EntryId& aEntryId, ContentType& aType) {
+ const nsLiteralCString getFileLocation =
+ "SELECT type FROM Files INNER JOIN Entries USING(handle) "
+ "WHERE handle = :entryId "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, getFileLocation));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEntryId)));
+ QM_TRY_UNWRAP(bool hasEntries, stmt.ExecuteStep());
+
+ // Type is an optional attribute
+ if (!hasEntries || stmt.IsNullByColumn(/* Column */ 0u)) {
+ return NS_OK;
+ }
+
+ QM_TRY_UNWRAP(aType, stmt.GetContentTypeByColumn(/* Column */ 0u));
+
+ return NS_OK;
+}
+
+nsresult GetEntries(const FileSystemConnection& aConnection,
+ const nsACString& aUnboundStmt, const EntryId& aParent,
+ PageNumber aPage, bool aDirectory,
+ FileSystemEntries& aEntries) {
+ // The entries inside a directory are sent to the child process in batches
+ // of pageSize items. Large value ensures that iteration is less often delayed
+ // by IPC messaging and querying the database.
+ // TODO: The current value 1024 is not optimized.
+ // TODO: Value "pageSize" is shared with the iterator implementation and
+ // should be defined in a common place.
+ const int32_t pageSize = 1024;
+
+ QM_TRY_UNWRAP(bool exists, DoesDirectoryExist(aConnection, aParent));
+ if (!exists) {
+ return NS_ERROR_DOM_NOT_FOUND_ERR;
+ }
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, aUnboundStmt));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aParent)));
+ QM_TRY(QM_TO_RESULT(stmt.BindPageNumberByName("pageSize"_ns, pageSize)));
+ QM_TRY(QM_TO_RESULT(
+ stmt.BindPageNumberByName("pageOffset"_ns, aPage * pageSize)));
+
+ QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep());
+
+ while (moreResults) {
+ QM_TRY_UNWRAP(EntryId entryId, stmt.GetEntryIdByColumn(/* Column */ 0u));
+ QM_TRY_UNWRAP(Name entryName, stmt.GetNameByColumn(/* Column */ 1u));
+
+ FileSystemEntryMetadata metadata(entryId, entryName, aDirectory);
+ aEntries.AppendElement(metadata);
+
+ QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep());
+ }
+
+ return NS_OK;
+}
+
+Result<EntryId, QMResult> GetUniqueEntryId(
+ const FileSystemConnection& aConnection,
+ const FileSystemChildMetadata& aHandle) {
+ const nsCString existsQuery =
+ "SELECT EXISTS "
+ "(SELECT 1 FROM Entries "
+ "WHERE handle = :handle )"
+ ";"_ns;
+
+ FileSystemChildMetadata generatorInput = aHandle;
+
+ const size_t maxRounds = 1024u;
+
+ for (size_t hangGuard = 0u; hangGuard < maxRounds; ++hangGuard) {
+ QM_TRY_UNWRAP(EntryId entryId, fs::data::GetEntryHandle(generatorInput));
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, existsQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+
+ QM_TRY_UNWRAP(bool alreadyInUse, stmt.YesOrNoQuery());
+
+ if (!alreadyInUse) {
+ return entryId;
+ }
+
+ generatorInput.parentId() = entryId;
+ }
+
+ return Err(QMResult(NS_ERROR_UNEXPECTED));
+}
+
+Result<EntryId, QMResult> FindEntryId(const FileSystemConnection& aConnection,
+ const FileSystemChildMetadata& aHandle,
+ bool aIsFile) {
+ const nsCString aDirectoryQuery =
+ "SELECT Entries.handle FROM Directories JOIN Entries USING (handle) "
+ "WHERE Directories.name = :name AND Entries.parent = :parent "
+ ";"_ns;
+
+ const nsCString aFileQuery =
+ "SELECT Entries.handle FROM Files JOIN Entries USING (handle) "
+ "WHERE Files.name = :name AND Entries.parent = :parent "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(
+ aConnection, aIsFile ? aFileQuery : aDirectoryQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aHandle.parentId())));
+ QM_TRY(QM_TO_RESULT(stmt.BindNameByName("name"_ns, aHandle.childName())));
+ QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep());
+
+ if (!moreResults) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ QM_TRY_UNWRAP(EntryId entryId, stmt.GetEntryIdByColumn(/* Column */ 0u));
+
+ return entryId;
+}
+
+bool IsSame(const FileSystemConnection& aConnection,
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewHandle, bool aIsFile) {
+ MOZ_ASSERT(!aNewHandle.parentId().IsEmpty());
+
+ QM_TRY_UNWRAP(EntryId entryId, FindEntryId(aConnection, aNewHandle, aIsFile),
+ false);
+ return entryId == aHandle.entryId();
+}
+
+Result<bool, QMResult> IsFile(const FileSystemConnection& aConnection,
+ const EntryId& aEntryId) {
+ QM_TRY_UNWRAP(bool exists, DoesFileExist(aConnection, aEntryId));
+ if (exists) {
+ return true;
+ }
+
+ QM_TRY_UNWRAP(exists, DoesDirectoryExist(aConnection, aEntryId));
+ if (exists) {
+ return false;
+ }
+
+ // Doesn't exist
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+}
+
+nsresult PerformRename(const FileSystemConnection& aConnection,
+ const FileSystemEntryMetadata& aHandle,
+ const Name& aNewName,
+ const nsLiteralCString& aNameUpdateQuery) {
+ MOZ_ASSERT(!aHandle.entryId().IsEmpty());
+ MOZ_ASSERT(IsValidName(aHandle.entryName()));
+
+ // same-name is checked in RenameEntry()
+ if (!IsValidName(aNewName)) {
+ return NS_ERROR_DOM_TYPE_MISMATCH_ERR;
+ }
+
+ auto toNSResult = [](const auto& aRv) { return ToNSResult(aRv); };
+
+ // TODO: This should fail when handle doesn't exist - the
+ // explicit file or directory existence queries are redundant
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, aNameUpdateQuery)
+ .mapErr(toNSResult));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindNameByName("name"_ns, aNewName)));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aHandle.entryId())));
+ QM_TRY(MOZ_TO_RESULT(stmt.Execute()));
+
+ return NS_OK;
+}
+
+nsresult PerformRenameDirectory(const FileSystemConnection& aConnection,
+ const FileSystemEntryMetadata& aHandle,
+ const Name& aNewName) {
+ const nsLiteralCString updateDirectoryNameQuery =
+ "UPDATE Directories "
+ "SET name = :name "
+ "WHERE handle = :handle "
+ ";"_ns;
+
+ return PerformRename(aConnection, aHandle, aNewName,
+ updateDirectoryNameQuery);
+}
+
+nsresult PerformRenameFile(const FileSystemConnection& aConnection,
+ const FileSystemEntryMetadata& aHandle,
+ const Name& aNewName) {
+ const nsLiteralCString updateFileNameQuery =
+ "UPDATE Files "
+ "SET name = :name "
+ "WHERE handle = :handle "
+ ";"_ns;
+
+ return PerformRename(aConnection, aHandle, aNewName, updateFileNameQuery);
+}
+
+} // namespace
+
+/* static */
+Result<Usage, QMResult> FileSystemDatabaseManagerVersion001::GetFileUsage(
+ const FileSystemConnection& aConnection) {
+ const nsLiteralCString sumUsagesQuery = "SELECT sum(usage) FROM Usages;"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConnection, sumUsagesQuery));
+
+ QM_TRY_UNWRAP(const bool moreResults, stmt.ExecuteStep());
+ if (!moreResults) {
+ return Err(QMResult(NS_ERROR_DOM_FILE_NOT_READABLE_ERR));
+ }
+
+ QM_TRY_UNWRAP(Usage totalFiles, stmt.GetUsageByColumn(/* Column */ 0u));
+
+ return totalFiles;
+}
+
+nsresult FileSystemDatabaseManagerVersion001::UpdateUsageInDatabase(
+ const EntryId& aEntry, int64_t aNewDiskUsage) {
+ const nsLiteralCString updateUsageQuery =
+ "INSERT INTO Usages "
+ "( handle, usage ) "
+ "VALUES "
+ "( :handle, :usage ) "
+ "ON CONFLICT(handle) DO "
+ "UPDATE SET usage = excluded.usage "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, updateUsageQuery));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindUsageByName("usage"_ns, aNewDiskUsage)));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntry)));
+ QM_TRY(MOZ_TO_RESULT(stmt.Execute()));
+
+ return NS_OK;
+}
+
+Result<EntryId, QMResult>
+FileSystemDatabaseManagerVersion001::GetOrCreateDirectory(
+ const FileSystemChildMetadata& aHandle, bool aCreate) {
+ MOZ_ASSERT(!aHandle.parentId().IsEmpty());
+
+ const auto& name = aHandle.childName();
+ // Belt and suspenders: check here as well as in child.
+ if (!IsValidName(name)) {
+ return Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR));
+ }
+ MOZ_ASSERT(!name.IsVoid() && !name.IsEmpty());
+
+ bool exists = true;
+ QM_TRY_UNWRAP(exists, DoesFileExist(mConnection, aHandle));
+
+ // By spec, we don't allow a file and a directory
+ // to have the same name and parent
+ if (exists) {
+ return Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR));
+ }
+
+ QM_TRY_UNWRAP(exists, DoesDirectoryExist(mConnection, aHandle));
+
+ // exists as directory
+ if (exists) {
+ return FindEntryId(mConnection, aHandle, false);
+ }
+
+ if (!aCreate) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ const nsLiteralCString insertEntryQuery =
+ "INSERT OR IGNORE INTO Entries "
+ "( handle, parent ) "
+ "VALUES "
+ "( :handle, :parent ) "
+ ";"_ns;
+
+ const nsLiteralCString insertDirectoryQuery =
+ "INSERT OR IGNORE INTO Directories "
+ "( handle, name ) "
+ "VALUES "
+ "( :handle, :name ) "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(EntryId entryId, GetUniqueEntryId(mConnection, aHandle));
+ MOZ_ASSERT(!entryId.IsEmpty());
+
+ mozStorageTransaction transaction(
+ mConnection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, insertEntryQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+ QM_TRY(
+ QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aHandle.parentId())));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, insertDirectoryQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+ QM_TRY(QM_TO_RESULT(stmt.BindNameByName("name"_ns, name)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ QM_TRY_UNWRAP(DebugOnly<bool> doesItExistNow,
+ DoesDirectoryExist(mConnection, aHandle));
+ MOZ_ASSERT(doesItExistNow);
+
+ return entryId;
+}
+
+Result<EntryId, QMResult> FileSystemDatabaseManagerVersion001::GetOrCreateFile(
+ const FileSystemChildMetadata& aHandle, bool aCreate) {
+ MOZ_ASSERT(!aHandle.parentId().IsEmpty());
+
+ const auto& name = aHandle.childName();
+ // Belt and suspenders: check here as well as in child.
+ if (!IsValidName(name)) {
+ return Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR));
+ }
+ MOZ_ASSERT(!name.IsVoid() && !name.IsEmpty());
+
+ QM_TRY_UNWRAP(bool exists, DoesDirectoryExist(mConnection, aHandle));
+
+ // By spec, we don't allow a file and a directory
+ // to have the same name and parent
+ if (exists) {
+ return Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR));
+ }
+
+ QM_TRY_UNWRAP(exists, DoesFileExist(mConnection, aHandle));
+
+ if (exists) {
+ return FindEntryId(mConnection, aHandle, true);
+ }
+
+ if (!aCreate) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ const nsLiteralCString insertEntryQuery =
+ "INSERT INTO Entries "
+ "( handle, parent ) "
+ "VALUES "
+ "( :handle, :parent ) "
+ ";"_ns;
+
+ const nsLiteralCString insertFileQuery =
+ "INSERT INTO Files "
+ "( handle, name ) "
+ "VALUES "
+ "( :handle, :name ) "
+ ";"_ns;
+
+ QM_TRY_UNWRAP(EntryId entryId, GetUniqueEntryId(mConnection, aHandle));
+ MOZ_ASSERT(!entryId.IsEmpty());
+
+ mozStorageTransaction transaction(
+ mConnection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, insertEntryQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+ QM_TRY(
+ QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aHandle.parentId())));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, insertFileQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+ QM_TRY(QM_TO_RESULT(stmt.BindNameByName("name"_ns, name)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ return entryId;
+}
+
+Result<FileSystemDirectoryListing, QMResult>
+FileSystemDatabaseManagerVersion001::GetDirectoryEntries(
+ const EntryId& aParent, PageNumber aPage) const {
+ // TODO: Offset is reported to have bad performance - see Bug 1780386.
+ const nsCString directoriesQuery =
+ "SELECT Dirs.handle, Dirs.name "
+ "FROM Directories AS Dirs "
+ "INNER JOIN ( "
+ "SELECT handle "
+ "FROM Entries "
+ "WHERE parent = :parent "
+ "LIMIT :pageSize "
+ "OFFSET :pageOffset ) "
+ "AS Ents "
+ "ON Dirs.handle = Ents.handle "
+ ";"_ns;
+ const nsCString filesQuery =
+ "SELECT Files.handle, Files.name "
+ "FROM Files "
+ "INNER JOIN ( "
+ "SELECT handle "
+ "FROM Entries "
+ "WHERE parent = :parent "
+ "LIMIT :pageSize "
+ "OFFSET :pageOffset ) "
+ "AS Ents "
+ "ON Files.handle = Ents.handle "
+ ";"_ns;
+
+ FileSystemDirectoryListing entries;
+ QM_TRY(
+ QM_TO_RESULT(GetEntries(mConnection, directoriesQuery, aParent, aPage,
+ /* aDirectory */ true, entries.directories())));
+
+ QM_TRY(QM_TO_RESULT(GetEntries(mConnection, filesQuery, aParent, aPage,
+ /* aDirectory */ false, entries.files())));
+
+ return entries;
+}
+
+nsresult FileSystemDatabaseManagerVersion001::GetFile(
+ const EntryId& aEntryId, nsString& aType,
+ TimeStamp& lastModifiedMilliSeconds, nsTArray<Name>& aPath,
+ nsCOMPtr<nsIFile>& aFile) const {
+ MOZ_ASSERT(!aEntryId.IsEmpty());
+
+ QM_TRY_UNWRAP(aFile, mFileManager->GetOrCreateFile(aEntryId));
+
+ QM_TRY(MOZ_TO_RESULT(GetFileAttributes(mConnection, aEntryId, aType)));
+
+ PRTime lastModTime = 0;
+ QM_TRY(MOZ_TO_RESULT(aFile->GetLastModifiedTime(&lastModTime)));
+ lastModifiedMilliSeconds = static_cast<TimeStamp>(lastModTime);
+
+ FileSystemEntryPair endPoints(mRootEntry, aEntryId);
+ QM_TRY_UNWRAP(aPath, ResolveReversedPath(mConnection, endPoints));
+ if (aPath.IsEmpty()) {
+ return NS_ERROR_DOM_NOT_FOUND_ERR;
+ }
+ aPath.Reverse();
+
+ return NS_OK;
+}
+
+nsresult FileSystemDatabaseManagerVersion001::UpdateUsage(
+ const EntryId& aEntry) {
+ auto toNSResult = [](const auto& aRv) { return ToNSResult(aRv); };
+
+ // We don't track directories or non-existent files.
+ QM_TRY_UNWRAP(bool fileExists,
+ DoesFileExist(mConnection, aEntry).mapErr(toNSResult));
+ if (!fileExists) {
+ return NS_OK; // May be deleted before update, no assert
+ }
+
+ QM_TRY_UNWRAP(bool isFolder,
+ DoesDirectoryExist(mConnection, aEntry).mapErr(toNSResult));
+ if (isFolder) {
+ return NS_OK; // May be deleted and replaced by a folder, no assert
+ }
+
+ nsCOMPtr<nsIFile> file;
+ QM_TRY_UNWRAP(file, mFileManager->GetOrCreateFile(aEntry));
+ MOZ_ASSERT(file);
+
+ int64_t fileSize = 0;
+ QM_TRY(MOZ_TO_RESULT(file->GetFileSize(&fileSize)));
+
+ QM_TRY(MOZ_TO_RESULT(UpdateUsageInDatabase(aEntry, fileSize)));
+
+ return NS_OK;
+}
+
+Result<bool, QMResult> FileSystemDatabaseManagerVersion001::RemoveDirectory(
+ const FileSystemChildMetadata& aHandle, bool aRecursive) {
+ MOZ_ASSERT(!aHandle.parentId().IsEmpty());
+
+ if (aHandle.childName().IsEmpty()) {
+ return false;
+ }
+ DebugOnly<Name> name = aHandle.childName();
+ MOZ_ASSERT(!name.inspect().IsVoid());
+
+ QM_TRY_UNWRAP(bool exists, DoesDirectoryExist(mConnection, aHandle));
+
+ if (!exists) {
+ return false;
+ }
+
+ // At this point, entry exists and is a directory.
+ QM_TRY_UNWRAP(EntryId entryId, FindEntryId(mConnection, aHandle, false));
+ MOZ_ASSERT(!entryId.IsEmpty());
+
+ QM_TRY_UNWRAP(bool isEmpty, IsDirectoryEmpty(mConnection, entryId));
+
+ if (!aRecursive && !isEmpty) {
+ return Err(QMResult(NS_ERROR_DOM_INVALID_MODIFICATION_ERR));
+ }
+ // If it's empty or we can delete recursively, deleting the handle will
+ // cascade
+
+ const nsLiteralCString descendantsQuery =
+ "WITH RECURSIVE traceChildren(handle, parent) AS ( "
+ "SELECT handle, parent "
+ "FROM Entries "
+ "WHERE handle=:handle "
+ "UNION "
+ "SELECT Entries.handle, Entries.parent FROM traceChildren, Entries "
+ "WHERE traceChildren.handle=Entries.parent ) "
+ "SELECT handle "
+ "FROM traceChildren INNER JOIN Files "
+ "USING(handle) "
+ ";"_ns;
+
+ const nsLiteralCString deleteEntryQuery =
+ "DELETE FROM Entries "
+ "WHERE handle = :handle "
+ ";"_ns;
+
+ mozStorageTransaction transaction(
+ mConnection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ nsTArray<EntryId> descendants;
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, descendantsQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+ QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep());
+
+ while (moreResults) {
+ QM_TRY_UNWRAP(EntryId entryId, stmt.GetEntryIdByColumn(/* Column */ 0u));
+
+ descendants.AppendElement(entryId);
+
+ QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep());
+ }
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, deleteEntryQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ for (const auto& child : descendants) {
+ QM_WARNONLY_TRY_UNWRAP(const auto maybeFileSize,
+ mFileManager->RemoveFile(child));
+
+ if (maybeFileSize) {
+ quota::QuotaManager* quotaManager = quota::QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ quotaManager->DecreaseUsageForClient(
+ quota::ClientMetadata{mDataManager->OriginMetadataRef(),
+ quota::Client::FILESYSTEM},
+ *maybeFileSize);
+ }
+ }
+
+ return true;
+}
+
+Result<bool, QMResult> FileSystemDatabaseManagerVersion001::RemoveFile(
+ const FileSystemChildMetadata& aHandle) {
+ MOZ_ASSERT(!aHandle.parentId().IsEmpty());
+
+ if (aHandle.childName().IsEmpty()) {
+ return false;
+ }
+ DebugOnly<Name> name = aHandle.childName();
+ MOZ_ASSERT(!name.inspect().IsVoid());
+
+ // Make it more evident that we won't remove directories
+ QM_TRY_UNWRAP(bool exists, DoesFileExist(mConnection, aHandle));
+
+ if (!exists) {
+ return false;
+ }
+ // At this point, entry exists and is a file
+ QM_TRY_UNWRAP(EntryId entryId, FindEntryId(mConnection, aHandle, true));
+ MOZ_ASSERT(!entryId.IsEmpty());
+
+ // XXX This code assumes the spec question is resolved to state
+ // removing an in-use file should fail. If it shouldn't fail, we need to
+ // do something to neuter all the extant FileAccessHandles/WritableFileStreams
+ // that reference it
+ if (mDataManager->IsLocked(entryId)) {
+ LOG(("Trying to remove in-use file"));
+ return Err(QMResult(NS_ERROR_DOM_INVALID_MODIFICATION_ERR));
+ }
+
+ const nsLiteralCString deleteEntryQuery =
+ "DELETE FROM Entries "
+ "WHERE handle = :handle "
+ ";"_ns;
+
+ mozStorageTransaction transaction(
+ mConnection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, deleteEntryQuery));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ QM_WARNONLY_TRY_UNWRAP(const auto maybeFileSize,
+ mFileManager->RemoveFile(entryId));
+
+ if (maybeFileSize) {
+ quota::QuotaManager* quotaManager = quota::QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ quotaManager->DecreaseUsageForClient(
+ quota::ClientMetadata{mDataManager->OriginMetadataRef(),
+ quota::Client::FILESYSTEM},
+ *maybeFileSize);
+ }
+
+ return true;
+}
+
+Result<bool, QMResult> FileSystemDatabaseManagerVersion001::RenameEntry(
+ const FileSystemEntryMetadata& aHandle, const Name& aNewName) {
+ // Can't rename root
+ if (mRootEntry == aHandle.entryId()) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ // Verify the source exists
+ QM_TRY_UNWRAP(bool isFile, IsFile(mConnection, aHandle.entryId()), false);
+
+ // At this point, entry exists
+ if (isFile && mDataManager->IsLocked(aHandle.entryId())) {
+ LOG(("Trying to move in-use file"));
+ return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR));
+ }
+
+ // Are we actually renaming?
+ if (aHandle.entryName() == aNewName) {
+ return true;
+ }
+
+ // If the destination file exists, fail explicitly.
+ FileSystemChildMetadata destination;
+ QM_TRY_UNWRAP(EntryId parent, FindParent(mConnection, aHandle.entryId()));
+ destination.parentId() = parent;
+ destination.childName() = aNewName;
+
+ QM_TRY_UNWRAP(bool exists, DoesFileExist(mConnection, destination));
+ if (exists) {
+ // If the destination file exists, check if it is in use
+ QM_TRY_INSPECT(const EntryId& destId,
+ FindEntryId(mConnection, destination, true));
+ if (mDataManager->IsLocked(destId)) {
+ LOG(("Trying to overwrite in-use file"));
+ return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR));
+ }
+
+ QM_TRY_UNWRAP(DebugOnly<bool> isRemoved, RemoveFile(destination));
+ MOZ_ASSERT(isRemoved);
+ } else {
+ QM_TRY_UNWRAP(exists, DoesDirectoryExist(mConnection, destination));
+ if (exists) {
+ return Err(QMResult(NS_ERROR_DOM_INVALID_MODIFICATION_ERR));
+ }
+ }
+
+ mozStorageTransaction transaction(
+ mConnection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ if (isFile) {
+ QM_TRY(QM_TO_RESULT(PerformRenameFile(mConnection, aHandle, aNewName)));
+ } else {
+ QM_TRY(
+ QM_TO_RESULT(PerformRenameDirectory(mConnection, aHandle, aNewName)));
+ }
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ return true;
+}
+
+Result<bool, QMResult> FileSystemDatabaseManagerVersion001::MoveEntry(
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewDesignation) {
+ MOZ_ASSERT(!aHandle.entryId().IsEmpty());
+
+ const EntryId& entryId = aHandle.entryId();
+ const Name& newName = aNewDesignation.childName();
+
+ if (mRootEntry == entryId) {
+ return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR));
+ }
+
+ // Verify the source exists
+ QM_TRY_UNWRAP(bool isFile, IsFile(mConnection, entryId), false);
+
+ // If the rename doesn't change the name or directory, just return success.
+ // XXX Needs to be added to the spec
+ if (IsSame(mConnection, aHandle, aNewDesignation, isFile)) {
+ return true;
+ }
+
+ // At this point, entry exists
+ if (isFile && mDataManager->IsLocked(entryId)) {
+ LOG(("Trying to move in-use file"));
+ return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR));
+ }
+
+ // If the destination file exists, fail explicitly. Spec author plans to
+ // revise the spec
+ QM_TRY_UNWRAP(bool exists, DoesFileExist(mConnection, aNewDesignation));
+ if (exists) {
+ QM_TRY_INSPECT(const EntryId& destId,
+ FindEntryId(mConnection, aNewDesignation, true));
+ if (mDataManager->IsLocked(destId)) {
+ LOG(("Trying to overwrite in-use file"));
+ return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR));
+ }
+
+ QM_TRY_UNWRAP(DebugOnly<bool> isRemoved, RemoveFile(aNewDesignation));
+ MOZ_ASSERT(isRemoved);
+ } else {
+ QM_TRY_UNWRAP(exists, DoesDirectoryExist(mConnection, aNewDesignation));
+ if (exists) {
+ return Err(QMResult(NS_ERROR_DOM_INVALID_MODIFICATION_ERR));
+ }
+ }
+
+ // To prevent cyclic paths, we check that there is no path from
+ // the item to be moved to the destination folder.
+ QM_TRY_UNWRAP(
+ const bool isDestinationUnderSelf,
+ IsAncestor(mConnection, {aHandle.entryId(), aNewDesignation.parentId()}));
+ if (isDestinationUnderSelf) {
+ return Err(QMResult(NS_ERROR_DOM_INVALID_MODIFICATION_ERR));
+ }
+
+ const nsLiteralCString updateEntryParentQuery =
+ "UPDATE Entries "
+ "SET parent = :parent "
+ "WHERE handle = :handle "
+ ";"_ns;
+
+ mozStorageTransaction transaction(
+ mConnection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ {
+ // We always change the parent because it's simpler than checking if the
+ // parent needs to be changed
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(mConnection, updateEntryParentQuery));
+ QM_TRY(QM_TO_RESULT(
+ stmt.BindEntryIdByName("parent"_ns, aNewDesignation.parentId())));
+ QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId)));
+ QM_TRY(QM_TO_RESULT(stmt.Execute()));
+ }
+
+ // Are we actually renaming?
+ if (aHandle.entryName() == newName) {
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ return true;
+ }
+
+ if (isFile) {
+ QM_TRY(QM_TO_RESULT(PerformRenameFile(mConnection, aHandle, newName)));
+ } else {
+ QM_TRY(QM_TO_RESULT(PerformRenameDirectory(mConnection, aHandle, newName)));
+ }
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ return true;
+}
+
+Result<Path, QMResult> FileSystemDatabaseManagerVersion001::Resolve(
+ const FileSystemEntryPair& aEndpoints) const {
+ QM_TRY_UNWRAP(Path path, ResolveReversedPath(mConnection, aEndpoints));
+ // Note: if not an ancestor, returns null
+
+ path.Reverse();
+ return path;
+}
+
+void FileSystemDatabaseManagerVersion001::Close() { mConnection->Close(); }
+
+} // namespace fs::data
+
+} // namespace mozilla::dom
diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h
new file mode 100644
index 0000000000..e6edc4ca8a
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h
@@ -0,0 +1,101 @@
+/* -*- 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/. */
+
+#ifndef DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION001_H_
+#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION001_H_
+
+#include "FileSystemDatabaseManager.h"
+#include "nsString.h"
+
+namespace mozilla::dom::fs::data {
+
+class FileSystemDataManager;
+class FileSystemFileManager;
+using FileSystemConnection = fs::ResultConnection;
+
+/**
+ * @brief Versioned implementation of database interface enables backwards
+ * support after the schema has changed. Version number 0 refers to
+ * uninitialized database, and versions after that are sequential upgrades.
+ *
+ * To change the schema to the next version x,
+ * - a new implementation FileSystemDatabaseManagerVersion00x is derived from
+ * the previous version and the required methods are overridden
+ * - a new apppropriate schema initialization class SchemaVersion00x is created
+ * or derived
+ * - the factory method of FileSystemDatabaseManager is extended to try to
+ * migrate the data from the previous version to version x, and to return
+ * FileSystemDatabaseManagerVersion00x implementation if the database version
+ * after the migrations is x
+ * - note that if the migration fails at some old version, the corresponding
+ * old implementation should be returned: this way the users whose migrations
+ * fail systematically due to hardware or other issues will not get locked out
+ */
+class FileSystemDatabaseManagerVersion001 : public FileSystemDatabaseManager {
+ public:
+ FileSystemDatabaseManagerVersion001(
+ FileSystemDataManager* aDataManager, FileSystemConnection&& aConnection,
+ UniquePtr<FileSystemFileManager>&& aFileManager,
+ const EntryId& aRootEntry)
+ : mDataManager(aDataManager),
+ mConnection(aConnection),
+ mFileManager(std::move(aFileManager)),
+ mRootEntry(aRootEntry) {}
+
+ static Result<Usage, QMResult> GetFileUsage(
+ const FileSystemConnection& aConnection);
+
+ virtual nsresult UpdateUsage(const EntryId& aEntry) override;
+
+ virtual Result<EntryId, QMResult> GetOrCreateDirectory(
+ const FileSystemChildMetadata& aHandle, bool aCreate) override;
+
+ virtual Result<EntryId, QMResult> GetOrCreateFile(
+ const FileSystemChildMetadata& aHandle, bool aCreate) override;
+
+ virtual nsresult GetFile(const EntryId& aEntryId, nsString& aType,
+ TimeStamp& lastModifiedMilliSeconds, Path& aPath,
+ nsCOMPtr<nsIFile>& aFile) const override;
+
+ virtual Result<FileSystemDirectoryListing, QMResult> GetDirectoryEntries(
+ const EntryId& aParent, PageNumber aPage) const override;
+
+ virtual Result<bool, QMResult> RenameEntry(
+ const FileSystemEntryMetadata& aHandle, const Name& aNewName) override;
+
+ virtual Result<bool, QMResult> MoveEntry(
+ const FileSystemEntryMetadata& aHandle,
+ const FileSystemChildMetadata& aNewDesignation) override;
+
+ virtual Result<bool, QMResult> RemoveDirectory(
+ const FileSystemChildMetadata& aHandle, bool aRecursive) override;
+
+ virtual Result<bool, QMResult> RemoveFile(
+ const FileSystemChildMetadata& aHandle) override;
+
+ virtual Result<Path, QMResult> Resolve(
+ const FileSystemEntryPair& aEndpoints) const override;
+
+ virtual void Close() override;
+
+ virtual ~FileSystemDatabaseManagerVersion001() = default;
+
+ private:
+ nsresult UpdateUsageInDatabase(const EntryId& aEntry, int64_t aNewDiskUsage);
+
+ // This is a raw pointer since we're owned by the FileSystemDataManager.
+ FileSystemDataManager* MOZ_NON_OWNING_REF mDataManager;
+
+ FileSystemConnection mConnection;
+
+ UniquePtr<FileSystemFileManager> mFileManager;
+
+ const EntryId mRootEntry;
+};
+
+} // namespace mozilla::dom::fs::data
+
+#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION001_H_
diff --git a/dom/fs/parent/datamodel/FileSystemFileManager.cpp b/dom/fs/parent/datamodel/FileSystemFileManager.cpp
new file mode 100644
index 0000000000..4a70e634e4
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemFileManager.cpp
@@ -0,0 +1,267 @@
+/* -*- 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 "FileSystemFileManager.h"
+
+#include "FileSystemDataManager.h"
+#include "FileSystemHashSource.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsIFileProtocolHandler.h"
+#include "nsIFileURL.h"
+#include "nsIURIMutator.h"
+#include "nsXPCOM.h"
+
+namespace mozilla::dom::fs::data {
+
+namespace {
+
+constexpr nsLiteralString kDatabaseFileName = u"metadata.sqlite"_ns;
+
+Result<nsCOMPtr<nsIFile>, QMResult> GetFileDestination(
+ const nsCOMPtr<nsIFile>& aTopDirectory, const EntryId& aEntryId) {
+ MOZ_ASSERT(32u == aEntryId.Length());
+
+ nsCOMPtr<nsIFile> destination;
+
+ // nsIFile Clone is not a constant method
+ QM_TRY(QM_TO_RESULT(aTopDirectory->Clone(getter_AddRefs(destination))));
+
+ QM_TRY_UNWRAP(Name encoded, FileSystemHashSource::EncodeHash(aEntryId));
+
+ MOZ_ALWAYS_TRUE(IsAscii(encoded));
+
+ nsString relativePath;
+ relativePath.Append(Substring(encoded, 0, 2));
+
+ QM_TRY(QM_TO_RESULT(destination->AppendRelativePath(relativePath)));
+
+ QM_TRY(QM_TO_RESULT(destination->AppendRelativePath(encoded)));
+
+ return destination;
+}
+
+Result<nsCOMPtr<nsIFile>, QMResult> GetOrCreateFileImpl(
+ const nsAString& aFilePath) {
+ MOZ_ASSERT(!aFilePath.IsEmpty());
+
+ nsCOMPtr<nsIFile> result;
+ QM_TRY(QM_TO_RESULT(NS_NewLocalFile(aFilePath,
+ /* aFollowLinks */ false,
+ getter_AddRefs(result))));
+
+ bool exists = true;
+ QM_TRY(QM_TO_RESULT(result->Exists(&exists)));
+
+ if (!exists) {
+ QM_TRY(QM_TO_RESULT(result->Create(nsIFile::NORMAL_FILE_TYPE, 0644)));
+
+ return result;
+ }
+
+ bool isDirectory = true;
+ QM_TRY(QM_TO_RESULT(result->IsDirectory(&isDirectory)));
+ QM_TRY(OkIf(!isDirectory), Err(QMResult(NS_ERROR_FILE_IS_DIRECTORY)));
+
+ return result;
+}
+
+Result<nsCOMPtr<nsIFile>, QMResult> GetFile(
+ const nsCOMPtr<nsIFile>& aTopDirectory, const EntryId& aEntryId) {
+ MOZ_ASSERT(!aEntryId.IsEmpty());
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject,
+ GetFileDestination(aTopDirectory, aEntryId));
+
+ nsString desiredPath;
+ QM_TRY(QM_TO_RESULT(pathObject->GetPath(desiredPath)));
+
+ nsCOMPtr<nsIFile> result;
+ QM_TRY(QM_TO_RESULT(NS_NewLocalFile(desiredPath,
+ /* aFollowLinks */ false,
+ getter_AddRefs(result))));
+
+ return result;
+}
+
+Result<nsCOMPtr<nsIFile>, QMResult> GetOrCreateFile(
+ const nsCOMPtr<nsIFile>& aTopDirectory, const EntryId& aEntryId) {
+ MOZ_ASSERT(!aEntryId.IsEmpty());
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject,
+ GetFileDestination(aTopDirectory, aEntryId));
+
+ nsString desiredPath;
+ QM_TRY(QM_TO_RESULT(pathObject->GetPath(desiredPath)));
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> result, GetOrCreateFileImpl(desiredPath));
+
+ return result;
+}
+
+} // namespace
+
+Result<nsCOMPtr<nsIFile>, QMResult> GetFileSystemDirectory(
+ const Origin& aOrigin) {
+ quota::QuotaManager* quotaManager = quota::QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> fileSystemDirectory,
+ QM_TO_RESULT_TRANSFORM(quotaManager->GetDirectoryForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT, aOrigin)));
+
+ QM_TRY(QM_TO_RESULT(fileSystemDirectory->AppendRelativePath(
+ NS_LITERAL_STRING_FROM_CSTRING(FILESYSTEM_DIRECTORY_NAME))));
+
+ return fileSystemDirectory;
+}
+
+nsresult EnsureFileSystemDirectory(
+ const quota::OriginMetadata& aOriginMetadata) {
+ quota::QuotaManager* quotaManager = quota::QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ QM_TRY(MOZ_TO_RESULT(quotaManager->EnsureStorageIsInitialized()));
+
+ QM_TRY(MOZ_TO_RESULT(quotaManager->EnsureTemporaryStorageIsInitialized()));
+
+ QM_TRY_INSPECT(const auto& fileSystemDirectory,
+ quotaManager
+ ->EnsureTemporaryOriginIsInitialized(
+ quota::PERSISTENCE_TYPE_DEFAULT, aOriginMetadata)
+ .map([](const auto& aPair) { return aPair.first; }));
+
+ QM_TRY(QM_TO_RESULT(fileSystemDirectory->AppendRelativePath(
+ NS_LITERAL_STRING_FROM_CSTRING(FILESYSTEM_DIRECTORY_NAME))));
+
+ bool exists = true;
+ QM_TRY(QM_TO_RESULT(fileSystemDirectory->Exists(&exists)));
+
+ if (!exists) {
+ QM_TRY(QM_TO_RESULT(
+ fileSystemDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755)));
+
+ return NS_OK;
+ }
+
+ bool isDirectory = true;
+ QM_TRY(QM_TO_RESULT(fileSystemDirectory->IsDirectory(&isDirectory)));
+ QM_TRY(OkIf(isDirectory), NS_ERROR_FILE_NOT_DIRECTORY);
+
+ return NS_OK;
+}
+
+Result<nsCOMPtr<nsIFile>, QMResult> GetDatabaseFile(const Origin& aOrigin) {
+ MOZ_ASSERT(!aOrigin.IsEmpty());
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> databaseFile,
+ GetFileSystemDirectory(aOrigin));
+
+ QM_TRY(QM_TO_RESULT(databaseFile->AppendRelativePath(kDatabaseFileName)));
+
+ return databaseFile;
+}
+
+/**
+ * TODO: This is almost identical to the corresponding function of IndexedDB
+ */
+Result<nsCOMPtr<nsIFileURL>, QMResult> GetDatabaseFileURL(
+ const Origin& aOrigin, const int64_t aDirectoryLockId) {
+ MOZ_ASSERT(aDirectoryLockId >= 0);
+
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> databaseFile, GetDatabaseFile(aOrigin));
+
+ QM_TRY_INSPECT(
+ const auto& protocolHandler,
+ QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_GET_TYPED(
+ nsCOMPtr<nsIProtocolHandler>, MOZ_SELECT_OVERLOAD(do_GetService),
+ NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "file")));
+
+ QM_TRY_INSPECT(const auto& fileHandler,
+ QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_GET_TYPED(
+ nsCOMPtr<nsIFileProtocolHandler>,
+ MOZ_SELECT_OVERLOAD(do_QueryInterface), protocolHandler)));
+
+ QM_TRY_INSPECT(const auto& mutator,
+ QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<nsIURIMutator>, fileHandler, NewFileURIMutator,
+ databaseFile)));
+
+ nsCString directoryLockIdClause = "&directoryLockId="_ns;
+ directoryLockIdClause.AppendInt(aDirectoryLockId);
+
+ nsCOMPtr<nsIFileURL> result;
+ QM_TRY(QM_TO_RESULT(
+ NS_MutateURI(mutator).SetQuery(directoryLockIdClause).Finalize(result)));
+
+ return result;
+}
+
+/* static */
+Result<FileSystemFileManager, QMResult>
+FileSystemFileManager::CreateFileSystemFileManager(
+ nsCOMPtr<nsIFile>&& topDirectory) {
+ return FileSystemFileManager(std::move(topDirectory));
+}
+
+/* static */
+Result<FileSystemFileManager, QMResult>
+FileSystemFileManager::CreateFileSystemFileManager(const Origin& aOrigin) {
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> topDirectory,
+ GetFileSystemDirectory(aOrigin));
+
+ return FileSystemFileManager(std::move(topDirectory));
+}
+
+FileSystemFileManager::FileSystemFileManager(nsCOMPtr<nsIFile>&& aTopDirectory)
+ : mTopDirectory(std::move(aTopDirectory)) {}
+
+Result<nsCOMPtr<nsIFile>, QMResult> FileSystemFileManager::GetFile(
+ const EntryId& aEntryId) const {
+ return data::GetFile(mTopDirectory, aEntryId);
+}
+
+Result<nsCOMPtr<nsIFile>, QMResult> FileSystemFileManager::GetOrCreateFile(
+ const EntryId& aEntryId) {
+ return data::GetOrCreateFile(mTopDirectory, aEntryId);
+}
+
+Result<int64_t, QMResult> FileSystemFileManager::RemoveFile(
+ const EntryId& aEntryId) {
+ MOZ_ASSERT(!aEntryId.IsEmpty());
+ QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject,
+ GetFileDestination(mTopDirectory, aEntryId));
+
+ bool exists = false;
+ QM_TRY(QM_TO_RESULT(pathObject->Exists(&exists)));
+
+ if (!exists) {
+ return 0;
+ }
+
+ bool isFile = false;
+ QM_TRY(QM_TO_RESULT(pathObject->IsFile(&isFile)));
+
+ if (!isFile) {
+ return Err(QMResult(NS_ERROR_FILE_IS_DIRECTORY));
+ }
+
+ QM_TRY_UNWRAP(int64_t fileSize,
+ QM_TO_RESULT_INVOKE_MEMBER(pathObject, GetFileSize));
+
+ QM_TRY(QM_TO_RESULT(pathObject->Remove(/* recursive */ false)));
+
+ return fileSize;
+}
+
+} // namespace mozilla::dom::fs::data
diff --git a/dom/fs/parent/datamodel/FileSystemFileManager.h b/dom/fs/parent/datamodel/FileSystemFileManager.h
new file mode 100644
index 0000000000..9df92b04f8
--- /dev/null
+++ b/dom/fs/parent/datamodel/FileSystemFileManager.h
@@ -0,0 +1,147 @@
+/* -*- 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/. */
+
+#ifndef DOM_FS_PARENT_DATAMODEL_FILESYSTEMFILEMANAGER_H_
+#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMFILEMANAGER_H_
+
+#include "ErrorList.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/QMResult.h"
+#include "nsIFile.h"
+#include "nsString.h"
+
+template <class T>
+class nsCOMPtr;
+
+class nsIFileURL;
+
+namespace mozilla::dom {
+
+namespace quota {
+
+struct OriginMetadata;
+
+} // namespace quota
+
+namespace fs::data {
+
+/**
+ * @brief Get the directory for file system items of specified origin.
+ * Use this instead of constructing the path from quota manager's storage path.
+ *
+ * @param aOrigin Specified origin
+ * @return Result<nsCOMPtr<nsIFile>, QMResult> Top file system directory
+ */
+Result<nsCOMPtr<nsIFile>, QMResult> GetFileSystemDirectory(
+ const Origin& aOrigin);
+
+/**
+ * @brief Ensure that the origin-specific directory for file system exists.
+ *
+ * @param aOriginMetadata Specified origin metadata
+ * @return nsresult Error if operation failed
+ */
+nsresult EnsureFileSystemDirectory(
+ const quota::OriginMetadata& aOriginMetadata);
+
+/**
+ * @brief Get file system's database path for specified origin.
+ * Use this to get the database path instead of constructing it from
+ * quota manager's storage path - without the side effect of potentially
+ * creating it.
+ *
+ * @param aOrigin Specified origin
+ * @return Result<nsCOMPtr<nsIFile>, QMResult> Database file object
+ */
+Result<nsCOMPtr<nsIFile>, QMResult> GetDatabaseFile(const Origin& aOrigin);
+
+/**
+ * @brief Get file system's database url with directory lock parameter for
+ * specified origin. Use this to open a database connection and have the quota
+ * manager guard against its deletion or busy errors due to other connections.
+ *
+ * @param aOrigin Specified origin
+ * @param aDirectoryLockId Directory lock id from the quota manager
+ * @return Result<nsCOMPtr<nsIFileURL>, QMResult> Database file URL object
+ */
+Result<nsCOMPtr<nsIFileURL>, QMResult> GetDatabaseFileURL(
+ const Origin& aOrigin, const int64_t aDirectoryLockId);
+
+/**
+ * @brief Creates and removes disk-backed representations of the file systems'
+ * file entries for a specified origin.
+ *
+ * Other components should not depend on how the files are organized on disk
+ * but instead rely on the entry id and have access to the local file using the
+ * GetOrCreateFile result.
+ *
+ * The local paths are not necessarily stable in the long term and if they
+ * absolutely must be cached, there should be a way to repopulate the cache
+ * after an internal reorganization of the file entry represenations on disk,
+ * for some currently unforeseen maintenance reason.
+ *
+ * Example: if GetOrCreateFile used to map entryId 'abc' to path '/c/u/1/123'
+ * and now it maps it to '/d/u/1/12/123', the cache should either update all
+ * paths at once through a migration, or purge them and save a new value
+ * whenever a call to GetOrCreateFile is made.
+ */
+class FileSystemFileManager {
+ public:
+ /**
+ * @brief Create a File System File Manager object for a specified origin.
+ *
+ * @param aOrigin
+ * @return Result<FileSystemFileManager, QMResult>
+ */
+ static Result<FileSystemFileManager, QMResult> CreateFileSystemFileManager(
+ const Origin& aOrigin);
+
+ /**
+ * @brief Create a File System File Manager object which keeps file entries
+ * under a specified directory instead of quota manager's storage path.
+ * This should only be used for testing and preferably removed.
+ *
+ * @param topDirectory
+ * @return Result<FileSystemFileManager, QMResult>
+ */
+ static Result<FileSystemFileManager, QMResult> CreateFileSystemFileManager(
+ nsCOMPtr<nsIFile>&& topDirectory);
+
+ /**
+ * @brief Get a file object for a specified entry id. If a file for the entry
+ * does not exist, returns an appropriate error.
+ *
+ * @param aEntryId Specified id of a file system entry
+ * @return Result<nsCOMPtr<nsIFile>, QMResult> File or error.
+ */
+ Result<nsCOMPtr<nsIFile>, QMResult> GetFile(const EntryId& aEntryId) const;
+
+ /**
+ * @brief Get or create a disk-backed file object for a specified entry id.
+ *
+ * @param aEntryId Specified id of a file system entry
+ * @return Result<nsCOMPtr<nsIFile>, QMResult> File abstraction or IO error
+ */
+ Result<nsCOMPtr<nsIFile>, QMResult> GetOrCreateFile(const EntryId& aEntryId);
+
+ /**
+ * @brief Remove the disk-backed file object for a specified entry id.
+ *
+ * @param aEntryId Specified id of a file system entry
+ * @return Result<int64_t, QMResult> Error or file size
+ */
+ Result<int64_t, QMResult> RemoveFile(const EntryId& aEntryId);
+
+ private:
+ explicit FileSystemFileManager(nsCOMPtr<nsIFile>&& aTopDirectory);
+
+ nsCOMPtr<nsIFile> mTopDirectory;
+};
+
+} // namespace fs::data
+} // namespace mozilla::dom
+
+#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMFILEMANAGER_H_
diff --git a/dom/fs/parent/datamodel/SchemaVersion001.cpp b/dom/fs/parent/datamodel/SchemaVersion001.cpp
new file mode 100644
index 0000000000..435b2a6dd1
--- /dev/null
+++ b/dom/fs/parent/datamodel/SchemaVersion001.cpp
@@ -0,0 +1,185 @@
+/* -*- 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 "SchemaVersion001.h"
+
+#include "FileSystemHashSource.h"
+#include "ResultStatement.h"
+#include "fs/FileSystemConstants.h"
+#include "mozStorageHelper.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+
+namespace mozilla::dom::fs {
+
+namespace {
+
+nsresult SetEncoding(ResultConnection& aConn) {
+ return aConn->ExecuteSimpleSQL(R"(PRAGMA encoding = "UTF-16";)"_ns);
+}
+
+nsresult CreateEntries(ResultConnection& aConn) {
+ return aConn->ExecuteSimpleSQL(
+ "CREATE TABLE IF NOT EXISTS Entries ( "
+ "handle BLOB PRIMARY KEY, " // Generated from parent + name, unique
+ "parent BLOB, " // Not null due to constraint
+ "CONSTRAINT parent_is_a_directory "
+ "FOREIGN KEY (parent) "
+ "REFERENCES Directories (handle) "
+ "ON DELETE CASCADE ) "
+ ";"_ns);
+}
+
+nsresult CreateDirectories(ResultConnection& aConn) {
+ return aConn->ExecuteSimpleSQL(
+ "CREATE TABLE IF NOT EXISTS Directories ( "
+ "handle BLOB PRIMARY KEY, "
+ "name BLOB NOT NULL, "
+ "CONSTRAINT directories_are_entries "
+ "FOREIGN KEY (handle) "
+ "REFERENCES Entries (handle) "
+ "ON DELETE CASCADE ) "
+ ";"_ns);
+}
+
+nsresult CreateFiles(ResultConnection& aConn) {
+ return aConn->ExecuteSimpleSQL(
+ "CREATE TABLE IF NOT EXISTS Files ( "
+ "handle BLOB PRIMARY KEY, "
+ "type TEXT, "
+ "name BLOB NOT NULL, "
+ "CONSTRAINT files_are_entries "
+ "FOREIGN KEY (handle) "
+ "REFERENCES Entries (handle) "
+ "ON DELETE CASCADE ) "
+ ";"_ns);
+}
+
+nsresult CreateUsages(ResultConnection& aConn) {
+ return aConn->ExecuteSimpleSQL(
+ "CREATE TABLE IF NOT EXISTS Usages ( "
+ "handle BLOB PRIMARY KEY, "
+ "usage INTEGER NOT NULL DEFAULT 0, "
+ "tracked BOOLEAN NOT NULL DEFAULT 0 CHECK (tracked IN (0, 1)), "
+ "CONSTRAINT handles_are_files "
+ "FOREIGN KEY (handle) "
+ "REFERENCES Files (handle) "
+ "ON DELETE CASCADE ) "
+ ";"_ns);
+}
+
+class KeepForeignKeysOffUntilScopeExit final {
+ public:
+ explicit KeepForeignKeysOffUntilScopeExit(const ResultConnection& aConn)
+ : mConn(aConn) {}
+
+ static Result<KeepForeignKeysOffUntilScopeExit, QMResult> Create(
+ const ResultConnection& aConn) {
+ QM_TRY(
+ QM_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_keys = OFF;"_ns)));
+ KeepForeignKeysOffUntilScopeExit result(aConn);
+ return result;
+ }
+
+ ~KeepForeignKeysOffUntilScopeExit() {
+ auto maskResult = [this]() -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(
+ mConn->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns)));
+
+ return Ok{};
+ };
+ QM_WARNONLY_TRY(maskResult());
+ }
+
+ private:
+ ResultConnection mConn;
+};
+
+nsresult CreateRootEntry(ResultConnection& aConn, const Origin& aOrigin) {
+ KeepForeignKeysOffUntilScopeExit foreignKeysGuard(aConn);
+
+ const nsLiteralCString createRootQuery =
+ "INSERT OR IGNORE INTO Entries "
+ "( handle, parent ) "
+ "VALUES ( :handle, NULL );"_ns;
+
+ const nsLiteralCString flagRootAsDirectoryQuery =
+ "INSERT OR IGNORE INTO Directories "
+ "( handle, name ) "
+ "VALUES ( :handle, :name );"_ns;
+
+ QM_TRY_UNWRAP(EntryId rootId,
+ data::FileSystemHashSource::GenerateHash(aOrigin, kRootName));
+
+ mozStorageTransaction transaction(
+ aConn.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConn, createRootQuery));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, rootId)));
+ QM_TRY(MOZ_TO_RESULT(stmt.Execute()));
+ }
+
+ {
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConn, flagRootAsDirectoryQuery));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, rootId)));
+ QM_TRY(MOZ_TO_RESULT(stmt.BindNameByName("name"_ns, kRootName)));
+ QM_TRY(MOZ_TO_RESULT(stmt.Execute()));
+ }
+
+ return transaction.Commit();
+}
+
+Result<bool, QMResult> CheckIfEmpty(ResultConnection& aConn) {
+ const nsLiteralCString areThereTablesQuery =
+ "SELECT EXISTS ("
+ "SELECT 1 FROM sqlite_master "
+ ");"_ns;
+
+ QM_TRY_UNWRAP(ResultStatement stmt,
+ ResultStatement::Create(aConn, areThereTablesQuery));
+
+ return stmt.YesOrNoQuery();
+};
+
+} // namespace
+
+Result<DatabaseVersion, QMResult> SchemaVersion001::InitializeConnection(
+ ResultConnection& aConn, const Origin& aOrigin) {
+ QM_TRY_UNWRAP(bool isEmpty, CheckIfEmpty(aConn));
+
+ DatabaseVersion currentVersion = 0;
+
+ if (isEmpty) {
+ QM_TRY(QM_TO_RESULT(SetEncoding(aConn)));
+ } else {
+ QM_TRY(QM_TO_RESULT(aConn->GetSchemaVersion(&currentVersion)));
+ }
+
+ if (currentVersion < sVersion) {
+ mozStorageTransaction transaction(
+ aConn.get(),
+ /* commit on complete */ false,
+ mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ QM_TRY(QM_TO_RESULT(CreateEntries(aConn)));
+ QM_TRY(QM_TO_RESULT(CreateDirectories(aConn)));
+ QM_TRY(QM_TO_RESULT(CreateFiles(aConn)));
+ QM_TRY(QM_TO_RESULT(CreateUsages(aConn)));
+ QM_TRY(QM_TO_RESULT(CreateRootEntry(aConn, aOrigin)));
+ QM_TRY(QM_TO_RESULT(aConn->SetSchemaVersion(sVersion)));
+
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+ }
+
+ QM_TRY(QM_TO_RESULT(aConn->GetSchemaVersion(&currentVersion)));
+
+ return currentVersion;
+}
+
+} // namespace mozilla::dom::fs
diff --git a/dom/fs/parent/datamodel/SchemaVersion001.h b/dom/fs/parent/datamodel/SchemaVersion001.h
new file mode 100644
index 0000000000..606cada50d
--- /dev/null
+++ b/dom/fs/parent/datamodel/SchemaVersion001.h
@@ -0,0 +1,25 @@
+/* -*- 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/. */
+
+#ifndef DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION001_H_
+#define DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION001_H_
+
+#include "ResultConnection.h"
+#include "mozilla/dom/FileSystemTypes.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+
+namespace mozilla::dom::fs {
+
+struct SchemaVersion001 {
+ static Result<DatabaseVersion, QMResult> InitializeConnection(
+ ResultConnection& aConn, const Origin& aOrigin);
+
+ static const DatabaseVersion sVersion = 1;
+};
+
+} // namespace mozilla::dom::fs
+
+#endif // DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION001_H_
diff --git a/dom/fs/parent/datamodel/moz.build b/dom/fs/parent/datamodel/moz.build
new file mode 100644
index 0000000000..111a793722
--- /dev/null
+++ b/dom/fs/parent/datamodel/moz.build
@@ -0,0 +1,26 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXPORTS.mozilla.dom += [
+ "FileSystemDataManager.h",
+]
+
+UNIFIED_SOURCES += [
+ "FileSystemDatabaseManager.cpp",
+ "FileSystemDatabaseManagerVersion001.cpp",
+ "FileSystemDataManager.cpp",
+ "FileSystemFileManager.cpp",
+ "SchemaVersion001.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/fs/include",
+ "/dom/fs/parent",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"