diff options
Diffstat (limited to '')
41 files changed, 5528 insertions, 0 deletions
diff --git a/dom/fs/parent/FileSystemAccessHandle.cpp b/dom/fs/parent/FileSystemAccessHandle.cpp new file mode 100644 index 0000000000..61be18445e --- /dev/null +++ b/dom/fs/parent/FileSystemAccessHandle.cpp @@ -0,0 +1,232 @@ +/* -*- 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 "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(MOZ_TO_RESULT(mDataManager->LockExclusive(mEntryId)), + [](const nsresult aRv) { + return InitPromise::CreateAndReject(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, 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..58a0e9901f --- /dev/null +++ b/dom/fs/parent/FileSystemContentTypeGuess.cpp @@ -0,0 +1,21 @@ +/* -*- 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 "mozilla/dom/mime_guess_ffi_generated.h" +#include "nsString.h" + +namespace mozilla::dom::fs { + +ContentType FileSystemContentTypeGuess::FromPath(const Name& aPath) { + NS_ConvertUTF16toUTF8 path(aPath); + ContentType contentType; + mimeGuessFromPath(&path, &contentType); + 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..08a3324942 --- /dev/null +++ b/dom/fs/parent/FileSystemContentTypeGuess.h @@ -0,0 +1,21 @@ +/* -*- 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/dom/FileSystemTypes.h" +#include "nsStringFwd.h" + +namespace mozilla::dom::fs { + +struct FileSystemContentTypeGuess { + static ContentType 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..eb43575371 --- /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 "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 EntryId& aEntryId) { + MOZ_ASSERT(32u == aEntryId.Length()); + nsCString encoded; + base32encode(&aEntryId, &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..2b7ca0136f --- /dev/null +++ b/dom/fs/parent/FileSystemHashSource.h @@ -0,0 +1,26 @@ +/* -*- 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::data { + +struct FileSystemHashSource { + static Result<EntryId, QMResult> GenerateHash(const EntryId& aParent, + const Name& aName); + + static Result<Name, QMResult> EncodeHash(const EntryId& aEntryId); +}; + +} // namespace mozilla::dom::fs::data + +#endif // DOM_FS_PARENT_FILESYSTEMHASHSOURCE_H_ diff --git a/dom/fs/parent/FileSystemManagerParent.cpp b/dom/fs/parent/FileSystemManagerParent.cpp new file mode 100644 index 0000000000..a63a355f79 --- /dev/null +++ b/dom/fs/parent/FileSystemManagerParent.cpp @@ -0,0 +1,496 @@ +/* -*- 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 "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)); +} + +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); + }; + + const ContentType& type = VoidCString(); // Currently cannot be set by user + QM_TRY_UNWRAP(fs::EntryId entryId, + mDataManager->MutableDatabaseManagerPtr()->GetOrCreateFile( + aRequest.handle(), type, 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); + + auto reportError = [aResolver](nsresult rv) { aResolver(rv); }; + const EntryId& entryId = aRequest.entryId(); + // TODO: Change to LockShared after temporary files + QM_TRY(MOZ_TO_RESULT(mDataManager->LockExclusive(entryId)), IPC_OK(), + reportError); + + auto autoUnlock = MakeScopeExit([self = RefPtr{this}, &entryId] { + // TODO: Change to UnlockShared after temporary files + self->mDataManager->UnlockExclusive(entryId); + }); + + fs::ContentType type; + fs::TimeStamp lastModifiedMilliSeconds; + fs::Path path; + nsCOMPtr<nsIFile> file; + QM_TRY(MOZ_TO_RESULT(mDataManager->MutableDatabaseManagerPtr()->GetFile( + entryId, 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()); + + 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](nsresult rv) { + LOG(("getFile() Failed!")); + aResolver(rv); + }; + + fs::ContentType type; + fs::TimeStamp lastModifiedMilliSeconds; + fs::Path path; + nsCOMPtr<nsIFile> fileObject; + QM_TRY(MOZ_TO_RESULT(mDataManager->MutableDatabaseManagerPtr()->GetFile( + aRequest.entryId(), 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(NS_OK); + aResolver(response); + + return IPC_OK(); + } + + QM_TRY_UNWRAP(isDeleted, + mDataManager->MutableDatabaseManagerPtr()->RemoveDirectory( + aRequest.handle(), aRequest.recursive()), + IPC_OK(), reportError); + + if (!isDeleted) { + FileSystemRemoveEntryResponse response(NS_ERROR_DOM_NOT_FOUND_ERR); + aResolver(response); + + return IPC_OK(); + } + + FileSystemRemoveEntryResponse response(NS_OK); + 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_UNWRAP(bool moved, + mDataManager->MutableDatabaseManagerPtr()->MoveEntry( + aRequest.handle(), aRequest.destHandle()), + IPC_OK(), reportError); + + fs::FileSystemMoveEntryResponse response(moved ? NS_OK : NS_ERROR_FAILURE); + 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_UNWRAP(bool moved, + mDataManager->MutableDatabaseManagerPtr()->RenameEntry( + aRequest.handle(), aRequest.name()), + IPC_OK(), reportError); + + fs::FileSystemMoveEntryResponse response(moved ? NS_OK : NS_ERROR_FAILURE); + 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..672a430621 --- /dev/null +++ b/dom/fs/parent/FileSystemManagerParent.h @@ -0,0 +1,87 @@ +/* -*- 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; + + // 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 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/FileSystemQuotaClient.cpp b/dom/fs/parent/FileSystemQuotaClient.cpp new file mode 100644 index 0000000000..e1fb3818b7 --- /dev/null +++ b/dom/fs/parent/FileSystemQuotaClient.cpp @@ -0,0 +1,245 @@ +/* -*- 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 "ResultStatement.h" +#include "datamodel/FileSystemDatabaseManager.h" +#include "datamodel/FileSystemFileManager.h" +#include "mozIStorageService.h" +#include "mozStorageCID.h" +#include "mozilla/dom/FileSystemDataManager.h" +#include "mozilla/dom/quota/Assertions.h" +#include "mozilla/dom/quota/Client.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); }; + +class QuotaClient final : public mozilla::dom::quota::Client { + public: + QuotaClient(); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::fs::QuotaClient, 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; + + private: + ~QuotaClient() = default; + + void InitiateShutdown() override; + bool IsShutdownCompleted() const override; + nsCString GetShutdownStatus() const override; + void ForceKillActors() override; + void FinalizeShutdown() override; +}; + +Result<ResultConnection, QMResult> GetStorageConnection( + const quota::OriginMetadata& aOriginMetadata) { + QM_TRY_INSPECT(const nsCOMPtr<nsIFile>& databaseFile, + data::GetDatabaseFile(aOriginMetadata)); + + 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, OpenDatabase, + databaseFile, mozIStorageService::CONNECTION_DEFAULT))); + + ResultConnection result(connection); + + return result; +} + +} // namespace + +QuotaClient::QuotaClient() { ::mozilla::ipc::AssertIsOnBackgroundThread(); } + +mozilla::dom::quota::Client::Type QuotaClient::GetType() { + return quota::Client::Type::FILESYSTEM; +} + +Result<quota::UsageInfo, nsresult> QuotaClient::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, + GetStorageConnection(aOriginMetadata).mapErr(toNSResult)); + + QM_TRY(MOZ_TO_RESULT( + data::FileSystemDatabaseManager::RescanUsages(conn, aOriginMetadata))); + + return data::FileSystemDatabaseManager::GetUsage(conn, aOriginMetadata) + .mapErr(toNSResult); +} + +nsresult QuotaClient::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> QuotaClient::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 QuotaClient::OnOriginClearCompleted( + quota::PersistenceType aPersistenceType, const nsACString& aOrigin) { + quota::AssertIsOnIOThread(); +} + +void QuotaClient::OnRepositoryClearCompleted( + quota::PersistenceType aPersistenceType) { + quota::AssertIsOnIOThread(); +} + +void QuotaClient::ReleaseIOThreadObjects() { quota::AssertIsOnIOThread(); } + +void QuotaClient::AbortOperationsForLocks( + const DirectoryLockIdTable& aDirectoryLockIds) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + data::FileSystemDataManager::AbortOperationsForLocks(aDirectoryLockIds); +} + +void QuotaClient::AbortOperationsForProcess(ContentParentId aContentParentId) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); +} + +void QuotaClient::AbortAllOperations() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); +} + +void QuotaClient::StartIdleMaintenance() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); +} + +void QuotaClient::StopIdleMaintenance() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); +} + +void QuotaClient::InitiateShutdown() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + data::FileSystemDataManager::InitiateShutdown(); +} + +bool QuotaClient::IsShutdownCompleted() const { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + return data::FileSystemDataManager::IsShutdownCompleted(); +} + +nsCString QuotaClient::GetShutdownStatus() const { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + return "Not implemented"_ns; +} + +void QuotaClient::ForceKillActors() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + // Hopefully not needed. +} + +void QuotaClient::FinalizeShutdown() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + // Empty for now. +} + +already_AddRefed<mozilla::dom::quota::Client> CreateQuotaClient() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + RefPtr<QuotaClient> client = new fs::QuotaClient(); + return client.forget(); +} + +} // namespace mozilla::dom::fs diff --git a/dom/fs/parent/FileSystemQuotaClient.h b/dom/fs/parent/FileSystemQuotaClient.h new file mode 100644 index 0000000000..9afc04ee7b --- /dev/null +++ b/dom/fs/parent/FileSystemQuotaClient.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_FILESYSTEMQUOTACLIENT_H_ +#define DOM_FS_PARENT_FILESYSTEMQUOTACLIENT_H_ + +#include "mozilla/AlreadyAddRefed.h" + +namespace mozilla::dom { + +namespace quota { +class Client; +} // namespace quota + +namespace fs { + +already_AddRefed<mozilla::dom::quota::Client> CreateQuotaClient(); + +} // namespace fs +} // namespace mozilla::dom + +#endif // DOM_FS_PARENT_FILESYSTEMQUOTACLIENT_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..a34bed96d2 --- /dev/null +++ b/dom/fs/parent/FileSystemWritableFileStreamParent.cpp @@ -0,0 +1,77 @@ +/* -*- 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) + : mManager(std::move(aManager)), mEntryId(aEntryId) {} + +FileSystemWritableFileStreamParent::~FileSystemWritableFileStreamParent() { + MOZ_ASSERT(mClosed); +} + +mozilla::ipc::IPCResult FileSystemWritableFileStreamParent::RecvClose( + CloseResolver&& aResolver) { + Close(); + + aResolver(void_t()); + + return IPC_OK(); +} + +void FileSystemWritableFileStreamParent::ActorDestroy(ActorDestroyReason aWhy) { + if (mStreamCallbacks) { + mStreamCallbacks->CloseRemoteQuotaObjectParent(); + mStreamCallbacks = nullptr; + } + + if (!IsClosed()) { + Close(); + } +} + +nsIInterfaceRequestor* +FileSystemWritableFileStreamParent::GetOrCreateStreamCallbacks() { + if (!mStreamCallbacks) { + if (mClosed) { + return nullptr; + } + + mStreamCallbacks = MakeRefPtr<FileSystemWritableFileStreamCallbacks>(); + } + + return mStreamCallbacks.get(); +} + +void FileSystemWritableFileStreamParent::Close() { + LOG(("Closing WritableFileStream")); + + mClosed.Flip(); + + // TODO: Change to UnlockShared after temporary files + mManager->DataManagerStrongRef()->UnlockExclusive(mEntryId); +} + +} // namespace mozilla::dom diff --git a/dom/fs/parent/FileSystemWritableFileStreamParent.h b/dom/fs/parent/FileSystemWritableFileStreamParent.h new file mode 100644 index 0000000000..58437b5ae6 --- /dev/null +++ b/dom/fs/parent/FileSystemWritableFileStreamParent.h @@ -0,0 +1,55 @@ +/* -*- 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/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); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileSystemWritableFileStreamParent, + override) + + mozilla::ipc::IPCResult RecvClose(CloseResolver&& aResolver); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + nsIInterfaceRequestor* GetOrCreateStreamCallbacks(); + + private: + class FileSystemWritableFileStreamCallbacks; + + virtual ~FileSystemWritableFileStreamParent(); + + bool IsClosed() const { return mClosed; } + + void Close(); + + const RefPtr<FileSystemManagerParent> mManager; + + RefPtr<FileSystemWritableFileStreamCallbacks> mStreamCallbacks; + + const fs::EntryId mEntryId; + + 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..3c492736e9 --- /dev/null +++ b/dom/fs/parent/ResultStatement.h @@ -0,0 +1,160 @@ +/* -*- 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 "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 BindContentTypeByName(const nsACString& aField, + const ContentType& aValue) { + if (0u == aValue.Length()) { // Otherwise empty string becomes null + return mStmt->BindUTF8StringByName(aField, aValue); + } + + return mStmt->BindUTF8StringAsBlobByName(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<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/datamodel/FileSystemDataManager.cpp b/dom/fs/parent/datamodel/FileSystemDataManager.cpp new file mode 100644 index 0000000000..d86140c317 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDataManager.cpp @@ -0,0 +1,573 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FileSystemDataManager.h" + +#include "FileSystemDatabaseManager.h" +#include "FileSystemDatabaseManagerVersion001.h" +#include "FileSystemFileManager.h" +#include "FileSystemHashSource.h" +#include "ResultStatement.h" +#include "SchemaVersion001.h" +#include "fs/FileSystemConstants.h" +#include "mozIStorageService.h" +#include "mozStorageCID.h" +#include "mozilla/Result.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/dom/FileSystemLog.h" +#include "mozilla/dom/FileSystemManagerParent.h" +#include "mozilla/dom/quota/ClientImpl.h" +#include "mozilla/dom/quota/DirectoryLock.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/dom/quota/UsageInfo.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "nsBaseHashtable.h" +#include "nsCOMPtr.h" +#include "nsHashKeys.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "nsNetCID.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsThreadUtils.h" + +namespace mozilla::dom::fs::data { + +namespace { + +// nsCStringHashKey with disabled memmove +class nsCStringHashKeyDM : public nsCStringHashKey { + public: + explicit nsCStringHashKeyDM(const nsCStringHashKey::KeyTypePointer aKey) + : nsCStringHashKey(aKey) {} + enum { ALLOW_MEMMOVE = false }; +}; + +// When CheckedUnsafePtr's checking is enabled, it's necessary to ensure that +// the hashtable uses the copy constructor instead of memmove for moving entries +// since memmove will break CheckedUnsafePtr in a memory-corrupting way. +using FileSystemDataManagerHashKey = + std::conditional<DiagnosticAssertEnabled::value, nsCStringHashKeyDM, + nsCStringHashKey>::type; + +// Raw (but checked when the diagnostic assert is enabled) references as we +// don't want to keep FileSystemDataManager objects alive forever. When a +// FileSystemDataManager is destroyed it calls RemoveFileSystemDataManager +// to clear itself. +using FileSystemDataManagerHashtable = + nsBaseHashtable<FileSystemDataManagerHashKey, + NotNull<CheckedUnsafePtr<FileSystemDataManager>>, + MovingNotNull<CheckedUnsafePtr<FileSystemDataManager>>>; + +// This hashtable isn't protected by any mutex but it is only ever touched on +// the PBackground thread. +StaticAutoPtr<FileSystemDataManagerHashtable> gDataManagers; + +RefPtr<FileSystemDataManager> GetFileSystemDataManager(const Origin& aOrigin) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (gDataManagers) { + auto maybeDataManager = gDataManagers->MaybeGet(aOrigin); + if (maybeDataManager) { + RefPtr<FileSystemDataManager> result( + std::move(*maybeDataManager).unwrapBasePtr()); + return result; + } + } + + return nullptr; +} + +void AddFileSystemDataManager( + const Origin& aOrigin, const RefPtr<FileSystemDataManager>& aDataManager) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(!quota::QuotaManager::IsShuttingDown()); + + if (!gDataManagers) { + gDataManagers = new FileSystemDataManagerHashtable(); + } + + MOZ_ASSERT(!gDataManagers->Contains(aOrigin)); + gDataManagers->InsertOrUpdate(aOrigin, + WrapMovingNotNullUnchecked(aDataManager)); +} + +void RemoveFileSystemDataManager(const Origin& aOrigin) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + MOZ_ASSERT(gDataManagers); + const DebugOnly<bool> removed = gDataManagers->Remove(aOrigin); + MOZ_ASSERT(removed); + + if (!gDataManagers->Count()) { + gDataManagers = nullptr; + } +} + +Result<ResultConnection, QMResult> GetStorageConnection( + const quota::OriginMetadata& aOriginMetadata, + const int64_t aDirectoryLockId) { + MOZ_ASSERT(aDirectoryLockId >= 0); + + // Ensure that storage is initialized and file system folder exists! + QM_TRY_INSPECT(const auto& dbFileUrl, + GetDatabaseFileURL(aOriginMetadata, aDirectoryLockId)); + + QM_TRY_INSPECT( + const auto& storageService, + QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_GET_TYPED( + nsCOMPtr<mozIStorageService>, MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID))); + + QM_TRY_UNWRAP(auto connection, + QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, storageService, + OpenDatabaseWithFileURL, dbFileUrl, ""_ns, + mozIStorageService::CONNECTION_DEFAULT))); + + ResultConnection result(connection); + + return result; +} + +} // namespace + +Result<EntryId, QMResult> GetRootHandle(const Origin& origin) { + MOZ_ASSERT(!origin.IsEmpty()); + + return FileSystemHashSource::GenerateHash(origin, kRootString); +} + +Result<EntryId, QMResult> GetEntryHandle( + const FileSystemChildMetadata& aHandle) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + return FileSystemHashSource::GenerateHash(aHandle.parentId(), + aHandle.childName()); +} + +FileSystemDataManager::FileSystemDataManager( + const quota::OriginMetadata& aOriginMetadata, + RefPtr<quota::QuotaManager> aQuotaManager, + MovingNotNull<nsCOMPtr<nsIEventTarget>> aIOTarget, + MovingNotNull<RefPtr<TaskQueue>> aIOTaskQueue) + : mOriginMetadata(aOriginMetadata), + mQuotaManager(std::move(aQuotaManager)), + mBackgroundTarget(WrapNotNull(GetCurrentSerialEventTarget())), + mIOTarget(std::move(aIOTarget)), + mIOTaskQueue(std::move(aIOTaskQueue)), + mRegCount(0), + 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); +} + +void FileSystemDataManager::UnregisterActor( + NotNull<FileSystemManagerParent*> aActor) { + MOZ_ASSERT(mBackgroundThreadAccessible.Access()->mActors.Contains(aActor)); + + mBackgroundThreadAccessible.Access()->mActors.Remove(aActor); + + if (IsInactive()) { + BeginClose(); + } +} + +void FileSystemDataManager::RegisterAccessHandle( + NotNull<FileSystemAccessHandle*> aAccessHandle) { + MOZ_ASSERT(!mBackgroundThreadAccessible.Access()->mAccessHandles.Contains( + aAccessHandle)); + + mBackgroundThreadAccessible.Access()->mAccessHandles.Insert(aAccessHandle); +} + +void FileSystemDataManager::UnregisterAccessHandle( + NotNull<FileSystemAccessHandle*> aAccessHandle) { + MOZ_ASSERT(mBackgroundThreadAccessible.Access()->mAccessHandles.Contains( + aAccessHandle)); + + mBackgroundThreadAccessible.Access()->mAccessHandles.Remove(aAccessHandle); + + if (IsInactive()) { + BeginClose(); + } +} + +RefPtr<BoolPromise> FileSystemDataManager::OnOpen() { + MOZ_ASSERT(mState == State::Opening); + + return mOpenPromiseHolder.Ensure(__func__); +} + +RefPtr<BoolPromise> FileSystemDataManager::OnClose() { + MOZ_ASSERT(mState == State::Closing); + + return mClosePromiseHolder.Ensure(__func__); +} + +bool FileSystemDataManager::IsLocked(const EntryId& aEntryId) const { + return mExclusiveLocks.Contains(aEntryId) || mSharedLocks.Contains(aEntryId); +} + +nsresult FileSystemDataManager::LockExclusive(const EntryId& aEntryId) { + if (IsLocked(aEntryId)) { + return NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR; + } + + // 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(MOZ_TO_RESULT(mDatabaseManager->BeginUsageTracking(aEntryId))); + + LOG_VERBOSE(("ExclusiveLock")); + mExclusiveLocks.Insert(aEntryId); + + return NS_OK; +} + +void FileSystemDataManager::UnlockExclusive(const EntryId& aEntryId) { + MOZ_ASSERT(mExclusiveLocks.Contains(aEntryId)); + + LOG_VERBOSE(("ExclusiveUnlock")); + mExclusiveLocks.Remove(aEntryId); + + // On error, usage tracking remains on to prevent writes until usage is + // updated successfully. + QM_TRY(MOZ_TO_RESULT(mDatabaseManager->UpdateUsage(aEntryId)), QM_VOID); + QM_TRY(MOZ_TO_RESULT(mDatabaseManager->EndUsageTracking(aEntryId)), QM_VOID); +} + +nsresult FileSystemDataManager::LockShared(const EntryId& aEntryId) { + if (mExclusiveLocks.Contains(aEntryId)) { + return NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR; + } + + auto& count = mSharedLocks.LookupOrInsert(aEntryId); + if (!(1u + CheckedUint32(count)).isValid()) { // don't make the count invalid + return NS_ERROR_UNEXPECTED; + } + + ++count; + LOG_VERBOSE(("SharedLock %u", count)); + + return NS_OK; +} + +void FileSystemDataManager::UnlockShared(const EntryId& aEntryId) { + 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(); + } +} + +bool FileSystemDataManager::IsInactive() const { + auto data = mBackgroundThreadAccessible.Access(); + return !mRegCount && !data->mActors.Count() && !data->mAccessHandles.Count(); +} + +void FileSystemDataManager::RequestAllowToClose() { + for (const auto& actor : mBackgroundThreadAccessible.Access()->mActors) { + actor->RequestAllowToClose(); + } +} + +RefPtr<BoolPromise> FileSystemDataManager::BeginOpen() { + MOZ_ASSERT(mQuotaManager); + MOZ_ASSERT(mState == State::Initial); + + mState = State::Opening; + + RefPtr<quota::ClientDirectoryLock> directoryLock = + mQuotaManager->CreateDirectoryLock( + quota::PERSISTENCE_TYPE_DEFAULT, mOriginMetadata, + mozilla::dom::quota::Client::FILESYSTEM, + /* aExclusive */ false); + + directoryLock->Acquire() + ->Then(GetCurrentSerialEventTarget(), __func__, + [self = RefPtr<FileSystemDataManager>(this), + directoryLock = directoryLock]( + const BoolPromise::ResolveOrRejectValue& value) mutable { + if (value.IsReject()) { + return BoolPromise::CreateAndReject(value.RejectValue(), + __func__); + } + + self->mDirectoryLock = std::move(directoryLock); + + return BoolPromise::CreateAndResolve(true, __func__); + }) + ->Then(mQuotaManager->IOThread(), __func__, + [self = RefPtr<FileSystemDataManager>(this)]( + const BoolPromise::ResolveOrRejectValue& value) { + if (value.IsReject()) { + return BoolPromise::CreateAndReject(value.RejectValue(), + __func__); + } + + QM_TRY(MOZ_TO_RESULT( + EnsureFileSystemDirectory(self->mOriginMetadata)), + CreateAndRejectBoolPromise); + + return BoolPromise::CreateAndResolve(true, __func__); + }) + ->Then(MutableIOTaskQueuePtr(), __func__, + [self = RefPtr<FileSystemDataManager>(this)]( + const BoolPromise::ResolveOrRejectValue& value) { + if (value.IsReject()) { + return BoolPromise::CreateAndReject(value.RejectValue(), + __func__); + } + + QM_TRY_UNWRAP( + auto connection, + fs::data::GetStorageConnection(self->mOriginMetadata, + self->mDirectoryLock->Id()), + CreateAndRejectBoolPromiseFromQMResult); + + QM_TRY_UNWRAP(DatabaseVersion version, + SchemaVersion001::InitializeConnection( + connection, self->mOriginMetadata.mOrigin), + CreateAndRejectBoolPromiseFromQMResult); + + if (1 == version) { + QM_TRY_UNWRAP( + FileSystemFileManager fmRes, + FileSystemFileManager::CreateFileSystemFileManager( + self->mOriginMetadata), + CreateAndRejectBoolPromiseFromQMResult); + + QM_TRY_UNWRAP( + EntryId rootId, + fs::data::GetRootHandle(self->mOriginMetadata.mOrigin), + CreateAndRejectBoolPromiseFromQMResult); + + self->mDatabaseManager = + MakeUnique<FileSystemDatabaseManagerVersion001>( + self, std::move(connection), + MakeUnique<FileSystemFileManager>(std::move(fmRes)), + rootId); + } + + return BoolPromise::CreateAndResolve(true, __func__); + }) + ->Then(GetCurrentSerialEventTarget(), __func__, + [self = RefPtr<FileSystemDataManager>(this)]( + const BoolPromise::ResolveOrRejectValue& value) { + if (value.IsReject()) { + self->mState = State::Initial; + + self->mOpenPromiseHolder.RejectIfExists(value.RejectValue(), + __func__); + + } else { + self->mState = State::Open; + + self->mOpenPromiseHolder.ResolveIfExists(true, __func__); + } + }); + + return OnOpen(); +} + +RefPtr<BoolPromise> FileSystemDataManager::BeginClose() { + MOZ_ASSERT(mState != State::Closing && mState != State::Closed); + MOZ_ASSERT(IsInactive()); + + mState = State::Closing; + + InvokeAsync(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..e23e4dd95b --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDataManager.h @@ -0,0 +1,174 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATAMANAGER_H_ +#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATAMANAGER_H_ + +#include "ResultConnection.h" +#include "mozilla/NotNull.h" +#include "mozilla/TaskQueue.h" +#include "mozilla/ThreadBound.h" +#include "mozilla/dom/FileSystemHelpers.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/quota/CheckedUnsafePtr.h" +#include "mozilla/dom/quota/CommonMetadata.h" +#include "mozilla/dom/quota/ForwardDecls.h" +#include "nsCOMPtr.h" +#include "nsISupportsUtils.h" +#include "nsString.h" +#include "nsTHashSet.h" + +namespace mozilla { + +template <typename V, typename E> +class Result; + +namespace dom { + +class FileSystemAccessHandle; +class FileSystemManagerParent; + +namespace fs { +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); + +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(); + + bool IsLocked(const EntryId& aEntryId) const; + + nsresult LockExclusive(const EntryId& aEntryId); + + void UnlockExclusive(const EntryId& aEntryId); + + nsresult LockShared(const EntryId& aEntryId); + + void UnlockShared(const EntryId& aEntryId); + + protected: + virtual ~FileSystemDataManager(); + + bool IsInactive() const; + + bool IsOpening() const { return mState == State::Opening; } + + bool IsClosing() const { return mState == State::Closing; } + + void RequestAllowToClose(); + + RefPtr<BoolPromise> BeginOpen(); + + RefPtr<BoolPromise> BeginClose(); + + // Things touched on background thread only. + struct BackgroundThreadAccessible { + nsTHashSet<FileSystemManagerParent*> mActors; + 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; + 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..516158c37b --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManager.cpp @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FileSystemDatabaseManager.h" + +#include "FileSystemDatabaseManagerVersion001.h" +#include "FileSystemFileManager.h" +#include "ResultConnection.h" +#include "ResultStatement.h" +#include "SchemaVersion001.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "nsCOMPtr.h" +#include "nsIFile.h" + +namespace mozilla::dom::fs::data { + +/* static */ +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); + } + + 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))); + + DatabaseVersion version = 0; + QM_TRY(QM_TO_RESULT(aConnection->GetSchemaVersion(&version))); + + switch (version) { + case 0: { + return result; + } + + case 1: { + QM_TRY_INSPECT( + const Usage& fileUsage, + FileSystemDatabaseManagerVersion001::GetFileUsage(aConnection)); + + // XXX: DatabaseUsage is currently total usage for most forms of storage + result += quota::DatabaseUsageType(Some(fileUsage)); + + return result; + } + + default: + break; + } + + return Err(QMResult(NS_ERROR_NOT_IMPLEMENTED)); +} + +} // namespace mozilla::dom::fs::data diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManager.h b/dom/fs/parent/datamodel/FileSystemDatabaseManager.h new file mode 100644 index 0000000000..c363bcb46f --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManager.h @@ -0,0 +1,182 @@ +/* -*- 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 { + +class FileSystemChildMetadata; +class FileSystemEntryMetadata; +class FileSystemDirectoryListing; +class FileSystemEntryPair; + +namespace data { + +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 EntryId& aEntry) = 0; + + /** + * @brief Returns directory identifier, optionally creating it if it doesn't + * exist + * + * @param aHandle Current directory and filename + * @return Result<bool, QMResult> Directory identifier or error + */ + virtual Result<EntryId, QMResult> GetOrCreateDirectory( + const FileSystemChildMetadata& aHandle, bool aCreate) = 0; + + /** + * @brief Returns file identifier, optionally creating it if it doesn't exist + * + * @param aHandle Current directory and filename + * @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, const ContentType& aType, + bool aCreate) = 0; + + /** + * @brief Returns the properties of a file corresponding to a file handle + */ + virtual nsresult GetFile(const EntryId& aEntryId, 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<bool, QMResult> False if entry didn't exist, otherwise true + * or error + */ + virtual Result<bool, QMResult> RenameEntry( + const FileSystemEntryMetadata& aHandle, const Name& aNewName) = 0; + + /** + * @brief Move a file/directory + * + * @param aHandle Source directory or file + * @param aNewDesignation Destination directory and entry name + * @return Result<bool, QMResult> False if entry didn't exist, otherwise true + * or error + */ + virtual Result<bool, QMResult> MoveEntry( + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation) = 0; + + /** + * @brief Tries to connect a parent directory to a file system item with a + * path, excluding the parent directory + * + * @param aHandle Pair of parent directory and child item candidates + * @return Result<Path, QMResult> Path or error if no it didn't exists + */ + virtual Result<Path, QMResult> Resolve( + const FileSystemEntryPair& aEndpoints) const = 0; + + /** + * @brief Close database connection. + */ + virtual void Close() = 0; + + /** + * @brief Start tracking file's usage. + */ + virtual nsresult BeginUsageTracking(const EntryId& aEntryId) = 0; + + /** + * @brief Stop tracking file's usage. + */ + virtual nsresult EndUsageTracking(const EntryId& aEntryId) = 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..7c481c9791 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp @@ -0,0 +1,1436 @@ +/* -*- 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 "FileSystemContentTypeGuess.h" +#include "FileSystemDataManager.h" +#include "FileSystemFileManager.h" +#include "ResultStatement.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" + +namespace mozilla::dom { + +using FileSystemEntries = nsTArray<fs::FileSystemEntryMetadata>; + +namespace fs::data { + +namespace { + +auto toNSResult = [](const auto& aRv) { return ToNSResult(aRv); }; + +Result<bool, QMResult> ApplyEntryExistsQuery( + const FileSystemConnection& aConnection, const nsACString& aQuery, + const FileSystemChildMetadata& aHandle) { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, aQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aHandle.parentId()))); + QM_TRY(QM_TO_RESULT(stmt.BindNameByName("name"_ns, aHandle.childName()))); + + return stmt.YesOrNoQuery(); +} + +Result<bool, QMResult> ApplyEntryExistsQuery( + const FileSystemConnection& aConnection, const nsACString& aQuery, + const EntryId& aEntry) { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, aQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntry))); + + return stmt.YesOrNoQuery(); +} + +Result<bool, QMResult> IsDirectoryEmpty(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 JOIN Entries USING (handle) " + "WHERE Directories.name = :name AND Entries.parent = :parent ) " + ";"_ns; + + QM_TRY_RETURN(ApplyEntryExistsQuery(mConnection, existsQuery, aHandle)); +} + +Result<bool, QMResult> DoesDirectoryExist( + const FileSystemConnection& mConnection, const EntryId& aEntry) { + MOZ_ASSERT(!aEntry.IsEmpty()); + + const nsCString existsQuery = + "SELECT EXISTS " + "(SELECT 1 FROM Directories WHERE handle = :handle ) " + ";"_ns; + + QM_TRY_RETURN(ApplyEntryExistsQuery(mConnection, existsQuery, aEntry)); +} + +Result<Path, QMResult> ResolveReversedPath( + const FileSystemConnection& aConnection, + const FileSystemEntryPair& aEndpoints) { + const nsCString pathQuery = + "WITH RECURSIVE followPath(handle, parent) AS ( " + "SELECT handle, parent " + "FROM Entries " + "WHERE handle=:entryId " + "UNION " + "SELECT Entries.handle, Entries.parent FROM followPath, Entries " + "WHERE followPath.parent=Entries.handle ) " + "SELECT COALESCE(Directories.name, Files.name), handle " + "FROM followPath " + "LEFT JOIN Directories USING(handle) " + "LEFT JOIN Files USING(handle);"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, pathQuery)); + QM_TRY( + QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEndpoints.childId()))); + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + + Path pathResult; + while (moreResults) { + QM_TRY_UNWRAP(Name entryName, stmt.GetNameByColumn(/* Column */ 0u)); + QM_TRY_UNWRAP(EntryId entryId, stmt.GetEntryIdByColumn(/* Column */ 1u)); + + if (aEndpoints.parentId() == entryId) { + return pathResult; + } + pathResult.AppendElement(entryName); + + QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep()); + } + + // Spec wants us to return 'null' for not-an-ancestor case + pathResult.Clear(); + return pathResult; +} + +Result<bool, QMResult> IsAncestor(const FileSystemConnection& aConnection, + const FileSystemEntryPair& aEndpoints) { + const nsCString pathQuery = + "WITH RECURSIVE followPath(handle, parent) AS ( " + "SELECT handle, parent " + "FROM Entries " + "WHERE handle=:entryId " + "UNION " + "SELECT Entries.handle, Entries.parent FROM followPath, Entries " + "WHERE followPath.parent=Entries.handle ) " + "SELECT EXISTS " + "(SELECT 1 FROM followPath " + "WHERE handle=:possibleAncestor ) " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, pathQuery)); + QM_TRY( + QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEndpoints.childId()))); + QM_TRY(QM_TO_RESULT( + stmt.BindEntryIdByName("possibleAncestor"_ns, aEndpoints.parentId()))); + + return stmt.YesOrNoQuery(); +} + +Result<bool, QMResult> DoesFileExist(const FileSystemConnection& aConnection, + const FileSystemChildMetadata& aHandle) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + const nsCString existsQuery = + "SELECT EXISTS " + "(SELECT 1 FROM Files JOIN Entries USING (handle) " + "WHERE Files.name = :name AND Entries.parent = :parent ) " + ";"_ns; + + QM_TRY_RETURN(ApplyEntryExistsQuery(aConnection, existsQuery, aHandle)); +} + +Result<bool, QMResult> DoesFileExist(const FileSystemConnection& aConnection, + const EntryId& aEntry) { + MOZ_ASSERT(!aEntry.IsEmpty()); + + const nsCString existsQuery = + "SELECT EXISTS " + "(SELECT 1 FROM Files WHERE handle = :handle ) " + ";"_ns; + + QM_TRY_RETURN(ApplyEntryExistsQuery(aConnection, existsQuery, aEntry)); +} + +Result<EntryId, QMResult> FindParent(const FileSystemConnection& aConnection, + const EntryId& aEntryId) { + const nsCString aParentQuery = + "SELECT handle FROM Entries " + "WHERE handle IN ( " + "SELECT parent FROM Entries WHERE " + "handle = :entryId ) " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, aParentQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEntryId))); + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + + if (!moreResults) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + QM_TRY_UNWRAP(EntryId parentId, stmt.GetEntryIdByColumn(/* Column */ 0u)); + return parentId; +} + +nsresult GetFileAttributes(const FileSystemConnection& aConnection, + const EntryId& aEntryId, ContentType& aType) { + const nsLiteralCString getFileLocation = + "SELECT type FROM Files INNER JOIN Entries USING(handle) " + "WHERE handle = :entryId " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, getFileLocation)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEntryId))); + QM_TRY_UNWRAP(bool hasEntries, stmt.ExecuteStep()); + + // Type is an optional attribute + if (!hasEntries || stmt.IsNullByColumn(/* Column */ 0u)) { + return NS_OK; + } + + QM_TRY_UNWRAP(aType, stmt.GetContentTypeByColumn(/* Column */ 0u)); + + return NS_OK; +} + +nsresult GetEntries(const FileSystemConnection& aConnection, + const nsACString& aUnboundStmt, const EntryId& aParent, + PageNumber aPage, bool aDirectory, + FileSystemEntries& aEntries) { + // The entries inside a directory are sent to the child process in batches + // of pageSize items. Large value ensures that iteration is less often delayed + // by IPC messaging and querying the database. + // TODO: The current value 1024 is not optimized. + // TODO: Value "pageSize" is shared with the iterator implementation and + // should be defined in a common place. + const int32_t pageSize = 1024; + + QM_TRY_UNWRAP(bool exists, DoesDirectoryExist(aConnection, aParent)); + if (!exists) { + return NS_ERROR_DOM_NOT_FOUND_ERR; + } + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, aUnboundStmt)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aParent))); + QM_TRY(QM_TO_RESULT(stmt.BindPageNumberByName("pageSize"_ns, pageSize))); + QM_TRY(QM_TO_RESULT( + stmt.BindPageNumberByName("pageOffset"_ns, aPage * pageSize))); + + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + + while (moreResults) { + QM_TRY_UNWRAP(EntryId entryId, stmt.GetEntryIdByColumn(/* Column */ 0u)); + QM_TRY_UNWRAP(Name entryName, stmt.GetNameByColumn(/* Column */ 1u)); + + FileSystemEntryMetadata metadata(entryId, entryName, aDirectory); + aEntries.AppendElement(metadata); + + QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep()); + } + + return NS_OK; +} + +Result<EntryId, QMResult> GetUniqueEntryId( + const FileSystemConnection& aConnection, + const FileSystemChildMetadata& aHandle) { + const nsCString existsQuery = + "SELECT EXISTS " + "(SELECT 1 FROM Entries " + "WHERE handle = :handle )" + ";"_ns; + + FileSystemChildMetadata generatorInput = aHandle; + + const size_t maxRounds = 1024u; + + for (size_t hangGuard = 0u; hangGuard < maxRounds; ++hangGuard) { + QM_TRY_UNWRAP(EntryId entryId, fs::data::GetEntryHandle(generatorInput)); + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, existsQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId))); + + QM_TRY_UNWRAP(bool alreadyInUse, stmt.YesOrNoQuery()); + + if (!alreadyInUse) { + return entryId; + } + + generatorInput.parentId() = entryId; + } + + return Err(QMResult(NS_ERROR_UNEXPECTED)); +} + +Result<EntryId, QMResult> FindEntryId(const FileSystemConnection& aConnection, + const FileSystemChildMetadata& aHandle, + bool aIsFile) { + const nsCString aDirectoryQuery = + "SELECT Entries.handle FROM Directories JOIN Entries USING (handle) " + "WHERE Directories.name = :name AND Entries.parent = :parent " + ";"_ns; + + const nsCString aFileQuery = + "SELECT Entries.handle FROM Files JOIN Entries USING (handle) " + "WHERE Files.name = :name AND Entries.parent = :parent " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create( + aConnection, aIsFile ? aFileQuery : aDirectoryQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aHandle.parentId()))); + QM_TRY(QM_TO_RESULT(stmt.BindNameByName("name"_ns, aHandle.childName()))); + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + + if (!moreResults) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + QM_TRY_UNWRAP(EntryId entryId, stmt.GetEntryIdByColumn(/* Column */ 0u)); + + return entryId; +} + +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<bool, QMResult> IsFile(const FileSystemConnection& aConnection, + const EntryId& aEntryId) { + QM_TRY_UNWRAP(bool exists, DoesFileExist(aConnection, aEntryId)); + if (exists) { + return true; + } + + QM_TRY_UNWRAP(exists, DoesDirectoryExist(aConnection, aEntryId)); + if (exists) { + return false; + } + + // Doesn't exist + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); +} + +nsresult PerformRename(const FileSystemConnection& aConnection, + const FileSystemEntryMetadata& aHandle, + const Name& aNewName, const 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)); + QM_TRY(QM_OR_ELSE_WARN_IF( + // Expression + MOZ_TO_RESULT(stmt.BindContentTypeByName("type"_ns, aNewType)), + // Predicate + IsSpecificError<NS_ERROR_ILLEGAL_VALUE>, + // Fallback + ErrToDefaultOk<>)); + 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, /* dummy type */ ""_ns, + updateDirectoryNameQuery); +} + +nsresult PerformRenameFile(const FileSystemConnection& aConnection, + const FileSystemEntryMetadata& aHandle, + const Name& aNewName, const ContentType& aNewType) { + const nsLiteralCString updateFileNameQuery = + "UPDATE Files " + "SET type = :type, name = :name " + "WHERE handle = :handle " + ";"_ns; + + return PerformRename(aConnection, aHandle, aNewName, aNewType, + updateFileNameQuery); +} + +Result<nsTArray<EntryId>, QMResult> FindDescendants( + const FileSystemConnection& aConnection, const EntryId& aEntryId) { + const nsLiteralCString descendantsQuery = + "WITH RECURSIVE traceChildren(handle, parent) AS ( " + "SELECT handle, parent " + "FROM Entries " + "WHERE handle=:handle " + "UNION " + "SELECT Entries.handle, Entries.parent FROM traceChildren, Entries " + "WHERE traceChildren.handle=Entries.parent ) " + "SELECT handle " + "FROM traceChildren INNER JOIN Files " + "USING(handle) " + ";"_ns; + + nsTArray<EntryId> descendants; + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, descendantsQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + + while (moreResults) { + QM_TRY_UNWRAP(EntryId entryId, stmt.GetEntryIdByColumn(/* Column */ 0u)); + + descendants.AppendElement(entryId); + + QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep()); + } + } + + return descendants; +} + +nsresult SetUsageTracking(const FileSystemConnection& aConnection, + const EntryId& aEntryId, bool aTracked) { + const nsLiteralCString setTrackedQuery = + "INSERT INTO Usages " + "( handle, tracked ) " + "VALUES " + "( :handle, :tracked ) " + "ON CONFLICT(handle) DO " + "UPDATE SET tracked = excluded.tracked " + ";"_ns; + + const nsresult onMissingFile = aTracked ? NS_ERROR_DOM_NOT_FOUND_ERR : NS_OK; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, setTrackedQuery)); + QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY(MOZ_TO_RESULT(stmt.BindBooleanByName("tracked"_ns, aTracked))); + QM_TRY(MOZ_TO_RESULT(stmt.Execute()), onMissingFile, + ([&aConnection, &aEntryId](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, + DoesFileExist(aConnection, aEntryId), QM_VOID); + MOZ_ASSERT(!fileExists); + })); + + return NS_OK; +} + +Result<nsTArray<EntryId>, QMResult> GetTrackedFiles( + const FileSystemConnection& aConnection) { + static const nsLiteralCString getTrackedFilesQuery = + "SELECT handle FROM Usages WHERE tracked = TRUE;"_ns; + + nsTArray<EntryId> trackedFiles; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, getTrackedFilesQuery)); + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + + while (moreResults) { + QM_TRY_UNWRAP(EntryId entryId, stmt.GetEntryIdByColumn(/* Column */ 0u)); + + trackedFiles.AppendElement(entryId); + + 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 EntryId& aEntryId, + const nsLiteralCString& aUpdateQuery, + QuotaCacheUpdate&& aUpdateCache) { + QM_TRY_INSPECT(const auto& fileHandle, aFileManager.GetFile(aEntryId)); + + // 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.BindEntryIdByName("handle"_ns, aEntryId))); + + 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 EntryId& aEntryId) { + 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, aEntryId, + updateUsagesUnsetTrackedQuery, + std::move(noCacheUpdateNeeded)); +} + +/** + * @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> GetUsagesOfDescendants( + const FileSystemConnection& aConnection, const EntryId& aEntryId) { + 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(aConnection, 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)); +} + +/** + * @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 EntryId& aEntryId) { + 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.BindEntryIdByName("handle"_ns, aEntryId))); + + QM_TRY_UNWRAP(const bool moreResults, stmt.ExecuteStep()); + if (!moreResults) { + return 0; + } + + QM_TRY_RETURN(stmt.GetUsageByColumn(/* Column */ 0u)); +} + +/** + * @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 EntryId& aEntryId) { + 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.BindEntryIdByName("handle"_ns, aEntryId))); + + 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<EntryId>& trackedFiles, + GetTrackedFiles(aConnection).mapErr(toNSResult)); + + bool ok = true; + for (const auto& entryId : trackedFiles) { + // On success, tracked is set to false, otherwise its value is kept (= true) + QM_WARNONLY_TRY(MOZ_TO_RESULT(UpdateUsageUnsetTracked( + aConnection, aFileManager, entryId)), + [&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 EntryId& aEntryId) { + if (!LOG_ENABLED()) { + return; + } + + QM_TRY_INSPECT(const auto& localFile, aFileManager.GetFile(aEntryId), + QM_VOID); + + nsAutoString localPath; + QM_TRY(MOZ_TO_RESULT(localFile->GetPath(localPath)), QM_VOID); + LOG((aFormat, NS_ConvertUTF16toUTF8(localPath).get())); +} + +// TODO: Implement idle maintenance +void TryRemoveDuringIdleMaintenance( + const nsTArray<EntryId>& /* aItemToRemove */) { + // Not implemented +} + +} // 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(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::UpdateUsageInDatabase( + const EntryId& aEntry, 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.BindEntryIdByName("handle"_ns, aEntry))); + QM_TRY(MOZ_TO_RESULT(stmt.Execute())); + + return NS_OK; +} + +Result<EntryId, QMResult> +FileSystemDatabaseManagerVersion001::GetOrCreateDirectory( + const FileSystemChildMetadata& aHandle, bool aCreate) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + const auto& name = aHandle.childName(); + // Belt and suspenders: check here as well as in child. + if (!IsValidName(name)) { + return Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR)); + } + MOZ_ASSERT(!name.IsVoid() && !name.IsEmpty()); + + bool exists = true; + QM_TRY_UNWRAP(exists, DoesFileExist(mConnection, aHandle)); + + // By spec, we don't allow a file and a directory + // to have the same name and parent + if (exists) { + return Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR)); + } + + QM_TRY_UNWRAP(exists, DoesDirectoryExist(mConnection, aHandle)); + + // exists as directory + if (exists) { + return FindEntryId(mConnection, aHandle, false); + } + + if (!aCreate) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + const nsLiteralCString insertEntryQuery = + "INSERT OR IGNORE INTO Entries " + "( handle, parent ) " + "VALUES " + "( :handle, :parent ) " + ";"_ns; + + const nsLiteralCString insertDirectoryQuery = + "INSERT OR IGNORE INTO Directories " + "( handle, name ) " + "VALUES " + "( :handle, :name ) " + ";"_ns; + + QM_TRY_UNWRAP(EntryId entryId, GetUniqueEntryId(mConnection, aHandle)); + MOZ_ASSERT(!entryId.IsEmpty()); + + mozStorageTransaction transaction( + mConnection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, insertEntryQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId))); + QM_TRY( + QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aHandle.parentId()))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, insertDirectoryQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId))); + QM_TRY(QM_TO_RESULT(stmt.BindNameByName("name"_ns, name))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + QM_TRY_UNWRAP(DebugOnly<bool> doesItExistNow, + DoesDirectoryExist(mConnection, aHandle)); + MOZ_ASSERT(doesItExistNow); + + return entryId; +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion001::GetOrCreateFile( + const FileSystemChildMetadata& aHandle, const ContentType& aType, + bool aCreate) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + const auto& name = aHandle.childName(); + // Belt and suspenders: check here as well as in child. + if (!IsValidName(name)) { + return Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR)); + } + MOZ_ASSERT(!name.IsVoid() && !name.IsEmpty()); + + QM_TRY_UNWRAP(bool exists, DoesDirectoryExist(mConnection, aHandle)); + + // By spec, we don't allow a file and a directory + // to have the same name and parent + if (exists) { + return Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR)); + } + + QM_TRY_UNWRAP(exists, DoesFileExist(mConnection, aHandle)); + + if (exists) { + return FindEntryId(mConnection, aHandle, true); + } + + if (!aCreate) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + const nsLiteralCString insertEntryQuery = + "INSERT INTO Entries " + "( handle, parent ) " + "VALUES " + "( :handle, :parent ) " + ";"_ns; + + const nsLiteralCString insertFileQuery = + "INSERT INTO Files " + "( handle, type, name ) " + "VALUES " + "( :handle, :type, :name ) " + ";"_ns; + + QM_TRY_UNWRAP(EntryId entryId, GetUniqueEntryId(mConnection, aHandle)); + MOZ_ASSERT(!entryId.IsEmpty()); + + const ContentType& type = + aType.IsVoid() ? FileSystemContentTypeGuess::FromPath(name) : aType; + + mozStorageTransaction transaction( + mConnection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, insertEntryQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId))); + QM_TRY( + QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aHandle.parentId()))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, insertFileQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId))); + QM_TRY(QM_TO_RESULT(stmt.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; +} + +Result<FileSystemDirectoryListing, QMResult> +FileSystemDatabaseManagerVersion001::GetDirectoryEntries( + const EntryId& aParent, PageNumber aPage) const { + // TODO: Offset is reported to have bad performance - see Bug 1780386. + const nsCString directoriesQuery = + "SELECT Dirs.handle, Dirs.name " + "FROM Directories AS Dirs " + "INNER JOIN ( " + "SELECT handle " + "FROM Entries " + "WHERE parent = :parent " + "LIMIT :pageSize " + "OFFSET :pageOffset ) " + "AS Ents " + "ON Dirs.handle = Ents.handle " + ";"_ns; + const nsCString filesQuery = + "SELECT Files.handle, Files.name " + "FROM Files " + "INNER JOIN ( " + "SELECT handle " + "FROM Entries " + "WHERE parent = :parent " + "LIMIT :pageSize " + "OFFSET :pageOffset ) " + "AS Ents " + "ON Files.handle = Ents.handle " + ";"_ns; + + FileSystemDirectoryListing entries; + QM_TRY( + QM_TO_RESULT(GetEntries(mConnection, directoriesQuery, aParent, aPage, + /* aDirectory */ true, entries.directories()))); + + QM_TRY(QM_TO_RESULT(GetEntries(mConnection, filesQuery, aParent, aPage, + /* aDirectory */ false, entries.files()))); + + return entries; +} + +nsresult FileSystemDatabaseManagerVersion001::GetFile( + const EntryId& aEntryId, ContentType& aType, + TimeStamp& lastModifiedMilliSeconds, nsTArray<Name>& aPath, + nsCOMPtr<nsIFile>& aFile) const { + MOZ_ASSERT(!aEntryId.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_UNWRAP(aFile, mFileManager->GetOrCreateFile(aEntryId)); + + QM_TRY(MOZ_TO_RESULT(GetFileAttributes(mConnection, aEntryId, aType))); + + PRTime lastModTime = 0; + QM_TRY(MOZ_TO_RESULT(aFile->GetLastModifiedTime(&lastModTime))); + lastModifiedMilliSeconds = static_cast<TimeStamp>(lastModTime); + + aPath.Reverse(); + + return NS_OK; +} + +nsresult FileSystemDatabaseManagerVersion001::UpdateUsage( + const EntryId& aEntry) { + // We don't track directories or non-existent files. + QM_TRY_UNWRAP(bool fileExists, + DoesFileExist(mConnection, aEntry).mapErr(toNSResult)); + if (!fileExists) { + return NS_OK; // May be deleted before update, no assert + } + + QM_TRY_UNWRAP(bool isFolder, + DoesDirectoryExist(mConnection, aEntry).mapErr(toNSResult)); + if (isFolder) { + return NS_OK; // May be deleted and replaced by a folder, no assert + } + + nsCOMPtr<nsIFile> file; + QM_TRY_UNWRAP(file, mFileManager->GetOrCreateFile(aEntry)); + MOZ_ASSERT(file); + + Usage fileSize = 0; + QM_TRY(MOZ_TO_RESULT(file->GetFileSize(&fileSize))); + + QM_TRY(MOZ_TO_RESULT(UpdateUsageInDatabase(aEntry, fileSize))); + + return NS_OK; +} + +nsresult FileSystemDatabaseManagerVersion001::UpdateCachedQuotaUsage( + const EntryId& aEntryId, Usage aOldUsage, Usage aNewUsage) { + quota::QuotaManager* quotaManager = quota::QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> fileObj, + mFileManager->GetFile(aEntryId).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; +} + +Result<Ok, QMResult> FileSystemDatabaseManagerVersion001::EnsureUsageIsKnown( + const EntryId& aEntryId) { + if (mFilesOfUnknownUsage < 0) { // Lazy initialization + QM_TRY_UNWRAP(mFilesOfUnknownUsage, GetTrackedFilesCount(mConnection)); + } + + if (mFilesOfUnknownUsage == 0) { + return Ok{}; + } + + QM_TRY_UNWRAP(Maybe<Usage> oldUsage, + GetMaybeTrackedUsage(mConnection, aEntryId)); + if (oldUsage.isNothing()) { + return Ok{}; // Usage is 0 or it was successfully recorded at unlocking. + } + + auto quotaCacheUpdate = [this, &aEntryId, + oldSize = oldUsage.value()](Usage aNewSize) { + return UpdateCachedQuotaUsage(aEntryId, 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, aEntryId, updateUsagesKeepTrackedQuery, + std::move(quotaCacheUpdate))), + Err(QMResult(NS_ERROR_DOM_FILE_NOT_READABLE_ERR)), + ([this, &aEntryId](const auto& /*aRv*/) { + LogWithFilename(*mFileManager, "Could not read the size of file %s", + aEntryId); + })); + + // We read and updated the quota usage successfully. + --mFilesOfUnknownUsage; + MOZ_ASSERT(mFilesOfUnknownUsage >= 0); + + return Ok{}; +} + +nsresult FileSystemDatabaseManagerVersion001::BeginUsageTracking( + const EntryId& aEntryId) { + MOZ_ASSERT(!aEntryId.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(aEntryId)); + + // If file does not exist, set usage tracking to true fails with + // file not found error. + return SetUsageTracking(mConnection, aEntryId, true); +} + +nsresult FileSystemDatabaseManagerVersion001::EndUsageTracking( + const EntryId& aEntryId) { + // This is expected to fail only if database is unreachable. + return SetUsageTracking(mConnection, aEntryId, false); +} + +Result<bool, QMResult> FileSystemDatabaseManagerVersion001::RemoveDirectory( + const FileSystemChildMetadata& aHandle, bool aRecursive) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + auto isAnyDescendantLocked = [this](const nsTArray<EntryId>& aDescendants) { + return std::any_of(aDescendants.cbegin(), aDescendants.cend(), + [this](const auto& descendant) { + return mDataManager->IsLocked(descendant); + }); + }; + + 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)); + + QM_TRY_INSPECT(const nsTArray<EntryId>& descendants, + FindDescendants(mConnection, entryId)); + + QM_TRY(OkIf(!isAnyDescendantLocked(descendants)), + 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(mConnection, entryId)); + + nsTArray<EntryId> removeFails; + QM_TRY_UNWRAP(DebugOnly<Usage> removedUsage, + mFileManager->RemoveFiles(descendants, removeFails)); + + // 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(removeFails.IsEmpty() && (0 == mFilesOfUnknownUsage), + usage == removedUsage); + + TryRemoveDuringIdleMaintenance(removeFails); + + 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 + if (mDataManager->IsLocked(entryId)) { + LOG(("Trying to remove in-use file")); + return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR)); + } + + QM_TRY_UNWRAP(Usage usage, GetKnownUsage(mConnection, entryId)); + QM_WARNONLY_TRY_UNWRAP(Maybe<Usage> removedUsage, + mFileManager->RemoveFile(entryId)); + + // 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(removedUsage && (0 == mFilesOfUnknownUsage), + usage == removedUsage.value()); + if (!removedUsage) { + TryRemoveDuringIdleMaintenance({entryId}); + } + + if (usage > 0) { // Performance! + DecreaseCachedQuotaUsage(usage); + } + + QM_TRY(DeleteEntry(mConnection, entryId)); + + return true; +} + +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)); + if (aDataManager->IsLocked(destId)) { + 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::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 && aDataManager->IsLocked(entryId)) { + 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))); + + // 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; +} + +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 && mDataManager->IsLocked(entryId)) { + 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; +} + +Result<bool, 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 true; + } + + QM_TRY(QM_TO_RESULT(PrepareRenameEntry(mConnection, mDataManager, aHandle, + aNewName, isFile))); + + mozStorageTransaction transaction( + mConnection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + if (isFile) { + const ContentType type = FileSystemContentTypeGuess::FromPath(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 true; +} + +Result<bool, 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 true; + } + + QM_TRY(QM_TO_RESULT(PrepareMoveEntry(mConnection, mDataManager, aHandle, + aNewDesignation, isFile))); + + const nsLiteralCString updateEntryParentQuery = + "UPDATE Entries " + "SET parent = :parent " + "WHERE handle = :handle " + ";"_ns; + + mozStorageTransaction transaction( + mConnection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + { + // We always change the parent because it's simpler than checking if the + // parent needs to be changed + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, updateEntryParentQuery)); + QM_TRY(QM_TO_RESULT( + stmt.BindEntryIdByName("parent"_ns, aNewDesignation.parentId()))); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + const Name& newName = aNewDesignation.childName(); + + // Are we actually renaming? + if (aHandle.entryName() == newName) { + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + return true; + } + + if (isFile) { + const ContentType type = FileSystemContentTypeGuess::FromPath(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 true; +} + +Result<Path, QMResult> FileSystemDatabaseManagerVersion001::Resolve( + const FileSystemEntryPair& aEndpoints) const { + QM_TRY_UNWRAP(Path path, ResolveReversedPath(mConnection, aEndpoints)); + // Note: if not an ancestor, returns null + + path.Reverse(); + return path; +} + +void FileSystemDatabaseManagerVersion001::Close() { mConnection->Close(); } + +void FileSystemDatabaseManagerVersion001::DecreaseCachedQuotaUsage( + int64_t aDelta) { + quota::QuotaManager* quotaManager = quota::QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + quotaManager->DecreaseUsageForClient(mClientMetadata, aDelta); +} + +} // 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..d15e4f5cc9 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h @@ -0,0 +1,137 @@ +/* -*- 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 "nsString.h" + +namespace mozilla::dom::fs::data { + +class FileSystemDataManager; +class FileSystemFileManager; +using FileSystemConnection = fs::ResultConnection; + +/** + * @brief Versioned implementation of database interface enables backwards + * support after the schema has changed. Version number 0 refers to + * uninitialized database, and versions after that are sequential upgrades. + * + * To change the schema to the next version x, + * - a new implementation FileSystemDatabaseManagerVersion00x is derived from + * the previous version and the required methods are overridden + * - a new apppropriate schema initialization class SchemaVersion00x is created + * or derived + * - the factory method of FileSystemDatabaseManager is extended to try to + * migrate the data from the previous version to version x, and to return + * FileSystemDatabaseManagerVersion00x implementation if the database version + * after the migrations is x + * - note that if the migration fails at some old version, the corresponding + * old implementation should be returned: this way the users whose migrations + * fail systematically due to hardware or other issues will not get locked out + */ +class FileSystemDatabaseManagerVersion001 : public FileSystemDatabaseManager { + public: + FileSystemDatabaseManagerVersion001( + FileSystemDataManager* aDataManager, FileSystemConnection&& aConnection, + UniquePtr<FileSystemFileManager>&& aFileManager, + const EntryId& aRootEntry); + + /* 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); + + virtual nsresult UpdateUsage(const EntryId& aEntry) override; + + virtual Result<EntryId, QMResult> GetOrCreateDirectory( + const FileSystemChildMetadata& aHandle, bool aCreate) override; + + virtual Result<EntryId, QMResult> GetOrCreateFile( + const FileSystemChildMetadata& aHandle, const ContentType& aType, + bool aCreate) override; + + virtual nsresult GetFile(const EntryId& aEntryId, ContentType& aType, + TimeStamp& lastModifiedMilliSeconds, Path& aPath, + nsCOMPtr<nsIFile>& aFile) const override; + + virtual Result<FileSystemDirectoryListing, QMResult> GetDirectoryEntries( + const EntryId& aParent, PageNumber aPage) const override; + + virtual Result<bool, QMResult> RenameEntry( + const FileSystemEntryMetadata& aHandle, const Name& aNewName) override; + + virtual Result<bool, QMResult> MoveEntry( + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation) override; + + virtual Result<bool, QMResult> RemoveDirectory( + const FileSystemChildMetadata& aHandle, bool aRecursive) override; + + virtual Result<bool, QMResult> RemoveFile( + const FileSystemChildMetadata& aHandle) override; + + virtual Result<Path, QMResult> Resolve( + const FileSystemEntryPair& aEndpoints) const override; + + virtual void Close() override; + + virtual nsresult BeginUsageTracking(const EntryId& aEntryId) override; + + virtual nsresult EndUsageTracking(const EntryId& aEntryId) override; + + virtual ~FileSystemDatabaseManagerVersion001() = default; + + private: + nsresult UpdateUsageInDatabase(const EntryId& aEntry, Usage aNewDiskUsage); + + Result<Ok, QMResult> EnsureUsageIsKnown(const EntryId& aEntryId); + + void DecreaseCachedQuotaUsage(int64_t aDelta); + + nsresult UpdateCachedQuotaUsage(const EntryId& aEntryId, Usage aOldUsage, + Usage aNewUsage); + + nsresult ClearDestinationIfNotLocked( + const FileSystemConnection& aConnection, + const FileSystemDataManager* const aDataManager, + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation); + + nsresult PrepareMoveEntry(const FileSystemConnection& aConnection, + const FileSystemDataManager* const aDataManager, + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation, + bool aIsFile); + + nsresult PrepareRenameEntry(const FileSystemConnection& aConnection, + const FileSystemDataManager* const aDataManager, + const FileSystemEntryMetadata& aHandle, + const Name& aNewName, 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; +}; + +} // namespace mozilla::dom::fs::data + +#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION001_H_ diff --git a/dom/fs/parent/datamodel/FileSystemFileManager.cpp b/dom/fs/parent/datamodel/FileSystemFileManager.cpp new file mode 100644 index 0000000000..c43ee67ae5 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemFileManager.cpp @@ -0,0 +1,358 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FileSystemFileManager.h" + +#include "FileSystemDataManager.h" +#include "FileSystemHashSource.h" +#include "mozilla/Assertions.h" +#include "mozilla/NotNull.h" +#include "mozilla/Result.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "nsCOMPtr.h" +#include "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 EntryId& aEntryId) { + MOZ_ASSERT(32u == aEntryId.Length()); + + nsCOMPtr<nsIFile> destination; + + // nsIFile Clone is not a constant method + QM_TRY(QM_TO_RESULT(aTopDirectory->Clone(getter_AddRefs(destination)))); + + QM_TRY_UNWRAP(Name encoded, FileSystemHashSource::EncodeHash(aEntryId)); + + MOZ_ALWAYS_TRUE(IsAscii(encoded)); + + nsString relativePath; + relativePath.Append(Substring(encoded, 0, 2)); + + QM_TRY(QM_TO_RESULT(destination->AppendRelativePath(relativePath))); + + QM_TRY(QM_TO_RESULT(destination->AppendRelativePath(encoded))); + + return destination; +} + +Result<nsCOMPtr<nsIFile>, QMResult> GetOrCreateFileImpl( + const nsAString& aFilePath) { + MOZ_ASSERT(!aFilePath.IsEmpty()); + + nsCOMPtr<nsIFile> result; + QM_TRY(QM_TO_RESULT(NS_NewLocalFile(aFilePath, + /* aFollowLinks */ false, + getter_AddRefs(result)))); + + bool exists = true; + QM_TRY(QM_TO_RESULT(result->Exists(&exists))); + + if (!exists) { + QM_TRY(QM_TO_RESULT(result->Create(nsIFile::NORMAL_FILE_TYPE, 0644))); + + return result; + } + + bool isDirectory = true; + QM_TRY(QM_TO_RESULT(result->IsDirectory(&isDirectory))); + QM_TRY(OkIf(!isDirectory), Err(QMResult(NS_ERROR_FILE_IS_DIRECTORY))); + + return result; +} + +Result<nsCOMPtr<nsIFile>, QMResult> GetFile( + const nsCOMPtr<nsIFile>& aTopDirectory, const EntryId& aEntryId) { + MOZ_ASSERT(!aEntryId.IsEmpty()); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject, + GetFileDestination(aTopDirectory, aEntryId)); + + nsString desiredPath; + QM_TRY(QM_TO_RESULT(pathObject->GetPath(desiredPath))); + + nsCOMPtr<nsIFile> result; + QM_TRY(QM_TO_RESULT(NS_NewLocalFile(desiredPath, + /* aFollowLinks */ false, + getter_AddRefs(result)))); + + return result; +} + +Result<nsCOMPtr<nsIFile>, QMResult> GetOrCreateFile( + const nsCOMPtr<nsIFile>& aTopDirectory, const EntryId& aEntryId) { + MOZ_ASSERT(!aEntryId.IsEmpty()); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject, + GetFileDestination(aTopDirectory, aEntryId)); + + nsString desiredPath; + QM_TRY(QM_TO_RESULT(pathObject->GetPath(desiredPath))); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> result, GetOrCreateFileImpl(desiredPath)); + + return result; +} + +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 entryId + // 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->EnsureStorageIsInitialized())); + + QM_TRY(MOZ_TO_RESULT(quotaManager->EnsureTemporaryStorageIsInitialized())); + + QM_TRY_INSPECT(const auto& fileSystemDirectory, + quotaManager + ->EnsureTemporaryOriginIsInitialized( + quota::PERSISTENCE_TYPE_DEFAULT, aOriginMetadata) + .map([](const auto& aPair) { return aPair.first; })); + + QM_TRY(QM_TO_RESULT(fileSystemDirectory->AppendRelativePath( + NS_LITERAL_STRING_FROM_CSTRING(FILESYSTEM_DIRECTORY_NAME)))); + + bool exists = true; + QM_TRY(QM_TO_RESULT(fileSystemDirectory->Exists(&exists))); + + if (!exists) { + QM_TRY(QM_TO_RESULT( + fileSystemDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755))); + + return NS_OK; + } + + bool isDirectory = true; + QM_TRY(QM_TO_RESULT(fileSystemDirectory->IsDirectory(&isDirectory))); + QM_TRY(OkIf(isDirectory), NS_ERROR_FILE_NOT_DIRECTORY); + + return NS_OK; +} + +Result<nsCOMPtr<nsIFile>, QMResult> GetDatabaseFile( + const 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 >= 0); + + 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))); + + nsCString directoryLockIdClause = "&directoryLockId="_ns; + directoryLockIdClause.AppendInt(aDirectoryLockId); + + nsCOMPtr<nsIFileURL> result; + QM_TRY(QM_TO_RESULT( + NS_MutateURI(mutator).SetQuery(directoryLockIdClause).Finalize(result))); + + return result; +} + +/* static */ +Result<FileSystemFileManager, QMResult> +FileSystemFileManager::CreateFileSystemFileManager( + nsCOMPtr<nsIFile>&& topDirectory) { + return FileSystemFileManager(std::move(topDirectory)); +} + +/* static */ +Result<FileSystemFileManager, QMResult> +FileSystemFileManager::CreateFileSystemFileManager( + const quota::OriginMetadata& aOriginMetadata) { + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> topDirectory, + GetFileSystemDirectory(aOriginMetadata)); + + return FileSystemFileManager(std::move(topDirectory)); +} + +FileSystemFileManager::FileSystemFileManager(nsCOMPtr<nsIFile>&& aTopDirectory) + : mTopDirectory(std::move(aTopDirectory)) {} + +Result<nsCOMPtr<nsIFile>, QMResult> FileSystemFileManager::GetFile( + const EntryId& aEntryId) const { + return data::GetFile(mTopDirectory, aEntryId); +} + +Result<nsCOMPtr<nsIFile>, QMResult> FileSystemFileManager::GetOrCreateFile( + const EntryId& aEntryId) { + return data::GetOrCreateFile(mTopDirectory, aEntryId); +} + +Result<Usage, QMResult> FileSystemFileManager::RemoveFile( + const EntryId& aEntryId) { + MOZ_ASSERT(!aEntryId.IsEmpty()); + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject, + GetFileDestination(mTopDirectory, aEntryId)); + + bool exists = false; + QM_TRY(QM_TO_RESULT(pathObject->Exists(&exists))); + + if (!exists) { + return 0; + } + + bool isFile = false; + QM_TRY(QM_TO_RESULT(pathObject->IsFile(&isFile))); + + // 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<EntryId>& aEntryIds, nsTArray<EntryId>& aRemoveFails) { + if (aEntryIds.IsEmpty()) { + return DebugOnly<Usage>(0); + } + + CheckedInt64 totalUsage = 0; + for (const auto& entryId : aEntryIds) { + QM_WARNONLY_TRY_UNWRAP(Maybe<nsCOMPtr<nsIFile>> maybeFile, + GetFileDestination(mTopDirectory, entryId)); + if (!maybeFile) { + aRemoveFails.AppendElement(entryId); + 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) { + aRemoveFails.AppendElement(entryId); + continue; + } + totalUsage += fileSize.value(); +#endif + + QM_WARNONLY_TRY_UNWRAP(Maybe<Ok> ok, + MOZ_TO_RESULT(RemoveFileObject(fileObject))); + if (!ok) { + aRemoveFails.AppendElement(entryId); + } + } + + 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..19e7ba5b16 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemFileManager.h @@ -0,0 +1,159 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_FS_PARENT_DATAMODEL_FILESYSTEMFILEMANAGER_H_ +#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMFILEMANAGER_H_ + +#include "ErrorList.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/QMResult.h" +#include "nsIFile.h" +#include "nsString.h" + +template <class T> +class nsCOMPtr; + +class nsIFileURL; + +namespace mozilla::dom { + +namespace quota { + +struct OriginMetadata; + +} // namespace quota + +namespace fs::data { + +/** + * @brief Get the directory for file system items of specified origin. + * Use this instead of constructing the path from quota manager's storage path. + * + * @param aOrigin Specified origin + * @return Result<nsCOMPtr<nsIFile>, QMResult> Top file system directory + */ +Result<nsCOMPtr<nsIFile>, QMResult> GetFileSystemDirectory( + const 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<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 EntryId& aEntryId) const; + + /** + * @brief Get or create a disk-backed file object for a specified entry id. + * + * @param aEntryId Specified id of a file system entry + * @return Result<nsCOMPtr<nsIFile>, QMResult> File abstraction or IO error + */ + Result<nsCOMPtr<nsIFile>, QMResult> GetOrCreateFile(const EntryId& aEntryId); + + /** + * @brief Remove the disk-backed file object for a specified entry id. + * Note: The returned value is 0 in release builds. + * + * @param aEntryId Specified id of a file system entry + * @return Result<Usage, QMResult> Error or file size + */ + Result<Usage, QMResult> RemoveFile(const EntryId& aEntryId); + + /** + * @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<EntryId>& aEntryIds, nsTArray<EntryId>& aRemoveFails); + + private: + explicit FileSystemFileManager(nsCOMPtr<nsIFile>&& aTopDirectory); + + nsCOMPtr<nsIFile> mTopDirectory; +}; + +} // namespace fs::data +} // namespace mozilla::dom + +#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMFILEMANAGER_H_ diff --git a/dom/fs/parent/datamodel/SchemaVersion001.cpp b/dom/fs/parent/datamodel/SchemaVersion001.cpp new file mode 100644 index 0000000000..c3bb9d216b --- /dev/null +++ b/dom/fs/parent/datamodel/SchemaVersion001.cpp @@ -0,0 +1,185 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SchemaVersion001.h" + +#include "FileSystemHashSource.h" +#include "ResultStatement.h" +#include "fs/FileSystemConstants.h" +#include "mozStorageHelper.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" + +namespace mozilla::dom::fs { + +namespace { + +nsresult SetEncoding(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL(R"(PRAGMA encoding = "UTF-16";)"_ns); +} + +nsresult CreateEntries(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "CREATE TABLE IF NOT EXISTS Entries ( " + "handle BLOB PRIMARY KEY, " // Generated from parent + name, unique + "parent BLOB, " // Not null due to constraint + "CONSTRAINT parent_is_a_directory " + "FOREIGN KEY (parent) " + "REFERENCES Directories (handle) " + "ON DELETE CASCADE ) " + ";"_ns); +} + +nsresult CreateDirectories(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "CREATE TABLE IF NOT EXISTS Directories ( " + "handle BLOB PRIMARY KEY, " + "name BLOB NOT NULL, " + "CONSTRAINT directories_are_entries " + "FOREIGN KEY (handle) " + "REFERENCES Entries (handle) " + "ON DELETE CASCADE ) " + ";"_ns); +} + +nsresult CreateFiles(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "CREATE TABLE IF NOT EXISTS Files ( " + "handle BLOB PRIMARY KEY, " + "type TEXT, " + "name BLOB NOT NULL, " + "CONSTRAINT files_are_entries " + "FOREIGN KEY (handle) " + "REFERENCES Entries (handle) " + "ON DELETE CASCADE ) " + ";"_ns); +} + +nsresult CreateUsages(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "CREATE TABLE IF NOT EXISTS Usages ( " + "handle BLOB PRIMARY KEY, " + "usage INTEGER NOT NULL DEFAULT 0, " + "tracked BOOLEAN NOT NULL DEFAULT 0 CHECK (tracked IN (0, 1)), " + "CONSTRAINT handles_are_files " + "FOREIGN KEY (handle) " + "REFERENCES Files (handle) " + "ON DELETE CASCADE ) " + ";"_ns); +} + +class KeepForeignKeysOffUntilScopeExit final { + public: + explicit KeepForeignKeysOffUntilScopeExit(const ResultConnection& aConn) + : mConn(aConn) {} + + static Result<KeepForeignKeysOffUntilScopeExit, QMResult> Create( + const ResultConnection& aConn) { + QM_TRY( + QM_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_keys = OFF;"_ns))); + KeepForeignKeysOffUntilScopeExit result(aConn); + return result; + } + + ~KeepForeignKeysOffUntilScopeExit() { + auto maskResult = [this]() -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT( + mConn->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns))); + + return Ok{}; + }; + QM_WARNONLY_TRY(maskResult()); + } + + private: + ResultConnection mConn; +}; + +nsresult CreateRootEntry(ResultConnection& aConn, const Origin& aOrigin) { + KeepForeignKeysOffUntilScopeExit foreignKeysGuard(aConn); + + const nsLiteralCString createRootQuery = + "INSERT OR IGNORE INTO Entries " + "( handle, parent ) " + "VALUES ( :handle, NULL );"_ns; + + const nsLiteralCString flagRootAsDirectoryQuery = + "INSERT OR IGNORE INTO Directories " + "( handle, name ) " + "VALUES ( :handle, :name );"_ns; + + QM_TRY_UNWRAP(EntryId rootId, + data::FileSystemHashSource::GenerateHash(aOrigin, kRootString)); + + mozStorageTransaction transaction( + aConn.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, createRootQuery)); + QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, rootId))); + QM_TRY(MOZ_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, flagRootAsDirectoryQuery)); + QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, rootId))); + QM_TRY(MOZ_TO_RESULT(stmt.BindNameByName("name"_ns, kRootString))); + QM_TRY(MOZ_TO_RESULT(stmt.Execute())); + } + + return transaction.Commit(); +} + +Result<bool, QMResult> CheckIfEmpty(ResultConnection& aConn) { + const nsLiteralCString areThereTablesQuery = + "SELECT EXISTS (" + "SELECT 1 FROM sqlite_master " + ");"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, areThereTablesQuery)); + + return stmt.YesOrNoQuery(); +}; + +} // namespace + +Result<DatabaseVersion, QMResult> SchemaVersion001::InitializeConnection( + ResultConnection& aConn, const Origin& aOrigin) { + QM_TRY_UNWRAP(bool isEmpty, CheckIfEmpty(aConn)); + + DatabaseVersion currentVersion = 0; + + if (isEmpty) { + QM_TRY(QM_TO_RESULT(SetEncoding(aConn))); + } else { + QM_TRY(QM_TO_RESULT(aConn->GetSchemaVersion(¤tVersion))); + } + + if (currentVersion < sVersion) { + mozStorageTransaction transaction( + aConn.get(), + /* commit on complete */ false, + mozIStorageConnection::TRANSACTION_IMMEDIATE); + + QM_TRY(QM_TO_RESULT(CreateEntries(aConn))); + QM_TRY(QM_TO_RESULT(CreateDirectories(aConn))); + QM_TRY(QM_TO_RESULT(CreateFiles(aConn))); + QM_TRY(QM_TO_RESULT(CreateUsages(aConn))); + QM_TRY(QM_TO_RESULT(CreateRootEntry(aConn, aOrigin))); + QM_TRY(QM_TO_RESULT(aConn->SetSchemaVersion(sVersion))); + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + } + + QM_TRY(QM_TO_RESULT(aConn->GetSchemaVersion(¤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..606cada50d --- /dev/null +++ b/dom/fs/parent/datamodel/SchemaVersion001.h @@ -0,0 +1,25 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION001_H_ +#define DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION001_H_ + +#include "ResultConnection.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/quota/ForwardDecls.h" + +namespace mozilla::dom::fs { + +struct SchemaVersion001 { + static Result<DatabaseVersion, QMResult> InitializeConnection( + ResultConnection& aConn, const Origin& aOrigin); + + static const DatabaseVersion sVersion = 1; +}; + +} // namespace mozilla::dom::fs + +#endif // DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION001_H_ diff --git a/dom/fs/parent/datamodel/moz.build b/dom/fs/parent/datamodel/moz.build new file mode 100644 index 0000000000..111a793722 --- /dev/null +++ b/dom/fs/parent/datamodel/moz.build @@ -0,0 +1,26 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla.dom += [ + "FileSystemDataManager.h", +] + +UNIFIED_SOURCES += [ + "FileSystemDatabaseManager.cpp", + "FileSystemDatabaseManagerVersion001.cpp", + "FileSystemDataManager.cpp", + "FileSystemFileManager.cpp", + "SchemaVersion001.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/fs/include", + "/dom/fs/parent", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/fs/parent/moz.build b/dom/fs/parent/moz.build new file mode 100644 index 0000000000..563bf8e4d3 --- /dev/null +++ b/dom/fs/parent/moz.build @@ -0,0 +1,58 @@ +# -*- 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", + "FileSystemQuotaClient.h", + "FileSystemWritableFileStreamParent.h", +] + +UNIFIED_SOURCES += [ + "FileSystemAccessHandle.cpp", + "FileSystemAccessHandleControlParent.cpp", + "FileSystemAccessHandleParent.cpp", + "FileSystemContentTypeGuess.cpp", + "FileSystemHashSource.cpp", + "FileSystemManagerParent.cpp", + "FileSystemManagerParentFactory.cpp", + "FileSystemQuotaClient.cpp", + "FileSystemStreamCallbacks.cpp", + "FileSystemWritableFileStreamParent.cpp", + "ResultStatement.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..3256f95f96 --- /dev/null +++ b/dom/fs/parent/rust/mime-guess-ffi/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "mime-guess-ffi" +version = "0.1.0" +license = "MPL-2.0" +authors = ["The Mozilla Project Developers"] + +[dependencies] +mime_guess = "2.0.4" +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..8f9492e724 --- /dev/null +++ b/dom/fs/parent/rust/mime-guess-ffi/src/lib.rs @@ -0,0 +1,25 @@ +/* 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 nsstring; + +use nsstring::{nsACString, nsCString}; +use std::path::Path; +use std::str; + +#[no_mangle] +pub extern "C" fn mimeGuessFromPath(path: &nsACString, content_type: &mut nsCString) { + let path_data = str::from_utf8(path.as_ref()); + if path_data.is_err() { + return; // Not UTF8 + } + + let maybe_mime = mime_guess::from_path(Path::new(path_data.unwrap())).first_raw(); + if maybe_mime.is_none() { + return; // Not recognized + } + + content_type.assign(maybe_mime.unwrap()); +} |