diff options
Diffstat (limited to '')
-rw-r--r-- | dom/fs/parent/datamodel/FileSystemDataManager.cpp | 672 | ||||
-rw-r--r-- | dom/fs/parent/datamodel/FileSystemDataManager.h | 186 | ||||
-rw-r--r-- | dom/fs/parent/datamodel/FileSystemDatabaseManager.cpp | 101 | ||||
-rw-r--r-- | dom/fs/parent/datamodel/FileSystemDatabaseManager.h | 229 | ||||
-rw-r--r-- | dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp | 1567 | ||||
-rw-r--r-- | dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h | 208 | ||||
-rw-r--r-- | dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.cpp | 832 | ||||
-rw-r--r-- | dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.h | 75 | ||||
-rw-r--r-- | dom/fs/parent/datamodel/FileSystemFileManager.cpp | 393 | ||||
-rw-r--r-- | dom/fs/parent/datamodel/FileSystemFileManager.h | 175 | ||||
-rw-r--r-- | dom/fs/parent/datamodel/SchemaVersion001.cpp | 193 | ||||
-rw-r--r-- | dom/fs/parent/datamodel/SchemaVersion001.h | 31 | ||||
-rw-r--r-- | dom/fs/parent/datamodel/SchemaVersion002.cpp | 618 | ||||
-rw-r--r-- | dom/fs/parent/datamodel/SchemaVersion002.h | 28 | ||||
-rw-r--r-- | dom/fs/parent/datamodel/moz.build | 28 |
15 files changed, 5336 insertions, 0 deletions
diff --git a/dom/fs/parent/datamodel/FileSystemDataManager.cpp b/dom/fs/parent/datamodel/FileSystemDataManager.cpp new file mode 100644 index 0000000000..549a8d5865 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDataManager.cpp @@ -0,0 +1,672 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FileSystemDataManager.h" + +#include "ErrorList.h" +#include "FileSystemDatabaseManager.h" +#include "FileSystemDatabaseManagerVersion001.h" +#include "FileSystemDatabaseManagerVersion002.h" +#include "FileSystemFileManager.h" +#include "FileSystemHashSource.h" +#include "FileSystemParentTypes.h" +#include "ResultStatement.h" +#include "SchemaVersion001.h" +#include "SchemaVersion002.h" +#include "fs/FileSystemConstants.h" +#include "mozIStorageService.h" +#include "mozStorageCID.h" +#include "mozilla/Result.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/dom/FileSystemLog.h" +#include "mozilla/dom/FileSystemManagerParent.h" +#include "mozilla/dom/QMResult.h" +#include "mozilla/dom/quota/ClientImpl.h" +#include "mozilla/dom/quota/DirectoryLock.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/dom/quota/UsageInfo.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "nsBaseHashtable.h" +#include "nsCOMPtr.h" +#include "nsHashKeys.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "nsNetCID.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsThreadUtils.h" + +namespace mozilla::dom::fs::data { + +namespace { + +// nsCStringHashKey with disabled memmove +class nsCStringHashKeyDM : public nsCStringHashKey { + public: + explicit nsCStringHashKeyDM(const nsCStringHashKey::KeyTypePointer aKey) + : nsCStringHashKey(aKey) {} + enum { ALLOW_MEMMOVE = false }; +}; + +// When CheckedUnsafePtr's checking is enabled, it's necessary to ensure that +// the hashtable uses the copy constructor instead of memmove for moving entries +// since memmove will break CheckedUnsafePtr in a memory-corrupting way. +using FileSystemDataManagerHashKey = + std::conditional<DiagnosticAssertEnabled::value, nsCStringHashKeyDM, + nsCStringHashKey>::type; + +// Raw (but checked when the diagnostic assert is enabled) references as we +// don't want to keep FileSystemDataManager objects alive forever. When a +// FileSystemDataManager is destroyed it calls RemoveFileSystemDataManager +// to clear itself. +using FileSystemDataManagerHashtable = + nsBaseHashtable<FileSystemDataManagerHashKey, + NotNull<CheckedUnsafePtr<FileSystemDataManager>>, + MovingNotNull<CheckedUnsafePtr<FileSystemDataManager>>>; + +// This hashtable isn't protected by any mutex but it is only ever touched on +// the PBackground thread. +StaticAutoPtr<FileSystemDataManagerHashtable> gDataManagers; + +RefPtr<FileSystemDataManager> GetFileSystemDataManager(const Origin& aOrigin) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (gDataManagers) { + auto maybeDataManager = gDataManagers->MaybeGet(aOrigin); + if (maybeDataManager) { + RefPtr<FileSystemDataManager> result( + std::move(*maybeDataManager).unwrapBasePtr()); + return result; + } + } + + return nullptr; +} + +void AddFileSystemDataManager( + const Origin& aOrigin, const RefPtr<FileSystemDataManager>& aDataManager) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(!quota::QuotaManager::IsShuttingDown()); + + if (!gDataManagers) { + gDataManagers = new FileSystemDataManagerHashtable(); + } + + MOZ_ASSERT(!gDataManagers->Contains(aOrigin)); + gDataManagers->InsertOrUpdate(aOrigin, + WrapMovingNotNullUnchecked(aDataManager)); +} + +void RemoveFileSystemDataManager(const Origin& aOrigin) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + MOZ_ASSERT(gDataManagers); + const DebugOnly<bool> removed = gDataManagers->Remove(aOrigin); + MOZ_ASSERT(removed); + + if (!gDataManagers->Count()) { + gDataManagers = nullptr; + } +} + +} // namespace + +Result<ResultConnection, QMResult> GetStorageConnection( + const quota::OriginMetadata& aOriginMetadata, + const int64_t aDirectoryLockId) { + MOZ_ASSERT(aDirectoryLockId >= -1); + + // Ensure that storage is initialized and file system folder exists! + QM_TRY_INSPECT(const auto& dbFileUrl, + GetDatabaseFileURL(aOriginMetadata, aDirectoryLockId)); + + QM_TRY_INSPECT( + const auto& storageService, + QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_GET_TYPED( + nsCOMPtr<mozIStorageService>, MOZ_SELECT_OVERLOAD(do_GetService), + MOZ_STORAGE_SERVICE_CONTRACTID))); + + QM_TRY_UNWRAP(auto connection, + QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<mozIStorageConnection>, storageService, + OpenDatabaseWithFileURL, dbFileUrl, ""_ns, + mozIStorageService::CONNECTION_DEFAULT))); + + ResultConnection result(connection); + + return result; +} + +Result<EntryId, QMResult> GetRootHandle(const Origin& origin) { + MOZ_ASSERT(!origin.IsEmpty()); + + return FileSystemHashSource::GenerateHash(origin, kRootString); +} + +Result<EntryId, QMResult> GetEntryHandle( + const FileSystemChildMetadata& aHandle) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + return FileSystemHashSource::GenerateHash(aHandle.parentId(), + aHandle.childName()); +} + +FileSystemDataManager::FileSystemDataManager( + const quota::OriginMetadata& aOriginMetadata, + RefPtr<quota::QuotaManager> aQuotaManager, + MovingNotNull<nsCOMPtr<nsIEventTarget>> aIOTarget, + MovingNotNull<RefPtr<TaskQueue>> aIOTaskQueue) + : mOriginMetadata(aOriginMetadata), + mQuotaManager(std::move(aQuotaManager)), + mBackgroundTarget(WrapNotNull(GetCurrentSerialEventTarget())), + mIOTarget(std::move(aIOTarget)), + mIOTaskQueue(std::move(aIOTaskQueue)), + mRegCount(0), + mVersion(0), + mState(State::Initial) {} + +FileSystemDataManager::~FileSystemDataManager() { + NS_ASSERT_OWNINGTHREAD(FileSystemDataManager); + MOZ_ASSERT(mState == State::Closed); + MOZ_ASSERT(!mDatabaseManager); +} + +RefPtr<FileSystemDataManager::CreatePromise> +FileSystemDataManager::GetOrCreateFileSystemDataManager( + const quota::OriginMetadata& aOriginMetadata) { + if (quota::QuotaManager::IsShuttingDown()) { + return CreatePromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + if (RefPtr<FileSystemDataManager> dataManager = + GetFileSystemDataManager(aOriginMetadata.mOrigin)) { + if (dataManager->IsOpening()) { + // We have to wait for the open to be finished before resolving the + // promise. The manager can't close itself in the meantime because we + // add a new registration in the lambda capture list. + return dataManager->OnOpen()->Then( + GetCurrentSerialEventTarget(), __func__, + [dataManager = Registered<FileSystemDataManager>(dataManager)]( + const BoolPromise::ResolveOrRejectValue& aValue) { + if (aValue.IsReject()) { + return CreatePromise::CreateAndReject(aValue.RejectValue(), + __func__); + } + return CreatePromise::CreateAndResolve(dataManager, __func__); + }); + } + + if (dataManager->IsClosing()) { + // First, we need to wait for the close to be finished. After that the + // manager is closed and it can't be opened again. The only option is + // to create a new manager and open it. However, all this stuff is + // asynchronous, so it can happen that something else called + // `GetOrCreateFileSystemManager` in the meantime. For that reason, we + // shouldn't try to create a new manager and open it here, a "recursive" + // call to `GetOrCreateFileSystemManager` will handle any new situation. + return dataManager->OnClose()->Then( + GetCurrentSerialEventTarget(), __func__, + [aOriginMetadata](const BoolPromise::ResolveOrRejectValue&) { + return GetOrCreateFileSystemDataManager(aOriginMetadata); + }); + } + + return CreatePromise::CreateAndResolve( + Registered<FileSystemDataManager>(std::move(dataManager)), __func__); + } + + RefPtr<quota::QuotaManager> quotaManager = quota::QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY_UNWRAP(auto streamTransportService, + MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIEventTarget>, + MOZ_SELECT_OVERLOAD(do_GetService), + NS_STREAMTRANSPORTSERVICE_CONTRACTID), + CreatePromise::CreateAndReject(NS_ERROR_FAILURE, __func__)); + + nsCString taskQueueName("OPFS "_ns + aOriginMetadata.mOrigin); + + RefPtr<TaskQueue> ioTaskQueue = + TaskQueue::Create(do_AddRef(streamTransportService), taskQueueName.get()); + + auto dataManager = MakeRefPtr<FileSystemDataManager>( + aOriginMetadata, std::move(quotaManager), + WrapMovingNotNull(streamTransportService), + WrapMovingNotNull(ioTaskQueue)); + + AddFileSystemDataManager(aOriginMetadata.mOrigin, dataManager); + + return dataManager->BeginOpen()->Then( + GetCurrentSerialEventTarget(), __func__, + [dataManager = Registered<FileSystemDataManager>(dataManager)]( + const BoolPromise::ResolveOrRejectValue& aValue) { + if (aValue.IsReject()) { + return CreatePromise::CreateAndReject(aValue.RejectValue(), __func__); + } + + return CreatePromise::CreateAndResolve(dataManager, __func__); + }); +} + +// static +void FileSystemDataManager::AbortOperationsForLocks( + const quota::Client::DirectoryLockIdTable& aDirectoryLockIds) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + // XXX Share the iteration code with `InitiateShutdown`, for example by + // creating a helper function which would take a predicate function. + + if (!gDataManagers) { + return; + } + + for (const auto& dataManager : gDataManagers->Values()) { + // Check if the Manager holds an acquired DirectoryLock. Origin clearing + // can't be blocked by this Manager if there is no acquired DirectoryLock. + // If there is an acquired DirectoryLock, check if the table contains the + // lock for the Manager. + if (quota::Client::IsLockForObjectAcquiredAndContainedInLockTable( + *dataManager, aDirectoryLockIds)) { + dataManager->RequestAllowToClose(); + } + } +} + +// static +void FileSystemDataManager::InitiateShutdown() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + if (!gDataManagers) { + return; + } + + for (const auto& dataManager : gDataManagers->Values()) { + dataManager->RequestAllowToClose(); + } +} + +// static +bool FileSystemDataManager::IsShutdownCompleted() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + + return !gDataManagers; +} + +void FileSystemDataManager::AssertIsOnIOTarget() const { + DebugOnly<bool> current = false; + MOZ_ASSERT(NS_SUCCEEDED(mIOTarget->IsOnCurrentThread(¤t))); + MOZ_ASSERT(current); +} + +void FileSystemDataManager::Register() { mRegCount++; } + +void FileSystemDataManager::Unregister() { + MOZ_ASSERT(mRegCount > 0); + + mRegCount--; + + if (IsInactive()) { + BeginClose(); + } +} + +void FileSystemDataManager::RegisterActor( + NotNull<FileSystemManagerParent*> aActor) { + MOZ_ASSERT(!mBackgroundThreadAccessible.Access()->mActors.Contains(aActor)); + + mBackgroundThreadAccessible.Access()->mActors.Insert(aActor); + +#ifdef DEBUG + aActor->SetRegistered(true); +#endif +} + +void FileSystemDataManager::UnregisterActor( + NotNull<FileSystemManagerParent*> aActor) { + MOZ_ASSERT(mBackgroundThreadAccessible.Access()->mActors.Contains(aActor)); + + mBackgroundThreadAccessible.Access()->mActors.Remove(aActor); + +#ifdef DEBUG + aActor->SetRegistered(false); +#endif + + if (IsInactive()) { + BeginClose(); + } +} + +void FileSystemDataManager::RegisterAccessHandle( + NotNull<FileSystemAccessHandle*> aAccessHandle) { + MOZ_ASSERT(!mBackgroundThreadAccessible.Access()->mAccessHandles.Contains( + aAccessHandle)); + + mBackgroundThreadAccessible.Access()->mAccessHandles.Insert(aAccessHandle); +} + +void FileSystemDataManager::UnregisterAccessHandle( + NotNull<FileSystemAccessHandle*> aAccessHandle) { + MOZ_ASSERT(mBackgroundThreadAccessible.Access()->mAccessHandles.Contains( + aAccessHandle)); + + mBackgroundThreadAccessible.Access()->mAccessHandles.Remove(aAccessHandle); + + if (IsInactive()) { + BeginClose(); + } +} + +RefPtr<BoolPromise> FileSystemDataManager::OnOpen() { + MOZ_ASSERT(mState == State::Opening); + + return mOpenPromiseHolder.Ensure(__func__); +} + +RefPtr<BoolPromise> FileSystemDataManager::OnClose() { + MOZ_ASSERT(mState == State::Closing); + + return mClosePromiseHolder.Ensure(__func__); +} + +// Note: Input can be temporary or main file id +Result<bool, QMResult> FileSystemDataManager::IsLocked( + const FileId& aFileId) const { + auto checkIfEntryIdIsLocked = [this, &aFileId]() -> Result<bool, QMResult> { + QM_TRY_INSPECT(const EntryId& entryId, + mDatabaseManager->GetEntryId(aFileId)); + + return IsLocked(entryId); + }; + + auto valueToSome = [](auto aValue) { return Some(std::move(aValue)); }; + + QM_TRY_UNWRAP(Maybe<bool> maybeLocked, + QM_OR_ELSE_LOG_VERBOSE_IF( + // Expression. + (checkIfEntryIdIsLocked().map(valueToSome)), + // Predicate. + IsSpecificError<NS_ERROR_DOM_NOT_FOUND_ERR>, + // Fallback. + ([](const auto&) -> Result<Maybe<bool>, QMResult> { + return Some(false); // Non-existent files are not locked. + }))); + + if (!maybeLocked) { + // If the metadata is inaccessible, we block modifications. + return true; + } + + return *maybeLocked; +} + +Result<bool, QMResult> FileSystemDataManager::IsLocked( + const EntryId& aEntryId) const { + return mExclusiveLocks.Contains(aEntryId) || mSharedLocks.Contains(aEntryId); +} + +Result<FileId, QMResult> FileSystemDataManager::LockExclusive( + const EntryId& aEntryId) { + QM_TRY_UNWRAP(const bool isLocked, IsLocked(aEntryId)); + if (isLocked) { + return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR)); + } + + QM_TRY_INSPECT(const FileId& fileId, + mDatabaseManager->EnsureFileId(aEntryId)); + + // If the file has been removed, we should get a file not found error. + // Otherwise, if usage tracking cannot be started because file size is not + // known and attempts to read it are failing, lock is denied to freeze the + // quota usage until the (external) blocker is gone or the file is removed. + QM_TRY(QM_TO_RESULT(mDatabaseManager->BeginUsageTracking(fileId))); + + LOG_VERBOSE(("ExclusiveLock")); + mExclusiveLocks.Insert(aEntryId); + + return fileId; +} + +// TODO: Improve reporting of failures, see bug 1840811. +void FileSystemDataManager::UnlockExclusive(const EntryId& aEntryId) { + MOZ_ASSERT(mExclusiveLocks.Contains(aEntryId)); + + LOG_VERBOSE(("ExclusiveUnlock")); + mExclusiveLocks.Remove(aEntryId); + + QM_TRY_INSPECT(const FileId& fileId, mDatabaseManager->GetFileId(aEntryId), + QM_VOID); + + // On error, usage tracking remains on to prevent writes until usage is + // updated successfully. + QM_TRY(MOZ_TO_RESULT(mDatabaseManager->UpdateUsage(fileId)), QM_VOID); + QM_TRY(MOZ_TO_RESULT(mDatabaseManager->EndUsageTracking(fileId)), QM_VOID); +} + +Result<FileId, QMResult> FileSystemDataManager::LockShared( + const EntryId& aEntryId) { + if (mExclusiveLocks.Contains(aEntryId)) { + return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR)); + } + + auto& count = mSharedLocks.LookupOrInsert(aEntryId); + if (!(1u + CheckedUint32(count)).isValid()) { // don't make the count invalid + return Err(QMResult(NS_ERROR_UNEXPECTED)); + } + + QM_TRY_INSPECT(const FileId& fileId, + mDatabaseManager->EnsureTemporaryFileId(aEntryId)); + + // If the file has been removed, we should get a file not found error. + // Otherwise, if usage tracking cannot be started because file size is not + // known and attempts to read it are failing, lock is denied to freeze the + // quota usage until the (external) blocker is gone or the file is removed. + QM_TRY(QM_TO_RESULT(mDatabaseManager->BeginUsageTracking(fileId))); + + ++count; + LOG_VERBOSE(("SharedLock %u", count)); + + return fileId; +} + +// TODO: Improve reporting of failures, see bug 1840811. +void FileSystemDataManager::UnlockShared(const EntryId& aEntryId, + const FileId& aFileId, bool aAbort) { + MOZ_ASSERT(!mExclusiveLocks.Contains(aEntryId)); + MOZ_ASSERT(mSharedLocks.Contains(aEntryId)); + + auto entry = mSharedLocks.Lookup(aEntryId); + MOZ_ASSERT(entry); + + MOZ_ASSERT(entry.Data() > 0); + --entry.Data(); + + LOG_VERBOSE(("SharedUnlock %u", *entry)); + + if (0u == entry.Data()) { + entry.Remove(); + } + + // On error, usage tracking remains on to prevent writes until usage is + // updated successfully. + QM_TRY(MOZ_TO_RESULT(mDatabaseManager->UpdateUsage(aFileId)), QM_VOID); + QM_TRY(MOZ_TO_RESULT(mDatabaseManager->EndUsageTracking(aFileId)), QM_VOID); + QM_TRY( + MOZ_TO_RESULT(mDatabaseManager->MergeFileId(aEntryId, aFileId, aAbort)), + QM_VOID); +} + +FileMode FileSystemDataManager::GetMode(bool aKeepData) const { + if (1 == mVersion) { + return FileMode::EXCLUSIVE; + } + + return aKeepData ? FileMode::SHARED_FROM_COPY : FileMode::SHARED_FROM_EMPTY; +} + +bool FileSystemDataManager::IsInactive() const { + auto data = mBackgroundThreadAccessible.Access(); + return !mRegCount && !data->mActors.Count() && !data->mAccessHandles.Count(); +} + +void FileSystemDataManager::RequestAllowToClose() { + for (const auto& actor : mBackgroundThreadAccessible.Access()->mActors) { + actor->RequestAllowToClose(); + } +} + +RefPtr<BoolPromise> FileSystemDataManager::BeginOpen() { + MOZ_ASSERT(mQuotaManager); + MOZ_ASSERT(mState == State::Initial); + + mState = State::Opening; + + mQuotaManager + ->OpenClientDirectory( + {mOriginMetadata, mozilla::dom::quota::Client::FILESYSTEM}) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self = RefPtr<FileSystemDataManager>(this)]( + quota::ClientDirectoryLockPromise::ResolveOrRejectValue&& value) { + if (value.IsReject()) { + return BoolPromise::CreateAndReject(value.RejectValue(), + __func__); + } + + self->mDirectoryLock = std::move(value.ResolveValue()); + + return BoolPromise::CreateAndResolve(true, __func__); + }) + ->Then(mQuotaManager->IOThread(), __func__, + [self = RefPtr<FileSystemDataManager>(this)]( + const BoolPromise::ResolveOrRejectValue& value) { + if (value.IsReject()) { + return BoolPromise::CreateAndReject(value.RejectValue(), + __func__); + } + + QM_TRY(MOZ_TO_RESULT( + EnsureFileSystemDirectory(self->mOriginMetadata)), + CreateAndRejectBoolPromise); + + return BoolPromise::CreateAndResolve(true, __func__); + }) + ->Then( + MutableIOTaskQueuePtr(), __func__, + [self = RefPtr<FileSystemDataManager>(this)]( + const BoolPromise::ResolveOrRejectValue& value) { + if (value.IsReject()) { + return BoolPromise::CreateAndReject(value.RejectValue(), + __func__); + } + + QM_TRY_UNWRAP(auto connection, + GetStorageConnection(self->mOriginMetadata, + self->mDirectoryLock->Id()), + CreateAndRejectBoolPromiseFromQMResult); + + QM_TRY_UNWRAP(UniquePtr<FileSystemFileManager> fmPtr, + FileSystemFileManager::CreateFileSystemFileManager( + self->mOriginMetadata), + CreateAndRejectBoolPromiseFromQMResult); + + QM_TRY_UNWRAP( + self->mVersion, + QM_OR_ELSE_WARN_IF( + // Expression. + SchemaVersion002::InitializeConnection( + connection, *fmPtr, self->mOriginMetadata.mOrigin), + // Predicate. + ([](const auto&) { return true; }), + // Fallback. + ([&self, &connection](const auto&) { + QM_TRY_RETURN(SchemaVersion001::InitializeConnection( + connection, self->mOriginMetadata.mOrigin)); + })), + CreateAndRejectBoolPromiseFromQMResult); + + QM_TRY_UNWRAP( + EntryId rootId, + fs::data::GetRootHandle(self->mOriginMetadata.mOrigin), + CreateAndRejectBoolPromiseFromQMResult); + + switch (self->mVersion) { + case 1: { + self->mDatabaseManager = + MakeUnique<FileSystemDatabaseManagerVersion001>( + self, std::move(connection), std::move(fmPtr), rootId); + break; + } + + case 2: { + self->mDatabaseManager = + MakeUnique<FileSystemDatabaseManagerVersion002>( + self, std::move(connection), std::move(fmPtr), rootId); + break; + } + + default: + break; + } + + return BoolPromise::CreateAndResolve(true, __func__); + }) + ->Then(GetCurrentSerialEventTarget(), __func__, + [self = RefPtr<FileSystemDataManager>(this)]( + const BoolPromise::ResolveOrRejectValue& value) { + if (value.IsReject()) { + self->mState = State::Initial; + + self->mOpenPromiseHolder.RejectIfExists(value.RejectValue(), + __func__); + + } else { + self->mState = State::Open; + + self->mOpenPromiseHolder.ResolveIfExists(true, __func__); + } + }); + + return OnOpen(); +} + +RefPtr<BoolPromise> FileSystemDataManager::BeginClose() { + MOZ_ASSERT(mState != State::Closing && mState != State::Closed); + MOZ_ASSERT(IsInactive()); + + mState = State::Closing; + + InvokeAsync(MutableIOTaskQueuePtr(), __func__, + [self = RefPtr<FileSystemDataManager>(this)]() { + if (self->mDatabaseManager) { + self->mDatabaseManager->Close(); + self->mDatabaseManager = nullptr; + } + + return BoolPromise::CreateAndResolve(true, __func__); + }) + ->Then(MutableBackgroundTargetPtr(), __func__, + [self = RefPtr<FileSystemDataManager>(this)]( + const BoolPromise::ResolveOrRejectValue&) { + return self->mIOTaskQueue->BeginShutdown(); + }) + ->Then(MutableBackgroundTargetPtr(), __func__, + [self = RefPtr<FileSystemDataManager>(this)]( + const ShutdownPromise::ResolveOrRejectValue&) { + self->mDirectoryLock = nullptr; + + RemoveFileSystemDataManager(self->mOriginMetadata.mOrigin); + + self->mState = State::Closed; + + self->mClosePromiseHolder.ResolveIfExists(true, __func__); + }); + + return OnClose(); +} + +} // namespace mozilla::dom::fs::data diff --git a/dom/fs/parent/datamodel/FileSystemDataManager.h b/dom/fs/parent/datamodel/FileSystemDataManager.h new file mode 100644 index 0000000000..ab0c603700 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDataManager.h @@ -0,0 +1,186 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATAMANAGER_H_ +#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATAMANAGER_H_ + +#include "FileSystemParentTypes.h" +#include "ResultConnection.h" +#include "mozilla/NotNull.h" +#include "mozilla/TaskQueue.h" +#include "mozilla/ThreadBound.h" +#include "mozilla/dom/FileSystemHelpers.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/quota/CheckedUnsafePtr.h" +#include "mozilla/dom/quota/CommonMetadata.h" +#include "mozilla/dom/quota/ForwardDecls.h" +#include "nsCOMPtr.h" +#include "nsISupportsUtils.h" +#include "nsString.h" +#include "nsTHashSet.h" + +namespace mozilla { + +template <typename V, typename E> +class Result; + +namespace dom { + +class FileSystemAccessHandle; +class FileSystemManagerParent; + +namespace fs { +struct FileId; +class FileSystemChildMetadata; +} // namespace fs + +namespace quota { +class DirectoryLock; +class QuotaManager; +} // namespace quota + +namespace fs::data { + +class FileSystemDatabaseManager; + +Result<EntryId, QMResult> GetRootHandle(const Origin& origin); + +Result<EntryId, QMResult> GetEntryHandle( + const FileSystemChildMetadata& aHandle); + +Result<ResultConnection, QMResult> GetStorageConnection( + const quota::OriginMetadata& aOriginMetadata, + const int64_t aDirectoryLockId); + +class FileSystemDataManager + : public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> { + public: + enum struct State : uint8_t { Initial = 0, Opening, Open, Closing, Closed }; + + FileSystemDataManager(const quota::OriginMetadata& aOriginMetadata, + RefPtr<quota::QuotaManager> aQuotaManager, + MovingNotNull<nsCOMPtr<nsIEventTarget>> aIOTarget, + MovingNotNull<RefPtr<TaskQueue>> aIOTaskQueue); + + // IsExclusive is true because we want to allow the move operations. There's + // always just one consumer anyway. + using CreatePromise = MozPromise<Registered<FileSystemDataManager>, nsresult, + /* IsExclusive */ true>; + static RefPtr<CreatePromise> GetOrCreateFileSystemDataManager( + const quota::OriginMetadata& aOriginMetadata); + + static void AbortOperationsForLocks( + const quota::Client::DirectoryLockIdTable& aDirectoryLockIds); + + static void InitiateShutdown(); + + static bool IsShutdownCompleted(); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileSystemDataManager) + + void AssertIsOnIOTarget() const; + + const quota::OriginMetadata& OriginMetadataRef() const { + return mOriginMetadata; + } + + nsISerialEventTarget* MutableBackgroundTargetPtr() const { + return mBackgroundTarget.get(); + } + + nsIEventTarget* MutableIOTargetPtr() const { return mIOTarget.get(); } + + nsISerialEventTarget* MutableIOTaskQueuePtr() const { + return mIOTaskQueue.get(); + } + + Maybe<quota::DirectoryLock&> MaybeDirectoryLockRef() const { + return ToMaybeRef(mDirectoryLock.get()); + } + + FileSystemDatabaseManager* MutableDatabaseManagerPtr() const { + MOZ_ASSERT(mDatabaseManager); + + return mDatabaseManager.get(); + } + + void Register(); + + void Unregister(); + + void RegisterActor(NotNull<FileSystemManagerParent*> aActor); + + void UnregisterActor(NotNull<FileSystemManagerParent*> aActor); + + void RegisterAccessHandle(NotNull<FileSystemAccessHandle*> aAccessHandle); + + void UnregisterAccessHandle(NotNull<FileSystemAccessHandle*> aAccessHandle); + + bool IsOpen() const { return mState == State::Open; } + + RefPtr<BoolPromise> OnOpen(); + + RefPtr<BoolPromise> OnClose(); + + Result<bool, QMResult> IsLocked(const FileId& aFileId) const; + + Result<bool, QMResult> IsLocked(const EntryId& aEntryId) const; + + Result<FileId, QMResult> LockExclusive(const EntryId& aEntryId); + + void UnlockExclusive(const EntryId& aEntryId); + + Result<FileId, QMResult> LockShared(const EntryId& aEntryId); + + void UnlockShared(const EntryId& aEntryId, const FileId& aFileId, + bool aAbort); + + FileMode GetMode(bool aKeepData) const; + + protected: + virtual ~FileSystemDataManager(); + + bool IsInactive() const; + + bool IsOpening() const { return mState == State::Opening; } + + bool IsClosing() const { return mState == State::Closing; } + + void RequestAllowToClose(); + + RefPtr<BoolPromise> BeginOpen(); + + RefPtr<BoolPromise> BeginClose(); + + // Things touched on background thread only. + struct BackgroundThreadAccessible { + nsTHashSet<FileSystemManagerParent*> mActors; + nsTHashSet<FileSystemAccessHandle*> mAccessHandles; + }; + ThreadBound<BackgroundThreadAccessible> mBackgroundThreadAccessible; + + const quota::OriginMetadata mOriginMetadata; + nsTHashSet<EntryId> mExclusiveLocks; + nsTHashMap<EntryId, uint32_t> mSharedLocks; + NS_DECL_OWNINGEVENTTARGET + const RefPtr<quota::QuotaManager> mQuotaManager; + const NotNull<nsCOMPtr<nsISerialEventTarget>> mBackgroundTarget; + const NotNull<nsCOMPtr<nsIEventTarget>> mIOTarget; + const NotNull<RefPtr<TaskQueue>> mIOTaskQueue; + RefPtr<quota::DirectoryLock> mDirectoryLock; + UniquePtr<FileSystemDatabaseManager> mDatabaseManager; + MozPromiseHolder<BoolPromise> mOpenPromiseHolder; + MozPromiseHolder<BoolPromise> mClosePromiseHolder; + uint32_t mRegCount; + DatabaseVersion mVersion; + State mState; +}; + +} // namespace fs::data +} // namespace dom +} // namespace mozilla + +#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATAMANAGER_H_ diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManager.cpp b/dom/fs/parent/datamodel/FileSystemDatabaseManager.cpp new file mode 100644 index 0000000000..2b76a3b09d --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManager.cpp @@ -0,0 +1,101 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FileSystemDatabaseManager.h" + +#include "FileSystemDatabaseManagerVersion001.h" +#include "FileSystemDatabaseManagerVersion002.h" +#include "FileSystemFileManager.h" +#include "ResultConnection.h" +#include "ResultStatement.h" +#include "SchemaVersion001.h" +#include "SchemaVersion002.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "nsCOMPtr.h" +#include "nsIFile.h" + +namespace mozilla::dom::fs::data { + +namespace { + +Result<Usage, QMResult> GetFileUsage(const ResultConnection& aConnection) { + DatabaseVersion version = 0; + QM_TRY(QM_TO_RESULT(aConnection->GetSchemaVersion(&version))); + + switch (version) { + case 0: { + return 0; + } + + case 1: { + QM_TRY_RETURN( + FileSystemDatabaseManagerVersion001::GetFileUsage(aConnection)); + } + + case 2: { + QM_TRY_RETURN( + FileSystemDatabaseManagerVersion002::GetFileUsage(aConnection)); + } + + default: + break; + } + + return Err(QMResult(NS_ERROR_NOT_IMPLEMENTED)); +} + +} // namespace + +/* static */ +nsresult FileSystemDatabaseManager::RescanUsages( + const ResultConnection& aConnection, + const quota::OriginMetadata& aOriginMetadata) { + DatabaseVersion version = 0; + QM_TRY(MOZ_TO_RESULT(aConnection->GetSchemaVersion(&version))); + + switch (version) { + case 0: { + return NS_OK; + } + + case 1: + return FileSystemDatabaseManagerVersion001::RescanTrackedUsages( + aConnection, aOriginMetadata); + + case 2: { + return FileSystemDatabaseManagerVersion002::RescanTrackedUsages( + aConnection, aOriginMetadata); + } + + default: + break; + } + + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* static */ +Result<quota::UsageInfo, QMResult> FileSystemDatabaseManager::GetUsage( + const ResultConnection& aConnection, + const quota::OriginMetadata& aOriginMetadata) { + QM_TRY_INSPECT(const auto& databaseFile, GetDatabaseFile(aOriginMetadata)); + + // If database is deleted between connection creation and now, error + int64_t dbSize = 0; + QM_TRY(QM_TO_RESULT(databaseFile->GetFileSize(&dbSize))); + + quota::UsageInfo result(quota::DatabaseUsageType(Some(dbSize))); + + QM_TRY_INSPECT(const Usage& fileUsage, GetFileUsage(aConnection)); + + // XXX: DatabaseUsage is currently total usage for most forms of storage + result += quota::DatabaseUsageType(Some(fileUsage)); + + return result; +} + +} // namespace mozilla::dom::fs::data diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManager.h b/dom/fs/parent/datamodel/FileSystemDatabaseManager.h new file mode 100644 index 0000000000..b7a4e352fe --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManager.h @@ -0,0 +1,229 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGER_H_ +#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGER_H_ + +#include "ResultConnection.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/quota/ForwardDecls.h" +#include "mozilla/dom/quota/UsageInfo.h" +#include "nsStringFwd.h" + +template <class T> +class nsCOMPtr; + +class nsIFile; + +namespace mozilla { + +template <typename V, typename E> +class Result; + +namespace dom { + +namespace quota { + +struct OriginMetadata; + +} // namespace quota + +namespace fs { + +struct FileId; +enum class FileMode; +class FileSystemChildMetadata; +class FileSystemEntryMetadata; +class FileSystemDirectoryListing; +class FileSystemEntryPair; + +namespace data { + +using FileSystemConnection = fs::ResultConnection; + +class FileSystemDatabaseManager { + public: + /** + * @brief Updates stored usage data for all tracked files. + * + * @return nsresult error code + */ + static nsresult RescanUsages(const ResultConnection& aConnection, + const quota::OriginMetadata& aOriginMetadata); + + /** + * @brief Obtains the current total usage for origin and connection. + * + * @return Result<quota::UsageInfo, QMResult> On success, + * - field UsageInfo::DatabaseUsage contains the sum of current + * total database and file usage, + * - field UsageInfo::FileUsage is not used and should be equal to Nothing. + * + * If the disk is inaccessible, various IO related errors may be returned. + */ + static Result<quota::UsageInfo, QMResult> GetUsage( + const ResultConnection& aConnection, + const quota::OriginMetadata& aOriginMetadata); + + /** + * @brief Refreshes the stored file size. + * + * @param aEntry EntryId of the file whose size is refreshed. + */ + virtual nsresult UpdateUsage(const FileId& aFileId) = 0; + + /** + * @brief Returns directory identifier, optionally creating it if it doesn't + * exist + * + * @param aHandle Current directory and filename + * @return Result<bool, QMResult> Directory identifier or error + */ + virtual Result<EntryId, QMResult> GetOrCreateDirectory( + const FileSystemChildMetadata& aHandle, bool aCreate) = 0; + + /** + * @brief Returns file identifier, optionally creating it if it doesn't exist + * + * @param aHandle Current directory and filename + * @param aType Content type which is ignored if the file already exists + * @param aCreate true if file is to be created when it does not already exist + * @return Result<bool, QMResult> File identifier or error + */ + virtual Result<EntryId, QMResult> GetOrCreateFile( + const FileSystemChildMetadata& aHandle, bool aCreate) = 0; + + /** + * @brief Returns the properties of a file corresponding to a file handle + */ + virtual nsresult GetFile(const EntryId& aEntryId, const FileId& aFileId, + const FileMode& aMode, ContentType& aType, + TimeStamp& lastModifiedMilliSeconds, Path& aPath, + nsCOMPtr<nsIFile>& aFile) const = 0; + + virtual Result<FileSystemDirectoryListing, QMResult> GetDirectoryEntries( + const EntryId& aParent, PageNumber aPage) const = 0; + + /** + * @brief Removes a directory + * + * @param aHandle Current directory and filename + * @return Result<bool, QMResult> False if file did not exist, otherwise true + * or error + */ + virtual Result<bool, QMResult> RemoveDirectory( + const FileSystemChildMetadata& aHandle, bool aRecursive) = 0; + + /** + * @brief Removes a file + * + * @param aHandle Current directory and filename + * @return Result<bool, QMResult> False if file did not exist, otherwise true + * or error + */ + virtual Result<bool, QMResult> RemoveFile( + const FileSystemChildMetadata& aHandle) = 0; + + /** + * @brief Rename a file/directory + * + * @param aHandle Source directory or file + * @param aNewName New entry name + * @return Result<EntryId, QMResult> The relevant entry id or error + */ + virtual Result<EntryId, QMResult> RenameEntry( + const FileSystemEntryMetadata& aHandle, const Name& aNewName) = 0; + + /** + * @brief Move a file/directory + * + * @param aHandle Source directory or file + * @param aNewDesignation Destination directory and entry name + * @return Result<EntryId, QMResult> The relevant entry id or error + */ + virtual Result<EntryId, QMResult> MoveEntry( + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation) = 0; + + /** + * @brief Tries to connect a parent directory to a file system item with a + * path, excluding the parent directory + * + * @param aHandle Pair of parent directory and child item candidates + * @return Result<Path, QMResult> Path or error if no it didn't exists + */ + virtual Result<Path, QMResult> Resolve( + const FileSystemEntryPair& aEndpoints) const = 0; + + /** + * @brief Generates an EntryId for a given parent EntryId and filename. + */ + virtual Result<EntryId, QMResult> GetEntryId( + const FileSystemChildMetadata& aHandle) const = 0; + + /** + * @brief To check if a file under a directory is locked, we need to map + * fileId's to entries. + * + * @param aFileId a FileId + * @return Result<EntryId, QMResult> Entry id of a temporary or main file + */ + virtual Result<EntryId, QMResult> GetEntryId(const FileId& aFileId) const = 0; + + /** + * @brief Make sure EntryId maps to a FileId. This method should be called + * before exclusive locking is attempted. + */ + virtual Result<FileId, QMResult> EnsureFileId(const EntryId& aEntryId) = 0; + + /** + * @brief Make sure EntryId maps to a temporary FileId. This method should be + * called before shared locking is attempted. + */ + virtual Result<FileId, QMResult> EnsureTemporaryFileId( + const EntryId& aEntryId) = 0; + + /** + * @brief To support moves in metadata, the actual files on disk are tagged + * with file id's which are mapped to entry id's which represent paths. + * This function returns the main file corresponding to an entry. + * + * @param aEntryId An id of an entry + * @return Result<EntryId, QMResult> Main file id, used by exclusive locks + */ + virtual Result<FileId, QMResult> GetFileId(const EntryId& aEntryId) const = 0; + + /** + * @brief Flag aFileId as the main file for aEntryId or abort. Removes the + * file which did not get flagged as the main file. + */ + virtual nsresult MergeFileId(const EntryId& aEntryId, const FileId& aFileId, + bool aAbort) = 0; + + /** + * @brief Close database connection. + */ + virtual void Close() = 0; + + /** + * @brief Start tracking file's usage. + */ + virtual nsresult BeginUsageTracking(const FileId& aFileId) = 0; + + /** + * @brief Stop tracking file's usage. + */ + virtual nsresult EndUsageTracking(const FileId& aFileId) = 0; + + virtual ~FileSystemDatabaseManager() = default; +}; + +} // namespace data +} // namespace fs +} // namespace dom +} // namespace mozilla + +#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGER_H_ diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp new file mode 100644 index 0000000000..c65bf01508 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp @@ -0,0 +1,1567 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FileSystemDatabaseManagerVersion001.h" + +#include "ErrorList.h" +#include "FileSystemContentTypeGuess.h" +#include "FileSystemDataManager.h" +#include "FileSystemFileManager.h" +#include "FileSystemParentTypes.h" +#include "ResultStatement.h" +#include "StartedTransaction.h" +#include "mozStorageHelper.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/dom/FileSystemDataManager.h" +#include "mozilla/dom/FileSystemHandle.h" +#include "mozilla/dom/FileSystemLog.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/PFileSystemManager.h" +#include "mozilla/dom/quota/Client.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/QuotaObject.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "nsString.h" + +namespace mozilla::dom { + +using FileSystemEntries = nsTArray<fs::FileSystemEntryMetadata>; + +namespace fs::data { + +namespace { + +constexpr const nsLiteralCString gDescendantsQuery = + "WITH RECURSIVE traceChildren(handle, parent) AS ( " + "SELECT handle, parent " + "FROM Entries " + "WHERE handle=:handle " + "UNION " + "SELECT Entries.handle, Entries.parent FROM traceChildren, Entries " + "WHERE traceChildren.handle=Entries.parent ) " + "SELECT handle " + "FROM traceChildren INNER JOIN Files " + "USING(handle) " + ";"_ns; + +Result<bool, QMResult> IsDirectoryEmpty(const FileSystemConnection& mConnection, + const EntryId& aEntryId) { + const nsLiteralCString isDirEmptyQuery = + "SELECT EXISTS (" + "SELECT 1 FROM Entries WHERE parent = :parent " + ");"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, isDirEmptyQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aEntryId))); + QM_TRY_UNWRAP(bool childrenExist, stmt.YesOrNoQuery()); + + return !childrenExist; +} + +Result<bool, QMResult> DoesDirectoryExist( + const FileSystemConnection& mConnection, + const FileSystemChildMetadata& aHandle) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + const nsCString existsQuery = + "SELECT EXISTS " + "(SELECT 1 FROM Directories INNER JOIN Entries USING (handle) " + "WHERE Directories.name = :name AND Entries.parent = :parent ) " + ";"_ns; + + QM_TRY_RETURN(ApplyEntryExistsQuery(mConnection, existsQuery, aHandle)); +} + +Result<bool, QMResult> DoesDirectoryExist( + const FileSystemConnection& mConnection, const EntryId& aEntry) { + MOZ_ASSERT(!aEntry.IsEmpty()); + + const nsCString existsQuery = + "SELECT EXISTS " + "(SELECT 1 FROM Directories WHERE handle = :handle ) " + ";"_ns; + + QM_TRY_RETURN(ApplyEntryExistsQuery(mConnection, existsQuery, aEntry)); +} + +Result<bool, QMResult> IsAncestor(const FileSystemConnection& aConnection, + const FileSystemEntryPair& aEndpoints) { + const nsCString pathQuery = + "WITH RECURSIVE followPath(handle, parent) AS ( " + "SELECT handle, parent " + "FROM Entries " + "WHERE handle=:entryId " + "UNION " + "SELECT Entries.handle, Entries.parent FROM followPath, Entries " + "WHERE followPath.parent=Entries.handle ) " + "SELECT EXISTS " + "(SELECT 1 FROM followPath " + "WHERE handle=:possibleAncestor ) " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, pathQuery)); + QM_TRY( + QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEndpoints.childId()))); + QM_TRY(QM_TO_RESULT( + stmt.BindEntryIdByName("possibleAncestor"_ns, aEndpoints.parentId()))); + + return stmt.YesOrNoQuery(); +} + +Result<bool, QMResult> DoesFileExist(const FileSystemConnection& aConnection, + const FileSystemChildMetadata& aHandle) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + const nsCString existsQuery = + "SELECT EXISTS " + "(SELECT 1 FROM Files INNER JOIN Entries USING (handle) " + "WHERE Files.name = :name AND Entries.parent = :parent ) " + ";"_ns; + + QM_TRY_RETURN(ApplyEntryExistsQuery(aConnection, existsQuery, aHandle)); +} + +nsresult GetEntries(const FileSystemConnection& aConnection, + const nsACString& aUnboundStmt, const EntryId& aParent, + PageNumber aPage, bool aDirectory, + FileSystemEntries& aEntries) { + // The entries inside a directory are sent to the child process in batches + // of pageSize items. Large value ensures that iteration is less often delayed + // by IPC messaging and querying the database. + // TODO: The current value 1024 is not optimized. + // TODO: Value "pageSize" is shared with the iterator implementation and + // should be defined in a common place. + const int32_t pageSize = 1024; + + QM_TRY_UNWRAP(bool exists, DoesDirectoryExist(aConnection, aParent)); + if (!exists) { + return NS_ERROR_DOM_NOT_FOUND_ERR; + } + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, aUnboundStmt)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aParent))); + QM_TRY(QM_TO_RESULT(stmt.BindPageNumberByName("pageSize"_ns, pageSize))); + QM_TRY(QM_TO_RESULT( + stmt.BindPageNumberByName("pageOffset"_ns, aPage * pageSize))); + + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + + while (moreResults) { + QM_TRY_UNWRAP(EntryId entryId, stmt.GetEntryIdByColumn(/* Column */ 0u)); + QM_TRY_UNWRAP(Name entryName, stmt.GetNameByColumn(/* Column */ 1u)); + + FileSystemEntryMetadata metadata(entryId, entryName, aDirectory); + aEntries.AppendElement(metadata); + + QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep()); + } + + return NS_OK; +} + +Result<EntryId, QMResult> GetUniqueEntryId( + const FileSystemConnection& aConnection, + const FileSystemChildMetadata& aHandle) { + const nsCString existsQuery = + "SELECT EXISTS " + "(SELECT 1 FROM Entries " + "WHERE handle = :handle )" + ";"_ns; + + FileSystemChildMetadata generatorInput = aHandle; + + const size_t maxRounds = 1024u; + + for (size_t hangGuard = 0u; hangGuard < maxRounds; ++hangGuard) { + QM_TRY_UNWRAP(EntryId entryId, fs::data::GetEntryHandle(generatorInput)); + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, existsQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId))); + + QM_TRY_UNWRAP(bool alreadyInUse, stmt.YesOrNoQuery()); + + if (!alreadyInUse) { + return entryId; + } + + generatorInput.parentId() = entryId; + } + + return Err(QMResult(NS_ERROR_UNEXPECTED)); +} + +nsresult PerformRename(const FileSystemConnection& aConnection, + const FileSystemEntryMetadata& aHandle, + const Name& aNewName, const ContentType& aNewType, + const nsLiteralCString& aNameUpdateQuery) { + MOZ_ASSERT(!aHandle.entryId().IsEmpty()); + MOZ_ASSERT(IsValidName(aHandle.entryName())); + + // same-name is checked in RenameEntry() + if (!IsValidName(aNewName)) { + return NS_ERROR_DOM_TYPE_MISMATCH_ERR; + } + + // TODO: This should fail when handle doesn't exist - the + // explicit file or directory existence queries are redundant + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, aNameUpdateQuery) + .mapErr(toNSResult)); + if (!aNewType.IsVoid()) { + QM_TRY(MOZ_TO_RESULT(stmt.BindContentTypeByName("type"_ns, aNewType))); + } + QM_TRY(MOZ_TO_RESULT(stmt.BindNameByName("name"_ns, aNewName))); + QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aHandle.entryId()))); + QM_TRY(MOZ_TO_RESULT(stmt.Execute())); + + return NS_OK; +} + +nsresult PerformRenameDirectory(const FileSystemConnection& aConnection, + const FileSystemEntryMetadata& aHandle, + const Name& aNewName) { + const nsLiteralCString updateDirectoryNameQuery = + "UPDATE Directories " + "SET name = :name " + "WHERE handle = :handle " + ";"_ns; + + return PerformRename(aConnection, aHandle, aNewName, VoidCString(), + updateDirectoryNameQuery); +} + +nsresult PerformRenameFile(const FileSystemConnection& aConnection, + const FileSystemEntryMetadata& aHandle, + const Name& aNewName, const ContentType& aNewType) { + const nsLiteralCString updateFileTypeAndNameQuery = + "UPDATE Files SET type = :type, name = :name " + "WHERE handle = :handle ;"_ns; + + const nsLiteralCString updateFileNameQuery = + "UPDATE Files SET name = :name WHERE handle = :handle ;"_ns; + + if (aNewType.IsVoid()) { + return PerformRename(aConnection, aHandle, aNewName, aNewType, + updateFileNameQuery); + } + + return PerformRename(aConnection, aHandle, aNewName, aNewType, + updateFileTypeAndNameQuery); +} + +template <class HandlerType> +nsresult SetUsageTrackingImpl(const FileSystemConnection& aConnection, + const FileId& aFileId, bool aTracked, + HandlerType&& aOnMissingFile) { + const nsLiteralCString setTrackedQuery = + "INSERT INTO Usages " + "( handle, tracked ) " + "VALUES " + "( :handle, :tracked ) " + "ON CONFLICT(handle) DO " + "UPDATE SET tracked = excluded.tracked " + ";"_ns; + + const nsresult customReturnValue = + aTracked ? NS_ERROR_DOM_NOT_FOUND_ERR : NS_OK; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, setTrackedQuery)); + QM_TRY(MOZ_TO_RESULT(stmt.BindFileIdByName("handle"_ns, aFileId))); + QM_TRY(MOZ_TO_RESULT(stmt.BindBooleanByName("tracked"_ns, aTracked))); + QM_TRY(MOZ_TO_RESULT(stmt.Execute()), customReturnValue, + std::forward<HandlerType>(aOnMissingFile)); + + return NS_OK; +} + +Result<nsTArray<FileId>, QMResult> GetTrackedFiles( + const FileSystemConnection& aConnection) { + // The same query works for both 001 and 002 schemas because handle is + // an entry id and later on a file id, respectively. + static const nsLiteralCString getTrackedFilesQuery = + "SELECT handle FROM Usages WHERE tracked = TRUE;"_ns; + nsTArray<FileId> trackedFiles; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, getTrackedFilesQuery)); + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + + while (moreResults) { + QM_TRY_UNWRAP(FileId fileId, stmt.GetFileIdByColumn(/* Column */ 0u)); + + trackedFiles.AppendElement(fileId); // TODO: fallible? + + QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep()); + } + + return trackedFiles; +} + +/** This handles the file not found error by assigning 0 usage to the dangling + * handle and puts the handle to a non-tracked state. Otherwise, when the + * file or database cannot be reached, the file remains in the tracked state. + */ +template <class QuotaCacheUpdate> +nsresult UpdateUsageForFileEntry(const FileSystemConnection& aConnection, + const FileSystemFileManager& aFileManager, + const FileId& aFileId, + const nsLiteralCString& aUpdateQuery, + QuotaCacheUpdate&& aUpdateCache) { + QM_TRY_INSPECT(const auto& fileHandle, aFileManager.GetFile(aFileId)); + + // A file could have changed in a way which doesn't allow to read its size. + QM_TRY_UNWRAP( + const Usage fileSize, + QM_OR_ELSE_WARN_IF( + // Expression. + MOZ_TO_RESULT_INVOKE_MEMBER(fileHandle, GetFileSize), + // Predicate. + ([](const nsresult rv) { return rv == NS_ERROR_FILE_NOT_FOUND; }), + // Fallback. If the file does no longer exist, treat it as 0-sized. + ErrToDefaultOk<Usage>)); + + QM_TRY(MOZ_TO_RESULT(aUpdateCache(fileSize))); + + // No transaction as one statement succeeds or fails atomically + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, aUpdateQuery)); + + QM_TRY(MOZ_TO_RESULT(stmt.BindFileIdByName("handle"_ns, aFileId))); + + QM_TRY(MOZ_TO_RESULT(stmt.BindUsageByName("usage"_ns, fileSize))); + + QM_TRY(MOZ_TO_RESULT(stmt.Execute())); + + return NS_OK; +} + +nsresult UpdateUsageUnsetTracked(const FileSystemConnection& aConnection, + const FileSystemFileManager& aFileManager, + const FileId& aFileId) { + static const nsLiteralCString updateUsagesUnsetTrackedQuery = + "UPDATE Usages SET usage = :usage, tracked = FALSE " + "WHERE handle = :handle;"_ns; + + auto noCacheUpdateNeeded = [](auto) { return NS_OK; }; + + return UpdateUsageForFileEntry(aConnection, aFileManager, aFileId, + updateUsagesUnsetTrackedQuery, + std::move(noCacheUpdateNeeded)); +} + +/** + * @brief Get the recorded usage only if the file is in tracked state. + * During origin initialization, if the usage on disk is unreadable, the latest + * recorded usage is reported to the quota manager for the tracked files. + * To allow writing, we attempt to update the real usage with one database and + * one file size query. + */ +Result<Maybe<Usage>, QMResult> GetMaybeTrackedUsage( + const FileSystemConnection& aConnection, const FileId& aFileId) { + const nsLiteralCString trackedUsageQuery = + "SELECT usage FROM Usages WHERE tracked = TRUE AND handle = :handle " + ");"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, trackedUsageQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("handle"_ns, aFileId))); + + QM_TRY_UNWRAP(const bool moreResults, stmt.ExecuteStep()); + if (!moreResults) { + return Maybe<Usage>(Nothing()); + } + + QM_TRY_UNWRAP(Usage trackedUsage, stmt.GetUsageByColumn(/* Column */ 0u)); + + return Some(trackedUsage); +} + +Result<bool, nsresult> ScanTrackedFiles( + const FileSystemConnection& aConnection, + const FileSystemFileManager& aFileManager) { + QM_TRY_INSPECT(const nsTArray<FileId>& trackedFiles, + GetTrackedFiles(aConnection).mapErr(toNSResult)); + + bool ok = true; + for (const auto& fileId : trackedFiles) { + // On success, tracked is set to false, otherwise its value is kept (= true) + QM_WARNONLY_TRY(MOZ_TO_RESULT(UpdateUsageUnsetTracked( + aConnection, aFileManager, fileId)), + [&ok](const auto& /*aRv*/) { ok = false; }); + } + + return ok; +} + +Result<Ok, QMResult> DeleteEntry(const FileSystemConnection& aConnection, + const EntryId& aEntryId) { + // If it's a directory, deleting the handle will cascade + const nsLiteralCString deleteEntryQuery = + "DELETE FROM Entries " + "WHERE handle = :handle " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, deleteEntryQuery)); + + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + + QM_TRY(QM_TO_RESULT(stmt.Execute())); + + return Ok{}; +} + +Result<int32_t, QMResult> GetTrackedFilesCount( + const FileSystemConnection& aConnection) { + // TODO: We could query the count directly + QM_TRY_INSPECT(const auto& trackedFiles, GetTrackedFiles(aConnection)); + + CheckedInt32 checkedFileCount = trackedFiles.Length(); + QM_TRY(OkIf(checkedFileCount.isValid()), + Err(QMResult(NS_ERROR_ILLEGAL_VALUE))); + + return checkedFileCount.value(); +} + +void LogWithFilename(const FileSystemFileManager& aFileManager, + const char* aFormat, const FileId& aFileId) { + if (!LOG_ENABLED()) { + return; + } + + QM_TRY_INSPECT(const auto& localFile, aFileManager.GetFile(aFileId), QM_VOID); + + nsAutoString localPath; + QM_TRY(MOZ_TO_RESULT(localFile->GetPath(localPath)), QM_VOID); + LOG((aFormat, NS_ConvertUTF16toUTF8(localPath).get())); +} + +Result<bool, QMResult> IsAnyDescendantLocked( + const FileSystemConnection& aConnection, + const FileSystemDataManager& aDataManager, const EntryId& aEntryId) { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, gDescendantsQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + + while (moreResults) { + // Works only for version 001 + QM_TRY_INSPECT(const EntryId& entryId, + stmt.GetEntryIdByColumn(/* Column */ 0u)); + + QM_TRY_UNWRAP(const bool isLocked, aDataManager.IsLocked(entryId), true); + if (isLocked) { + return true; + } + + QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep()); + } + + return false; +} + +} // namespace + +FileSystemDatabaseManagerVersion001::FileSystemDatabaseManagerVersion001( + FileSystemDataManager* aDataManager, FileSystemConnection&& aConnection, + UniquePtr<FileSystemFileManager>&& aFileManager, const EntryId& aRootEntry) + : mDataManager(aDataManager), + mConnection(aConnection), + mFileManager(std::move(aFileManager)), + mRootEntry(aRootEntry), + mClientMetadata(aDataManager->OriginMetadataRef(), + quota::Client::FILESYSTEM), + mFilesOfUnknownUsage(-1) {} + +/* static */ +nsresult FileSystemDatabaseManagerVersion001::RescanTrackedUsages( + const FileSystemConnection& aConnection, + const quota::OriginMetadata& aOriginMetadata) { + QM_TRY_UNWRAP(UniquePtr<FileSystemFileManager> fileManager, + data::FileSystemFileManager::CreateFileSystemFileManager( + aOriginMetadata)); + + QM_TRY_UNWRAP(bool ok, ScanTrackedFiles(aConnection, *fileManager)); + if (ok) { + return NS_OK; + } + + // Retry once without explicit delay + QM_TRY_UNWRAP(ok, ScanTrackedFiles(aConnection, *fileManager)); + if (!ok) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +/* static */ +Result<Usage, QMResult> FileSystemDatabaseManagerVersion001::GetFileUsage( + const FileSystemConnection& aConnection) { + const nsLiteralCString sumUsagesQuery = "SELECT sum(usage) FROM Usages;"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, sumUsagesQuery)); + + QM_TRY_UNWRAP(const bool moreResults, stmt.ExecuteStep()); + if (!moreResults) { + return Err(QMResult(NS_ERROR_DOM_FILE_NOT_READABLE_ERR)); + } + + QM_TRY_UNWRAP(Usage totalFiles, stmt.GetUsageByColumn(/* Column */ 0u)); + + return totalFiles; +} + +nsresult FileSystemDatabaseManagerVersion001::UpdateUsage( + const FileId& aFileId) { + // We don't track directories or non-existent files. + QM_TRY_UNWRAP(bool fileExists, DoesFileIdExist(aFileId).mapErr(toNSResult)); + if (!fileExists) { + return NS_OK; // May be deleted before update, no assert + } + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> file, mFileManager->GetFile(aFileId)); + MOZ_ASSERT(file); + + Usage fileSize = 0; + bool exists = false; + QM_TRY(MOZ_TO_RESULT(file->Exists(&exists))); + if (exists) { + QM_TRY(MOZ_TO_RESULT(file->GetFileSize(&fileSize))); + } + + QM_TRY(MOZ_TO_RESULT(UpdateUsageInDatabase(aFileId, fileSize))); + + return NS_OK; +} + +Result<EntryId, QMResult> +FileSystemDatabaseManagerVersion001::GetOrCreateDirectory( + const FileSystemChildMetadata& aHandle, bool aCreate) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + const auto& name = aHandle.childName(); + // Belt and suspenders: check here as well as in child. + if (!IsValidName(name)) { + return Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR)); + } + MOZ_ASSERT(!(name.IsVoid() || name.IsEmpty())); + + bool exists = true; + QM_TRY_UNWRAP(exists, DoesFileExist(mConnection, aHandle)); + + // By spec, we don't allow a file and a directory + // to have the same name and parent + if (exists) { + return Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR)); + } + + QM_TRY_UNWRAP(exists, DoesDirectoryExist(mConnection, aHandle)); + + // exists as directory + if (exists) { + return FindEntryId(mConnection, aHandle, false); + } + + if (!aCreate) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + const nsLiteralCString insertEntryQuery = + "INSERT OR IGNORE INTO Entries " + "( handle, parent ) " + "VALUES " + "( :handle, :parent ) " + ";"_ns; + + const nsLiteralCString insertDirectoryQuery = + "INSERT OR IGNORE INTO Directories " + "( handle, name ) " + "VALUES " + "( :handle, :name ) " + ";"_ns; + + QM_TRY_INSPECT(const EntryId& entryId, GetEntryId(aHandle)); + MOZ_ASSERT(!entryId.IsEmpty()); + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection)); + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, insertEntryQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId))); + QM_TRY( + QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aHandle.parentId()))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, insertDirectoryQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId))); + QM_TRY(QM_TO_RESULT(stmt.BindNameByName("name"_ns, name))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + QM_TRY_UNWRAP(DebugOnly<bool> doesItExistNow, + DoesDirectoryExist(mConnection, aHandle)); + MOZ_ASSERT(doesItExistNow); + + return entryId; +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion001::GetOrCreateFile( + const FileSystemChildMetadata& aHandle, bool aCreate) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + const auto& name = aHandle.childName(); + // Belt and suspenders: check here as well as in child. + if (!IsValidName(name)) { + return Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR)); + } + MOZ_ASSERT(!(name.IsVoid() || name.IsEmpty())); + + QM_TRY_UNWRAP(bool exists, DoesDirectoryExist(mConnection, aHandle)); + + // By spec, we don't allow a file and a directory + // to have the same name and parent + QM_TRY(OkIf(!exists), Err(QMResult(NS_ERROR_DOM_TYPE_MISMATCH_ERR))); + + QM_TRY_UNWRAP(exists, DoesFileExist(mConnection, aHandle)); + + if (exists) { + QM_TRY_RETURN(FindEntryId(mConnection, aHandle, /* aIsFile */ true)); + } + + if (!aCreate) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + const nsLiteralCString insertEntryQuery = + "INSERT INTO Entries " + "( handle, parent ) " + "VALUES " + "( :handle, :parent ) " + ";"_ns; + + const nsLiteralCString insertFileQuery = + "INSERT INTO Files " + "( handle, type, name ) " + "VALUES " + "( :handle, :type, :name ) " + ";"_ns; + + QM_TRY_INSPECT(const EntryId& entryId, GetEntryId(aHandle)); + MOZ_ASSERT(!entryId.IsEmpty()); + + const ContentType type = DetermineContentType(name); + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection)); + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, insertEntryQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId))); + QM_TRY( + QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aHandle.parentId()))); + QM_TRY(QM_TO_RESULT(stmt.Execute()), QM_PROPAGATE, + ([this, &aHandle](const auto& aRv) { + QM_TRY_UNWRAP(bool parentExists, + DoesDirectoryExist(mConnection, aHandle.parentId()), + QM_VOID); + QM_TRY(OkIf(parentExists), QM_VOID); + })); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, insertFileQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId))); + QM_TRY(QM_TO_RESULT(stmt.BindContentTypeByName("type"_ns, type))); + QM_TRY(QM_TO_RESULT(stmt.BindNameByName("name"_ns, name))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + return entryId; +} + +nsresult FileSystemDatabaseManagerVersion001::GetFile( + const EntryId& aEntryId, const FileId& aFileId, const FileMode& aMode, + ContentType& aType, TimeStamp& lastModifiedMilliSeconds, + nsTArray<Name>& aPath, nsCOMPtr<nsIFile>& aFile) const { + MOZ_ASSERT(!aFileId.IsEmpty()); + MOZ_ASSERT(aMode == FileMode::EXCLUSIVE); + + const FileSystemEntryPair endPoints(mRootEntry, aEntryId); + QM_TRY_UNWRAP(aPath, ResolveReversedPath(mConnection, endPoints)); + if (aPath.IsEmpty()) { + return NS_ERROR_DOM_NOT_FOUND_ERR; + } + + QM_TRY(MOZ_TO_RESULT(GetFileAttributes(mConnection, aEntryId, aType))); + QM_TRY_UNWRAP(aFile, mFileManager->GetOrCreateFile(aFileId)); + + PRTime lastModTime = 0; + QM_TRY(MOZ_TO_RESULT(aFile->GetLastModifiedTime(&lastModTime))); + lastModifiedMilliSeconds = static_cast<TimeStamp>(lastModTime); + + aPath.Reverse(); + + return NS_OK; +} + +Result<FileSystemDirectoryListing, QMResult> +FileSystemDatabaseManagerVersion001::GetDirectoryEntries( + const EntryId& aParent, PageNumber aPage) const { + // TODO: Offset is reported to have bad performance - see Bug 1780386. + const nsCString directoriesQuery = + "SELECT Dirs.handle, Dirs.name " + "FROM Directories AS Dirs " + "INNER JOIN ( " + "SELECT handle " + "FROM Entries " + "WHERE parent = :parent " + "LIMIT :pageSize " + "OFFSET :pageOffset ) " + "AS Ents " + "ON Dirs.handle = Ents.handle " + ";"_ns; + const nsCString filesQuery = + "SELECT Files.handle, Files.name " + "FROM Files " + "INNER JOIN ( " + "SELECT handle " + "FROM Entries " + "WHERE parent = :parent " + "LIMIT :pageSize " + "OFFSET :pageOffset ) " + "AS Ents " + "ON Files.handle = Ents.handle " + ";"_ns; + + FileSystemDirectoryListing entries; + QM_TRY( + QM_TO_RESULT(GetEntries(mConnection, directoriesQuery, aParent, aPage, + /* aDirectory */ true, entries.directories()))); + + QM_TRY(QM_TO_RESULT(GetEntries(mConnection, filesQuery, aParent, aPage, + /* aDirectory */ false, entries.files()))); + + return entries; +} + +Result<bool, QMResult> FileSystemDatabaseManagerVersion001::RemoveDirectory( + const FileSystemChildMetadata& aHandle, bool aRecursive) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + if (aHandle.childName().IsEmpty()) { + return false; + } + + DebugOnly<Name> name = aHandle.childName(); + MOZ_ASSERT(!name.inspect().IsVoid()); + + QM_TRY_UNWRAP(bool exists, DoesDirectoryExist(mConnection, aHandle)); + + if (!exists) { + return false; + } + + // At this point, entry exists and is a directory. + QM_TRY_UNWRAP(EntryId entryId, FindEntryId(mConnection, aHandle, false)); + MOZ_ASSERT(!entryId.IsEmpty()); + + QM_TRY_UNWRAP(bool isEmpty, IsDirectoryEmpty(mConnection, entryId)); + + MOZ_ASSERT(mDataManager); + QM_TRY_UNWRAP(const bool isLocked, + IsAnyDescendantLocked(mConnection, *mDataManager, entryId)); + + QM_TRY(OkIf(!isLocked), + Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR))); + + if (!aRecursive && !isEmpty) { + return Err(QMResult(NS_ERROR_DOM_INVALID_MODIFICATION_ERR)); + } + + QM_TRY_UNWRAP(Usage usage, GetUsagesOfDescendants(entryId)); + + QM_TRY_INSPECT(const nsTArray<FileId>& descendants, + FindFilesUnderEntry(entryId)); + + nsTArray<FileId> failedRemovals; + QM_TRY_UNWRAP(DebugOnly<Usage> removedUsage, + mFileManager->RemoveFiles(descendants, failedRemovals)); + + // Usage is for the current main file but we remove temporary files too. + MOZ_ASSERT_IF(failedRemovals.IsEmpty() && (0 == mFilesOfUnknownUsage), + usage <= removedUsage); + + TryRemoveDuringIdleMaintenance(failedRemovals); + + auto isInFailedRemovals = [&failedRemovals](const auto& aFileId) { + return failedRemovals.cend() != + std::find_if(failedRemovals.cbegin(), failedRemovals.cend(), + [&aFileId](const auto& aFailedRemoval) { + return aFileId == aFailedRemoval; + }); + }; + + for (const auto& fileId : descendants) { + if (!isInFailedRemovals(fileId)) { + QM_WARNONLY_TRY(QM_TO_RESULT(RemoveFileId(fileId))); + } + } + + if (usage > 0) { // Performance! + DecreaseCachedQuotaUsage(usage); + } + + QM_TRY(DeleteEntry(mConnection, entryId)); + + return true; +} + +Result<bool, QMResult> FileSystemDatabaseManagerVersion001::RemoveFile( + const FileSystemChildMetadata& aHandle) { + MOZ_ASSERT(!aHandle.parentId().IsEmpty()); + + if (aHandle.childName().IsEmpty()) { + return false; + } + + DebugOnly<Name> name = aHandle.childName(); + MOZ_ASSERT(!name.inspect().IsVoid()); + + // Make it more evident that we won't remove directories + QM_TRY_UNWRAP(bool exists, DoesFileExist(mConnection, aHandle)); + + if (!exists) { + return false; + } + + // At this point, entry exists and is a file + QM_TRY_UNWRAP(EntryId entryId, FindEntryId(mConnection, aHandle, true)); + MOZ_ASSERT(!entryId.IsEmpty()); + + // XXX This code assumes the spec question is resolved to state + // removing an in-use file should fail. If it shouldn't fail, we need to + // do something to neuter all the extant FileAccessHandles/WritableFileStreams + // that reference it + QM_TRY_UNWRAP(const bool isLocked, mDataManager->IsLocked(entryId)); + if (isLocked) { + LOG(("Trying to remove in-use file")); + return Err(QMResult(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR)); + } + + QM_TRY_INSPECT(const nsTArray<FileId>& diskItems, + FindFilesUnderEntry(entryId)); + + QM_TRY_UNWRAP(Usage usage, GetUsagesOfDescendants(entryId)); + + nsTArray<FileId> failedRemovals; + QM_TRY_UNWRAP(DebugOnly<Usage> removedUsage, + mFileManager->RemoveFiles(diskItems, failedRemovals)); + + // We only check the most common case. This can fail spuriously if an external + // application writes to the file, or OS reports zero size due to corruption. + MOZ_ASSERT_IF(failedRemovals.IsEmpty() && (0 == mFilesOfUnknownUsage), + usage == removedUsage); + + TryRemoveDuringIdleMaintenance(failedRemovals); + + auto isInFailedRemovals = [&failedRemovals](const auto& aFileId) { + return failedRemovals.cend() != + std::find_if(failedRemovals.cbegin(), failedRemovals.cend(), + [&aFileId](const auto& aFailedRemoval) { + return aFileId == aFailedRemoval; + }); + }; + + for (const auto& fileId : diskItems) { + if (!isInFailedRemovals(fileId)) { + QM_WARNONLY_TRY(QM_TO_RESULT(RemoveFileId(fileId))); + } + } + + if (usage > 0) { // Performance! + DecreaseCachedQuotaUsage(usage); + } + + QM_TRY(DeleteEntry(mConnection, entryId)); + + return true; +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion001::RenameEntry( + const FileSystemEntryMetadata& aHandle, const Name& aNewName) { + const auto& entryId = aHandle.entryId(); + + // Can't rename root + if (mRootEntry == entryId) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + // Verify the source exists + QM_TRY_UNWRAP(bool isFile, IsFile(mConnection, entryId), + Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR))); + + // Are we actually renaming? + if (aHandle.entryName() == aNewName) { + return entryId; + } + + QM_TRY(QM_TO_RESULT(PrepareRenameEntry(mConnection, mDataManager, aHandle, + aNewName, isFile))); + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection)); + + if (isFile) { + const ContentType type = DetermineContentType(aNewName); + QM_TRY( + QM_TO_RESULT(PerformRenameFile(mConnection, aHandle, aNewName, type))); + } else { + QM_TRY( + QM_TO_RESULT(PerformRenameDirectory(mConnection, aHandle, aNewName))); + } + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + return entryId; +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion001::MoveEntry( + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation) { + const auto& entryId = aHandle.entryId(); + MOZ_ASSERT(!entryId.IsEmpty()); + + if (mRootEntry == entryId) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + // Verify the source exists + QM_TRY_UNWRAP(bool isFile, IsFile(mConnection, entryId), + Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR))); + + // If the rename doesn't change the name or directory, just return success. + // XXX Needs to be added to the spec + QM_WARNONLY_TRY_UNWRAP(Maybe<bool> maybeSame, + IsSame(mConnection, aHandle, aNewDesignation, isFile)); + if (maybeSame && maybeSame.value()) { + return entryId; + } + + QM_TRY(QM_TO_RESULT(PrepareMoveEntry(mConnection, mDataManager, aHandle, + aNewDesignation, isFile))); + + const nsLiteralCString updateEntryParentQuery = + "UPDATE Entries " + "SET parent = :parent " + "WHERE handle = :handle " + ";"_ns; + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection)); + + { + // We always change the parent because it's simpler than checking if the + // parent needs to be changed + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, updateEntryParentQuery)); + QM_TRY(QM_TO_RESULT( + stmt.BindEntryIdByName("parent"_ns, aNewDesignation.parentId()))); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, entryId))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + const Name& newName = aNewDesignation.childName(); + + // Are we actually renaming? + if (aHandle.entryName() == newName) { + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + return entryId; + } + + if (isFile) { + const ContentType type = DetermineContentType(newName); + QM_TRY( + QM_TO_RESULT(PerformRenameFile(mConnection, aHandle, newName, type))); + } else { + QM_TRY(QM_TO_RESULT(PerformRenameDirectory(mConnection, aHandle, newName))); + } + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + return entryId; +} + +Result<Path, QMResult> FileSystemDatabaseManagerVersion001::Resolve( + const FileSystemEntryPair& aEndpoints) const { + QM_TRY_UNWRAP(Path path, ResolveReversedPath(mConnection, aEndpoints)); + // Note: if not an ancestor, returns null + + path.Reverse(); + return path; +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion001::GetEntryId( + const FileSystemChildMetadata& aHandle) const { + return GetUniqueEntryId(mConnection, aHandle); +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion001::GetEntryId( + const FileId& aFileId) const { + return aFileId.Value(); +} + +Result<FileId, QMResult> FileSystemDatabaseManagerVersion001::EnsureFileId( + const EntryId& aEntryId) { + return FileId(aEntryId); +} + +Result<FileId, QMResult> +FileSystemDatabaseManagerVersion001::EnsureTemporaryFileId( + const EntryId& aEntryId) { + return FileId(aEntryId); +} + +Result<FileId, QMResult> FileSystemDatabaseManagerVersion001::GetFileId( + const EntryId& aEntryId) const { + return FileId(aEntryId); +} + +nsresult FileSystemDatabaseManagerVersion001::MergeFileId( + const EntryId& /* aEntryId */, const FileId& /* aFileId */, + bool /* aAbort */) { + // Version 001 should always use exclusive mode and not get here. + return NS_ERROR_NOT_IMPLEMENTED; +} + +void FileSystemDatabaseManagerVersion001::Close() { mConnection->Close(); } + +nsresult FileSystemDatabaseManagerVersion001::BeginUsageTracking( + const FileId& aFileId) { + MOZ_ASSERT(!aFileId.IsEmpty()); + + // If file is already tracked but we cannot read its size, error. + // If file does not exist, this will succeed because usage is zero. + QM_TRY(EnsureUsageIsKnown(aFileId)); + + // If file does not exist, set usage tracking to true fails with + // file not found error. + QM_TRY(MOZ_TO_RESULT(SetUsageTracking(aFileId, true))); + + return NS_OK; +} + +nsresult FileSystemDatabaseManagerVersion001::EndUsageTracking( + const FileId& aFileId) { + // This is expected to fail only if database is unreachable. + QM_TRY(MOZ_TO_RESULT(SetUsageTracking(aFileId, false))); + + return NS_OK; +} + +Result<bool, QMResult> FileSystemDatabaseManagerVersion001::DoesFileIdExist( + const FileId& aFileId) const { + MOZ_ASSERT(!aFileId.IsEmpty()); + + QM_TRY_RETURN(DoesFileExist(mConnection, aFileId.Value())); +} + +nsresult FileSystemDatabaseManagerVersion001::RemoveFileId( + const FileId& /* aFileId */) { + return NS_OK; +} + +/** + * @brief Get the sum of usages for all file descendants of a directory entry. + * We obtain the value with one query, which is presumably better than having a + * separate query for each individual descendant. + * TODO: Check if this is true + * + * Please see GetFileUsage documentation for why we use the latest recorded + * value from the database instead of the file size property from the disk. + */ +Result<Usage, QMResult> +FileSystemDatabaseManagerVersion001::GetUsagesOfDescendants( + const EntryId& aEntryId) const { + const nsLiteralCString descendantUsagesQuery = + "WITH RECURSIVE traceChildren(handle, parent) AS ( " + "SELECT handle, parent " + "FROM Entries " + "WHERE handle=:handle " + "UNION " + "SELECT Entries.handle, Entries.parent FROM traceChildren, Entries " + "WHERE traceChildren.handle=Entries.parent ) " + "SELECT sum(Usages.usage) " + "FROM traceChildren INNER JOIN Usages " + "USING(handle) " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, descendantUsagesQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY_UNWRAP(const bool moreResults, stmt.ExecuteStep()); + if (!moreResults) { + return 0; + } + + QM_TRY_RETURN(stmt.GetUsageByColumn(/* Column */ 0u)); +} + +Result<nsTArray<FileId>, QMResult> +FileSystemDatabaseManagerVersion001::FindFilesUnderEntry( + const EntryId& aEntryId) const { + nsTArray<FileId> descendants; + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, gDescendantsQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + + while (moreResults) { + // Works only for version 001 + QM_TRY_INSPECT(const FileId& fileId, + stmt.GetFileIdByColumn(/* Column */ 0u)); + + descendants.AppendElement(fileId); + + QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep()); + } + } + + return descendants; +} + +nsresult FileSystemDatabaseManagerVersion001::SetUsageTracking( + const FileId& aFileId, bool aTracked) { + auto onMissingFile = [this, &aFileId](const auto& aRv) { + // Usages constrains entryId to be present in Files + MOZ_ASSERT(NS_ERROR_STORAGE_CONSTRAINT == ToNSResult(aRv)); + + // The query *should* fail if and only if file does not exist + QM_TRY_UNWRAP(DebugOnly<bool> fileExists, DoesFileIdExist(aFileId), + QM_VOID); + MOZ_ASSERT(!fileExists); + }; + + return SetUsageTrackingImpl(mConnection, aFileId, aTracked, onMissingFile); +} + +nsresult FileSystemDatabaseManagerVersion001::UpdateUsageInDatabase( + const FileId& aFileId, Usage aNewDiskUsage) { + const nsLiteralCString updateUsageQuery = + "INSERT INTO Usages " + "( handle, usage ) " + "VALUES " + "( :handle, :usage ) " + "ON CONFLICT(handle) DO " + "UPDATE SET usage = excluded.usage " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, updateUsageQuery)); + QM_TRY(MOZ_TO_RESULT(stmt.BindUsageByName("usage"_ns, aNewDiskUsage))); + QM_TRY(MOZ_TO_RESULT(stmt.BindFileIdByName("handle"_ns, aFileId))); + QM_TRY(MOZ_TO_RESULT(stmt.Execute())); + + return NS_OK; +} + +Result<Ok, QMResult> FileSystemDatabaseManagerVersion001::EnsureUsageIsKnown( + const FileId& aFileId) { + if (mFilesOfUnknownUsage < 0) { // Lazy initialization + QM_TRY_UNWRAP(mFilesOfUnknownUsage, GetTrackedFilesCount(mConnection)); + } + + if (mFilesOfUnknownUsage == 0) { + return Ok{}; + } + + QM_TRY_UNWRAP(Maybe<Usage> oldUsage, + GetMaybeTrackedUsage(mConnection, aFileId)); + if (oldUsage.isNothing()) { + return Ok{}; // Usage is 0 or it was successfully recorded at unlocking. + } + + auto quotaCacheUpdate = [this, &aFileId, + oldSize = oldUsage.value()](Usage aNewSize) { + return UpdateCachedQuotaUsage(aFileId, oldSize, aNewSize); + }; + + static const nsLiteralCString updateUsagesKeepTrackedQuery = + "UPDATE Usages SET usage = :usage WHERE handle = :handle;"_ns; + + // If usage update fails, we log an error and keep things the way they were. + QM_TRY(QM_TO_RESULT(UpdateUsageForFileEntry( + mConnection, *mFileManager, aFileId, updateUsagesKeepTrackedQuery, + std::move(quotaCacheUpdate))), + Err(QMResult(NS_ERROR_DOM_FILE_NOT_READABLE_ERR)), + ([this, &aFileId](const auto& /*aRv*/) { + LogWithFilename(*mFileManager, "Could not read the size of file %s", + aFileId); + })); + + // We read and updated the quota usage successfully. + --mFilesOfUnknownUsage; + MOZ_ASSERT(mFilesOfUnknownUsage >= 0); + + return Ok{}; +} + +void FileSystemDatabaseManagerVersion001::DecreaseCachedQuotaUsage( + int64_t aDelta) { + quota::QuotaManager* quotaManager = quota::QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + quotaManager->DecreaseUsageForClient(mClientMetadata, aDelta); +} + +nsresult FileSystemDatabaseManagerVersion001::UpdateCachedQuotaUsage( + const FileId& aFileId, Usage aOldUsage, Usage aNewUsage) { + quota::QuotaManager* quotaManager = quota::QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> fileObj, + mFileManager->GetFile(aFileId).mapErr(toNSResult)); + + RefPtr<quota::QuotaObject> quotaObject = quotaManager->GetQuotaObject( + quota::PERSISTENCE_TYPE_DEFAULT, mClientMetadata, + quota::Client::FILESYSTEM, fileObj, aOldUsage); + MOZ_ASSERT(quotaObject); + + QM_TRY(OkIf(quotaObject->MaybeUpdateSize(aNewUsage, /* aTruncate */ true)), + NS_ERROR_FILE_NO_DEVICE_SPACE); + + return NS_OK; +} + +nsresult FileSystemDatabaseManagerVersion001::ClearDestinationIfNotLocked( + const FileSystemConnection& aConnection, + const FileSystemDataManager* const aDataManager, + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation) { + // If the destination file exists, fail explicitly. Spec author plans to + // revise the spec + QM_TRY_UNWRAP(bool exists, DoesFileExist(aConnection, aNewDesignation)); + if (exists) { + QM_TRY_INSPECT(const EntryId& destId, + FindEntryId(aConnection, aNewDesignation, true)); + QM_TRY_UNWRAP(const bool isLocked, aDataManager->IsLocked(destId)); + if (isLocked) { + LOG(("Trying to overwrite in-use file")); + return NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR; + } + + QM_TRY_UNWRAP(DebugOnly<bool> isRemoved, RemoveFile(aNewDesignation)); + MOZ_ASSERT(isRemoved); + } else { + QM_TRY_UNWRAP(exists, DoesDirectoryExist(aConnection, aNewDesignation)); + if (exists) { + // Fails if directory contains locked files, otherwise total wipeout + QM_TRY_UNWRAP(DebugOnly<bool> isRemoved, + MOZ_TO_RESULT(RemoveDirectory(aNewDesignation, + /* recursive */ true))); + MOZ_ASSERT(isRemoved); + } + } + + return NS_OK; +} + +nsresult FileSystemDatabaseManagerVersion001::PrepareRenameEntry( + const FileSystemConnection& aConnection, + const FileSystemDataManager* const aDataManager, + const FileSystemEntryMetadata& aHandle, const Name& aNewName, + bool aIsFile) { + const EntryId& entryId = aHandle.entryId(); + + // At this point, entry exists + if (aIsFile) { + QM_TRY_UNWRAP(const bool isLocked, aDataManager->IsLocked(entryId)); + if (isLocked) { + LOG(("Trying to move in-use file")); + return NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR; + } + } + + // If the destination file exists, fail explicitly. + FileSystemChildMetadata destination; + QM_TRY_UNWRAP(EntryId parent, FindParent(mConnection, entryId)); + destination.parentId() = parent; + destination.childName() = aNewName; + + QM_TRY(MOZ_TO_RESULT(ClearDestinationIfNotLocked(mConnection, mDataManager, + aHandle, destination))); + + return NS_OK; +} + +nsresult FileSystemDatabaseManagerVersion001::PrepareMoveEntry( + const FileSystemConnection& aConnection, + const FileSystemDataManager* const aDataManager, + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation, bool aIsFile) { + const EntryId& entryId = aHandle.entryId(); + + // At this point, entry exists + if (aIsFile) { + QM_TRY_UNWRAP(const bool isLocked, aDataManager->IsLocked(entryId)); + if (isLocked) { + LOG(("Trying to move in-use file")); + return NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR; + } + } + + QM_TRY(QM_TO_RESULT(ClearDestinationIfNotLocked(aConnection, aDataManager, + aHandle, aNewDesignation))); + + // XXX: This should be before clearing the target + + // To prevent cyclic paths, we check that there is no path from + // the item to be moved to the destination folder. + QM_TRY_UNWRAP(const bool isDestinationUnderSelf, + IsAncestor(aConnection, {entryId, aNewDesignation.parentId()})); + if (isDestinationUnderSelf) { + return NS_ERROR_DOM_INVALID_MODIFICATION_ERR; + } + + return NS_OK; +} + +/** + * Free functions + */ + +Result<bool, QMResult> ApplyEntryExistsQuery( + const FileSystemConnection& aConnection, const nsACString& aQuery, + const FileSystemChildMetadata& aHandle) { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, aQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aHandle.parentId()))); + QM_TRY(QM_TO_RESULT(stmt.BindNameByName("name"_ns, aHandle.childName()))); + + return stmt.YesOrNoQuery(); +} + +Result<bool, QMResult> ApplyEntryExistsQuery( + const FileSystemConnection& aConnection, const nsACString& aQuery, + const EntryId& aEntry) { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, aQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntry))); + + return stmt.YesOrNoQuery(); +} + +Result<bool, QMResult> DoesFileExist(const FileSystemConnection& aConnection, + const EntryId& aEntryId) { + MOZ_ASSERT(!aEntryId.IsEmpty()); + + const nsCString existsQuery = + "SELECT EXISTS " + "(SELECT 1 FROM Files WHERE handle = :handle ) " + ";"_ns; + + QM_TRY_RETURN(ApplyEntryExistsQuery(aConnection, existsQuery, aEntryId)); +} + +Result<bool, QMResult> IsFile(const FileSystemConnection& aConnection, + const EntryId& aEntryId) { + QM_TRY_UNWRAP(bool exists, DoesFileExist(aConnection, aEntryId)); + if (exists) { + return true; + } + + QM_TRY_UNWRAP(exists, DoesDirectoryExist(aConnection, aEntryId)); + if (exists) { + return false; + } + + // Doesn't exist + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); +} + +Result<EntryId, QMResult> FindEntryId(const FileSystemConnection& aConnection, + const FileSystemChildMetadata& aHandle, + bool aIsFile) { + const nsCString aDirectoryQuery = + "SELECT Entries.handle FROM Directories " + "INNER JOIN Entries USING (handle) " + "WHERE Directories.name = :name AND Entries.parent = :parent " + ";"_ns; + + const nsCString aFileQuery = + "SELECT Entries.handle FROM Files INNER JOIN Entries USING (handle) " + "WHERE Files.name = :name AND Entries.parent = :parent " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create( + aConnection, aIsFile ? aFileQuery : aDirectoryQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("parent"_ns, aHandle.parentId()))); + QM_TRY(QM_TO_RESULT(stmt.BindNameByName("name"_ns, aHandle.childName()))); + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + + if (!moreResults) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + QM_TRY_UNWRAP(EntryId entryId, stmt.GetEntryIdByColumn(/* Column */ 0u)); + + return entryId; +} + +Result<EntryId, QMResult> FindParent(const FileSystemConnection& aConnection, + const EntryId& aEntryId) { + const nsCString aParentQuery = + "SELECT handle FROM Entries " + "WHERE handle IN ( " + "SELECT parent FROM Entries WHERE " + "handle = :entryId ) " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, aParentQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEntryId))); + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + + if (!moreResults) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + QM_TRY_UNWRAP(EntryId parentId, stmt.GetEntryIdByColumn(/* Column */ 0u)); + return parentId; +} + +Result<bool, QMResult> IsSame(const FileSystemConnection& aConnection, + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewHandle, + bool aIsFile) { + MOZ_ASSERT(!aNewHandle.parentId().IsEmpty()); + + // Typically aNewHandle does not exist which is not an error + QM_TRY_RETURN(QM_OR_ELSE_LOG_VERBOSE_IF( + // Expression. + FindEntryId(aConnection, aNewHandle, aIsFile) + .map([&aHandle](const EntryId& entryId) { + return entryId == aHandle.entryId(); + }), + // Predicate. + IsSpecificError<NS_ERROR_DOM_NOT_FOUND_ERR>, + // Fallback. + ErrToOkFromQMResult<false>)); +} + +Result<Path, QMResult> ResolveReversedPath( + const FileSystemConnection& aConnection, + const FileSystemEntryPair& aEndpoints) { + const nsLiteralCString pathQuery = + "WITH RECURSIVE followPath(handle, parent) AS ( " + "SELECT handle, parent " + "FROM Entries " + "WHERE handle=:entryId " + "UNION " + "SELECT Entries.handle, Entries.parent FROM followPath, Entries " + "WHERE followPath.parent=Entries.handle ) " + "SELECT COALESCE(Directories.name, Files.name), handle " + "FROM followPath " + "LEFT JOIN Directories USING(handle) " + "LEFT JOIN Files USING(handle);"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, pathQuery)); + QM_TRY( + QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEndpoints.childId()))); + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + + Path pathResult; + while (moreResults) { + QM_TRY_UNWRAP(Name entryName, stmt.GetNameByColumn(/* Column */ 0u)); + QM_TRY_UNWRAP(EntryId entryId, stmt.GetEntryIdByColumn(/* Column */ 1u)); + + if (aEndpoints.parentId() == entryId) { + return pathResult; + } + pathResult.AppendElement(entryName); + + QM_TRY_UNWRAP(moreResults, stmt.ExecuteStep()); + } + + // Spec wants us to return 'null' for not-an-ancestor case + pathResult.Clear(); + return pathResult; +} + +nsresult GetFileAttributes(const FileSystemConnection& aConnection, + const EntryId& aEntryId, ContentType& aType) { + const nsLiteralCString getFileLocation = + "SELECT type FROM Files INNER JOIN Entries USING(handle) " + "WHERE handle = :entryId " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, getFileLocation)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEntryId))); + QM_TRY_UNWRAP(bool hasEntries, stmt.ExecuteStep()); + + // Type is an optional attribute + if (!hasEntries || stmt.IsNullByColumn(/* Column */ 0u)) { + return NS_OK; + } + + QM_TRY_UNWRAP(aType, stmt.GetContentTypeByColumn(/* Column */ 0u)); + + return NS_OK; +} + +// TODO: Implement idle maintenance +void TryRemoveDuringIdleMaintenance( + const nsTArray<FileId>& /* aItemToRemove */) { + // Not implemented +} + +ContentType DetermineContentType(const Name& aName) { + QM_TRY_UNWRAP( + auto typeResult, + QM_OR_ELSE_LOG_VERBOSE( + FileSystemContentTypeGuess::FromPath(aName), + ([](const auto& aRv) -> Result<ContentType, QMResult> { + const nsresult rv = ToNSResult(aRv); + switch (rv) { + case NS_ERROR_FAILURE: /* There is an unknown new extension. */ + return ContentType(""_ns); /* We clear the old extension. */ + + case NS_ERROR_INVALID_ARG: /* The name is garbled. */ + [[fallthrough]]; + case NS_ERROR_NOT_AVAILABLE: /* There is no extension. */ + return VoidCString(); /* We keep the old extension. */ + + default: + MOZ_ASSERT_UNREACHABLE("Should never get here!"); + return Err(aRv); + } + })), + ContentType(""_ns)); + + return typeResult; +} + +} // namespace fs::data + +} // namespace mozilla::dom diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h new file mode 100644 index 0000000000..333c5af6c2 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h @@ -0,0 +1,208 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION001_H_ +#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION001_H_ + +#include "FileSystemDatabaseManager.h" +#include "mozilla/dom/quota/CommonMetadata.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "nsString.h" + +namespace mozilla::dom::fs { + +struct FileId; + +namespace data { + +class FileSystemDataManager; +class FileSystemFileManager; + +/** + * @brief Versioned implementation of database interface enables backwards + * support after the schema has changed. Version number 0 refers to + * uninitialized database, and versions after that are sequential upgrades. + * + * To change the schema to the next version x, + * - a new implementation FileSystemDatabaseManagerVersion00x is derived from + * the previous version and the required methods are overridden + * - a new apppropriate schema initialization class SchemaVersion00x is created + * or derived + * - the factory method of FileSystemDatabaseManager is extended to try to + * migrate the data from the previous version to version x, and to return + * FileSystemDatabaseManagerVersion00x implementation if the database version + * after the migrations is x + * - note that if the migration fails at some old version, the corresponding + * old implementation should be returned: this way the users whose migrations + * fail systematically due to hardware or other issues will not get locked out + */ +class FileSystemDatabaseManagerVersion001 : public FileSystemDatabaseManager { + public: + FileSystemDatabaseManagerVersion001( + FileSystemDataManager* aDataManager, FileSystemConnection&& aConnection, + UniquePtr<FileSystemFileManager>&& aFileManager, + const EntryId& aRootEntry); + + /* Static to allow use by quota client without instantiation */ + static nsresult RescanTrackedUsages( + const FileSystemConnection& aConnection, + const quota::OriginMetadata& aOriginMetadata); + + /* Static to allow use by quota client without instantiation */ + static Result<Usage, QMResult> GetFileUsage( + const FileSystemConnection& aConnection); + + nsresult UpdateUsage(const FileId& aFileId) override; + + Result<EntryId, QMResult> GetOrCreateDirectory( + const FileSystemChildMetadata& aHandle, bool aCreate) override; + + Result<EntryId, QMResult> GetOrCreateFile( + const FileSystemChildMetadata& aHandle, bool aCreate) override; + + nsresult GetFile(const EntryId& aEntryId, const FileId& aFileId, + const FileMode& aMode, ContentType& aType, + TimeStamp& lastModifiedMilliSeconds, Path& aPath, + nsCOMPtr<nsIFile>& aFile) const override; + + Result<FileSystemDirectoryListing, QMResult> GetDirectoryEntries( + const EntryId& aParent, PageNumber aPage) const override; + + Result<bool, QMResult> RemoveDirectory(const FileSystemChildMetadata& aHandle, + bool aRecursive) override; + + Result<bool, QMResult> RemoveFile( + const FileSystemChildMetadata& aHandle) override; + + Result<EntryId, QMResult> RenameEntry(const FileSystemEntryMetadata& aHandle, + const Name& aNewName) override; + + Result<EntryId, QMResult> MoveEntry( + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation) override; + + Result<Path, QMResult> Resolve( + const FileSystemEntryPair& aEndpoints) const override; + + Result<EntryId, QMResult> GetEntryId( + const FileSystemChildMetadata& aHandle) const override; + + Result<EntryId, QMResult> GetEntryId(const FileId& aFileId) const override; + + Result<FileId, QMResult> EnsureFileId(const EntryId& aEntryId) override; + + Result<FileId, QMResult> EnsureTemporaryFileId( + const EntryId& aEntryId) override; + + Result<FileId, QMResult> GetFileId(const EntryId& aEntryId) const override; + + nsresult MergeFileId(const EntryId& aEntryId, const FileId& aFileId, + bool aAbort) override; + + void Close() override; + + nsresult BeginUsageTracking(const FileId& aFileId) override; + + nsresult EndUsageTracking(const FileId& aFileId) override; + + virtual ~FileSystemDatabaseManagerVersion001() = default; + + protected: + virtual Result<bool, QMResult> DoesFileIdExist(const FileId& aFileId) const; + + virtual nsresult RemoveFileId(const FileId& aFileId); + + virtual Result<Usage, QMResult> GetUsagesOfDescendants( + const EntryId& aEntryId) const; + + virtual Result<nsTArray<FileId>, QMResult> FindFilesUnderEntry( + const EntryId& aEntryId) const; + + nsresult SetUsageTracking(const FileId& aFileId, bool aTracked); + + nsresult UpdateUsageInDatabase(const FileId& aFileId, Usage aNewDiskUsage); + + Result<Ok, QMResult> EnsureUsageIsKnown(const FileId& aFileId); + + void DecreaseCachedQuotaUsage(int64_t aDelta); + + nsresult UpdateCachedQuotaUsage(const FileId& aFileId, Usage aOldUsage, + Usage aNewUsage); + + nsresult ClearDestinationIfNotLocked( + const FileSystemConnection& aConnection, + const FileSystemDataManager* const aDataManager, + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation); + + nsresult PrepareRenameEntry(const FileSystemConnection& aConnection, + const FileSystemDataManager* const aDataManager, + const FileSystemEntryMetadata& aHandle, + const Name& aNewName, bool aIsFile); + + nsresult PrepareMoveEntry(const FileSystemConnection& aConnection, + const FileSystemDataManager* const aDataManager, + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation, + bool aIsFile); + + // This is a raw pointer since we're owned by the FileSystemDataManager. + FileSystemDataManager* MOZ_NON_OWNING_REF mDataManager; + + FileSystemConnection mConnection; + + UniquePtr<FileSystemFileManager> mFileManager; + + const EntryId mRootEntry; + + const quota::ClientMetadata mClientMetadata; + + int32_t mFilesOfUnknownUsage; +}; + +inline auto toNSResult = [](const auto& aRv) { return ToNSResult(aRv); }; + +Result<bool, QMResult> ApplyEntryExistsQuery( + const FileSystemConnection& aConnection, const nsACString& aQuery, + const FileSystemChildMetadata& aHandle); + +Result<bool, QMResult> ApplyEntryExistsQuery( + const FileSystemConnection& aConnection, const nsACString& aQuery, + const EntryId& aEntry); + +Result<bool, QMResult> DoesFileExist(const FileSystemConnection& aConnection, + const EntryId& aEntryId); + +Result<bool, QMResult> IsFile(const FileSystemConnection& aConnection, + const EntryId& aEntryId); + +Result<EntryId, QMResult> FindEntryId(const FileSystemConnection& aConnection, + const FileSystemChildMetadata& aHandle, + bool aIsFile); + +Result<EntryId, QMResult> FindParent(const FileSystemConnection& aConnection, + const EntryId& aEntryId); + +Result<bool, QMResult> IsSame(const FileSystemConnection& aConnection, + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewHandle, + bool aIsFile); + +Result<Path, QMResult> ResolveReversedPath( + const FileSystemConnection& aConnection, + const FileSystemEntryPair& aEndpoints); + +nsresult GetFileAttributes(const FileSystemConnection& aConnection, + const EntryId& aEntryId, ContentType& aType); + +void TryRemoveDuringIdleMaintenance(const nsTArray<FileId>& aItemToRemove); + +ContentType DetermineContentType(const Name& aName); + +} // namespace data +} // namespace mozilla::dom::fs + +#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION001_H_ diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.cpp b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.cpp new file mode 100644 index 0000000000..3543346ff0 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.cpp @@ -0,0 +1,832 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FileSystemDatabaseManagerVersion002.h" + +#include "ErrorList.h" +#include "FileSystemContentTypeGuess.h" +#include "FileSystemDataManager.h" +#include "FileSystemFileManager.h" +#include "FileSystemHashSource.h" +#include "FileSystemHashStorageFunction.h" +#include "FileSystemParentTypes.h" +#include "ResultStatement.h" +#include "StartedTransaction.h" +#include "mozStorageHelper.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/dom/FileSystemDataManager.h" +#include "mozilla/dom/FileSystemHandle.h" +#include "mozilla/dom/FileSystemLog.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/PFileSystemManager.h" +#include "mozilla/dom/QMResult.h" +#include "mozilla/dom/quota/Client.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/QuotaObject.h" +#include "mozilla/dom/quota/ResultExtensions.h" + +namespace mozilla::dom::fs::data { + +namespace { + +Result<FileId, QMResult> GetFileId002(const FileSystemConnection& aConnection, + const EntryId& aEntryId) { + const nsLiteralCString fileIdQuery = + "SELECT fileId FROM MainFiles WHERE handle = :entryId ;"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, fileIdQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEntryId))); + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + + if (!moreResults) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + QM_TRY_INSPECT(const FileId& fileId, stmt.GetFileIdByColumn(/* Column */ 0u)); + + return fileId; +} + +Result<bool, QMResult> DoesFileIdExist(const FileSystemConnection& aConnection, + const FileId& aFileId) { + MOZ_ASSERT(!aFileId.IsEmpty()); + + const nsLiteralCString existsQuery = + "SELECT EXISTS " + "(SELECT 1 FROM FileIds WHERE fileId = :handle ) " + ";"_ns; + + QM_TRY_RETURN( + ApplyEntryExistsQuery(aConnection, existsQuery, aFileId.Value())); +} + +nsresult RehashFile(const FileSystemConnection& aConnection, + const EntryId& aEntryId, + const FileSystemChildMetadata& aNewDesignation, + const ContentType& aNewType) { + QM_TRY_INSPECT(const EntryId& newId, + FileSystemHashSource::GenerateHash( + aNewDesignation.parentId(), aNewDesignation.childName())); + + // The destination should be empty at this point: either we exited because + // overwrite was not desired, or the existing content was removed. + const nsLiteralCString insertNewEntryQuery = + "INSERT INTO Entries ( handle, parent ) " + "VALUES ( :newId, :newParent ) " + ";"_ns; + + const nsLiteralCString insertNewFileAndTypeQuery = + "INSERT INTO Files ( handle, type, name ) " + "VALUES ( :newId, :type, :newName ) " + ";"_ns; + + const nsLiteralCString insertNewFileKeepTypeQuery = + "INSERT INTO Files ( handle, type, name ) " + "SELECT :newId, type, :newName FROM Files " + "WHERE handle = :oldId ;"_ns; + + const auto& insertNewFileQuery = aNewType.IsVoid() + ? insertNewFileKeepTypeQuery + : insertNewFileAndTypeQuery; + + const nsLiteralCString updateFileMappingsQuery = + "UPDATE FileIds SET handle = :newId WHERE handle = :handle ;"_ns; + + const nsLiteralCString updateMainFilesQuery = + "UPDATE MainFiles SET handle = :newId WHERE handle = :handle ;"_ns; + + const nsLiteralCString cleanupOldEntryQuery = + "DELETE FROM Entries WHERE handle = :handle ;"_ns; + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConnection)); + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewEntryQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("newId"_ns, newId))); + QM_TRY(QM_TO_RESULT( + stmt.BindEntryIdByName("newParent"_ns, aNewDesignation.parentId()))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewFileQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("newId"_ns, newId))); + if (aNewType.IsVoid()) { + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("oldId"_ns, aEntryId))); + } else { + QM_TRY(QM_TO_RESULT(stmt.BindContentTypeByName("type"_ns, aNewType))); + } + QM_TRY(QM_TO_RESULT( + stmt.BindNameByName("newName"_ns, aNewDesignation.childName()))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, updateFileMappingsQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("newId"_ns, newId))); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, updateMainFilesQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("newId"_ns, newId))); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, cleanupOldEntryQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + return NS_OK; +} + +nsresult RehashDirectory(const FileSystemConnection& aConnection, + const EntryId& aEntryId, + const FileSystemChildMetadata& aNewDesignation) { + // This name won't match up with the entryId for the old path but + // it will be removed at the end + const nsLiteralCString updateNameQuery = + "UPDATE Directories SET name = :newName WHERE handle = :handle " + "; "_ns; + + const nsLiteralCString calculateHashesQuery = + "CREATE TEMPORARY TABLE ParentChildHash AS " + "WITH RECURSIVE " + "rehashMap( depth, isFile, handle, parent, name, hash ) AS ( " + "SELECT 0, isFile, handle, parent, name, hashEntry( :newParent, name ) " + "FROM EntryNames WHERE handle = :handle UNION SELECT " + "1 + depth, EntryNames.isFile, EntryNames.handle, EntryNames.parent, " + "EntryNames.name, hashEntry( rehashMap.hash, EntryNames.name ) " + "FROM rehashMap, EntryNames WHERE rehashMap.handle = EntryNames.parent ) " + "SELECT depth, isFile, handle, parent, name, hash FROM rehashMap " + ";"_ns; + + const nsLiteralCString createIndexByDepthQuery = + "CREATE INDEX indexOnDepth ON ParentChildHash ( depth ); "_ns; + + // To avoid constraint violation, we insert new entries under the old parent. + // The destination should be empty at this point: either we exited because + // overwrite was not desired, or the existing content was removed. + const nsLiteralCString insertNewEntriesQuery = + "INSERT INTO Entries ( handle, parent ) " + "SELECT hash, :parent FROM ParentChildHash " + ";"_ns; + + const nsLiteralCString insertNewDirectoriesQuery = + "INSERT INTO Directories ( handle, name ) " + "SELECT hash, name FROM ParentChildHash WHERE isFile = 0 " + "ORDER BY depth " + ";"_ns; + + const nsLiteralCString insertNewFilesQuery = + "INSERT INTO Files ( handle, type, name ) " + "SELECT ParentChildHash.hash, Files.type, ParentChildHash.name " + "FROM ParentChildHash INNER JOIN Files USING (handle) " + "WHERE ParentChildHash.isFile = 1 " + ";"_ns; + + const nsLiteralCString updateFileMappingsQuery = + "UPDATE FileIds SET handle = hash " + "FROM ( SELECT handle, hash FROM ParentChildHash ) AS replacement " + "WHERE FileIds.handle = replacement.handle " + ";"_ns; + + const nsLiteralCString updateMainFilesQuery = + "UPDATE MainFiles SET handle = hash " + "FROM ( SELECT handle, hash FROM ParentChildHash ) AS replacement " + "WHERE MainFiles.handle = replacement.handle " + ";"_ns; + + // Now fix the parents + const nsLiteralCString updateEntryMappingsQuery = + "UPDATE Entries SET parent = hash " + "FROM ( SELECT Lhs.hash AS handle, Rhs.hash AS hash, Lhs.depth AS depth " + "FROM ParentChildHash AS Lhs " + "INNER JOIN ParentChildHash AS Rhs " + "ON Rhs.handle = Lhs.parent ORDER BY depth ) AS replacement " + "WHERE Entries.handle = replacement.handle " + ";"_ns; + + const nsLiteralCString cleanupOldEntriesQuery = + "DELETE FROM Entries WHERE handle = :handle " + ";"_ns; + + // Index is automatically deleted + const nsLiteralCString cleanupTemporaries = + "DROP TABLE ParentChildHash " + ";"_ns; + + nsCOMPtr<mozIStorageFunction> rehashFunction = + new data::FileSystemHashStorageFunction(); + QM_TRY(MOZ_TO_RESULT(aConnection->CreateFunction("hashEntry"_ns, + /* number of arguments */ 2, + rehashFunction))); + auto finallyRemoveFunction = MakeScopeExit([&aConnection]() { + QM_WARNONLY_TRY(MOZ_TO_RESULT(aConnection->RemoveFunction("hashEntry"_ns))); + }); + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConnection)); + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, updateNameQuery)); + QM_TRY(QM_TO_RESULT( + stmt.BindNameByName("newName"_ns, aNewDesignation.childName()))); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, calculateHashesQuery)); + QM_TRY(QM_TO_RESULT( + stmt.BindEntryIdByName("newParent"_ns, aNewDesignation.parentId()))); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + QM_TRY(QM_TO_RESULT(aConnection->ExecuteSimpleSQL(createIndexByDepthQuery))); + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewEntriesQuery)); + QM_TRY(QM_TO_RESULT( + stmt.BindEntryIdByName("parent"_ns, aNewDesignation.parentId()))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewDirectoriesQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewFilesQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, updateFileMappingsQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, updateMainFilesQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, updateEntryMappingsQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, cleanupOldEntriesQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, cleanupTemporaries)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + return NS_OK; +} + +/** + * @brief Each entryId is interpreted as a large integer, which is increased + * until an unused value is found. This process is in principle infallible. + * The files associated with a given path will form a cluster next to the + * entryId which could be used for recovery because our hash function is + * expected to distribute all clusters far from each other. + */ +Result<FileId, QMResult> GetNextFreeFileId( + const FileSystemConnection& aConnection, + const FileSystemFileManager& aFileManager, const EntryId& aEntryId) { + MOZ_ASSERT(32u == aEntryId.Length()); + + auto DoesExist = [&aConnection, &aFileManager]( + const FileId& aId) -> Result<bool, QMResult> { + QM_TRY_INSPECT(const nsCOMPtr<nsIFile>& diskFile, + aFileManager.GetFile(aId)); + + bool result = true; + QM_TRY(QM_TO_RESULT(diskFile->Exists(&result))); + if (result) { + return true; + } + + QM_TRY_RETURN(DoesFileIdExist(aConnection, aId)); + }; + + auto Next = [](FileId& aId) { + // Using a larger integer would make fileIds depend on platform endianness. + using IntegerType = uint8_t; + constexpr int32_t bufferSize = 32 / sizeof(IntegerType); + using IdBuffer = std::array<IntegerType, bufferSize>; + + auto Increase = [](IdBuffer& aIn) { + for (int i = 0; i < bufferSize; ++i) { + if (1u + aIn[i] != 0u) { + ++aIn[i]; + return; + } + aIn[i] = 0u; + } + }; + + DebugOnly<nsCString> original = aId.Value(); + Increase(*reinterpret_cast<IdBuffer*>(aId.mValue.BeginWriting())); + MOZ_ASSERT(!aId.Value().Equals(original)); + }; + + FileId id = FileId(aEntryId); + + while (true) { + QM_WARNONLY_TRY_UNWRAP(Maybe<bool> maybeExists, DoesExist(id)); + if (maybeExists.isSome() && !maybeExists.value()) { + return id; + } + + Next(id); + } +} + +Result<FileId, QMResult> AddNewFileId(const FileSystemConnection& aConnection, + const FileSystemFileManager& aFileManager, + const EntryId& aEntryId) { + QM_TRY_INSPECT(const FileId& nextFreeId, + GetNextFreeFileId(aConnection, aFileManager, aEntryId)); + + const nsLiteralCString insertNewFileIdQuery = + "INSERT INTO FileIds ( fileId, handle ) " + "VALUES ( :fileId, :entryId ) " + "; "_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewFileIdQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, nextFreeId))); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEntryId))); + + QM_TRY(QM_TO_RESULT(stmt.Execute())); + + return nextFreeId; +} + +/** + * @brief Get recorded usage or zero if nothing was ever written to the file. + * Removing files is only allowed when there is no lock on the file, and their + * usage is either correctly recorded in the database during unlock, or nothing, + * or they remain in tracked state and the quota manager assumes their usage to + * be equal to the latest recorded value. In all cases, the latest recorded + * value (or nothing) is the correct amount of quota to be released. + */ +Result<Usage, QMResult> GetKnownUsage(const FileSystemConnection& aConnection, + const FileId& aFileId) { + const nsLiteralCString trackedUsageQuery = + "SELECT usage FROM Usages WHERE handle = :handle ;"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, trackedUsageQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("handle"_ns, aFileId))); + + QM_TRY_UNWRAP(const bool moreResults, stmt.ExecuteStep()); + if (!moreResults) { + return 0; + } + + QM_TRY_RETURN(stmt.GetUsageByColumn(/* Column */ 0u)); +} + +} // namespace + +/* static */ +nsresult FileSystemDatabaseManagerVersion002::RescanTrackedUsages( + const FileSystemConnection& aConnection, + const quota::OriginMetadata& aOriginMetadata) { + return FileSystemDatabaseManagerVersion001::RescanTrackedUsages( + aConnection, aOriginMetadata); +} + +/* static */ +Result<Usage, QMResult> FileSystemDatabaseManagerVersion002::GetFileUsage( + const FileSystemConnection& aConnection) { + return FileSystemDatabaseManagerVersion001::GetFileUsage(aConnection); +} + +nsresult FileSystemDatabaseManagerVersion002::GetFile( + const EntryId& aEntryId, const FileId& aFileId, const FileMode& aMode, + ContentType& aType, TimeStamp& lastModifiedMilliSeconds, + nsTArray<Name>& aPath, nsCOMPtr<nsIFile>& aFile) const { + MOZ_ASSERT(!aFileId.IsEmpty()); + + const FileSystemEntryPair endPoints(mRootEntry, aEntryId); + QM_TRY_UNWRAP(aPath, ResolveReversedPath(mConnection, endPoints)); + if (aPath.IsEmpty()) { + return NS_ERROR_DOM_NOT_FOUND_ERR; + } + + QM_TRY(MOZ_TO_RESULT(GetFileAttributes(mConnection, aEntryId, aType))); + + if (aMode == FileMode::SHARED_FROM_COPY) { + QM_WARNONLY_TRY_UNWRAP(Maybe<FileId> mainFileId, GetFileId(aEntryId)); + if (mainFileId) { + QM_TRY_UNWRAP(aFile, + mFileManager->CreateFileFrom(aFileId, mainFileId.value())); + } else { + // LockShared/EnsureTemporaryFileId has provided a brand new fileId. + QM_TRY_UNWRAP(aFile, mFileManager->GetOrCreateFile(aFileId)); + } + } else { + MOZ_ASSERT(aMode == FileMode::EXCLUSIVE || + aMode == FileMode::SHARED_FROM_EMPTY); + + QM_TRY_UNWRAP(aFile, mFileManager->GetOrCreateFile(aFileId)); + } + + PRTime lastModTime = 0; + QM_TRY(MOZ_TO_RESULT(aFile->GetLastModifiedTime(&lastModTime))); + lastModifiedMilliSeconds = static_cast<TimeStamp>(lastModTime); + + aPath.Reverse(); + + return NS_OK; +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion002::RenameEntry( + const FileSystemEntryMetadata& aHandle, const Name& aNewName) { + MOZ_ASSERT(!aNewName.IsEmpty()); + + const auto& entryId = aHandle.entryId(); + MOZ_ASSERT(!entryId.IsEmpty()); + + // Can't rename root + if (mRootEntry == entryId) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + // Verify the source exists + QM_TRY_UNWRAP(bool isFile, IsFile(mConnection, entryId), + Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR))); + + // Are we actually renaming? + if (aHandle.entryName() == aNewName) { + return entryId; + } + + QM_TRY(QM_TO_RESULT(PrepareRenameEntry(mConnection, mDataManager, aHandle, + aNewName, isFile))); + + QM_TRY_UNWRAP(EntryId parentId, FindParent(mConnection, entryId)); + FileSystemChildMetadata newDesignation(parentId, aNewName); + + if (isFile) { + const ContentType type = DetermineContentType(aNewName); + QM_TRY( + QM_TO_RESULT(RehashFile(mConnection, entryId, newDesignation, type))); + } else { + QM_TRY(QM_TO_RESULT(RehashDirectory(mConnection, entryId, newDesignation))); + } + + QM_TRY_UNWRAP(DebugOnly<EntryId> dbId, + FindEntryId(mConnection, newDesignation, isFile)); + QM_TRY_UNWRAP(EntryId generated, + FileSystemHashSource::GenerateHash(parentId, aNewName)); + MOZ_ASSERT(static_cast<EntryId&>(dbId).Equals(generated)); + + return generated; +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion002::MoveEntry( + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation) { + MOZ_ASSERT(!aHandle.entryId().IsEmpty()); + + const auto& entryId = aHandle.entryId(); + + if (mRootEntry == entryId) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + // Verify the source exists + QM_TRY_UNWRAP(bool isFile, IsFile(mConnection, entryId), + Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR))); + + // If the rename doesn't change the name or directory, just return success. + // XXX Needs to be added to the spec + QM_WARNONLY_TRY_UNWRAP(Maybe<bool> maybeSame, + IsSame(mConnection, aHandle, aNewDesignation, isFile)); + if (maybeSame && maybeSame.value()) { + return entryId; + } + + QM_TRY(QM_TO_RESULT(PrepareMoveEntry(mConnection, mDataManager, aHandle, + aNewDesignation, isFile))); + + if (isFile) { + const ContentType type = DetermineContentType(aNewDesignation.childName()); + QM_TRY( + QM_TO_RESULT(RehashFile(mConnection, entryId, aNewDesignation, type))); + } else { + QM_TRY( + QM_TO_RESULT(RehashDirectory(mConnection, entryId, aNewDesignation))); + } + + QM_TRY_UNWRAP(DebugOnly<EntryId> dbId, + FindEntryId(mConnection, aNewDesignation, isFile)); + QM_TRY_UNWRAP(EntryId generated, + FileSystemHashSource::GenerateHash( + aNewDesignation.parentId(), aNewDesignation.childName())); + MOZ_ASSERT(static_cast<EntryId&>(dbId).Equals(generated)); + + return generated; +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion002::GetEntryId( + const FileSystemChildMetadata& aHandle) const { + return fs::data::GetEntryHandle(aHandle); +} + +Result<EntryId, QMResult> FileSystemDatabaseManagerVersion002::GetEntryId( + const FileId& aFileId) const { + const nsLiteralCString getEntryIdQuery = + "SELECT handle FROM FileIds WHERE fileId = :fileId ;"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, getEntryIdQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, aFileId))); + QM_TRY_UNWRAP(bool hasEntries, stmt.ExecuteStep()); + + if (!hasEntries || stmt.IsNullByColumn(/* Column */ 0u)) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + QM_TRY_RETURN(stmt.GetEntryIdByColumn(/* Column */ 0u)); +} + +Result<FileId, QMResult> FileSystemDatabaseManagerVersion002::EnsureFileId( + const EntryId& aEntryId) { + QM_TRY_UNWRAP(bool exists, DoesFileExist(mConnection, aEntryId)); + if (!exists) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + QM_TRY_UNWRAP(Maybe<FileId> maybeMainFileId, + QM_OR_ELSE_LOG_VERBOSE_IF( + // Expression. + GetFileId(aEntryId).map([](auto mainFileId) { + return Some(std::move(mainFileId)); + }), + // Predicate. + IsSpecificError<NS_ERROR_DOM_NOT_FOUND_ERR>, + // Fallback. + ([](const auto&) -> Result<Maybe<FileId>, QMResult> { + return Maybe<FileId>{}; + }))); + + if (maybeMainFileId) { + return *maybeMainFileId; + } + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection)); + + QM_TRY_INSPECT(const FileId& fileId, + AddNewFileId(mConnection, *mFileManager, aEntryId)); + + QM_TRY(QM_TO_RESULT(MergeFileId(aEntryId, fileId, /* aAbort */ false))); + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + return fileId; +} + +Result<FileId, QMResult> +FileSystemDatabaseManagerVersion002::EnsureTemporaryFileId( + const EntryId& aEntryId) { + QM_TRY_UNWRAP(bool exists, DoesFileExist(mConnection, aEntryId)); + if (!exists) { + return Err(QMResult(NS_ERROR_DOM_NOT_FOUND_ERR)); + } + + QM_TRY_RETURN(AddNewFileId(mConnection, *mFileManager, aEntryId)); +} + +Result<FileId, QMResult> FileSystemDatabaseManagerVersion002::GetFileId( + const EntryId& aEntryId) const { + MOZ_ASSERT(mConnection); + return data::GetFileId002(mConnection, aEntryId); +} + +nsresult FileSystemDatabaseManagerVersion002::MergeFileId( + const EntryId& aEntryId, const FileId& aFileId, bool aAbort) { + MOZ_ASSERT(mConnection); + + auto doCleanUp = [this](const FileId& aCleanable) -> nsresult { + // We need to clean up the old main file. + QM_TRY_UNWRAP(Usage usage, + GetKnownUsage(mConnection, aCleanable).mapErr(toNSResult)); + + QM_WARNONLY_TRY_UNWRAP(Maybe<Usage> removedUsage, + mFileManager->RemoveFile(aCleanable)); + + if (removedUsage) { + // Removal of file data was ok, update the related fileId and usage + QM_WARNONLY_TRY(QM_TO_RESULT(RemoveFileId(aCleanable))); + + if (usage > 0) { // Performance! + DecreaseCachedQuotaUsage(usage); + } + + // We only check the most common case. This can fail spuriously if an + // external application writes to the file, or OS reports zero size due to + // corruption. + MOZ_ASSERT_IF(0 == mFilesOfUnknownUsage, usage == removedUsage.value()); + + return NS_OK; + } + + // Removal failed + const nsLiteralCString forgetCleanable = + "UPDATE FileIds SET handle = NULL WHERE fileId = :fileId ; "_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, forgetCleanable) + .mapErr(toNSResult)); + QM_TRY(MOZ_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, aCleanable))); + QM_TRY(MOZ_TO_RESULT(stmt.Execute())); + + TryRemoveDuringIdleMaintenance({aCleanable}); + + return NS_OK; + }; + + if (aAbort) { + QM_TRY(MOZ_TO_RESULT(doCleanUp(aFileId))); + + return NS_OK; + } + + QM_TRY_UNWRAP( + Maybe<FileId> maybeOldFileId, + QM_OR_ELSE_LOG_VERBOSE_IF( + // Expression. + GetFileId(aEntryId) + .map([](auto oldFileId) { return Some(std::move(oldFileId)); }) + .mapErr(toNSResult), + // Predicate. + IsSpecificError<NS_ERROR_DOM_NOT_FOUND_ERR>, + // Fallback. + ErrToDefaultOk<Maybe<FileId>>)); + + if (maybeOldFileId && *maybeOldFileId == aFileId) { + return NS_OK; // Nothing to do + } + + // Main file changed + const nsLiteralCString flagAsMainFileQuery = + "INSERT INTO MainFiles ( handle, fileId ) " + "VALUES ( :entryId, :fileId ) " + "ON CONFLICT (handle) " + "DO UPDATE SET fileId = excluded.fileId " + "; "_ns; + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(mConnection)); + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, flagAsMainFileQuery) + .mapErr(toNSResult)); + QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, aEntryId))); + QM_TRY(MOZ_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, aFileId))); + + QM_TRY(MOZ_TO_RESULT(stmt.Execute())); + + if (!maybeOldFileId) { + // We successfully added a new main file and there is nothing to clean up. + QM_TRY(MOZ_TO_RESULT(transaction.Commit())); + + return NS_OK; + } + + MOZ_ASSERT(maybeOldFileId); + MOZ_ASSERT(*maybeOldFileId != aFileId); + + QM_TRY(MOZ_TO_RESULT(doCleanUp(*maybeOldFileId))); + + // If the old fileId and usage were not deleted, main file update fails. + QM_TRY(MOZ_TO_RESULT(transaction.Commit())); + + return NS_OK; +} + +Result<bool, QMResult> FileSystemDatabaseManagerVersion002::DoesFileIdExist( + const FileId& aFileId) const { + QM_TRY_RETURN(data::DoesFileIdExist(mConnection, aFileId)); +} + +nsresult FileSystemDatabaseManagerVersion002::RemoveFileId( + const FileId& aFileId) { + const nsLiteralCString removeFileIdQuery = + "DELETE FROM FileIds " + "WHERE fileId = :fileId " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, removeFileIdQuery) + .mapErr(toNSResult)); + + QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("fileId"_ns, aFileId.Value()))); + + return stmt.Execute(); +} + +Result<Usage, QMResult> +FileSystemDatabaseManagerVersion002::GetUsagesOfDescendants( + const EntryId& aEntryId) const { + const nsLiteralCString descendantUsagesQuery = + "WITH RECURSIVE traceChildren(handle, parent) AS ( " + "SELECT handle, parent FROM Entries WHERE handle = :handle " + "UNION " + "SELECT Entries.handle, Entries.parent FROM traceChildren, Entries " + "WHERE traceChildren.handle=Entries.parent ) " + "SELECT sum(Usages.usage) " + "FROM traceChildren " + "INNER JOIN FileIds ON traceChildren.handle = FileIds.handle " + "INNER JOIN Usages ON Usages.handle = FileIds.fileId " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, descendantUsagesQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + QM_TRY_UNWRAP(const bool moreResults, stmt.ExecuteStep()); + if (!moreResults) { + return 0; + } + + QM_TRY_RETURN(stmt.GetUsageByColumn(/* Column */ 0u)); +} + +Result<nsTArray<FileId>, QMResult> +FileSystemDatabaseManagerVersion002::FindFilesUnderEntry( + const EntryId& aEntryId) const { + const nsLiteralCString descendantsQuery = + "WITH RECURSIVE traceChildren(handle, parent) AS ( " + "SELECT handle, parent FROM Entries WHERE handle = :handle " + "UNION " + "SELECT Entries.handle, Entries.parent FROM traceChildren, Entries " + "WHERE traceChildren.handle = Entries.parent ) " + "SELECT FileIds.fileId " + "FROM traceChildren INNER JOIN FileIds USING (handle) " + ";"_ns; + + nsTArray<FileId> descendants; + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(mConnection, descendantsQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, aEntryId))); + + while (true) { + QM_TRY_INSPECT(const bool& moreResults, stmt.ExecuteStep()); + if (!moreResults) { + break; + } + + QM_TRY_INSPECT(const FileId& fileId, + stmt.GetFileIdByColumn(/* Column */ 0u)); + descendants.AppendElement(fileId); + } + } + + return descendants; +} + +} // namespace mozilla::dom::fs::data diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.h b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.h new file mode 100644 index 0000000000..6dec629632 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION002_H_ +#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION002_H_ + +#include "FileSystemDatabaseManagerVersion001.h" + +namespace mozilla::dom::fs::data { + +class FileSystemDatabaseManagerVersion002 + : public FileSystemDatabaseManagerVersion001 { + public: + FileSystemDatabaseManagerVersion002( + FileSystemDataManager* aDataManager, FileSystemConnection&& aConnection, + UniquePtr<FileSystemFileManager>&& aFileManager, + const EntryId& aRootEntry) + : FileSystemDatabaseManagerVersion001( + aDataManager, std::move(aConnection), std::move(aFileManager), + aRootEntry) {} + + /* Static to allow use by quota client without instantiation */ + static nsresult RescanTrackedUsages( + const FileSystemConnection& aConnection, + const quota::OriginMetadata& aOriginMetadata); + + /* Static to allow use by quota client without instantiation */ + static Result<Usage, QMResult> GetFileUsage( + const FileSystemConnection& aConnection); + + nsresult GetFile(const EntryId& aEntryId, const FileId& aFileId, + const FileMode& aMode, ContentType& aType, + TimeStamp& lastModifiedMilliSeconds, Path& aPath, + nsCOMPtr<nsIFile>& aFile) const override; + + Result<EntryId, QMResult> RenameEntry(const FileSystemEntryMetadata& aHandle, + const Name& aNewName) override; + + Result<EntryId, QMResult> MoveEntry( + const FileSystemEntryMetadata& aHandle, + const FileSystemChildMetadata& aNewDesignation) override; + + Result<EntryId, QMResult> GetEntryId( + const FileSystemChildMetadata& aHandle) const override; + + Result<EntryId, QMResult> GetEntryId(const FileId& aFileId) const override; + + Result<FileId, QMResult> EnsureFileId(const EntryId& aEntryId) override; + + Result<FileId, QMResult> EnsureTemporaryFileId( + const EntryId& aEntryId) override; + + Result<FileId, QMResult> GetFileId(const EntryId& aEntryId) const override; + + nsresult MergeFileId(const EntryId& aEntryId, const FileId& aFileId, + bool aAbort) override; + + protected: + Result<bool, QMResult> DoesFileIdExist(const FileId& aFileId) const override; + + nsresult RemoveFileId(const FileId& aFileId) override; + + Result<Usage, QMResult> GetUsagesOfDescendants( + const EntryId& aEntryId) const override; + + Result<nsTArray<FileId>, QMResult> FindFilesUnderEntry( + const EntryId& aEntryId) const override; +}; + +} // namespace mozilla::dom::fs::data + +#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMDATABASEMANAGERVERSION002_H_ diff --git a/dom/fs/parent/datamodel/FileSystemFileManager.cpp b/dom/fs/parent/datamodel/FileSystemFileManager.cpp new file mode 100644 index 0000000000..02a69467dc --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemFileManager.cpp @@ -0,0 +1,393 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FileSystemFileManager.h" + +#include "FileSystemDataManager.h" +#include "FileSystemHashSource.h" +#include "FileSystemParentTypes.h" +#include "mozilla/Assertions.h" +#include "mozilla/NotNull.h" +#include "mozilla/Result.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "nsCOMPtr.h" +#include "nsHashKeys.h" +#include "nsIFile.h" +#include "nsIFileProtocolHandler.h" +#include "nsIFileURL.h" +#include "nsIURIMutator.h" +#include "nsTHashMap.h" +#include "nsXPCOM.h" + +namespace mozilla::dom::fs::data { + +namespace { + +constexpr nsLiteralString kDatabaseFileName = u"metadata.sqlite"_ns; + +Result<nsCOMPtr<nsIFile>, QMResult> GetFileDestination( + const nsCOMPtr<nsIFile>& aTopDirectory, const FileId& aFileId) { + MOZ_ASSERT(32u == aFileId.Value().Length()); + + nsCOMPtr<nsIFile> destination; + + // nsIFile Clone is not a constant method + QM_TRY(QM_TO_RESULT(aTopDirectory->Clone(getter_AddRefs(destination)))); + + QM_TRY_UNWRAP(Name encoded, FileSystemHashSource::EncodeHash(aFileId)); + + MOZ_ALWAYS_TRUE(IsAscii(encoded)); + + nsString relativePath; + relativePath.Append(Substring(encoded, 0, 2)); + + QM_TRY(QM_TO_RESULT(destination->AppendRelativePath(relativePath))); + + QM_TRY(QM_TO_RESULT(destination->AppendRelativePath(encoded))); + + return destination; +} + +Result<nsCOMPtr<nsIFile>, QMResult> GetOrCreateFileImpl( + const nsAString& aFilePath) { + MOZ_ASSERT(!aFilePath.IsEmpty()); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> result, + QM_TO_RESULT_TRANSFORM(quota::QM_NewLocalFile(aFilePath))); + + bool exists = true; + QM_TRY(QM_TO_RESULT(result->Exists(&exists))); + + if (!exists) { + QM_TRY(QM_TO_RESULT(result->Create(nsIFile::NORMAL_FILE_TYPE, 0644))); + + return result; + } + + bool isDirectory = true; + QM_TRY(QM_TO_RESULT(result->IsDirectory(&isDirectory))); + QM_TRY(OkIf(!isDirectory), Err(QMResult(NS_ERROR_FILE_IS_DIRECTORY))); + + return result; +} + +Result<nsCOMPtr<nsIFile>, QMResult> GetFile( + const nsCOMPtr<nsIFile>& aTopDirectory, const FileId& aFileId) { + MOZ_ASSERT(!aFileId.IsEmpty()); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject, + GetFileDestination(aTopDirectory, aFileId)); + + nsString desiredPath; + QM_TRY(QM_TO_RESULT(pathObject->GetPath(desiredPath))); + + QM_TRY_RETURN(QM_TO_RESULT_TRANSFORM(quota::QM_NewLocalFile(desiredPath))); +} + +Result<nsCOMPtr<nsIFile>, QMResult> GetOrCreateFile( + const nsCOMPtr<nsIFile>& aTopDirectory, const FileId& aFileId) { + MOZ_ASSERT(!aFileId.IsEmpty()); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject, + GetFileDestination(aTopDirectory, aFileId)); + + nsString desiredPath; + QM_TRY(QM_TO_RESULT(pathObject->GetPath(desiredPath))); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> result, GetOrCreateFileImpl(desiredPath)); + + return result; +} + +nsresult RemoveFileObject(const nsCOMPtr<nsIFile>& aFilePtr) { + // If we cannot tell whether the object is file or directory, or it is a + // directory, it is abandoned as an unknown object. If an attempt is made to + // create a new object with the same path on disk, we regenerate the FileId + // until the collision is resolved. + + bool isFile = false; + QM_TRY(MOZ_TO_RESULT(aFilePtr->IsFile(&isFile))); + + QM_TRY(OkIf(isFile), NS_ERROR_FILE_IS_DIRECTORY); + + QM_TRY(QM_TO_RESULT(aFilePtr->Remove(/* recursive */ false))); + + return NS_OK; +} + +#ifdef DEBUG +// Unused in release builds +Result<Usage, QMResult> GetFileSize(const nsCOMPtr<nsIFile>& aFileObject) { + bool exists = false; + QM_TRY(QM_TO_RESULT(aFileObject->Exists(&exists))); + + if (!exists) { + return 0; + } + + bool isFile = false; + QM_TRY(QM_TO_RESULT(aFileObject->IsFile(&isFile))); + + // We never create directories with this path: this is an unknown object + // and the file does not exist + QM_TRY(OkIf(isFile), 0); + + QM_TRY_UNWRAP(Usage fileSize, + QM_TO_RESULT_INVOKE_MEMBER(aFileObject, GetFileSize)); + + return fileSize; +} +#endif + +} // namespace + +Result<nsCOMPtr<nsIFile>, QMResult> GetFileSystemDirectory( + const quota::OriginMetadata& aOriginMetadata) { + MOZ_ASSERT(aOriginMetadata.mPersistenceType == + quota::PERSISTENCE_TYPE_DEFAULT); + + quota::QuotaManager* quotaManager = quota::QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> fileSystemDirectory, + QM_TO_RESULT_TRANSFORM( + quotaManager->GetOriginDirectory(aOriginMetadata))); + + QM_TRY(QM_TO_RESULT(fileSystemDirectory->AppendRelativePath( + NS_LITERAL_STRING_FROM_CSTRING(FILESYSTEM_DIRECTORY_NAME)))); + + return fileSystemDirectory; +} + +nsresult EnsureFileSystemDirectory( + const quota::OriginMetadata& aOriginMetadata) { + quota::QuotaManager* quotaManager = quota::QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + QM_TRY(MOZ_TO_RESULT( + quotaManager->EnsureTemporaryStorageIsInitializedInternal())); + + QM_TRY_INSPECT(const auto& fileSystemDirectory, + quotaManager + ->EnsureTemporaryOriginIsInitialized( + quota::PERSISTENCE_TYPE_DEFAULT, aOriginMetadata) + .map([](const auto& aPair) { return aPair.first; })); + + QM_TRY(QM_TO_RESULT(fileSystemDirectory->AppendRelativePath( + NS_LITERAL_STRING_FROM_CSTRING(FILESYSTEM_DIRECTORY_NAME)))); + + bool exists = true; + QM_TRY(QM_TO_RESULT(fileSystemDirectory->Exists(&exists))); + + if (!exists) { + QM_TRY(QM_TO_RESULT( + fileSystemDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755))); + + return NS_OK; + } + + bool isDirectory = true; + QM_TRY(QM_TO_RESULT(fileSystemDirectory->IsDirectory(&isDirectory))); + QM_TRY(OkIf(isDirectory), NS_ERROR_FILE_NOT_DIRECTORY); + + return NS_OK; +} + +Result<nsCOMPtr<nsIFile>, QMResult> GetDatabaseFile( + const quota::OriginMetadata& aOriginMetadata) { + MOZ_ASSERT(!aOriginMetadata.mOrigin.IsEmpty()); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> databaseFile, + GetFileSystemDirectory(aOriginMetadata)); + + QM_TRY(QM_TO_RESULT(databaseFile->AppendRelativePath(kDatabaseFileName))); + + return databaseFile; +} + +/** + * TODO: This is almost identical to the corresponding function of IndexedDB + */ +Result<nsCOMPtr<nsIFileURL>, QMResult> GetDatabaseFileURL( + const quota::OriginMetadata& aOriginMetadata, + const int64_t aDirectoryLockId) { + MOZ_ASSERT(aDirectoryLockId >= -1); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> databaseFile, + GetDatabaseFile(aOriginMetadata)); + + QM_TRY_INSPECT( + const auto& protocolHandler, + QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_GET_TYPED( + nsCOMPtr<nsIProtocolHandler>, MOZ_SELECT_OVERLOAD(do_GetService), + NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "file"))); + + QM_TRY_INSPECT(const auto& fileHandler, + QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_GET_TYPED( + nsCOMPtr<nsIFileProtocolHandler>, + MOZ_SELECT_OVERLOAD(do_QueryInterface), protocolHandler))); + + QM_TRY_INSPECT(const auto& mutator, + QM_TO_RESULT_TRANSFORM(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED( + nsCOMPtr<nsIURIMutator>, fileHandler, NewFileURIMutator, + databaseFile))); + + // aDirectoryLockId should only be -1 when we are called from + // FileSystemQuotaClient::InitOrigin when the temporary storage hasn't been + // initialized yet. At that time, the in-memory objects (e.g. OriginInfo) are + // only being created so it doesn't make sense to tunnel quota information to + // QuotaVFS to get corresponding QuotaObject instances for SQLite files. + const nsCString directoryLockIdClause = + "&directoryLockId="_ns + IntToCString(aDirectoryLockId); + + nsCOMPtr<nsIFileURL> result; + QM_TRY(QM_TO_RESULT(NS_MutateURI(mutator) + .SetQuery("cache=private"_ns + directoryLockIdClause) + .Finalize(result))); + + return result; +} + +/* static */ +Result<FileSystemFileManager, QMResult> +FileSystemFileManager::CreateFileSystemFileManager( + nsCOMPtr<nsIFile>&& topDirectory) { + return FileSystemFileManager(std::move(topDirectory)); +} + +/* static */ +Result<UniquePtr<FileSystemFileManager>, QMResult> +FileSystemFileManager::CreateFileSystemFileManager( + const quota::OriginMetadata& aOriginMetadata) { + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> topDirectory, + GetFileSystemDirectory(aOriginMetadata)); + + return MakeUnique<FileSystemFileManager>( + FileSystemFileManager(std::move(topDirectory))); +} + +FileSystemFileManager::FileSystemFileManager(nsCOMPtr<nsIFile>&& aTopDirectory) + : mTopDirectory(std::move(aTopDirectory)) {} + +Result<nsCOMPtr<nsIFile>, QMResult> FileSystemFileManager::GetFile( + const FileId& aFileId) const { + return data::GetFile(mTopDirectory, aFileId); +} + +Result<nsCOMPtr<nsIFile>, QMResult> FileSystemFileManager::GetOrCreateFile( + const FileId& aFileId) { + return data::GetOrCreateFile(mTopDirectory, aFileId); +} + +Result<nsCOMPtr<nsIFile>, QMResult> FileSystemFileManager::CreateFileFrom( + const FileId& aDestinationFileId, const FileId& aSourceFileId) { + MOZ_ASSERT(!aDestinationFileId.IsEmpty()); + MOZ_ASSERT(!aSourceFileId.IsEmpty()); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> original, GetFile(aSourceFileId)); + + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> destination, + GetFileDestination(mTopDirectory, aDestinationFileId)); + + nsAutoString leafName; + QM_TRY(QM_TO_RESULT(destination->GetLeafName(leafName))); + + nsCOMPtr<nsIFile> destParent; + QM_TRY(QM_TO_RESULT(destination->GetParent(getter_AddRefs(destParent)))); + + QM_TRY(QM_TO_RESULT(original->CopyTo(destParent, leafName))); + +#ifdef DEBUG + bool exists = false; + QM_TRY(QM_TO_RESULT(destination->Exists(&exists))); + MOZ_ASSERT(exists); + + int64_t destSize = 0; + QM_TRY(QM_TO_RESULT(destination->GetFileSize(&destSize))); + + int64_t origSize = 0; + QM_TRY(QM_TO_RESULT(original->GetFileSize(&origSize))); + + MOZ_ASSERT(destSize == origSize); +#endif + + return destination; +} + +Result<Usage, QMResult> FileSystemFileManager::RemoveFile( + const FileId& aFileId) { + MOZ_ASSERT(!aFileId.IsEmpty()); + QM_TRY_UNWRAP(nsCOMPtr<nsIFile> pathObject, + GetFileDestination(mTopDirectory, aFileId)); + + bool exists = false; + QM_TRY(QM_TO_RESULT(pathObject->Exists(&exists))); + + if (!exists) { + return 0; + } + + bool isFile = false; + QM_TRY(QM_TO_RESULT(pathObject->IsFile(&isFile))); + + // We could handle this also as a nonexistent file. + if (!isFile) { + return Err(QMResult(NS_ERROR_FILE_IS_DIRECTORY)); + } + + Usage totalUsage = 0; +#ifdef DEBUG + QM_TRY_UNWRAP(totalUsage, + QM_TO_RESULT_INVOKE_MEMBER(pathObject, GetFileSize)); +#endif + + QM_TRY(QM_TO_RESULT(pathObject->Remove(/* recursive */ false))); + + return totalUsage; +} + +Result<DebugOnly<Usage>, QMResult> FileSystemFileManager::RemoveFiles( + const nsTArray<FileId>& aFileIds, nsTArray<FileId>& aFailedRemovals) { + if (aFileIds.IsEmpty()) { + return DebugOnly<Usage>(0); + } + + CheckedInt64 totalUsage = 0; + for (const auto& someId : aFileIds) { + QM_WARNONLY_TRY_UNWRAP(Maybe<nsCOMPtr<nsIFile>> maybeFile, + GetFileDestination(mTopDirectory, someId)); + if (!maybeFile) { + aFailedRemovals.AppendElement(someId); + continue; + } + nsCOMPtr<nsIFile> fileObject = maybeFile.value(); + +// Size recorded at close is checked to be equal to the sum of sizes on disk +#ifdef DEBUG + QM_WARNONLY_TRY_UNWRAP(Maybe<Usage> fileSize, GetFileSize(fileObject)); + if (!fileSize) { + aFailedRemovals.AppendElement(someId); + continue; + } + totalUsage += fileSize.value(); +#endif + + QM_WARNONLY_TRY_UNWRAP(Maybe<Ok> ok, + MOZ_TO_RESULT(RemoveFileObject(fileObject))); + if (!ok) { + aFailedRemovals.AppendElement(someId); + } + } + + MOZ_ASSERT(totalUsage.isValid()); + + return DebugOnly<Usage>(totalUsage.value()); +} + +} // namespace mozilla::dom::fs::data diff --git a/dom/fs/parent/datamodel/FileSystemFileManager.h b/dom/fs/parent/datamodel/FileSystemFileManager.h new file mode 100644 index 0000000000..46391db6f7 --- /dev/null +++ b/dom/fs/parent/datamodel/FileSystemFileManager.h @@ -0,0 +1,175 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_FS_PARENT_DATAMODEL_FILESYSTEMFILEMANAGER_H_ +#define DOM_FS_PARENT_DATAMODEL_FILESYSTEMFILEMANAGER_H_ + +#include "ErrorList.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/QMResult.h" +#include "nsIFile.h" +#include "nsString.h" + +template <class T> +class nsCOMPtr; + +class nsIFileURL; + +namespace mozilla::dom { + +namespace quota { + +struct OriginMetadata; + +} // namespace quota + +namespace fs { + +struct FileId; + +namespace data { + +/** + * @brief Get the directory for file system items of specified origin. + * Use this instead of constructing the path from quota manager's storage path. + * + * @param aOrigin Specified origin + * @return Result<nsCOMPtr<nsIFile>, QMResult> Top file system directory + */ +Result<nsCOMPtr<nsIFile>, QMResult> GetFileSystemDirectory( + const quota::OriginMetadata& aOriginMetadata); + +/** + * @brief Ensure that the origin-specific directory for file system exists. + * + * @param aOriginMetadata Specified origin metadata + * @return nsresult Error if operation failed + */ +nsresult EnsureFileSystemDirectory( + const quota::OriginMetadata& aOriginMetadata); + +/** + * @brief Get file system's database path for specified origin. + * Use this to get the database path instead of constructing it from + * quota manager's storage path - without the side effect of potentially + * creating it. + * + * @param aOrigin Specified origin + * @return Result<nsCOMPtr<nsIFile>, QMResult> Database file object + */ +Result<nsCOMPtr<nsIFile>, QMResult> GetDatabaseFile( + const quota::OriginMetadata& aOriginMetadata); + +/** + * @brief Get file system's database url with directory lock parameter for + * specified origin. Use this to open a database connection and have the quota + * manager guard against its deletion or busy errors due to other connections. + * + * @param aOrigin Specified origin + * @param aDirectoryLockId Directory lock id from the quota manager + * @return Result<nsCOMPtr<nsIFileURL>, QMResult> Database file URL object + */ +Result<nsCOMPtr<nsIFileURL>, QMResult> GetDatabaseFileURL( + const quota::OriginMetadata& aOriginMetadata, + const int64_t aDirectoryLockId); + +/** + * @brief Creates and removes disk-backed representations of the file systems' + * file entries for a specified origin. + * + * Other components should not depend on how the files are organized on disk + * but instead rely on the entry id and have access to the local file using the + * GetOrCreateFile result. + * + * The local paths are not necessarily stable in the long term and if they + * absolutely must be cached, there should be a way to repopulate the cache + * after an internal reorganization of the file entry represenations on disk, + * for some currently unforeseen maintenance reason. + * + * Example: if GetOrCreateFile used to map entryId 'abc' to path '/c/u/1/123' + * and now it maps it to '/d/u/1/12/123', the cache should either update all + * paths at once through a migration, or purge them and save a new value + * whenever a call to GetOrCreateFile is made. + */ +class FileSystemFileManager { + public: + /** + * @brief Create a File System File Manager object for a specified origin. + * + * @param aOrigin + * @return Result<FileSystemFileManager, QMResult> + */ + static Result<UniquePtr<FileSystemFileManager>, QMResult> + CreateFileSystemFileManager(const quota::OriginMetadata& aOriginMetadata); + + /** + * @brief Create a File System File Manager object which keeps file entries + * under a specified directory instead of quota manager's storage path. + * This should only be used for testing and preferably removed. + * + * @param topDirectory + * @return Result<FileSystemFileManager, QMResult> + */ + static Result<FileSystemFileManager, QMResult> CreateFileSystemFileManager( + nsCOMPtr<nsIFile>&& topDirectory); + + /** + * @brief Get a file object for a specified entry id. If a file for the entry + * does not exist, returns an appropriate error. + * + * @param aEntryId Specified id of a file system entry + * @return Result<nsCOMPtr<nsIFile>, QMResult> File or error. + */ + Result<nsCOMPtr<nsIFile>, QMResult> GetFile(const FileId& aFileId) const; + + /** + * @brief Get or create a disk-backed file object for a specified entry id. + * + * @param aFileId Specified id of a file system entry + * @return Result<nsCOMPtr<nsIFile>, QMResult> File abstraction or IO error + */ + Result<nsCOMPtr<nsIFile>, QMResult> GetOrCreateFile(const FileId& aFileId); + + /** + * @brief Create a disk-backed file object as a copy. + * + * @param aDestinationFileId Specified id of file to be created + * @param aSourceFileId Specified id of the file from which we make a copy + * @return Result<nsCOMPtr<nsIFile>, QMResult> File abstraction or IO error + */ + Result<nsCOMPtr<nsIFile>, QMResult> CreateFileFrom( + const FileId& aDestinationFileId, const FileId& aSourceFileId); + + /** + * @brief Remove the disk-backed file object for a specified entry id. + * Note: The returned value is 0 in release builds. + * + * @param aFileId Specified id of a file system entry + * @return Result<Usage, QMResult> Error or file size + */ + Result<Usage, QMResult> RemoveFile(const FileId& aFileId); + + /** + * @brief This method can be used to try to delete a group of files from the + * disk. In debug builds, the sum of the usages is provided ad return value, + * in release builds the sum is not calculated. + * The method attempts to remove all the files requested. + */ + Result<DebugOnly<Usage>, QMResult> RemoveFiles( + const nsTArray<FileId>& aFileIds, nsTArray<FileId>& aFailedRemovals); + + private: + explicit FileSystemFileManager(nsCOMPtr<nsIFile>&& aTopDirectory); + + nsCOMPtr<nsIFile> mTopDirectory; +}; + +} // namespace data +} // namespace fs +} // namespace mozilla::dom + +#endif // DOM_FS_PARENT_DATAMODEL_FILESYSTEMFILEMANAGER_H_ diff --git a/dom/fs/parent/datamodel/SchemaVersion001.cpp b/dom/fs/parent/datamodel/SchemaVersion001.cpp new file mode 100644 index 0000000000..cf6eee72cc --- /dev/null +++ b/dom/fs/parent/datamodel/SchemaVersion001.cpp @@ -0,0 +1,193 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SchemaVersion001.h" + +#include "FileSystemHashSource.h" +#include "ResultStatement.h" +#include "StartedTransaction.h" +#include "fs/FileSystemConstants.h" +#include "mozStorageHelper.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" + +namespace mozilla::dom::fs { + +namespace { + +nsresult CreateEntries(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "CREATE TABLE IF NOT EXISTS Entries ( " + "handle BLOB PRIMARY KEY, " // Generated from parent + name, unique + "parent BLOB, " // Not null due to constraint + "CONSTRAINT parent_is_a_directory " + "FOREIGN KEY (parent) " + "REFERENCES Directories (handle) " + "ON DELETE CASCADE ) " + ";"_ns); +} + +nsresult CreateDirectories(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "CREATE TABLE IF NOT EXISTS Directories ( " + "handle BLOB PRIMARY KEY, " + "name BLOB NOT NULL, " + "CONSTRAINT directories_are_entries " + "FOREIGN KEY (handle) " + "REFERENCES Entries (handle) " + "ON DELETE CASCADE ) " + ";"_ns); +} + +nsresult CreateFiles(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "CREATE TABLE IF NOT EXISTS Files ( " + "handle BLOB PRIMARY KEY, " + "type TEXT, " + "name BLOB NOT NULL, " + "CONSTRAINT files_are_entries " + "FOREIGN KEY (handle) " + "REFERENCES Entries (handle) " + "ON DELETE CASCADE ) " + ";"_ns); +} + +nsresult CreateUsages(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "CREATE TABLE IF NOT EXISTS Usages ( " + "handle BLOB PRIMARY KEY, " + "usage INTEGER NOT NULL DEFAULT 0, " + "tracked BOOLEAN NOT NULL DEFAULT 0 CHECK (tracked IN (0, 1)), " + "CONSTRAINT handles_are_files " + "FOREIGN KEY (handle) " + "REFERENCES Files (handle) " + "ON DELETE CASCADE ) " + ";"_ns); +} + +class KeepForeignKeysOffUntilScopeExit final { + public: + explicit KeepForeignKeysOffUntilScopeExit(const ResultConnection& aConn) + : mConn(aConn) {} + + static Result<KeepForeignKeysOffUntilScopeExit, QMResult> Create( + const ResultConnection& aConn) { + QM_TRY( + QM_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_keys = OFF;"_ns))); + KeepForeignKeysOffUntilScopeExit result(aConn); + return result; + } + + ~KeepForeignKeysOffUntilScopeExit() { + auto maskResult = [this]() -> Result<Ok, nsresult> { + QM_TRY(MOZ_TO_RESULT( + mConn->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns))); + + return Ok{}; + }; + QM_WARNONLY_TRY(maskResult()); + } + + private: + ResultConnection mConn; +}; + +nsresult CreateRootEntry(ResultConnection& aConn, const Origin& aOrigin) { + KeepForeignKeysOffUntilScopeExit foreignKeysGuard(aConn); + + const nsLiteralCString createRootQuery = + "INSERT OR IGNORE INTO Entries " + "( handle, parent ) " + "VALUES ( :handle, NULL );"_ns; + + const nsLiteralCString flagRootAsDirectoryQuery = + "INSERT OR IGNORE INTO Directories " + "( handle, name ) " + "VALUES ( :handle, :name );"_ns; + + QM_TRY_UNWRAP(EntryId rootId, + data::FileSystemHashSource::GenerateHash(aOrigin, kRootString)); + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConn)); + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, createRootQuery)); + QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, rootId))); + QM_TRY(MOZ_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, flagRootAsDirectoryQuery)); + QM_TRY(MOZ_TO_RESULT(stmt.BindEntryIdByName("handle"_ns, rootId))); + QM_TRY(MOZ_TO_RESULT(stmt.BindNameByName("name"_ns, kRootString))); + QM_TRY(MOZ_TO_RESULT(stmt.Execute())); + } + + return transaction.Commit(); +} + +} // namespace + +nsresult SetEncoding(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL(R"(PRAGMA encoding = "UTF-16";)"_ns); +} + +Result<bool, QMResult> CheckIfEmpty(ResultConnection& aConn) { + const nsLiteralCString areThereTablesQuery = + "SELECT EXISTS (" + "SELECT 1 FROM sqlite_master " + ");"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, areThereTablesQuery)); + + QM_TRY_UNWRAP(bool tablesExist, stmt.YesOrNoQuery()); + + return !tablesExist; +}; + +nsresult SchemaVersion001::CreateTables(ResultConnection& aConn, + const Origin& aOrigin) { + QM_TRY(MOZ_TO_RESULT(CreateEntries(aConn))); + QM_TRY(MOZ_TO_RESULT(CreateDirectories(aConn))); + QM_TRY(MOZ_TO_RESULT(CreateFiles(aConn))); + QM_TRY(MOZ_TO_RESULT(CreateUsages(aConn))); + QM_TRY(MOZ_TO_RESULT(CreateRootEntry(aConn, aOrigin))); + + return NS_OK; +} + +Result<DatabaseVersion, QMResult> SchemaVersion001::InitializeConnection( + ResultConnection& aConn, const Origin& aOrigin) { + QM_TRY_UNWRAP(bool isEmpty, CheckIfEmpty(aConn)); + + DatabaseVersion currentVersion = 0; + + if (isEmpty) { + QM_TRY(QM_TO_RESULT(SetEncoding(aConn))); + } else { + QM_TRY(QM_TO_RESULT(aConn->GetSchemaVersion(¤tVersion))); + } + + if (currentVersion < sVersion) { + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConn)); + + QM_TRY(QM_TO_RESULT(SchemaVersion001::CreateTables(aConn, aOrigin))); + QM_TRY(QM_TO_RESULT(aConn->SetSchemaVersion(sVersion))); + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + } + + QM_TRY(QM_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns))); + + QM_TRY(QM_TO_RESULT(aConn->GetSchemaVersion(¤tVersion))); + + return currentVersion; +} + +} // namespace mozilla::dom::fs diff --git a/dom/fs/parent/datamodel/SchemaVersion001.h b/dom/fs/parent/datamodel/SchemaVersion001.h new file mode 100644 index 0000000000..72d38e4dfd --- /dev/null +++ b/dom/fs/parent/datamodel/SchemaVersion001.h @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION001_H_ +#define DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION001_H_ + +#include "ResultConnection.h" +#include "mozilla/dom/FileSystemTypes.h" +#include "mozilla/dom/quota/ForwardDecls.h" + +namespace mozilla::dom::fs { + +struct SchemaVersion001 { + static nsresult CreateTables(ResultConnection& aConn, const Origin& aOrigin); + + static Result<DatabaseVersion, QMResult> InitializeConnection( + ResultConnection& aConn, const Origin& aOrigin); + + static constexpr DatabaseVersion sVersion = 1; +}; + +nsresult SetEncoding(ResultConnection& aConn); + +Result<bool, QMResult> CheckIfEmpty(ResultConnection& aConn); + +} // namespace mozilla::dom::fs + +#endif // DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION001_H_ diff --git a/dom/fs/parent/datamodel/SchemaVersion002.cpp b/dom/fs/parent/datamodel/SchemaVersion002.cpp new file mode 100644 index 0000000000..57d4736b88 --- /dev/null +++ b/dom/fs/parent/datamodel/SchemaVersion002.cpp @@ -0,0 +1,618 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SchemaVersion002.h" + +#include "FileSystemFileManager.h" +#include "FileSystemHashSource.h" +#include "FileSystemHashStorageFunction.h" +#include "ResultStatement.h" +#include "StartedTransaction.h" +#include "fs/FileSystemConstants.h" +#include "mozStorageHelper.h" +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "nsID.h" + +namespace mozilla::dom::fs { + +namespace { + +nsresult CreateFileIds(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "CREATE TABLE IF NOT EXISTS FileIds ( " + "fileId BLOB PRIMARY KEY, " + "handle BLOB, " + "FOREIGN KEY (handle) " + "REFERENCES Files (handle) " + "ON DELETE SET NULL ) " + ";"_ns); +} + +nsresult CreateMainFiles(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "CREATE TABLE IF NOT EXISTS MainFiles ( " + "handle BLOB UNIQUE, " + "fileId BLOB UNIQUE, " + "FOREIGN KEY (handle) REFERENCES Files (handle) " + "ON DELETE CASCADE, " + "FOREIGN KEY (fileId) REFERENCES FileIds (fileId) " + "ON DELETE SET NULL ) " + ";"_ns); +} + +nsresult PopulateFileIds(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "INSERT OR IGNORE INTO FileIds ( fileId, handle ) " + "SELECT handle, handle FROM Files " + ";"_ns); +} + +nsresult PopulateMainFiles(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "INSERT OR IGNORE INTO MainFiles ( fileId, handle ) " + "SELECT handle, handle FROM Files " + ";"_ns); +} + +Result<Ok, QMResult> ClearInvalidFileIds( + ResultConnection& aConn, data::FileSystemFileManager& aFileManager) { + // We cant't just clear all file ids because if a file was accessed using + // writable file stream a new file id was created which is not the same as + // entry id. + + // Get all file ids first. + QM_TRY_INSPECT( + const auto& allFileIds, + ([&aConn]() -> Result<nsTArray<FileId>, QMResult> { + const nsLiteralCString allFileIdsQuery = + "SELECT fileId FROM FileIds;"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, allFileIdsQuery)); + + nsTArray<FileId> fileIds; + + while (true) { + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + if (!moreResults) { + break; + } + + QM_TRY_UNWRAP(FileId fileId, stmt.GetFileIdByColumn(/* Column */ 0u)); + + fileIds.AppendElement(fileId); + } + + return std::move(fileIds); + }())); + + // Filter out file ids which have non-zero-sized files on disk. + QM_TRY_INSPECT(const auto& invalidFileIds, + ([&aFileManager](const nsTArray<FileId>& aFileIds) + -> Result<nsTArray<FileId>, QMResult> { + nsTArray<FileId> fileIds; + + for (const auto& fileId : aFileIds) { + QM_TRY_UNWRAP(auto file, aFileManager.GetFile(fileId)); + + QM_TRY_INSPECT(const bool& exists, + QM_TO_RESULT_INVOKE_MEMBER(file, Exists)); + + if (exists) { + QM_TRY_INSPECT( + const int64_t& fileSize, + QM_TO_RESULT_INVOKE_MEMBER(file, GetFileSize)); + + if (fileSize != 0) { + continue; + } + + QM_TRY(QM_TO_RESULT(file->Remove(false))); + } + + fileIds.AppendElement(fileId); + } + + return std::move(fileIds); + }(allFileIds))); + + // Finally, clear invalid file ids. + QM_TRY(([&aConn](const nsTArray<FileId>& aFileIds) -> Result<Ok, QMResult> { + for (const auto& fileId : aFileIds) { + const nsLiteralCString clearFileIdsQuery = + "DELETE FROM FileIds " + "WHERE fileId = :fileId " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, clearFileIdsQuery)); + + QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, fileId))); + + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + return Ok{}; + }(invalidFileIds))); + + return Ok{}; +} + +Result<Ok, QMResult> ClearInvalidMainFiles( + ResultConnection& aConn, data::FileSystemFileManager& aFileManager) { + // We cant't just clear all main files because if a file was accessed using + // writable file stream a new main file was created which is not the same as + // entry id. + + // Get all main files first. + QM_TRY_INSPECT( + const auto& allMainFiles, + ([&aConn]() -> Result<nsTArray<std::pair<EntryId, FileId>>, QMResult> { + const nsLiteralCString allMainFilesQuery = + "SELECT handle, fileId FROM MainFiles;"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, allMainFilesQuery)); + + nsTArray<std::pair<EntryId, FileId>> mainFiles; + + while (true) { + QM_TRY_UNWRAP(bool moreResults, stmt.ExecuteStep()); + if (!moreResults) { + break; + } + + QM_TRY_UNWRAP(EntryId entryId, + stmt.GetEntryIdByColumn(/* Column */ 0u)); + QM_TRY_UNWRAP(FileId fileId, stmt.GetFileIdByColumn(/* Column */ 1u)); + + mainFiles.AppendElement(std::pair<EntryId, FileId>(entryId, fileId)); + } + + return std::move(mainFiles); + }())); + + // Filter out main files which have non-zero-sized files on disk. + QM_TRY_INSPECT( + const auto& invalidMainFiles, + ([&aFileManager](const nsTArray<std::pair<EntryId, FileId>>& aMainFiles) + -> Result<nsTArray<std::pair<EntryId, FileId>>, QMResult> { + nsTArray<std::pair<EntryId, FileId>> mainFiles; + + for (const auto& mainFile : aMainFiles) { + QM_TRY_UNWRAP(auto file, aFileManager.GetFile(mainFile.second)); + + QM_TRY_INSPECT(const bool& exists, + QM_TO_RESULT_INVOKE_MEMBER(file, Exists)); + + if (exists) { + QM_TRY_INSPECT(const int64_t& fileSize, + QM_TO_RESULT_INVOKE_MEMBER(file, GetFileSize)); + + if (fileSize != 0) { + continue; + } + + QM_TRY(QM_TO_RESULT(file->Remove(false))); + } + + mainFiles.AppendElement(mainFile); + } + + return std::move(mainFiles); + }(allMainFiles))); + + // Finally, clear invalid main files. + QM_TRY(([&aConn](const nsTArray<std::pair<EntryId, FileId>>& aMainFiles) + -> Result<Ok, QMResult> { + for (const auto& mainFile : aMainFiles) { + const nsLiteralCString clearMainFilesQuery = + "DELETE FROM MainFiles " + "WHERE handle = :entryId AND fileId = :fileId " + ";"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, clearMainFilesQuery)); + + QM_TRY( + QM_TO_RESULT(stmt.BindEntryIdByName("entryId"_ns, mainFile.first))); + QM_TRY(QM_TO_RESULT(stmt.BindFileIdByName("fileId"_ns, mainFile.second))); + + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + return Ok{}; + }(invalidMainFiles))); + + return Ok{}; +} + +nsresult ConnectUsagesToFileIds(ResultConnection& aConn) { + QM_TRY( + MOZ_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_keys = OFF;"_ns))); + + auto turnForeignKeysBackOn = MakeScopeExit([&aConn]() { + QM_WARNONLY_TRY( + MOZ_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns))); + }); + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConn)); + + QM_TRY(MOZ_TO_RESULT( + aConn->ExecuteSimpleSQL("DROP TABLE IF EXISTS migrateUsages ;"_ns))); + + QM_TRY(MOZ_TO_RESULT(aConn->ExecuteSimpleSQL( + "CREATE TABLE migrateUsages ( " + "handle BLOB PRIMARY KEY, " + "usage INTEGER NOT NULL DEFAULT 0, " + "tracked BOOLEAN NOT NULL DEFAULT 0 CHECK (tracked IN (0, 1)), " + "CONSTRAINT handles_are_fileIds " + "FOREIGN KEY (handle) " + "REFERENCES FileIds (fileId) " + "ON DELETE CASCADE ) " + ";"_ns))); + + QM_TRY(MOZ_TO_RESULT(aConn->ExecuteSimpleSQL( + "INSERT INTO migrateUsages ( handle, usage, tracked ) " + "SELECT handle, usage, tracked FROM Usages ;"_ns))); + + QM_TRY(MOZ_TO_RESULT(aConn->ExecuteSimpleSQL("DROP TABLE Usages;"_ns))); + + QM_TRY(MOZ_TO_RESULT(aConn->ExecuteSimpleSQL( + "ALTER TABLE migrateUsages RENAME TO Usages;"_ns))); + + QM_TRY( + MOZ_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_key_check;"_ns))); + + QM_TRY(MOZ_TO_RESULT(transaction.Commit())); + + return NS_OK; +} + +nsresult CreateEntryNamesView(ResultConnection& aConn) { + return aConn->ExecuteSimpleSQL( + "CREATE VIEW IF NOT EXISTS EntryNames AS " + "SELECT isFile, handle, parent, name FROM Entries INNER JOIN ( " + "SELECT 1 AS isFile, handle, name FROM Files UNION " + "SELECT 0, handle, name FROM Directories ) " + "USING (handle) " + ";"_ns); +} + +nsresult FixEntryIds(const ResultConnection& aConnection, + const EntryId& aRootEntry) { + const nsLiteralCString calculateHashesQuery = + "CREATE TEMPORARY TABLE EntryMigrationTable AS " + "WITH RECURSIVE " + "rehashMap( depth, isFile, handle, parent, name, hash ) AS ( " + "SELECT 0, isFile, handle, parent, name, hashEntry( :rootEntry, name ) " + "FROM EntryNames WHERE parent = :rootEntry UNION SELECT " + "1 + depth, EntryNames.isFile, EntryNames.handle, EntryNames.parent, " + "EntryNames.name, hashEntry( rehashMap.hash, EntryNames.name ) " + "FROM rehashMap, EntryNames WHERE rehashMap.handle = EntryNames.parent ) " + "SELECT depth, isFile, handle, parent, name, hash FROM rehashMap " + ";"_ns; + + const nsLiteralCString createIndexByDepthQuery = + "CREATE INDEX indexOnDepth ON EntryMigrationTable ( depth ); "_ns; + + // To avoid constraint violation, new entries are inserted under a temporary + // parent. + + const nsLiteralCString insertTemporaryParentEntry = + "INSERT INTO Entries ( handle, parent ) " + "VALUES ( :tempParent, :rootEntry ) ;"_ns; + + const nsLiteralCString flagTemporaryParentAsDir = + "INSERT INTO Directories ( handle, name ) " + "VALUES ( :tempParent, 'temp' ) ;"_ns; + + const nsLiteralCString insertNewEntriesQuery = + "INSERT INTO Entries ( handle, parent ) " + "SELECT hash, :tempParent FROM EntryMigrationTable WHERE hash != handle " + ";"_ns; + + const nsLiteralCString insertNewDirectoriesQuery = + "INSERT INTO Directories ( handle, name ) " + "SELECT hash, name FROM EntryMigrationTable " + "WHERE isFile = 0 AND hash != handle " + "ORDER BY depth " + ";"_ns; + + const nsLiteralCString insertNewFilesQuery = + "INSERT INTO Files ( handle, type, name ) " + "SELECT EntryMigrationTable.hash, Files.type, EntryMigrationTable.name " + "FROM EntryMigrationTable INNER JOIN Files USING (handle) " + "WHERE EntryMigrationTable.isFile = 1 AND hash != handle " + ";"_ns; + + const nsLiteralCString updateFileMappingsQuery = + "UPDATE FileIds SET handle = hash " + "FROM ( SELECT handle, hash FROM EntryMigrationTable WHERE hash != " + "handle ) " + "AS replacement WHERE FileIds.handle = replacement.handle " + ";"_ns; + + const nsLiteralCString updateMainFilesQuery = + "UPDATE MainFiles SET handle = hash " + "FROM ( SELECT handle, hash FROM EntryMigrationTable WHERE hash != " + "handle ) " + "AS replacement WHERE MainFiles.handle = replacement.handle " + ";"_ns; + + // Now fix the parents. + const nsLiteralCString updateEntryMappingsQuery = + "UPDATE Entries SET parent = hash " + "FROM ( SELECT Lhs.hash AS handle, Rhs.hash AS hash, Lhs.depth AS depth " + "FROM EntryMigrationTable AS Lhs " + "INNER JOIN EntryMigrationTable AS Rhs " + "ON Rhs.handle = Lhs.parent ORDER BY depth ) AS replacement " + "WHERE Entries.handle = replacement.handle " + "AND Entries.parent = :tempParent " + ";"_ns; + + const nsLiteralCString cleanupOldEntriesQuery = + "DELETE FROM Entries WHERE handle IN " + "( SELECT handle FROM EntryMigrationTable WHERE hash != handle ) " + ";"_ns; + + const nsLiteralCString cleanupTemporaryParent = + "DELETE FROM Entries WHERE handle = :tempParent ;"_ns; + + const nsLiteralCString dropIndexByDepthQuery = + "DROP INDEX indexOnDepth ; "_ns; + + // Index is automatically deleted + const nsLiteralCString cleanupTemporaries = + "DROP TABLE EntryMigrationTable ;"_ns; + + EntryId tempParent(nsCString(nsID::GenerateUUID().ToString().get())); + + nsCOMPtr<mozIStorageFunction> rehashFunction = + new data::FileSystemHashStorageFunction(); + QM_TRY(MOZ_TO_RESULT(aConnection->CreateFunction("hashEntry"_ns, + /* number of arguments */ 2, + rehashFunction))); + auto finallyRemoveFunction = MakeScopeExit([&aConnection]() { + QM_WARNONLY_TRY(MOZ_TO_RESULT(aConnection->RemoveFunction("hashEntry"_ns))); + }); + + // We need this to make sure the old entries get removed + QM_TRY(MOZ_TO_RESULT( + aConnection->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns))); + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConnection)); + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, calculateHashesQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("rootEntry"_ns, aRootEntry))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + QM_TRY(QM_TO_RESULT(aConnection->ExecuteSimpleSQL(createIndexByDepthQuery))); + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, insertTemporaryParentEntry)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("tempParent"_ns, tempParent))); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("rootEntry"_ns, aRootEntry))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, flagTemporaryParentAsDir)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("tempParent"_ns, tempParent))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewEntriesQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("tempParent"_ns, tempParent))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewDirectoriesQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, insertNewFilesQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, updateFileMappingsQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, updateMainFilesQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP( + ResultStatement stmt, + ResultStatement::Create(aConnection, updateEntryMappingsQuery)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("tempParent"_ns, tempParent))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, cleanupOldEntriesQuery)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, cleanupTemporaryParent)); + QM_TRY(QM_TO_RESULT(stmt.BindEntryIdByName("tempParent"_ns, tempParent))); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + QM_TRY(QM_TO_RESULT(aConnection->ExecuteSimpleSQL(dropIndexByDepthQuery))); + + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConnection, cleanupTemporaries)); + QM_TRY(QM_TO_RESULT(stmt.Execute())); + } + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + QM_WARNONLY_TRY(QM_TO_RESULT(aConnection->ExecuteSimpleSQL("VACUUM;"_ns))); + + return NS_OK; +} + +} // namespace + +Result<DatabaseVersion, QMResult> SchemaVersion002::InitializeConnection( + ResultConnection& aConn, data::FileSystemFileManager& aFileManager, + const Origin& aOrigin) { + QM_TRY_UNWRAP(const bool wasEmpty, CheckIfEmpty(aConn)); + + DatabaseVersion currentVersion = 0; + + if (wasEmpty) { + QM_TRY(QM_TO_RESULT(SetEncoding(aConn))); + } else { + QM_TRY(QM_TO_RESULT(aConn->GetSchemaVersion(¤tVersion))); + } + + if (currentVersion < sVersion) { + MOZ_ASSERT_IF(0 != currentVersion, 1 == currentVersion); + + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConn)); + + if (0 == currentVersion) { + QM_TRY(QM_TO_RESULT(SchemaVersion001::CreateTables(aConn, aOrigin))); + } + + QM_TRY(QM_TO_RESULT(CreateFileIds(aConn))); + + if (!wasEmpty) { + QM_TRY(QM_TO_RESULT(PopulateFileIds(aConn))); + } + + QM_TRY(QM_TO_RESULT(ConnectUsagesToFileIds(aConn))); + + QM_TRY(QM_TO_RESULT(CreateMainFiles(aConn))); + if (!wasEmpty) { + QM_TRY(QM_TO_RESULT(PopulateMainFiles(aConn))); + } + + QM_TRY(QM_TO_RESULT(CreateEntryNamesView(aConn))); + + QM_TRY(QM_TO_RESULT(aConn->SetSchemaVersion(sVersion))); + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + if (!wasEmpty) { + QM_TRY(QM_TO_RESULT(aConn->ExecuteSimpleSQL("VACUUM;"_ns))); + } + } + + // The upgrade from version 1 to version 2 was buggy, so we have to check if + // the Usages table still references the Files table which is a sign that + // the upgrade wasn't complete. This extra query has only negligible perf + // impact. See bug 1847989. + auto UsagesTableRefsFilesTable = [&aConn]() -> Result<bool, QMResult> { + const nsLiteralCString query = + "SELECT pragma_foreign_key_list.'table'=='Files' " + "FROM pragma_foreign_key_list('Usages');"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, ResultStatement::Create(aConn, query)); + + return stmt.YesOrNoQuery(); + }; + + QM_TRY_UNWRAP(auto usagesTableRefsFilesTable, UsagesTableRefsFilesTable()); + + if (usagesTableRefsFilesTable) { + QM_TRY_UNWRAP(auto transaction, StartedTransaction::Create(aConn)); + + // The buggy upgrade didn't call PopulateFileIds, ConnectUsagesToFileIds + // and PopulateMainFiles was completely missing. Since invalid file ids + // and main files could be inserted when the profile was broken, we need + // to clear them before populating. + QM_TRY(ClearInvalidFileIds(aConn, aFileManager)); + QM_TRY(QM_TO_RESULT(PopulateFileIds(aConn))); + QM_TRY(QM_TO_RESULT(ConnectUsagesToFileIds(aConn))); + QM_TRY(ClearInvalidMainFiles(aConn, aFileManager)); + QM_TRY(QM_TO_RESULT(PopulateMainFiles(aConn))); + + QM_TRY(QM_TO_RESULT(transaction.Commit())); + + QM_TRY(QM_TO_RESULT(aConn->ExecuteSimpleSQL("VACUUM;"_ns))); + + QM_TRY_UNWRAP(usagesTableRefsFilesTable, UsagesTableRefsFilesTable()); + MOZ_ASSERT(!usagesTableRefsFilesTable); + } + + // In schema version 001, entryId was unique but not necessarily related to + // a path. For schema 002, we have to fix all entryIds to be derived from + // the underlying path. + auto OneTimeRehashingDone = [&aConn]() -> Result<bool, QMResult> { + const nsLiteralCString query = + "SELECT EXISTS (SELECT 1 FROM sqlite_master " + "WHERE type='table' AND name='RehashedFrom001to002' ) ;"_ns; + + QM_TRY_UNWRAP(ResultStatement stmt, ResultStatement::Create(aConn, query)); + + return stmt.YesOrNoQuery(); + }; + + QM_TRY_UNWRAP(auto oneTimeRehashingDone, OneTimeRehashingDone()); + + if (!oneTimeRehashingDone) { + const nsLiteralCString findRootEntry = + "SELECT handle FROM Entries WHERE parent IS NULL ;"_ns; + + EntryId rootId; + { + QM_TRY_UNWRAP(ResultStatement stmt, + ResultStatement::Create(aConn, findRootEntry)); + + QM_TRY_UNWRAP(DebugOnly<bool> moreResults, stmt.ExecuteStep()); + MOZ_ASSERT(moreResults); + + QM_TRY_UNWRAP(rootId, stmt.GetEntryIdByColumn(/* Column */ 0u)); + } + + MOZ_ASSERT(!rootId.IsEmpty()); + + QM_TRY(QM_TO_RESULT(FixEntryIds(aConn, rootId))); + + QM_TRY(QM_TO_RESULT(aConn->ExecuteSimpleSQL( + "CREATE TABLE RehashedFrom001to002 (id INTEGER PRIMARY KEY);"_ns))); + + QM_TRY_UNWRAP(DebugOnly<bool> isDoneNow, OneTimeRehashingDone()); + MOZ_ASSERT(isDoneNow); + } + + QM_TRY(QM_TO_RESULT(aConn->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns))); + + QM_TRY(QM_TO_RESULT(aConn->GetSchemaVersion(¤tVersion))); + + return currentVersion; +} + +} // namespace mozilla::dom::fs diff --git a/dom/fs/parent/datamodel/SchemaVersion002.h b/dom/fs/parent/datamodel/SchemaVersion002.h new file mode 100644 index 0000000000..60f2aa53cb --- /dev/null +++ b/dom/fs/parent/datamodel/SchemaVersion002.h @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION002_H_ +#define DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION002_H_ + +#include "SchemaVersion001.h" + +namespace mozilla::dom::fs { + +namespace data { +class FileSystemFileManager; +} // namespace data + +struct SchemaVersion002 { + static Result<DatabaseVersion, QMResult> InitializeConnection( + ResultConnection& aConn, data::FileSystemFileManager& aFileManager, + const Origin& aOrigin); + + static constexpr DatabaseVersion sVersion = 2; +}; + +} // namespace mozilla::dom::fs + +#endif // DOM_FS_PARENT_DATAMODEL_SCHEMAVERSION002_H_ diff --git a/dom/fs/parent/datamodel/moz.build b/dom/fs/parent/datamodel/moz.build new file mode 100644 index 0000000000..8bb5d35381 --- /dev/null +++ b/dom/fs/parent/datamodel/moz.build @@ -0,0 +1,28 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS.mozilla.dom += [ + "FileSystemDataManager.h", +] + +UNIFIED_SOURCES += [ + "FileSystemDatabaseManager.cpp", + "FileSystemDatabaseManagerVersion001.cpp", + "FileSystemDatabaseManagerVersion002.cpp", + "FileSystemDataManager.cpp", + "FileSystemFileManager.cpp", + "SchemaVersion001.cpp", + "SchemaVersion002.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/fs/include", + "/dom/fs/parent", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" |