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/. */
7 #include "QuotaClientImpl.h"
10 #include "FileUtilsImpl.h"
11 #include "mozilla/DebugOnly.h"
12 #include "mozilla/ResultExtensions.h"
13 #include "mozilla/Telemetry.h"
14 #include "mozilla/Unused.h"
15 #include "mozilla/dom/cache/DBSchema.h"
16 #include "mozilla/dom/cache/Manager.h"
17 #include "mozilla/dom/quota/PersistenceType.h"
18 #include "mozilla/dom/quota/QuotaCommon.h"
19 #include "mozilla/dom/quota/QuotaManager.h"
20 #include "mozilla/dom/quota/UsageInfo.h"
21 #include "mozilla/ipc/BackgroundParent.h"
23 #include "nsThreadUtils.h"
25 namespace mozilla::dom::cache
{
27 using mozilla::dom::quota::AssertIsOnIOThread
;
28 using mozilla::dom::quota::Client
;
29 using mozilla::dom::quota::CloneFileAndAppend
;
30 using mozilla::dom::quota::DatabaseUsageType
;
31 using mozilla::dom::quota::GetDirEntryKind
;
32 using mozilla::dom::quota::nsIFileKind
;
33 using mozilla::dom::quota::OriginMetadata
;
34 using mozilla::dom::quota::PrincipalMetadata
;
35 using mozilla::dom::quota::QuotaManager
;
36 using mozilla::dom::quota::UsageInfo
;
37 using mozilla::ipc::AssertIsOnBackgroundThread
;
41 template <typename StepFunc
>
42 Result
<UsageInfo
, nsresult
> ReduceUsageInfo(nsIFile
& aDir
,
43 const Atomic
<bool>& aCanceled
,
44 const StepFunc
& aStepFunc
) {
45 QM_TRY_RETURN(quota::ReduceEachFileAtomicCancelable(
46 aDir
, aCanceled
, UsageInfo
{},
47 [&aStepFunc
](UsageInfo usageInfo
, const nsCOMPtr
<nsIFile
>& bodyDir
)
48 -> Result
<UsageInfo
, nsresult
> {
49 QM_TRY(OkIf(!QuotaManager::IsShuttingDown()).mapErr([](const auto&) {
50 return NS_ERROR_ABORT
;
53 QM_TRY_INSPECT(const auto& stepUsageInfo
, aStepFunc(bodyDir
));
55 return usageInfo
+ stepUsageInfo
;
59 Result
<int64_t, nsresult
> GetPaddingSizeFromDB(
60 nsIFile
& aDir
, nsIFile
& aDBFile
, const OriginMetadata
& aOriginMetadata
,
61 const Maybe
<CipherKey
>& aMaybeCipherKey
) {
62 CacheDirectoryMetadata
directoryMetadata(aOriginMetadata
);
63 // directoryMetadata.mDirectoryLockId must be -1 (which is default for new
64 // CacheDirectoryMetadata) because this method should only be called from
65 // QuotaClient::InitOrigin when the temporary storage hasn't been initialized
66 // yet. At that time, the in-memory objects (e.g. OriginInfo) are only being
67 // created so it doesn't make sense to tunnel quota information to QuotaVFS
68 // to get corresponding QuotaObject instance for the SQLite file).
69 MOZ_DIAGNOSTIC_ASSERT(directoryMetadata
.mDirectoryLockId
== -1);
73 QM_TRY_INSPECT(const bool& exists
,
74 MOZ_TO_RESULT_INVOKE_MEMBER(aDBFile
, Exists
));
79 QM_TRY_INSPECT(const auto& conn
,
80 OpenDBConnection(directoryMetadata
, aDBFile
, aMaybeCipherKey
));
82 // Make sure that the database has the latest schema before we try to read
83 // from it. We have to do this because GetPaddingSizeFromDB is called
84 // by InitOrigin. And it means that SetupAction::RunSyncWithDBOnTarget hasn't
85 // checked the schema for the given origin yet).
86 QM_TRY(MOZ_TO_RESULT(db::CreateOrMigrateSchema(aDir
, *conn
)));
88 QM_TRY_RETURN(DirectoryPaddingRestore(aDir
, *conn
,
89 /* aMustRestore */ false));
92 Result
<int64_t, nsresult
> GetTotalDiskUsageFromDB(
93 nsIFile
& aDir
, nsIFile
& aDBFile
, const OriginMetadata
& aOriginMetadata
,
94 const Maybe
<CipherKey
>& aMaybeCipherKey
) {
95 CacheDirectoryMetadata
directoryMetadata(aOriginMetadata
);
96 // directoryMetadata.mDirectoryLockId must be -1 (which is default for new
97 // CacheDirectoryMetadata) because this method should only be called from
98 // QuotaClient::InitOrigin when the temporary storage hasn't been initialized
99 // yet. At that time, the in-memory objects (e.g. OriginInfo) are only being
100 // created so it doesn't make sense to tunnel quota information to QuotaVFS
101 // to get corresponding QuotaObject instance for the SQLite file).
102 MOZ_DIAGNOSTIC_ASSERT(directoryMetadata
.mDirectoryLockId
== -1);
106 QM_TRY_INSPECT(const bool& exists
,
107 MOZ_TO_RESULT_INVOKE_MEMBER(aDBFile
, Exists
));
112 QM_TRY_INSPECT(const auto& conn
,
113 OpenDBConnection(directoryMetadata
, aDBFile
, aMaybeCipherKey
));
115 // Make sure that the database has the latest schema before we try to read
116 // from it. We have to do this because GetTotalDiskUsageFromDB is called
117 // by InitOrigin. And it means that SetupAction::RunSyncWithDBOnTarget hasn't
118 // checked the schema for the given origin yet).
119 QM_TRY(MOZ_TO_RESULT(db::CreateOrMigrateSchema(aDir
, *conn
)));
121 QM_TRY_RETURN(db::GetTotalDiskUsage(*conn
));
126 const nsLiteralString kCachesSQLiteFilename
= u
"caches.sqlite"_ns
;
127 const nsLiteralString kMorgueDirectoryFilename
= u
"morgue"_ns
;
129 CacheQuotaClient::CacheQuotaClient() {
130 AssertIsOnBackgroundThread();
131 MOZ_DIAGNOSTIC_ASSERT(!sInstance
);
136 CacheQuotaClient
* CacheQuotaClient::Get() {
137 MOZ_DIAGNOSTIC_ASSERT(sInstance
);
141 CacheQuotaClient::Type
CacheQuotaClient::GetType() { return DOMCACHE
; }
143 Result
<UsageInfo
, nsresult
> CacheQuotaClient::InitOrigin(
144 PersistenceType aPersistenceType
, const OriginMetadata
& aOriginMetadata
,
145 const AtomicBool
& aCanceled
) {
146 AssertIsOnIOThread();
147 MOZ_ASSERT(aOriginMetadata
.mPersistenceType
== aPersistenceType
);
149 QuotaManager
* const qm
= QuotaManager::Get();
150 MOZ_DIAGNOSTIC_ASSERT(qm
);
152 QM_TRY_INSPECT(const auto& dir
, qm
->GetOriginDirectory(aOriginMetadata
));
154 QM_TRY(MOZ_TO_RESULT(
155 dir
->Append(NS_LITERAL_STRING_FROM_CSTRING(DOMCACHE_DIRECTORY_NAME
))));
158 const auto& cachesSQLiteFile
,
159 ([dir
]() -> Result
<nsCOMPtr
<nsIFile
>, nsresult
> {
160 QM_TRY_INSPECT(const auto& cachesSQLite
,
161 CloneFileAndAppend(*dir
, kCachesSQLiteFilename
));
163 // IsDirectory is used to check if caches.sqlite exists or not. Another
164 // benefit of this is that we can test the failed cases by creating a
165 // directory named "caches.sqlite".
166 QM_TRY_INSPECT(const auto& dirEntryKind
,
167 GetDirEntryKind(*cachesSQLite
));
168 if (dirEntryKind
== nsIFileKind::DoesNotExist
) {
169 // We only ensure padding files and morgue directory get removed like
170 // WipeDatabase in DBAction.cpp. The -wal journal file will be
171 // automatically deleted by sqlite when the new database is created.
172 // XXX Ideally, we would delete the -wal journal file as well (here
173 // and also in WipeDatabase).
174 // XXX We should have something like WipeDatabaseNoQuota for this.
175 // XXX Long term, we might even think about removing entire origin
176 // directory because missing caches.sqlite while other files exist can
177 // be interpreted as database corruption.
178 QM_TRY(MOZ_TO_RESULT(mozilla::dom::cache::DirectoryPaddingDeleteFile(
179 *dir
, DirPaddingFile::TMP_FILE
)));
181 QM_TRY(MOZ_TO_RESULT(mozilla::dom::cache::DirectoryPaddingDeleteFile(
182 *dir
, DirPaddingFile::FILE)));
184 QM_TRY_INSPECT(const auto& morgueDir
,
185 CloneFileAndAppend(*dir
, kMorgueDirectoryFilename
));
187 QM_TRY(MOZ_TO_RESULT(mozilla::dom::cache::RemoveNsIFileRecursively(
188 Nothing(), *morgueDir
,
189 /* aTrackQuota */ false)));
191 return nsCOMPtr
<nsIFile
>{nullptr};
194 QM_TRY(OkIf(dirEntryKind
== nsIFileKind::ExistsAsFile
),
195 Err(NS_ERROR_FAILURE
));
200 // If the caches.sqlite doesn't exist, then padding files and morgue directory
201 // should have been removed if they existed. We ignore the rest of known files
202 // because we assume that they will be removed when a new database is created.
203 // XXX Ensure the -wel file is removed if the caches.sqlite doesn't exist.
204 QM_TRY(OkIf(!!cachesSQLiteFile
), UsageInfo
{});
206 const auto maybeCipherKey
= [this, &aOriginMetadata
] {
207 Maybe
<CipherKey
> maybeCipherKey
;
208 auto cipherKeyManager
= GetOrCreateCipherKeyManager(aOriginMetadata
);
209 if (cipherKeyManager
) {
210 maybeCipherKey
= Some(cipherKeyManager
->Ensure());
212 return maybeCipherKey
;
216 const auto& paddingSize
,
217 ([dir
, cachesSQLiteFile
, &aOriginMetadata
,
218 &maybeCipherKey
]() -> Result
<int64_t, nsresult
> {
219 if (!DirectoryPaddingFileExists(*dir
, DirPaddingFile::TMP_FILE
)) {
220 QM_WARNONLY_TRY_UNWRAP(const auto maybePaddingSize
,
221 DirectoryPaddingGet(*dir
));
222 if (maybePaddingSize
) {
223 return maybePaddingSize
.ref();
227 // If the temporary file still exists or failing to get the padding size
228 // from the padding file, then we need to get the padding size from the
229 // database and restore the padding file.
230 QM_TRY_RETURN(GetPaddingSizeFromDB(*dir
, *cachesSQLiteFile
,
231 aOriginMetadata
, maybeCipherKey
));
234 QM_TRY_INSPECT(const auto& totalDiskUsage
,
235 GetTotalDiskUsageFromDB(*dir
, *cachesSQLiteFile
,
236 aOriginMetadata
, maybeCipherKey
));
239 const auto& innerUsageInfo
,
242 [](const nsCOMPtr
<nsIFile
>& file
) -> Result
<UsageInfo
, nsresult
> {
243 QM_TRY_INSPECT(const auto& leafName
,
244 MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString
, file
,
247 QM_TRY_INSPECT(const auto& dirEntryKind
, GetDirEntryKind(*file
));
249 switch (dirEntryKind
) {
250 case nsIFileKind::ExistsAsDirectory
:
251 if (!leafName
.EqualsLiteral("morgue")) {
252 NS_WARNING("Unknown Cache directory found!");
257 case nsIFileKind::ExistsAsFile
:
258 // Ignore transient sqlite files and marker files
259 if (leafName
.EqualsLiteral("caches.sqlite-journal") ||
260 leafName
.EqualsLiteral("caches.sqlite-shm") ||
261 StringBeginsWith(leafName
, u
"caches.sqlite-mj"_ns
) ||
262 leafName
.EqualsLiteral("context_open.marker")) {
266 if (leafName
.Equals(kCachesSQLiteFilename
) ||
267 leafName
.EqualsLiteral("caches.sqlite-wal")) {
269 const int64_t& fileSize
,
270 MOZ_TO_RESULT_INVOKE_MEMBER(file
, GetFileSize
));
271 MOZ_DIAGNOSTIC_ASSERT(fileSize
>= 0);
273 return UsageInfo
{DatabaseUsageType(Some(fileSize
))};
276 // Ignore directory padding file
277 if (leafName
.EqualsLiteral(PADDING_FILE_NAME
) ||
278 leafName
.EqualsLiteral(PADDING_TMP_FILE_NAME
)) {
282 NS_WARNING("Unknown Cache file found!");
286 case nsIFileKind::DoesNotExist
:
287 // Ignore files that got removed externally while iterating.
294 // FIXME: Separate file usage and database usage in OriginInfo so that the
295 // workaround for treating padding file size as database usage can be removed.
296 return UsageInfo
{DatabaseUsageType(Some(paddingSize
))} +
297 UsageInfo
{DatabaseUsageType(Some(totalDiskUsage
))} + innerUsageInfo
;
300 nsresult
CacheQuotaClient::InitOriginWithoutTracking(
301 PersistenceType aPersistenceType
, const OriginMetadata
& aOriginMetadata
,
302 const AtomicBool
& aCanceled
) {
303 AssertIsOnIOThread();
305 // This is called when a storage/permanent/${origin}/cache directory exists.
306 // Even though this shouldn't happen with a "good" profile, we shouldn't
307 // return an error here, since that would cause origin initialization to fail.
308 // We just warn and otherwise ignore that.
309 UNKNOWN_FILE_WARNING(NS_LITERAL_STRING_FROM_CSTRING(DOMCACHE_DIRECTORY_NAME
));
313 Result
<UsageInfo
, nsresult
> CacheQuotaClient::GetUsageForOrigin(
314 PersistenceType aPersistenceType
, const OriginMetadata
& aOriginMetadata
,
315 const AtomicBool
& aCanceled
) {
316 AssertIsOnIOThread();
318 // We can't open the database at this point, since it can be already used by
319 // the Cache IO thread. Use the cached value instead.
321 QuotaManager
* quotaManager
= QuotaManager::Get();
322 MOZ_ASSERT(quotaManager
);
324 return quotaManager
->GetUsageForClient(aOriginMetadata
.mPersistenceType
,
325 aOriginMetadata
, Client::DOMCACHE
);
328 void CacheQuotaClient::OnOriginClearCompleted(PersistenceType aPersistenceType
,
329 const nsACString
& aOrigin
) {
330 AssertIsOnIOThread();
332 if (aPersistenceType
== quota::PERSISTENCE_TYPE_PRIVATE
) {
333 if (auto entry
= mCipherKeyManagers
.Lookup(aOrigin
)) {
334 entry
.Data()->Invalidate();
340 void CacheQuotaClient::OnRepositoryClearCompleted(
341 PersistenceType aPersistenceType
) {
342 AssertIsOnIOThread();
344 if (aPersistenceType
== quota::PERSISTENCE_TYPE_PRIVATE
) {
345 for (const auto& cipherKeyManager
: mCipherKeyManagers
.Values()) {
346 cipherKeyManager
->Invalidate();
349 mCipherKeyManagers
.Clear();
353 void CacheQuotaClient::ReleaseIOThreadObjects() {
354 // Nothing to do here as the Context handles cleaning everything up
358 void CacheQuotaClient::AbortOperationsForLocks(
359 const DirectoryLockIdTable
& aDirectoryLockIds
) {
360 AssertIsOnBackgroundThread();
362 Manager::Abort(aDirectoryLockIds
);
365 void CacheQuotaClient::AbortOperationsForProcess(
366 ContentParentId aContentParentId
) {
367 // The Cache and Context can be shared by multiple client processes. They
368 // are not exclusively owned by a single process.
370 // As far as I can tell this is used by QuotaManager to abort operations
371 // when a particular process goes away. We definitely don't want this
372 // since we are shared. Also, the Cache actor code already properly
373 // handles asynchronous actor destruction when the child process dies.
375 // Therefore, do nothing here.
378 void CacheQuotaClient::AbortAllOperations() {
379 AssertIsOnBackgroundThread();
384 void CacheQuotaClient::StartIdleMaintenance() {}
386 void CacheQuotaClient::StopIdleMaintenance() {}
388 void CacheQuotaClient::InitiateShutdown() {
389 AssertIsOnBackgroundThread();
391 Manager::InitiateShutdown();
394 bool CacheQuotaClient::IsShutdownCompleted() const {
395 AssertIsOnBackgroundThread();
397 return Manager::IsShutdownAllComplete();
400 void CacheQuotaClient::ForceKillActors() {
401 // Currently we don't implement killing actors (are there any to kill here?).
404 nsCString
CacheQuotaClient::GetShutdownStatus() const {
405 AssertIsOnBackgroundThread();
407 return Manager::GetShutdownStatus();
410 void CacheQuotaClient::FinalizeShutdown() {
411 // Nothing to do here.
414 nsresult
CacheQuotaClient::UpgradeStorageFrom2_0To2_1(nsIFile
* aDirectory
) {
415 AssertIsOnIOThread();
416 MOZ_DIAGNOSTIC_ASSERT(aDirectory
);
418 QM_TRY(MOZ_TO_RESULT(DirectoryPaddingInit(*aDirectory
)));
423 nsresult
CacheQuotaClient::RestorePaddingFileInternal(
424 nsIFile
* aBaseDir
, mozIStorageConnection
* aConn
) {
425 MOZ_ASSERT(!NS_IsMainThread());
426 MOZ_DIAGNOSTIC_ASSERT(aBaseDir
);
427 MOZ_DIAGNOSTIC_ASSERT(aConn
);
429 QM_TRY_INSPECT(const int64_t& dummyPaddingSize
,
430 DirectoryPaddingRestore(*aBaseDir
, *aConn
,
431 /* aMustRestore */ true));
432 Unused
<< dummyPaddingSize
;
437 nsresult
CacheQuotaClient::WipePaddingFileInternal(
438 const CacheDirectoryMetadata
& aDirectoryMetadata
, nsIFile
* aBaseDir
) {
439 MOZ_ASSERT(!NS_IsMainThread());
440 MOZ_DIAGNOSTIC_ASSERT(aBaseDir
);
442 MOZ_ASSERT(DirectoryPaddingFileExists(*aBaseDir
, DirPaddingFile::FILE));
445 const int64_t& paddingSize
, ([&aBaseDir
]() -> Result
<int64_t, nsresult
> {
446 const bool temporaryPaddingFileExist
=
447 DirectoryPaddingFileExists(*aBaseDir
, DirPaddingFile::TMP_FILE
);
449 Maybe
<int64_t> directoryPaddingGetResult
;
450 if (!temporaryPaddingFileExist
) {
451 QM_TRY_UNWRAP(directoryPaddingGetResult
,
452 ([&aBaseDir
]() -> Result
<Maybe
<int64_t>, nsresult
> {
454 DirectoryPaddingGet(*aBaseDir
).map(Some
<int64_t>),
459 if (temporaryPaddingFileExist
|| !directoryPaddingGetResult
) {
460 // XXXtt: Maybe have a method in the QuotaManager to clean the usage
461 // under the quota client and the origin. There is nothing we can do
462 // to recover the file.
463 NS_WARNING("Cannnot read padding size from file!");
467 return *directoryPaddingGetResult
;
470 if (paddingSize
> 0) {
471 DecreaseUsageForDirectoryMetadata(aDirectoryMetadata
, paddingSize
);
474 QM_TRY(MOZ_TO_RESULT(
475 DirectoryPaddingDeleteFile(*aBaseDir
, DirPaddingFile::FILE)));
477 // Remove temporary file if we have one.
478 QM_TRY(MOZ_TO_RESULT(
479 DirectoryPaddingDeleteFile(*aBaseDir
, DirPaddingFile::TMP_FILE
)));
481 QM_TRY(MOZ_TO_RESULT(DirectoryPaddingInit(*aBaseDir
)));
486 RefPtr
<CipherKeyManager
> CacheQuotaClient::GetOrCreateCipherKeyManager(
487 const PrincipalMetadata
& aMetadata
) {
488 AssertIsOnIOThread();
490 auto privateOrigin
= aMetadata
.mIsPrivate
;
491 if (!privateOrigin
) {
495 const auto& origin
= aMetadata
.mOrigin
;
496 return mCipherKeyManagers
.LookupOrInsertWith(
497 origin
, [] { return new CipherKeyManager("CacheCipherKeyManager"); });
500 CacheQuotaClient::~CacheQuotaClient() {
501 AssertIsOnBackgroundThread();
502 MOZ_DIAGNOSTIC_ASSERT(sInstance
== this);
508 CacheQuotaClient
* CacheQuotaClient::sInstance
= nullptr;
511 already_AddRefed
<quota::Client
> CreateQuotaClient() {
512 AssertIsOnBackgroundThread();
514 RefPtr
<CacheQuotaClient
> ref
= new CacheQuotaClient();
519 nsresult
RestorePaddingFile(nsIFile
* aBaseDir
, mozIStorageConnection
* aConn
) {
520 MOZ_ASSERT(!NS_IsMainThread());
521 MOZ_DIAGNOSTIC_ASSERT(aBaseDir
);
522 MOZ_DIAGNOSTIC_ASSERT(aConn
);
524 RefPtr
<CacheQuotaClient
> cacheQuotaClient
= CacheQuotaClient::Get();
525 MOZ_DIAGNOSTIC_ASSERT(cacheQuotaClient
);
527 QM_TRY(MOZ_TO_RESULT(
528 cacheQuotaClient
->RestorePaddingFileInternal(aBaseDir
, aConn
)));
534 nsresult
WipePaddingFile(const CacheDirectoryMetadata
& aDirectoryMetadata
,
536 MOZ_ASSERT(!NS_IsMainThread());
537 MOZ_DIAGNOSTIC_ASSERT(aBaseDir
);
539 RefPtr
<CacheQuotaClient
> cacheQuotaClient
= CacheQuotaClient::Get();
540 MOZ_DIAGNOSTIC_ASSERT(cacheQuotaClient
);
542 QM_TRY(MOZ_TO_RESULT(
543 cacheQuotaClient
->WipePaddingFileInternal(aDirectoryMetadata
, aBaseDir
)));
548 } // namespace mozilla::dom::cache