blob: ab1ad88b955b25778de85013ef3b368997cf5b39 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/drive/file_cache_metadata.h"
#include "base/callback.h"
#include "base/file_util.h"
#include "base/files/file_enumerator.h"
#include "base/metrics/histogram.h"
#include "base/sequenced_task_runner.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/chromeos/drive/drive.pb.h"
#include "chrome/browser/chromeos/drive/file_cache.h"
#include "chrome/browser/chromeos/drive/file_system_util.h"
#include "third_party/leveldatabase/src/include/leveldb/db.h"
namespace drive {
namespace internal {
namespace {
typedef std::map<std::string, FileCacheEntry> CacheMap;
enum DBOpenStatus {
DB_OPEN_SUCCESS,
DB_OPEN_FAILURE_CORRUPTION,
DB_OPEN_FAILURE_OTHER,
DB_OPEN_FAILURE_UNRECOVERABLE,
DB_OPEN_MAX_VALUE,
};
// A map table of resource ID to file path.
typedef std::map<std::string, base::FilePath> ResourceIdToFilePathMap;
// Scans cache subdirectory and build or update |cache_map| with found files.
//
// The resource IDs and file paths of discovered files are collected as a
// ResourceIdToFilePathMap, if these are processed properly.
void ScanCacheDirectory(const std::vector<base::FilePath>& cache_paths,
FileCache::CacheSubDirectoryType sub_dir_type,
CacheMap* cache_map,
ResourceIdToFilePathMap* processed_file_map) {
DCHECK(cache_map);
DCHECK(processed_file_map);
base::FileEnumerator enumerator(cache_paths[sub_dir_type],
false, // not recursive
base::FileEnumerator::FILES,
util::kWildCard);
for (base::FilePath current = enumerator.Next(); !current.empty();
current = enumerator.Next()) {
// Extract resource_id and md5 from filename.
std::string resource_id;
std::string md5;
std::string extra_extension;
util::ParseCacheFilePath(current, &resource_id, &md5, &extra_extension);
// Determine cache state.
FileCacheEntry cache_entry;
cache_entry.set_md5(md5);
if (sub_dir_type == FileCache::CACHE_TYPE_PERSISTENT)
cache_entry.set_is_persistent(true);
if (extra_extension == util::kMountedArchiveFileExtension) {
// Mounted archives in cache should be unmounted upon logout/shutdown.
// But if we encounter a mounted file at start, delete it and create an
// entry with not PRESENT state.
DCHECK(sub_dir_type == FileCache::CACHE_TYPE_PERSISTENT);
file_util::Delete(current, false);
} else {
// The cache file is present.
cache_entry.set_is_present(true);
// Adds the dirty bit if |md5| indicates that the file is dirty, and
// the file is in the persistent directory.
if (md5 == util::kLocallyModifiedFileExtension) {
if (sub_dir_type == FileCache::CACHE_TYPE_PERSISTENT) {
cache_entry.set_is_dirty(true);
} else {
LOG(WARNING) << "Removing a dirty file in tmp directory: "
<< current.value();
file_util::Delete(current, false);
continue;
}
}
}
// Create and insert new entry into cache map.
cache_map->insert(std::make_pair(resource_id, cache_entry));
processed_file_map->insert(std::make_pair(resource_id, current));
}
}
void ScanCachePaths(const std::vector<base::FilePath>& cache_paths,
CacheMap* cache_map) {
DVLOG(1) << "Scanning directories";
// Scan cache persistent and tmp directories to enumerate all files and create
// corresponding entries for cache map.
ResourceIdToFilePathMap persistent_file_map;
ScanCacheDirectory(cache_paths,
FileCache::CACHE_TYPE_PERSISTENT,
cache_map,
&persistent_file_map);
ResourceIdToFilePathMap tmp_file_map;
ScanCacheDirectory(cache_paths,
FileCache::CACHE_TYPE_TMP,
cache_map,
&tmp_file_map);
// On DB corruption, keep only dirty-and-committed files in persistent
// directory. Other files are deleted or moved to temporary directory.
for (ResourceIdToFilePathMap::const_iterator iter =
persistent_file_map.begin();
iter != persistent_file_map.end(); ++iter) {
const std::string& resource_id = iter->first;
const base::FilePath& file_path = iter->second;
CacheMap::iterator cache_map_iter = cache_map->find(resource_id);
if (cache_map_iter != cache_map->end()) {
FileCacheEntry* cache_entry = &cache_map_iter->second;
const bool is_dirty = cache_entry->is_dirty();
if (!is_dirty) {
// If the file is not dirty, move to temporary directory.
base::FilePath new_file_path =
cache_paths[FileCache::CACHE_TYPE_TMP].Append(
file_path.BaseName());
DLOG(WARNING) << "Moving: " << file_path.value()
<< " to: " << new_file_path.value();
file_util::Move(file_path, new_file_path);
cache_entry->set_is_persistent(false);
}
}
}
DVLOG(1) << "Directory scan finished";
}
// Returns true if |md5| matches the one in |cache_entry| with some
// exceptions. See the function definition for details.
bool CheckIfMd5Matches(const std::string& md5,
const FileCacheEntry& cache_entry) {
if (cache_entry.is_dirty()) {
// If the entry is dirty, its MD5 may have been replaced by "local"
// during cache initialization, so we don't compare MD5.
return true;
} else if (cache_entry.is_pinned() && cache_entry.md5().empty()) {
// If the entry is pinned, it's ok for the entry to have an empty
// MD5. This can happen if the pinned file is not fetched. MD5 for pinned
// files are collected from files in "persistent" directory, but the
// persistent files do not exist if these are not fetched yet.
return true;
} else if (md5.empty()) {
// If the MD5 matching is not requested, don't check MD5.
return true;
} else if (md5 == cache_entry.md5()) {
// Otherwise, compare the MD5.
return true;
}
return false;
}
} // namespace
// static
const base::FilePath::CharType* FileCacheMetadata::kCacheMetadataDBPath =
FILE_PATH_LITERAL("cache_metadata.db");
FileCacheMetadata::Iterator::Iterator(scoped_ptr<leveldb::Iterator> it)
: it_(it.Pass()) {
base::ThreadRestrictions::AssertIOAllowed();
DCHECK(it_);
it_->SeekToFirst();
AdvanceInternal();
}
FileCacheMetadata::Iterator::~Iterator() {
base::ThreadRestrictions::AssertIOAllowed();
}
bool FileCacheMetadata::Iterator::IsAtEnd() const {
base::ThreadRestrictions::AssertIOAllowed();
return !it_->Valid();
}
std::string FileCacheMetadata::Iterator::GetKey() const {
base::ThreadRestrictions::AssertIOAllowed();
DCHECK(!IsAtEnd());
return it_->key().ToString();
}
const FileCacheEntry& FileCacheMetadata::Iterator::GetValue() const {
base::ThreadRestrictions::AssertIOAllowed();
DCHECK(!IsAtEnd());
return entry_;
}
void FileCacheMetadata::Iterator::Advance() {
base::ThreadRestrictions::AssertIOAllowed();
DCHECK(!IsAtEnd());
it_->Next();
AdvanceInternal();
}
bool FileCacheMetadata::Iterator::HasError() const {
base::ThreadRestrictions::AssertIOAllowed();
return !it_->status().ok();
}
void FileCacheMetadata::Iterator::AdvanceInternal() {
for (; it_->Valid(); it_->Next()) {
// Skip unparsable broken entries.
// TODO(hashimoto): Broken entries should be cleaned up at some point.
if (entry_.ParseFromArray(it_->value().data(), it_->value().size()))
break;
}
}
FileCacheMetadata::FileCacheMetadata(
base::SequencedTaskRunner* blocking_task_runner)
: blocking_task_runner_(blocking_task_runner) {
AssertOnSequencedWorkerPool();
}
FileCacheMetadata::~FileCacheMetadata() {
AssertOnSequencedWorkerPool();
}
bool FileCacheMetadata::Initialize(
const std::vector<base::FilePath>& cache_paths) {
AssertOnSequencedWorkerPool();
const base::FilePath db_path =
cache_paths[FileCache::CACHE_TYPE_META].Append(kCacheMetadataDBPath);
DVLOG(1) << "db path=" << db_path.value();
bool scan_cache = !file_util::PathExists(db_path);
leveldb::DB* level_db = NULL;
leveldb::Options options;
options.create_if_missing = true;
leveldb::Status db_status = leveldb::DB::Open(options, db_path.AsUTF8Unsafe(),
&level_db);
// Delete the db and scan the physical cache. This will fix a corrupt db, but
// perhaps not other causes of failed DB::Open.
DBOpenStatus uma_status = DB_OPEN_SUCCESS;
if (!db_status.ok()) {
LOG(WARNING) << "Cache db failed to open: " << db_status.ToString();
uma_status = db_status.IsCorruption() ?
DB_OPEN_FAILURE_CORRUPTION : DB_OPEN_FAILURE_OTHER;
const bool deleted = file_util::Delete(db_path, true);
DCHECK(deleted);
db_status = leveldb::DB::Open(options, db_path.value(), &level_db);
if (!db_status.ok()) {
LOG(WARNING) << "Still failed to open: " << db_status.ToString();
UMA_HISTOGRAM_ENUMERATION("Drive.CacheDBOpenStatus",
DB_OPEN_FAILURE_UNRECOVERABLE,
DB_OPEN_MAX_VALUE);
// Failed to open the cache metadata DB. Drive will be disabled.
return false;
}
scan_cache = true;
}
UMA_HISTOGRAM_ENUMERATION("Drive.CacheDBOpenStatus", uma_status,
DB_OPEN_MAX_VALUE);
DCHECK(level_db);
level_db_.reset(level_db);
// We scan the cache directories to initialize the cache database if we
// were previously using the cache map.
if (scan_cache) {
CacheMap cache_map;
ScanCachePaths(cache_paths, &cache_map);
for (CacheMap::const_iterator it = cache_map.begin();
it != cache_map.end(); ++it) {
AddOrUpdateCacheEntry(it->first, it->second);
}
}
return true;
}
void FileCacheMetadata::AddOrUpdateCacheEntry(
const std::string& resource_id,
const FileCacheEntry& cache_entry) {
AssertOnSequencedWorkerPool();
DVLOG(1) << "AddOrUpdateCacheEntry, resource_id=" << resource_id;
std::string serialized;
const bool ok = cache_entry.SerializeToString(&serialized);
if (ok)
level_db_->Put(leveldb::WriteOptions(),
leveldb::Slice(resource_id),
leveldb::Slice(serialized));
}
void FileCacheMetadata::RemoveCacheEntry(const std::string& resource_id) {
AssertOnSequencedWorkerPool();
DVLOG(1) << "RemoveCacheEntry, resource_id=" << resource_id;
level_db_->Delete(leveldb::WriteOptions(), leveldb::Slice(resource_id));
}
bool FileCacheMetadata::GetCacheEntry(const std::string& resource_id,
const std::string& md5,
FileCacheEntry* entry) {
DCHECK(entry);
AssertOnSequencedWorkerPool();
std::string serialized;
const leveldb::Status status = level_db_->Get(
leveldb::ReadOptions(),
leveldb::Slice(resource_id), &serialized);
if (!status.ok()) {
DVLOG(1) << "Can't find " << resource_id << " in cache db";
return false;
}
FileCacheEntry cache_entry;
const bool ok = cache_entry.ParseFromString(serialized);
if (!ok) {
LOG(ERROR) << "Failed to parse " << serialized;
return false;
}
if (!CheckIfMd5Matches(md5, cache_entry)) {
return false;
}
*entry = cache_entry;
return true;
}
scoped_ptr<FileCacheMetadata::Iterator> FileCacheMetadata::GetIterator() {
AssertOnSequencedWorkerPool();
scoped_ptr<leveldb::Iterator> iter(level_db_->NewIterator(
leveldb::ReadOptions()));
return make_scoped_ptr(new Iterator(iter.Pass()));
}
void FileCacheMetadata::AssertOnSequencedWorkerPool() {
DCHECK(!blocking_task_runner_ ||
blocking_task_runner_->RunsTasksOnCurrentThread());
}
} // namespace internal
} // namespace drive