From bbf9109b684f71756e5269047f57c4b6a7a5d90d Mon Sep 17 00:00:00 2001 From: Jari Jalkanen Date: Thu, 1 Sep 2022 12:58:08 +0000 Subject: [PATCH] Bug 1758092 - Establish database support for origin private file system; r=jesup Differential Revision: https://phabricator.services.mozilla.com/D140308 --- dom/fs/api/FileSystemHandle.h | 2 + dom/fs/child/FileSystemRequestHandler.cpp | 69 ++- dom/fs/include/fs/FileSystemRequestHandler.h | 12 +- dom/fs/parent/FileSystemManagerParent.cpp | 109 +++- .../parent/FileSystemManagerParentFactory.cpp | 25 +- dom/fs/parent/GetDirectoryForOrigin.cpp | 41 ++ dom/fs/parent/GetDirectoryForOrigin.h | 36 ++ dom/fs/parent/ResultConnection.h | 19 + dom/fs/parent/ResultStatement.cpp | 23 + dom/fs/parent/ResultStatement.h | 163 +++++ .../datamodel/FileSystemDataManager.cpp | 206 ++++++- .../parent/datamodel/FileSystemDataManager.h | 25 +- .../datamodel/FileSystemDatabaseManager.h | 115 ++++ .../FileSystemDatabaseManagerVersion001.cpp | 581 ++++++++++++++++++ .../FileSystemDatabaseManagerVersion001.h | 75 +++ dom/fs/parent/datamodel/SchemaVersion001.cpp | 193 ++++++ dom/fs/parent/datamodel/SchemaVersion001.h | 25 + dom/fs/parent/datamodel/moz.build | 2 + dom/fs/parent/moz.build | 7 + dom/fs/shared/FileSystemTypes.h | 3 +- .../common/test_fileSystemDirectoryHandle.js | 122 ++++ dom/fs/test/gtest/FileSystemMocks.h | 5 + dom/fs/test/gtest/TestHelpers.h | 23 +- .../api/TestFileSystemDirectoryHandle.cpp | 2 + .../gtest/parent/TestFileSystemHashSource.cpp | 2 +- .../datamodel/TestFileSystemDataManager.cpp | 23 +- .../TestFileSystemDataManagerVersion001.cpp | 306 +++++++++ dom/fs/test/gtest/parent/datamodel/moz.build | 3 + dom/fs/test/moz.build | 1 + .../test_fileSystemDirectoryHandle.js | 14 + dom/fs/test/xpcshell/xpcshell.ini | 1 + 31 files changed, 2166 insertions(+), 67 deletions(-) create mode 100644 dom/fs/parent/GetDirectoryForOrigin.cpp create mode 100644 dom/fs/parent/GetDirectoryForOrigin.h create mode 100644 dom/fs/parent/ResultConnection.h create mode 100644 dom/fs/parent/ResultStatement.cpp create mode 100644 dom/fs/parent/ResultStatement.h create mode 100644 dom/fs/parent/datamodel/FileSystemDatabaseManager.h create mode 100644 dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp create mode 100644 dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h create mode 100644 dom/fs/parent/datamodel/SchemaVersion001.cpp create mode 100644 dom/fs/parent/datamodel/SchemaVersion001.h create mode 100644 dom/fs/test/common/test_fileSystemDirectoryHandle.js create mode 100644 dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersion001.cpp create mode 100644 dom/fs/test/xpcshell/test_fileSystemDirectoryHandle.js diff --git a/dom/fs/api/FileSystemHandle.h b/dom/fs/api/FileSystemHandle.h index 9592126e4c67..4b1eac5db563 100644 --- a/dom/fs/api/FileSystemHandle.h +++ b/dom/fs/api/FileSystemHandle.h @@ -48,6 +48,8 @@ class FileSystemHandle : public nsISupports, public nsWrapperCache { NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(FileSystemHandle) + const fs::EntryId& GetId() const { return mMetadata.entryId(); } + // WebIDL Boilerplate nsIGlobalObject* GetParentObject() const; diff --git a/dom/fs/child/FileSystemRequestHandler.cpp b/dom/fs/child/FileSystemRequestHandler.cpp index ca6220ddd179..4cae297c8c97 100644 --- a/dom/fs/child/FileSystemRequestHandler.cpp +++ b/dom/fs/child/FileSystemRequestHandler.cpp @@ -36,29 +36,32 @@ RefPtr MakeGetFileResult(nsIGlobalObject* aGlobal, const nsString& aName, return result; } -void GetDirectoryContentsResponseHandler(nsIGlobalObject* aGlobal, - FileSystemDirectoryListing&& aResponse, - ArrayAppendable& /* aSink */, - RefPtr& aManager) { +void GetDirectoryContentsResponseHandler( + nsIGlobalObject* aGlobal, FileSystemGetEntriesResponse&& aResponse, + ArrayAppendable& aSink, RefPtr& aManager) { // TODO: Add page size to FileSystemConstants, preallocate and handle overflow + const auto& listing = aResponse.get_FileSystemDirectoryListing(); + nsTArray> batch; - for (const auto& it : aResponse.files()) { + for (const auto& it : listing.files()) { RefPtr handle = new FileSystemFileHandle(aGlobal, aManager, it); batch.AppendElement(handle); } - for (const auto& it : aResponse.directories()) { + for (const auto& it : listing.directories()) { RefPtr handle = new FileSystemDirectoryHandle(aGlobal, aManager, it); batch.AppendElement(handle); } + + aSink.append(batch); } RefPtr MakeResolution( nsIGlobalObject* aGlobal, FileSystemGetHandleResponse&& aResponse, - const RefPtr& /* aResolution */, + const RefPtr& /* aResult */, RefPtr& aManager) { RefPtr result = new FileSystemDirectoryHandle( aGlobal, aManager, @@ -68,8 +71,8 @@ RefPtr MakeResolution( RefPtr MakeResolution( nsIGlobalObject* aGlobal, FileSystemGetHandleResponse&& aResponse, - const RefPtr& /* aResolution */, - const Name& aName, RefPtr& aManager) { + const RefPtr& /* aResult */, const Name& aName, + RefPtr& aManager) { RefPtr result = new FileSystemDirectoryHandle( aGlobal, aManager, FileSystemEntryMetadata(aResponse.get_EntryId(), aName)); @@ -79,7 +82,7 @@ RefPtr MakeResolution( RefPtr MakeResolution( nsIGlobalObject* aGlobal, FileSystemGetHandleResponse&& aResponse, - const RefPtr& /* aResolution */, const Name& aName, + const RefPtr& /* aResult */, const Name& aName, RefPtr& aManager) { RefPtr result = new FileSystemFileHandle( aGlobal, aManager, @@ -89,7 +92,7 @@ RefPtr MakeResolution( RefPtr MakeResolution(nsIGlobalObject* aGlobal, FileSystemGetFileResponse&& aResponse, - const RefPtr& /* aResolution */, + const RefPtr& /* aResult */, const Name& aName, RefPtr& aManager) { auto& fileProperties = aResponse.get_FileSystemFileProperties(); @@ -167,6 +170,27 @@ void ResolveCallback(FileSystemGetEntriesResponse&& aResponse, aPromise->MaybeReject(NS_ERROR_NOT_IMPLEMENTED); } +template <> +void ResolveCallback(FileSystemResolveResponse&& aResponse, + // NOLINTNEXTLINE(performance-unnecessary-value-param) + RefPtr aPromise) { + MOZ_ASSERT(aPromise); + QM_TRY(OkIf(Promise::PromiseState::Pending == aPromise->State()), QM_VOID); + + if (FileSystemResolveResponse::Tnsresult == aResponse.type()) { + aPromise->MaybeReject(aResponse.get_nsresult()); + return; + } + + auto& maybePath = aResponse.get_MaybeFileSystemPath(); + if (maybePath.isSome()) { + aPromise->MaybeResolve(maybePath.value().path()); + return; + } + + aPromise->MaybeResolveWithUndefined(); +} + template ::value, bool> = true> mozilla::ipc::ResolveCallback SelectResolveCallback( @@ -374,4 +398,27 @@ void FileSystemRequestHandler::RemoveEntry( std::move(onReject)); } +void FileSystemRequestHandler::Resolve( + RefPtr& aManager, + // NOLINTNEXTLINE(performance-unnecessary-value-param) + const FileSystemEntryPair& aEndpoints, RefPtr aPromise) { + MOZ_ASSERT(aManager); + MOZ_ASSERT(!aEndpoints.parentId().IsEmpty()); + MOZ_ASSERT(!aEndpoints.childId().IsEmpty()); + MOZ_ASSERT(aPromise); + + FileSystemResolveRequest request(aEndpoints); + + auto&& onResolve = + SelectResolveCallback(aPromise); + + auto&& onReject = GetRejectCallback(aPromise); + + QM_TRY(OkIf(aManager->Actor()), QM_VOID, [aPromise](const auto&) { + aPromise->MaybeRejectWithUnknownError("Invalid actor"); + }); + aManager->Actor()->SendResolve(request, std::move(onResolve), + std::move(onReject)); +} + } // namespace mozilla::dom::fs diff --git a/dom/fs/include/fs/FileSystemRequestHandler.h b/dom/fs/include/fs/FileSystemRequestHandler.h index 0d53fcf7939a..69db30efeee2 100644 --- a/dom/fs/include/fs/FileSystemRequestHandler.h +++ b/dom/fs/include/fs/FileSystemRequestHandler.h @@ -14,14 +14,20 @@ class RefPtr; namespace mozilla::dom { +class FileSystemHandle; class FileSystemManager; class Promise; namespace fs { -class ArrayAppendable {}; class FileSystemChildMetadata; class FileSystemEntryMetadata; +class FileSystemEntryPair; + +class ArrayAppendable { + public: + void append(const nsTArray>& /* aBatch */) {} +}; class FileSystemRequestHandler { public: @@ -48,6 +54,10 @@ class FileSystemRequestHandler { const FileSystemChildMetadata& aEntry, bool aRecursive, RefPtr aPromise); + virtual void Resolve(RefPtr& aManager, + const FileSystemEntryPair& aEndpoints, + RefPtr aPromise); + virtual ~FileSystemRequestHandler() = default; }; // class FileSystemRequestHandler diff --git a/dom/fs/parent/FileSystemManagerParent.cpp b/dom/fs/parent/FileSystemManagerParent.cpp index c554ebdac3f9..adb781d99487 100644 --- a/dom/fs/parent/FileSystemManagerParent.cpp +++ b/dom/fs/parent/FileSystemManagerParent.cpp @@ -5,11 +5,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "FileSystemManagerParent.h" -#include "nsNetCID.h" + +#include "FileSystemDatabaseManager.h" +#include "mozilla/Maybe.h" #include "mozilla/dom/FileSystemDataManager.h" #include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/QMResult.h" #include "mozilla/dom/quota/ForwardDecls.h" -#include "mozilla/ipc/Endpoint.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "nsString.h" +#include "nsTArray.h" using IPCResult = mozilla::ipc::IPCResult; @@ -42,9 +48,23 @@ IPCResult FileSystemManagerParent::RecvGetRootHandleMsg( } IPCResult FileSystemManagerParent::RecvGetDirectoryHandleMsg( - FileSystemGetHandleRequest&& /* aRequest */, + FileSystemGetHandleRequest&& aRequest, GetDirectoryHandleMsgResolver&& aResolver) { - FileSystemGetHandleResponse response(NS_ERROR_NOT_IMPLEMENTED); + MOZ_ASSERT(!aRequest.handle().parentId().IsEmpty()); + MOZ_ASSERT(mDataManager); + + auto reportError = [&aResolver](const QMResult& aRv) { + FileSystemGetHandleResponse response(aRv.NSResult()); + aResolver(response); + }; + + QM_TRY_UNWRAP(fs::EntryId entryId, + mDataManager->MutableDatabaseManagerPtr()->GetOrCreateDirectory( + aRequest.handle(), aRequest.create()), + IPC_OK(), reportError); + MOZ_ASSERT(!entryId.IsEmpty()); + + FileSystemGetHandleResponse response(entryId); aResolver(response); return IPC_OK(); @@ -53,7 +73,21 @@ IPCResult FileSystemManagerParent::RecvGetDirectoryHandleMsg( IPCResult FileSystemManagerParent::RecvGetFileHandleMsg( FileSystemGetHandleRequest&& aRequest, GetFileHandleMsgResolver&& aResolver) { - FileSystemGetHandleResponse response(NS_ERROR_NOT_IMPLEMENTED); + MOZ_ASSERT(!aRequest.handle().parentId().IsEmpty()); + MOZ_ASSERT(mDataManager); + + auto reportError = [&aResolver](const QMResult& aRv) { + FileSystemGetHandleResponse response(aRv.NSResult()); + aResolver(response); + }; + + QM_TRY_UNWRAP(fs::EntryId entryId, + mDataManager->MutableDatabaseManagerPtr()->GetOrCreateFile( + aRequest.handle(), aRequest.create()), + IPC_OK(), reportError); + MOZ_ASSERT(!entryId.IsEmpty()); + + FileSystemGetHandleResponse response(entryId); aResolver(response); return IPC_OK(); @@ -69,7 +103,36 @@ IPCResult FileSystemManagerParent::RecvGetFileMsg( IPCResult FileSystemManagerParent::RecvResolveMsg( FileSystemResolveRequest&& aRequest, ResolveMsgResolver&& aResolver) { - FileSystemResolveResponse response(NS_ERROR_NOT_IMPLEMENTED); + MOZ_ASSERT(!aRequest.endpoints().parentId().IsEmpty()); + MOZ_ASSERT(!aRequest.endpoints().childId().IsEmpty()); + MOZ_ASSERT(mDataManager); + + fs::Path filePath; + if (aRequest.endpoints().parentId() == aRequest.endpoints().childId()) { + FileSystemResolveResponse response(Some(FileSystemPath(filePath))); + aResolver(response); + + return IPC_OK(); + } + + auto reportError = [&aResolver](const QMResult& aRv) { + FileSystemResolveResponse response(aRv.NSResult()); + aResolver(response); + }; + + QM_TRY_UNWRAP( + filePath, + mDataManager->MutableDatabaseManagerPtr()->Resolve(aRequest.endpoints()), + IPC_OK(), reportError); + + if (filePath.IsEmpty()) { + FileSystemResolveResponse response(Nothing{}); + aResolver(response); + + return IPC_OK(); + } + + FileSystemResolveResponse response(Some(FileSystemPath(filePath))); aResolver(response); return IPC_OK(); @@ -86,7 +149,39 @@ IPCResult FileSystemManagerParent::RecvGetEntriesMsg( IPCResult FileSystemManagerParent::RecvRemoveEntryMsg( FileSystemRemoveEntryRequest&& aRequest, RemoveEntryMsgResolver&& aResolver) { - FileSystemRemoveEntryResponse response(NS_ERROR_NOT_IMPLEMENTED); + MOZ_ASSERT(!aRequest.handle().parentId().IsEmpty()); + MOZ_ASSERT(mDataManager); + + auto reportError = [&aResolver](const QMResult& aRv) { + FileSystemRemoveEntryResponse response(aRv.NSResult()); + aResolver(response); + }; + + QM_TRY_UNWRAP( + bool isDeleted, + mDataManager->MutableDatabaseManagerPtr()->RemoveFile(aRequest.handle()), + IPC_OK(), reportError); + + if (isDeleted) { + FileSystemRemoveEntryResponse response(NS_OK); + aResolver(response); + + return IPC_OK(); + } + + QM_TRY_UNWRAP(isDeleted, + mDataManager->MutableDatabaseManagerPtr()->RemoveDirectory( + aRequest.handle(), aRequest.recursive()), + IPC_OK(), reportError); + + if (!isDeleted) { + FileSystemRemoveEntryResponse response(NS_ERROR_UNEXPECTED); + aResolver(response); + + return IPC_OK(); + } + + FileSystemRemoveEntryResponse response(NS_OK); aResolver(response); return IPC_OK(); diff --git a/dom/fs/parent/FileSystemManagerParentFactory.cpp b/dom/fs/parent/FileSystemManagerParentFactory.cpp index 6bfd32444c4d..6fdf3bad7647 100644 --- a/dom/fs/parent/FileSystemManagerParentFactory.cpp +++ b/dom/fs/parent/FileSystemManagerParentFactory.cpp @@ -12,6 +12,7 @@ #include "mozilla/dom/FileSystemTypes.h" #include "mozilla/dom/quota/QuotaCommon.h" #include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/ResultExtensions.h" #include "mozilla/ipc/Endpoint.h" #include "nsIScriptObjectPrincipal.h" #include "nsString.h" @@ -27,12 +28,6 @@ LazyLogModule gOPFSLog("OPFS"); namespace mozilla::dom { -namespace fs::data { - -EntryId GetRootHandle(const Origin& aOrigin) { return "not implemented"_ns; } - -} // namespace fs::data - mozilla::ipc::IPCResult CreateFileSystemManagerParent( const mozilla::ipc::PrincipalInfo& aPrincipalInfo, mozilla::ipc::Endpoint&& aParentEndpoint, @@ -46,18 +41,26 @@ mozilla::ipc::IPCResult CreateFileSystemManagerParent( QM_TRY(OkIf(aParentEndpoint.IsValid()), IPC_OK(), [aResolver](const auto&) { aResolver(NS_ERROR_INVALID_ARG); }); - nsAutoCString origin = - quota::QuotaManager::GetOriginFromValidatedPrincipalInfo(aPrincipalInfo); + quota::OriginMetadata originMetadata( + quota::QuotaManager::GetInfoFromValidatedPrincipalInfo(aPrincipalInfo), + quota::PERSISTENCE_TYPE_DEFAULT); + + LOG(("CreateFileSystemManagerParent, origin: %s", + originMetadata.mOrigin.get())); // This creates the file system data manager, which has to be done on // PBackground - fs::data::FileSystemDataManager::GetOrCreateFileSystemDataManager(origin) + fs::data::FileSystemDataManager::GetOrCreateFileSystemDataManager( + originMetadata) ->Then( GetCurrentSerialEventTarget(), __func__, - [origin, parentEndpoint = std::move(aParentEndpoint), + [origin = originMetadata.mOrigin, + parentEndpoint = std::move(aParentEndpoint), aResolver](const fs::Registered& dataManager) mutable { - fs::EntryId rootId = fs::data::GetRootHandle(origin); + QM_TRY_UNWRAP( + fs::EntryId rootId, fs::data::GetRootHandle(origin), QM_VOID, + [aResolver](const auto& aRv) { aResolver(aRv.NSResult()); }); InvokeAsync( dataManager->MutableIOTargetPtr(), __func__, diff --git a/dom/fs/parent/GetDirectoryForOrigin.cpp b/dom/fs/parent/GetDirectoryForOrigin.cpp new file mode 100644 index 000000000000..7a1d9403ae8e --- /dev/null +++ b/dom/fs/parent/GetDirectoryForOrigin.cpp @@ -0,0 +1,41 @@ +/* -*- 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 "GetDirectoryForOrigin.h" + +#include "FileSystemHashSource.h" +#include "nsCOMPtr.h" +#include "nsIFile.h" +#include "nsDirectoryServiceDefs.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/dom/QMResult.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/ResultExtensions.h" + +namespace mozilla::dom::fs { + +Result, QMResult> GetDirectoryForOrigin( + const quota::QuotaManager& aQuotaManager, const Origin& aOrigin) { + QM_TRY_UNWRAP(auto directory, QM_TO_RESULT_TRANSFORM(quota::QM_NewLocalFile( + aQuotaManager.GetBasePath()))); + + QM_TRY(QM_TO_RESULT(directory->Append(u"opfs-storage"_ns))); + + QM_TRY_UNWRAP(auto originHash, + data::FileSystemHashSource::GenerateHash( + "parent"_ns, NS_ConvertUTF8toUTF16(aOrigin))); + + QM_TRY_UNWRAP(auto encodedOrigin, + data::FileSystemHashSource::EncodeHash(originHash)); + + QM_TRY(QM_TO_RESULT(directory->Append(encodedOrigin))); + + return directory; +} + +} // namespace mozilla::dom::fs diff --git a/dom/fs/parent/GetDirectoryForOrigin.h b/dom/fs/parent/GetDirectoryForOrigin.h new file mode 100644 index 000000000000..8733197f94ea --- /dev/null +++ b/dom/fs/parent/GetDirectoryForOrigin.h @@ -0,0 +1,36 @@ +/* -*- 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_GETDIRECTORYFORORIGIN_H_ +#define DOM_FS_PARENT_GETDIRECTORYFORORIGIN_H_ + +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/quota/ForwardDecls.h" + +template +class nsCOMPtr; + +class nsIFile; + +namespace mozilla { +template +class Result; +} + +namespace mozilla::dom::quota { +class QuotaManager; +} + +namespace mozilla::dom::fs { + +// XXX This function can be removed once the integration with quota manager is +// finished. +Result, QMResult> GetDirectoryForOrigin( + const quota::QuotaManager& aQuotaManager, const Origin& aOrigin); + +} // namespace mozilla::dom::fs + +#endif // DOM_FS_PARENT_GETDIRECTORYFORORIGIN_H_ diff --git a/dom/fs/parent/ResultConnection.h b/dom/fs/parent/ResultConnection.h new file mode 100644 index 000000000000..3606a55a6bde --- /dev/null +++ b/dom/fs/parent/ResultConnection.h @@ -0,0 +1,19 @@ +/* -*- 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_RESULTCONNECTION_H_ +#define DOM_FS_PARENT_RESULTCONNECTION_H_ + +#include "mozIStorageConnection.h" +#include "nsCOMPtr.h" + +namespace mozilla::dom::fs { + +using ResultConnection = nsCOMPtr; + +} // namespace mozilla::dom::fs + +#endif // DOM_FS_PARENT_RESULTCONNECTION_H_ diff --git a/dom/fs/parent/ResultStatement.cpp b/dom/fs/parent/ResultStatement.cpp new file mode 100644 index 000000000000..0b0e2cf4a894 --- /dev/null +++ b/dom/fs/parent/ResultStatement.cpp @@ -0,0 +1,23 @@ +/* -*- 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 "ResultStatement.h" + +#include "mozIStorageConnection.h" + +namespace mozilla::dom::fs { + +Result ResultStatement::Create( + const ResultConnection& aConnection, const nsACString& aSQLStatement) { + nsCOMPtr stmt; + + QM_TRY(QM_TO_RESULT( + aConnection->CreateStatement(aSQLStatement, getter_AddRefs(stmt)))); + + return ResultStatement(stmt); +}; + +} // namespace mozilla::dom::fs diff --git a/dom/fs/parent/ResultStatement.h b/dom/fs/parent/ResultStatement.h new file mode 100644 index 000000000000..94892595dbd4 --- /dev/null +++ b/dom/fs/parent/ResultStatement.h @@ -0,0 +1,163 @@ +/* -*- 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_RESULTSTATEMENT_H_ +#define DOM_FS_PARENT_RESULTSTATEMENT_H_ + +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozIStorageStatement.h" +#include "nsCOMPtr.h" +#include "nsString.h" + +class mozIStorageConnection; + +namespace mozilla::dom::fs { + +using Column = uint32_t; + +using ResultConnection = nsCOMPtr; + +/** + * @brief ResultStatement + * - provides error monad Result compatible interface to the lower level + * error code-based statement implementation in order to enable remote + * debugging with error stack traces + * - converts between OPFS internal data types and the generic data types of + * the lower level implementation + * - provides a customization point for requests aimed at the lower level + * implementation allowing for example to remap errors or implement mocks + */ +class ResultStatement { + public: + using underlying_t = nsCOMPtr; + + explicit ResultStatement(underlying_t aStmt) : mStmt(std::move(aStmt)) {} + + ResultStatement(const ResultStatement& aOther) + : ResultStatement(aOther.mStmt) {} + + ResultStatement(ResultStatement&& aOther) noexcept + : ResultStatement(std::move(aOther.mStmt)) {} + + ResultStatement& operator=(const ResultStatement& aOther) = default; + + ResultStatement& operator=(ResultStatement&& aOther) noexcept { + mStmt = std::move(aOther.mStmt); + return *this; + } + + static Result Create( + const ResultConnection& aConnection, const nsACString& aSQLStatement); + + // XXX Consider moving all these "inline" methods into a separate file + // called ResultStatementInlines.h. ResultStatement.h wouldn't have to then + // include ResultExtensions.h, QuotaCommon.h and mozIStorageStatement.h + // which are quite large and should be preferable only included from cpp + // files or special headers like ResultStatementInlines.h. So in the end, + // other headers would include ResultStatement.h only and other cpp files + // would include ResultStatementInlines.h. See also IndedexDababase.h and + // IndexedDatabaseInlines.h to see how it's done. + + inline nsresult BindEntryIdByName(const nsACString& aField, + const EntryId& aValue) { + return mStmt->BindUTF8StringAsBlobByName(aField, aValue); + } + + inline nsresult BindContentTypeByName(const nsACString& aField, + const ContentType& aValue) { + return mStmt->BindStringByName(aField, aValue); + } + + inline nsresult BindNameByName(const nsACString& aField, const Name& aValue) { + return mStmt->BindStringAsBlobByName(aField, aValue); + } + + inline nsresult BindPageNumberByName(const nsACString& aField, + PageNumber aValue) { + return mStmt->BindInt32ByName(aField, aValue); + } + + inline nsresult BindUsageByName(const nsACString& aField, Usage aValue) { + return mStmt->BindInt64ByName(aField, aValue); + } + + inline Result GetBoolByColumn(Column aColumn) { + int32_t value = 0; + QM_TRY(QM_TO_RESULT(mStmt->GetInt32(aColumn, &value))); + + return 0 != value; + } + + inline Result GetContentTypeByColumn(Column aColumn) { + ContentType value; + QM_TRY(QM_TO_RESULT(mStmt->GetString(aColumn, value))); + + return value; + } + + inline Result GetDatabaseVersion() { + bool hasEntries = false; + QM_TRY(QM_TO_RESULT(mStmt->ExecuteStep(&hasEntries))); + MOZ_ALWAYS_TRUE(hasEntries); + + DatabaseVersion value = 0; + QM_TRY(QM_TO_RESULT(mStmt->GetInt32(0u, &value))); + + return value; + } + + inline Result GetEntryIdByColumn(Column aColumn) { + EntryId value; + QM_TRY(QM_TO_RESULT(mStmt->GetBlobAsUTF8String(aColumn, value))); + + return value; + } + + inline Result GetNameByColumn(Column aColumn) { + Name value; + QM_TRY(QM_TO_RESULT(mStmt->GetBlobAsString(aColumn, value))); + + return value; + } + + inline Result GetUsageByColumn(Column aColumn) { + Usage value = 0; + QM_TRY(QM_TO_RESULT(mStmt->GetInt64(aColumn, &value))); + + return value; + } + + inline bool IsNullByColumn(Column aColumn) const { + bool value = mStmt->IsNull(aColumn); + + return value; + } + + inline nsresult Execute() { return mStmt->Execute(); } + + inline Result ExecuteStep() { + bool hasEntries = false; + QM_TRY(QM_TO_RESULT(mStmt->ExecuteStep(&hasEntries))); + + return hasEntries; + } + + inline Result YesOrNoQuery() { + bool hasEntries = false; + QM_TRY(QM_TO_RESULT(mStmt->ExecuteStep(&hasEntries))); + MOZ_ALWAYS_TRUE(hasEntries); + return GetBoolByColumn(0u); + } + + private: + underlying_t mStmt; +}; + +} // namespace mozilla::dom::fs + +#endif // DOM_FS_PARENT_RESULTSTATEMENT_H_ diff --git a/dom/fs/parent/datamodel/FileSystemDataManager.cpp b/dom/fs/parent/datamodel/FileSystemDataManager.cpp index e4d69c90063e..b589df594a04 100644 --- a/dom/fs/parent/datamodel/FileSystemDataManager.cpp +++ b/dom/fs/parent/datamodel/FileSystemDataManager.cpp @@ -6,6 +6,11 @@ #include "FileSystemDataManager.h" +#include "FileSystemDatabaseManager.h" +#include "FileSystemDatabaseManagerVersion001.h" +#include "FileSystemHashSource.h" +#include "fs/FileSystemConstants.h" +#include "GetDirectoryForOrigin.h" #include "mozilla/Result.h" #include "mozilla/StaticPtr.h" #include "mozilla/dom/FileSystemManagerParent.h" @@ -13,12 +18,18 @@ #include "mozilla/dom/quota/QuotaManager.h" #include "mozilla/dom/quota/ResultExtensions.h" #include "mozilla/ipc/BackgroundParent.h" +#include "mozIStorageService.h" +#include "mozStorageCID.h" #include "nsBaseHashtable.h" +#include "nsCOMPtr.h" #include "nsHashKeys.h" +#include "nsIFile.h" #include "nsNetCID.h" #include "nsProxyRelease.h" #include "nsServiceManagerUtils.h" +#include "nsString.h" #include "nsThreadUtils.h" +#include "SchemaVersion001.h" namespace mozilla::dom::fs::data { @@ -93,11 +104,106 @@ void RemoveFileSystemDataManager(const Origin& aOrigin) { } } +nsresult GetOrCreateEntry(const nsAString& aDesiredFilePath, bool& aExists, + nsCOMPtr& aFile, + decltype(nsIFile::NORMAL_FILE_TYPE) aKind, + uint32_t aPermissions) { + MOZ_ASSERT(!aDesiredFilePath.IsEmpty()); + + QM_TRY(MOZ_TO_RESULT(NS_NewLocalFile(aDesiredFilePath, + /* aFollowLinks */ false, + getter_AddRefs(aFile)))); + + QM_TRY(MOZ_TO_RESULT(aFile->Exists(&aExists))); + if (aExists) { + return NS_OK; + } + + QM_TRY(MOZ_TO_RESULT(aFile->Create(aKind, aPermissions))); + + return NS_OK; +} + +nsresult GetFileSystemDirectory(const Origin& aOrigin, + nsCOMPtr& aDirectory) { + QM_TRY_UNWRAP(RefPtr quotaManager, + quota::QuotaManager::GetOrCreate()); + +#if 0 + QM_TRY_UNWRAP(aDirectory, quotaManager->GetDirectoryForOrigin( + quota::PERSISTENCE_TYPE_DEFAULT, aOrigin)); +#else + QM_TRY_UNWRAP(aDirectory, GetDirectoryForOrigin(*quotaManager, aOrigin)); +#endif + + QM_TRY(MOZ_TO_RESULT(aDirectory->AppendRelativePath(u"fs"_ns))); + + return NS_OK; +} + +nsresult GetFileSystemDatabaseFile(const Origin& aOrigin, + nsString& aDatabaseFile) { + nsCOMPtr directoryPath; + QM_TRY(MOZ_TO_RESULT(GetFileSystemDirectory(aOrigin, directoryPath))); + + QM_TRY( + MOZ_TO_RESULT(directoryPath->AppendRelativePath(u"metadata.sqlite"_ns))); + + QM_TRY(MOZ_TO_RESULT(directoryPath->GetPath(aDatabaseFile))); + + return NS_OK; +} + +nsresult GetDatabasePath(const Origin& aOrigin, bool& aExists, + nsCOMPtr& aFile) { + nsString databaseFilePath; + QM_TRY(MOZ_TO_RESULT(GetFileSystemDatabaseFile(aOrigin, databaseFilePath))); + + MOZ_ASSERT(!databaseFilePath.IsEmpty()); + + return GetOrCreateEntry(databaseFilePath, aExists, aFile, + nsIFile::NORMAL_FILE_TYPE, 0644); +} + +Result GetStorageConnection(const Origin& aOrigin) { + bool exists = false; + nsCOMPtr databaseFile; + QM_TRY(QM_TO_RESULT(GetDatabasePath(aOrigin, exists, databaseFile))); + + QM_TRY_INSPECT( + const auto& storageService, + QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_GET_TYPED( + nsCOMPtr, MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID))); + + const auto flags = mozIStorageService::CONNECTION_DEFAULT; + ResultConnection connection; + QM_TRY(QM_TO_RESULT(storageService->OpenDatabase( + databaseFile, flags, getter_AddRefs(connection)))); + + return connection; +} + } // namespace +Result GetRootHandle(const Origin& origin) { + MOZ_ASSERT(!origin.IsEmpty()); + + return FileSystemHashSource::GenerateHash(origin, kRootName); +} + +Result GetEntryHandle( + const FileSystemChildMetadata& aHandle) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + return FileSystemHashSource::GenerateHash(aHandle.parentId(), + aHandle.childName()); +} + FileSystemDataManager::FileSystemDataManager( - const Origin& aOrigin, MovingNotNull> aIOTaskQueue) - : mOrigin(aOrigin), + const quota::OriginMetadata& aOriginMetadata, + MovingNotNull> aIOTaskQueue) + : mOriginMetadata(aOriginMetadata), mBackgroundTarget(WrapNotNull(GetCurrentSerialEventTarget())), mIOTaskQueue(std::move(aIOTaskQueue)), mRegCount(0), @@ -108,13 +214,14 @@ FileSystemDataManager::~FileSystemDataManager() { } RefPtr -FileSystemDataManager::GetOrCreateFileSystemDataManager(const Origin& aOrigin) { +FileSystemDataManager::GetOrCreateFileSystemDataManager( + const quota::OriginMetadata& aOriginMetadata) { if (quota::QuotaManager::IsShuttingDown()) { return CreatePromise::CreateAndReject(NS_ERROR_FAILURE, __func__); } if (RefPtr dataManager = - GetFileSystemDataManager(aOrigin)) { + 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 @@ -137,8 +244,8 @@ FileSystemDataManager::GetOrCreateFileSystemDataManager(const Origin& aOrigin) { // call to `GetOrCreateFileSystemManager` will handle any new situation. return dataManager->OnClose()->Then( GetCurrentSerialEventTarget(), __func__, - [aOrigin](const BoolPromise::ResolveOrRejectValue&) { - return GetOrCreateFileSystemDataManager(aOrigin); + [aOriginMetadata](const BoolPromise::ResolveOrRejectValue&) { + return GetOrCreateFileSystemDataManager(aOriginMetadata); }); } @@ -152,15 +259,15 @@ FileSystemDataManager::GetOrCreateFileSystemDataManager(const Origin& aOrigin) { NS_STREAMTRANSPORTSERVICE_CONTRACTID), CreatePromise::CreateAndReject(NS_ERROR_FAILURE, __func__)); - nsCString taskQueueName("OPFS "_ns + aOrigin); + nsCString taskQueueName("OPFS "_ns + aOriginMetadata.mOrigin); RefPtr ioTaskQueue = TaskQueue::Create(streamTransportService.forget(), taskQueueName.get()); auto dataManager = MakeRefPtr( - aOrigin, WrapMovingNotNull(ioTaskQueue)); + aOriginMetadata, WrapMovingNotNull(ioTaskQueue)); - AddFileSystemDataManager(aOrigin, dataManager); + AddFileSystemDataManager(aOriginMetadata.mOrigin, dataManager); return dataManager->BeginOpen()->Then( GetCurrentSerialEventTarget(), __func__, @@ -259,18 +366,79 @@ RefPtr FileSystemDataManager::BeginOpen() { mState = State::Opening; - // XXX Add storage initialization to the chain. + // These have to be done on the PBackground thread + auto connectionRes = fs::data::GetStorageConnection(mOriginMetadata.mOrigin); + if (NS_WARN_IF(connectionRes.isErr())) { + return BoolPromise::CreateAndReject(connectionRes.unwrapErr().NSResult(), + __func__); + } + auto connection = connectionRes.unwrap(); + QM_TRY_UNWRAP(DatabaseVersion version, + SchemaVersion001::InitializeConnection(connection, + mOriginMetadata.mOrigin), + BoolPromise::CreateAndReject(NS_ERROR_FAILURE, __func__)); - InvokeAsync(MutableIOTargetPtr(), __func__, - [self = RefPtr(this)]() mutable { - nsCOMPtr target = - self->MutableBackgroundTargetPtr(); + auto quotaManagerRes = quota::QuotaManager::GetOrCreate(); + if (NS_WARN_IF(quotaManagerRes.isErr())) { + return BoolPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + RefPtr quotamanager = quotaManagerRes.unwrap(); - NS_ProxyRelease("ReleaseFileSystemDataManager", target, - self.forget()); + if (1 == version) { + mDatabaseManager = + MakeUnique(std::move(connection)); + } - return BoolPromise::CreateAndResolve(true, __func__); - }) + // XXX Take a directory lock here so our origin isn't evicted + + InvokeAsync( + quotamanager->IOThread(), __func__, + [self = RefPtr(this)]() mutable { + auto autoProxyReleaseManager = MakeScopeExit([&self] { + nsCOMPtr target = + self->MutableBackgroundTargetPtr(); + + NS_ProxyRelease("ReleaseFileSystemDataManager", target, + self.forget()); + }); + + quota::QuotaManager* quotamanager = quota::QuotaManager::Get(); + if (NS_WARN_IF(!quotamanager)) { + return BoolPromise::CreateAndReject(NS_ERROR_UNEXPECTED, __func__); + } + nsresult rv = quotamanager->EnsureStorageIsInitialized(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return BoolPromise::CreateAndReject(rv, __func__); + } + rv = quotamanager->EnsureTemporaryStorageIsInitialized(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return BoolPromise::CreateAndReject(rv, __func__); + } + auto result = quotamanager->EnsureTemporaryOriginIsInitialized( + quota::PERSISTENCE_TYPE_DEFAULT, self->mOriginMetadata); + if (result.isOk()) { + return BoolPromise::CreateAndResolve(true, __func__); + } + return BoolPromise::CreateAndReject(result.unwrapErr(), __func__); + }) + ->Then(MutableIOTargetPtr(), __func__, + [self = RefPtr(this)]( + const BoolPromise::ResolveOrRejectValue& value) mutable { + auto autoProxyReleaseManager = MakeScopeExit([&self] { + nsCOMPtr target = + self->MutableBackgroundTargetPtr(); + + NS_ProxyRelease("ReleaseFileSystemDataManager", target, + self.forget()); + }); + + if (value.IsReject()) { + return BoolPromise::CreateAndReject(value.RejectValue(), + __func__); + } + + return BoolPromise::CreateAndResolve(true, __func__); + }) ->Then(GetCurrentSerialEventTarget(), __func__, [self = RefPtr(this)]( const BoolPromise::ResolveOrRejectValue& value) { @@ -306,7 +474,7 @@ RefPtr FileSystemDataManager::BeginClose() { ->Then(MutableBackgroundTargetPtr(), __func__, [self = RefPtr(this)]( const ShutdownPromise::ResolveOrRejectValue&) { - RemoveFileSystemDataManager(self->mOrigin); + RemoveFileSystemDataManager(self->mOriginMetadata.mOrigin); self->mState = State::Closed; diff --git a/dom/fs/parent/datamodel/FileSystemDataManager.h b/dom/fs/parent/datamodel/FileSystemDataManager.h index fdd6265e19c0..bc194c00c5d3 100644 --- a/dom/fs/parent/datamodel/FileSystemDataManager.h +++ b/dom/fs/parent/datamodel/FileSystemDataManager.h @@ -12,6 +12,7 @@ #include "mozilla/dom/FileSystemTypes.h" #include "mozilla/dom/FileSystemHelpers.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" @@ -27,20 +28,31 @@ namespace dom { class FileSystemManagerParent; +namespace fs { +class FileSystemChildMetadata; +} // namespace fs + namespace fs::data { +class FileSystemDatabaseManager; + +Result GetRootHandle(const Origin& origin); + +Result GetEntryHandle( + const FileSystemChildMetadata& aHandle); + class FileSystemDataManager : public SupportsCheckedUnsafePtr> { public: enum struct State : uint8_t { Initial = 0, Opening, Open, Closing, Closed }; - FileSystemDataManager(const Origin& aOrigin, + FileSystemDataManager(const quota::OriginMetadata& aOriginMetadata, MovingNotNull> aIOTaskQueue); using CreatePromise = MozPromise, nsresult, /* IsExclusive */ true>; static RefPtr GetOrCreateFileSystemDataManager( - const Origin& aOrigin); + const quota::OriginMetadata& aOriginMetadata); static void InitiateShutdown(); @@ -56,6 +68,12 @@ class FileSystemDataManager return mIOTaskQueue.get(); } + FileSystemDatabaseManager* MutableDatabaseManagerPtr() const { + MOZ_ASSERT(mDatabaseManager); + + return mDatabaseManager.get(); + } + void Register(); void Unregister(); @@ -86,9 +104,10 @@ class FileSystemDataManager RefPtr BeginClose(); nsTHashSet mActors; - const Origin mOrigin; + const quota::OriginMetadata mOriginMetadata; const NotNull> mBackgroundTarget; const NotNull> mIOTaskQueue; + UniquePtr mDatabaseManager; MozPromiseHolder mOpenPromiseHolder; MozPromiseHolder mClosePromiseHolder; uint32_t mRegCount; diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManager.h b/dom/fs/parent/datamodel/FileSystemDatabaseManager.h new file mode 100644 index 000000000000..440f3e5c0a7a --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManager.h @@ -0,0 +1,115 @@ +/* -*- 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 "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/quota/ForwardDecls.h" +#include "nsStringFwd.h" +#include "ResultConnection.h" + +template +class nsCOMPtr; + +class nsIFile; + +namespace mozilla { + +template +class Result; + +namespace dom::fs { + +class FileSystemChildMetadata; +class FileSystemDirectoryListing; +class FileSystemEntryPair; + +namespace data { + +using FileSystemConnection = fs::ResultConnection; + +class FileSystemDatabaseManager { + public: + /** + * @brief Returns current total usage + * + * @return Result Usage or error + */ + virtual Result GetUsage() const = 0; + + /** + * @brief Returns directory identifier, optionally creating it if it doesn't + * exist + * + * @param aHandle Current directory and filename + * @return Result Directory identifier or error + */ + virtual Result 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 File identifier or error + */ + virtual Result GetOrCreateFile( + const FileSystemChildMetadata& aHandle, bool aCreate) = 0; + + /** + * @brief Returns the properties of a file corresponding to a file handle + */ + virtual nsresult GetFile(const FileSystemEntryPair& aEndpoints, + nsString& aType, TimeStamp& lastModifiedMilliSeconds, + Path& aPath, nsCOMPtr& aFile) const = 0; + + virtual Result GetDirectoryEntries( + const EntryId& aParent, PageNumber aPage) const = 0; + + /** + * @brief Removes a directory + * + * @param aHandle Current directory and filename + * @return Result False if file did not exist, otherwise true + * or error + */ + virtual Result RemoveDirectory( + const FileSystemChildMetadata& aHandle, bool aRecursive) = 0; + + /** + * @brief Removes a file + * + * @param aHandle Current directory and filename + * @return Result False if file did not exist, otherwise true + * or error + */ + virtual Result RemoveFile( + const FileSystemChildMetadata& aHandle) = 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 or error if no it didn't exists + */ + virtual Result 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 000000000000..5407a7a6b6ca --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp @@ -0,0 +1,581 @@ +/* -*- 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 "mozilla/dom/FileSystemDataManager.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/dom/PFileSystemManager.h" +#include "mozStorageHelper.h" +#include "ResultStatement.h" + +namespace mozilla::dom { + +using FileSystemEntries = nsTArray; + +namespace fs::data { + +namespace { + +Result ApplyEntryExistsQuery( + const FileSystemConnection& aConnection, const nsACString& aQuery, + const EntryId& aEntryId) { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, aQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + + return stmt.YesOrNoQuery(); +} + +Result 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; +} + +// TODO: Running two aggregations simultaneously leads to +// undefined outcomes and should be prevented. +// However, appending new values while +// one aggreration is ongoing should work if the aggregation is only +// done up to a row which is fixed in the beginning. +nsresult AggregateUsages(FileSystemConnection& mConnection) { + bool rollbackOnScopeExit{false}; // We roll back unless explicitly committed + mozStorageTransaction transaction( + mConnection.get(), rollbackOnScopeExit, + mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(MOZ_TO_RESULT(mConnection->ExecuteSimpleSQL( + "INSERT INTO Usages " + "( usage, aggregated ) " + "SELECT SUM(usage) OVER (ROWS UNBOUNDED PRECEDING), TRUE " + "FROM Usages " + "WHERE aggregated = FALSE " + ";"_ns))); + + QM_TRY( + MOZ_TO_RESULT(mConnection->ExecuteSimpleSQL("DELETE FROM Usages " + "WHERE aggregated = FALSE " + ";"_ns))); + + QM_TRY(MOZ_TO_RESULT( + mConnection->ExecuteSimpleSQL("UPDATE Usages " + "SET aggregated = NOT aggregated " + ";"_ns))); + + return transaction.Commit(); +} + +Result DoesDirectoryExist( + const FileSystemConnection& mConnection, const EntryId& aEntryId) { + MOZ_ASSERT(!aEntryId.IsEmpty()); + + const nsCString existsQuery = + "SELECT EXISTS " + "(SELECT 1 FROM Directories " + "WHERE handle = :handle )" + ";"_ns; + + QM_TRY_UNWRAP(bool exists, + ApplyEntryExistsQuery(mConnection, existsQuery, aEntryId)); + + return exists; +} + +Result 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()); + } + + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); +} + +Result DoesFileExist(const FileSystemConnection& mConnection, + const EntryId& aEntryId) { + MOZ_ASSERT(!aEntryId.IsEmpty()); + + const nsCString existsQuery = + "SELECT EXISTS " + "(SELECT 1 FROM Files " + "WHERE handle = :handle ) " + ";"_ns; + + QM_TRY_UNWRAP(bool exists, + ApplyEntryExistsQuery(mConnection, existsQuery, aEntryId)); + + return exists; +} + +nsresult GetFileAttributes(const FileSystemConnection& aConnection, + const EntryId& aEntryId, + TimeStamp& aLastModifiedMilliSeconds, + ContentType& aType) { + const nsLiteralCString getFileLocation = + "SELECT type FROM Files INNER JOIN Entries USING(handle) WHERE handle = :entryId;"_ns; + + // TODO: Request this from filemanager who makes the system call + aLastModifiedMilliSeconds = 0; + + 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, 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(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); + aEntries.AppendElement(metadata); + + QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep()); + } + + return NS_OK; +} + +} // namespace + +Result FileSystemDatabaseManagerVersion001::GetUsage() const { + const nsLiteralCString sumUsagesQuery = + "SELECT sum(deltas) FROM Usages WHERE aggregated = FALSE;"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, sumUsagesQuery)); + QM_TRY_UNWRAP(Usage total, stmt.GetUsageByColumn(/* Column */ 0u)); + + return total; +} + +nsresult FileSystemDatabaseManagerVersion001::UpdateUsage(int64_t aDelta) { + const nsLiteralCString addUsageQuery = + "INSERT INTO Usages " + "( usage ) " + "VALUES " + "( :usage ) " + ";"_ns; + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, addUsageQuery)); + QM_TRY(MOZ_TO_RESULT(stmt.BindUsageByName("usage"_ns, aDelta))); + QM_TRY(MOZ_TO_RESULT(stmt.Execute())); + } + + // Try again later, no harm done; + QM_WARNONLY_TRY(MOZ_TO_RESULT(AggregateUsages(mConnection))); + + return NS_OK; +} + +Result +FileSystemDatabaseManagerVersion001::GetOrCreateDirectory( + const FileSystemChildMetadata& aHandle, bool aCreate) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + const auto& name = aHandle.childName(); + MOZ_ASSERT(!name.IsVoid() && !name.IsEmpty()); + + QM_TRY_UNWRAP(EntryId entryId, fs::data::GetEntryHandle(aHandle)); + MOZ_ASSERT(!entryId.IsEmpty()); + + bool exists = true; + QM_TRY_UNWRAP(exists, DoesFileExist(mConnection, entryId)); + + // 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, entryId)); + + // exists as directory + if (exists) { + return entryId; + } + + 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; + + 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(UpdateUsage(name.Length()))); + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + QM_TRY_UNWRAP(DebugOnly doesItExistNow, + DoesDirectoryExist(mConnection, entryId)); + MOZ_ASSERT(doesItExistNow); + + return entryId; +} + +Result FileSystemDatabaseManagerVersion001::GetOrCreateFile( + const FileSystemChildMetadata& aHandle, bool aCreate) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + const auto& name = aHandle.childName(); + MOZ_ASSERT(!name.IsVoid() && !name.IsEmpty()); + + QM_TRY_UNWRAP(EntryId entryId, fs::data::GetEntryHandle(aHandle)); + MOZ_ASSERT(!entryId.IsEmpty()); + + QM_TRY_UNWRAP(bool exists, DoesDirectoryExist(mConnection, entryId)); + + // 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, entryId)); + + if (exists) { + return entryId; + } + + 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; + + // TODO: This needs a scope quard + 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(UpdateUsage(name.Length()))); + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + return entryId; +} + +Result +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, + entries.directories()))); + + QM_TRY(QM_TO_RESULT( + GetEntries(mConnection, filesQuery, aParent, aPage, entries.files()))); + + return entries; +} + +nsresult FileSystemDatabaseManagerVersion001::GetFile( + const FileSystemEntryPair& aEndpoints, nsString& aType, + TimeStamp& lastModifiedMilliSeconds, nsTArray& aPath, + nsCOMPtr& aFile) const { + MOZ_ASSERT(!aEndpoints.parentId().IsEmpty()); + MOZ_ASSERT(!aEndpoints.childId().IsEmpty()); + + const auto& entryId = aEndpoints.childId(); + + QM_TRY(MOZ_TO_RESULT(GetFileAttributes(mConnection, entryId, + lastModifiedMilliSeconds, aType))); + + QM_TRY_UNWRAP(aPath, ResolveReversedPath(mConnection, aEndpoints)); + + aPath.Reverse(); + + return NS_OK; +} + +Result FileSystemDatabaseManagerVersion001::RemoveDirectory( + const FileSystemChildMetadata& aHandle, bool aRecursive) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + DebugOnly name = aHandle.childName(); + MOZ_ASSERT(!name.inspect().IsVoid() && !name.inspect().IsEmpty()); + + QM_TRY_UNWRAP(EntryId entryId, fs::data::GetEntryHandle(aHandle)); + MOZ_ASSERT(!entryId.IsEmpty()); + + QM_TRY_UNWRAP(bool exists, DoesDirectoryExist(mConnection, entryId)); + + if (!exists) { + return false; + } + // At this point, entry exists and is a directory. + + QM_TRY_UNWRAP(bool isEmpty, IsDirectoryEmpty(mConnection, entryId)); + + if (!aRecursive && !isEmpty) { + return Err(QMResult( + NS_ERROR_DOM_FILEHANDLE_NOT_ALLOWED_ERR)); // Is this correct error? + } + // 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 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( + UpdateUsage(static_cast(aHandle.childName().Length())))); + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + /* TODO: Delete descendants here */ + + return true; +} + +Result FileSystemDatabaseManagerVersion001::RemoveFile( + const FileSystemChildMetadata& aHandle) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + DebugOnly name = aHandle.childName(); + MOZ_ASSERT(!name.inspect().IsVoid() && !name.inspect().IsEmpty()); + + QM_TRY_UNWRAP(EntryId entryId, fs::data::GetEntryHandle(aHandle)); + MOZ_ASSERT(!entryId.IsEmpty()); + + // Make it more evident that we won't remove directories + QM_TRY_UNWRAP(bool exists, DoesFileExist(mConnection, entryId)); + + if (!exists) { + return false; + } + // At this point, entry exists and is a file + + 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( + UpdateUsage(static_cast(aHandle.childName().Length())))); + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + /* TODO: Delete file from the disk */ + + return true; +} + +Result FileSystemDatabaseManagerVersion001::Resolve( + const FileSystemEntryPair& aEndpoints) const { + QM_TRY_UNWRAP(Path path, ResolveReversedPath(mConnection, aEndpoints)); + + 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 000000000000..5254f11b92e7 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h @@ -0,0 +1,75 @@ +/* -*- 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" + +namespace mozilla::dom::fs::data { + +/** + * @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: + explicit FileSystemDatabaseManagerVersion001( + fs::data::FileSystemConnection&& aConnection) + : mConnection(aConnection) {} + + virtual Result GetUsage() const override; + + virtual Result GetOrCreateDirectory( + const FileSystemChildMetadata& aHandle, bool aCreate) override; + + virtual Result GetOrCreateFile( + const FileSystemChildMetadata& aHandle, bool aCreate) override; + + virtual nsresult GetFile(const FileSystemEntryPair& aEndpoints, + nsString& aType, TimeStamp& lastModifiedMilliSeconds, + Path& aPath, + nsCOMPtr& aFile) const override; + + virtual Result GetDirectoryEntries( + const EntryId& aParent, PageNumber aPage) const override; + + virtual Result RemoveDirectory( + const FileSystemChildMetadata& aHandle, bool aRecursive) override; + + virtual Result RemoveFile( + const FileSystemChildMetadata& aHandle) override; + + virtual Result Resolve( + const FileSystemEntryPair& aEndpoints) const override; + + virtual void Close() override; + + virtual ~FileSystemDatabaseManagerVersion001() = default; + + private: + nsresult UpdateUsage(int64_t aDelta); + + FileSystemConnection mConnection; +}; + +} // namespace mozilla::dom::fs::data + +#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION001_H_ diff --git a/dom/fs/parent/datamodel/SchemaVersion001.cpp b/dom/fs/parent/datamodel/SchemaVersion001.cpp new file mode 100644 index 000000000000..ca091e1f5aa6 --- /dev/null +++ b/dom/fs/parent/datamodel/SchemaVersion001.cpp @@ -0,0 +1,193 @@ +/* -*- 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 "fs/FileSystemConstants.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozStorageHelper.h" +#include "ResultStatement.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); +} + +class KeepForeignKeysOffUntilScopeExit final { + public: + explicit KeepForeignKeysOffUntilScopeExit(const ResultConnection& aConn) + : mConn(aConn) {} + + static Result 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 { + 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(); +} + +nsresult CreateUsages(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "CREATE TABLE IF NOT EXISTS Usages ( " + "usage INTEGER NOT NULL, " + "aggregated BOOLEAN DEFAULT FALSE " + ") " + ";"_ns); +} + +Result 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(); +}; + +Result GetDatabaseVersion(ResultConnection& aConn) { + const nsLiteralCString getUserVersionQuery = "PRAGMA USER_VERSION;"_ns; + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, getUserVersionQuery)); + + return stmt.GetDatabaseVersion(); +} + +nsresult SetDatabaseVersion(ResultConnection& aConn) { + // Unfortunately bind does not work. + nsCString setUserVersionQuery = "PRAGMA USER_VERSION = "_ns; + setUserVersionQuery.AppendInt(SchemaVersion001::sVersion); + setUserVersionQuery.Append(" ;"_ns); + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, setUserVersionQuery)); + QM_TRY(MOZ_TO_RESULT(stmt.Execute())); + + return NS_OK; +} + +} // namespace + +Result SchemaVersion001::InitializeConnection( + ResultConnection& aConn, const Origin& aOrigin) { + QM_TRY_UNWRAP(bool isEmpty, CheckIfEmpty(aConn)); + + DatabaseVersion previous = 0; + + if (isEmpty) { + QM_TRY(QM_TO_RESULT(SetEncoding(aConn))); + } else { + QM_TRY_UNWRAP(previous, GetDatabaseVersion(aConn)); + } + + if (previous < sVersion) { + 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(SetDatabaseVersion(aConn))); + } + + return GetDatabaseVersion(aConn); +} + +} // 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 000000000000..1bfa349a6fa5 --- /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 "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/quota/ForwardDecls.h" +#include "ResultConnection.h" + +namespace mozilla::dom::fs { + +struct SchemaVersion001 { + static Result 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 index 964bfeec7b84..90cbfa0eeb86 100644 --- a/dom/fs/parent/datamodel/moz.build +++ b/dom/fs/parent/datamodel/moz.build @@ -9,7 +9,9 @@ EXPORTS.mozilla.dom += [ ] UNIFIED_SOURCES += [ + "FileSystemDatabaseManagerVersion001.cpp", "FileSystemDataManager.cpp", + "SchemaVersion001.cpp", ] LOCAL_INCLUDES += [ diff --git a/dom/fs/parent/moz.build b/dom/fs/parent/moz.build index e19e2318a441..20417ccb0a3c 100644 --- a/dom/fs/parent/moz.build +++ b/dom/fs/parent/moz.build @@ -19,6 +19,13 @@ UNIFIED_SOURCES += [ "FileSystemManagerParent.cpp", "FileSystemManagerParentFactory.cpp", "FileSystemQuotaClient.cpp", + "GetDirectoryForOrigin.cpp", + "ResultStatement.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/fs/include", + "/dom/fs/parent/datamodel", ] FINAL_LIBRARY = "xul" diff --git a/dom/fs/shared/FileSystemTypes.h b/dom/fs/shared/FileSystemTypes.h index 8f97479b5218..afa51651c958 100644 --- a/dom/fs/shared/FileSystemTypes.h +++ b/dom/fs/shared/FileSystemTypes.h @@ -15,10 +15,11 @@ class nsTArray; namespace mozilla::dom::fs { using ContentType = nsString; +using DatabaseVersion = int32_t; using EntryId = nsCString; using Name = nsString; using Origin = nsCString; -using PageNumber = uint32_t; +using PageNumber = int32_t; using Path = nsTArray; using TimeStamp = int64_t; using Usage = int64_t; diff --git a/dom/fs/test/common/test_fileSystemDirectoryHandle.js b/dom/fs/test/common/test_fileSystemDirectoryHandle.js new file mode 100644 index 000000000000..7f2781b56327 --- /dev/null +++ b/dom/fs/test/common/test_fileSystemDirectoryHandle.js @@ -0,0 +1,122 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +exported_symbols.smokeTest = async function smokeTest() { + const step = async aIter => { + return aIter.next().catch(err => { + /* console.log(err.message); */ + }); + }; + + const storage = navigator.storage; + const subdirectoryNames = new Set(["Documents", "Downloads", "Music"]); + const allowCreate = { create: true }; + + { + let root = await storage.getDirectory(); + Assert.ok(root, "Can we access the root directory?"); + + let it = root.values(); + Assert.ok(!!it, "Does root have values iterator?"); + + let elem = await step(it); + Assert.ok(!elem, "Is root directory empty?"); + + for (let dirName of subdirectoryNames) { + try { + await root.getDirectoryHandle(dirName, allowCreate); + } catch (err) { + Assert.equal( + Cr.NS_ERROR_NOT_IMPLEMENTED, + err.result, + "Iterator not implemented yet" + ); + } + // TODO: Implement iterator + // Assert.ok(true, "Was it possible to add subdirectory " + dirName + "?"); + } + } + + { + let root = await storage.getDirectory(); + Assert.ok(root, "Can we refresh the root directory?"); + + let it = root.values(); + Assert.ok(!!it, "Does root have values iterator?"); + + let hasElements = false; + let hangGuard = 0; + for (let elem = await step(it); elem; elem = await step(it)) { + Assert.ok(!!elem, "Is element not non-empty?"); + Assert.equal("directory", elem.kind, "Is found item a directory?"); + Assert.ok( + elem.name.length >= 1 && elem.name.match("^[A-Za-z]{1,64}"), + "Are names of the elements strings?" + ); + Assert.ok(subdirectoryNames.has(elem.name), "Is name among known names?"); + hasElements = true; + ++hangGuard; + if (hangGuard == 10) { + break; // Exit if there is a hang + } + } + + Assert.ok(!hasElements, "Iterator not implemented yet"); + // TODO: Implement iterator + // Assert.ok(hasElements, "Is values container now non-empty?"); + // Assert.equal(3, hangGuard, "Do we only have three elements?"); + + { + it = root.values(); + Assert.ok(!!it, "Does root have values iterator?"); + let elem = await step(it); + Assert.ok(!elem, "Iterator not yet implemented"); + // TODO: Implement iterator + // Assert.ok(!!elem, "Is element not non-empty?"); + // await elem.getDirectoryHandle("Trash", allowCreate); + // let subit = elem.values(); + // let subdir = await step(subit); + // Assert.ok(!!subdir, "Is element not non-empty?"); + // Assert.equal("directory", subdir.kind, "Is found item a directory?"); + // Assert.equal("Trash", subdir.name, "Is found item a directory?"); + } + + const wipeEverything = { recursive: true }; + for (let dirName of subdirectoryNames) { + try { + await root.removeEntry(dirName, wipeEverything); + } catch (err) { + Assert.equal( + Cr.NS_ERROR_NOT_IMPLEMENTED, + err.result, + "Iterator not implemented yet" + ); + } + // TODO: Implement iterator + // Assert.ok( + // true, + // "Was it possible to remove subdirectory " + dirName + "?" + // ); + } + } + + { + let root = await storage.getDirectory(); + Assert.ok(root, "Can we refresh the root directory?"); + + let it = root.values(); + Assert.ok(!!it, "Does root have values iterator?"); + + let elem = await step(it); + Assert.ok(!elem, "Is root directory empty?"); + } +}; + +for (const [key, value] of Object.entries(exported_symbols)) { + Object.defineProperty(value, "name", { + value: key, + writable: false, + }); +} diff --git a/dom/fs/test/gtest/FileSystemMocks.h b/dom/fs/test/gtest/FileSystemMocks.h index 1aedb5e0a99a..afd703c4daa4 100644 --- a/dom/fs/test/gtest/FileSystemMocks.h +++ b/dom/fs/test/gtest/FileSystemMocks.h @@ -78,6 +78,11 @@ class MockFileSystemRequestHandler : public FileSystemRequestHandler { const FileSystemChildMetadata& aEntry, bool aRecursive, RefPtr aPromise), (override)); + + MOCK_METHOD(void, Resolve, + (RefPtr & aManager, + const FileSystemEntryPair& aEndpoints, RefPtr aPromise), + (override)); }; class WaitablePromiseListener { diff --git a/dom/fs/test/gtest/TestHelpers.h b/dom/fs/test/gtest/TestHelpers.h index 9526edc25c35..6520efdb683f 100644 --- a/dom/fs/test/gtest/TestHelpers.h +++ b/dom/fs/test/gtest/TestHelpers.h @@ -13,18 +13,27 @@ #include "mozilla/dom/quota/QuotaCommon.h" #include "mozilla/ErrorNames.h" +namespace mozilla::dom::fs::test::gtest::detail { +constexpr nsresult ToNSError(nsresult aError) { return aError; } + +inline nsresult ToNSError(const QMResult& aError) { return aError.NSResult(); } +} // namespace mozilla::dom::fs::test::gtest::detail + #define ASSERT_NSEQ(lhs, rhs) \ ASSERT_STREQ(GetStaticErrorName((lhs)), GetStaticErrorName((rhs))) -#define TEST_TRY_UNWRAP_META(tempVar, target, expr) \ - auto MOZ_REMOVE_PAREN(tempVar) = (expr); \ - ASSERT_TRUE(MOZ_REMOVE_PAREN(tempVar).isOk()); \ +#define TEST_TRY_UNWRAP_META(tempVar, target, expr) \ + auto MOZ_REMOVE_PAREN(tempVar) = (expr); \ + ASSERT_TRUE(MOZ_REMOVE_PAREN(tempVar).isOk()) \ + << GetStaticErrorName(mozilla::dom::fs::test::gtest::detail::ToNSError( \ + MOZ_REMOVE_PAREN(tempVar).unwrapErr())); \ MOZ_REMOVE_PAREN(target) = MOZ_REMOVE_PAREN(tempVar).unwrap(); -#define TEST_TRY_UNWRAP_ERR_META(tempVar, target, expr) \ - auto MOZ_REMOVE_PAREN(tempVar) = (expr); \ - ASSERT_TRUE(MOZ_REMOVE_PAREN(tempVar).isErr()); \ - MOZ_REMOVE_PAREN(target) = MOZ_REMOVE_PAREN(tempVar).unwrapErr().NSResult(); +#define TEST_TRY_UNWRAP_ERR_META(tempVar, target, expr) \ + auto MOZ_REMOVE_PAREN(tempVar) = (expr); \ + ASSERT_TRUE(MOZ_REMOVE_PAREN(tempVar).isErr()); \ + MOZ_REMOVE_PAREN(target) = mozilla::dom::fs::test::gtest::detail::ToNSError( \ + MOZ_REMOVE_PAREN(tempVar).unwrapErr()); #define TEST_TRY_UNWRAP(target, expr) \ TEST_TRY_UNWRAP_META(MOZ_UNIQUE_VAR(testVar), target, expr) diff --git a/dom/fs/test/gtest/api/TestFileSystemDirectoryHandle.cpp b/dom/fs/test/gtest/api/TestFileSystemDirectoryHandle.cpp index e4a79eb6ef62..49245502603c 100644 --- a/dom/fs/test/gtest/api/TestFileSystemDirectoryHandle.cpp +++ b/dom/fs/test/gtest/api/TestFileSystemDirectoryHandle.cpp @@ -19,6 +19,8 @@ #include "mozilla/UniquePtr.h" #include "nsIGlobalObject.h" +using ::testing::_; + namespace mozilla::dom::fs::test { class TestFileSystemDirectoryHandle : public ::testing::Test { diff --git a/dom/fs/test/gtest/parent/TestFileSystemHashSource.cpp b/dom/fs/test/gtest/parent/TestFileSystemHashSource.cpp index 1bdc3aa4ca47..7aa55fe5df5d 100644 --- a/dom/fs/test/gtest/parent/TestFileSystemHashSource.cpp +++ b/dom/fs/test/gtest/parent/TestFileSystemHashSource.cpp @@ -169,7 +169,7 @@ TEST(TestFileSystemHashSource, encodeGeneratedHash) // Always the same length ASSERT_EQ(kExpectedLength, result.Length()); - // Reused buffer should be different + // Encoded versions should differ ASSERT_STRNE(asWide(expected).c_str(), asWide(result).c_str()); // Padding length should have been stripped diff --git a/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp index a9102e84ac89..e7c83bc4ff8c 100644 --- a/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp +++ b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManager.cpp @@ -10,6 +10,8 @@ #include "mozilla/dom/ScriptSettings.h" #include "mozilla/dom/quota/QuotaManagerService.h" #include "mozilla/dom/quota/QuotaManager.h" +#include "mozIStorageService.h" +#include "mozStorageCID.h" #include "nsIPrefBranch.h" #include "nsIPrefService.h" #include "nsIQuotaCallbacks.h" @@ -19,7 +21,10 @@ namespace mozilla::dom::fs::test { namespace { -constexpr auto kTestOriginName = "http:://example.com"_ns; +quota::OriginMetadata GetTestOriginMetadata() { + return quota::OriginMetadata{""_ns, "example.com"_ns, "http://example.com"_ns, + quota::PERSISTENCE_TYPE_DEFAULT}; +} } // namespace @@ -46,6 +51,12 @@ class TestFileSystemDataManager : public ::testing::Test { }; static void SetUpTestCase() { + // The first initialization of storage service must be done on the main + // thread. + nsCOMPtr storageService = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID); + ASSERT_TRUE(storageService); + nsIObserver* observer = quota::QuotaManager::GetObserver(); ASSERT_TRUE(observer); @@ -131,7 +142,7 @@ TEST_F(TestFileSystemDataManager, GetOrCreateFileSystemDataManager) { bool done = false; data::FileSystemDataManager::GetOrCreateFileSystemDataManager( - Origin(kTestOriginName)) + GetTestOriginMetadata()) ->Then( GetCurrentSerialEventTarget(), __func__, [](Registered registeredDataManager) { @@ -176,7 +187,7 @@ TEST_F(TestFileSystemDataManager, bool done1 = false; data::FileSystemDataManager::GetOrCreateFileSystemDataManager( - Origin(kTestOriginName)) + GetTestOriginMetadata()) ->Then( GetCurrentSerialEventTarget(), __func__, [&rdm1, &done1](Registered @@ -192,7 +203,7 @@ TEST_F(TestFileSystemDataManager, bool done2 = false; data::FileSystemDataManager::GetOrCreateFileSystemDataManager( - Origin(kTestOriginName)) + GetTestOriginMetadata()) ->Then( GetCurrentSerialEventTarget(), __func__, [&rdm2, &done2](Registered @@ -253,7 +264,7 @@ TEST_F(TestFileSystemDataManager, bool done = false; data::FileSystemDataManager::GetOrCreateFileSystemDataManager( - Origin(kTestOriginName)) + GetTestOriginMetadata()) ->Then( GetCurrentSerialEventTarget(), __func__, [&rdm, &done](Registered @@ -277,7 +288,7 @@ TEST_F(TestFileSystemDataManager, bool done = false; data::FileSystemDataManager::GetOrCreateFileSystemDataManager( - Origin(kTestOriginName)) + GetTestOriginMetadata()) ->Then( GetCurrentSerialEventTarget(), __func__, [](Registered diff --git a/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersion001.cpp b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersion001.cpp new file mode 100644 index 000000000000..784cfefccc82 --- /dev/null +++ b/dom/fs/test/gtest/parent/datamodel/TestFileSystemDataManagerVersion001.cpp @@ -0,0 +1,306 @@ +/* -*- 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 "ErrorList.h" +#include "FileSystemDataManager.h" +#include "FileSystemDatabaseManagerVersion001.h" +#include "FileSystemHashSource.h" +#include "gtest/gtest.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/PFileSystemManager.h" +#include "mozilla/Array.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/Result.h" +#include "mozIStorageService.h" +#include "mozStorageCID.h" +#include "mozStorageHelper.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsContentUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIFile.h" +#include "nsLiteralString.h" +#include "nsReadableUtils.h" +#include "nsStringFwd.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsTHashSet.h" +#include "ResultStatement.h" +#include "SchemaVersion001.h" +#include "TestHelpers.h" + +namespace mozilla::dom::fs::test { + +using data::FileSystemDatabaseManagerVersion001; + +static const Origin& getTestOrigin() { + static const Origin orig = "testOrigin"_ns; + return orig; +} + +static void MakeDatabaseManagerVersion001( + FileSystemDatabaseManagerVersion001*& aResult) { + TEST_TRY_UNWRAP(auto storageService, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr, + MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID)); + + const auto flags = mozIStorageService::CONNECTION_DEFAULT; + ResultConnection connection; + + nsresult rv = storageService->OpenSpecialDatabase(kMozStorageMemoryStorageKey, + VoidCString(), flags, + getter_AddRefs(connection)); + ASSERT_NSEQ(NS_OK, rv); + + const Origin& testOrigin = getTestOrigin(); + + TEST_TRY_UNWRAP( + DatabaseVersion version, + SchemaVersion001::InitializeConnection(connection, testOrigin)); + ASSERT_EQ(1, version); + + aResult = new FileSystemDatabaseManagerVersion001(std::move(connection)); +} + +TEST(TestFileSystemDatabaseManagerVersion001, smokeTestCreateRemoveDirectories) +{ + nsresult rv = NS_OK; + FileSystemDatabaseManagerVersion001* rdm = nullptr; + ASSERT_NO_FATAL_FAILURE(MakeDatabaseManagerVersion001(rdm)); + UniquePtr dm(rdm); + + TEST_TRY_UNWRAP(EntryId rootId, data::GetRootHandle(getTestOrigin())); + + FileSystemChildMetadata firstChildMeta(rootId, u"First"_ns); + TEST_TRY_UNWRAP_ERR( + rv, dm->GetOrCreateDirectory(firstChildMeta, /* create */ false)); + ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv); + + TEST_TRY_UNWRAP(EntryId firstChild, + dm->GetOrCreateDirectory(firstChildMeta, /* create */ true)); + + int32_t dbVersion = 0; + TEST_TRY_UNWRAP(FileSystemDirectoryListing entries, + dm->GetDirectoryEntries(rootId, dbVersion)); + ASSERT_EQ(1u, entries.directories().Length()); + ASSERT_EQ(0u, entries.files().Length()); + + const auto& firstItemRef = entries.directories()[0]; + ASSERT_TRUE(u"First"_ns == firstItemRef.entryName()) + << firstItemRef.entryName(); + ASSERT_EQ(firstChild, firstItemRef.entryId()); + + TEST_TRY_UNWRAP(EntryId firstChildClone, + dm->GetOrCreateDirectory(firstChildMeta, /* create */ true)); + ASSERT_EQ(firstChild, firstChildClone); + + FileSystemChildMetadata secondChildMeta(firstChild, u"Second"_ns); + TEST_TRY_UNWRAP(EntryId secondChild, + dm->GetOrCreateDirectory(secondChildMeta, /* create */ true)); + + FileSystemEntryPair shortPair(firstChild, secondChild); + TEST_TRY_UNWRAP(Path shortPath, dm->Resolve(shortPair)); + ASSERT_EQ(1u, shortPath.Length()); + ASSERT_EQ(u"Second"_ns, shortPath[0]); + + FileSystemEntryPair longPair(rootId, secondChild); + TEST_TRY_UNWRAP(Path longPath, dm->Resolve(longPair)); + ASSERT_EQ(2u, longPath.Length()); + ASSERT_EQ(u"First"_ns, longPath[0]); + ASSERT_EQ(u"Second"_ns, longPath[1]); + + FileSystemEntryPair wrongPair(secondChild, rootId); + TEST_TRY_UNWRAP_ERR(rv, dm->Resolve(wrongPair)); + ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv); + + PageNumber page = 0; + TEST_TRY_UNWRAP(FileSystemDirectoryListing fEntries, + dm->GetDirectoryEntries(firstChild, page)); + ASSERT_EQ(1u, fEntries.directories().Length()); + ASSERT_EQ(0u, fEntries.files().Length()); + + const auto& secItemRef = fEntries.directories()[0]; + ASSERT_TRUE(u"Second"_ns == secItemRef.entryName()) + << secItemRef.entryName(); + ASSERT_EQ(secondChild, secItemRef.entryId()); + + TEST_TRY_UNWRAP_ERR( + rv, dm->RemoveDirectory(firstChildMeta, /* recursive */ false)); + ASSERT_NSEQ(NS_ERROR_DOM_FILEHANDLE_NOT_ALLOWED_ERR, + rv); // Is this a good error? + + TEST_TRY_UNWRAP(bool isDeleted, + dm->RemoveDirectory(firstChildMeta, /* recursive */ true)); + ASSERT_TRUE(isDeleted); + + FileSystemChildMetadata thirdChildMeta(secondChild, u"Second"_ns); + TEST_TRY_UNWRAP_ERR( + rv, dm->GetOrCreateDirectory(thirdChildMeta, /* create */ true)); + ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Is this a good error? + + dm->Close(); +} + +TEST(TestFileSystemDatabaseManagerVersion001, smokeTestCreateRemoveFiles) +{ + nsresult rv = NS_OK; + // Create data manager + FileSystemDatabaseManagerVersion001* rdm = nullptr; + ASSERT_NO_FATAL_FAILURE(MakeDatabaseManagerVersion001(rdm)); + UniquePtr dm(rdm); + + TEST_TRY_UNWRAP(EntryId rootId, data::GetRootHandle(getTestOrigin())); + + FileSystemChildMetadata firstChildMeta(rootId, u"First"_ns); + // If creating is not allowed, getting a file from empty root fails + TEST_TRY_UNWRAP_ERR(rv, + dm->GetOrCreateFile(firstChildMeta, /* create */ false)); + ASSERT_NSEQ(NS_ERROR_DOM_NOT_FOUND_ERR, rv); + + // Creating a file under empty root succeeds + TEST_TRY_UNWRAP(EntryId firstChild, + dm->GetOrCreateFile(firstChildMeta, /* create */ true)); + + // Second time, the same file is returned + TEST_TRY_UNWRAP(EntryId firstChildClone, + dm->GetOrCreateFile(firstChildMeta, /* create */ true)); + ASSERT_STREQ(firstChild.get(), firstChildClone.get()); + + // Directory listing returns the created file + PageNumber page = 0; + TEST_TRY_UNWRAP(FileSystemDirectoryListing entries, + dm->GetDirectoryEntries(rootId, page)); + ASSERT_EQ(0u, entries.directories().Length()); + ASSERT_EQ(1u, entries.files().Length()); + + const auto& firstItemRef = entries.files()[0]; + ASSERT_TRUE(u"First"_ns == firstItemRef.entryName()) + << firstItemRef.entryName(); + ASSERT_STREQ(firstChild.get(), firstItemRef.entryId().get()); + + // Getting the file entry as directory fails + TEST_TRY_UNWRAP_ERR( + rv, dm->GetOrCreateDirectory(firstChildMeta, /* create */ false)); + ASSERT_NSEQ(NS_ERROR_DOM_TYPE_MISMATCH_ERR, rv); + + // Getting or creating the file entry as directory also fails + TEST_TRY_UNWRAP_ERR( + rv, dm->GetOrCreateDirectory(firstChildMeta, /* create */ true)); + ASSERT_NSEQ(NS_ERROR_DOM_TYPE_MISMATCH_ERR, rv); + + // Creating a file with non existing parent hash fails + + EntryId notAChildHash = "0123456789abcdef0123456789abcdef"_ns; + FileSystemChildMetadata notAChildMeta(notAChildHash, u"Dummy"_ns); + TEST_TRY_UNWRAP_ERR(rv, + dm->GetOrCreateFile(notAChildMeta, /* create */ true)); + ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Is this a good error? + + // We create a directory under root + FileSystemChildMetadata secondChildMeta(rootId, u"Second"_ns); + TEST_TRY_UNWRAP(EntryId secondChild, + dm->GetOrCreateDirectory(secondChildMeta, /* create */ true)); + + // The root should now contain the existing file and the new directory + TEST_TRY_UNWRAP(FileSystemDirectoryListing fEntries, + dm->GetDirectoryEntries(rootId, page)); + ASSERT_EQ(1u, fEntries.directories().Length()); + ASSERT_EQ(1u, fEntries.files().Length()); + + const auto& secItemRef = fEntries.directories()[0]; + ASSERT_TRUE(u"Second"_ns == secItemRef.entryName()) + << secItemRef.entryName(); + ASSERT_EQ(secondChild, secItemRef.entryId()); + + // Create a file under the new directory + FileSystemChildMetadata thirdChildMeta(secondChild, u"Third"_ns); + TEST_TRY_UNWRAP(EntryId thirdChild, + dm->GetOrCreateFile(thirdChildMeta, /* create */ true)); + + FileSystemEntryPair entryPair(rootId, thirdChild); + TEST_TRY_UNWRAP(Path entryPath, dm->Resolve(entryPair)); + ASSERT_EQ(2u, entryPath.Length()); + ASSERT_EQ(u"Second"_ns, entryPath[0]); + ASSERT_EQ(u"Third"_ns, entryPath[1]); + + // If recursion is not allowed, the non-empty new directory may not be removed + TEST_TRY_UNWRAP_ERR( + rv, dm->RemoveDirectory(secondChildMeta, /* recursive */ false)); + ASSERT_NSEQ(NS_ERROR_DOM_FILEHANDLE_NOT_ALLOWED_ERR, + rv); // Is this a good error? + + // If recursion is allowed, the new directory goes away. + TEST_TRY_UNWRAP(bool isDeleted, + dm->RemoveDirectory(secondChildMeta, /* recursive */ true)); + ASSERT_TRUE(isDeleted); + + // The file under the removed directory is no longer accessible. + TEST_TRY_UNWRAP_ERR(rv, + dm->GetOrCreateFile(thirdChildMeta, /* create */ true)); + ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Is this a good error? + + // The deletion is reflected by the root directory listing + TEST_TRY_UNWRAP(FileSystemDirectoryListing nEntries, + dm->GetDirectoryEntries(rootId, 0)); + ASSERT_EQ(0u, nEntries.directories().Length()); + ASSERT_EQ(1u, nEntries.files().Length()); + + const auto& fileItemRef = nEntries.files()[0]; + ASSERT_TRUE(u"First"_ns == fileItemRef.entryName()) + << fileItemRef.entryName(); + ASSERT_EQ(firstChild, fileItemRef.entryId()); + + // Non-empty root directory also may not be removed except recursively + FileSystemChildMetadata rootChildMeta(getTestOrigin(), u"root"_ns); + TEST_TRY_UNWRAP_ERR( + rv, dm->RemoveDirectory(rootChildMeta, /* recursive */ false)); + ASSERT_NSEQ(NS_ERROR_DOM_FILEHANDLE_NOT_ALLOWED_ERR, + rv); // Is this a good error? + + TEST_TRY_UNWRAP(isDeleted, + dm->RemoveDirectory(rootChildMeta, /* recursive */ true)); + ASSERT_TRUE(isDeleted); + + // Directory listing of root directory works after it has been cleared + TEST_TRY_UNWRAP(FileSystemDirectoryListing rEntries, + dm->GetDirectoryEntries(rootId, 0)); + ASSERT_EQ(0u, rEntries.directories().Length()); + ASSERT_EQ(0u, rEntries.files().Length()); + + // Creating a file under the removed root directory fails + TEST_TRY_UNWRAP_ERR(rv, + dm->GetOrCreateFile(firstChildMeta, /* create */ true)); + ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Root doesn't exist + + // Creating a non-child file still fails + TEST_TRY_UNWRAP_ERR(rv, + dm->GetOrCreateFile(notAChildMeta, /* create */ true)); + ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Is this a good error? + + // Creating a directory under the removed root directory fails + TEST_TRY_UNWRAP_ERR( + rv, dm->GetOrCreateDirectory(firstChildMeta, /* create */ true)); + ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Root doesn't exist + + // Creating a non-child directory still fails + TEST_TRY_UNWRAP_ERR( + rv, dm->GetOrCreateDirectory(notAChildMeta, /* create */ true)); + ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Is this a good error? + + // Creating a root file fails + TEST_TRY_UNWRAP_ERR(rv, + dm->GetOrCreateFile(rootChildMeta, /* create */ true)); + ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Is this a good error? + + // Creating a root directory via GetOrCreateDirectory fails + TEST_TRY_UNWRAP_ERR( + rv, dm->GetOrCreateDirectory(rootChildMeta, /* create */ true)); + ASSERT_NSEQ(NS_ERROR_STORAGE_CONSTRAINT, rv); // Is this a good error? + + dm->Close(); +} + +} // namespace mozilla::dom::fs::test diff --git a/dom/fs/test/gtest/parent/datamodel/moz.build b/dom/fs/test/gtest/parent/datamodel/moz.build index f2b8f20fb9ad..76ed6634e85b 100644 --- a/dom/fs/test/gtest/parent/datamodel/moz.build +++ b/dom/fs/test/gtest/parent/datamodel/moz.build @@ -6,6 +6,7 @@ UNIFIED_SOURCES = [ "TestFileSystemDataManager.cpp", + "TestFileSystemDataManagerVersion001.cpp", ] include("/ipc/chromium/chromium-config.mozbuild") @@ -13,6 +14,8 @@ include("/ipc/chromium/chromium-config.mozbuild") FINAL_LIBRARY = "xul-gtest" LOCAL_INCLUDES += [ + "/dom/fs/include", + "/dom/fs/parent", "/dom/fs/parent/datamodel", "/dom/fs/test/gtest", ] diff --git a/dom/fs/test/moz.build b/dom/fs/test/moz.build index b31669735c70..d2861bf39822 100644 --- a/dom/fs/test/moz.build +++ b/dom/fs/test/moz.build @@ -25,6 +25,7 @@ TEST_HARNESS_FILES.testing.mochitest.tests.dom.fs.test.mochitest.worker += [ TEST_HARNESS_FILES.xpcshell.dom.fs.test.common += [ "common/nsresult.js", "common/test_basics.js", + "common/test_fileSystemDirectoryHandle.js", ] TEST_DIRS += [ diff --git a/dom/fs/test/xpcshell/test_fileSystemDirectoryHandle.js b/dom/fs/test/xpcshell/test_fileSystemDirectoryHandle.js new file mode 100644 index 000000000000..6349d9aab852 --- /dev/null +++ b/dom/fs/test/xpcshell/test_fileSystemDirectoryHandle.js @@ -0,0 +1,14 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +add_task(async function init() { + const testSet = "dom/fs/test/common/test_fileSystemDirectoryHandle.js"; + + const testCases = await require_module(testSet); + + Object.values(testCases).forEach(testItem => { + add_task(testItem); + }); +}); diff --git a/dom/fs/test/xpcshell/xpcshell.ini b/dom/fs/test/xpcshell/xpcshell.ini index 8ed624f85f04..44cdf644dc9e 100644 --- a/dom/fs/test/xpcshell/xpcshell.ini +++ b/dom/fs/test/xpcshell/xpcshell.ini @@ -6,3 +6,4 @@ head = head.js [test_basics.js] +[test_fileSystemDirectoryHandle.js]