/* -*- 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; 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 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 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 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 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 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 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 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(aOnMissingFile)); return NS_OK; } Result, 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 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 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)); 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, 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(Nothing()); } QM_TRY_UNWRAP(Usage trackedUsage, stmt.GetUsageByColumn(/* Column */ 0u)); return Some(trackedUsage); } Result ScanTrackedFiles( const FileSystemConnection& aConnection, const FileSystemFileManager& aFileManager) { QM_TRY_INSPECT(const nsTArray& 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 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 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 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&& 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 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 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 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 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 doesItExistNow, DoesDirectoryExist(mConnection, aHandle)); MOZ_ASSERT(doesItExistNow); return entryId; } Result 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& aPath, nsCOMPtr& 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(lastModTime); aPath.Reverse(); return NS_OK; } Result FileSystemDatabaseManagerVersion001::GetDirectoryEntries( const EntryId& aParent, PageNumber aPage) const { // TODO: Offset is reported to have bad performance - see Bug 1780386. const nsCString directoriesQuery = "SELECT Dirs.handle, Dirs.name " "FROM Directories AS Dirs " "INNER JOIN ( " "SELECT handle " "FROM Entries " "WHERE parent = :parent " "LIMIT :pageSize " "OFFSET :pageOffset ) " "AS Ents " "ON Dirs.handle = Ents.handle " ";"_ns; const nsCString filesQuery = "SELECT Files.handle, Files.name " "FROM Files " "INNER JOIN ( " "SELECT handle " "FROM Entries " "WHERE parent = :parent " "LIMIT :pageSize " "OFFSET :pageOffset ) " "AS Ents " "ON Files.handle = Ents.handle " ";"_ns; FileSystemDirectoryListing entries; QM_TRY( QM_TO_RESULT(GetEntries(mConnection, directoriesQuery, aParent, aPage, /* aDirectory */ true, entries.directories()))); QM_TRY(QM_TO_RESULT(GetEntries(mConnection, filesQuery, aParent, aPage, /* aDirectory */ false, entries.files()))); return entries; } Result FileSystemDatabaseManagerVersion001::RemoveDirectory( const FileSystemChildMetadata& aHandle, bool aRecursive) { MOZ_ASSERT(!aHandle.parentId().IsEmpty()); if (aHandle.childName().IsEmpty()) { return false; } DebugOnly 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& descendants, FindFilesUnderEntry(entryId)); nsTArray failedRemovals; QM_TRY_UNWRAP(DebugOnly 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 FileSystemDatabaseManagerVersion001::RemoveFile( const FileSystemChildMetadata& aHandle) { MOZ_ASSERT(!aHandle.parentId().IsEmpty()); if (aHandle.childName().IsEmpty()) { return false; } DebugOnly 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& diskItems, FindFilesUnderEntry(entryId)); QM_TRY_UNWRAP(Usage usage, GetUsagesOfDescendants(entryId)); nsTArray failedRemovals; QM_TRY_UNWRAP(DebugOnly 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 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 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 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 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 FileSystemDatabaseManagerVersion001::GetEntryId( const FileSystemChildMetadata& aHandle) const { return GetUniqueEntryId(mConnection, aHandle); } Result FileSystemDatabaseManagerVersion001::GetEntryId( const FileId& aFileId) const { return aFileId.Value(); } Result FileSystemDatabaseManagerVersion001::EnsureFileId( const EntryId& aEntryId) { return FileId(aEntryId); } Result FileSystemDatabaseManagerVersion001::EnsureTemporaryFileId( const EntryId& aEntryId) { return FileId(aEntryId); } Result 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 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 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, QMResult> FileSystemDatabaseManagerVersion001::FindFilesUnderEntry( const EntryId& aEntryId) const { nsTArray 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 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 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 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) const { quota::QuotaManager* quotaManager = quota::QuotaManager::Get(); MOZ_ASSERT(quotaManager); QM_TRY_UNWRAP(nsCOMPtr fileObj, mFileManager->GetFile(aFileId).mapErr(toNSResult)); RefPtr 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 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 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 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 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 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 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 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 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 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, // Fallback. ErrToOkFromQMResult)); } Result 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& /* 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 { 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