1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/extensions/updater/local_extension_cache.h"
8 #include "base/files/file_enumerator.h"
9 #include "base/files/file_util.h"
10 #include "base/sequenced_task_runner.h"
11 #include "base/strings/string_util.h"
12 #include "base/sys_info.h"
13 #include "base/version.h"
14 #include "components/crx_file/id_util.h"
15 #include "content/public/browser/browser_thread.h"
17 namespace extensions
{
20 // File name extension for CRX files (not case sensitive).
21 const char kCRXFileExtension
[] = ".crx";
23 // Delay between checks for flag file presence when waiting for the cache to
25 const int64_t kCacheStatusPollingDelayMs
= 1000;
29 const char LocalExtensionCache::kCacheReadyFlagFileName
[] = ".initialized";
31 LocalExtensionCache::LocalExtensionCache(
32 const base::FilePath
& cache_dir
,
33 uint64 max_cache_size
,
34 const base::TimeDelta
& max_cache_age
,
35 const scoped_refptr
<base::SequencedTaskRunner
>& backend_task_runner
)
36 : cache_dir_(cache_dir
),
37 max_cache_size_(max_cache_size
),
38 min_cache_age_(base::Time::Now() - max_cache_age
),
39 backend_task_runner_(backend_task_runner
),
40 state_(kUninitialized
),
41 weak_ptr_factory_(this),
42 cache_status_polling_delay_(
43 base::TimeDelta::FromMilliseconds(kCacheStatusPollingDelayMs
)) {
46 LocalExtensionCache::~LocalExtensionCache() {
51 void LocalExtensionCache::Init(bool wait_for_cache_initialization
,
52 const base::Closure
& callback
) {
53 DCHECK_EQ(state_
, kUninitialized
);
55 state_
= kWaitInitialization
;
56 if (wait_for_cache_initialization
)
57 CheckCacheStatus(callback
);
59 CheckCacheContents(callback
);
62 void LocalExtensionCache::Shutdown(const base::Closure
& callback
) {
63 DCHECK_NE(state_
, kShutdown
);
67 backend_task_runner_
->PostTaskAndReply(FROM_HERE
,
68 base::Bind(&base::DoNothing
), callback
);
71 bool LocalExtensionCache::GetExtension(const std::string
& id
,
72 base::FilePath
* file_path
,
73 std::string
* version
) {
77 CacheMap::iterator it
= cached_extensions_
.find(id
);
78 if (it
== cached_extensions_
.end())
82 *file_path
= it
->second
.file_path
;
84 // If caller is not interesting in file_path, extension is not used.
85 base::Time now
= base::Time::Now();
86 backend_task_runner_
->PostTask(FROM_HERE
,
87 base::Bind(&LocalExtensionCache::BackendMarkFileUsed
,
88 it
->second
.file_path
, now
));
89 it
->second
.last_used
= now
;
93 *version
= it
->second
.version
;
98 void LocalExtensionCache::PutExtension(const std::string
& id
,
99 const base::FilePath
& file_path
,
100 const std::string
& version
,
101 const PutExtensionCallback
& callback
) {
102 if (state_
!= kReady
) {
103 callback
.Run(file_path
, true);
107 Version
version_validator(version
);
108 if (!version_validator
.IsValid()) {
109 LOG(ERROR
) << "Extension " << id
<< " has bad version " << version
;
110 callback
.Run(file_path
, true);
114 CacheMap::iterator it
= cached_extensions_
.find(id
);
115 if (it
!= cached_extensions_
.end()) {
116 Version
new_version(version
);
117 Version
prev_version(it
->second
.version
);
118 if (new_version
.CompareTo(prev_version
) <= 0) {
119 LOG(WARNING
) << "Cache contains newer or the same version "
120 << prev_version
.GetString() << " for extension "
121 << id
<< " version " << version
;
122 callback
.Run(file_path
, true);
127 backend_task_runner_
->PostTask(
129 base::Bind(&LocalExtensionCache::BackendInstallCacheEntry
,
130 weak_ptr_factory_
.GetWeakPtr(),
138 bool LocalExtensionCache::RemoveExtension(const std::string
& id
) {
139 if (state_
!= kReady
)
142 CacheMap::iterator it
= cached_extensions_
.find(id
);
143 if (it
== cached_extensions_
.end())
146 backend_task_runner_
->PostTask(
149 &LocalExtensionCache::BackendRemoveCacheEntry
, cache_dir_
, id
));
151 cached_extensions_
.erase(it
);
155 bool LocalExtensionCache::GetStatistics(uint64
* cache_size
,
156 size_t* extensions_count
) {
157 if (state_
!= kReady
)
161 for (CacheMap::iterator it
= cached_extensions_
.begin();
162 it
!= cached_extensions_
.end(); ++it
) {
163 *cache_size
+= it
->second
.size
;
165 *extensions_count
= cached_extensions_
.size();
170 void LocalExtensionCache::SetCacheStatusPollingDelayForTests(
171 const base::TimeDelta
& delay
) {
172 cache_status_polling_delay_
= delay
;
175 void LocalExtensionCache::CheckCacheStatus(const base::Closure
& callback
) {
176 if (state_
== kShutdown
) {
181 backend_task_runner_
->PostTask(
183 base::Bind(&LocalExtensionCache::BackendCheckCacheStatus
,
184 weak_ptr_factory_
.GetWeakPtr(),
190 void LocalExtensionCache::BackendCheckCacheStatus(
191 base::WeakPtr
<LocalExtensionCache
> local_cache
,
192 const base::FilePath
& cache_dir
,
193 const base::Closure
& callback
) {
195 base::PathExists(cache_dir
.AppendASCII(kCacheReadyFlagFileName
));
197 static bool first_check
= true;
198 if (first_check
&& !exists
&& !base::SysInfo::IsRunningOnChromeOS()) {
199 LOG(WARNING
) << "Extensions will not be installed from update URLs until "
200 << cache_dir
.AppendASCII(kCacheReadyFlagFileName
).value()
205 content::BrowserThread::PostTask(
206 content::BrowserThread::UI
,
208 base::Bind(&LocalExtensionCache::OnCacheStatusChecked
,
214 void LocalExtensionCache::OnCacheStatusChecked(bool ready
,
215 const base::Closure
& callback
) {
216 if (state_
== kShutdown
) {
222 CheckCacheContents(callback
);
224 content::BrowserThread::PostDelayedTask(
225 content::BrowserThread::UI
,
227 base::Bind(&LocalExtensionCache::CheckCacheStatus
,
228 weak_ptr_factory_
.GetWeakPtr(),
230 cache_status_polling_delay_
);
234 void LocalExtensionCache::CheckCacheContents(const base::Closure
& callback
) {
235 DCHECK_EQ(state_
, kWaitInitialization
);
236 backend_task_runner_
->PostTask(
238 base::Bind(&LocalExtensionCache::BackendCheckCacheContents
,
239 weak_ptr_factory_
.GetWeakPtr(),
245 void LocalExtensionCache::BackendCheckCacheContents(
246 base::WeakPtr
<LocalExtensionCache
> local_cache
,
247 const base::FilePath
& cache_dir
,
248 const base::Closure
& callback
) {
249 scoped_ptr
<CacheMap
> cache_content(new CacheMap
);
250 BackendCheckCacheContentsInternal(cache_dir
, cache_content
.get());
251 content::BrowserThread::PostTask(
252 content::BrowserThread::UI
,
254 base::Bind(&LocalExtensionCache::OnCacheContentsChecked
,
256 base::Passed(&cache_content
),
261 void LocalExtensionCache::BackendCheckCacheContentsInternal(
262 const base::FilePath
& cache_dir
,
263 CacheMap
* cache_content
) {
264 // Start by verifying that the cache_dir exists.
265 if (!base::DirectoryExists(cache_dir
)) {
267 if (!base::CreateDirectory(cache_dir
)) {
268 LOG(ERROR
) << "Failed to create cache directory at "
269 << cache_dir
.value();
272 // Nothing else to do. Cache is empty.
276 // Enumerate all the files in the cache |cache_dir|, including directories
277 // and symlinks. Each unrecognized file will be erased.
278 int types
= base::FileEnumerator::FILES
| base::FileEnumerator::DIRECTORIES
;
279 base::FileEnumerator
enumerator(cache_dir
, false /* recursive */, types
);
280 for (base::FilePath path
= enumerator
.Next();
281 !path
.empty(); path
= enumerator
.Next()) {
282 base::FileEnumerator::FileInfo info
= enumerator
.GetInfo();
283 std::string basename
= path
.BaseName().value();
285 if (info
.IsDirectory() || base::IsLink(info
.GetName())) {
286 LOG(ERROR
) << "Erasing bad file in cache directory: " << basename
;
287 base::DeleteFile(path
, true /* recursive */);
291 // Skip flag file that indicates that cache is ready.
292 if (basename
== kCacheReadyFlagFileName
)
295 // crx files in the cache are named <extension-id>-<version>.crx.
298 if (EndsWith(basename
, kCRXFileExtension
, false /* case-sensitive */)) {
299 size_t n
= basename
.find('-');
300 if (n
!= std::string::npos
&& n
+ 1 < basename
.size() - 4) {
301 id
= basename
.substr(0, n
);
302 // Size of |version| = total size - "<id>" - "-" - ".crx"
303 version
= basename
.substr(n
+ 1, basename
.size() - 5 - id
.size());
307 // Enforce a lower-case id.
308 id
= base::StringToLowerASCII(id
);
309 if (!crx_file::id_util::IdIsValid(id
)) {
310 LOG(ERROR
) << "Bad extension id in cache: " << id
;
314 if (!Version(version
).IsValid()) {
315 LOG(ERROR
) << "Bad extension version in cache: " << version
;
319 if (id
.empty() || version
.empty()) {
320 LOG(ERROR
) << "Invalid file in cache, erasing: " << basename
;
321 base::DeleteFile(path
, true /* recursive */);
325 VLOG(1) << "Found cached version " << version
326 << " for extension id " << id
;
328 CacheMap::iterator it
= cache_content
->find(id
);
329 if (it
!= cache_content
->end()) {
330 // |cache_content| already has version for this ID. Removed older one.
331 Version
curr_version(version
);
332 Version
prev_version(it
->second
.version
);
333 if (prev_version
.CompareTo(curr_version
) <= 0) {
334 base::DeleteFile(base::FilePath(it
->second
.file_path
),
335 true /* recursive */);
336 cache_content
->erase(id
);
337 VLOG(1) << "Remove older version " << it
->second
.version
338 << " for extension id " << id
;
340 base::DeleteFile(path
, true /* recursive */);
341 VLOG(1) << "Remove older version " << version
342 << " for extension id " << id
;
347 cache_content
->insert(std::make_pair(id
, CacheItemInfo(
348 version
, info
.GetLastModifiedTime(), info
.GetSize(), path
)));
352 void LocalExtensionCache::OnCacheContentsChecked(
353 scoped_ptr
<CacheMap
> cache_content
,
354 const base::Closure
& callback
) {
355 cache_content
->swap(cached_extensions_
);
361 void LocalExtensionCache::BackendMarkFileUsed(const base::FilePath
& file_path
,
362 const base::Time
& time
) {
363 base::TouchFile(file_path
, time
, time
);
367 void LocalExtensionCache::BackendInstallCacheEntry(
368 base::WeakPtr
<LocalExtensionCache
> local_cache
,
369 const base::FilePath
& cache_dir
,
370 const std::string
& id
,
371 const base::FilePath
& file_path
,
372 const std::string
& version
,
373 const PutExtensionCallback
& callback
) {
374 std::string basename
= id
+ "-" + version
+ kCRXFileExtension
;
375 base::FilePath cached_crx_path
= cache_dir
.AppendASCII(basename
);
377 bool was_error
= false;
378 if (base::PathExists(cached_crx_path
)) {
379 LOG(ERROR
) << "File already exists " << file_path
.value();
380 cached_crx_path
= file_path
;
384 base::File::Info info
;
386 if (!base::Move(file_path
, cached_crx_path
)) {
387 LOG(ERROR
) << "Failed to copy from " << file_path
.value()
388 << " to " << cached_crx_path
.value();
389 cached_crx_path
= file_path
;
392 was_error
= !base::GetFileInfo(cached_crx_path
, &info
);
393 VLOG(1) << "Cache entry installed for extension id " << id
394 << " version " << version
;
398 content::BrowserThread::PostTask(
399 content::BrowserThread::UI
,
401 base::Bind(&LocalExtensionCache::OnCacheEntryInstalled
,
404 CacheItemInfo(version
, info
.last_modified
,
405 info
.size
, cached_crx_path
),
410 void LocalExtensionCache::OnCacheEntryInstalled(
411 const std::string
& id
,
412 const CacheItemInfo
& info
,
414 const PutExtensionCallback
& callback
) {
415 if (state_
== kShutdown
|| was_error
) {
416 callback
.Run(info
.file_path
, true);
420 CacheMap::iterator it
= cached_extensions_
.find(id
);
421 if (it
!= cached_extensions_
.end()) {
422 Version
new_version(info
.version
);
423 Version
prev_version(it
->second
.version
);
424 if (new_version
.CompareTo(prev_version
) <= 0) {
425 DCHECK(0) << "Cache contains newer or the same version";
426 callback
.Run(info
.file_path
, true);
431 it
= cached_extensions_
.insert(std::make_pair(id
, info
)).first
;
433 // Time from file system can have lower precision so use precise "now".
434 it
->second
.last_used
= base::Time::Now();
436 callback
.Run(info
.file_path
, false);
440 void LocalExtensionCache::BackendRemoveCacheEntry(
441 const base::FilePath
& cache_dir
,
442 const std::string
& id
) {
443 std::string file_pattern
= id
+ "-*" + kCRXFileExtension
;
444 base::FileEnumerator
enumerator(cache_dir
,
445 false /* not recursive */,
446 base::FileEnumerator::FILES
,
448 for (base::FilePath path
= enumerator
.Next(); !path
.empty();
449 path
= enumerator
.Next()) {
450 base::DeleteFile(path
, false);
451 VLOG(1) << "Removed cached file " << path
.value();
456 bool LocalExtensionCache::CompareCacheItemsAge(const CacheMap::iterator
& lhs
,
457 const CacheMap::iterator
& rhs
) {
458 return lhs
->second
.last_used
< rhs
->second
.last_used
;
461 void LocalExtensionCache::CleanUp() {
462 DCHECK_EQ(state_
, kReady
);
464 std::vector
<CacheMap::iterator
> items
;
465 items
.reserve(cached_extensions_
.size());
466 uint64_t total_size
= 0;
467 for (CacheMap::iterator it
= cached_extensions_
.begin();
468 it
!= cached_extensions_
.end(); ++it
) {
470 total_size
+= it
->second
.size
;
472 std::sort(items
.begin(), items
.end(), CompareCacheItemsAge
);
474 for (std::vector
<CacheMap::iterator
>::iterator it
= items
.begin();
475 it
!= items
.end(); ++it
) {
476 if ((*it
)->second
.last_used
< min_cache_age_
||
477 (max_cache_size_
&& total_size
> max_cache_size_
)) {
478 total_size
-= (*it
)->second
.size
;
479 VLOG(1) << "Clean up cached extension id " << (*it
)->first
;
480 RemoveExtension((*it
)->first
);
485 LocalExtensionCache::CacheItemInfo::CacheItemInfo(
486 const std::string
& version
,
487 const base::Time
& last_used
,
489 const base::FilePath
& file_path
)
490 : version(version
), last_used(last_used
), size(size
), file_path(file_path
) {
493 } // namespace extensions