| // 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/gdata/gdata_cache_metadata.h" |
| |
| #include "base/file_util.h" |
| #include "chrome/browser/chromeos/gdata/gdata_util.h" |
| |
| namespace gdata { |
| |
| namespace { |
| |
| // Returns true if |file_path| is a valid symbolic link as |sub_dir_type|. |
| // Otherwise, returns false with the reason. |
| bool IsValidSymbolicLink(const FilePath& file_path, |
| GDataCache::CacheSubDirectoryType sub_dir_type, |
| const std::vector<FilePath>& cache_paths, |
| std::string* reason) { |
| DCHECK(sub_dir_type == GDataCache::CACHE_TYPE_PINNED || |
| sub_dir_type == GDataCache::CACHE_TYPE_OUTGOING); |
| |
| FilePath destination; |
| if (!file_util::ReadSymbolicLink(file_path, &destination)) { |
| *reason = "failed to read the symlink (maybe not a symlink)"; |
| return false; |
| } |
| |
| if (!file_util::PathExists(destination)) { |
| *reason = "pointing to a non-existent file"; |
| return false; |
| } |
| |
| // pinned-but-not-fetched files are symlinks to kSymLinkToDevNull. |
| if (sub_dir_type == GDataCache::CACHE_TYPE_PINNED && |
| destination == FilePath::FromUTF8Unsafe(util::kSymLinkToDevNull)) { |
| return true; |
| } |
| |
| // The destination file should be in the persistent directory. |
| if (!cache_paths[GDataCache::CACHE_TYPE_PERSISTENT].IsParent(destination)) { |
| *reason = "pointing to a file outside of persistent directory"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Remove invalid files from persistent directory. |
| // |
| // 1) dirty-but-not-committed files. The dirty files should be committed |
| // (i.e. symlinks created in 'outgoing' directory) before shutdown, but the |
| // symlinks may not be created if the system shuts down unexpectedly. |
| // |
| // 2) neither dirty nor pinned. Files in the persistent directory should be |
| // in the either of the states. |
| void RemoveInvalidFilesFromPersistentDirectory( |
| const GDataCacheMetadataMap::ResourceIdToFilePathMap& persistent_file_map, |
| const GDataCacheMetadataMap::ResourceIdToFilePathMap& outgoing_file_map, |
| GDataCacheMetadataMap::CacheMap* cache_map) { |
| for (GDataCacheMetadataMap::ResourceIdToFilePathMap::const_iterator iter = |
| persistent_file_map.begin(); |
| iter != persistent_file_map.end(); ++iter) { |
| const std::string& resource_id = iter->first; |
| const FilePath& file_path = iter->second; |
| |
| GDataCacheMetadataMap::CacheMap::iterator cache_map_iter = |
| cache_map->find(resource_id); |
| if (cache_map_iter != cache_map->end()) { |
| const GDataCacheEntry& cache_entry = cache_map_iter->second; |
| // If the file is dirty but not committed, remove it. |
| if (cache_entry.IsDirty() && outgoing_file_map.count(resource_id) == 0) { |
| LOG(WARNING) << "Removing dirty-but-not-committed file: " |
| << file_path.value(); |
| file_util::Delete(file_path, false); |
| cache_map->erase(cache_map_iter); |
| } else if (!cache_entry.IsDirty() && !cache_entry.IsPinned()) { |
| // If the file is neither dirty nor pinned, remove it. |
| LOG(WARNING) << "Removing persistent-but-dangling file: " |
| << file_path.value(); |
| file_util::Delete(file_path, false); |
| cache_map->erase(cache_map_iter); |
| } |
| } |
| } |
| } |
| |
| } // namespace |
| |
| GDataCacheMetadata::GDataCacheMetadata( |
| base::SequencedWorkerPool* pool, |
| const base::SequencedWorkerPool::SequenceToken& sequence_token) |
| : pool_(pool), |
| sequence_token_(sequence_token) { |
| AssertOnSequencedWorkerPool(); |
| } |
| |
| GDataCacheMetadata::~GDataCacheMetadata() { |
| AssertOnSequencedWorkerPool(); |
| } |
| |
| void GDataCacheMetadata::AssertOnSequencedWorkerPool() { |
| DCHECK(!pool_ || pool_->IsRunningSequenceOnCurrentThread(sequence_token_)); |
| } |
| |
| GDataCacheMetadataMap::GDataCacheMetadataMap( |
| base::SequencedWorkerPool* pool, |
| const base::SequencedWorkerPool::SequenceToken& sequence_token) |
| : GDataCacheMetadata(pool, sequence_token) { |
| AssertOnSequencedWorkerPool(); |
| } |
| |
| GDataCacheMetadataMap::~GDataCacheMetadataMap() { |
| AssertOnSequencedWorkerPool(); |
| } |
| |
| |
| void GDataCacheMetadataMap::Initialize( |
| const std::vector<FilePath>& cache_paths) { |
| AssertOnSequencedWorkerPool(); |
| |
| if (cache_paths.size() < GDataCache::NUM_CACHE_TYPES) { |
| LOG(ERROR) << "Size of cache_paths is invalid."; |
| return; |
| } |
| |
| if (!GDataCache::CreateCacheDirectories(cache_paths)) |
| return; |
| |
| // Change permissions of cache persistent directory to u+rwx,og+x in order to |
| // allow archive files in that directory to be mounted by cros-disks. |
| if (!file_util::SetPosixFilePermissions( |
| cache_paths[GDataCache::CACHE_TYPE_PERSISTENT], |
| S_IRWXU | S_IXGRP | S_IXOTH)) |
| return; |
| |
| 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, |
| GDataCache::CACHE_TYPE_PERSISTENT, |
| &cache_map_, |
| &persistent_file_map); |
| ResourceIdToFilePathMap tmp_file_map; |
| ScanCacheDirectory(cache_paths, |
| GDataCache::CACHE_TYPE_TMP, |
| &cache_map_, |
| &tmp_file_map); |
| |
| // Then scan pinned directory to update existing entries in cache map, or |
| // create new ones for pinned symlinks to /dev/null which target nothing. |
| // |
| // Pinned directory should be scanned after the persistent directory as |
| // we'll add PINNED states to the existing files in the persistent |
| // directory per the contents of the pinned directory. |
| ResourceIdToFilePathMap pinned_file_map; |
| ScanCacheDirectory(cache_paths, |
| GDataCache::CACHE_TYPE_PINNED, |
| &cache_map_, |
| &pinned_file_map); |
| // Then scan outgoing directory to check if dirty-files are committed |
| // properly (i.e. symlinks created in outgoing directory). |
| ResourceIdToFilePathMap outgoing_file_map; |
| ScanCacheDirectory(cache_paths, |
| GDataCache::CACHE_TYPE_OUTGOING, |
| &cache_map_, |
| &outgoing_file_map); |
| |
| RemoveInvalidFilesFromPersistentDirectory(persistent_file_map, |
| outgoing_file_map, |
| &cache_map_); |
| DVLOG(1) << "Directory scan finished"; |
| } |
| |
| void GDataCacheMetadataMap::UpdateCache( |
| const std::string& resource_id, |
| const GDataCacheEntry& cache_entry) { |
| AssertOnSequencedWorkerPool(); |
| |
| CacheMap::iterator iter = cache_map_.find(resource_id); |
| if (iter == cache_map_.end()) { // New resource, create new entry. |
| // Makes no sense to create new entry if cache state is NONE. |
| DCHECK(cache_entry.cache_state() != CACHE_STATE_NONE); |
| if (cache_entry.cache_state() != CACHE_STATE_NONE) { |
| cache_map_.insert(std::make_pair(resource_id, cache_entry)); |
| DVLOG(1) << "Added res_id=" << resource_id |
| << ", " << cache_entry.ToString(); |
| } |
| } else { // Resource exists. |
| // If cache state is NONE, delete entry from cache map. |
| if (cache_entry.cache_state() == CACHE_STATE_NONE) { |
| DVLOG(1) << "Deleting res_id=" << resource_id |
| << ", " << iter->second.ToString(); |
| cache_map_.erase(iter); |
| } else { // Otherwise, update entry in cache map. |
| iter->second.set_md5(cache_entry.md5()); |
| iter->second.set_cache_state(cache_entry.cache_state()); |
| DVLOG(1) << "Updated res_id=" << resource_id |
| << ", " << iter->second.ToString(); |
| } |
| } |
| } |
| |
| void GDataCacheMetadataMap::RemoveFromCache(const std::string& resource_id) { |
| AssertOnSequencedWorkerPool(); |
| |
| CacheMap::iterator iter = cache_map_.find(resource_id); |
| if (iter != cache_map_.end()) { |
| // Delete the CacheEntry and remove it from the map. |
| cache_map_.erase(iter); |
| } |
| } |
| |
| scoped_ptr<GDataCacheEntry> GDataCacheMetadataMap::GetCacheEntry( |
| const std::string& resource_id, |
| const std::string& md5) { |
| AssertOnSequencedWorkerPool(); |
| |
| CacheMap::iterator iter = cache_map_.find(resource_id); |
| if (iter == cache_map_.end()) { |
| DVLOG(1) << "Can't find " << resource_id << " in cache map"; |
| return scoped_ptr<GDataCacheEntry>(); |
| } |
| |
| scoped_ptr<GDataCacheEntry> cache_entry( |
| new GDataCacheEntry(iter->second)); |
| |
| if (!CheckIfMd5Matches(md5, *cache_entry)) { |
| DVLOG(1) << "Non-matching md5: want=" << md5 |
| << ", found=[res_id=" << resource_id |
| << ", " << cache_entry->ToString() |
| << "]"; |
| return scoped_ptr<GDataCacheEntry>(); |
| } |
| |
| DVLOG(1) << "Found entry for res_id=" << resource_id |
| << ", " << cache_entry->ToString(); |
| |
| return cache_entry.Pass(); |
| } |
| |
| void GDataCacheMetadataMap::RemoveTemporaryFiles() { |
| AssertOnSequencedWorkerPool(); |
| |
| CacheMap::iterator iter = cache_map_.begin(); |
| while (iter != cache_map_.end()) { |
| if (!iter->second.IsPersistent()) { |
| // Post-increment the iterator to avoid iterator invalidation. |
| cache_map_.erase(iter++); |
| } else { |
| ++iter; |
| } |
| } |
| } |
| |
| void GDataCacheMetadataMap::Iterate(const IterateCallback& callback) { |
| AssertOnSequencedWorkerPool(); |
| |
| for (CacheMap::const_iterator iter = cache_map_.begin(); |
| iter != cache_map_.end(); ++iter) { |
| callback.Run(iter->first, iter->second); |
| } |
| } |
| |
| // static |
| void GDataCacheMetadataMap::ScanCacheDirectory( |
| const std::vector<FilePath>& cache_paths, |
| GDataCache::CacheSubDirectoryType sub_dir_type, |
| CacheMap* cache_map, |
| ResourceIdToFilePathMap* processed_file_map) { |
| DCHECK(cache_map); |
| DCHECK(processed_file_map); |
| |
| file_util::FileEnumerator enumerator( |
| cache_paths[sub_dir_type], |
| false, // not recursive |
| static_cast<file_util::FileEnumerator::FileType>( |
| file_util::FileEnumerator::FILES | |
| file_util::FileEnumerator::SHOW_SYM_LINKS), |
| util::kWildCard); |
| for (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. |
| GDataCacheEntry cache_entry(md5, CACHE_STATE_NONE); |
| // If we're scanning pinned directory and if entry already exists, just |
| // update its pinned state. |
| if (sub_dir_type == GDataCache::CACHE_TYPE_PINNED) { |
| std::string reason; |
| if (!IsValidSymbolicLink(current, sub_dir_type, cache_paths, &reason)) { |
| LOG(WARNING) << "Removing an invalid symlink: " << current.value() |
| << ": " << reason; |
| file_util::Delete(current, false); |
| continue; |
| } |
| |
| CacheMap::iterator iter = cache_map->find(resource_id); |
| if (iter != cache_map->end()) { // Entry exists, update pinned state. |
| iter->second.SetPinned(true); |
| |
| processed_file_map->insert(std::make_pair(resource_id, current)); |
| continue; |
| } |
| // Entry doesn't exist, this is a special symlink that refers to |
| // /dev/null; follow through to create an entry with the PINNED but not |
| // PRESENT state. |
| cache_entry.SetPinned(true); |
| } else if (sub_dir_type == GDataCache::CACHE_TYPE_OUTGOING) { |
| std::string reason; |
| if (!IsValidSymbolicLink(current, sub_dir_type, cache_paths, &reason)) { |
| LOG(WARNING) << "Removing an invalid symlink: " << current.value() |
| << ": " << reason; |
| file_util::Delete(current, false); |
| continue; |
| } |
| |
| // If we're scanning outgoing directory, entry must exist and be dirty. |
| // Otherwise, it's a logic error from previous execution, remove this |
| // outgoing symlink and move on. |
| CacheMap::iterator iter = cache_map->find(resource_id); |
| if (iter == cache_map->end() || !iter->second.IsDirty()) { |
| LOG(WARNING) << "Removing an symlink to a non-dirty file: " |
| << current.value(); |
| file_util::Delete(current, false); |
| continue; |
| } |
| |
| processed_file_map->insert(std::make_pair(resource_id, current)); |
| continue; |
| } else if (sub_dir_type == GDataCache::CACHE_TYPE_PERSISTENT || |
| sub_dir_type == GDataCache::CACHE_TYPE_TMP) { |
| if (sub_dir_type == GDataCache::CACHE_TYPE_PERSISTENT) |
| cache_entry.SetPersistent(true); |
| |
| if (file_util::IsLink(current)) { |
| LOG(WARNING) << "Removing a symlink in persistent/tmp directory" |
| << current.value(); |
| file_util::Delete(current, false); |
| continue; |
| } |
| 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 == GDataCache::CACHE_TYPE_PERSISTENT); |
| file_util::Delete(current, false); |
| } else { |
| // The cache file is present. |
| cache_entry.SetPresent(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 == GDataCache::CACHE_TYPE_PERSISTENT) { |
| cache_entry.SetDirty(true); |
| } else { |
| LOG(WARNING) << "Removing a dirty file in tmp directory: " |
| << current.value(); |
| file_util::Delete(current, false); |
| continue; |
| } |
| } |
| } |
| } else { |
| NOTREACHED() << "Unexpected sub directory type: " << sub_dir_type; |
| } |
| |
| // 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)); |
| } |
| } |
| |
| // static |
| bool GDataCacheMetadataMap::CheckIfMd5Matches( |
| const std::string& md5, |
| const GDataCacheEntry& cache_entry) { |
| if (cache_entry.IsDirty()) { |
| // 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.IsPinned() && 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 exisit 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 gdata |