diff options
Diffstat (limited to 'dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp')
-rw-r--r-- | dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp | 1567 |
1 files changed, 1567 insertions, 0 deletions
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 |