QuotaClient.cpp (19654B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "DBAction.h" 8 #include "FileUtilsImpl.h" 9 #include "QuotaClientImpl.h" 10 #include "mozilla/GeckoTrace.h" 11 #include "mozilla/ResultExtensions.h" 12 #include "mozilla/dom/cache/DBSchema.h" 13 #include "mozilla/dom/cache/Manager.h" 14 #include "mozilla/dom/quota/PersistenceType.h" 15 #include "mozilla/dom/quota/QuotaCommon.h" 16 #include "mozilla/dom/quota/QuotaManager.h" 17 #include "mozilla/dom/quota/UsageInfo.h" 18 #include "mozilla/ipc/BackgroundParent.h" 19 #include "nsIFile.h" 20 #include "nsThreadUtils.h" 21 22 namespace mozilla::dom::cache { 23 24 using mozilla::dom::quota::AssertIsOnIOThread; 25 using mozilla::dom::quota::Client; 26 using mozilla::dom::quota::CloneFileAndAppend; 27 using mozilla::dom::quota::DatabaseUsageType; 28 using mozilla::dom::quota::GetDirEntryKind; 29 using mozilla::dom::quota::nsIFileKind; 30 using mozilla::dom::quota::OriginMetadata; 31 using mozilla::dom::quota::PrincipalMetadata; 32 using mozilla::dom::quota::QuotaManager; 33 using mozilla::dom::quota::UsageInfo; 34 using mozilla::ipc::AssertIsOnBackgroundThread; 35 36 namespace { 37 38 template <typename StepFunc> 39 Result<UsageInfo, nsresult> ReduceUsageInfo(nsIFile& aDir, 40 const Atomic<bool>& aCanceled, 41 const StepFunc& aStepFunc) { 42 QM_TRY_RETURN(quota::ReduceEachFileAtomicCancelable( 43 aDir, aCanceled, UsageInfo{}, 44 [&aStepFunc](UsageInfo usageInfo, const nsCOMPtr<nsIFile>& bodyDir) 45 -> Result<UsageInfo, nsresult> { 46 QM_TRY(OkIf(!QuotaManager::IsShuttingDown()).mapErr([](const auto&) { 47 return NS_ERROR_ABORT; 48 })); 49 50 QM_TRY_INSPECT(const auto& stepUsageInfo, aStepFunc(bodyDir)); 51 52 return usageInfo + stepUsageInfo; 53 })); 54 } 55 56 Result<int64_t, nsresult> GetPaddingSizeFromDB( 57 nsIFile& aDir, nsIFile& aDBFile, const OriginMetadata& aOriginMetadata, 58 const Maybe<CipherKey>& aMaybeCipherKey) { 59 CacheDirectoryMetadata directoryMetadata(aOriginMetadata); 60 // directoryMetadata.mDirectoryLockId must be -1 (which is default for new 61 // CacheDirectoryMetadata) because this method should only be called from 62 // QuotaClient::InitOrigin when the temporary storage hasn't been initialized 63 // yet. At that time, the in-memory objects (e.g. OriginInfo) are only being 64 // created so it doesn't make sense to tunnel quota information to QuotaVFS 65 // to get corresponding QuotaObject instance for the SQLite file). 66 MOZ_DIAGNOSTIC_ASSERT(directoryMetadata.mDirectoryLockId == -1); 67 68 #ifdef DEBUG 69 { 70 QM_TRY_INSPECT(const bool& exists, 71 MOZ_TO_RESULT_INVOKE_MEMBER(aDBFile, Exists)); 72 MOZ_ASSERT(exists); 73 } 74 #endif 75 76 QM_TRY_INSPECT(const auto& conn, 77 OpenDBConnection(directoryMetadata, aDBFile, aMaybeCipherKey)); 78 79 // Make sure that the database has the latest schema before we try to read 80 // from it. We have to do this because GetPaddingSizeFromDB is called 81 // by InitOrigin. And it means that SetupAction::RunSyncWithDBOnTarget hasn't 82 // checked the schema for the given origin yet). 83 QM_TRY(MOZ_TO_RESULT(db::CreateOrMigrateSchema(aDir, *conn))); 84 85 QM_TRY_RETURN(DirectoryPaddingRestore(aDir, *conn, 86 /* aMustRestore */ false)); 87 } 88 89 Result<int64_t, nsresult> GetTotalDiskUsageFromDB( 90 nsIFile& aDir, nsIFile& aDBFile, const OriginMetadata& aOriginMetadata, 91 const Maybe<CipherKey>& aMaybeCipherKey) { 92 CacheDirectoryMetadata directoryMetadata(aOriginMetadata); 93 // directoryMetadata.mDirectoryLockId must be -1 (which is default for new 94 // CacheDirectoryMetadata) because this method should only be called from 95 // QuotaClient::InitOrigin when the temporary storage hasn't been initialized 96 // yet. At that time, the in-memory objects (e.g. OriginInfo) are only being 97 // created so it doesn't make sense to tunnel quota information to QuotaVFS 98 // to get corresponding QuotaObject instance for the SQLite file). 99 MOZ_DIAGNOSTIC_ASSERT(directoryMetadata.mDirectoryLockId == -1); 100 101 #ifdef DEBUG 102 { 103 QM_TRY_INSPECT(const bool& exists, 104 MOZ_TO_RESULT_INVOKE_MEMBER(aDBFile, Exists)); 105 MOZ_ASSERT(exists); 106 } 107 #endif 108 109 QM_TRY_INSPECT(const auto& conn, 110 OpenDBConnection(directoryMetadata, aDBFile, aMaybeCipherKey)); 111 112 // Make sure that the database has the latest schema before we try to read 113 // from it. We have to do this because GetTotalDiskUsageFromDB is called 114 // by InitOrigin. And it means that SetupAction::RunSyncWithDBOnTarget hasn't 115 // checked the schema for the given origin yet). 116 QM_TRY(MOZ_TO_RESULT(db::CreateOrMigrateSchema(aDir, *conn))); 117 118 QM_TRY_RETURN(db::GetTotalDiskUsage(*conn)); 119 } 120 121 } // namespace 122 123 const nsLiteralString kCachesSQLiteFilename = u"caches.sqlite"_ns; 124 const nsLiteralString kMorgueDirectoryFilename = u"morgue"_ns; 125 126 CacheQuotaClient::CacheQuotaClient() { 127 AssertIsOnBackgroundThread(); 128 MOZ_DIAGNOSTIC_ASSERT(!sInstance); 129 sInstance = this; 130 } 131 132 // static 133 CacheQuotaClient* CacheQuotaClient::Get() { 134 MOZ_DIAGNOSTIC_ASSERT(sInstance); 135 return sInstance; 136 } 137 138 CacheQuotaClient::Type CacheQuotaClient::GetType() { return DOMCACHE; } 139 140 Result<UsageInfo, nsresult> CacheQuotaClient::InitOrigin( 141 PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, 142 const AtomicBool& aCanceled) { 143 GECKO_TRACE_SCOPE("dom::cache", "CacheQuotaClient::InitOrigin"); 144 145 AssertIsOnIOThread(); 146 MOZ_ASSERT(aOriginMetadata.mPersistenceType == aPersistenceType); 147 148 QuotaManager* const qm = QuotaManager::Get(); 149 MOZ_DIAGNOSTIC_ASSERT(qm); 150 151 QM_TRY_INSPECT(const auto& dir, qm->GetOriginDirectory(aOriginMetadata)); 152 153 QM_TRY(MOZ_TO_RESULT( 154 dir->Append(NS_LITERAL_STRING_FROM_CSTRING(DOMCACHE_DIRECTORY_NAME)))); 155 156 QM_TRY_INSPECT( 157 const auto& cachesSQLiteFile, 158 ([dir]() -> Result<nsCOMPtr<nsIFile>, nsresult> { 159 QM_TRY_INSPECT(const auto& cachesSQLite, 160 CloneFileAndAppend(*dir, kCachesSQLiteFilename)); 161 162 // IsDirectory is used to check if caches.sqlite exists or not. Another 163 // benefit of this is that we can test the failed cases by creating a 164 // directory named "caches.sqlite". 165 QM_TRY_INSPECT(const auto& dirEntryKind, 166 GetDirEntryKind(*cachesSQLite)); 167 if (dirEntryKind == nsIFileKind::DoesNotExist) { 168 // We only ensure padding files and morgue directory get removed like 169 // WipeDatabase in DBAction.cpp. The -wal journal file will be 170 // automatically deleted by sqlite when the new database is created. 171 // XXX Ideally, we would delete the -wal journal file as well (here 172 // and also in WipeDatabase). 173 // XXX We should have something like WipeDatabaseNoQuota for this. 174 // XXX Long term, we might even think about removing entire origin 175 // directory because missing caches.sqlite while other files exist can 176 // be interpreted as database corruption. 177 QM_TRY(MOZ_TO_RESULT(mozilla::dom::cache::DirectoryPaddingDeleteFile( 178 *dir, DirPaddingFile::TMP_FILE))); 179 180 QM_TRY(MOZ_TO_RESULT(mozilla::dom::cache::DirectoryPaddingDeleteFile( 181 *dir, DirPaddingFile::FILE))); 182 183 QM_TRY_INSPECT(const auto& morgueDir, 184 CloneFileAndAppend(*dir, kMorgueDirectoryFilename)); 185 186 QM_TRY(MOZ_TO_RESULT(mozilla::dom::cache::RemoveNsIFileRecursively( 187 Nothing(), *morgueDir, 188 /* aTrackQuota */ false))); 189 190 return nsCOMPtr<nsIFile>{nullptr}; 191 } 192 193 QM_TRY(OkIf(dirEntryKind == nsIFileKind::ExistsAsFile), 194 Err(NS_ERROR_FAILURE)); 195 196 return cachesSQLite; 197 }())); 198 199 // If the caches.sqlite doesn't exist, then padding files and morgue directory 200 // should have been removed if they existed. We ignore the rest of known files 201 // because we assume that they will be removed when a new database is created. 202 // XXX Ensure the -wel file is removed if the caches.sqlite doesn't exist. 203 QM_TRY(OkIf(!!cachesSQLiteFile), UsageInfo{}); 204 205 const auto maybeCipherKey = [this, &aOriginMetadata] { 206 Maybe<CipherKey> maybeCipherKey; 207 auto cipherKeyManager = GetOrCreateCipherKeyManager(aOriginMetadata); 208 if (cipherKeyManager) { 209 maybeCipherKey = Some(cipherKeyManager->Ensure()); 210 } 211 return maybeCipherKey; 212 }(); 213 214 QM_TRY_INSPECT( 215 const auto& paddingSize, 216 ([dir, cachesSQLiteFile, &aOriginMetadata, 217 &maybeCipherKey]() -> Result<int64_t, nsresult> { 218 if (!DirectoryPaddingFileExists(*dir, DirPaddingFile::TMP_FILE)) { 219 QM_WARNONLY_TRY_UNWRAP(const auto maybePaddingSize, 220 DirectoryPaddingGet(*dir)); 221 if (maybePaddingSize) { 222 return maybePaddingSize.ref(); 223 } 224 } 225 226 // If the temporary file still exists or failing to get the padding size 227 // from the padding file, then we need to get the padding size from the 228 // database and restore the padding file. 229 QM_TRY_RETURN(GetPaddingSizeFromDB(*dir, *cachesSQLiteFile, 230 aOriginMetadata, maybeCipherKey)); 231 }())); 232 233 QM_TRY_INSPECT(const auto& totalDiskUsage, 234 GetTotalDiskUsageFromDB(*dir, *cachesSQLiteFile, 235 aOriginMetadata, maybeCipherKey)); 236 237 QM_TRY_INSPECT( 238 const auto& innerUsageInfo, 239 ReduceUsageInfo( 240 *dir, aCanceled, 241 [](const nsCOMPtr<nsIFile>& file) -> Result<UsageInfo, nsresult> { 242 QM_TRY_INSPECT(const auto& leafName, 243 MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, file, 244 GetLeafName)); 245 246 QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file)); 247 248 switch (dirEntryKind) { 249 case nsIFileKind::ExistsAsDirectory: 250 if (!leafName.EqualsLiteral("morgue")) { 251 NS_WARNING("Unknown Cache directory found!"); 252 } 253 254 break; 255 256 case nsIFileKind::ExistsAsFile: 257 // Ignore transient sqlite files and marker files 258 if (leafName.EqualsLiteral("caches.sqlite-journal") || 259 leafName.EqualsLiteral("caches.sqlite-shm") || 260 StringBeginsWith(leafName, u"caches.sqlite-mj"_ns) || 261 leafName.EqualsLiteral("context_open.marker")) { 262 break; 263 } 264 265 if (leafName.Equals(kCachesSQLiteFilename) || 266 leafName.EqualsLiteral("caches.sqlite-wal")) { 267 QM_TRY_INSPECT( 268 const int64_t& fileSize, 269 MOZ_TO_RESULT_INVOKE_MEMBER(file, GetFileSize)); 270 MOZ_DIAGNOSTIC_ASSERT(fileSize >= 0); 271 272 return UsageInfo{DatabaseUsageType(Some(fileSize))}; 273 } 274 275 // Ignore directory padding file 276 if (leafName.EqualsLiteral(PADDING_FILE_NAME) || 277 leafName.EqualsLiteral(PADDING_TMP_FILE_NAME)) { 278 break; 279 } 280 281 NS_WARNING("Unknown Cache file found!"); 282 283 break; 284 285 case nsIFileKind::DoesNotExist: 286 // Ignore files that got removed externally while iterating. 287 break; 288 } 289 290 return UsageInfo{}; 291 })); 292 293 // FIXME: Separate file usage and database usage in OriginInfo so that the 294 // workaround for treating padding file size as database usage can be removed. 295 return UsageInfo{DatabaseUsageType(Some(paddingSize))} + 296 UsageInfo{DatabaseUsageType(Some(totalDiskUsage))} + innerUsageInfo; 297 } 298 299 nsresult CacheQuotaClient::InitOriginWithoutTracking( 300 PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, 301 const AtomicBool& aCanceled) { 302 AssertIsOnIOThread(); 303 304 // This is called when a storage/permanent/${origin}/cache directory exists. 305 // Even though this shouldn't happen with a "good" profile, we shouldn't 306 // return an error here, since that would cause origin initialization to fail. 307 // We just warn and otherwise ignore that. 308 UNKNOWN_FILE_WARNING(NS_LITERAL_STRING_FROM_CSTRING(DOMCACHE_DIRECTORY_NAME)); 309 return NS_OK; 310 } 311 312 Result<UsageInfo, nsresult> CacheQuotaClient::GetUsageForOrigin( 313 PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, 314 const AtomicBool& aCanceled) { 315 AssertIsOnIOThread(); 316 317 // We can't open the database at this point, since it can be already used by 318 // the Cache IO thread. Use the cached value instead. 319 320 QuotaManager* quotaManager = QuotaManager::Get(); 321 MOZ_ASSERT(quotaManager); 322 323 return quotaManager->GetUsageForClient(aOriginMetadata.mPersistenceType, 324 aOriginMetadata, Client::DOMCACHE); 325 } 326 327 void CacheQuotaClient::OnOriginClearCompleted( 328 const OriginMetadata& aOriginMetadata) { 329 AssertIsOnIOThread(); 330 331 if (aOriginMetadata.mPersistenceType == quota::PERSISTENCE_TYPE_PRIVATE) { 332 if (auto entry = mCipherKeyManagers.Lookup(aOriginMetadata.mOrigin)) { 333 entry.Data()->Invalidate(); 334 entry.Remove(); 335 } 336 } 337 } 338 339 void CacheQuotaClient::OnRepositoryClearCompleted( 340 PersistenceType aPersistenceType) { 341 AssertIsOnIOThread(); 342 343 if (aPersistenceType == quota::PERSISTENCE_TYPE_PRIVATE) { 344 for (const auto& cipherKeyManager : mCipherKeyManagers.Values()) { 345 cipherKeyManager->Invalidate(); 346 } 347 348 mCipherKeyManagers.Clear(); 349 } 350 } 351 352 void CacheQuotaClient::ReleaseIOThreadObjects() { 353 // Nothing to do here as the Context handles cleaning everything up 354 // automatically. 355 } 356 357 void CacheQuotaClient::AbortOperationsForLocks( 358 const DirectoryLockIdTable& aDirectoryLockIds) { 359 AssertIsOnBackgroundThread(); 360 361 Manager::Abort(aDirectoryLockIds); 362 } 363 364 void CacheQuotaClient::AbortOperationsForProcess( 365 ContentParentId aContentParentId) { 366 // The Cache and Context can be shared by multiple client processes. They 367 // are not exclusively owned by a single process. 368 // 369 // As far as I can tell this is used by QuotaManager to abort operations 370 // when a particular process goes away. We definitely don't want this 371 // since we are shared. Also, the Cache actor code already properly 372 // handles asynchronous actor destruction when the child process dies. 373 // 374 // Therefore, do nothing here. 375 } 376 377 void CacheQuotaClient::AbortAllOperations() { 378 AssertIsOnBackgroundThread(); 379 380 Manager::AbortAll(); 381 } 382 383 void CacheQuotaClient::StartIdleMaintenance() {} 384 385 void CacheQuotaClient::StopIdleMaintenance() {} 386 387 void CacheQuotaClient::InitiateShutdown() { 388 AssertIsOnBackgroundThread(); 389 390 Manager::InitiateShutdown(); 391 } 392 393 bool CacheQuotaClient::IsShutdownCompleted() const { 394 AssertIsOnBackgroundThread(); 395 396 return Manager::IsShutdownAllComplete(); 397 } 398 399 void CacheQuotaClient::ForceKillActors() { 400 // Currently we don't implement killing actors (are there any to kill here?). 401 } 402 403 nsCString CacheQuotaClient::GetShutdownStatus() const { 404 AssertIsOnBackgroundThread(); 405 406 return Manager::GetShutdownStatus(); 407 } 408 409 void CacheQuotaClient::FinalizeShutdown() { 410 // Nothing to do here. 411 } 412 413 nsresult CacheQuotaClient::UpgradeStorageFrom2_0To2_1(nsIFile* aDirectory) { 414 AssertIsOnIOThread(); 415 MOZ_DIAGNOSTIC_ASSERT(aDirectory); 416 417 QM_TRY(MOZ_TO_RESULT(DirectoryPaddingInit(*aDirectory))); 418 419 return NS_OK; 420 } 421 422 nsresult CacheQuotaClient::RestorePaddingFileInternal( 423 nsIFile* aBaseDir, mozIStorageConnection* aConn) { 424 MOZ_ASSERT(!NS_IsMainThread()); 425 MOZ_DIAGNOSTIC_ASSERT(aBaseDir); 426 MOZ_DIAGNOSTIC_ASSERT(aConn); 427 428 QM_TRY_INSPECT(const int64_t& dummyPaddingSize, 429 DirectoryPaddingRestore(*aBaseDir, *aConn, 430 /* aMustRestore */ true)); 431 (void)dummyPaddingSize; 432 433 return NS_OK; 434 } 435 436 nsresult CacheQuotaClient::WipePaddingFileInternal( 437 const CacheDirectoryMetadata& aDirectoryMetadata, nsIFile* aBaseDir) { 438 MOZ_ASSERT(!NS_IsMainThread()); 439 MOZ_DIAGNOSTIC_ASSERT(aBaseDir); 440 441 MOZ_ASSERT(DirectoryPaddingFileExists(*aBaseDir, DirPaddingFile::FILE)); 442 443 QM_TRY_INSPECT( 444 const int64_t& paddingSize, ([&aBaseDir]() -> Result<int64_t, nsresult> { 445 const bool temporaryPaddingFileExist = 446 DirectoryPaddingFileExists(*aBaseDir, DirPaddingFile::TMP_FILE); 447 448 Maybe<int64_t> directoryPaddingGetResult; 449 if (!temporaryPaddingFileExist) { 450 QM_TRY_UNWRAP(directoryPaddingGetResult, 451 ([&aBaseDir]() -> Result<Maybe<int64_t>, nsresult> { 452 QM_TRY_RETURN( 453 DirectoryPaddingGet(*aBaseDir).map(Some<int64_t>), 454 Maybe<int64_t>{}); 455 }())); 456 } 457 458 if (temporaryPaddingFileExist || !directoryPaddingGetResult) { 459 // XXXtt: Maybe have a method in the QuotaManager to clean the usage 460 // under the quota client and the origin. There is nothing we can do 461 // to recover the file. 462 NS_WARNING("Cannnot read padding size from file!"); 463 return 0; 464 } 465 466 return *directoryPaddingGetResult; 467 }())); 468 469 if (paddingSize > 0) { 470 DecreaseUsageForDirectoryMetadata(aDirectoryMetadata, paddingSize); 471 } 472 473 QM_TRY(MOZ_TO_RESULT( 474 DirectoryPaddingDeleteFile(*aBaseDir, DirPaddingFile::FILE))); 475 476 // Remove temporary file if we have one. 477 QM_TRY(MOZ_TO_RESULT( 478 DirectoryPaddingDeleteFile(*aBaseDir, DirPaddingFile::TMP_FILE))); 479 480 QM_TRY(MOZ_TO_RESULT(DirectoryPaddingInit(*aBaseDir))); 481 482 return NS_OK; 483 } 484 485 RefPtr<CipherKeyManager> CacheQuotaClient::GetOrCreateCipherKeyManager( 486 const PrincipalMetadata& aMetadata) { 487 AssertIsOnIOThread(); 488 489 auto privateOrigin = aMetadata.mIsPrivate; 490 if (!privateOrigin) { 491 return nullptr; 492 } 493 494 const auto& origin = aMetadata.mOrigin; 495 return mCipherKeyManagers.LookupOrInsertWith( 496 origin, [] { return new CipherKeyManager("CacheCipherKeyManager"); }); 497 } 498 499 CacheQuotaClient::~CacheQuotaClient() { 500 AssertIsOnBackgroundThread(); 501 MOZ_DIAGNOSTIC_ASSERT(sInstance == this); 502 503 sInstance = nullptr; 504 } 505 506 // static 507 CacheQuotaClient* CacheQuotaClient::sInstance = nullptr; 508 509 // static 510 already_AddRefed<quota::Client> CreateQuotaClient() { 511 AssertIsOnBackgroundThread(); 512 513 RefPtr<CacheQuotaClient> ref = new CacheQuotaClient(); 514 return ref.forget(); 515 } 516 517 // static 518 nsresult RestorePaddingFile(nsIFile* aBaseDir, mozIStorageConnection* aConn) { 519 MOZ_ASSERT(!NS_IsMainThread()); 520 MOZ_DIAGNOSTIC_ASSERT(aBaseDir); 521 MOZ_DIAGNOSTIC_ASSERT(aConn); 522 523 RefPtr<CacheQuotaClient> cacheQuotaClient = CacheQuotaClient::Get(); 524 MOZ_DIAGNOSTIC_ASSERT(cacheQuotaClient); 525 526 QM_TRY(MOZ_TO_RESULT( 527 cacheQuotaClient->RestorePaddingFileInternal(aBaseDir, aConn))); 528 529 return NS_OK; 530 } 531 532 // static 533 nsresult WipePaddingFile(const CacheDirectoryMetadata& aDirectoryMetadata, 534 nsIFile* aBaseDir) { 535 MOZ_ASSERT(!NS_IsMainThread()); 536 MOZ_DIAGNOSTIC_ASSERT(aBaseDir); 537 538 RefPtr<CacheQuotaClient> cacheQuotaClient = CacheQuotaClient::Get(); 539 MOZ_DIAGNOSTIC_ASSERT(cacheQuotaClient); 540 541 QM_TRY(MOZ_TO_RESULT( 542 cacheQuotaClient->WipePaddingFileInternal(aDirectoryMetadata, aBaseDir))); 543 544 return NS_OK; 545 } 546 547 } // namespace mozilla::dom::cache