diff options
Diffstat (limited to 'dom/fs/parent')
52 files changed, 7897 insertions, 0 deletions
diff --git a/dom/fs/parent/FileSystemAccessHandle.cpp b/dom/fs/parent/FileSystemAccessHandle.cpp new file mode 100644 index 0000000000..07430ce476 --- /dev/null +++ b/dom/fs/parent/FileSystemAccessHandle.cpp @@ -0,0 +1,235 @@ +/* -*- 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 "FileSystemAccessHandle.h" + +#include "FileSystemDatabaseManager.h" +#include "FileSystemParentTypes.h" +#include "mozilla/Result.h" +#include "mozilla/dom/FileSystemDataManager.h" +#include "mozilla/dom/FileSystemHelpers.h" +#include "mozilla/dom/FileSystemLog.h" +#include "mozilla/dom/quota/FileStreams.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/RemoteQuotaObjectParent.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/ipc/RandomAccessStreamParams.h" +#include "mozilla/ipc/RandomAccessStreamUtils.h" +#include "nsIFileStreams.h" + +namespace mozilla::dom { + +FileSystemAccessHandle::FileSystemAccessHandle( + RefPtr<fs::data::FileSystemDataManager> aDataManager, + const fs::EntryId& aEntryId, MovingNotNull<RefPtr<TaskQueue>> aIOTaskQueue) + : mEntryId(aEntryId), + mDataManager(std::move(aDataManager)), + mIOTaskQueue(std::move(aIOTaskQueue)), + mActor(nullptr), + mControlActor(nullptr), + mRegCount(0), + mLocked(false), + mRegistered(false), + mClosed(false) {} + +FileSystemAccessHandle::~FileSystemAccessHandle() { + MOZ_DIAGNOSTIC_ASSERT(mClosed); +} + +// static +RefPtr<FileSystemAccessHandle::CreatePromise> FileSystemAccessHandle::Create( + RefPtr<fs::data::FileSystemDataManager> aDataManager, + const fs::EntryId& aEntryId) { + MOZ_ASSERT(aDataManager); + aDataManager->AssertIsOnIOTarget(); + + RefPtr<TaskQueue> ioTaskQueue = TaskQueue::Create( + do_AddRef(aDataManager->MutableIOTargetPtr()), "FileSystemAccessHandle"); + + RefPtr<FileSystemAccessHandle> accessHandle = new FileSystemAccessHandle( + std::move(aDataManager), aEntryId, WrapMovingNotNull(ioTaskQueue)); + + return accessHandle->BeginInit()->Then( + GetCurrentSerialEventTarget(), __func__, + [accessHandle = fs::Registered<FileSystemAccessHandle>(accessHandle)]( + InitPromise::ResolveOrRejectValue&& value) mutable { + if (value.IsReject()) { + return CreatePromise::CreateAndReject(value.RejectValue(), __func__); + } + + mozilla::ipc::RandomAccessStreamParams streamParams = + std::move(value.ResolveValue()); + + return CreatePromise::CreateAndResolve( + std::pair(std::move(accessHandle), std::move(streamParams)), + __func__); + }); +} + +NS_IMPL_ISUPPORTS_INHERITED0(FileSystemAccessHandle, FileSystemStreamCallbacks) + +void FileSystemAccessHandle::Register() { ++mRegCount; } + +void FileSystemAccessHandle::Unregister() { + MOZ_ASSERT(mRegCount > 0); + + --mRegCount; + + if (IsInactive() && IsOpen()) { + BeginClose(); + } +} + +void FileSystemAccessHandle::RegisterActor( + NotNull<FileSystemAccessHandleParent*> aActor) { + MOZ_ASSERT(!mActor); + + mActor = aActor; +} + +void FileSystemAccessHandle::UnregisterActor( + NotNull<FileSystemAccessHandleParent*> aActor) { + MOZ_ASSERT(mActor); + MOZ_ASSERT(mActor == aActor); + + mActor = nullptr; + + if (IsInactive() && IsOpen()) { + BeginClose(); + } +} + +void FileSystemAccessHandle::RegisterControlActor( + NotNull<FileSystemAccessHandleControlParent*> aControlActor) { + MOZ_ASSERT(!mControlActor); + + mControlActor = aControlActor; +} + +void FileSystemAccessHandle::UnregisterControlActor( + NotNull<FileSystemAccessHandleControlParent*> aControlActor) { + MOZ_ASSERT(mControlActor); + MOZ_ASSERT(mControlActor == aControlActor); + + mControlActor = nullptr; + + if (IsInactive() && IsOpen()) { + BeginClose(); + } +} + +bool FileSystemAccessHandle::IsOpen() const { return !mClosed; } + +RefPtr<BoolPromise> FileSystemAccessHandle::BeginClose() { + MOZ_ASSERT(IsOpen()); + + LOG(("Closing AccessHandle")); + + mClosed = true; + + return InvokeAsync(mIOTaskQueue.get(), __func__, + [self = RefPtr(this)]() { + if (self->mRemoteQuotaObjectParent) { + self->mRemoteQuotaObjectParent->Close(); + } + + return BoolPromise::CreateAndResolve(true, __func__); + }) + ->Then(GetCurrentSerialEventTarget(), __func__, + [self = RefPtr(this)](const BoolPromise::ResolveOrRejectValue&) { + return self->mIOTaskQueue->BeginShutdown(); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self = RefPtr(this)](const ShutdownPromise::ResolveOrRejectValue&) { + if (self->mLocked) { + self->mDataManager->UnlockExclusive(self->mEntryId); + } + + return BoolPromise::CreateAndResolve(true, __func__); + }) + ->Then(mDataManager->MutableBackgroundTargetPtr(), __func__, + [self = RefPtr(this)](const BoolPromise::ResolveOrRejectValue&) { + if (self->mRegistered) { + self->mDataManager->UnregisterAccessHandle(WrapNotNull(self)); + } + + self->mDataManager = nullptr; + + return BoolPromise::CreateAndResolve(true, __func__); + }); +} + +bool FileSystemAccessHandle::IsInactive() const { + return !mRegCount && !mActor && !mControlActor; +} + +RefPtr<FileSystemAccessHandle::InitPromise> +FileSystemAccessHandle::BeginInit() { + QM_TRY_UNWRAP(fs::FileId fileId, mDataManager->LockExclusive(mEntryId), + [](const auto& aRv) { + return InitPromise::CreateAndReject(ToNSResult(aRv), + __func__); + }); + + mLocked = true; + + auto CreateAndRejectInitPromise = [](const char* aFunc, nsresult aRv) { + return CreateAndRejectMozPromise<InitPromise>(aFunc, aRv); + }; + + fs::ContentType type; + fs::TimeStamp lastModifiedMilliSeconds; + fs::Path path; + nsCOMPtr<nsIFile> file; + QM_TRY(MOZ_TO_RESULT(mDataManager->MutableDatabaseManagerPtr()->GetFile( + mEntryId, fileId, fs::FileMode::EXCLUSIVE, type, + lastModifiedMilliSeconds, path, file)), + CreateAndRejectInitPromise); + + if (LOG_ENABLED()) { + nsAutoString path; + if (NS_SUCCEEDED(file->GetPath(path))) { + LOG(("Opening SyncAccessHandle %s", NS_ConvertUTF16toUTF8(path).get())); + } + } + + return InvokeAsync( + mDataManager->MutableBackgroundTargetPtr(), __func__, + [self = RefPtr(this)]() { + self->mDataManager->RegisterAccessHandle(WrapNotNull(self)); + + self->mRegistered = true; + + return BoolPromise::CreateAndResolve(true, __func__); + }) + ->Then(mIOTaskQueue.get(), __func__, + [self = RefPtr(this), CreateAndRejectInitPromise, + file = std::move(file)]( + const BoolPromise::ResolveOrRejectValue& value) { + if (value.IsReject()) { + return InitPromise::CreateAndReject(value.RejectValue(), + __func__); + } + + QM_TRY_UNWRAP(nsCOMPtr<nsIRandomAccessStream> stream, + CreateFileRandomAccessStream( + quota::PERSISTENCE_TYPE_DEFAULT, + self->mDataManager->OriginMetadataRef(), + quota::Client::FILESYSTEM, file, -1, -1, + nsIFileRandomAccessStream::DEFER_OPEN), + CreateAndRejectInitPromise); + + mozilla::ipc::RandomAccessStreamParams streamParams = + mozilla::ipc::SerializeRandomAccessStream( + WrapMovingNotNullUnchecked(std::move(stream)), self); + + return InitPromise::CreateAndResolve(std::move(streamParams), + __func__); + }); +} + +} // namespace mozilla::dom diff --git a/dom/fs/parent/FileSystemAccessHandle.h b/dom/fs/parent/FileSystemAccessHandle.h new file mode 100644 index 0000000000..74b177d04d --- /dev/null +++ b/dom/fs/parent/FileSystemAccessHandle.h @@ -0,0 +1,107 @@ +/* -*- 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_FILESYSTEMACCESSHANDLE_H_ +#define DOM_FS_PARENT_FILESYSTEMACCESSHANDLE_H_ + +#include "FileSystemStreamCallbacks.h" +#include "mozilla/MozPromise.h" +#include "mozilla/NotNull.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/quota/ForwardDecls.h" +#include "nsISupportsImpl.h" +#include "nsString.h" + +enum class nsresult : uint32_t; + +namespace mozilla { + +class TaskQueue; + +namespace ipc { +class RandomAccessStreamParams; +} + +namespace dom { + +class FileSystemAccessHandleControlParent; +class FileSystemAccessHandleParent; + +namespace fs { + +template <class T> +class Registered; + +namespace data { + +class FileSystemDataManager; + +} // namespace data +} // namespace fs + +class FileSystemAccessHandle : public FileSystemStreamCallbacks { + public: + using CreateResult = std::pair<fs::Registered<FileSystemAccessHandle>, + mozilla::ipc::RandomAccessStreamParams>; + // IsExclusive is true because we want to allow moving of CreateResult. + // There's always just one consumer anyway (When IsExclusive is true, there + // can be at most one call to either Then or ChainTo). + using CreatePromise = MozPromise<CreateResult, nsresult, + /* IsExclusive */ true>; + static RefPtr<CreatePromise> Create( + RefPtr<fs::data::FileSystemDataManager> aDataManager, + const fs::EntryId& aEntryId); + + NS_DECL_ISUPPORTS_INHERITED + + void Register(); + + void Unregister(); + + void RegisterActor(NotNull<FileSystemAccessHandleParent*> aActor); + + void UnregisterActor(NotNull<FileSystemAccessHandleParent*> aActor); + + void RegisterControlActor( + NotNull<FileSystemAccessHandleControlParent*> aControlActor); + + void UnregisterControlActor( + NotNull<FileSystemAccessHandleControlParent*> aControlActor); + + bool IsOpen() const; + + RefPtr<BoolPromise> BeginClose(); + + private: + FileSystemAccessHandle(RefPtr<fs::data::FileSystemDataManager> aDataManager, + const fs::EntryId& aEntryId, + MovingNotNull<RefPtr<TaskQueue>> aIOTaskQueue); + + ~FileSystemAccessHandle(); + + bool IsInactive() const; + + using InitPromise = + MozPromise<mozilla::ipc::RandomAccessStreamParams, nsresult, + /* IsExclusive */ true>; + RefPtr<InitPromise> BeginInit(); + + const fs::EntryId mEntryId; + RefPtr<fs::data::FileSystemDataManager> mDataManager; + const NotNull<RefPtr<TaskQueue>> mIOTaskQueue; + FileSystemAccessHandleParent* mActor; + FileSystemAccessHandleControlParent* mControlActor; + nsAutoRefCnt mRegCount; + bool mLocked; + bool mRegistered; + bool mClosed; +}; + +} // namespace dom +} // namespace mozilla + +#endif // DOM_FS_PARENT_FILESYSTEMACCESSHANDLE_H_ diff --git a/dom/fs/parent/FileSystemAccessHandleControlParent.cpp b/dom/fs/parent/FileSystemAccessHandleControlParent.cpp new file mode 100644 index 0000000000..90f325aeb5 --- /dev/null +++ b/dom/fs/parent/FileSystemAccessHandleControlParent.cpp @@ -0,0 +1,45 @@ +/* -*- 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 "FileSystemAccessHandleControlParent.h" + +#include "mozilla/dom/FileSystemAccessHandle.h" +#include "mozilla/ipc/IPCCore.h" + +namespace mozilla::dom { + +FileSystemAccessHandleControlParent::FileSystemAccessHandleControlParent( + RefPtr<FileSystemAccessHandle> aAccessHandle) + : mAccessHandle(std::move(aAccessHandle)) {} + +FileSystemAccessHandleControlParent::~FileSystemAccessHandleControlParent() { + MOZ_ASSERT(mActorDestroyed); +} + +mozilla::ipc::IPCResult FileSystemAccessHandleControlParent::RecvClose( + CloseResolver&& aResolver) { + mAccessHandle->BeginClose()->Then( + GetCurrentSerialEventTarget(), __func__, + [resolver = std::move(aResolver)]( + const BoolPromise::ResolveOrRejectValue&) { resolver(void_t()); }); + + return IPC_OK(); +} + +void FileSystemAccessHandleControlParent::ActorDestroy( + ActorDestroyReason /* aWhy */) { + MOZ_ASSERT(!mActorDestroyed); + +#ifdef DEBUG + mActorDestroyed = true; +#endif + + mAccessHandle->UnregisterControlActor(WrapNotNullUnchecked(this)); + + mAccessHandle = nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/fs/parent/FileSystemAccessHandleControlParent.h b/dom/fs/parent/FileSystemAccessHandleControlParent.h new file mode 100644 index 0000000000..80e76a6e53 --- /dev/null +++ b/dom/fs/parent/FileSystemAccessHandleControlParent.h @@ -0,0 +1,43 @@ +/* -*- 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_FILESYSTEMACCESSHANDLECONTROLPARENT_H_ +#define DOM_FS_PARENT_FILESYSTEMACCESSHANDLECONTROLPARENT_H_ + +#include "mozilla/dom/PFileSystemAccessHandleControlParent.h" +#include "nsISupportsUtils.h" + +namespace mozilla::dom { + +class FileSystemAccessHandle; + +class FileSystemAccessHandleControlParent + : public PFileSystemAccessHandleControlParent { + public: + explicit FileSystemAccessHandleControlParent( + RefPtr<FileSystemAccessHandle> aAccessHandle); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileSystemAccessHandleControlParent, + override) + + mozilla::ipc::IPCResult RecvClose(CloseResolver&& aResolver); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + protected: + virtual ~FileSystemAccessHandleControlParent(); + + private: + RefPtr<FileSystemAccessHandle> mAccessHandle; + +#ifdef DEBUG + bool mActorDestroyed = false; +#endif +}; + +} // namespace mozilla::dom + +#endif // DOM_FS_PARENT_FILESYSTEMACCESSHANDLECONTROLPARENT_H_ diff --git a/dom/fs/parent/FileSystemAccessHandleParent.cpp b/dom/fs/parent/FileSystemAccessHandleParent.cpp new file mode 100644 index 0000000000..36d21fdd2d --- /dev/null +++ b/dom/fs/parent/FileSystemAccessHandleParent.cpp @@ -0,0 +1,37 @@ +/* -*- 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 "FileSystemAccessHandleParent.h" + +#include "mozilla/dom/FileSystemAccessHandle.h" + +namespace mozilla::dom { + +FileSystemAccessHandleParent::FileSystemAccessHandleParent( + RefPtr<FileSystemAccessHandle> aAccessHandle) + : mAccessHandle(std::move(aAccessHandle)) {} + +FileSystemAccessHandleParent::~FileSystemAccessHandleParent() { + MOZ_ASSERT(mActorDestroyed); +} + +mozilla::ipc::IPCResult FileSystemAccessHandleParent::RecvClose() { + mAccessHandle->BeginClose(); + + return IPC_OK(); +} + +void FileSystemAccessHandleParent::ActorDestroy(ActorDestroyReason aWhy) { + MOZ_ASSERT(!mActorDestroyed); + + DEBUGONLY(mActorDestroyed = true); + + mAccessHandle->UnregisterActor(WrapNotNullUnchecked(this)); + + mAccessHandle = nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/fs/parent/FileSystemAccessHandleParent.h b/dom/fs/parent/FileSystemAccessHandleParent.h new file mode 100644 index 0000000000..0efeab2539 --- /dev/null +++ b/dom/fs/parent/FileSystemAccessHandleParent.h @@ -0,0 +1,38 @@ +/* -*- 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_FILESYSTEMACCESSHANDLEPARENT_H_ +#define DOM_FS_PARENT_FILESYSTEMACCESSHANDLEPARENT_H_ + +#include "mozilla/dom/PFileSystemAccessHandleParent.h" +#include "mozilla/dom/quota/DebugOnlyMacro.h" + +namespace mozilla::dom { + +class FileSystemAccessHandle; + +class FileSystemAccessHandleParent : public PFileSystemAccessHandleParent { + public: + explicit FileSystemAccessHandleParent( + RefPtr<FileSystemAccessHandle> aAccessHandle); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileSystemAccessHandleParent, override) + + mozilla::ipc::IPCResult RecvClose(); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + virtual ~FileSystemAccessHandleParent(); + + RefPtr<FileSystemAccessHandle> mAccessHandle; + + DEBUGONLY(bool mActorDestroyed = false); +}; + +} // namespace mozilla::dom + +#endif // DOM_FS_PARENT_FILESYSTEMACCESSHANDLEPARENT_H_ diff --git a/dom/fs/parent/FileSystemContentTypeGuess.cpp b/dom/fs/parent/FileSystemContentTypeGuess.cpp new file mode 100644 index 0000000000..e8ff8a760b --- /dev/null +++ b/dom/fs/parent/FileSystemContentTypeGuess.cpp @@ -0,0 +1,32 @@ +/* -*- 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 "FileSystemContentTypeGuess.h" + +#include "ErrorList.h" +#include "mozilla/dom/QMResult.h" +#include "mozilla/dom/mime_guess_ffi_generated.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "nsString.h" + +namespace mozilla::dom::fs { + +Result<ContentType, QMResult> FileSystemContentTypeGuess::FromPath( + const Name& aPath) { + NS_ConvertUTF16toUTF8 path(aPath); + ContentType contentType; + nsresult rv = mimeGuessFromPath(&path, &contentType); + + // QM_TRY is too verbose. + if (NS_FAILED(rv)) { + return Err(QMResult(rv)); + } + + return contentType; +} + +} // namespace mozilla::dom::fs diff --git a/dom/fs/parent/FileSystemContentTypeGuess.h b/dom/fs/parent/FileSystemContentTypeGuess.h new file mode 100644 index 0000000000..8b6d13f78c --- /dev/null +++ b/dom/fs/parent/FileSystemContentTypeGuess.h @@ -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/. */ + +#ifndef DOM_FS_PARENT_FILESYSTEMCONTENTTYPEGUESS_H_ +#define DOM_FS_PARENT_FILESYSTEMCONTENTTYPEGUESS_H_ + +#include "mozilla/Result.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/quota/ForwardDecls.h" +#include "nsStringFwd.h" + +namespace mozilla::dom::fs { + +struct FileSystemContentTypeGuess { + static Result<ContentType, QMResult> FromPath(const Name& aPath); +}; + +} // namespace mozilla::dom::fs + +#endif // DOM_FS_PARENT_FILESYSTEMCONTENTTYPEGUESS_H_ diff --git a/dom/fs/parent/FileSystemHashSource.cpp b/dom/fs/parent/FileSystemHashSource.cpp new file mode 100644 index 0000000000..67a5a79c8c --- /dev/null +++ b/dom/fs/parent/FileSystemHashSource.cpp @@ -0,0 +1,67 @@ +/* -*- 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 "FileSystemHashSource.h" + +#include "FileSystemParentTypes.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/data_encoding_ffi_generated.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "nsComponentManagerUtils.h" +#include "nsICryptoHash.h" +#include "nsNetCID.h" +#include "nsString.h" +#include "nsStringFwd.h" + +namespace mozilla::dom::fs::data { + +Result<EntryId, QMResult> FileSystemHashSource::GenerateHash( + const EntryId& aParent, const Name& aName) { + auto makeHasher = [](nsresult* aRv) { + return do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, aRv); + }; + QM_TRY_INSPECT(const auto& hasher, + QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_GET_TYPED( + nsCOMPtr<nsICryptoHash>, makeHasher))); + + QM_TRY(QM_TO_RESULT(hasher->Init(nsICryptoHash::SHA256))); + + QM_TRY(QM_TO_RESULT( + hasher->Update(reinterpret_cast<const uint8_t*>(aName.BeginReading()), + sizeof(char16_t) * aName.Length()))); + + QM_TRY(QM_TO_RESULT( + hasher->Update(reinterpret_cast<const uint8_t*>(aParent.BeginReading()), + aParent.Length()))); + + EntryId entryId; + QM_TRY(QM_TO_RESULT(hasher->Finish(/* aASCII */ false, entryId))); + MOZ_ASSERT(!entryId.IsEmpty()); + + return entryId; +} + +Result<Name, QMResult> FileSystemHashSource::EncodeHash(const FileId& aFileId) { + MOZ_ASSERT(32u == aFileId.Value().Length()); + nsCString encoded; + base32encode(&aFileId.Value(), &encoded); + + // We are stripping last four padding characters because + // it may not be allowed in some file systems. + MOZ_ASSERT(56u == encoded.Length() && '=' == encoded[52u] && + '=' == encoded[53u] && '=' == encoded[54u] && '=' == encoded[55u]); + encoded.SetLength(52u); + + Name result; + QM_TRY(OkIf(result.SetCapacity(encoded.Length(), mozilla::fallible)), + Err(QMResult(NS_ERROR_OUT_OF_MEMORY))); + + result.AppendASCII(encoded); + + return result; +} + +} // namespace mozilla::dom::fs::data diff --git a/dom/fs/parent/FileSystemHashSource.h b/dom/fs/parent/FileSystemHashSource.h new file mode 100644 index 0000000000..722500342d --- /dev/null +++ b/dom/fs/parent/FileSystemHashSource.h @@ -0,0 +1,31 @@ +/* -*- 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_FILESYSTEMHASHSOURCE_H_ +#define DOM_FS_PARENT_FILESYSTEMHASHSOURCE_H_ + +#include "mozilla/Result.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/QMResult.h" +#include "mozilla/dom/quota/ResultExtensions.h" + +namespace mozilla::dom::fs { + +struct FileId; + +namespace data { + +struct FileSystemHashSource { + static Result<EntryId, QMResult> GenerateHash(const EntryId& aParent, + const Name& aName); + + static Result<Name, QMResult> EncodeHash(const FileId& aFileId); +}; + +} // namespace data +} // namespace mozilla::dom::fs + +#endif // DOM_FS_PARENT_FILESYSTEMHASHSOURCE_H_ diff --git a/dom/fs/parent/FileSystemHashStorageFunction.cpp b/dom/fs/parent/FileSystemHashStorageFunction.cpp new file mode 100644 index 0000000000..601f66b9c7 --- /dev/null +++ b/dom/fs/parent/FileSystemHashStorageFunction.cpp @@ -0,0 +1,69 @@ +/* -*- 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 "FileSystemHashStorageFunction.h" + +#include "FileSystemHashSource.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/storage/Variant.h" +#include "nsString.h" +#include "nsStringFwd.h" + +namespace mozilla::dom::fs::data { + +NS_IMPL_ISUPPORTS(FileSystemHashStorageFunction, mozIStorageFunction) + +NS_IMETHODIMP +FileSystemHashStorageFunction::OnFunctionCall( + mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) { + MOZ_ASSERT(aFunctionArguments); + MOZ_ASSERT(aResult); + + const int32_t parentIndex = 0; + const int32_t childIndex = 1; + +#ifdef DEBUG + { + uint32_t argCount; + MOZ_ALWAYS_SUCCEEDS(aFunctionArguments->GetNumEntries(&argCount)); + MOZ_ASSERT(argCount == 2u); + + int32_t parentType = mozIStorageValueArray::VALUE_TYPE_INTEGER; + MOZ_ALWAYS_SUCCEEDS( + aFunctionArguments->GetTypeOfIndex(parentIndex, &parentType)); + MOZ_ASSERT(parentType == mozIStorageValueArray::VALUE_TYPE_BLOB); + + int32_t childType = mozIStorageValueArray::VALUE_TYPE_INTEGER; + MOZ_ALWAYS_SUCCEEDS( + aFunctionArguments->GetTypeOfIndex(childIndex, &childType)); + MOZ_ASSERT(childType == mozIStorageValueArray::VALUE_TYPE_BLOB); + } +#endif + + QM_TRY_INSPECT( + const EntryId& parentHash, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, aFunctionArguments, + GetBlobAsUTF8String, parentIndex)); + + QM_TRY_INSPECT(const Name& childName, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsString, aFunctionArguments, + GetBlobAsString, childIndex)); + + QM_TRY_INSPECT(const EntryId& buffer, + FileSystemHashSource::GenerateHash(parentHash, childName) + .mapErr([](const auto& aRv) { return ToNSResult(aRv); })); + + nsCOMPtr<nsIVariant> result = + new mozilla::storage::BlobVariant(std::make_pair( + static_cast<const void*>(buffer.get()), int(buffer.Length()))); + + result.forget(aResult); + + return NS_OK; +} + +} // namespace mozilla::dom::fs::data diff --git a/dom/fs/parent/FileSystemHashStorageFunction.h b/dom/fs/parent/FileSystemHashStorageFunction.h new file mode 100644 index 0000000000..7f7f9aa26c --- /dev/null +++ b/dom/fs/parent/FileSystemHashStorageFunction.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_FILESYSTEMHASHSTORAGEFUNCTION_H_ +#define DOM_FS_PARENT_FILESYSTEMHASHSTORAGEFUNCTION_H_ + +#include "mozIStorageFunction.h" +#include "nsISupportsImpl.h" + +namespace mozilla::dom::fs::data { + +class FileSystemHashStorageFunction final : public mozIStorageFunction { + private: + ~FileSystemHashStorageFunction() = default; + + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION +}; + +} // namespace mozilla::dom::fs::data + +#endif // DOM_FS_PARENT_FILESYSTEMHASHSTORAGEFUNCTION_H_ diff --git a/dom/fs/parent/FileSystemManagerParent.cpp b/dom/fs/parent/FileSystemManagerParent.cpp new file mode 100644 index 0000000000..6f59c179f9 --- /dev/null +++ b/dom/fs/parent/FileSystemManagerParent.cpp @@ -0,0 +1,519 @@ +/* -*- 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 "FileSystemManagerParent.h" + +#include "FileSystemDatabaseManager.h" +#include "FileSystemParentTypes.h" +#include "mozilla/Maybe.h" +#include "mozilla/dom/FileBlobImpl.h" +#include "mozilla/dom/FileSystemAccessHandle.h" +#include "mozilla/dom/FileSystemAccessHandleControlParent.h" +#include "mozilla/dom/FileSystemAccessHandleParent.h" +#include "mozilla/dom/FileSystemDataManager.h" +#include "mozilla/dom/FileSystemLog.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/FileSystemWritableFileStreamParent.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include "mozilla/dom/QMResult.h" +#include "mozilla/dom/quota/FileStreams.h" +#include "mozilla/dom/quota/ForwardDecls.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/FileDescriptorUtils.h" +#include "mozilla/ipc/RandomAccessStreamUtils.h" +#include "nsNetUtil.h" +#include "nsString.h" +#include "nsTArray.h" + +using IPCResult = mozilla::ipc::IPCResult; + +namespace mozilla::dom { + +FileSystemManagerParent::FileSystemManagerParent( + RefPtr<fs::data::FileSystemDataManager> aDataManager, + const EntryId& aRootEntry) + : mDataManager(std::move(aDataManager)), mRootResponse(aRootEntry) {} + +FileSystemManagerParent::~FileSystemManagerParent() { + LOG(("Destroying FileSystemManagerParent %p", this)); + MOZ_ASSERT(!mRegistered); +} + +void FileSystemManagerParent::AssertIsOnIOTarget() const { + MOZ_ASSERT(mDataManager); + + mDataManager->AssertIsOnIOTarget(); +} + +const RefPtr<fs::data::FileSystemDataManager>& +FileSystemManagerParent::DataManagerStrongRef() const { + MOZ_ASSERT(!mActorDestroyed); + MOZ_ASSERT(mDataManager); + + return mDataManager; +} + +IPCResult FileSystemManagerParent::RecvGetRootHandle( + GetRootHandleResolver&& aResolver) { + AssertIsOnIOTarget(); + + aResolver(mRootResponse); + + return IPC_OK(); +} + +IPCResult FileSystemManagerParent::RecvGetDirectoryHandle( + FileSystemGetHandleRequest&& aRequest, + GetDirectoryHandleResolver&& aResolver) { + LOG(("GetDirectoryHandle %s ", + NS_ConvertUTF16toUTF8(aRequest.handle().childName()).get())); + AssertIsOnIOTarget(); + MOZ_ASSERT(!aRequest.handle().parentId().IsEmpty()); + MOZ_ASSERT(mDataManager); + + auto reportError = [&aResolver](const QMResult& aRv) { + FileSystemGetHandleResponse response(ToNSResult(aRv)); + 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(); +} + +IPCResult FileSystemManagerParent::RecvGetFileHandle( + FileSystemGetHandleRequest&& aRequest, GetFileHandleResolver&& aResolver) { + AssertIsOnIOTarget(); + MOZ_ASSERT(!aRequest.handle().parentId().IsEmpty()); + MOZ_ASSERT(mDataManager); + + auto reportError = [&aResolver](const QMResult& aRv) { + FileSystemGetHandleResponse response(ToNSResult(aRv)); + 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(); +} + +// Could use a template, but you need several types +mozilla::ipc::IPCResult FileSystemManagerParent::RecvGetAccessHandle( + FileSystemGetAccessHandleRequest&& aRequest, + GetAccessHandleResolver&& aResolver) { + AssertIsOnIOTarget(); + MOZ_ASSERT(mDataManager); + + EntryId entryId = aRequest.entryId(); + + FileSystemAccessHandle::Create(mDataManager, entryId) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self = RefPtr(this), request = std::move(aRequest), + resolver = std::move(aResolver)]( + FileSystemAccessHandle::CreatePromise::ResolveOrRejectValue&& + aValue) { + if (!self->CanSend()) { + return; + } + + if (aValue.IsReject()) { + resolver(aValue.RejectValue()); + return; + } + + FileSystemAccessHandle::CreateResult result = + std::move(aValue.ResolveValue()); + + fs::Registered<FileSystemAccessHandle> accessHandle = + std::move(result.first); + + RandomAccessStreamParams streamParams = std::move(result.second); + + auto accessHandleParent = MakeRefPtr<FileSystemAccessHandleParent>( + accessHandle.inspect()); + + auto resolveAndReturn = [&resolver](nsresult rv) { resolver(rv); }; + + ManagedEndpoint<PFileSystemAccessHandleChild> + accessHandleChildEndpoint = + self->OpenPFileSystemAccessHandleEndpoint( + accessHandleParent); + QM_TRY(MOZ_TO_RESULT(accessHandleChildEndpoint.IsValid()), + resolveAndReturn); + + accessHandle->RegisterActor(WrapNotNull(accessHandleParent)); + + auto accessHandleControlParent = + MakeRefPtr<FileSystemAccessHandleControlParent>( + accessHandle.inspect()); + + Endpoint<PFileSystemAccessHandleControlParent> + accessHandleControlParentEndpoint; + Endpoint<PFileSystemAccessHandleControlChild> + accessHandleControlChildEndpoint; + MOZ_ALWAYS_SUCCEEDS(PFileSystemAccessHandleControl::CreateEndpoints( + &accessHandleControlParentEndpoint, + &accessHandleControlChildEndpoint)); + + accessHandleControlParentEndpoint.Bind(accessHandleControlParent); + + accessHandle->RegisterControlActor( + WrapNotNull(accessHandleControlParent)); + + resolver(FileSystemAccessHandleProperties( + std::move(streamParams), std::move(accessHandleChildEndpoint), + std::move(accessHandleControlChildEndpoint))); + }); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult FileSystemManagerParent::RecvGetWritable( + FileSystemGetWritableRequest&& aRequest, GetWritableResolver&& aResolver) { + AssertIsOnIOTarget(); + MOZ_ASSERT(mDataManager); + + const fs::FileMode mode = mDataManager->GetMode(aRequest.keepData()); + + auto reportError = [aResolver](const auto& aRv) { + aResolver(ToNSResult(aRv)); + }; + + // TODO: Get rid of mode and switching based on it, have the right unlocking + // automatically + const fs::EntryId& entryId = aRequest.entryId(); + QM_TRY_UNWRAP( + fs::FileId fileId, + (mode == fs::FileMode::EXCLUSIVE ? mDataManager->LockExclusive(entryId) + : mDataManager->LockShared(entryId)), + IPC_OK(), reportError); + MOZ_ASSERT(!fileId.IsEmpty()); + + auto autoUnlock = MakeScopeExit( + [self = RefPtr<FileSystemManagerParent>(this), &entryId, &fileId, mode] { + if (mode == fs::FileMode::EXCLUSIVE) { + self->mDataManager->UnlockExclusive(entryId); + } else { + self->mDataManager->UnlockShared(entryId, fileId, /* aAbort */ true); + } + }); + + fs::ContentType type; + fs::TimeStamp lastModifiedMilliSeconds; + fs::Path path; + nsCOMPtr<nsIFile> file; + QM_TRY( + MOZ_TO_RESULT(mDataManager->MutableDatabaseManagerPtr()->GetFile( + entryId, fileId, mode, type, lastModifiedMilliSeconds, path, file)), + IPC_OK(), reportError); + + if (LOG_ENABLED()) { + nsAutoString path; + if (NS_SUCCEEDED(file->GetPath(path))) { + LOG(("Opening Writable %s", NS_ConvertUTF16toUTF8(path).get())); + } + } + + auto writableFileStreamParent = + MakeNotNull<RefPtr<FileSystemWritableFileStreamParent>>( + this, aRequest.entryId(), fileId, mode == fs::FileMode::EXCLUSIVE); + + QM_TRY_UNWRAP( + nsCOMPtr<nsIRandomAccessStream> stream, + CreateFileRandomAccessStream(quota::PERSISTENCE_TYPE_DEFAULT, + mDataManager->OriginMetadataRef(), + quota::Client::FILESYSTEM, file, -1, -1, + nsIFileRandomAccessStream::DEFER_OPEN), + IPC_OK(), reportError); + + RandomAccessStreamParams streamParams = + mozilla::ipc::SerializeRandomAccessStream( + WrapMovingNotNullUnchecked(std::move(stream)), + writableFileStreamParent->GetOrCreateStreamCallbacks()); + + // Release the auto unlock helper just before calling + // SendPFileSystemWritableFileStreamConstructor which is responsible for + // destroying the actor if the sending fails (we call `UnlockExclusive` when + // the actor is destroyed). + autoUnlock.release(); + + if (!SendPFileSystemWritableFileStreamConstructor(writableFileStreamParent)) { + aResolver(NS_ERROR_FAILURE); + return IPC_OK(); + } + + aResolver(FileSystemWritableFileStreamProperties(std::move(streamParams), + writableFileStreamParent)); + + return IPC_OK(); +} + +IPCResult FileSystemManagerParent::RecvGetFile( + FileSystemGetFileRequest&& aRequest, GetFileResolver&& aResolver) { + AssertIsOnIOTarget(); + + // XXX Spec https://www.w3.org/TR/FileAPI/#dfn-file wants us to snapshot the + // state of the file at getFile() time + + // You can create a File with getFile() even if the file is locked + // XXX factor out this part of the code for accesshandle/ and getfile + auto reportError = [aResolver](const auto& rv) { + LOG(("getFile() Failed!")); + aResolver(ToNSResult(rv)); + }; + + const auto& entryId = aRequest.entryId(); + + QM_TRY_INSPECT( + const fs::FileId& fileId, + mDataManager->MutableDatabaseManagerPtr()->EnsureFileId(entryId), + IPC_OK(), reportError); + + fs::ContentType type; + fs::TimeStamp lastModifiedMilliSeconds; + fs::Path path; + nsCOMPtr<nsIFile> fileObject; + QM_TRY(MOZ_TO_RESULT(mDataManager->MutableDatabaseManagerPtr()->GetFile( + entryId, fileId, fs::FileMode::EXCLUSIVE, type, + lastModifiedMilliSeconds, path, fileObject)), + IPC_OK(), reportError); + + if (LOG_ENABLED()) { + nsAutoString path; + if (NS_SUCCEEDED(fileObject->GetPath(path))) { + LOG(("Opening File as blob: %s", NS_ConvertUTF16toUTF8(path).get())); + } + } + + // TODO: Currently, there is no way to assign type and it is empty. + // See bug 1826780. + RefPtr<BlobImpl> blob = MakeRefPtr<FileBlobImpl>( + fileObject, path.LastElement(), NS_ConvertUTF8toUTF16(type)); + + IPCBlob ipcBlob; + QM_TRY(MOZ_TO_RESULT(IPCBlobUtils::Serialize(blob, ipcBlob)), IPC_OK(), + reportError); + + aResolver( + FileSystemFileProperties(lastModifiedMilliSeconds, ipcBlob, type, path)); + return IPC_OK(); +} + +IPCResult FileSystemManagerParent::RecvResolve( + FileSystemResolveRequest&& aRequest, ResolveResolver&& aResolver) { + AssertIsOnIOTarget(); + 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(ToNSResult(aRv)); + aResolver(response); + }; + + QM_TRY_UNWRAP( + filePath, + mDataManager->MutableDatabaseManagerPtr()->Resolve(aRequest.endpoints()), + IPC_OK(), reportError); + + if (LOG_ENABLED()) { + nsString path; + for (auto& entry : filePath) { + path.Append(entry); + } + LOG(("Resolve path: %s", NS_ConvertUTF16toUTF8(path).get())); + } + + if (filePath.IsEmpty()) { + FileSystemResolveResponse response(Nothing{}); + aResolver(response); + + return IPC_OK(); + } + + FileSystemResolveResponse response(Some(FileSystemPath(filePath))); + aResolver(response); + + return IPC_OK(); +} + +IPCResult FileSystemManagerParent::RecvGetEntries( + FileSystemGetEntriesRequest&& aRequest, GetEntriesResolver&& aResolver) { + AssertIsOnIOTarget(); + MOZ_ASSERT(!aRequest.parentId().IsEmpty()); + MOZ_ASSERT(mDataManager); + + auto reportError = [&aResolver](const QMResult& aRv) { + FileSystemGetEntriesResponse response(ToNSResult(aRv)); + aResolver(response); + }; + + QM_TRY_UNWRAP(FileSystemDirectoryListing entries, + mDataManager->MutableDatabaseManagerPtr()->GetDirectoryEntries( + aRequest.parentId(), aRequest.page()), + IPC_OK(), reportError); + + FileSystemGetEntriesResponse response(entries); + aResolver(response); + + return IPC_OK(); +} + +IPCResult FileSystemManagerParent::RecvRemoveEntry( + FileSystemRemoveEntryRequest&& aRequest, RemoveEntryResolver&& aResolver) { + LOG(("RemoveEntry %s", + NS_ConvertUTF16toUTF8(aRequest.handle().childName()).get())); + AssertIsOnIOTarget(); + MOZ_ASSERT(!aRequest.handle().parentId().IsEmpty()); + MOZ_ASSERT(mDataManager); + + auto reportError = [&aResolver](const QMResult& aRv) { + FileSystemRemoveEntryResponse response(ToNSResult(aRv)); + aResolver(response); + }; + + QM_TRY_UNWRAP( + bool isDeleted, + mDataManager->MutableDatabaseManagerPtr()->RemoveFile(aRequest.handle()), + IPC_OK(), reportError); + + if (isDeleted) { + FileSystemRemoveEntryResponse response(void_t{}); + 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_DOM_NOT_FOUND_ERR); + aResolver(response); + + return IPC_OK(); + } + + FileSystemRemoveEntryResponse response(void_t{}); + aResolver(response); + + return IPC_OK(); +} + +IPCResult FileSystemManagerParent::RecvMoveEntry( + FileSystemMoveEntryRequest&& aRequest, MoveEntryResolver&& aResolver) { + LOG(("MoveEntry %s to %s", + NS_ConvertUTF16toUTF8(aRequest.handle().entryName()).get(), + NS_ConvertUTF16toUTF8(aRequest.destHandle().childName()).get())); + MOZ_ASSERT(!aRequest.handle().entryId().IsEmpty()); + MOZ_ASSERT(!aRequest.destHandle().parentId().IsEmpty()); + MOZ_ASSERT(mDataManager); + + auto reportError = [&aResolver](const QMResult& aRv) { + FileSystemMoveEntryResponse response(ToNSResult(aRv)); + aResolver(response); + }; + + QM_TRY_INSPECT(const EntryId& newId, + mDataManager->MutableDatabaseManagerPtr()->MoveEntry( + aRequest.handle(), aRequest.destHandle()), + IPC_OK(), reportError); + + fs::FileSystemMoveEntryResponse response(newId); + aResolver(response); + return IPC_OK(); +} + +IPCResult FileSystemManagerParent::RecvRenameEntry( + FileSystemRenameEntryRequest&& aRequest, MoveEntryResolver&& aResolver) { + // if destHandle's parentId is empty, then we're renaming in the same + // directory + LOG(("RenameEntry %s to %s", + NS_ConvertUTF16toUTF8(aRequest.handle().entryName()).get(), + NS_ConvertUTF16toUTF8(aRequest.name()).get())); + MOZ_ASSERT(!aRequest.handle().entryId().IsEmpty()); + MOZ_ASSERT(mDataManager); + + auto reportError = [&aResolver](const QMResult& aRv) { + FileSystemMoveEntryResponse response(ToNSResult(aRv)); + aResolver(response); + }; + + QM_TRY_INSPECT(const EntryId& newId, + mDataManager->MutableDatabaseManagerPtr()->RenameEntry( + aRequest.handle(), aRequest.name()), + IPC_OK(), reportError); + + fs::FileSystemMoveEntryResponse response(newId); + aResolver(response); + return IPC_OK(); +} + +void FileSystemManagerParent::RequestAllowToClose() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (mRequestedAllowToClose) { + return; + } + + mRequestedAllowToClose.Flip(); + + InvokeAsync(mDataManager->MutableIOTaskQueuePtr(), __func__, + [self = RefPtr<FileSystemManagerParent>(this)]() { + return self->SendCloseAll(); + }) + ->Then(mDataManager->MutableIOTaskQueuePtr(), __func__, + [self = RefPtr<FileSystemManagerParent>(this)]( + const CloseAllPromise::ResolveOrRejectValue& aValue) { + self->Close(); + + return BoolPromise::CreateAndResolve(true, __func__); + }); +} + +void FileSystemManagerParent::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnIOTarget(); + MOZ_ASSERT(!mActorDestroyed); + + DEBUGONLY(mActorDestroyed = true); + + InvokeAsync(mDataManager->MutableBackgroundTargetPtr(), __func__, + [self = RefPtr<FileSystemManagerParent>(this)]() { + self->mDataManager->UnregisterActor(WrapNotNull(self)); + + self->mDataManager = nullptr; + + return BoolPromise::CreateAndResolve(true, __func__); + }); +} + +} // namespace mozilla::dom diff --git a/dom/fs/parent/FileSystemManagerParent.h b/dom/fs/parent/FileSystemManagerParent.h new file mode 100644 index 0000000000..01f9f23f6b --- /dev/null +++ b/dom/fs/parent/FileSystemManagerParent.h @@ -0,0 +1,93 @@ +/* -*- 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_FILESYSTEMMANAGERPARENT_H_ +#define DOM_FS_PARENT_FILESYSTEMMANAGERPARENT_H_ + +#include "ErrorList.h" +#include "mozilla/dom/FlippedOnce.h" +#include "mozilla/dom/PFileSystemManagerParent.h" +#include "mozilla/dom/quota/DebugOnlyMacro.h" +#include "nsISupports.h" + +namespace mozilla::dom { + +namespace fs::data { +class FileSystemDataManager; +} // namespace fs::data + +class FileSystemManagerParent : public PFileSystemManagerParent { + public: + FileSystemManagerParent(RefPtr<fs::data::FileSystemDataManager> aDataManager, + const EntryId& aRootEntry); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileSystemManagerParent, override) + + void AssertIsOnIOTarget() const; + +#ifdef DEBUG + void SetRegistered(bool aRegistered) { mRegistered = aRegistered; } +#endif + + // Safe to call while the actor is live. + const RefPtr<fs::data::FileSystemDataManager>& DataManagerStrongRef() const; + + mozilla::ipc::IPCResult RecvGetRootHandle(GetRootHandleResolver&& aResolver); + + mozilla::ipc::IPCResult RecvGetDirectoryHandle( + FileSystemGetHandleRequest&& aRequest, + GetDirectoryHandleResolver&& aResolver); + + mozilla::ipc::IPCResult RecvGetFileHandle( + FileSystemGetHandleRequest&& aRequest, GetFileHandleResolver&& aResolver); + + mozilla::ipc::IPCResult RecvGetAccessHandle( + FileSystemGetAccessHandleRequest&& aRequest, + GetAccessHandleResolver&& aResolver); + + mozilla::ipc::IPCResult RecvGetWritable( + FileSystemGetWritableRequest&& aRequest, GetWritableResolver&& aResolver); + + mozilla::ipc::IPCResult RecvGetFile(FileSystemGetFileRequest&& aRequest, + GetFileResolver&& aResolver); + + mozilla::ipc::IPCResult RecvResolve(FileSystemResolveRequest&& aRequest, + ResolveResolver&& aResolver); + + mozilla::ipc::IPCResult RecvGetEntries(FileSystemGetEntriesRequest&& aRequest, + GetEntriesResolver&& aResolver); + + mozilla::ipc::IPCResult RecvRemoveEntry( + FileSystemRemoveEntryRequest&& aRequest, RemoveEntryResolver&& aResolver); + + mozilla::ipc::IPCResult RecvMoveEntry(FileSystemMoveEntryRequest&& aRequest, + MoveEntryResolver&& aResolver); + + mozilla::ipc::IPCResult RecvRenameEntry( + FileSystemRenameEntryRequest&& aRequest, MoveEntryResolver&& aResolver); + + void RequestAllowToClose(); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + protected: + virtual ~FileSystemManagerParent(); + + private: + RefPtr<fs::data::FileSystemDataManager> mDataManager; + + FileSystemGetHandleResponse mRootResponse; + + FlippedOnce<false> mRequestedAllowToClose; + + DEBUGONLY(bool mRegistered = false); + + DEBUGONLY(bool mActorDestroyed = false); +}; + +} // namespace mozilla::dom + +#endif // DOM_FS_PARENT_FILESYSTEMMANAGERPARENT_H_ diff --git a/dom/fs/parent/FileSystemManagerParentFactory.cpp b/dom/fs/parent/FileSystemManagerParentFactory.cpp new file mode 100644 index 0000000000..a7a4b13664 --- /dev/null +++ b/dom/fs/parent/FileSystemManagerParentFactory.cpp @@ -0,0 +1,114 @@ +/* -*- 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 "FileSystemManagerParentFactory.h" + +#include "mozilla/OriginAttributes.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/FileSystemDataManager.h" +#include "mozilla/dom/FileSystemLog.h" +#include "mozilla/dom/FileSystemManagerParent.h" +#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" + +namespace mozilla::dom { +mozilla::ipc::IPCResult CreateFileSystemManagerParent( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + mozilla::ipc::Endpoint<PFileSystemManagerParent>&& aParentEndpoint, + std::function<void(const nsresult&)>&& aResolver) { + using CreateActorPromise = + MozPromise<RefPtr<FileSystemManagerParent>, nsresult, true>; + + QM_TRY(OkIf(StaticPrefs::dom_fs_enabled()), IPC_OK(), + [aResolver](const auto&) { aResolver(NS_ERROR_DOM_NOT_ALLOWED_ERR); }); + + QM_TRY(OkIf(aParentEndpoint.IsValid()), IPC_OK(), + [aResolver](const auto&) { aResolver(NS_ERROR_INVALID_ARG); }); + + // This blocks Null and Expanded principals + QM_TRY(OkIf(quota::QuotaManager::IsPrincipalInfoValid(aPrincipalInfo)), + IPC_OK(), + [aResolver](const auto&) { aResolver(NS_ERROR_DOM_SECURITY_ERR); }); + + QM_TRY(quota::QuotaManager::EnsureCreated(), IPC_OK(), + [aResolver](const auto rv) { aResolver(rv); }); + + auto* const quotaManager = quota::QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY_UNWRAP(auto principalMetadata, + quotaManager->GetInfoFromValidatedPrincipalInfo(aPrincipalInfo), + IPC_OK(), [aResolver](const auto rv) { aResolver(rv); }); + + quota::OriginMetadata originMetadata(std::move(principalMetadata), + quota::PERSISTENCE_TYPE_DEFAULT); + + // Block use for now in PrivateBrowsing + QM_TRY(OkIf(!OriginAttributes::IsPrivateBrowsing(originMetadata.mOrigin)), + IPC_OK(), + [aResolver](const auto&) { aResolver(NS_ERROR_DOM_NOT_ALLOWED_ERR); }); + + 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( + originMetadata) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [origin = originMetadata.mOrigin, + parentEndpoint = std::move(aParentEndpoint), + aResolver](const fs::Registered<fs::data::FileSystemDataManager>& + dataManager) mutable { + QM_TRY_UNWRAP( + fs::EntryId rootId, fs::data::GetRootHandle(origin), QM_VOID, + [aResolver](const auto& aRv) { aResolver(ToNSResult(aRv)); }); + + InvokeAsync( + dataManager->MutableIOTaskQueuePtr(), __func__, + [dataManager = + RefPtr<fs::data::FileSystemDataManager>(dataManager), + rootId, parentEndpoint = std::move(parentEndpoint)]() mutable { + RefPtr<FileSystemManagerParent> parent = + new FileSystemManagerParent(std::move(dataManager), + rootId); + + LOG(("Binding parent endpoint")); + if (!parentEndpoint.Bind(parent)) { + return CreateActorPromise::CreateAndReject(NS_ERROR_FAILURE, + __func__); + } + + return CreateActorPromise::CreateAndResolve(std::move(parent), + __func__); + }) + ->Then(GetCurrentSerialEventTarget(), __func__, + [dataManager = dataManager, aResolver]( + CreateActorPromise::ResolveOrRejectValue&& aValue) { + if (aValue.IsReject()) { + aResolver(aValue.RejectValue()); + } else { + RefPtr<FileSystemManagerParent> parent = + std::move(aValue.ResolveValue()); + + dataManager->RegisterActor(WrapNotNull(parent)); + + aResolver(NS_OK); + } + }); + }, + [aResolver](nsresult aRejectValue) { aResolver(aRejectValue); }); + + return IPC_OK(); +} + +} // namespace mozilla::dom diff --git a/dom/fs/parent/FileSystemManagerParentFactory.h b/dom/fs/parent/FileSystemManagerParentFactory.h new file mode 100644 index 0000000000..6d581ffa3f --- /dev/null +++ b/dom/fs/parent/FileSystemManagerParentFactory.h @@ -0,0 +1,39 @@ +/* -*- 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_FILESYSTEMMANAGER_H_ +#define DOM_FS_PARENT_FILESYSTEMMANAGER_H_ + +#include <functional> + +enum class nsresult : uint32_t; + +namespace mozilla { + +namespace ipc { + +template <class T> +class Endpoint; + +class IPCResult; +class PrincipalInfo; + +} // namespace ipc + +namespace dom { + +class PFileSystemManagerParent; + +mozilla::ipc::IPCResult CreateFileSystemManagerParent( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + mozilla::ipc::Endpoint<mozilla::dom::PFileSystemManagerParent>&& + aParentEndpoint, + std::function<void(const nsresult&)>&& aResolver); + +} // namespace dom +} // namespace mozilla + +#endif // DOM_FS_PARENT_FILESYSTEMMANAGER_H_ diff --git a/dom/fs/parent/FileSystemParentTypes.h b/dom/fs/parent/FileSystemParentTypes.h new file mode 100644 index 0000000000..15cfe42bb0 --- /dev/null +++ b/dom/fs/parent/FileSystemParentTypes.h @@ -0,0 +1,46 @@ +/* -*- 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_FILESYSTEMPARENTTYPES_H_ +#define DOM_FS_PARENT_FILESYSTEMPARENTTYPES_H_ + +#include "nsStringFwd.h" +#include "nsTString.h" + +namespace mozilla::dom::fs { + +/** + * @brief FileId refers to a file on disk while EntryId refers to a path. + * Same user input path will always generate the same EntryId while the FileId + * can be different. Move methods can change the FileId which underlies + * an EntryId and multiple FileIds for temporary files can all map to the same + * EntryId. + */ +struct FileId { + explicit FileId(const nsCString& aValue) : mValue(aValue) {} + + explicit FileId(nsCString&& aValue) : mValue(std::move(aValue)) {} + + constexpr bool IsEmpty() const { return mValue.IsEmpty(); } + + constexpr const nsCString& Value() const { return mValue; } + + nsCString mValue; +}; + +inline bool operator==(const FileId& aLhs, const FileId& aRhs) { + return aLhs.mValue == aRhs.mValue; +} + +inline bool operator!=(const FileId& aLhs, const FileId& aRhs) { + return aLhs.mValue != aRhs.mValue; +} + +enum class FileMode { EXCLUSIVE, SHARED_FROM_EMPTY, SHARED_FROM_COPY }; + +} // namespace mozilla::dom::fs + +#endif // DOM_FS_PARENT_FILESYSTEMPARENTTYPES_H_ diff --git a/dom/fs/parent/FileSystemQuotaClient.cpp b/dom/fs/parent/FileSystemQuotaClient.cpp new file mode 100644 index 0000000000..fbe61b59df --- /dev/null +++ b/dom/fs/parent/FileSystemQuotaClient.cpp @@ -0,0 +1,167 @@ +/* -*- 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 "FileSystemQuotaClient.h" + +#include "datamodel/FileSystemDatabaseManager.h" +#include "datamodel/FileSystemFileManager.h" +#include "mozilla/dom/FileSystemDataManager.h" +#include "mozilla/dom/quota/Assertions.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 "nsIFile.h" + +namespace mozilla::dom::fs { + +namespace { + +auto toNSResult = [](const auto& aRv) { return ToNSResult(aRv); }; + +} // namespace + +FileSystemQuotaClient::FileSystemQuotaClient() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); +} + +quota::Client::Type FileSystemQuotaClient::GetType() { + return quota::Client::Type::FILESYSTEM; +} + +Result<quota::UsageInfo, nsresult> FileSystemQuotaClient::InitOrigin( + quota::PersistenceType aPersistenceType, + const quota::OriginMetadata& aOriginMetadata, const AtomicBool& aCanceled) { + quota::AssertIsOnIOThread(); + + { + QM_TRY_INSPECT(const nsCOMPtr<nsIFile>& databaseFile, + data::GetDatabaseFile(aOriginMetadata).mapErr(toNSResult)); + + bool exists = false; + QM_TRY(MOZ_TO_RESULT(databaseFile->Exists(&exists))); + // If database doesn't already exist, we do not create it + if (!exists) { + return quota::UsageInfo(); + } + } + + QM_TRY_INSPECT( + const ResultConnection& conn, + data::GetStorageConnection(aOriginMetadata, /* aDirectoryLockId */ -1) + .mapErr(toNSResult)); + + QM_TRY(MOZ_TO_RESULT( + data::FileSystemDatabaseManager::RescanUsages(conn, aOriginMetadata))); + + return data::FileSystemDatabaseManager::GetUsage(conn, aOriginMetadata) + .mapErr(toNSResult); +} + +nsresult FileSystemQuotaClient::InitOriginWithoutTracking( + quota::PersistenceType /* aPersistenceType */, + const quota::OriginMetadata& /* aOriginMetadata */, + const AtomicBool& /* aCanceled */) { + quota::AssertIsOnIOThread(); + + // This is called when a storage/permanent/${origin}/fs directory exists. Even + // though this shouldn't happen with a "good" profile, we shouldn't return an + // error here, since that would cause origin initialization to fail. We just + // warn and otherwise ignore that. + UNKNOWN_FILE_WARNING( + NS_LITERAL_STRING_FROM_CSTRING(FILESYSTEM_DIRECTORY_NAME)); + + return NS_OK; +} + +Result<quota::UsageInfo, nsresult> FileSystemQuotaClient::GetUsageForOrigin( + quota::PersistenceType aPersistenceType, + const quota::OriginMetadata& aOriginMetadata, + const AtomicBool& /* aCanceled */) { + quota::AssertIsOnIOThread(); + + MOZ_ASSERT(aPersistenceType == + quota::PersistenceType::PERSISTENCE_TYPE_DEFAULT); + + quota::QuotaManager* quotaManager = quota::QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + // We can't open the database at this point because the quota manager may not + // allow it. Use the cached value instead. + return quotaManager->GetUsageForClient(aPersistenceType, aOriginMetadata, + quota::Client::FILESYSTEM); +} + +void FileSystemQuotaClient::OnOriginClearCompleted( + quota::PersistenceType aPersistenceType, const nsACString& aOrigin) { + quota::AssertIsOnIOThread(); +} + +void FileSystemQuotaClient::OnRepositoryClearCompleted( + quota::PersistenceType aPersistenceType) { + quota::AssertIsOnIOThread(); +} + +void FileSystemQuotaClient::ReleaseIOThreadObjects() { + quota::AssertIsOnIOThread(); +} + +void FileSystemQuotaClient::AbortOperationsForLocks( + const DirectoryLockIdTable& aDirectoryLockIds) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + data::FileSystemDataManager::AbortOperationsForLocks(aDirectoryLockIds); +} + +void FileSystemQuotaClient::AbortOperationsForProcess( + ContentParentId aContentParentId) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); +} + +void FileSystemQuotaClient::AbortAllOperations() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); +} + +void FileSystemQuotaClient::StartIdleMaintenance() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); +} + +void FileSystemQuotaClient::StopIdleMaintenance() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); +} + +void FileSystemQuotaClient::InitiateShutdown() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + data::FileSystemDataManager::InitiateShutdown(); +} + +nsCString FileSystemQuotaClient::GetShutdownStatus() const { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + return "Not implemented"_ns; +} + +bool FileSystemQuotaClient::IsShutdownCompleted() const { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + return data::FileSystemDataManager::IsShutdownCompleted(); +} + +void FileSystemQuotaClient::ForceKillActors() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + // Hopefully not needed. +} + +void FileSystemQuotaClient::FinalizeShutdown() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + // Empty for now. +} + +} // namespace mozilla::dom::fs diff --git a/dom/fs/parent/FileSystemQuotaClient.h b/dom/fs/parent/FileSystemQuotaClient.h new file mode 100644 index 0000000000..e0eced35b9 --- /dev/null +++ b/dom/fs/parent/FileSystemQuotaClient.h @@ -0,0 +1,69 @@ +/* -*- 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_FILESYSTEMQUOTACLIENT_H_ +#define DOM_FS_PARENT_FILESYSTEMQUOTACLIENT_H_ + +#include "mozilla/dom/quota/Client.h" + +namespace mozilla::dom::fs { + +class FileSystemQuotaClient : public quota::Client { + public: + FileSystemQuotaClient(); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::fs::FileSystemQuotaClient, + override) + + Type GetType() override; + + Result<quota::UsageInfo, nsresult> InitOrigin( + quota::PersistenceType aPersistenceType, + const quota::OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) override; + + nsresult InitOriginWithoutTracking( + quota::PersistenceType aPersistenceType, + const quota::OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) override; + + Result<quota::UsageInfo, nsresult> GetUsageForOrigin( + quota::PersistenceType aPersistenceType, + const quota::OriginMetadata& aOriginMetadata, + const AtomicBool& aCanceled) override; + + void OnOriginClearCompleted(quota::PersistenceType aPersistenceType, + const nsACString& aOrigin) override; + + void OnRepositoryClearCompleted( + quota::PersistenceType aPersistenceType) override; + + void ReleaseIOThreadObjects() override; + + void AbortOperationsForLocks( + const DirectoryLockIdTable& aDirectoryLockIds) override; + + void AbortOperationsForProcess(ContentParentId aContentParentId) override; + + void AbortAllOperations() override; + + void StartIdleMaintenance() override; + + void StopIdleMaintenance() override; + + protected: + ~FileSystemQuotaClient() = default; + + void InitiateShutdown() override; + bool IsShutdownCompleted() const override; + nsCString GetShutdownStatus() const override; + void ForceKillActors() override; + void FinalizeShutdown() override; +}; + +} // namespace mozilla::dom::fs + +#endif // DOM_FS_PARENT_FILESYSTEMQUOTACLIENT_H_ diff --git a/dom/fs/parent/FileSystemQuotaClientFactory.cpp b/dom/fs/parent/FileSystemQuotaClientFactory.cpp new file mode 100644 index 0000000000..5d4897dfee --- /dev/null +++ b/dom/fs/parent/FileSystemQuotaClientFactory.cpp @@ -0,0 +1,49 @@ +/* -*- 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 "FileSystemQuotaClientFactory.h" + +#include "FileSystemQuotaClient.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ipc/BackgroundParent.h" + +namespace mozilla::dom::fs { + +namespace { + +StaticRefPtr<FileSystemQuotaClientFactory> gCustomFactory; + +} // namespace + +// static +void FileSystemQuotaClientFactory::SetCustomFactory( + RefPtr<FileSystemQuotaClientFactory> aCustomFactory) { + gCustomFactory = std::move(aCustomFactory); +} + +// static +already_AddRefed<quota::Client> +FileSystemQuotaClientFactory::CreateQuotaClient() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (gCustomFactory) { + return gCustomFactory->AllocQuotaClient(); + } + + auto factory = MakeRefPtr<FileSystemQuotaClientFactory>(); + + return factory->AllocQuotaClient(); +} + +already_AddRefed<quota::Client> +FileSystemQuotaClientFactory::AllocQuotaClient() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + RefPtr<FileSystemQuotaClient> result = new FileSystemQuotaClient(); + return result.forget(); +} + +} // namespace mozilla::dom::fs diff --git a/dom/fs/parent/FileSystemQuotaClientFactory.h b/dom/fs/parent/FileSystemQuotaClientFactory.h new file mode 100644 index 0000000000..f71587fac0 --- /dev/null +++ b/dom/fs/parent/FileSystemQuotaClientFactory.h @@ -0,0 +1,49 @@ +/* -*- 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_FILESYSTEMQUOTACLIENTFACTORY_H_ +#define DOM_FS_PARENT_FILESYSTEMQUOTACLIENTFACTORY_H_ + +#include "mozilla/AlreadyAddRefed.h" +#include "nsISupportsUtils.h" + +template <class> +class RefPtr; + +namespace mozilla::dom { + +namespace quota { + +class Client; + +} // namespace quota + +namespace fs { + +class FileSystemQuotaClientFactory { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING( + mozilla::dom::fs::FileSystemQuotaClientFactory); + + static void SetCustomFactory( + RefPtr<FileSystemQuotaClientFactory> aCustomFactory); + + static already_AddRefed<quota::Client> CreateQuotaClient(); + + protected: + virtual ~FileSystemQuotaClientFactory() = default; + + virtual already_AddRefed<quota::Client> AllocQuotaClient(); +}; + +inline already_AddRefed<quota::Client> CreateQuotaClient() { + return FileSystemQuotaClientFactory::CreateQuotaClient(); +} + +} // namespace fs +} // namespace mozilla::dom + +#endif // DOM_FS_PARENT_FILESYSTEMQUOTACLIENTFACTORY_H_ diff --git a/dom/fs/parent/FileSystemStreamCallbacks.cpp b/dom/fs/parent/FileSystemStreamCallbacks.cpp new file mode 100644 index 0000000000..fbe9d5f67b --- /dev/null +++ b/dom/fs/parent/FileSystemStreamCallbacks.cpp @@ -0,0 +1,38 @@ +/* -*- 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 "FileSystemStreamCallbacks.h" + +#include "mozilla/dom/quota/RemoteQuotaObjectParent.h" + +namespace mozilla::dom { + +FileSystemStreamCallbacks::FileSystemStreamCallbacks() + : mRemoteQuotaObjectParent(nullptr) {} + +NS_IMPL_ISUPPORTS(FileSystemStreamCallbacks, nsIInterfaceRequestor, + quota::RemoteQuotaObjectParentTracker) + +NS_IMETHODIMP +FileSystemStreamCallbacks::GetInterface(const nsIID& aIID, void** aResult) { + return QueryInterface(aIID, aResult); +} + +void FileSystemStreamCallbacks::RegisterRemoteQuotaObjectParent( + NotNull<quota::RemoteQuotaObjectParent*> aActor) { + MOZ_ASSERT(!mRemoteQuotaObjectParent); + + mRemoteQuotaObjectParent = aActor; +} + +void FileSystemStreamCallbacks::UnregisterRemoteQuotaObjectParent( + NotNull<quota::RemoteQuotaObjectParent*> aActor) { + MOZ_ASSERT(mRemoteQuotaObjectParent); + + mRemoteQuotaObjectParent = nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/fs/parent/FileSystemStreamCallbacks.h b/dom/fs/parent/FileSystemStreamCallbacks.h new file mode 100644 index 0000000000..98e4713faf --- /dev/null +++ b/dom/fs/parent/FileSystemStreamCallbacks.h @@ -0,0 +1,38 @@ +/* -*- 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_FILESYSTEMSTREAMCALLBACKS_H_ +#define DOM_FS_PARENT_FILESYSTEMSTREAMCALLBACKS_H_ + +#include "mozilla/dom/quota/RemoteQuotaObjectParentTracker.h" +#include "nsIInterfaceRequestor.h" + +namespace mozilla::dom { + +class FileSystemStreamCallbacks : public nsIInterfaceRequestor, + public quota::RemoteQuotaObjectParentTracker { + public: + FileSystemStreamCallbacks(); + + NS_DECL_THREADSAFE_ISUPPORTS + + NS_DECL_NSIINTERFACEREQUESTOR + + void RegisterRemoteQuotaObjectParent( + NotNull<quota::RemoteQuotaObjectParent*> aActor) override; + + void UnregisterRemoteQuotaObjectParent( + NotNull<quota::RemoteQuotaObjectParent*> aActor) override; + + protected: + virtual ~FileSystemStreamCallbacks() = default; + + quota::RemoteQuotaObjectParent* mRemoteQuotaObjectParent; +}; + +} // namespace mozilla::dom + +#endif // DOM_FS_PARENT_FILESYSTEMSTREAMCALLBACKS_H_ diff --git a/dom/fs/parent/FileSystemWritableFileStreamParent.cpp b/dom/fs/parent/FileSystemWritableFileStreamParent.cpp new file mode 100644 index 0000000000..f0e9a3852c --- /dev/null +++ b/dom/fs/parent/FileSystemWritableFileStreamParent.cpp @@ -0,0 +1,85 @@ +/* -*- 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 "FileSystemWritableFileStreamParent.h" + +#include "FileSystemDataManager.h" +#include "FileSystemStreamCallbacks.h" +#include "mozilla/dom/FileSystemLog.h" +#include "mozilla/dom/FileSystemManagerParent.h" +#include "mozilla/dom/quota/RemoteQuotaObjectParent.h" + +namespace mozilla::dom { + +class FileSystemWritableFileStreamParent::FileSystemWritableFileStreamCallbacks + : public FileSystemStreamCallbacks { + public: + void CloseRemoteQuotaObjectParent() { + if (mRemoteQuotaObjectParent) { + mRemoteQuotaObjectParent->Close(); + } + } +}; + +FileSystemWritableFileStreamParent::FileSystemWritableFileStreamParent( + RefPtr<FileSystemManagerParent> aManager, const fs::EntryId& aEntryId, + const fs::FileId& aTemporaryFileId, bool aIsExclusive) + : mManager(std::move(aManager)), + mEntryId(aEntryId), + mTemporaryFileId(aTemporaryFileId), + mIsExclusive(aIsExclusive) {} + +FileSystemWritableFileStreamParent::~FileSystemWritableFileStreamParent() { + MOZ_ASSERT(mClosed); +} + +mozilla::ipc::IPCResult FileSystemWritableFileStreamParent::RecvClose( + bool aAbort, CloseResolver&& aResolver) { + Close(aAbort); + + aResolver(void_t()); + + return IPC_OK(); +} + +void FileSystemWritableFileStreamParent::ActorDestroy(ActorDestroyReason aWhy) { + if (mStreamCallbacks) { + mStreamCallbacks->CloseRemoteQuotaObjectParent(); + mStreamCallbacks = nullptr; + } + + if (!IsClosed()) { + Close(/* aAbort */ true); + } +} + +nsIInterfaceRequestor* +FileSystemWritableFileStreamParent::GetOrCreateStreamCallbacks() { + if (!mStreamCallbacks) { + if (mClosed) { + return nullptr; + } + + mStreamCallbacks = MakeRefPtr<FileSystemWritableFileStreamCallbacks>(); + } + + return mStreamCallbacks.get(); +} + +void FileSystemWritableFileStreamParent::Close(bool aAbort) { + LOG(("Closing WritableFileStream")); + + mClosed.Flip(); + + if (mIsExclusive) { + mManager->DataManagerStrongRef()->UnlockExclusive(mEntryId); + } else { + mManager->DataManagerStrongRef()->UnlockShared(mEntryId, mTemporaryFileId, + aAbort); + } +} + +} // namespace mozilla::dom diff --git a/dom/fs/parent/FileSystemWritableFileStreamParent.h b/dom/fs/parent/FileSystemWritableFileStreamParent.h new file mode 100644 index 0000000000..bf24f5d146 --- /dev/null +++ b/dom/fs/parent/FileSystemWritableFileStreamParent.h @@ -0,0 +1,62 @@ +/* -*- 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_FILESYSTEMWRITABLEFILESTREAM_H_ +#define DOM_FS_PARENT_FILESYSTEMWRITABLEFILESTREAM_H_ + +#include "mozilla/dom/FileSystemParentTypes.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/FlippedOnce.h" +#include "mozilla/dom/PFileSystemWritableFileStreamParent.h" + +class nsIInterfaceRequestor; + +namespace mozilla::dom { + +class FileSystemManagerParent; + +class FileSystemWritableFileStreamParent + : public PFileSystemWritableFileStreamParent { + public: + FileSystemWritableFileStreamParent(RefPtr<FileSystemManagerParent> aManager, + const fs::EntryId& aEntryId, + const fs::FileId& aTemporaryFileId, + bool aIsExclusive); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileSystemWritableFileStreamParent, + override) + + mozilla::ipc::IPCResult RecvClose(bool aAbort, CloseResolver&& aResolver); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + nsIInterfaceRequestor* GetOrCreateStreamCallbacks(); + + private: + class FileSystemWritableFileStreamCallbacks; + + virtual ~FileSystemWritableFileStreamParent(); + + bool IsClosed() const { return mClosed; } + + void Close(bool aAbort); + + const RefPtr<FileSystemManagerParent> mManager; + + RefPtr<FileSystemWritableFileStreamCallbacks> mStreamCallbacks; + + const fs::EntryId mEntryId; + + const fs::FileId mTemporaryFileId; + + const bool mIsExclusive; + + FlippedOnce<false> mClosed; +}; + +} // namespace mozilla::dom + +#endif // DOM_FS_PARENT_FILESYSTEMWRITABLEFILESTREAM_H_ diff --git a/dom/fs/parent/ResultConnection.h b/dom/fs/parent/ResultConnection.h new file mode 100644 index 0000000000..3606a55a6b --- /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<mozIStorageConnection>; + +} // 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 0000000000..0b0e2cf4a8 --- /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, QMResult> ResultStatement::Create( + const ResultConnection& aConnection, const nsACString& aSQLStatement) { + nsCOMPtr<mozIStorageStatement> 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 0000000000..3e532f3ae4 --- /dev/null +++ b/dom/fs/parent/ResultStatement.h @@ -0,0 +1,173 @@ +/* -*- 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 "FileSystemParentTypes.h" +#include "mozIStorageStatement.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "nsCOMPtr.h" +#include "nsString.h" + +class mozIStorageConnection; + +namespace mozilla::dom::fs { + +using Column = uint32_t; + +using ResultConnection = nsCOMPtr<mozIStorageConnection>; + +/** + * @brief ResultStatement + * - provides error monad Result<T, E> 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<mozIStorageStatement>; + + 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<ResultStatement, QMResult> 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 BindFileIdByName(const nsACString& aField, + const FileId& aValue) { + return mStmt->BindUTF8StringAsBlobByName(aField, aValue.Value()); + } + + inline nsresult BindContentTypeByName(const nsACString& aField, + const ContentType& aValue) { + if (aValue.IsVoid()) { + return mStmt->BindNullByName(aField); + } + + return mStmt->BindUTF8StringByName(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 nsresult BindBooleanByName(const nsACString& aField, bool aValue) { + return mStmt->BindInt32ByName(aField, aValue ? 1 : 0); + } + + inline Result<bool, QMResult> GetBooleanByColumn(Column aColumn) { + int32_t value = 0; + QM_TRY(QM_TO_RESULT(mStmt->GetInt32(aColumn, &value))); + + return 0 != value; + } + + inline Result<ContentType, QMResult> GetContentTypeByColumn(Column aColumn) { + ContentType value; + QM_TRY(QM_TO_RESULT(mStmt->GetUTF8String(aColumn, value))); + + return value; + } + + inline Result<EntryId, QMResult> GetEntryIdByColumn(Column aColumn) { + EntryId value; + QM_TRY(QM_TO_RESULT(mStmt->GetBlobAsUTF8String(aColumn, value))); + + return value; + } + + inline Result<FileId, QMResult> GetFileIdByColumn(Column aColumn) { + nsCString value; + QM_TRY(QM_TO_RESULT(mStmt->GetBlobAsUTF8String(aColumn, value))); + + return FileId(std::move(value)); + } + + inline Result<Name, QMResult> GetNameByColumn(Column aColumn) { + Name value; + QM_TRY(QM_TO_RESULT(mStmt->GetBlobAsString(aColumn, value))); + + return value; + } + + inline Result<Usage, QMResult> 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<bool, QMResult> ExecuteStep() { + bool hasEntries = false; + QM_TRY(QM_TO_RESULT(mStmt->ExecuteStep(&hasEntries))); + + return hasEntries; + } + + inline Result<bool, QMResult> YesOrNoQuery() { + bool hasEntries = false; + QM_TRY(QM_TO_RESULT(mStmt->ExecuteStep(&hasEntries))); + MOZ_ALWAYS_TRUE(hasEntries); + return GetBooleanByColumn(0u); + } + + private: + underlying_t mStmt; +}; + +} // namespace mozilla::dom::fs + +#endif // DOM_FS_PARENT_RESULTSTATEMENT_H_ diff --git a/dom/fs/parent/StartedTransaction.cpp b/dom/fs/parent/StartedTransaction.cpp new file mode 100644 index 0000000000..7fee51e61e --- /dev/null +++ b/dom/fs/parent/StartedTransaction.cpp @@ -0,0 +1,35 @@ +/* -*- 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 "StartedTransaction.h" + +#include "ResultConnection.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" + +namespace mozilla::dom::fs { + +/* static */ +Result<StartedTransaction, QMResult> StartedTransaction::Create( + const ResultConnection& aConn) { + auto transaction = MakeUnique<mozStorageTransaction>( + aConn.get(), /* aCommitOnComplete */ false, + mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(QM_TO_RESULT(transaction->Start())); + + return StartedTransaction(std::move(transaction)); +} + +nsresult StartedTransaction::Commit() { return mTransaction->Commit(); } + +nsresult StartedTransaction::Rollback() { return mTransaction->Rollback(); } + +StartedTransaction::StartedTransaction( + UniquePtr<mozStorageTransaction>&& aTransaction) + : mTransaction(std::move(aTransaction)) {} + +} // namespace mozilla::dom::fs diff --git a/dom/fs/parent/StartedTransaction.h b/dom/fs/parent/StartedTransaction.h new file mode 100644 index 0000000000..95a01326bb --- /dev/null +++ b/dom/fs/parent/StartedTransaction.h @@ -0,0 +1,39 @@ +/* -*- 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_STARTEDTRANSACTION_H_ +#define DOM_FS_PARENT_STARTEDTRANSACTION_H_ + +#include "ResultConnection.h" +#include "mozStorageHelper.h" +#include "mozilla/dom/QMResult.h" + +namespace mozilla::dom::fs { + +class StartedTransaction { + public: + static Result<StartedTransaction, QMResult> Create( + const ResultConnection& aConn); + + StartedTransaction(StartedTransaction&& aOther) = default; + + StartedTransaction(const StartedTransaction& aOther) = delete; + + nsresult Commit(); + + nsresult Rollback(); + + ~StartedTransaction() = default; + + private: + explicit StartedTransaction(UniquePtr<mozStorageTransaction>&& aTransaction); + + UniquePtr<mozStorageTransaction> mTransaction; +}; + +} // namespace mozilla::dom::fs + +#endif // DOM_FS_PARENT_STARTEDTRANSACTION_H_ diff --git a/dom/fs/parent/datamodel/FileSystemDataManager.cpp b/dom/fs/parent/datamodel/FileSystemDataManager.cpp new file mode 100644 index 0000000000..549a8d5865 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDataManager.cpp @@ -0,0 +1,672 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FileSystemDataManager.h" + +#include "ErrorList.h" +#include "FileSystemDatabaseManager.h" +#include "FileSystemDatabaseManagerVersion001.h" +#include "FileSystemDatabaseManagerVersion002.h" +#include "FileSystemFileManager.h" +#include "FileSystemHashSource.h" +#include "FileSystemParentTypes.h" +#include "ResultStatement.h" +#include "SchemaVersion001.h" +#include "SchemaVersion002.h" +#include "fs/FileSystemConstants.h" +#include "mozIStorageService.h" +#include "mozStorageCID.h" +#include "mozilla/Result.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/dom/FileSystemLog.h" +#include "mozilla/dom/FileSystemManagerParent.h" +#include "mozilla/dom/QMResult.h" +#include "mozilla/dom/quota/ClientImpl.h" +#include "mozilla/dom/quota/DirectoryLock.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/dom/quota/UsageInfo.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "nsBaseHashtable.h" +#include "nsCOMPtr.h" +#include "nsHashKeys.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "nsNetCID.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsThreadUtils.h" + +namespace mozilla::dom::fs::data { + +namespace { + +// nsCStringHashKey with disabled memmove +class nsCStringHashKeyDM : public nsCStringHashKey { + public: + explicit nsCStringHashKeyDM(const nsCStringHashKey::KeyTypePointer aKey) + : nsCStringHashKey(aKey) {} + enum { ALLOW_MEMMOVE = false }; +}; + +// When CheckedUnsafePtr's checking is enabled, it's necessary to ensure that +// the hashtable uses the copy constructor instead of memmove for moving entries +// since memmove will break CheckedUnsafePtr in a memory-corrupting way. +using FileSystemDataManagerHashKey = + std::conditional<DiagnosticAssertEnabled::value, nsCStringHashKeyDM, + nsCStringHashKey>::type; + +// Raw (but checked when the diagnostic assert is enabled) references as we +// don't want to keep FileSystemDataManager objects alive forever. When a +// FileSystemDataManager is destroyed it calls RemoveFileSystemDataManager +// to clear itself. +using FileSystemDataManagerHashtable = + nsBaseHashtable<FileSystemDataManagerHashKey, + NotNull<CheckedUnsafePtr<FileSystemDataManager>>, + MovingNotNull<CheckedUnsafePtr<FileSystemDataManager>>>; + +// This hashtable isn't protected by any mutex but it is only ever touched on +// the PBackground thread. +StaticAutoPtr<FileSystemDataManagerHashtable> gDataManagers; + +RefPtr<FileSystemDataManager> GetFileSystemDataManager(const Origin& aOrigin) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (gDataManagers) { + auto maybeDataManager = gDataManagers->MaybeGet(aOrigin); + if (maybeDataManager) { + RefPtr<FileSystemDataManager> result( + std::move(*maybeDataManager).unwrapBasePtr()); + return result; + } + } + + return nullptr; +} + +void AddFileSystemDataManager( + const Origin& aOrigin, const RefPtr<FileSystemDataManager>& aDataManager) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(!quota::QuotaManager::IsShuttingDown()); + + if (!gDataManagers) { + gDataManagers = new FileSystemDataManagerHashtable(); + } + + MOZ_ASSERT(!gDataManagers->Contains(aOrigin)); + gDataManagers->InsertOrUpdate(aOrigin, + WrapMovingNotNullUnchecked(aDataManager)); +} + +void RemoveFileSystemDataManager(const Origin& aOrigin) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + MOZ_ASSERT(gDataManagers); + const DebugOnly<bool> removed = gDataManagers->Remove(aOrigin); + MOZ_ASSERT(removed); + + if (!gDataManagers->Count()) { + gDataManagers = nullptr; + } +} + +} // namespace + +Result<ResultConnection, QMResult> GetStorageConnection( + const quota::OriginMetadata& aOriginMetadata, + const int64_t aDirectoryLockId) { + MOZ_ASSERT(aDirectoryLockId >= -1); + + // Ensure that storage is initialized and file system folder exists! + QM_TRY_INSPECT(const auto& dbFileUrl, + GetDatabaseFileURL(aOriginMetadata, aDirectoryLockId)); + + QM_TRY_INSPECT( + const auto& storageService, + QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_GET_TYPED( + nsCOMPtr<mozIStorageService>, MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID))); + + QM_TRY_UNWRAP(auto connection, + QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, storageService, + OpenDatabaseWithFileURL, dbFileUrl, ""_ns, + mozIStorageService::CONNECTION_DEFAULT))); + + ResultConnection result(connection); + + return result; +} + +Result<EntryId, QMResult> GetRootHandle(const Origin& origin) { + MOZ_ASSERT(!origin.IsEmpty()); + + return FileSystemHashSource::GenerateHash(origin, kRootString); +} + +Result<EntryId, QMResult> GetEntryHandle( + const FileSystemChildMetadata& aHandle) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + return FileSystemHashSource::GenerateHash(aHandle.parentId(), + aHandle.childName()); +} + +FileSystemDataManager::FileSystemDataManager( + const quota::OriginMetadata& aOriginMetadata, + RefPtr<quota::QuotaManager> aQuotaManager, + MovingNotNull<nsCOMPtr<nsIEventTarget>> aIOTarget, + MovingNotNull<RefPtr<TaskQueue>> aIOTaskQueue) + : mOriginMetadata(aOriginMetadata), + mQuotaManager(std::move(aQuotaManager)), + mBackgroundTarget(WrapNotNull(GetCurrentSerialEventTarget())), + mIOTarget(std::move(aIOTarget)), + mIOTaskQueue(std::move(aIOTaskQueue)), + mRegCount(0), + mVersion(0), + mState(State::Initial) {} + +FileSystemDataManager::~FileSystemDataManager() { + NS_ASSERT_OWNINGTHREAD(FileSystemDataManager); + MOZ_ASSERT(mState == State::Closed); + MOZ_ASSERT(!mDatabaseManager); +} + +RefPtr<FileSystemDataManager::CreatePromise> +FileSystemDataManager::GetOrCreateFileSystemDataManager( + const quota::OriginMetadata& aOriginMetadata) { + if (quota::QuotaManager::IsShuttingDown()) { + return CreatePromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + if (RefPtr<FileSystemDataManager> dataManager = + GetFileSystemDataManager(aOriginMetadata.mOrigin)) { + if (dataManager->IsOpening()) { + // We have to wait for the open to be finished before resolving the + // promise. The manager can't close itself in the meantime because we + // add a new registration in the lambda capture list. + return dataManager->OnOpen()->Then( + GetCurrentSerialEventTarget(), __func__, + [dataManager = Registered<FileSystemDataManager>(dataManager)]( + const BoolPromise::ResolveOrRejectValue& aValue) { + if (aValue.IsReject()) { + return CreatePromise::CreateAndReject(aValue.RejectValue(), + __func__); + } + return CreatePromise::CreateAndResolve(dataManager, __func__); + }); + } + + if (dataManager->IsClosing()) { + // First, we need to wait for the close to be finished. After that the + // manager is closed and it can't be opened again. The only option is + // to create a new manager and open it. However, all this stuff is + // asynchronous, so it can happen that something else called + // `GetOrCreateFileSystemManager` in the meantime. For that reason, we + // shouldn't try to create a new manager and open it here, a "recursive" + // call to `GetOrCreateFileSystemManager` will handle any new situation. + return dataManager->OnClose()->Then( + GetCurrentSerialEventTarget(), __func__, + [aOriginMetadata](const BoolPromise::ResolveOrRejectValue&) { + return GetOrCreateFileSystemDataManager(aOriginMetadata); + }); + } + + return CreatePromise::CreateAndResolve( + Registered<FileSystemDataManager>(std::move(dataManager)), __func__); + } + + RefPtr<quota::QuotaManager> quotaManager = quota::QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY_UNWRAP(auto streamTransportService, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIEventTarget>, + MOZ_SELECT_OVERLOAD(do_GetService), + NS_STREAMTRANSPORTSERVICE_CONTRACTID), + CreatePromise::CreateAndReject(NS_ERROR_FAILURE, __func__)); + + nsCString taskQueueName("OPFS "_ns + aOriginMetadata.mOrigin); + + RefPtr<TaskQueue> ioTaskQueue = + TaskQueue::Create(do_AddRef(streamTransportService), taskQueueName.get()); + + auto dataManager = MakeRefPtr<FileSystemDataManager>( + aOriginMetadata, std::move(quotaManager), + WrapMovingNotNull(streamTransportService), + WrapMovingNotNull(ioTaskQueue)); + + AddFileSystemDataManager(aOriginMetadata.mOrigin, dataManager); + + return dataManager->BeginOpen()->Then( + GetCurrentSerialEventTarget(), __func__, + [dataManager = Registered<FileSystemDataManager>(dataManager)]( + const BoolPromise::ResolveOrRejectValue& aValue) { + if (aValue.IsReject()) { + return CreatePromise::CreateAndReject(aValue.RejectValue(), __func__); + } + + return CreatePromise::CreateAndResolve(dataManager, __func__); + }); +} + +// static +void FileSystemDataManager::AbortOperationsForLocks( + const quota::Client::DirectoryLockIdTable& aDirectoryLockIds) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + // XXX Share the iteration code with `InitiateShutdown`, for example by + // creating a helper function which would take a predicate function. + + if (!gDataManagers) { + return; + } + + for (const auto& dataManager : gDataManagers->Values()) { + // Check if the Manager holds an acquired DirectoryLock. Origin clearing + // can't be blocked by this Manager if there is no acquired DirectoryLock. + // If there is an acquired DirectoryLock, check if the table contains the + // lock for the Manager. + if (quota::Client::IsLockForObjectAcquiredAndContainedInLockTable( + *dataManager, aDirectoryLockIds)) { + dataManager->RequestAllowToClose(); + } + } +} + +// static +void FileSystemDataManager::InitiateShutdown() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (!gDataManagers) { + return; + } + + for (const auto& dataManager : gDataManagers->Values()) { + dataManager->RequestAllowToClose(); + } +} + +// static +bool FileSystemDataManager::IsShutdownCompleted() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + return !gDataManagers; +} + +void FileSystemDataManager::AssertIsOnIOTarget() const { + DebugOnly<bool> current = false; + MOZ_ASSERT(NS_SUCCEEDED(mIOTarget->IsOnCurrentThread(¤t))); + 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); + +#ifdef DEBUG + aActor->SetRegistered(true); +#endif +} + +void FileSystemDataManager::UnregisterActor( + NotNull<FileSystemManagerParent*> aActor) { + MOZ_ASSERT(mBackgroundThreadAccessible.Access()->mActors.Contains(aActor)); + + mBackgroundThreadAccessible.Access()->mActors.Remove(aActor); + +#ifdef DEBUG + aActor->SetRegistered(false); +#endif + + if (IsInactive()) { + BeginClose(); + } +} + +void FileSystemDataManager::RegisterAccessHandle( + NotNull<FileSystemAccessHandle*> aAccessHandle) { + MOZ_ASSERT(!mBackgroundThreadAccessible.Access()->mAccessHandles.Contains( + aAccessHandle)); + + mBackgroundThreadAccessible.Access()->mAccessHandles.Insert(aAccessHandle); +} + +void FileSystemDataManager::UnregisterAccessHandle( + NotNull<FileSystemAccessHandle*> aAccessHandle) { + MOZ_ASSERT(mBackgroundThreadAccessible.Access()->mAccessHandles.Contains( + aAccessHandle)); + + mBackgroundThreadAccessible.Access()->mAccessHandles.Remove(aAccessHandle); + + if (IsInactive()) { + BeginClose(); + } +} + +RefPtr<BoolPromise> FileSystemDataManager::OnOpen() { + MOZ_ASSERT(mState == State::Opening); + + return mOpenPromiseHolder.Ensure(__func__); +} + +RefPtr<BoolPromise> FileSystemDataManager::OnClose() { + MOZ_ASSERT(mState == State::Closing); + + return mClosePromiseHolder.Ensure(__func__); +} + +// Note: Input can be temporary or main file id +Result<bool, QMResult> FileSystemDataManager::IsLocked( + const FileId& aFileId) const { + auto checkIfEntryIdIsLocked = [this, &aFileId]() -> Result<bool, QMResult> { + QM_TRY_INSPECT(const EntryId& entryId, + mDatabaseManager->GetEntryId(aFileId)); + + return IsLocked(entryId); + }; + + auto valueToSome = [](auto aValue) { return Some(std::move(aValue)); }; + + QM_TRY_UNWRAP(Maybe<bool> maybeLocked, + QM_OR_ELSE_LOG_VERBOSE_IF( + // Expression. + (checkIfEntryIdIsLocked().map(valueToSome)), + // Predicate. + IsSpecificError<NS_ERROR_DOM_NOT_FOUND_ERR>, + // Fallback. + ([](const auto&) -> Result<Maybe<bool>, QMResult> { + return Some(false); // Non-existent files are not locked. + }))); + + if (!maybeLocked) { + // If the metadata is inaccessible, we block modifications. + return true; + } + + return *maybeLocked; +} + +Result<bool, QMResult> FileSystemDataManager::IsLocked( + const EntryId& aEntryId) const { + return mExclusiveLocks.Contains(aEntryId) || mSharedLocks.Contains(aEntryId); +} + +Result<FileId, QMResult> FileSystemDataManager::LockExclusive( + const EntryId& aEntryId) { + QM_TRY_UNWRAP(const bool isLocked, IsLocked(aEntryId)); + if (isLocked) { + return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR)); + } + + QM_TRY_INSPECT(const FileId& fileId, + mDatabaseManager->EnsureFileId(aEntryId)); + + // If the file has been removed, we should get a file not found error. + // Otherwise, if usage tracking cannot be started because file size is not + // known and attempts to read it are failing, lock is denied to freeze the + // quota usage until the (external) blocker is gone or the file is removed. + QM_TRY(QM_TO_RESULT(mDatabaseManager->BeginUsageTracking(fileId))); + + LOG_VERBOSE(("ExclusiveLock")); + mExclusiveLocks.Insert(aEntryId); + + return fileId; +} + +// TODO: Improve reporting of failures, see bug 1840811. +void FileSystemDataManager::UnlockExclusive(const EntryId& aEntryId) { + MOZ_ASSERT(mExclusiveLocks.Contains(aEntryId)); + + LOG_VERBOSE(("ExclusiveUnlock")); + mExclusiveLocks.Remove(aEntryId); + + QM_TRY_INSPECT(const FileId& fileId, mDatabaseManager->GetFileId(aEntryId), + QM_VOID); + + // On error, usage tracking remains on to prevent writes until usage is + // updated successfully. + QM_TRY(MOZ_TO_RESULT(mDatabaseManager->UpdateUsage(fileId)), QM_VOID); + QM_TRY(MOZ_TO_RESULT(mDatabaseManager->EndUsageTracking(fileId)), QM_VOID); +} + +Result<FileId, QMResult> FileSystemDataManager::LockShared( + const EntryId& aEntryId) { + if (mExclusiveLocks.Contains(aEntryId)) { + return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR)); + } + + auto& count = mSharedLocks.LookupOrInsert(aEntryId); + if (!(1u + CheckedUint32(count)).isValid()) { // don't make the count invalid + return Err(QMResult(NS_ERROR_UNEXPECTED)); + } + + QM_TRY_INSPECT(const FileId& fileId, + mDatabaseManager->EnsureTemporaryFileId(aEntryId)); + + // If the file has been removed, we should get a file not found error. + // Otherwise, if usage tracking cannot be started because file size is not + // known and attempts to read it are failing, lock is denied to freeze the + // quota usage until the (external) blocker is gone or the file is removed. + QM_TRY(QM_TO_RESULT(mDatabaseManager->BeginUsageTracking(fileId))); + + ++count; + LOG_VERBOSE(("SharedLock %u", count)); + + return fileId; +} + +// TODO: Improve reporting of failures, see bug 1840811. +void FileSystemDataManager::UnlockShared(const EntryId& aEntryId, + const FileId& aFileId, bool aAbort) { + MOZ_ASSERT(!mExclusiveLocks.Contains(aEntryId)); + MOZ_ASSERT(mSharedLocks.Contains(aEntryId)); + + auto entry = mSharedLocks.Lookup(aEntryId); + MOZ_ASSERT(entry); + + MOZ_ASSERT(entry.Data() > 0); + --entry.Data(); + + LOG_VERBOSE(("SharedUnlock %u", *entry)); + + if (0u == entry.Data()) { + entry.Remove(); + } + + // On error, usage tracking remains on to prevent writes until usage is + // updated successfully. + QM_TRY(MOZ_TO_RESULT(mDatabaseManager->UpdateUsage(aFileId)), QM_VOID); + QM_TRY(MOZ_TO_RESULT(mDatabaseManager->EndUsageTracking(aFileId)), QM_VOID); + QM_TRY( + MOZ_TO_RESULT(mDatabaseManager->MergeFileId(aEntryId, aFileId, aAbort)), + QM_VOID); +} + +FileMode FileSystemDataManager::GetMode(bool aKeepData) const { + if (1 == mVersion) { + return FileMode::EXCLUSIVE; + } + + return aKeepData ? FileMode::SHARED_FROM_COPY : FileMode::SHARED_FROM_EMPTY; +} + +bool FileSystemDataManager::IsInactive() const { + auto data = mBackgroundThreadAccessible.Access(); + return !mRegCount && !data->mActors.Count() && !data->mAccessHandles.Count(); +} + +void FileSystemDataManager::RequestAllowToClose() { + for (const auto& actor : mBackgroundThreadAccessible.Access()->mActors) { + actor->RequestAllowToClose(); + } +} + +RefPtr<BoolPromise> FileSystemDataManager::BeginOpen() { + MOZ_ASSERT(mQuotaManager); + MOZ_ASSERT(mState == State::Initial); + + mState = State::Opening; + + mQuotaManager + ->OpenClientDirectory( + {mOriginMetadata, mozilla::dom::quota::Client::FILESYSTEM}) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self = RefPtr<FileSystemDataManager>(this)]( + quota::ClientDirectoryLockPromise::ResolveOrRejectValue&& value) { + if (value.IsReject()) { + return BoolPromise::CreateAndReject(value.RejectValue(), + __func__); + } + + self->mDirectoryLock = std::move(value.ResolveValue()); + + return BoolPromise::CreateAndResolve(true, __func__); + }) + ->Then(mQuotaManager->IOThread(), __func__, + [self = RefPtr<FileSystemDataManager>(this)]( + const BoolPromise::ResolveOrRejectValue& value) { + if (value.IsReject()) { + return BoolPromise::CreateAndReject(value.RejectValue(), + __func__); + } + + QM_TRY(MOZ_TO_RESULT( + EnsureFileSystemDirectory(self->mOriginMetadata)), + CreateAndRejectBoolPromise); + + return BoolPromise::CreateAndResolve(true, __func__); + }) + ->Then( + MutableIOTaskQueuePtr(), __func__, + [self = RefPtr<FileSystemDataManager>(this)]( + const BoolPromise::ResolveOrRejectValue& value) { + if (value.IsReject()) { + return BoolPromise::CreateAndReject(value.RejectValue(), + __func__); + } + + QM_TRY_UNWRAP(auto connection, + GetStorageConnection(self->mOriginMetadata, + self->mDirectoryLock->Id()), + CreateAndRejectBoolPromiseFromQMResult); + + QM_TRY_UNWRAP(UniquePtr<FileSystemFileManager> fmPtr, + FileSystemFileManager::CreateFileSystemFileManager( + self->mOriginMetadata), + CreateAndRejectBoolPromiseFromQMResult); + + QM_TRY_UNWRAP( + self->mVersion, + QM_OR_ELSE_WARN_IF( + // Expression. + SchemaVersion002::InitializeConnection( + connection, *fmPtr, self->mOriginMetadata.mOrigin), + // Predicate. + ([](const auto&) { return true; }), + // Fallback. + ([&self, &connection](const auto&) { + QM_TRY_RETURN(SchemaVersion001::InitializeConnection( + connection, self->mOriginMetadata.mOrigin)); + })), + CreateAndRejectBoolPromiseFromQMResult); + + QM_TRY_UNWRAP( + EntryId rootId, + fs::data::GetRootHandle(self->mOriginMetadata.mOrigin), + CreateAndRejectBoolPromiseFromQMResult); + + switch (self->mVersion) { + case 1: { + self->mDatabaseManager = + MakeUnique<FileSystemDatabaseManagerVersion001>( + self, std::move(connection), std::move(fmPtr), rootId); + break; + } + + case 2: { + self->mDatabaseManager = + MakeUnique<FileSystemDatabaseManagerVersion002>( + self, std::move(connection), std::move(fmPtr), rootId); + break; + } + + default: + break; + } + + return BoolPromise::CreateAndResolve(true, __func__); + }) + ->Then(GetCurrentSerialEventTarget(), __func__, + [self = RefPtr<FileSystemDataManager>(this)]( + const BoolPromise::ResolveOrRejectValue& value) { + if (value.IsReject()) { + self->mState = State::Initial; + + self->mOpenPromiseHolder.RejectIfExists(value.RejectValue(), + __func__); + + } else { + self->mState = State::Open; + + self->mOpenPromiseHolder.ResolveIfExists(true, __func__); + } + }); + + return OnOpen(); +} + +RefPtr<BoolPromise> FileSystemDataManager::BeginClose() { + MOZ_ASSERT(mState != State::Closing && mState != State::Closed); + MOZ_ASSERT(IsInactive()); + + mState = State::Closing; + + InvokeAsync(MutableIOTaskQueuePtr(), __func__, + [self = RefPtr<FileSystemDataManager>(this)]() { + if (self->mDatabaseManager) { + self->mDatabaseManager->Close(); + self->mDatabaseManager = nullptr; + } + + return BoolPromise::CreateAndResolve(true, __func__); + }) + ->Then(MutableBackgroundTargetPtr(), __func__, + [self = RefPtr<FileSystemDataManager>(this)]( + const BoolPromise::ResolveOrRejectValue&) { + return self->mIOTaskQueue->BeginShutdown(); + }) + ->Then(MutableBackgroundTargetPtr(), __func__, + [self = RefPtr<FileSystemDataManager>(this)]( + const ShutdownPromise::ResolveOrRejectValue&) { + self->mDirectoryLock = nullptr; + + RemoveFileSystemDataManager(self->mOriginMetadata.mOrigin); + + self->mState = State::Closed; + + self->mClosePromiseHolder.ResolveIfExists(true, __func__); + }); + + return OnClose(); +} + +} // namespace mozilla::dom::fs::data diff --git a/dom/fs/parent/datamodel/FileSystemDataManager.h b/dom/fs/parent/datamodel/FileSystemDataManager.h new file mode 100644 index 0000000000..ab0c603700 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDataManager.h @@ -0,0 +1,186 @@ +/* -*- 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 "FileSystemParentTypes.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 FileSystemAccessHandle; +class FileSystemManagerParent; + +namespace fs { +struct FileId; +class FileSystemChildMetadata; +} // namespace fs + +namespace quota { +class DirectoryLock; +class QuotaManager; +} // namespace quota + +namespace fs::data { + +class FileSystemDatabaseManager; + +Result<EntryId, QMResult> GetRootHandle(const Origin& origin); + +Result<EntryId, QMResult> GetEntryHandle( + const FileSystemChildMetadata& aHandle); + +Result<ResultConnection, QMResult> GetStorageConnection( + const quota::OriginMetadata& aOriginMetadata, + const int64_t aDirectoryLockId); + +class FileSystemDataManager + : public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> { + public: + enum struct State : uint8_t { Initial = 0, Opening, Open, Closing, Closed }; + + FileSystemDataManager(const quota::OriginMetadata& aOriginMetadata, + RefPtr<quota::QuotaManager> aQuotaManager, + 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_THREADSAFE_REFCOUNTING(FileSystemDataManager) + + void AssertIsOnIOTarget() const; + + const quota::OriginMetadata& OriginMetadataRef() const { + return mOriginMetadata; + } + + nsISerialEventTarget* MutableBackgroundTargetPtr() const { + return mBackgroundTarget.get(); + } + + nsIEventTarget* MutableIOTargetPtr() const { return mIOTarget.get(); } + + nsISerialEventTarget* MutableIOTaskQueuePtr() 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); + + void RegisterAccessHandle(NotNull<FileSystemAccessHandle*> aAccessHandle); + + void UnregisterAccessHandle(NotNull<FileSystemAccessHandle*> aAccessHandle); + + bool IsOpen() const { return mState == State::Open; } + + RefPtr<BoolPromise> OnOpen(); + + RefPtr<BoolPromise> OnClose(); + + Result<bool, QMResult> IsLocked(const FileId& aFileId) const; + + Result<bool, QMResult> IsLocked(const EntryId& aEntryId) const; + + Result<FileId, QMResult> LockExclusive(const EntryId& aEntryId); + + void UnlockExclusive(const EntryId& aEntryId); + + Result<FileId, QMResult> LockShared(const EntryId& aEntryId); + + void UnlockShared(const EntryId& aEntryId, const FileId& aFileId, + bool aAbort); + + FileMode GetMode(bool aKeepData) const; + + 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; + nsTHashSet<FileSystemAccessHandle*> mAccessHandles; + }; + ThreadBound<BackgroundThreadAccessible> mBackgroundThreadAccessible; + + const quota::OriginMetadata mOriginMetadata; + nsTHashSet<EntryId> mExclusiveLocks; + nsTHashMap<EntryId, uint32_t> mSharedLocks; + NS_DECL_OWNINGEVENTTARGET + const RefPtr<quota::QuotaManager> mQuotaManager; + 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; + DatabaseVersion mVersion; + 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..2b76a3b09d --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManager.cpp @@ -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/. */ + +#include "FileSystemDatabaseManager.h" + +#include "FileSystemDatabaseManagerVersion001.h" +#include "FileSystemDatabaseManagerVersion002.h" +#include "FileSystemFileManager.h" +#include "ResultConnection.h" +#include "ResultStatement.h" +#include "SchemaVersion001.h" +#include "SchemaVersion002.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "nsCOMPtr.h" +#include "nsIFile.h" + +namespace mozilla::dom::fs::data { + +namespace { + +Result<Usage, QMResult> GetFileUsage(const ResultConnection& aConnection) { + DatabaseVersion version = 0; + QM_TRY(QM_TO_RESULT(aConnection->GetSchemaVersion(&version))); + + switch (version) { + case 0: { + return 0; + } + + case 1: { + QM_TRY_RETURN( + FileSystemDatabaseManagerVersion001::GetFileUsage(aConnection)); + } + + case 2: { + QM_TRY_RETURN( + FileSystemDatabaseManagerVersion002::GetFileUsage(aConnection)); + } + + default: + break; + } + + return Err(QMResult(NS_ERROR_NOT_IMPLEMENTED)); +} + +} // namespace + +/* static */ +nsresult FileSystemDatabaseManager::RescanUsages( + const ResultConnection& aConnection, + const quota::OriginMetadata& aOriginMetadata) { + DatabaseVersion version = 0; + QM_TRY(MOZ_TO_RESULT(aConnection->GetSchemaVersion(&version))); + + switch (version) { + case 0: { + return NS_OK; + } + + case 1: + return FileSystemDatabaseManagerVersion001::RescanTrackedUsages( + aConnection, aOriginMetadata); + + case 2: { + return FileSystemDatabaseManagerVersion002::RescanTrackedUsages( + aConnection, aOriginMetadata); + } + + default: + break; + } + + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* static */ +Result<quota::UsageInfo, QMResult> FileSystemDatabaseManager::GetUsage( + const ResultConnection& aConnection, + const quota::OriginMetadata& aOriginMetadata) { + QM_TRY_INSPECT(const auto& databaseFile, GetDatabaseFile(aOriginMetadata)); + + // 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))); + + QM_TRY_INSPECT(const Usage& fileUsage, GetFileUsage(aConnection)); + + // XXX: DatabaseUsage is currently total usage for most forms of storage + result += quota::DatabaseUsageType(Some(fileUsage)); + + return result; +} + +} // 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..b7a4e352fe --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManager.h @@ -0,0 +1,229 @@ +/* -*- 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 { + +namespace quota { + +struct OriginMetadata; + +} // namespace quota + +namespace fs { + +struct FileId; +enum class FileMode; +class FileSystemChildMetadata; +class FileSystemEntryMetadata; +class FileSystemDirectoryListing; +class FileSystemEntryPair; + +namespace data { + +using FileSystemConnection = fs::ResultConnection; + +class FileSystemDatabaseManager { + public: + /** + * @brief Updates stored usage data for all tracked files. + * + * @return nsresult error code + */ + static nsresult RescanUsages(const ResultConnection& aConnection, + const quota::OriginMetadata& aOriginMetadata); + + /** + * @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 quota::OriginMetadata& aOriginMetadata); + + /** + * @brief Refreshes the stored file size. + * + * @param aEntry EntryId of the file whose size is refreshed. + */ + virtual nsresult UpdateUsage(const FileId& aFileId) = 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 + * @param aType Content type which is ignored if the file already exists + * @param aCreate true if file is to be created when it does not already exist + * @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, const FileId& aFileId, + const FileMode& aMode, ContentType& 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<EntryId, QMResult> The relevant entry id or error + */ + virtual Result<EntryId, 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<EntryId, QMResult> The relevant entry id or error + */ + virtual Result<EntryId, 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 Generates an EntryId for a given parent EntryId and filename. + */ + virtual Result<EntryId, QMResult> GetEntryId( + const FileSystemChildMetadata& aHandle) const = 0; + + /** + * @brief To check if a file under a directory is locked, we need to map + * fileId's to entries. + * + * @param aFileId a FileId + * @return Result<EntryId, QMResult> Entry id of a temporary or main file + */ + virtual Result<EntryId, QMResult> GetEntryId(const FileId& aFileId) const = 0; + + /** + * @brief Make sure EntryId maps to a FileId. This method should be called + * before exclusive locking is attempted. + */ + virtual Result<FileId, QMResult> EnsureFileId(const EntryId& aEntryId) = 0; + + /** + * @brief Make sure EntryId maps to a temporary FileId. This method should be + * called before shared locking is attempted. + */ + virtual Result<FileId, QMResult> EnsureTemporaryFileId( + const EntryId& aEntryId) = 0; + + /** + * @brief To support moves in metadata, the actual files on disk are tagged + * with file id's which are mapped to entry id's which represent paths. + * This function returns the main file corresponding to an entry. + * + * @param aEntryId An id of an entry + * @return Result<EntryId, QMResult> Main file id, used by exclusive locks + */ + virtual Result<FileId, QMResult> GetFileId(const EntryId& aEntryId) const = 0; + + /** + * @brief Flag aFileId as the main file for aEntryId or abort. Removes the + * file which did not get flagged as the main file. + */ + virtual nsresult MergeFileId(const EntryId& aEntryId, const FileId& aFileId, + bool aAbort) = 0; + + /** + * @brief Close database connection. + */ + virtual void Close() = 0; + + /** + * @brief Start tracking file's usage. + */ + virtual nsresult BeginUsageTracking(const FileId& aFileId) = 0; + + /** + * @brief Stop tracking file's usage. + */ + virtual nsresult EndUsageTracking(const FileId& aFileId) = 0; + + virtual ~FileSystemDatabaseManager() = default; +}; + +} // namespace data +} // namespace fs +} // namespace dom +} // 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..c65bf01508 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp @@ -0,0 +1,1567 @@ +/* -*- 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 "ErrorList.h" +#include "FileSystemContentTypeGuess.h" +#include "FileSystemDataManager.h" +#include "FileSystemFileManager.h" +#include "FileSystemParentTypes.h" +#include "ResultStatement.h" +#include "StartedTransaction.h" +#include "mozStorageHelper.h" +#include "mozilla/CheckedInt.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/Client.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/QuotaObject.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "nsString.h" + +namespace mozilla::dom { + +using FileSystemEntries = nsTArray<fs::FileSystemEntryMetadata>; + +namespace fs::data { + +namespace { + +constexpr const nsLiteralCString gDescendantsQuery = + "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; + +Result<bool, QMResult> IsDirectoryEmpty(const 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 INNER 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<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 INNER JOIN Entries USING (handle) " + "WHERE Files.name = :name AND Entries.parent = :parent ) " + ";"_ns; + + QM_TRY_RETURN(ApplyEntryExistsQuery(aConnection, existsQuery, aHandle)); +} + +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)); +} + +nsresult PerformRename(const FileSystemConnection& aConnection, + const FileSystemEntryMetadata& aHandle, + const Name& aNewName, const ContentType& aNewType, + 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; + } + + // 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)); + if (!aNewType.IsVoid()) { + QM_TRY(MOZ_TO_RESULT(stmt.BindContentTypeByName("type"_ns, aNewType))); + } + 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, VoidCString(), + updateDirectoryNameQuery); +} + +nsresult PerformRenameFile(const FileSystemConnection& aConnection, + const FileSystemEntryMetadata& aHandle, + const Name& aNewName, const ContentType& aNewType) { + const nsLiteralCString updateFileTypeAndNameQuery = + "UPDATE Files SET type = :type, name = :name " + "WHERE handle = :handle ;"_ns; + + const nsLiteralCString updateFileNameQuery = + "UPDATE Files SET name = :name WHERE handle = :handle ;"_ns; + + if (aNewType.IsVoid()) { + return PerformRename(aConnection, aHandle, aNewName, aNewType, + updateFileNameQuery); + } + + return PerformRename(aConnection, aHandle, aNewName, aNewType, + updateFileTypeAndNameQuery); +} + +template <class HandlerType> +nsresult SetUsageTrackingImpl(const FileSystemConnection& aConnection, + const FileId& aFileId, bool aTracked, + HandlerType&& aOnMissingFile) { + const nsLiteralCString setTrackedQuery = + "INSERT INTO Usages " + "( handle, tracked ) " + "VALUES " + "( :handle, :tracked ) " + "ON CONFLICT(handle) DO " + "UPDATE SET tracked = excluded.tracked " + ";"_ns; + + const nsresult customReturnValue = + aTracked ? NS_ERROR_DOM_NOT_FOUND_ERR : NS_OK; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, setTrackedQuery)); + QM_TRY(MOZ_TO_RESULT(stmt.BindFileIdByName("handle"_ns, aFileId))); + QM_TRY(MOZ_TO_RESULT(stmt.BindBooleanByName("tracked"_ns, aTracked))); + QM_TRY(MOZ_TO_RESULT(stmt.Execute()), customReturnValue, + std::forward<HandlerType>(aOnMissingFile)); + + return NS_OK; +} + +Result<nsTArray<FileId>, QMResult> GetTrackedFiles( + const FileSystemConnection& aConnection) { + // The same query works for both 001 and 002 schemas because handle is + // an entry id and later on a file id, respectively. + static const nsLiteralCString getTrackedFilesQuery = + "SELECT handle FROM Usages WHERE tracked = TRUE;"_ns; + nsTArray<FileId> trackedFiles; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, getTrackedFilesQuery)); + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + + while (moreResults) { + QM_TRY_UNWRAP(FileId fileId, stmt.GetFileIdByColumn(/* Column */ 0u)); + + trackedFiles.AppendElement(fileId); // TODO: fallible? + + QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep()); + } + + return trackedFiles; +} + +/** This handles the file not found error by assigning 0 usage to the dangling + * handle and puts the handle to a non-tracked state. Otherwise, when the + * file or database cannot be reached, the file remains in the tracked state. + */ +template <class QuotaCacheUpdate> +nsresult UpdateUsageForFileEntry(const FileSystemConnection& aConnection, + const FileSystemFileManager& aFileManager, + const FileId& aFileId, + const nsLiteralCString& aUpdateQuery, + QuotaCacheUpdate&& aUpdateCache) { + QM_TRY_INSPECT(const auto& fileHandle, aFileManager.GetFile(aFileId)); + + // A file could have changed in a way which doesn't allow to read its size. + QM_TRY_UNWRAP( + const Usage fileSize, + QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT_INVOKE_MEMBER(fileHandle, GetFileSize), + // Predicate. + ([](const nsresult rv) { return rv == NS_ERROR_FILE_NOT_FOUND; }), + // Fallback. If the file does no longer exist, treat it as 0-sized. + ErrToDefaultOk<Usage>)); + + QM_TRY(MOZ_TO_RESULT(aUpdateCache(fileSize))); + + // No transaction as one statement succeeds or fails atomically + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, aUpdateQuery)); + + QM_TRY(MOZ_TO_RESULT(stmt.BindFileIdByName("handle"_ns, aFileId))); + + QM_TRY(MOZ_TO_RESULT(stmt.BindUsageByName("usage"_ns, fileSize))); + + QM_TRY(MOZ_TO_RESULT(stmt.Execute())); + + return NS_OK; +} + +nsresult UpdateUsageUnsetTracked(const FileSystemConnection& aConnection, + const FileSystemFileManager& aFileManager, + const FileId& aFileId) { + static const nsLiteralCString updateUsagesUnsetTrackedQuery = + "UPDATE Usages SET usage = :usage, tracked = FALSE " + "WHERE handle = :handle;"_ns; + + auto noCacheUpdateNeeded = [](auto) { return NS_OK; }; + + return UpdateUsageForFileEntry(aConnection, aFileManager, aFileId, + updateUsagesUnsetTrackedQuery, + std::move(noCacheUpdateNeeded)); +} + +/** + * @brief Get the recorded usage only if the file is in tracked state. + * During origin initialization, if the usage on disk is unreadable, the latest + * recorded usage is reported to the quota manager for the tracked files. + * To allow writing, we attempt to update the real usage with one database and + * one file size query. + */ +Result<Maybe<Usage>, QMResult> GetMaybeTrackedUsage( + const FileSystemConnection& aConnection, const FileId& aFileId) { + const nsLiteralCString trackedUsageQuery = + "SELECT usage FROM Usages WHERE tracked = TRUE AND handle = :handle " + ");"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, trackedUsageQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("handle"_ns, aFileId))); + + QM_TRY_UNWRAP(const bool moreResults, stmt.ExecuteStep()); + if (!moreResults) { + return Maybe<Usage>(Nothing()); + } + + QM_TRY_UNWRAP(Usage trackedUsage, stmt.GetUsageByColumn(/* Column */ 0u)); + + return Some(trackedUsage); +} + +Result<bool, nsresult> ScanTrackedFiles( + const FileSystemConnection& aConnection, + const FileSystemFileManager& aFileManager) { + QM_TRY_INSPECT(const nsTArray<FileId>& trackedFiles, + GetTrackedFiles(aConnection).mapErr(toNSResult)); + + bool ok = true; + for (const auto& fileId : trackedFiles) { + // On success, tracked is set to false, otherwise its value is kept (= true) + QM_WARNONLY_TRY(MOZ_TO_RESULT(UpdateUsageUnsetTracked( + aConnection, aFileManager, fileId)), + [&ok](const auto& /*aRv*/) { ok = false; }); + } + + return ok; +} + +Result<Ok, QMResult> DeleteEntry(const FileSystemConnection& aConnection, + const EntryId& aEntryId) { + // If it's a directory, deleting the handle will cascade + const nsLiteralCString deleteEntryQuery = + "DELETE FROM Entries " + "WHERE handle = :handle " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, deleteEntryQuery)); + + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + + QM_TRY(QM_TO_RESULT(stmt.Execute())); + + return Ok{}; +} + +Result<int32_t, QMResult> GetTrackedFilesCount( + const FileSystemConnection& aConnection) { + // TODO: We could query the count directly + QM_TRY_INSPECT(const auto& trackedFiles, GetTrackedFiles(aConnection)); + + CheckedInt32 checkedFileCount = trackedFiles.Length(); + QM_TRY(OkIf(checkedFileCount.isValid()), + Err(QMResult(NS_ERROR_ILLEGAL_VALUE))); + + return checkedFileCount.value(); +} + +void LogWithFilename(const FileSystemFileManager& aFileManager, + const char* aFormat, const FileId& aFileId) { + if (!LOG_ENABLED()) { + return; + } + + QM_TRY_INSPECT(const auto& localFile, aFileManager.GetFile(aFileId), QM_VOID); + + nsAutoString localPath; + QM_TRY(MOZ_TO_RESULT(localFile->GetPath(localPath)), QM_VOID); + LOG((aFormat, NS_ConvertUTF16toUTF8(localPath).get())); +} + +Result<bool, QMResult> IsAnyDescendantLocked( + const FileSystemConnection& aConnection, + const FileSystemDataManager& aDataManager, const EntryId& aEntryId) { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, gDescendantsQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + + while (moreResults) { + // Works only for version 001 + QM_TRY_INSPECT(const EntryId& entryId, + stmt.GetEntryIdByColumn(/* Column */ 0u)); + + QM_TRY_UNWRAP(const bool isLocked, aDataManager.IsLocked(entryId), true); + if (isLocked) { + return true; + } + + QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep()); + } + + return false; +} + +} // namespace + +FileSystemDatabaseManagerVersion001::FileSystemDatabaseManagerVersion001( + FileSystemDataManager* aDataManager, FileSystemConnection&& aConnection, + UniquePtr<FileSystemFileManager>&& aFileManager, const EntryId& aRootEntry) + : mDataManager(aDataManager), + mConnection(aConnection), + mFileManager(std::move(aFileManager)), + mRootEntry(aRootEntry), + mClientMetadata(aDataManager->OriginMetadataRef(), + quota::Client::FILESYSTEM), + mFilesOfUnknownUsage(-1) {} + +/* static */ +nsresult FileSystemDatabaseManagerVersion001::RescanTrackedUsages( + const FileSystemConnection& aConnection, + const quota::OriginMetadata& aOriginMetadata) { + QM_TRY_UNWRAP(UniquePtr<FileSystemFileManager> fileManager, + data::FileSystemFileManager::CreateFileSystemFileManager( + aOriginMetadata)); + + QM_TRY_UNWRAP(bool ok, ScanTrackedFiles(aConnection, *fileManager)); + if (ok) { + return NS_OK; + } + + // Retry once without explicit delay + QM_TRY_UNWRAP(ok, ScanTrackedFiles(aConnection, *fileManager)); + if (!ok) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +/* 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::UpdateUsage( + const FileId& aFileId) { + // We don't track directories or non-existent files. + QM_TRY_UNWRAP(bool fileExists, DoesFileIdExist(aFileId).mapErr(toNSResult)); + if (!fileExists) { + return NS_OK; // May be deleted before update, no assert + } + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> file, mFileManager->GetFile(aFileId)); + MOZ_ASSERT(file); + + Usage fileSize = 0; + bool exists = false; + QM_TRY(MOZ_TO_RESULT(file->Exists(&exists))); + if (exists) { + QM_TRY(MOZ_TO_RESULT(file->GetFileSize(&fileSize))); + } + + QM_TRY(MOZ_TO_RESULT(UpdateUsageInDatabase(aFileId, fileSize))); + + 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_INSPECT(const EntryId& entryId, GetEntryId(aHandle)); + MOZ_ASSERT(!entryId.IsEmpty()); + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection)); + + { + 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 + QM_TRY(OkIf(!exists), Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR))); + + QM_TRY_UNWRAP(exists, DoesFileExist(mConnection, aHandle)); + + if (exists) { + QM_TRY_RETURN(FindEntryId(mConnection, aHandle, /* aIsFile */ 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, type, name ) " + "VALUES " + "( :handle, :type, :name ) " + ";"_ns; + + QM_TRY_INSPECT(const EntryId& entryId, GetEntryId(aHandle)); + MOZ_ASSERT(!entryId.IsEmpty()); + + const ContentType type = DetermineContentType(name); + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection)); + + { + 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_PROPAGATE, + ([this, &aHandle](const auto& aRv) { + QM_TRY_UNWRAP(bool parentExists, + DoesDirectoryExist(mConnection, aHandle.parentId()), + QM_VOID); + QM_TRY(OkIf(parentExists), QM_VOID); + })); + } + + { + 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.BindContentTypeByName("type"_ns, type))); + 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; +} + +nsresult FileSystemDatabaseManagerVersion001::GetFile( + const EntryId& aEntryId, const FileId& aFileId, const FileMode& aMode, + ContentType& aType, TimeStamp& lastModifiedMilliSeconds, + nsTArray<Name>& aPath, nsCOMPtr<nsIFile>& aFile) const { + MOZ_ASSERT(!aFileId.IsEmpty()); + MOZ_ASSERT(aMode == FileMode::EXCLUSIVE); + + const FileSystemEntryPair endPoints(mRootEntry, aEntryId); + QM_TRY_UNWRAP(aPath, ResolveReversedPath(mConnection, endPoints)); + if (aPath.IsEmpty()) { + return NS_ERROR_DOM_NOT_FOUND_ERR; + } + + QM_TRY(MOZ_TO_RESULT(GetFileAttributes(mConnection, aEntryId, aType))); + QM_TRY_UNWRAP(aFile, mFileManager->GetOrCreateFile(aFileId)); + + PRTime lastModTime = 0; + QM_TRY(MOZ_TO_RESULT(aFile->GetLastModifiedTime(&lastModTime))); + lastModifiedMilliSeconds = static_cast<TimeStamp>(lastModTime); + + aPath.Reverse(); + + return NS_OK; +} + +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; +} + +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)); + + MOZ_ASSERT(mDataManager); + QM_TRY_UNWRAP(const bool isLocked, + IsAnyDescendantLocked(mConnection, *mDataManager, entryId)); + + QM_TRY(OkIf(!isLocked), + Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR))); + + if (!aRecursive && !isEmpty) { + return Err(QMResult(NS_ERROR_DOM_INVALID_MODIFICATION_ERR)); + } + + QM_TRY_UNWRAP(Usage usage, GetUsagesOfDescendants(entryId)); + + QM_TRY_INSPECT(const nsTArray<FileId>& descendants, + FindFilesUnderEntry(entryId)); + + nsTArray<FileId> failedRemovals; + QM_TRY_UNWRAP(DebugOnly<Usage> removedUsage, + mFileManager->RemoveFiles(descendants, failedRemovals)); + + // Usage is for the current main file but we remove temporary files too. + MOZ_ASSERT_IF(failedRemovals.IsEmpty() && (0 == mFilesOfUnknownUsage), + usage <= removedUsage); + + TryRemoveDuringIdleMaintenance(failedRemovals); + + auto isInFailedRemovals = [&failedRemovals](const auto& aFileId) { + return failedRemovals.cend() != + std::find_if(failedRemovals.cbegin(), failedRemovals.cend(), + [&aFileId](const auto& aFailedRemoval) { + return aFileId == aFailedRemoval; + }); + }; + + for (const auto& fileId : descendants) { + if (!isInFailedRemovals(fileId)) { + QM_WARNONLY_TRY(QM_TO_RESULT(RemoveFileId(fileId))); + } + } + + if (usage > 0) { // Performance! + DecreaseCachedQuotaUsage(usage); + } + + QM_TRY(DeleteEntry(mConnection, entryId)); + + 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 + QM_TRY_UNWRAP(const bool isLocked, mDataManager->IsLocked(entryId)); + if (isLocked) { + LOG(("Trying to remove in-use file")); + return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR)); + } + + QM_TRY_INSPECT(const nsTArray<FileId>& diskItems, + FindFilesUnderEntry(entryId)); + + QM_TRY_UNWRAP(Usage usage, GetUsagesOfDescendants(entryId)); + + nsTArray<FileId> failedRemovals; + QM_TRY_UNWRAP(DebugOnly<Usage> removedUsage, + mFileManager->RemoveFiles(diskItems, failedRemovals)); + + // We only check the most common case. This can fail spuriously if an external + // application writes to the file, or OS reports zero size due to corruption. + MOZ_ASSERT_IF(failedRemovals.IsEmpty() && (0 == mFilesOfUnknownUsage), + usage == removedUsage); + + TryRemoveDuringIdleMaintenance(failedRemovals); + + auto isInFailedRemovals = [&failedRemovals](const auto& aFileId) { + return failedRemovals.cend() != + std::find_if(failedRemovals.cbegin(), failedRemovals.cend(), + [&aFileId](const auto& aFailedRemoval) { + return aFileId == aFailedRemoval; + }); + }; + + for (const auto& fileId : diskItems) { + if (!isInFailedRemovals(fileId)) { + QM_WARNONLY_TRY(QM_TO_RESULT(RemoveFileId(fileId))); + } + } + + if (usage > 0) { // Performance! + DecreaseCachedQuotaUsage(usage); + } + + QM_TRY(DeleteEntry(mConnection, entryId)); + + return true; +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion001::RenameEntry( + const FileSystemEntryMetadata& aHandle, const Name& aNewName) { + const auto& entryId = aHandle.entryId(); + + // Can't rename root + if (mRootEntry == entryId) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + // Verify the source exists + QM_TRY_UNWRAP(bool isFile, IsFile(mConnection, entryId), + Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR))); + + // Are we actually renaming? + if (aHandle.entryName() == aNewName) { + return entryId; + } + + QM_TRY(QM_TO_RESULT(PrepareRenameEntry(mConnection, mDataManager, aHandle, + aNewName, isFile))); + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection)); + + if (isFile) { + const ContentType type = DetermineContentType(aNewName); + QM_TRY( + QM_TO_RESULT(PerformRenameFile(mConnection, aHandle, aNewName, type))); + } else { + QM_TRY( + QM_TO_RESULT(PerformRenameDirectory(mConnection, aHandle, aNewName))); + } + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + return entryId; +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion001::MoveEntry( + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation) { + const auto& entryId = aHandle.entryId(); + MOZ_ASSERT(!entryId.IsEmpty()); + + if (mRootEntry == entryId) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + // Verify the source exists + QM_TRY_UNWRAP(bool isFile, IsFile(mConnection, entryId), + Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR))); + + // If the rename doesn't change the name or directory, just return success. + // XXX Needs to be added to the spec + QM_WARNONLY_TRY_UNWRAP(Maybe<bool> maybeSame, + IsSame(mConnection, aHandle, aNewDesignation, isFile)); + if (maybeSame && maybeSame.value()) { + return entryId; + } + + QM_TRY(QM_TO_RESULT(PrepareMoveEntry(mConnection, mDataManager, aHandle, + aNewDesignation, isFile))); + + const nsLiteralCString updateEntryParentQuery = + "UPDATE Entries " + "SET parent = :parent " + "WHERE handle = :handle " + ";"_ns; + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection)); + + { + // 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())); + } + + const Name& newName = aNewDesignation.childName(); + + // Are we actually renaming? + if (aHandle.entryName() == newName) { + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + return entryId; + } + + if (isFile) { + const ContentType type = DetermineContentType(newName); + QM_TRY( + QM_TO_RESULT(PerformRenameFile(mConnection, aHandle, newName, type))); + } else { + QM_TRY(QM_TO_RESULT(PerformRenameDirectory(mConnection, aHandle, newName))); + } + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + return entryId; +} + +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; +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion001::GetEntryId( + const FileSystemChildMetadata& aHandle) const { + return GetUniqueEntryId(mConnection, aHandle); +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion001::GetEntryId( + const FileId& aFileId) const { + return aFileId.Value(); +} + +Result<FileId, QMResult> FileSystemDatabaseManagerVersion001::EnsureFileId( + const EntryId& aEntryId) { + return FileId(aEntryId); +} + +Result<FileId, QMResult> +FileSystemDatabaseManagerVersion001::EnsureTemporaryFileId( + const EntryId& aEntryId) { + return FileId(aEntryId); +} + +Result<FileId, QMResult> FileSystemDatabaseManagerVersion001::GetFileId( + const EntryId& aEntryId) const { + return FileId(aEntryId); +} + +nsresult FileSystemDatabaseManagerVersion001::MergeFileId( + const EntryId& /* aEntryId */, const FileId& /* aFileId */, + bool /* aAbort */) { + // Version 001 should always use exclusive mode and not get here. + return NS_ERROR_NOT_IMPLEMENTED; +} + +void FileSystemDatabaseManagerVersion001::Close() { mConnection->Close(); } + +nsresult FileSystemDatabaseManagerVersion001::BeginUsageTracking( + const FileId& aFileId) { + MOZ_ASSERT(!aFileId.IsEmpty()); + + // If file is already tracked but we cannot read its size, error. + // If file does not exist, this will succeed because usage is zero. + QM_TRY(EnsureUsageIsKnown(aFileId)); + + // If file does not exist, set usage tracking to true fails with + // file not found error. + QM_TRY(MOZ_TO_RESULT(SetUsageTracking(aFileId, true))); + + return NS_OK; +} + +nsresult FileSystemDatabaseManagerVersion001::EndUsageTracking( + const FileId& aFileId) { + // This is expected to fail only if database is unreachable. + QM_TRY(MOZ_TO_RESULT(SetUsageTracking(aFileId, false))); + + return NS_OK; +} + +Result<bool, QMResult> FileSystemDatabaseManagerVersion001::DoesFileIdExist( + const FileId& aFileId) const { + MOZ_ASSERT(!aFileId.IsEmpty()); + + QM_TRY_RETURN(DoesFileExist(mConnection, aFileId.Value())); +} + +nsresult FileSystemDatabaseManagerVersion001::RemoveFileId( + const FileId& /* aFileId */) { + return NS_OK; +} + +/** + * @brief Get the sum of usages for all file descendants of a directory entry. + * We obtain the value with one query, which is presumably better than having a + * separate query for each individual descendant. + * TODO: Check if this is true + * + * Please see GetFileUsage documentation for why we use the latest recorded + * value from the database instead of the file size property from the disk. + */ +Result<Usage, QMResult> +FileSystemDatabaseManagerVersion001::GetUsagesOfDescendants( + const EntryId& aEntryId) const { + const nsLiteralCString descendantUsagesQuery = + "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 sum(Usages.usage) " + "FROM traceChildren INNER JOIN Usages " + "USING(handle) " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, descendantUsagesQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY_UNWRAP(const bool moreResults, stmt.ExecuteStep()); + if (!moreResults) { + return 0; + } + + QM_TRY_RETURN(stmt.GetUsageByColumn(/* Column */ 0u)); +} + +Result<nsTArray<FileId>, QMResult> +FileSystemDatabaseManagerVersion001::FindFilesUnderEntry( + const EntryId& aEntryId) const { + nsTArray<FileId> descendants; + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, gDescendantsQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + + while (moreResults) { + // Works only for version 001 + QM_TRY_INSPECT(const FileId& fileId, + stmt.GetFileIdByColumn(/* Column */ 0u)); + + descendants.AppendElement(fileId); + + QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep()); + } + } + + return descendants; +} + +nsresult FileSystemDatabaseManagerVersion001::SetUsageTracking( + const FileId& aFileId, bool aTracked) { + auto onMissingFile = [this, &aFileId](const auto& aRv) { + // Usages constrains entryId to be present in Files + MOZ_ASSERT(NS_ERROR_STORAGE_CONSTRAINT == ToNSResult(aRv)); + + // The query *should* fail if and only if file does not exist + QM_TRY_UNWRAP(DebugOnly<bool> fileExists, DoesFileIdExist(aFileId), + QM_VOID); + MOZ_ASSERT(!fileExists); + }; + + return SetUsageTrackingImpl(mConnection, aFileId, aTracked, onMissingFile); +} + +nsresult FileSystemDatabaseManagerVersion001::UpdateUsageInDatabase( + const FileId& aFileId, Usage 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.BindFileIdByName("handle"_ns, aFileId))); + QM_TRY(MOZ_TO_RESULT(stmt.Execute())); + + return NS_OK; +} + +Result<Ok, QMResult> FileSystemDatabaseManagerVersion001::EnsureUsageIsKnown( + const FileId& aFileId) { + if (mFilesOfUnknownUsage < 0) { // Lazy initialization + QM_TRY_UNWRAP(mFilesOfUnknownUsage, GetTrackedFilesCount(mConnection)); + } + + if (mFilesOfUnknownUsage == 0) { + return Ok{}; + } + + QM_TRY_UNWRAP(Maybe<Usage> oldUsage, + GetMaybeTrackedUsage(mConnection, aFileId)); + if (oldUsage.isNothing()) { + return Ok{}; // Usage is 0 or it was successfully recorded at unlocking. + } + + auto quotaCacheUpdate = [this, &aFileId, + oldSize = oldUsage.value()](Usage aNewSize) { + return UpdateCachedQuotaUsage(aFileId, oldSize, aNewSize); + }; + + static const nsLiteralCString updateUsagesKeepTrackedQuery = + "UPDATE Usages SET usage = :usage WHERE handle = :handle;"_ns; + + // If usage update fails, we log an error and keep things the way they were. + QM_TRY(QM_TO_RESULT(UpdateUsageForFileEntry( + mConnection, *mFileManager, aFileId, updateUsagesKeepTrackedQuery, + std::move(quotaCacheUpdate))), + Err(QMResult(NS_ERROR_DOM_FILE_NOT_READABLE_ERR)), + ([this, &aFileId](const auto& /*aRv*/) { + LogWithFilename(*mFileManager, "Could not read the size of file %s", + aFileId); + })); + + // We read and updated the quota usage successfully. + --mFilesOfUnknownUsage; + MOZ_ASSERT(mFilesOfUnknownUsage >= 0); + + return Ok{}; +} + +void FileSystemDatabaseManagerVersion001::DecreaseCachedQuotaUsage( + int64_t aDelta) { + quota::QuotaManager* quotaManager = quota::QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + quotaManager->DecreaseUsageForClient(mClientMetadata, aDelta); +} + +nsresult FileSystemDatabaseManagerVersion001::UpdateCachedQuotaUsage( + const FileId& aFileId, Usage aOldUsage, Usage aNewUsage) { + quota::QuotaManager* quotaManager = quota::QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> fileObj, + mFileManager->GetFile(aFileId).mapErr(toNSResult)); + + RefPtr<quota::QuotaObject> quotaObject = quotaManager->GetQuotaObject( + quota::PERSISTENCE_TYPE_DEFAULT, mClientMetadata, + quota::Client::FILESYSTEM, fileObj, aOldUsage); + MOZ_ASSERT(quotaObject); + + QM_TRY(OkIf(quotaObject->MaybeUpdateSize(aNewUsage, /* aTruncate */ true)), + NS_ERROR_FILE_NO_DEVICE_SPACE); + + return NS_OK; +} + +nsresult FileSystemDatabaseManagerVersion001::ClearDestinationIfNotLocked( + const FileSystemConnection& aConnection, + const FileSystemDataManager* const aDataManager, + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation) { + // If the destination file exists, fail explicitly. Spec author plans to + // revise the spec + QM_TRY_UNWRAP(bool exists, DoesFileExist(aConnection, aNewDesignation)); + if (exists) { + QM_TRY_INSPECT(const EntryId& destId, + FindEntryId(aConnection, aNewDesignation, true)); + QM_TRY_UNWRAP(const bool isLocked, aDataManager->IsLocked(destId)); + if (isLocked) { + LOG(("Trying to overwrite in-use file")); + return NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR; + } + + QM_TRY_UNWRAP(DebugOnly<bool> isRemoved, RemoveFile(aNewDesignation)); + MOZ_ASSERT(isRemoved); + } else { + QM_TRY_UNWRAP(exists, DoesDirectoryExist(aConnection, aNewDesignation)); + if (exists) { + // Fails if directory contains locked files, otherwise total wipeout + QM_TRY_UNWRAP(DebugOnly<bool> isRemoved, + MOZ_TO_RESULT(RemoveDirectory(aNewDesignation, + /* recursive */ true))); + MOZ_ASSERT(isRemoved); + } + } + + return NS_OK; +} + +nsresult FileSystemDatabaseManagerVersion001::PrepareRenameEntry( + const FileSystemConnection& aConnection, + const FileSystemDataManager* const aDataManager, + const FileSystemEntryMetadata& aHandle, const Name& aNewName, + bool aIsFile) { + const EntryId& entryId = aHandle.entryId(); + + // At this point, entry exists + if (aIsFile) { + QM_TRY_UNWRAP(const bool isLocked, aDataManager->IsLocked(entryId)); + if (isLocked) { + LOG(("Trying to move in-use file")); + return NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR; + } + } + + // If the destination file exists, fail explicitly. + FileSystemChildMetadata destination; + QM_TRY_UNWRAP(EntryId parent, FindParent(mConnection, entryId)); + destination.parentId() = parent; + destination.childName() = aNewName; + + QM_TRY(MOZ_TO_RESULT(ClearDestinationIfNotLocked(mConnection, mDataManager, + aHandle, destination))); + + return NS_OK; +} + +nsresult FileSystemDatabaseManagerVersion001::PrepareMoveEntry( + const FileSystemConnection& aConnection, + const FileSystemDataManager* const aDataManager, + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation, bool aIsFile) { + const EntryId& entryId = aHandle.entryId(); + + // At this point, entry exists + if (aIsFile) { + QM_TRY_UNWRAP(const bool isLocked, aDataManager->IsLocked(entryId)); + if (isLocked) { + LOG(("Trying to move in-use file")); + return NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR; + } + } + + QM_TRY(QM_TO_RESULT(ClearDestinationIfNotLocked(aConnection, aDataManager, + aHandle, aNewDesignation))); + + // XXX: This should be before clearing the target + + // 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(aConnection, {entryId, aNewDesignation.parentId()})); + if (isDestinationUnderSelf) { + return NS_ERROR_DOM_INVALID_MODIFICATION_ERR; + } + + return NS_OK; +} + +/** + * Free functions + */ + +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> DoesFileExist(const FileSystemConnection& aConnection, + const EntryId& aEntryId) { + MOZ_ASSERT(!aEntryId.IsEmpty()); + + const nsCString existsQuery = + "SELECT EXISTS " + "(SELECT 1 FROM Files WHERE handle = :handle ) " + ";"_ns; + + QM_TRY_RETURN(ApplyEntryExistsQuery(aConnection, existsQuery, aEntryId)); +} + +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)); +} + +Result<EntryId, QMResult> FindEntryId(const FileSystemConnection& aConnection, + const FileSystemChildMetadata& aHandle, + bool aIsFile) { + const nsCString aDirectoryQuery = + "SELECT Entries.handle FROM Directories " + "INNER JOIN Entries USING (handle) " + "WHERE Directories.name = :name AND Entries.parent = :parent " + ";"_ns; + + const nsCString aFileQuery = + "SELECT Entries.handle FROM Files INNER 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; +} + +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; +} + +Result<bool, QMResult> IsSame(const FileSystemConnection& aConnection, + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewHandle, + bool aIsFile) { + MOZ_ASSERT(!aNewHandle.parentId().IsEmpty()); + + // Typically aNewHandle does not exist which is not an error + QM_TRY_RETURN(QM_OR_ELSE_LOG_VERBOSE_IF( + // Expression. + FindEntryId(aConnection, aNewHandle, aIsFile) + .map([&aHandle](const EntryId& entryId) { + return entryId == aHandle.entryId(); + }), + // Predicate. + IsSpecificError<NS_ERROR_DOM_NOT_FOUND_ERR>, + // Fallback. + ErrToOkFromQMResult<false>)); +} + +Result<Path, QMResult> ResolveReversedPath( + const FileSystemConnection& aConnection, + const FileSystemEntryPair& aEndpoints) { + const nsLiteralCString 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; +} + +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; +} + +// TODO: Implement idle maintenance +void TryRemoveDuringIdleMaintenance( + const nsTArray<FileId>& /* aItemToRemove */) { + // Not implemented +} + +ContentType DetermineContentType(const Name& aName) { + QM_TRY_UNWRAP( + auto typeResult, + QM_OR_ELSE_LOG_VERBOSE( + FileSystemContentTypeGuess::FromPath(aName), + ([](const auto& aRv) -> Result<ContentType, QMResult> { + const nsresult rv = ToNSResult(aRv); + switch (rv) { + case NS_ERROR_FAILURE: /* There is an unknown new extension. */ + return ContentType(""_ns); /* We clear the old extension. */ + + case NS_ERROR_INVALID_ARG: /* The name is garbled. */ + [[fallthrough]]; + case NS_ERROR_NOT_AVAILABLE: /* There is no extension. */ + return VoidCString(); /* We keep the old extension. */ + + default: + MOZ_ASSERT_UNREACHABLE("Should never get here!"); + return Err(aRv); + } + })), + ContentType(""_ns)); + + return typeResult; +} + +} // 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..333c5af6c2 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h @@ -0,0 +1,208 @@ +/* -*- 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 "mozilla/dom/quota/CommonMetadata.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "nsString.h" + +namespace mozilla::dom::fs { + +struct FileId; + +namespace data { + +class FileSystemDataManager; +class FileSystemFileManager; + +/** + * @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); + + /* Static to allow use by quota client without instantiation */ + static nsresult RescanTrackedUsages( + const FileSystemConnection& aConnection, + const quota::OriginMetadata& aOriginMetadata); + + /* Static to allow use by quota client without instantiation */ + static Result<Usage, QMResult> GetFileUsage( + const FileSystemConnection& aConnection); + + nsresult UpdateUsage(const FileId& aFileId) override; + + Result<EntryId, QMResult> GetOrCreateDirectory( + const FileSystemChildMetadata& aHandle, bool aCreate) override; + + Result<EntryId, QMResult> GetOrCreateFile( + const FileSystemChildMetadata& aHandle, bool aCreate) override; + + nsresult GetFile(const EntryId& aEntryId, const FileId& aFileId, + const FileMode& aMode, ContentType& aType, + TimeStamp& lastModifiedMilliSeconds, Path& aPath, + nsCOMPtr<nsIFile>& aFile) const override; + + Result<FileSystemDirectoryListing, QMResult> GetDirectoryEntries( + const EntryId& aParent, PageNumber aPage) const override; + + Result<bool, QMResult> RemoveDirectory(const FileSystemChildMetadata& aHandle, + bool aRecursive) override; + + Result<bool, QMResult> RemoveFile( + const FileSystemChildMetadata& aHandle) override; + + Result<EntryId, QMResult> RenameEntry(const FileSystemEntryMetadata& aHandle, + const Name& aNewName) override; + + Result<EntryId, QMResult> MoveEntry( + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation) override; + + Result<Path, QMResult> Resolve( + const FileSystemEntryPair& aEndpoints) const override; + + Result<EntryId, QMResult> GetEntryId( + const FileSystemChildMetadata& aHandle) const override; + + Result<EntryId, QMResult> GetEntryId(const FileId& aFileId) const override; + + Result<FileId, QMResult> EnsureFileId(const EntryId& aEntryId) override; + + Result<FileId, QMResult> EnsureTemporaryFileId( + const EntryId& aEntryId) override; + + Result<FileId, QMResult> GetFileId(const EntryId& aEntryId) const override; + + nsresult MergeFileId(const EntryId& aEntryId, const FileId& aFileId, + bool aAbort) override; + + void Close() override; + + nsresult BeginUsageTracking(const FileId& aFileId) override; + + nsresult EndUsageTracking(const FileId& aFileId) override; + + virtual ~FileSystemDatabaseManagerVersion001() = default; + + protected: + virtual Result<bool, QMResult> DoesFileIdExist(const FileId& aFileId) const; + + virtual nsresult RemoveFileId(const FileId& aFileId); + + virtual Result<Usage, QMResult> GetUsagesOfDescendants( + const EntryId& aEntryId) const; + + virtual Result<nsTArray<FileId>, QMResult> FindFilesUnderEntry( + const EntryId& aEntryId) const; + + nsresult SetUsageTracking(const FileId& aFileId, bool aTracked); + + nsresult UpdateUsageInDatabase(const FileId& aFileId, Usage aNewDiskUsage); + + Result<Ok, QMResult> EnsureUsageIsKnown(const FileId& aFileId); + + void DecreaseCachedQuotaUsage(int64_t aDelta); + + nsresult UpdateCachedQuotaUsage(const FileId& aFileId, Usage aOldUsage, + Usage aNewUsage); + + nsresult ClearDestinationIfNotLocked( + const FileSystemConnection& aConnection, + const FileSystemDataManager* const aDataManager, + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation); + + nsresult PrepareRenameEntry(const FileSystemConnection& aConnection, + const FileSystemDataManager* const aDataManager, + const FileSystemEntryMetadata& aHandle, + const Name& aNewName, bool aIsFile); + + nsresult PrepareMoveEntry(const FileSystemConnection& aConnection, + const FileSystemDataManager* const aDataManager, + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation, + bool aIsFile); + + // 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; + + const quota::ClientMetadata mClientMetadata; + + int32_t mFilesOfUnknownUsage; +}; + +inline auto toNSResult = [](const auto& aRv) { return ToNSResult(aRv); }; + +Result<bool, QMResult> ApplyEntryExistsQuery( + const FileSystemConnection& aConnection, const nsACString& aQuery, + const FileSystemChildMetadata& aHandle); + +Result<bool, QMResult> ApplyEntryExistsQuery( + const FileSystemConnection& aConnection, const nsACString& aQuery, + const EntryId& aEntry); + +Result<bool, QMResult> DoesFileExist(const FileSystemConnection& aConnection, + const EntryId& aEntryId); + +Result<bool, QMResult> IsFile(const FileSystemConnection& aConnection, + const EntryId& aEntryId); + +Result<EntryId, QMResult> FindEntryId(const FileSystemConnection& aConnection, + const FileSystemChildMetadata& aHandle, + bool aIsFile); + +Result<EntryId, QMResult> FindParent(const FileSystemConnection& aConnection, + const EntryId& aEntryId); + +Result<bool, QMResult> IsSame(const FileSystemConnection& aConnection, + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewHandle, + bool aIsFile); + +Result<Path, QMResult> ResolveReversedPath( + const FileSystemConnection& aConnection, + const FileSystemEntryPair& aEndpoints); + +nsresult GetFileAttributes(const FileSystemConnection& aConnection, + const EntryId& aEntryId, ContentType& aType); + +void TryRemoveDuringIdleMaintenance(const nsTArray<FileId>& aItemToRemove); + +ContentType DetermineContentType(const Name& aName); + +} // namespace data +} // namespace mozilla::dom::fs + +#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION001_H_ diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.cpp b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.cpp new file mode 100644 index 0000000000..3543346ff0 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.cpp @@ -0,0 +1,832 @@ +/* -*- 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 "FileSystemDatabaseManagerVersion002.h" + +#include "ErrorList.h" +#include "FileSystemContentTypeGuess.h" +#include "FileSystemDataManager.h" +#include "FileSystemFileManager.h" +#include "FileSystemHashSource.h" +#include "FileSystemHashStorageFunction.h" +#include "FileSystemParentTypes.h" +#include "ResultStatement.h" +#include "StartedTransaction.h" +#include "mozStorageHelper.h" +#include "mozilla/CheckedInt.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/QMResult.h" +#include "mozilla/dom/quota/Client.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/QuotaObject.h" +#include "mozilla/dom/quota/ResultExtensions.h" + +namespace mozilla::dom::fs::data { + +namespace { + +Result<FileId, QMResult> GetFileId002(const FileSystemConnection& aConnection, + const EntryId& aEntryId) { + const nsLiteralCString fileIdQuery = + "SELECT fileId FROM MainFiles WHERE handle = :entryId ;"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, fileIdQuery)); + 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_INSPECT(const FileId& fileId, stmt.GetFileIdByColumn(/* Column */ 0u)); + + return fileId; +} + +Result<bool, QMResult> DoesFileIdExist(const FileSystemConnection& aConnection, + const FileId& aFileId) { + MOZ_ASSERT(!aFileId.IsEmpty()); + + const nsLiteralCString existsQuery = + "SELECT EXISTS " + "(SELECT 1 FROM FileIds WHERE fileId = :handle ) " + ";"_ns; + + QM_TRY_RETURN( + ApplyEntryExistsQuery(aConnection, existsQuery, aFileId.Value())); +} + +nsresult RehashFile(const FileSystemConnection& aConnection, + const EntryId& aEntryId, + const FileSystemChildMetadata& aNewDesignation, + const ContentType& aNewType) { + QM_TRY_INSPECT(const EntryId& newId, + FileSystemHashSource::GenerateHash( + aNewDesignation.parentId(), aNewDesignation.childName())); + + // The destination should be empty at this point: either we exited because + // overwrite was not desired, or the existing content was removed. + const nsLiteralCString insertNewEntryQuery = + "INSERT INTO Entries ( handle, parent ) " + "VALUES ( :newId, :newParent ) " + ";"_ns; + + const nsLiteralCString insertNewFileAndTypeQuery = + "INSERT INTO Files ( handle, type, name ) " + "VALUES ( :newId, :type, :newName ) " + ";"_ns; + + const nsLiteralCString insertNewFileKeepTypeQuery = + "INSERT INTO Files ( handle, type, name ) " + "SELECT :newId, type, :newName FROM Files " + "WHERE handle = :oldId ;"_ns; + + const auto& insertNewFileQuery = aNewType.IsVoid() + ? insertNewFileKeepTypeQuery + : insertNewFileAndTypeQuery; + + const nsLiteralCString updateFileMappingsQuery = + "UPDATE FileIds SET handle = :newId WHERE handle = :handle ;"_ns; + + const nsLiteralCString updateMainFilesQuery = + "UPDATE MainFiles SET handle = :newId WHERE handle = :handle ;"_ns; + + const nsLiteralCString cleanupOldEntryQuery = + "DELETE FROM Entries WHERE handle = :handle ;"_ns; + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConnection)); + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewEntryQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("newId"_ns, newId))); + QM_TRY(QM_TO_RESULT( + stmt.BindEntryIdByName("newParent"_ns, aNewDesignation.parentId()))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewFileQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("newId"_ns, newId))); + if (aNewType.IsVoid()) { + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("oldId"_ns, aEntryId))); + } else { + QM_TRY(QM_TO_RESULT(stmt.BindContentTypeByName("type"_ns, aNewType))); + } + QM_TRY(QM_TO_RESULT( + stmt.BindNameByName("newName"_ns, aNewDesignation.childName()))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, updateFileMappingsQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("newId"_ns, newId))); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, updateMainFilesQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("newId"_ns, newId))); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, cleanupOldEntryQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + return NS_OK; +} + +nsresult RehashDirectory(const FileSystemConnection& aConnection, + const EntryId& aEntryId, + const FileSystemChildMetadata& aNewDesignation) { + // This name won't match up with the entryId for the old path but + // it will be removed at the end + const nsLiteralCString updateNameQuery = + "UPDATE Directories SET name = :newName WHERE handle = :handle " + "; "_ns; + + const nsLiteralCString calculateHashesQuery = + "CREATE TEMPORARY TABLE ParentChildHash AS " + "WITH RECURSIVE " + "rehashMap( depth, isFile, handle, parent, name, hash ) AS ( " + "SELECT 0, isFile, handle, parent, name, hashEntry( :newParent, name ) " + "FROM EntryNames WHERE handle = :handle UNION SELECT " + "1 + depth, EntryNames.isFile, EntryNames.handle, EntryNames.parent, " + "EntryNames.name, hashEntry( rehashMap.hash, EntryNames.name ) " + "FROM rehashMap, EntryNames WHERE rehashMap.handle = EntryNames.parent ) " + "SELECT depth, isFile, handle, parent, name, hash FROM rehashMap " + ";"_ns; + + const nsLiteralCString createIndexByDepthQuery = + "CREATE INDEX indexOnDepth ON ParentChildHash ( depth ); "_ns; + + // To avoid constraint violation, we insert new entries under the old parent. + // The destination should be empty at this point: either we exited because + // overwrite was not desired, or the existing content was removed. + const nsLiteralCString insertNewEntriesQuery = + "INSERT INTO Entries ( handle, parent ) " + "SELECT hash, :parent FROM ParentChildHash " + ";"_ns; + + const nsLiteralCString insertNewDirectoriesQuery = + "INSERT INTO Directories ( handle, name ) " + "SELECT hash, name FROM ParentChildHash WHERE isFile = 0 " + "ORDER BY depth " + ";"_ns; + + const nsLiteralCString insertNewFilesQuery = + "INSERT INTO Files ( handle, type, name ) " + "SELECT ParentChildHash.hash, Files.type, ParentChildHash.name " + "FROM ParentChildHash INNER JOIN Files USING (handle) " + "WHERE ParentChildHash.isFile = 1 " + ";"_ns; + + const nsLiteralCString updateFileMappingsQuery = + "UPDATE FileIds SET handle = hash " + "FROM ( SELECT handle, hash FROM ParentChildHash ) AS replacement " + "WHERE FileIds.handle = replacement.handle " + ";"_ns; + + const nsLiteralCString updateMainFilesQuery = + "UPDATE MainFiles SET handle = hash " + "FROM ( SELECT handle, hash FROM ParentChildHash ) AS replacement " + "WHERE MainFiles.handle = replacement.handle " + ";"_ns; + + // Now fix the parents + const nsLiteralCString updateEntryMappingsQuery = + "UPDATE Entries SET parent = hash " + "FROM ( SELECT Lhs.hash AS handle, Rhs.hash AS hash, Lhs.depth AS depth " + "FROM ParentChildHash AS Lhs " + "INNER JOIN ParentChildHash AS Rhs " + "ON Rhs.handle = Lhs.parent ORDER BY depth ) AS replacement " + "WHERE Entries.handle = replacement.handle " + ";"_ns; + + const nsLiteralCString cleanupOldEntriesQuery = + "DELETE FROM Entries WHERE handle = :handle " + ";"_ns; + + // Index is automatically deleted + const nsLiteralCString cleanupTemporaries = + "DROP TABLE ParentChildHash " + ";"_ns; + + nsCOMPtr<mozIStorageFunction> rehashFunction = + new data::FileSystemHashStorageFunction(); + QM_TRY(MOZ_TO_RESULT(aConnection->CreateFunction("hashEntry"_ns, + /* number of arguments */ 2, + rehashFunction))); + auto finallyRemoveFunction = MakeScopeExit([&aConnection]() { + QM_WARNONLY_TRY(MOZ_TO_RESULT(aConnection->RemoveFunction("hashEntry"_ns))); + }); + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConnection)); + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, updateNameQuery)); + QM_TRY(QM_TO_RESULT( + stmt.BindNameByName("newName"_ns, aNewDesignation.childName()))); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, calculateHashesQuery)); + QM_TRY(QM_TO_RESULT( + stmt.BindEntryIdByName("newParent"_ns, aNewDesignation.parentId()))); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + QM_TRY(QM_TO_RESULT(aConnection->ExecuteSimpleSQL(createIndexByDepthQuery))); + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewEntriesQuery)); + QM_TRY(QM_TO_RESULT( + stmt.BindEntryIdByName("parent"_ns, aNewDesignation.parentId()))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewDirectoriesQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewFilesQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, updateFileMappingsQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, updateMainFilesQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, updateEntryMappingsQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, cleanupOldEntriesQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, cleanupTemporaries)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + return NS_OK; +} + +/** + * @brief Each entryId is interpreted as a large integer, which is increased + * until an unused value is found. This process is in principle infallible. + * The files associated with a given path will form a cluster next to the + * entryId which could be used for recovery because our hash function is + * expected to distribute all clusters far from each other. + */ +Result<FileId, QMResult> GetNextFreeFileId( + const FileSystemConnection& aConnection, + const FileSystemFileManager& aFileManager, const EntryId& aEntryId) { + MOZ_ASSERT(32u == aEntryId.Length()); + + auto DoesExist = [&aConnection, &aFileManager]( + const FileId& aId) -> Result<bool, QMResult> { + QM_TRY_INSPECT(const nsCOMPtr<nsIFile>& diskFile, + aFileManager.GetFile(aId)); + + bool result = true; + QM_TRY(QM_TO_RESULT(diskFile->Exists(&result))); + if (result) { + return true; + } + + QM_TRY_RETURN(DoesFileIdExist(aConnection, aId)); + }; + + auto Next = [](FileId& aId) { + // Using a larger integer would make fileIds depend on platform endianness. + using IntegerType = uint8_t; + constexpr int32_t bufferSize = 32 / sizeof(IntegerType); + using IdBuffer = std::array<IntegerType, bufferSize>; + + auto Increase = [](IdBuffer& aIn) { + for (int i = 0; i < bufferSize; ++i) { + if (1u + aIn[i] != 0u) { + ++aIn[i]; + return; + } + aIn[i] = 0u; + } + }; + + DebugOnly<nsCString> original = aId.Value(); + Increase(*reinterpret_cast<IdBuffer*>(aId.mValue.BeginWriting())); + MOZ_ASSERT(!aId.Value().Equals(original)); + }; + + FileId id = FileId(aEntryId); + + while (true) { + QM_WARNONLY_TRY_UNWRAP(Maybe<bool> maybeExists, DoesExist(id)); + if (maybeExists.isSome() && !maybeExists.value()) { + return id; + } + + Next(id); + } +} + +Result<FileId, QMResult> AddNewFileId(const FileSystemConnection& aConnection, + const FileSystemFileManager& aFileManager, + const EntryId& aEntryId) { + QM_TRY_INSPECT(const FileId& nextFreeId, + GetNextFreeFileId(aConnection, aFileManager, aEntryId)); + + const nsLiteralCString insertNewFileIdQuery = + "INSERT INTO FileIds ( fileId, handle ) " + "VALUES ( :fileId, :entryId ) " + "; "_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewFileIdQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, nextFreeId))); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEntryId))); + + QM_TRY(QM_TO_RESULT(stmt.Execute())); + + return nextFreeId; +} + +/** + * @brief Get recorded usage or zero if nothing was ever written to the file. + * Removing files is only allowed when there is no lock on the file, and their + * usage is either correctly recorded in the database during unlock, or nothing, + * or they remain in tracked state and the quota manager assumes their usage to + * be equal to the latest recorded value. In all cases, the latest recorded + * value (or nothing) is the correct amount of quota to be released. + */ +Result<Usage, QMResult> GetKnownUsage(const FileSystemConnection& aConnection, + const FileId& aFileId) { + const nsLiteralCString trackedUsageQuery = + "SELECT usage FROM Usages WHERE handle = :handle ;"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, trackedUsageQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("handle"_ns, aFileId))); + + QM_TRY_UNWRAP(const bool moreResults, stmt.ExecuteStep()); + if (!moreResults) { + return 0; + } + + QM_TRY_RETURN(stmt.GetUsageByColumn(/* Column */ 0u)); +} + +} // namespace + +/* static */ +nsresult FileSystemDatabaseManagerVersion002::RescanTrackedUsages( + const FileSystemConnection& aConnection, + const quota::OriginMetadata& aOriginMetadata) { + return FileSystemDatabaseManagerVersion001::RescanTrackedUsages( + aConnection, aOriginMetadata); +} + +/* static */ +Result<Usage, QMResult> FileSystemDatabaseManagerVersion002::GetFileUsage( + const FileSystemConnection& aConnection) { + return FileSystemDatabaseManagerVersion001::GetFileUsage(aConnection); +} + +nsresult FileSystemDatabaseManagerVersion002::GetFile( + const EntryId& aEntryId, const FileId& aFileId, const FileMode& aMode, + ContentType& aType, TimeStamp& lastModifiedMilliSeconds, + nsTArray<Name>& aPath, nsCOMPtr<nsIFile>& aFile) const { + MOZ_ASSERT(!aFileId.IsEmpty()); + + const FileSystemEntryPair endPoints(mRootEntry, aEntryId); + QM_TRY_UNWRAP(aPath, ResolveReversedPath(mConnection, endPoints)); + if (aPath.IsEmpty()) { + return NS_ERROR_DOM_NOT_FOUND_ERR; + } + + QM_TRY(MOZ_TO_RESULT(GetFileAttributes(mConnection, aEntryId, aType))); + + if (aMode == FileMode::SHARED_FROM_COPY) { + QM_WARNONLY_TRY_UNWRAP(Maybe<FileId> mainFileId, GetFileId(aEntryId)); + if (mainFileId) { + QM_TRY_UNWRAP(aFile, + mFileManager->CreateFileFrom(aFileId, mainFileId.value())); + } else { + // LockShared/EnsureTemporaryFileId has provided a brand new fileId. + QM_TRY_UNWRAP(aFile, mFileManager->GetOrCreateFile(aFileId)); + } + } else { + MOZ_ASSERT(aMode == FileMode::EXCLUSIVE || + aMode == FileMode::SHARED_FROM_EMPTY); + + QM_TRY_UNWRAP(aFile, mFileManager->GetOrCreateFile(aFileId)); + } + + PRTime lastModTime = 0; + QM_TRY(MOZ_TO_RESULT(aFile->GetLastModifiedTime(&lastModTime))); + lastModifiedMilliSeconds = static_cast<TimeStamp>(lastModTime); + + aPath.Reverse(); + + return NS_OK; +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion002::RenameEntry( + const FileSystemEntryMetadata& aHandle, const Name& aNewName) { + MOZ_ASSERT(!aNewName.IsEmpty()); + + const auto& entryId = aHandle.entryId(); + MOZ_ASSERT(!entryId.IsEmpty()); + + // Can't rename root + if (mRootEntry == entryId) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + // Verify the source exists + QM_TRY_UNWRAP(bool isFile, IsFile(mConnection, entryId), + Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR))); + + // Are we actually renaming? + if (aHandle.entryName() == aNewName) { + return entryId; + } + + QM_TRY(QM_TO_RESULT(PrepareRenameEntry(mConnection, mDataManager, aHandle, + aNewName, isFile))); + + QM_TRY_UNWRAP(EntryId parentId, FindParent(mConnection, entryId)); + FileSystemChildMetadata newDesignation(parentId, aNewName); + + if (isFile) { + const ContentType type = DetermineContentType(aNewName); + QM_TRY( + QM_TO_RESULT(RehashFile(mConnection, entryId, newDesignation, type))); + } else { + QM_TRY(QM_TO_RESULT(RehashDirectory(mConnection, entryId, newDesignation))); + } + + QM_TRY_UNWRAP(DebugOnly<EntryId> dbId, + FindEntryId(mConnection, newDesignation, isFile)); + QM_TRY_UNWRAP(EntryId generated, + FileSystemHashSource::GenerateHash(parentId, aNewName)); + MOZ_ASSERT(static_cast<EntryId&>(dbId).Equals(generated)); + + return generated; +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion002::MoveEntry( + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation) { + MOZ_ASSERT(!aHandle.entryId().IsEmpty()); + + const auto& entryId = aHandle.entryId(); + + if (mRootEntry == entryId) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + // Verify the source exists + QM_TRY_UNWRAP(bool isFile, IsFile(mConnection, entryId), + Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR))); + + // If the rename doesn't change the name or directory, just return success. + // XXX Needs to be added to the spec + QM_WARNONLY_TRY_UNWRAP(Maybe<bool> maybeSame, + IsSame(mConnection, aHandle, aNewDesignation, isFile)); + if (maybeSame && maybeSame.value()) { + return entryId; + } + + QM_TRY(QM_TO_RESULT(PrepareMoveEntry(mConnection, mDataManager, aHandle, + aNewDesignation, isFile))); + + if (isFile) { + const ContentType type = DetermineContentType(aNewDesignation.childName()); + QM_TRY( + QM_TO_RESULT(RehashFile(mConnection, entryId, aNewDesignation, type))); + } else { + QM_TRY( + QM_TO_RESULT(RehashDirectory(mConnection, entryId, aNewDesignation))); + } + + QM_TRY_UNWRAP(DebugOnly<EntryId> dbId, + FindEntryId(mConnection, aNewDesignation, isFile)); + QM_TRY_UNWRAP(EntryId generated, + FileSystemHashSource::GenerateHash( + aNewDesignation.parentId(), aNewDesignation.childName())); + MOZ_ASSERT(static_cast<EntryId&>(dbId).Equals(generated)); + + return generated; +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion002::GetEntryId( + const FileSystemChildMetadata& aHandle) const { + return fs::data::GetEntryHandle(aHandle); +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion002::GetEntryId( + const FileId& aFileId) const { + const nsLiteralCString getEntryIdQuery = + "SELECT handle FROM FileIds WHERE fileId = :fileId ;"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, getEntryIdQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, aFileId))); + QM_TRY_UNWRAP(bool hasEntries, stmt.ExecuteStep()); + + if (!hasEntries || stmt.IsNullByColumn(/* Column */ 0u)) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + QM_TRY_RETURN(stmt.GetEntryIdByColumn(/* Column */ 0u)); +} + +Result<FileId, QMResult> FileSystemDatabaseManagerVersion002::EnsureFileId( + const EntryId& aEntryId) { + QM_TRY_UNWRAP(bool exists, DoesFileExist(mConnection, aEntryId)); + if (!exists) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + QM_TRY_UNWRAP(Maybe<FileId> maybeMainFileId, + QM_OR_ELSE_LOG_VERBOSE_IF( + // Expression. + GetFileId(aEntryId).map([](auto mainFileId) { + return Some(std::move(mainFileId)); + }), + // Predicate. + IsSpecificError<NS_ERROR_DOM_NOT_FOUND_ERR>, + // Fallback. + ([](const auto&) -> Result<Maybe<FileId>, QMResult> { + return Maybe<FileId>{}; + }))); + + if (maybeMainFileId) { + return *maybeMainFileId; + } + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection)); + + QM_TRY_INSPECT(const FileId& fileId, + AddNewFileId(mConnection, *mFileManager, aEntryId)); + + QM_TRY(QM_TO_RESULT(MergeFileId(aEntryId, fileId, /* aAbort */ false))); + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + return fileId; +} + +Result<FileId, QMResult> +FileSystemDatabaseManagerVersion002::EnsureTemporaryFileId( + const EntryId& aEntryId) { + QM_TRY_UNWRAP(bool exists, DoesFileExist(mConnection, aEntryId)); + if (!exists) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + QM_TRY_RETURN(AddNewFileId(mConnection, *mFileManager, aEntryId)); +} + +Result<FileId, QMResult> FileSystemDatabaseManagerVersion002::GetFileId( + const EntryId& aEntryId) const { + MOZ_ASSERT(mConnection); + return data::GetFileId002(mConnection, aEntryId); +} + +nsresult FileSystemDatabaseManagerVersion002::MergeFileId( + const EntryId& aEntryId, const FileId& aFileId, bool aAbort) { + MOZ_ASSERT(mConnection); + + auto doCleanUp = [this](const FileId& aCleanable) -> nsresult { + // We need to clean up the old main file. + QM_TRY_UNWRAP(Usage usage, + GetKnownUsage(mConnection, aCleanable).mapErr(toNSResult)); + + QM_WARNONLY_TRY_UNWRAP(Maybe<Usage> removedUsage, + mFileManager->RemoveFile(aCleanable)); + + if (removedUsage) { + // Removal of file data was ok, update the related fileId and usage + QM_WARNONLY_TRY(QM_TO_RESULT(RemoveFileId(aCleanable))); + + if (usage > 0) { // Performance! + DecreaseCachedQuotaUsage(usage); + } + + // We only check the most common case. This can fail spuriously if an + // external application writes to the file, or OS reports zero size due to + // corruption. + MOZ_ASSERT_IF(0 == mFilesOfUnknownUsage, usage == removedUsage.value()); + + return NS_OK; + } + + // Removal failed + const nsLiteralCString forgetCleanable = + "UPDATE FileIds SET handle = NULL WHERE fileId = :fileId ; "_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, forgetCleanable) + .mapErr(toNSResult)); + QM_TRY(MOZ_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, aCleanable))); + QM_TRY(MOZ_TO_RESULT(stmt.Execute())); + + TryRemoveDuringIdleMaintenance({aCleanable}); + + return NS_OK; + }; + + if (aAbort) { + QM_TRY(MOZ_TO_RESULT(doCleanUp(aFileId))); + + return NS_OK; + } + + QM_TRY_UNWRAP( + Maybe<FileId> maybeOldFileId, + QM_OR_ELSE_LOG_VERBOSE_IF( + // Expression. + GetFileId(aEntryId) + .map([](auto oldFileId) { return Some(std::move(oldFileId)); }) + .mapErr(toNSResult), + // Predicate. + IsSpecificError<NS_ERROR_DOM_NOT_FOUND_ERR>, + // Fallback. + ErrToDefaultOk<Maybe<FileId>>)); + + if (maybeOldFileId && *maybeOldFileId == aFileId) { + return NS_OK; // Nothing to do + } + + // Main file changed + const nsLiteralCString flagAsMainFileQuery = + "INSERT INTO MainFiles ( handle, fileId ) " + "VALUES ( :entryId, :fileId ) " + "ON CONFLICT (handle) " + "DO UPDATE SET fileId = excluded.fileId " + "; "_ns; + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection)); + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, flagAsMainFileQuery) + .mapErr(toNSResult)); + QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEntryId))); + QM_TRY(MOZ_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, aFileId))); + + QM_TRY(MOZ_TO_RESULT(stmt.Execute())); + + if (!maybeOldFileId) { + // We successfully added a new main file and there is nothing to clean up. + QM_TRY(MOZ_TO_RESULT(transaction.Commit())); + + return NS_OK; + } + + MOZ_ASSERT(maybeOldFileId); + MOZ_ASSERT(*maybeOldFileId != aFileId); + + QM_TRY(MOZ_TO_RESULT(doCleanUp(*maybeOldFileId))); + + // If the old fileId and usage were not deleted, main file update fails. + QM_TRY(MOZ_TO_RESULT(transaction.Commit())); + + return NS_OK; +} + +Result<bool, QMResult> FileSystemDatabaseManagerVersion002::DoesFileIdExist( + const FileId& aFileId) const { + QM_TRY_RETURN(data::DoesFileIdExist(mConnection, aFileId)); +} + +nsresult FileSystemDatabaseManagerVersion002::RemoveFileId( + const FileId& aFileId) { + const nsLiteralCString removeFileIdQuery = + "DELETE FROM FileIds " + "WHERE fileId = :fileId " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, removeFileIdQuery) + .mapErr(toNSResult)); + + QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("fileId"_ns, aFileId.Value()))); + + return stmt.Execute(); +} + +Result<Usage, QMResult> +FileSystemDatabaseManagerVersion002::GetUsagesOfDescendants( + const EntryId& aEntryId) const { + const nsLiteralCString descendantUsagesQuery = + "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 sum(Usages.usage) " + "FROM traceChildren " + "INNER JOIN FileIds ON traceChildren.handle = FileIds.handle " + "INNER JOIN Usages ON Usages.handle = FileIds.fileId " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, descendantUsagesQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY_UNWRAP(const bool moreResults, stmt.ExecuteStep()); + if (!moreResults) { + return 0; + } + + QM_TRY_RETURN(stmt.GetUsageByColumn(/* Column */ 0u)); +} + +Result<nsTArray<FileId>, QMResult> +FileSystemDatabaseManagerVersion002::FindFilesUnderEntry( + const EntryId& aEntryId) const { + 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 FileIds.fileId " + "FROM traceChildren INNER JOIN FileIds USING (handle) " + ";"_ns; + + nsTArray<FileId> descendants; + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, descendantsQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + + while (true) { + QM_TRY_INSPECT(const bool& moreResults, stmt.ExecuteStep()); + if (!moreResults) { + break; + } + + QM_TRY_INSPECT(const FileId& fileId, + stmt.GetFileIdByColumn(/* Column */ 0u)); + descendants.AppendElement(fileId); + } + } + + return descendants; +} + +} // namespace mozilla::dom::fs::data diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.h b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.h new file mode 100644 index 0000000000..6dec629632 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.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_FILESYSTEMDATABASEMANAGERVERSION002_H_ +#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION002_H_ + +#include "FileSystemDatabaseManagerVersion001.h" + +namespace mozilla::dom::fs::data { + +class FileSystemDatabaseManagerVersion002 + : public FileSystemDatabaseManagerVersion001 { + public: + FileSystemDatabaseManagerVersion002( + FileSystemDataManager* aDataManager, FileSystemConnection&& aConnection, + UniquePtr<FileSystemFileManager>&& aFileManager, + const EntryId& aRootEntry) + : FileSystemDatabaseManagerVersion001( + aDataManager, std::move(aConnection), std::move(aFileManager), + aRootEntry) {} + + /* Static to allow use by quota client without instantiation */ + static nsresult RescanTrackedUsages( + const FileSystemConnection& aConnection, + const quota::OriginMetadata& aOriginMetadata); + + /* Static to allow use by quota client without instantiation */ + static Result<Usage, QMResult> GetFileUsage( + const FileSystemConnection& aConnection); + + nsresult GetFile(const EntryId& aEntryId, const FileId& aFileId, + const FileMode& aMode, ContentType& aType, + TimeStamp& lastModifiedMilliSeconds, Path& aPath, + nsCOMPtr<nsIFile>& aFile) const override; + + Result<EntryId, QMResult> RenameEntry(const FileSystemEntryMetadata& aHandle, + const Name& aNewName) override; + + Result<EntryId, QMResult> MoveEntry( + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation) override; + + Result<EntryId, QMResult> GetEntryId( + const FileSystemChildMetadata& aHandle) const override; + + Result<EntryId, QMResult> GetEntryId(const FileId& aFileId) const override; + + Result<FileId, QMResult> EnsureFileId(const EntryId& aEntryId) override; + + Result<FileId, QMResult> EnsureTemporaryFileId( + const EntryId& aEntryId) override; + + Result<FileId, QMResult> GetFileId(const EntryId& aEntryId) const override; + + nsresult MergeFileId(const EntryId& aEntryId, const FileId& aFileId, + bool aAbort) override; + + protected: + Result<bool, QMResult> DoesFileIdExist(const FileId& aFileId) const override; + + nsresult RemoveFileId(const FileId& aFileId) override; + + Result<Usage, QMResult> GetUsagesOfDescendants( + const EntryId& aEntryId) const override; + + Result<nsTArray<FileId>, QMResult> FindFilesUnderEntry( + const EntryId& aEntryId) const override; +}; + +} // namespace mozilla::dom::fs::data + +#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION002_H_ diff --git a/dom/fs/parent/datamodel/FileSystemFileManager.cpp b/dom/fs/parent/datamodel/FileSystemFileManager.cpp new file mode 100644 index 0000000000..02a69467dc --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemFileManager.cpp @@ -0,0 +1,393 @@ +/* -*- 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 "FileSystemParentTypes.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 "nsHashKeys.h" +#include "nsIFile.h" +#include "nsIFileProtocolHandler.h" +#include "nsIFileURL.h" +#include "nsIURIMutator.h" +#include "nsTHashMap.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 FileId& aFileId) { + MOZ_ASSERT(32u == aFileId.Value().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(aFileId)); + + 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()); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> result, + QM_TO_RESULT_TRANSFORM(quota::QM_NewLocalFile(aFilePath))); + + 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 FileId& aFileId) { + MOZ_ASSERT(!aFileId.IsEmpty()); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject, + GetFileDestination(aTopDirectory, aFileId)); + + nsString desiredPath; + QM_TRY(QM_TO_RESULT(pathObject->GetPath(desiredPath))); + + QM_TRY_RETURN(QM_TO_RESULT_TRANSFORM(quota::QM_NewLocalFile(desiredPath))); +} + +Result<nsCOMPtr<nsIFile>, QMResult> GetOrCreateFile( + const nsCOMPtr<nsIFile>& aTopDirectory, const FileId& aFileId) { + MOZ_ASSERT(!aFileId.IsEmpty()); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject, + GetFileDestination(aTopDirectory, aFileId)); + + nsString desiredPath; + QM_TRY(QM_TO_RESULT(pathObject->GetPath(desiredPath))); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> result, GetOrCreateFileImpl(desiredPath)); + + return result; +} + +nsresult RemoveFileObject(const nsCOMPtr<nsIFile>& aFilePtr) { + // If we cannot tell whether the object is file or directory, or it is a + // directory, it is abandoned as an unknown object. If an attempt is made to + // create a new object with the same path on disk, we regenerate the FileId + // until the collision is resolved. + + bool isFile = false; + QM_TRY(MOZ_TO_RESULT(aFilePtr->IsFile(&isFile))); + + QM_TRY(OkIf(isFile), NS_ERROR_FILE_IS_DIRECTORY); + + QM_TRY(QM_TO_RESULT(aFilePtr->Remove(/* recursive */ false))); + + return NS_OK; +} + +#ifdef DEBUG +// Unused in release builds +Result<Usage, QMResult> GetFileSize(const nsCOMPtr<nsIFile>& aFileObject) { + bool exists = false; + QM_TRY(QM_TO_RESULT(aFileObject->Exists(&exists))); + + if (!exists) { + return 0; + } + + bool isFile = false; + QM_TRY(QM_TO_RESULT(aFileObject->IsFile(&isFile))); + + // We never create directories with this path: this is an unknown object + // and the file does not exist + QM_TRY(OkIf(isFile), 0); + + QM_TRY_UNWRAP(Usage fileSize, + QM_TO_RESULT_INVOKE_MEMBER(aFileObject, GetFileSize)); + + return fileSize; +} +#endif + +} // namespace + +Result<nsCOMPtr<nsIFile>, QMResult> GetFileSystemDirectory( + const quota::OriginMetadata& aOriginMetadata) { + MOZ_ASSERT(aOriginMetadata.mPersistenceType == + quota::PERSISTENCE_TYPE_DEFAULT); + + quota::QuotaManager* quotaManager = quota::QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> fileSystemDirectory, + QM_TO_RESULT_TRANSFORM( + quotaManager->GetOriginDirectory(aOriginMetadata))); + + 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->EnsureTemporaryStorageIsInitializedInternal())); + + 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 quota::OriginMetadata& aOriginMetadata) { + MOZ_ASSERT(!aOriginMetadata.mOrigin.IsEmpty()); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> databaseFile, + GetFileSystemDirectory(aOriginMetadata)); + + 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 quota::OriginMetadata& aOriginMetadata, + const int64_t aDirectoryLockId) { + MOZ_ASSERT(aDirectoryLockId >= -1); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> databaseFile, + GetDatabaseFile(aOriginMetadata)); + + 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))); + + // aDirectoryLockId should only be -1 when we are called from + // FileSystemQuotaClient::InitOrigin when the temporary storage hasn't been + // initialized yet. At that time, the in-memory objects (e.g. OriginInfo) are + // only being created so it doesn't make sense to tunnel quota information to + // QuotaVFS to get corresponding QuotaObject instances for SQLite files. + const nsCString directoryLockIdClause = + "&directoryLockId="_ns + IntToCString(aDirectoryLockId); + + nsCOMPtr<nsIFileURL> result; + QM_TRY(QM_TO_RESULT(NS_MutateURI(mutator) + .SetQuery("cache=private"_ns + directoryLockIdClause) + .Finalize(result))); + + return result; +} + +/* static */ +Result<FileSystemFileManager, QMResult> +FileSystemFileManager::CreateFileSystemFileManager( + nsCOMPtr<nsIFile>&& topDirectory) { + return FileSystemFileManager(std::move(topDirectory)); +} + +/* static */ +Result<UniquePtr<FileSystemFileManager>, QMResult> +FileSystemFileManager::CreateFileSystemFileManager( + const quota::OriginMetadata& aOriginMetadata) { + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> topDirectory, + GetFileSystemDirectory(aOriginMetadata)); + + return MakeUnique<FileSystemFileManager>( + FileSystemFileManager(std::move(topDirectory))); +} + +FileSystemFileManager::FileSystemFileManager(nsCOMPtr<nsIFile>&& aTopDirectory) + : mTopDirectory(std::move(aTopDirectory)) {} + +Result<nsCOMPtr<nsIFile>, QMResult> FileSystemFileManager::GetFile( + const FileId& aFileId) const { + return data::GetFile(mTopDirectory, aFileId); +} + +Result<nsCOMPtr<nsIFile>, QMResult> FileSystemFileManager::GetOrCreateFile( + const FileId& aFileId) { + return data::GetOrCreateFile(mTopDirectory, aFileId); +} + +Result<nsCOMPtr<nsIFile>, QMResult> FileSystemFileManager::CreateFileFrom( + const FileId& aDestinationFileId, const FileId& aSourceFileId) { + MOZ_ASSERT(!aDestinationFileId.IsEmpty()); + MOZ_ASSERT(!aSourceFileId.IsEmpty()); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> original, GetFile(aSourceFileId)); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> destination, + GetFileDestination(mTopDirectory, aDestinationFileId)); + + nsAutoString leafName; + QM_TRY(QM_TO_RESULT(destination->GetLeafName(leafName))); + + nsCOMPtr<nsIFile> destParent; + QM_TRY(QM_TO_RESULT(destination->GetParent(getter_AddRefs(destParent)))); + + QM_TRY(QM_TO_RESULT(original->CopyTo(destParent, leafName))); + +#ifdef DEBUG + bool exists = false; + QM_TRY(QM_TO_RESULT(destination->Exists(&exists))); + MOZ_ASSERT(exists); + + int64_t destSize = 0; + QM_TRY(QM_TO_RESULT(destination->GetFileSize(&destSize))); + + int64_t origSize = 0; + QM_TRY(QM_TO_RESULT(original->GetFileSize(&origSize))); + + MOZ_ASSERT(destSize == origSize); +#endif + + return destination; +} + +Result<Usage, QMResult> FileSystemFileManager::RemoveFile( + const FileId& aFileId) { + MOZ_ASSERT(!aFileId.IsEmpty()); + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject, + GetFileDestination(mTopDirectory, aFileId)); + + 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))); + + // We could handle this also as a nonexistent file. + if (!isFile) { + return Err(QMResult(NS_ERROR_FILE_IS_DIRECTORY)); + } + + Usage totalUsage = 0; +#ifdef DEBUG + QM_TRY_UNWRAP(totalUsage, + QM_TO_RESULT_INVOKE_MEMBER(pathObject, GetFileSize)); +#endif + + QM_TRY(QM_TO_RESULT(pathObject->Remove(/* recursive */ false))); + + return totalUsage; +} + +Result<DebugOnly<Usage>, QMResult> FileSystemFileManager::RemoveFiles( + const nsTArray<FileId>& aFileIds, nsTArray<FileId>& aFailedRemovals) { + if (aFileIds.IsEmpty()) { + return DebugOnly<Usage>(0); + } + + CheckedInt64 totalUsage = 0; + for (const auto& someId : aFileIds) { + QM_WARNONLY_TRY_UNWRAP(Maybe<nsCOMPtr<nsIFile>> maybeFile, + GetFileDestination(mTopDirectory, someId)); + if (!maybeFile) { + aFailedRemovals.AppendElement(someId); + continue; + } + nsCOMPtr<nsIFile> fileObject = maybeFile.value(); + +// Size recorded at close is checked to be equal to the sum of sizes on disk +#ifdef DEBUG + QM_WARNONLY_TRY_UNWRAP(Maybe<Usage> fileSize, GetFileSize(fileObject)); + if (!fileSize) { + aFailedRemovals.AppendElement(someId); + continue; + } + totalUsage += fileSize.value(); +#endif + + QM_WARNONLY_TRY_UNWRAP(Maybe<Ok> ok, + MOZ_TO_RESULT(RemoveFileObject(fileObject))); + if (!ok) { + aFailedRemovals.AppendElement(someId); + } + } + + MOZ_ASSERT(totalUsage.isValid()); + + return DebugOnly<Usage>(totalUsage.value()); +} + +} // 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..46391db6f7 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemFileManager.h @@ -0,0 +1,175 @@ +/* -*- 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/UniquePtr.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 { + +struct FileId; + +namespace 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 quota::OriginMetadata& aOriginMetadata); + +/** + * @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 quota::OriginMetadata& aOriginMetadata); + +/** + * @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 quota::OriginMetadata& aOriginMetadata, + 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<UniquePtr<FileSystemFileManager>, QMResult> + CreateFileSystemFileManager(const quota::OriginMetadata& aOriginMetadata); + + /** + * @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 FileId& aFileId) const; + + /** + * @brief Get or create a disk-backed file object for a specified entry id. + * + * @param aFileId Specified id of a file system entry + * @return Result<nsCOMPtr<nsIFile>, QMResult> File abstraction or IO error + */ + Result<nsCOMPtr<nsIFile>, QMResult> GetOrCreateFile(const FileId& aFileId); + + /** + * @brief Create a disk-backed file object as a copy. + * + * @param aDestinationFileId Specified id of file to be created + * @param aSourceFileId Specified id of the file from which we make a copy + * @return Result<nsCOMPtr<nsIFile>, QMResult> File abstraction or IO error + */ + Result<nsCOMPtr<nsIFile>, QMResult> CreateFileFrom( + const FileId& aDestinationFileId, const FileId& aSourceFileId); + + /** + * @brief Remove the disk-backed file object for a specified entry id. + * Note: The returned value is 0 in release builds. + * + * @param aFileId Specified id of a file system entry + * @return Result<Usage, QMResult> Error or file size + */ + Result<Usage, QMResult> RemoveFile(const FileId& aFileId); + + /** + * @brief This method can be used to try to delete a group of files from the + * disk. In debug builds, the sum of the usages is provided ad return value, + * in release builds the sum is not calculated. + * The method attempts to remove all the files requested. + */ + Result<DebugOnly<Usage>, QMResult> RemoveFiles( + const nsTArray<FileId>& aFileIds, nsTArray<FileId>& aFailedRemovals); + + private: + explicit FileSystemFileManager(nsCOMPtr<nsIFile>&& aTopDirectory); + + nsCOMPtr<nsIFile> mTopDirectory; +}; + +} // namespace data +} // namespace fs +} // 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..cf6eee72cc --- /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 "ResultStatement.h" +#include "StartedTransaction.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 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, kRootString)); + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConn)); + + { + 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, kRootString))); + QM_TRY(MOZ_TO_RESULT(stmt.Execute())); + } + + return transaction.Commit(); +} + +} // namespace + +nsresult SetEncoding(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL(R"(PRAGMA encoding = "UTF-16";)"_ns); +} + +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)); + + QM_TRY_UNWRAP(bool tablesExist, stmt.YesOrNoQuery()); + + return !tablesExist; +}; + +nsresult SchemaVersion001::CreateTables(ResultConnection& aConn, + const Origin& aOrigin) { + QM_TRY(MOZ_TO_RESULT(CreateEntries(aConn))); + QM_TRY(MOZ_TO_RESULT(CreateDirectories(aConn))); + QM_TRY(MOZ_TO_RESULT(CreateFiles(aConn))); + QM_TRY(MOZ_TO_RESULT(CreateUsages(aConn))); + QM_TRY(MOZ_TO_RESULT(CreateRootEntry(aConn, aOrigin))); + + return NS_OK; +} + +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(¤tVersion))); + } + + if (currentVersion < sVersion) { + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConn)); + + QM_TRY(QM_TO_RESULT(SchemaVersion001::CreateTables(aConn, aOrigin))); + QM_TRY(QM_TO_RESULT(aConn->SetSchemaVersion(sVersion))); + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + } + + QM_TRY(QM_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns))); + + QM_TRY(QM_TO_RESULT(aConn->GetSchemaVersion(¤tVersion))); + + 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..72d38e4dfd --- /dev/null +++ b/dom/fs/parent/datamodel/SchemaVersion001.h @@ -0,0 +1,31 @@ +/* -*- 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 nsresult CreateTables(ResultConnection& aConn, const Origin& aOrigin); + + static Result<DatabaseVersion, QMResult> InitializeConnection( + ResultConnection& aConn, const Origin& aOrigin); + + static constexpr DatabaseVersion sVersion = 1; +}; + +nsresult SetEncoding(ResultConnection& aConn); + +Result<bool, QMResult> CheckIfEmpty(ResultConnection& aConn); + +} // namespace mozilla::dom::fs + +#endif // DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION001_H_ diff --git a/dom/fs/parent/datamodel/SchemaVersion002.cpp b/dom/fs/parent/datamodel/SchemaVersion002.cpp new file mode 100644 index 0000000000..57d4736b88 --- /dev/null +++ b/dom/fs/parent/datamodel/SchemaVersion002.cpp @@ -0,0 +1,618 @@ +/* -*- 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 "SchemaVersion002.h" + +#include "FileSystemFileManager.h" +#include "FileSystemHashSource.h" +#include "FileSystemHashStorageFunction.h" +#include "ResultStatement.h" +#include "StartedTransaction.h" +#include "fs/FileSystemConstants.h" +#include "mozStorageHelper.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "nsID.h" + +namespace mozilla::dom::fs { + +namespace { + +nsresult CreateFileIds(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "CREATE TABLE IF NOT EXISTS FileIds ( " + "fileId BLOB PRIMARY KEY, " + "handle BLOB, " + "FOREIGN KEY (handle) " + "REFERENCES Files (handle) " + "ON DELETE SET NULL ) " + ";"_ns); +} + +nsresult CreateMainFiles(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "CREATE TABLE IF NOT EXISTS MainFiles ( " + "handle BLOB UNIQUE, " + "fileId BLOB UNIQUE, " + "FOREIGN KEY (handle) REFERENCES Files (handle) " + "ON DELETE CASCADE, " + "FOREIGN KEY (fileId) REFERENCES FileIds (fileId) " + "ON DELETE SET NULL ) " + ";"_ns); +} + +nsresult PopulateFileIds(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "INSERT OR IGNORE INTO FileIds ( fileId, handle ) " + "SELECT handle, handle FROM Files " + ";"_ns); +} + +nsresult PopulateMainFiles(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "INSERT OR IGNORE INTO MainFiles ( fileId, handle ) " + "SELECT handle, handle FROM Files " + ";"_ns); +} + +Result<Ok, QMResult> ClearInvalidFileIds( + ResultConnection& aConn, data::FileSystemFileManager& aFileManager) { + // We cant't just clear all file ids because if a file was accessed using + // writable file stream a new file id was created which is not the same as + // entry id. + + // Get all file ids first. + QM_TRY_INSPECT( + const auto& allFileIds, + ([&aConn]() -> Result<nsTArray<FileId>, QMResult> { + const nsLiteralCString allFileIdsQuery = + "SELECT fileId FROM FileIds;"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, allFileIdsQuery)); + + nsTArray<FileId> fileIds; + + while (true) { + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + if (!moreResults) { + break; + } + + QM_TRY_UNWRAP(FileId fileId, stmt.GetFileIdByColumn(/* Column */ 0u)); + + fileIds.AppendElement(fileId); + } + + return std::move(fileIds); + }())); + + // Filter out file ids which have non-zero-sized files on disk. + QM_TRY_INSPECT(const auto& invalidFileIds, + ([&aFileManager](const nsTArray<FileId>& aFileIds) + -> Result<nsTArray<FileId>, QMResult> { + nsTArray<FileId> fileIds; + + for (const auto& fileId : aFileIds) { + QM_TRY_UNWRAP(auto file, aFileManager.GetFile(fileId)); + + QM_TRY_INSPECT(const bool& exists, + QM_TO_RESULT_INVOKE_MEMBER(file, Exists)); + + if (exists) { + QM_TRY_INSPECT( + const int64_t& fileSize, + QM_TO_RESULT_INVOKE_MEMBER(file, GetFileSize)); + + if (fileSize != 0) { + continue; + } + + QM_TRY(QM_TO_RESULT(file->Remove(false))); + } + + fileIds.AppendElement(fileId); + } + + return std::move(fileIds); + }(allFileIds))); + + // Finally, clear invalid file ids. + QM_TRY(([&aConn](const nsTArray<FileId>& aFileIds) -> Result<Ok, QMResult> { + for (const auto& fileId : aFileIds) { + const nsLiteralCString clearFileIdsQuery = + "DELETE FROM FileIds " + "WHERE fileId = :fileId " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, clearFileIdsQuery)); + + QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, fileId))); + + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + return Ok{}; + }(invalidFileIds))); + + return Ok{}; +} + +Result<Ok, QMResult> ClearInvalidMainFiles( + ResultConnection& aConn, data::FileSystemFileManager& aFileManager) { + // We cant't just clear all main files because if a file was accessed using + // writable file stream a new main file was created which is not the same as + // entry id. + + // Get all main files first. + QM_TRY_INSPECT( + const auto& allMainFiles, + ([&aConn]() -> Result<nsTArray<std::pair<EntryId, FileId>>, QMResult> { + const nsLiteralCString allMainFilesQuery = + "SELECT handle, fileId FROM MainFiles;"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, allMainFilesQuery)); + + nsTArray<std::pair<EntryId, FileId>> mainFiles; + + while (true) { + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + if (!moreResults) { + break; + } + + QM_TRY_UNWRAP(EntryId entryId, + stmt.GetEntryIdByColumn(/* Column */ 0u)); + QM_TRY_UNWRAP(FileId fileId, stmt.GetFileIdByColumn(/* Column */ 1u)); + + mainFiles.AppendElement(std::pair<EntryId, FileId>(entryId, fileId)); + } + + return std::move(mainFiles); + }())); + + // Filter out main files which have non-zero-sized files on disk. + QM_TRY_INSPECT( + const auto& invalidMainFiles, + ([&aFileManager](const nsTArray<std::pair<EntryId, FileId>>& aMainFiles) + -> Result<nsTArray<std::pair<EntryId, FileId>>, QMResult> { + nsTArray<std::pair<EntryId, FileId>> mainFiles; + + for (const auto& mainFile : aMainFiles) { + QM_TRY_UNWRAP(auto file, aFileManager.GetFile(mainFile.second)); + + QM_TRY_INSPECT(const bool& exists, + QM_TO_RESULT_INVOKE_MEMBER(file, Exists)); + + if (exists) { + QM_TRY_INSPECT(const int64_t& fileSize, + QM_TO_RESULT_INVOKE_MEMBER(file, GetFileSize)); + + if (fileSize != 0) { + continue; + } + + QM_TRY(QM_TO_RESULT(file->Remove(false))); + } + + mainFiles.AppendElement(mainFile); + } + + return std::move(mainFiles); + }(allMainFiles))); + + // Finally, clear invalid main files. + QM_TRY(([&aConn](const nsTArray<std::pair<EntryId, FileId>>& aMainFiles) + -> Result<Ok, QMResult> { + for (const auto& mainFile : aMainFiles) { + const nsLiteralCString clearMainFilesQuery = + "DELETE FROM MainFiles " + "WHERE handle = :entryId AND fileId = :fileId " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, clearMainFilesQuery)); + + QM_TRY( + QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, mainFile.first))); + QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, mainFile.second))); + + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + return Ok{}; + }(invalidMainFiles))); + + return Ok{}; +} + +nsresult ConnectUsagesToFileIds(ResultConnection& aConn) { + QM_TRY( + MOZ_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_keys = OFF;"_ns))); + + auto turnForeignKeysBackOn = MakeScopeExit([&aConn]() { + QM_WARNONLY_TRY( + MOZ_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns))); + }); + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConn)); + + QM_TRY(MOZ_TO_RESULT( + aConn->ExecuteSimpleSQL("DROP TABLE IF EXISTS migrateUsages ;"_ns))); + + QM_TRY(MOZ_TO_RESULT(aConn->ExecuteSimpleSQL( + "CREATE TABLE migrateUsages ( " + "handle BLOB PRIMARY KEY, " + "usage INTEGER NOT NULL DEFAULT 0, " + "tracked BOOLEAN NOT NULL DEFAULT 0 CHECK (tracked IN (0, 1)), " + "CONSTRAINT handles_are_fileIds " + "FOREIGN KEY (handle) " + "REFERENCES FileIds (fileId) " + "ON DELETE CASCADE ) " + ";"_ns))); + + QM_TRY(MOZ_TO_RESULT(aConn->ExecuteSimpleSQL( + "INSERT INTO migrateUsages ( handle, usage, tracked ) " + "SELECT handle, usage, tracked FROM Usages ;"_ns))); + + QM_TRY(MOZ_TO_RESULT(aConn->ExecuteSimpleSQL("DROP TABLE Usages;"_ns))); + + QM_TRY(MOZ_TO_RESULT(aConn->ExecuteSimpleSQL( + "ALTER TABLE migrateUsages RENAME TO Usages;"_ns))); + + QM_TRY( + MOZ_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_key_check;"_ns))); + + QM_TRY(MOZ_TO_RESULT(transaction.Commit())); + + return NS_OK; +} + +nsresult CreateEntryNamesView(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "CREATE VIEW IF NOT EXISTS EntryNames AS " + "SELECT isFile, handle, parent, name FROM Entries INNER JOIN ( " + "SELECT 1 AS isFile, handle, name FROM Files UNION " + "SELECT 0, handle, name FROM Directories ) " + "USING (handle) " + ";"_ns); +} + +nsresult FixEntryIds(const ResultConnection& aConnection, + const EntryId& aRootEntry) { + const nsLiteralCString calculateHashesQuery = + "CREATE TEMPORARY TABLE EntryMigrationTable AS " + "WITH RECURSIVE " + "rehashMap( depth, isFile, handle, parent, name, hash ) AS ( " + "SELECT 0, isFile, handle, parent, name, hashEntry( :rootEntry, name ) " + "FROM EntryNames WHERE parent = :rootEntry UNION SELECT " + "1 + depth, EntryNames.isFile, EntryNames.handle, EntryNames.parent, " + "EntryNames.name, hashEntry( rehashMap.hash, EntryNames.name ) " + "FROM rehashMap, EntryNames WHERE rehashMap.handle = EntryNames.parent ) " + "SELECT depth, isFile, handle, parent, name, hash FROM rehashMap " + ";"_ns; + + const nsLiteralCString createIndexByDepthQuery = + "CREATE INDEX indexOnDepth ON EntryMigrationTable ( depth ); "_ns; + + // To avoid constraint violation, new entries are inserted under a temporary + // parent. + + const nsLiteralCString insertTemporaryParentEntry = + "INSERT INTO Entries ( handle, parent ) " + "VALUES ( :tempParent, :rootEntry ) ;"_ns; + + const nsLiteralCString flagTemporaryParentAsDir = + "INSERT INTO Directories ( handle, name ) " + "VALUES ( :tempParent, 'temp' ) ;"_ns; + + const nsLiteralCString insertNewEntriesQuery = + "INSERT INTO Entries ( handle, parent ) " + "SELECT hash, :tempParent FROM EntryMigrationTable WHERE hash != handle " + ";"_ns; + + const nsLiteralCString insertNewDirectoriesQuery = + "INSERT INTO Directories ( handle, name ) " + "SELECT hash, name FROM EntryMigrationTable " + "WHERE isFile = 0 AND hash != handle " + "ORDER BY depth " + ";"_ns; + + const nsLiteralCString insertNewFilesQuery = + "INSERT INTO Files ( handle, type, name ) " + "SELECT EntryMigrationTable.hash, Files.type, EntryMigrationTable.name " + "FROM EntryMigrationTable INNER JOIN Files USING (handle) " + "WHERE EntryMigrationTable.isFile = 1 AND hash != handle " + ";"_ns; + + const nsLiteralCString updateFileMappingsQuery = + "UPDATE FileIds SET handle = hash " + "FROM ( SELECT handle, hash FROM EntryMigrationTable WHERE hash != " + "handle ) " + "AS replacement WHERE FileIds.handle = replacement.handle " + ";"_ns; + + const nsLiteralCString updateMainFilesQuery = + "UPDATE MainFiles SET handle = hash " + "FROM ( SELECT handle, hash FROM EntryMigrationTable WHERE hash != " + "handle ) " + "AS replacement WHERE MainFiles.handle = replacement.handle " + ";"_ns; + + // Now fix the parents. + const nsLiteralCString updateEntryMappingsQuery = + "UPDATE Entries SET parent = hash " + "FROM ( SELECT Lhs.hash AS handle, Rhs.hash AS hash, Lhs.depth AS depth " + "FROM EntryMigrationTable AS Lhs " + "INNER JOIN EntryMigrationTable AS Rhs " + "ON Rhs.handle = Lhs.parent ORDER BY depth ) AS replacement " + "WHERE Entries.handle = replacement.handle " + "AND Entries.parent = :tempParent " + ";"_ns; + + const nsLiteralCString cleanupOldEntriesQuery = + "DELETE FROM Entries WHERE handle IN " + "( SELECT handle FROM EntryMigrationTable WHERE hash != handle ) " + ";"_ns; + + const nsLiteralCString cleanupTemporaryParent = + "DELETE FROM Entries WHERE handle = :tempParent ;"_ns; + + const nsLiteralCString dropIndexByDepthQuery = + "DROP INDEX indexOnDepth ; "_ns; + + // Index is automatically deleted + const nsLiteralCString cleanupTemporaries = + "DROP TABLE EntryMigrationTable ;"_ns; + + EntryId tempParent(nsCString(nsID::GenerateUUID().ToString().get())); + + nsCOMPtr<mozIStorageFunction> rehashFunction = + new data::FileSystemHashStorageFunction(); + QM_TRY(MOZ_TO_RESULT(aConnection->CreateFunction("hashEntry"_ns, + /* number of arguments */ 2, + rehashFunction))); + auto finallyRemoveFunction = MakeScopeExit([&aConnection]() { + QM_WARNONLY_TRY(MOZ_TO_RESULT(aConnection->RemoveFunction("hashEntry"_ns))); + }); + + // We need this to make sure the old entries get removed + QM_TRY(MOZ_TO_RESULT( + aConnection->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns))); + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConnection)); + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, calculateHashesQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("rootEntry"_ns, aRootEntry))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + QM_TRY(QM_TO_RESULT(aConnection->ExecuteSimpleSQL(createIndexByDepthQuery))); + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, insertTemporaryParentEntry)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("tempParent"_ns, tempParent))); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("rootEntry"_ns, aRootEntry))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, flagTemporaryParentAsDir)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("tempParent"_ns, tempParent))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewEntriesQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("tempParent"_ns, tempParent))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewDirectoriesQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewFilesQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, updateFileMappingsQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, updateMainFilesQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, updateEntryMappingsQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("tempParent"_ns, tempParent))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, cleanupOldEntriesQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, cleanupTemporaryParent)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("tempParent"_ns, tempParent))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + QM_TRY(QM_TO_RESULT(aConnection->ExecuteSimpleSQL(dropIndexByDepthQuery))); + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, cleanupTemporaries)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + QM_WARNONLY_TRY(QM_TO_RESULT(aConnection->ExecuteSimpleSQL("VACUUM;"_ns))); + + return NS_OK; +} + +} // namespace + +Result<DatabaseVersion, QMResult> SchemaVersion002::InitializeConnection( + ResultConnection& aConn, data::FileSystemFileManager& aFileManager, + const Origin& aOrigin) { + QM_TRY_UNWRAP(const bool wasEmpty, CheckIfEmpty(aConn)); + + DatabaseVersion currentVersion = 0; + + if (wasEmpty) { + QM_TRY(QM_TO_RESULT(SetEncoding(aConn))); + } else { + QM_TRY(QM_TO_RESULT(aConn->GetSchemaVersion(¤tVersion))); + } + + if (currentVersion < sVersion) { + MOZ_ASSERT_IF(0 != currentVersion, 1 == currentVersion); + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConn)); + + if (0 == currentVersion) { + QM_TRY(QM_TO_RESULT(SchemaVersion001::CreateTables(aConn, aOrigin))); + } + + QM_TRY(QM_TO_RESULT(CreateFileIds(aConn))); + + if (!wasEmpty) { + QM_TRY(QM_TO_RESULT(PopulateFileIds(aConn))); + } + + QM_TRY(QM_TO_RESULT(ConnectUsagesToFileIds(aConn))); + + QM_TRY(QM_TO_RESULT(CreateMainFiles(aConn))); + if (!wasEmpty) { + QM_TRY(QM_TO_RESULT(PopulateMainFiles(aConn))); + } + + QM_TRY(QM_TO_RESULT(CreateEntryNamesView(aConn))); + + QM_TRY(QM_TO_RESULT(aConn->SetSchemaVersion(sVersion))); + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + if (!wasEmpty) { + QM_TRY(QM_TO_RESULT(aConn->ExecuteSimpleSQL("VACUUM;"_ns))); + } + } + + // The upgrade from version 1 to version 2 was buggy, so we have to check if + // the Usages table still references the Files table which is a sign that + // the upgrade wasn't complete. This extra query has only negligible perf + // impact. See bug 1847989. + auto UsagesTableRefsFilesTable = [&aConn]() -> Result<bool, QMResult> { + const nsLiteralCString query = + "SELECT pragma_foreign_key_list.'table'=='Files' " + "FROM pragma_foreign_key_list('Usages');"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, ResultStatement::Create(aConn, query)); + + return stmt.YesOrNoQuery(); + }; + + QM_TRY_UNWRAP(auto usagesTableRefsFilesTable, UsagesTableRefsFilesTable()); + + if (usagesTableRefsFilesTable) { + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConn)); + + // The buggy upgrade didn't call PopulateFileIds, ConnectUsagesToFileIds + // and PopulateMainFiles was completely missing. Since invalid file ids + // and main files could be inserted when the profile was broken, we need + // to clear them before populating. + QM_TRY(ClearInvalidFileIds(aConn, aFileManager)); + QM_TRY(QM_TO_RESULT(PopulateFileIds(aConn))); + QM_TRY(QM_TO_RESULT(ConnectUsagesToFileIds(aConn))); + QM_TRY(ClearInvalidMainFiles(aConn, aFileManager)); + QM_TRY(QM_TO_RESULT(PopulateMainFiles(aConn))); + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + QM_TRY(QM_TO_RESULT(aConn->ExecuteSimpleSQL("VACUUM;"_ns))); + + QM_TRY_UNWRAP(usagesTableRefsFilesTable, UsagesTableRefsFilesTable()); + MOZ_ASSERT(!usagesTableRefsFilesTable); + } + + // In schema version 001, entryId was unique but not necessarily related to + // a path. For schema 002, we have to fix all entryIds to be derived from + // the underlying path. + auto OneTimeRehashingDone = [&aConn]() -> Result<bool, QMResult> { + const nsLiteralCString query = + "SELECT EXISTS (SELECT 1 FROM sqlite_master " + "WHERE type='table' AND name='RehashedFrom001to002' ) ;"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, ResultStatement::Create(aConn, query)); + + return stmt.YesOrNoQuery(); + }; + + QM_TRY_UNWRAP(auto oneTimeRehashingDone, OneTimeRehashingDone()); + + if (!oneTimeRehashingDone) { + const nsLiteralCString findRootEntry = + "SELECT handle FROM Entries WHERE parent IS NULL ;"_ns; + + EntryId rootId; + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, findRootEntry)); + + QM_TRY_UNWRAP(DebugOnly<bool> moreResults, stmt.ExecuteStep()); + MOZ_ASSERT(moreResults); + + QM_TRY_UNWRAP(rootId, stmt.GetEntryIdByColumn(/* Column */ 0u)); + } + + MOZ_ASSERT(!rootId.IsEmpty()); + + QM_TRY(QM_TO_RESULT(FixEntryIds(aConn, rootId))); + + QM_TRY(QM_TO_RESULT(aConn->ExecuteSimpleSQL( + "CREATE TABLE RehashedFrom001to002 (id INTEGER PRIMARY KEY);"_ns))); + + QM_TRY_UNWRAP(DebugOnly<bool> isDoneNow, OneTimeRehashingDone()); + MOZ_ASSERT(isDoneNow); + } + + QM_TRY(QM_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns))); + + QM_TRY(QM_TO_RESULT(aConn->GetSchemaVersion(¤tVersion))); + + return currentVersion; +} + +} // namespace mozilla::dom::fs diff --git a/dom/fs/parent/datamodel/SchemaVersion002.h b/dom/fs/parent/datamodel/SchemaVersion002.h new file mode 100644 index 0000000000..60f2aa53cb --- /dev/null +++ b/dom/fs/parent/datamodel/SchemaVersion002.h @@ -0,0 +1,28 @@ +/* -*- 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_SCHEMAVERSION002_H_ +#define DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION002_H_ + +#include "SchemaVersion001.h" + +namespace mozilla::dom::fs { + +namespace data { +class FileSystemFileManager; +} // namespace data + +struct SchemaVersion002 { + static Result<DatabaseVersion, QMResult> InitializeConnection( + ResultConnection& aConn, data::FileSystemFileManager& aFileManager, + const Origin& aOrigin); + + static constexpr DatabaseVersion sVersion = 2; +}; + +} // namespace mozilla::dom::fs + +#endif // DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION002_H_ diff --git a/dom/fs/parent/datamodel/moz.build b/dom/fs/parent/datamodel/moz.build new file mode 100644 index 0000000000..8bb5d35381 --- /dev/null +++ b/dom/fs/parent/datamodel/moz.build @@ -0,0 +1,28 @@ +# -*- 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", + "FileSystemDatabaseManagerVersion002.cpp", + "FileSystemDataManager.cpp", + "FileSystemFileManager.cpp", + "SchemaVersion001.cpp", + "SchemaVersion002.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/fs/include", + "/dom/fs/parent", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/fs/parent/moz.build b/dom/fs/parent/moz.build new file mode 100644 index 0000000000..95298ad0f2 --- /dev/null +++ b/dom/fs/parent/moz.build @@ -0,0 +1,63 @@ +# -*- 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/. + +DIRS += [ + "datamodel", +] + +EXPORTS.mozilla.dom += [ + "FileSystemAccessHandle.h", + "FileSystemAccessHandleControlParent.h", + "FileSystemAccessHandleParent.h", + "FileSystemManagerParent.h", + "FileSystemManagerParentFactory.h", + "FileSystemParentTypes.h", + "FileSystemQuotaClient.h", + "FileSystemQuotaClientFactory.h", + "FileSystemWritableFileStreamParent.h", +] + +UNIFIED_SOURCES += [ + "FileSystemAccessHandle.cpp", + "FileSystemAccessHandleControlParent.cpp", + "FileSystemAccessHandleParent.cpp", + "FileSystemContentTypeGuess.cpp", + "FileSystemHashSource.cpp", + "FileSystemHashStorageFunction.cpp", + "FileSystemManagerParent.cpp", + "FileSystemManagerParentFactory.cpp", + "FileSystemQuotaClient.cpp", + "FileSystemQuotaClientFactory.cpp", + "FileSystemStreamCallbacks.cpp", + "FileSystemWritableFileStreamParent.cpp", + "ResultStatement.cpp", + "StartedTransaction.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/fs/include", + "/dom/fs/parent/datamodel", +] + +FINAL_LIBRARY = "xul" + +include("/ipc/chromium/chromium-config.mozbuild") + +if CONFIG["COMPILE_ENVIRONMENT"]: + CbindgenHeader( + "data_encoding_ffi_generated.h", + inputs=["/dom/fs/parent/rust/data-encoding-ffi"], + ) + + CbindgenHeader( + "mime_guess_ffi_generated.h", + inputs=["/dom/fs/parent/rust/mime-guess-ffi"], + ) + + EXPORTS.mozilla.dom += [ + "!data_encoding_ffi_generated.h", + "!mime_guess_ffi_generated.h", + ] diff --git a/dom/fs/parent/rust/data-encoding-ffi/Cargo.toml b/dom/fs/parent/rust/data-encoding-ffi/Cargo.toml new file mode 100644 index 0000000000..1a641e5c7b --- /dev/null +++ b/dom/fs/parent/rust/data-encoding-ffi/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "data-encoding-ffi" +version = "0.1.0" +license = "MPL-2.0" +authors = ["The Mozilla Project Developers"] + +[dependencies] +data-encoding = "2.2.1" +nsstring = { path = "../../../../../xpcom/rust/nsstring" } diff --git a/dom/fs/parent/rust/data-encoding-ffi/cbindgen.toml b/dom/fs/parent/rust/data-encoding-ffi/cbindgen.toml new file mode 100644 index 0000000000..37da12d3b7 --- /dev/null +++ b/dom/fs/parent/rust/data-encoding-ffi/cbindgen.toml @@ -0,0 +1,11 @@ +header = """/* 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/. */""" +autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */""" +include_guard = "DOM_FS_PARENT_RUST_DATA_ENCODING_FFI_H_" +include_version = true +braces = "SameLine" +line_length = 100 +tab_width = 2 +language = "C++" +namespaces = ["mozilla", "dom", "fs"] diff --git a/dom/fs/parent/rust/data-encoding-ffi/src/lib.rs b/dom/fs/parent/rust/data-encoding-ffi/src/lib.rs new file mode 100644 index 0000000000..74900b62ae --- /dev/null +++ b/dom/fs/parent/rust/data-encoding-ffi/src/lib.rs @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate data_encoding; +extern crate nsstring; + +use data_encoding::BASE32; +use nsstring::{nsACString, nsCString}; + +#[no_mangle] +pub extern "C" fn base32encode(unencoded: &nsACString, encoded: &mut nsCString) { + encoded.assign(&BASE32.encode(&unencoded[..])); +} diff --git a/dom/fs/parent/rust/mime-guess-ffi/Cargo.toml b/dom/fs/parent/rust/mime-guess-ffi/Cargo.toml new file mode 100644 index 0000000000..d97f8202d7 --- /dev/null +++ b/dom/fs/parent/rust/mime-guess-ffi/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "mime-guess-ffi" +version = "0.1.0" +license = "MPL-2.0" +authors = ["The Mozilla Project Developers"] + +[dependencies] +mime_guess = "2.0.4" +nserror = { path = "../../../../../xpcom/rust/nserror" } +nsstring = { path = "../../../../../xpcom/rust/nsstring" } diff --git a/dom/fs/parent/rust/mime-guess-ffi/cbindgen.toml b/dom/fs/parent/rust/mime-guess-ffi/cbindgen.toml new file mode 100644 index 0000000000..d7f94927ab --- /dev/null +++ b/dom/fs/parent/rust/mime-guess-ffi/cbindgen.toml @@ -0,0 +1,11 @@ +header = """/* 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/. */""" +autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */""" +include_guard = "DOM_FS_PARENT_RUST_MIME_GUESS_FFI_H_" +include_version = true +braces = "SameLine" +line_length = 100 +tab_width = 2 +language = "C++" +namespaces = ["mozilla", "dom", "fs"] diff --git a/dom/fs/parent/rust/mime-guess-ffi/src/lib.rs b/dom/fs/parent/rust/mime-guess-ffi/src/lib.rs new file mode 100644 index 0000000000..19833fd202 --- /dev/null +++ b/dom/fs/parent/rust/mime-guess-ffi/src/lib.rs @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate mime_guess; +extern crate nserror; +extern crate nsstring; + +use nserror::{nsresult, NS_ERROR_FAILURE, NS_ERROR_INVALID_ARG, NS_ERROR_NOT_AVAILABLE, NS_OK}; +use nsstring::{nsACString, nsCString}; +use std::path::Path; +use std::str; + +#[no_mangle] +pub extern "C" fn mimeGuessFromPath(path: &nsACString, content_type: &mut nsCString) -> nsresult { + let path_data = str::from_utf8(path.as_ref()); + if path_data.is_err() { + return NS_ERROR_INVALID_ARG; // Not UTF8 + } + + let content_path = Path::new(path_data.unwrap()); + if content_path.extension().is_none() { + return NS_ERROR_NOT_AVAILABLE; // No mime type information + } + + let maybe_mime = mime_guess::from_path(content_path).first_raw(); + if maybe_mime.is_none() { + return NS_ERROR_FAILURE; // Not recognized + } + + content_type.assign(maybe_mime.unwrap()); + + NS_OK +} |