blob: d4931e71f28e72eb8385c41158b1cd7a61b37574 [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 "components/drive/chromeos/file_cache.h"
#include <linux/fs.h>
#include <sys/ioctl.h>
#include <sys/xattr.h>
#include <memory>
#include <queue>
#include <vector>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback_helpers.h"
#include "base/files/file.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/stl_util.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/system/sys_info.h"
#include "build/build_config.h"
#include "components/drive/drive.pb.h"
#include "components/drive/drive_api_util.h"
#include "components/drive/file_system_core_util.h"
#include "components/drive/resource_metadata_storage.h"
#include "google_apis/drive/task_util.h"
#include "net/base/filename_util.h"
#include "net/base/mime_sniffer.h"
#include "net/base/mime_util.h"
namespace drive {
namespace internal {
namespace {
typedef std::pair<base::File::Info, ResourceEntry> CacheInfo;
typedef long FileAttributes; // NOLINT(runtime/int)
// Returns ID extracted from the path.
std::string GetIdFromPath(const base::FilePath& path) {
return util::UnescapeCacheFileName(path.BaseName().AsUTF8Unsafe());
}
base::FilePath GetPathForId(const base::FilePath& cache_directory,
const std::string& id) {
return cache_directory.Append(
base::FilePath::FromUTF8Unsafe(util::EscapeCacheFileName(id)));
}
// Returns if the filesystem backing |path| supports file attributes.
// This will return false if the filesystem is for example tmpfs, which is used
// for ephemeral mode.
bool IsFileAttributesSupported(const base::FilePath& path) {
if (getxattr(path.value().c_str(), "user.foo", nullptr, 0) >= 0) {
return true;
}
return errno != ENOTSUP;
}
// Sets extended file attribute as |name| |value| pair.
bool SetExtendedFileAttributes(const base::FilePath& path,
const std::string& name, const std::string& value) {
if (setxattr(path.value().c_str(), name.c_str(), value.c_str(),
value.size() + 1, 0) != 0) {
PLOG(ERROR) << "setxattr: " << path.value();
return false;
}
return true;
}
// Remove extended file attribute with |name|.
bool UnsetExtendedFileAttributes(const base::FilePath& path,
const std::string& name) {
if (removexattr(path.value().c_str(), name.c_str()) != 0) {
PLOG_IF(ERROR, errno != ENODATA) << "removexattr: " << path.value();
return false;
}
return true;
}
// Changes attributes of the file with |flags|, e.g. FS_NODUMP_FL (cryptohome
// will remove Drive caches with this attribute).
// See linux/fs.h for available flags, and chattr(1) which does similar thing.
// Returns whether operation succeeded.
bool SetFileAttributes(const base::FilePath& path, FileAttributes flags) {
base::File file(path, base::File::FLAG_OPEN | base::File::FLAG_READ);
if (!file.IsValid()) {
PLOG(ERROR) << "Failed to open file: " << path.value();
return false;
}
if (ioctl(file.GetPlatformFile(), FS_IOC_SETFLAGS, &flags) < 0) {
PLOG(ERROR) << "ioctl: " << path.value();
return false;
}
return true;
}
// Gets file attributes similarly to lsattr(1). Returns flags or -1 on error.
// See linux/fs.h for the definition of the returned flags e.g. FS_NODUMP_FL.
FileAttributes GetFileAttributes(const base::FilePath& path) {
base::File file(path, base::File::FLAG_OPEN | base::File::FLAG_READ);
if (!file.IsValid()) {
PLOG(ERROR) << "Failed to open file: " << path.value();
return -1;
}
FileAttributes flags = 0;
if (ioctl(file.GetPlatformFile(), FS_IOC_GETFLAGS, &flags) < 0) {
PLOG(ERROR) << "ioctl: " << path.value();
return -1;
}
return flags;
}
// Marks the cache file to be removable by cryptohome, or do nothing if
// underlying filesystem doesn't support file attributes, as tmpfs for ephemeral
// mode.
bool SetRemovable(const base::FilePath& path) {
// For ephemeral mode.
if (!IsFileAttributesSupported(path)) {
return true;
}
FileAttributes flags = GetFileAttributes(path);
bool xattr = flags >= 0 && SetFileAttributes(path, flags | FS_NODUMP_FL);
bool fattr = SetExtendedFileAttributes(
path, FileCache::kGCacheRemovableAttribute, "1");
return xattr || fattr;
}
// Marks the cache file to be unremovable by cryptohome, or do nothing if
// underlying filesystem doesn't support file attributes, as tmpfs for ephemeral
// mode.
bool UnsetRemovable(const base::FilePath& path) {
// For ephemeral mode.
if (!IsFileAttributesSupported(path)) {
return true;
}
FileAttributes flags = GetFileAttributes(path);
bool xattr = flags >= 0 && SetFileAttributes(path, flags & ~FS_NODUMP_FL);
bool fattr =
UnsetExtendedFileAttributes(path, FileCache::kGCacheRemovableAttribute);
return xattr || fattr;
}
// Marks |path| as drive cache dir, or do nothing if underlying filesystem
// doesn't support file attributes, as tmpfs for ephemeral mode. Returns if the
// operation succeeded.
bool MarkAsDriveCacheDir(const base::FilePath& path) {
// For ephemeral mode.
if (!IsFileAttributesSupported(path)) {
return true;
}
return SetRemovable(path)
&& SetExtendedFileAttributes(path, FileCache::kGCacheFilesAttribute, "");
}
class CacheInfoLatestCompare {
public:
bool operator()(const CacheInfo& info_a, const CacheInfo& info_b) {
return info_a.first.last_accessed < info_b.first.last_accessed;
}
};
// Returns true if the cache file is present.
bool IsPresent(const ResourceEntry& entry) {
return entry.has_file_specific_info() &&
entry.file_specific_info().has_cache_state() &&
entry.file_specific_info().cache_state().is_present();
}
const size_t kMaxNumOfEvictedCacheFiles = 30000;
} // namespace
// static
const char FileCache::kGCacheFilesAttribute[] = "user.GCacheFiles";
const char FileCache::kGCacheRemovableAttribute[] = "user.GCacheRemovable";
FileCache::FileCache(ResourceMetadataStorage* storage,
const base::FilePath& cache_file_directory,
base::SequencedTaskRunner* blocking_task_runner,
FreeDiskSpaceGetterInterface* free_disk_space_getter)
: cache_file_directory_(cache_file_directory),
blocking_task_runner_(blocking_task_runner),
storage_(storage),
free_disk_space_getter_(free_disk_space_getter),
max_num_of_evicted_cache_files_(kMaxNumOfEvictedCacheFiles),
weak_ptr_factory_(this) {
DCHECK(blocking_task_runner_.get());
}
FileCache::~FileCache() {
// Must be on the sequenced worker pool, as |metadata_| must be deleted on
// the sequenced worker pool.
AssertOnSequencedWorkerPool();
}
void FileCache::SetMaxNumOfEvictedCacheFilesForTest(
size_t max_num_of_evicted_cache_files) {
max_num_of_evicted_cache_files_ = max_num_of_evicted_cache_files;
}
base::FilePath FileCache::GetCacheFilePath(const std::string& id) const {
return GetPathForId(cache_file_directory_, id);
}
void FileCache::AssertOnSequencedWorkerPool() {
DCHECK(blocking_task_runner_->RunsTasksInCurrentSequence());
}
bool FileCache::IsUnderFileCacheDirectory(const base::FilePath& path) const {
return cache_file_directory_.IsParent(path);
}
bool FileCache::FreeDiskSpaceIfNeededFor(int64_t num_bytes) {
AssertOnSequencedWorkerPool();
// Do nothing and return if we have enough space.
if (GetAvailableSpace() >= num_bytes)
return true;
// Otherwise, try to free up the disk space.
DVLOG(1) << "Freeing up disk space for " << num_bytes;
// Remove all files which have no corresponding cache entries.
base::FileEnumerator enumerator(cache_file_directory_,
false, // not recursive
base::FileEnumerator::FILES);
ResourceEntry entry;
for (base::FilePath current = enumerator.Next(); !current.empty();
current = enumerator.Next()) {
const std::string id = GetIdFromPath(current);
const FileError error = storage_->GetEntry(id, &entry);
if (error == FILE_ERROR_NOT_FOUND)
base::DeleteFile(current, false /* recursive */);
else if (error != FILE_ERROR_OK)
return false;
}
// Check available space again. If we have enough space here, do nothing.
const int64_t available_space = GetAvailableSpace();
if (available_space >= num_bytes)
return true;
const int64_t requested_space = num_bytes - available_space;
// Put all entries in priority queue where latest entry becomes top.
std::priority_queue<CacheInfo, std::vector<CacheInfo>, CacheInfoLatestCompare>
cache_info_queue;
std::unique_ptr<ResourceMetadataStorage::Iterator> it =
storage_->GetIterator();
for (; !it->IsAtEnd(); it->Advance()) {
if (IsEvictable(it->GetID(), it->GetValue())) {
const ResourceEntry& entry = it->GetValue();
const base::FilePath& cache_path = GetCacheFilePath(entry.local_id());
base::File::Info info;
// If it fails to get file info of |cache_path|, use default value as its
// file info. i.e. the file becomes least recently used one.
base::GetFileInfo(cache_path, &info);
CacheInfo cache_info = std::make_pair(info, entry);
if (cache_info_queue.size() < max_num_of_evicted_cache_files_) {
cache_info_queue.push(cache_info);
} else if (cache_info_queue.size() >= max_num_of_evicted_cache_files_ &&
cache_info.first.last_accessed <
cache_info_queue.top().first.last_accessed) {
// Do not enqueue more than max_num_of_evicted_cache_files_ not to use
// up memory with this queue.
cache_info_queue.pop();
cache_info_queue.push(cache_info);
}
}
}
if (it->HasError())
return false;
// Copy entries to the vector. This becomes last-accessed desc order.
std::vector<CacheInfo> cache_info_list;
while (!cache_info_queue.empty()) {
cache_info_list.push_back(cache_info_queue.top());
cache_info_queue.pop();
}
// Update DB and delete files with accessing to the vector in ascending order.
int64_t evicted_cache_size = 0;
auto iter = cache_info_list.rbegin();
while (evicted_cache_size < requested_space &&
iter != cache_info_list.rend()) {
const CacheInfo& cache_info = *iter;
// Update DB.
ResourceEntry entry = cache_info.second;
entry.mutable_file_specific_info()->clear_cache_state();
storage_->PutEntry(entry);
// Delete cache file.
const base::FilePath& path = GetCacheFilePath(entry.local_id());
if (base::DeleteFile(path, false /* recursive */))
evicted_cache_size += cache_info.first.size;
++iter;
}
// Check the disk space again.
return GetAvailableSpace() >= num_bytes;
}
int64_t FileCache::CalculateCacheSize() {
AssertOnSequencedWorkerPool();
int64_t total_cache_size = 0;
int64_t cache_size = 0;
std::unique_ptr<ResourceMetadataStorage::Iterator> it =
storage_->GetIterator();
for (; !it->IsAtEnd(); it->Advance()) {
if (IsPresent(it->GetValue()) &&
base::GetFileSize(GetCacheFilePath(it->GetID()), &cache_size)) {
DCHECK_GE(cache_size, 0);
total_cache_size += cache_size;
}
}
if (it->HasError())
return 0;
return total_cache_size;
}
int64_t FileCache::CalculateEvictableCacheSize() {
AssertOnSequencedWorkerPool();
int64_t evictable_cache_size = 0;
int64_t cache_size = 0;
std::unique_ptr<ResourceMetadataStorage::Iterator> it =
storage_->GetIterator();
for (; !it->IsAtEnd(); it->Advance()) {
if (IsEvictable(it->GetID(), it->GetValue()) &&
base::GetFileSize(GetCacheFilePath(it->GetID()), &cache_size)) {
DCHECK_GE(cache_size, 0);
evictable_cache_size += cache_size;
}
}
if (it->HasError())
return 0;
return evictable_cache_size;
}
FileError FileCache::GetFile(const std::string& id,
base::FilePath* cache_file_path) {
AssertOnSequencedWorkerPool();
DCHECK(cache_file_path);
ResourceEntry entry;
FileError error = storage_->GetEntry(id, &entry);
if (error != FILE_ERROR_OK)
return error;
if (!entry.file_specific_info().cache_state().is_present())
return FILE_ERROR_NOT_FOUND;
*cache_file_path = GetCacheFilePath(id);
return FILE_ERROR_OK;
}
FileError FileCache::Store(const std::string& id,
const std::string& md5,
const base::FilePath& source_path,
FileOperationType file_operation_type) {
AssertOnSequencedWorkerPool();
ResourceEntry entry;
FileError error = storage_->GetEntry(id, &entry);
if (error != FILE_ERROR_OK)
return error;
int64_t file_size = 0;
if (file_operation_type == FILE_OPERATION_COPY) {
if (!base::GetFileSize(source_path, &file_size)) {
LOG(WARNING) << "Couldn't get file size for: " << source_path.value();
return FILE_ERROR_FAILED;
}
}
if (!FreeDiskSpaceIfNeededFor(file_size))
return FILE_ERROR_NO_LOCAL_SPACE;
// If file is mounted, return error.
if (mounted_files_.count(id))
return FILE_ERROR_IN_USE;
base::FilePath dest_path = GetCacheFilePath(id);
bool success = false;
switch (file_operation_type) {
case FILE_OPERATION_MOVE:
success = base::Move(source_path, dest_path);
break;
case FILE_OPERATION_COPY:
success = base::CopyFile(source_path, dest_path);
break;
default:
NOTREACHED();
}
if (!success) {
LOG(ERROR) << "Failed to store: "
<< "source_path = " << source_path.value() << ", "
<< "dest_path = " << dest_path.value() << ", "
<< "file_operation_type = " << file_operation_type;
return FILE_ERROR_FAILED;
}
// Now that file operations have completed, update metadata.
FileCacheEntry* cache_state =
entry.mutable_file_specific_info()->mutable_cache_state();
cache_state->set_md5(md5);
cache_state->set_is_present(true);
if (md5.empty())
cache_state->set_is_dirty(true);
if (!cache_state->is_pinned() && !cache_state->is_dirty()) {
if (!SetRemovable(dest_path))
return FILE_ERROR_FAILED;
} else {
if (!UnsetRemovable(dest_path))
return FILE_ERROR_FAILED;
}
return storage_->PutEntry(entry);
}
FileError FileCache::Pin(const std::string& id) {
AssertOnSequencedWorkerPool();
ResourceEntry entry;
FileError error = storage_->GetEntry(id, &entry);
if (error != FILE_ERROR_OK)
return error;
entry.mutable_file_specific_info()->mutable_cache_state()->set_is_pinned(
true);
base::FilePath file_path = GetCacheFilePath(entry.local_id());
// Cache file can be created later.
if (entry.file_specific_info().cache_state().is_present()) {
if (!UnsetRemovable(file_path))
return FILE_ERROR_FAILED;
}
return storage_->PutEntry(entry);
}
FileError FileCache::Unpin(const std::string& id) {
AssertOnSequencedWorkerPool();
// Unpinning a file means its entry must exist in cache.
ResourceEntry entry;
FileError error = storage_->GetEntry(id, &entry);
if (error != FILE_ERROR_OK)
return error;
// Now that file operations have completed, update metadata.
if (entry.file_specific_info().cache_state().is_present()) {
entry.mutable_file_specific_info()->mutable_cache_state()->set_is_pinned(
false);
if (!entry.file_specific_info().cache_state().is_dirty()) {
if (!SetRemovable(GetCacheFilePath(entry.local_id())))
return FILE_ERROR_FAILED;
}
} else {
// Remove the existing entry if we are unpinning a non-present file.
entry.mutable_file_specific_info()->clear_cache_state();
}
error = storage_->PutEntry(entry);
if (error != FILE_ERROR_OK)
return error;
// Now it's a chance to free up space if needed.
FreeDiskSpaceIfNeededFor(0);
return FILE_ERROR_OK;
}
bool FileCache::IsMarkedAsMounted(const std::string& id) {
AssertOnSequencedWorkerPool();
return mounted_files_.count(id);
}
FileError FileCache::MarkAsMounted(const std::string& id,
base::FilePath* cache_file_path) {
AssertOnSequencedWorkerPool();
DCHECK(cache_file_path);
// Get cache entry associated with the id and md5
ResourceEntry entry;
FileError error = storage_->GetEntry(id, &entry);
if (error != FILE_ERROR_OK)
return error;
if (!entry.file_specific_info().cache_state().is_present())
return FILE_ERROR_NOT_FOUND;
if (mounted_files_.count(id))
return FILE_ERROR_INVALID_OPERATION;
base::FilePath path = GetCacheFilePath(id);
#if defined(OS_CHROMEOS)
// Ensure the file is readable to cros_disks. See crbug.com/236994.
if (!base::SetPosixFilePermissions(
path,
base::FILE_PERMISSION_READ_BY_USER |
base::FILE_PERMISSION_WRITE_BY_USER |
base::FILE_PERMISSION_READ_BY_GROUP |
base::FILE_PERMISSION_READ_BY_OTHERS))
return FILE_ERROR_FAILED;
#endif
mounted_files_.insert(id);
*cache_file_path = path;
return FILE_ERROR_OK;
}
FileError FileCache::OpenForWrite(
const std::string& id,
std::unique_ptr<base::ScopedClosureRunner>* file_closer) {
AssertOnSequencedWorkerPool();
// Marking a file dirty means its entry and actual file blob must exist in
// cache.
ResourceEntry entry;
FileError error = storage_->GetEntry(id, &entry);
if (error != FILE_ERROR_OK)
return error;
if (!entry.file_specific_info().cache_state().is_present()) {
LOG(WARNING) << "Can't mark dirty a file that wasn't cached: " << id;
return FILE_ERROR_NOT_FOUND;
}
entry.mutable_file_specific_info()->mutable_cache_state()->set_is_dirty(true);
if (!UnsetRemovable(GetCacheFilePath(entry.local_id())))
return FILE_ERROR_FAILED;
entry.mutable_file_specific_info()->mutable_cache_state()->clear_md5();
error = storage_->PutEntry(entry);
if (error != FILE_ERROR_OK)
return error;
write_opened_files_[id]++;
*file_closer = std::make_unique<base::ScopedClosureRunner>(
base::Bind(&google_apis::RunTaskWithTaskRunner, blocking_task_runner_,
base::Bind(&FileCache::CloseForWrite,
weak_ptr_factory_.GetWeakPtr(), id)));
return FILE_ERROR_OK;
}
bool FileCache::IsOpenedForWrite(const std::string& id) {
AssertOnSequencedWorkerPool();
return write_opened_files_.count(id) != 0;
}
FileError FileCache::UpdateMd5(const std::string& id) {
AssertOnSequencedWorkerPool();
if (IsOpenedForWrite(id))
return FILE_ERROR_IN_USE;
ResourceEntry entry;
FileError error = storage_->GetEntry(id, &entry);
if (error != FILE_ERROR_OK)
return error;
if (!entry.file_specific_info().cache_state().is_present())
return FILE_ERROR_NOT_FOUND;
const std::string& md5 =
util::GetMd5Digest(GetCacheFilePath(id), &in_shutdown_);
if (in_shutdown_.IsSet())
return FILE_ERROR_ABORT;
if (md5.empty())
return FILE_ERROR_NOT_FOUND;
entry.mutable_file_specific_info()->mutable_cache_state()->set_md5(md5);
return storage_->PutEntry(entry);
}
FileError FileCache::ClearDirty(const std::string& id) {
AssertOnSequencedWorkerPool();
if (IsOpenedForWrite(id))
return FILE_ERROR_IN_USE;
// Clearing a dirty file means its entry and actual file blob must exist in
// cache.
ResourceEntry entry;
FileError error = storage_->GetEntry(id, &entry);
if (error != FILE_ERROR_OK)
return error;
if (!entry.file_specific_info().cache_state().is_present()) {
LOG(WARNING) << "Can't clear dirty state of a file that wasn't cached: "
<< id;
return FILE_ERROR_NOT_FOUND;
}
// If a file is not dirty (it should have been marked dirty via OpenForWrite),
// clearing its dirty state is an invalid operation.
if (!entry.file_specific_info().cache_state().is_dirty()) {
LOG(WARNING) << "Can't clear dirty state of a non-dirty file: " << id;
return FILE_ERROR_INVALID_OPERATION;
}
entry.mutable_file_specific_info()->mutable_cache_state()->set_is_dirty(
false);
if (!entry.file_specific_info().cache_state().is_pinned()) {
if (!SetRemovable(GetCacheFilePath(entry.local_id())))
return FILE_ERROR_FAILED;
}
return storage_->PutEntry(entry);
}
FileError FileCache::Remove(const std::string& id) {
AssertOnSequencedWorkerPool();
ResourceEntry entry;
// If entry doesn't exist, nothing to do.
FileError error = storage_->GetEntry(id, &entry);
if (error == FILE_ERROR_NOT_FOUND)
return FILE_ERROR_OK;
if (error != FILE_ERROR_OK)
return error;
if (!entry.file_specific_info().has_cache_state())
return FILE_ERROR_OK;
// Cannot delete a mounted file.
if (mounted_files_.count(id))
return FILE_ERROR_IN_USE;
// Delete the file.
base::FilePath path = GetCacheFilePath(id);
if (!base::DeleteFile(path, false /* recursive */))
return FILE_ERROR_FAILED;
// Now that all file operations have completed, remove from metadata.
entry.mutable_file_specific_info()->clear_cache_state();
return storage_->PutEntry(entry);
}
bool FileCache::ClearAll() {
AssertOnSequencedWorkerPool();
// Remove files.
base::FileEnumerator enumerator(cache_file_directory_,
false, // not recursive
base::FileEnumerator::FILES);
for (base::FilePath file = enumerator.Next(); !file.empty();
file = enumerator.Next())
base::DeleteFile(file, false /* recursive */);
return true;
}
bool FileCache::Initialize() {
AssertOnSequencedWorkerPool();
// Older versions do not clear MD5 when marking entries dirty.
// Clear MD5 of all dirty entries to deal with old data.
std::unique_ptr<ResourceMetadataStorage::Iterator> it =
storage_->GetIterator();
for (; !it->IsAtEnd(); it->Advance()) {
if (it->GetValue().file_specific_info().cache_state().is_dirty()) {
ResourceEntry new_entry(it->GetValue());
new_entry.mutable_file_specific_info()->mutable_cache_state()->
clear_md5();
if (storage_->PutEntry(new_entry) != FILE_ERROR_OK)
return false;
}
}
if (it->HasError())
return false;
if (!RenameCacheFilesToNewFormat())
return false;
// Run this every time to resolve inconsistency between metadata
// and file attributes which possibly occurs on abrupt power failure.
if (!FixMetadataAndFileAttributes()) {
return false;
}
return true;
}
void FileCache::Destroy() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
in_shutdown_.Set();
// Destroy myself on the blocking pool.
// Note that base::DeletePointer<> cannot be used as the destructor of this
// class is private.
blocking_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&FileCache::DestroyOnBlockingPool,
base::Unretained(this)));
}
void FileCache::DestroyOnBlockingPool() {
AssertOnSequencedWorkerPool();
delete this;
}
bool FileCache::RecoverFilesFromCacheDirectory(
const base::FilePath& dest_directory,
const ResourceMetadataStorage::RecoveredCacheInfoMap&
recovered_cache_info) {
int file_number = 1;
base::FileEnumerator enumerator(cache_file_directory_,
false, // not recursive
base::FileEnumerator::FILES);
for (base::FilePath current = enumerator.Next(); !current.empty();
current = enumerator.Next()) {
const std::string& id = GetIdFromPath(current);
ResourceEntry entry;
FileError error = storage_->GetEntry(id, &entry);
if (error != FILE_ERROR_OK && error != FILE_ERROR_NOT_FOUND)
return false;
if (error == FILE_ERROR_OK &&
entry.file_specific_info().cache_state().is_present()) {
// This file is managed by FileCache, no need to recover it.
continue;
}
// If a cache entry which is non-dirty and has matching MD5 is found in
// |recovered_cache_entries|, it means the current file is already uploaded
// to the server. Just delete it instead of recovering it.
ResourceMetadataStorage::RecoveredCacheInfoMap::const_iterator it =
recovered_cache_info.find(id);
if (it != recovered_cache_info.end()) {
// Due to the DB corruption, cache info might be recovered from old
// revision. Perform MD5 check even when is_dirty is false just in case.
if (!it->second.is_dirty &&
it->second.md5 == util::GetMd5Digest(current, &in_shutdown_)) {
base::DeleteFile(current, false /* recursive */);
continue;
}
}
// Read file contents to sniff mime type.
std::vector<char> content(net::kMaxBytesToSniff);
const int read_result =
base::ReadFile(current, &content[0], content.size());
if (read_result < 0) {
LOG(WARNING) << "Cannot read: " << current.value();
return false;
}
if (read_result == 0) // Skip empty files.
continue;
// Use recovered file name if available, otherwise decide file name with
// sniffed mime type.
base::FilePath dest_base_name(FILE_PATH_LITERAL("file"));
std::string mime_type;
if (it != recovered_cache_info.end() && !it->second.title.empty()) {
// We can use a file name recovered from the trashed DB.
dest_base_name = base::FilePath::FromUTF8Unsafe(it->second.title);
} else if (net::SniffMimeType(
&content[0], read_result, net::FilePathToFileURL(current),
std::string(), net::ForceSniffFileUrlsForHtml::kDisabled,
&mime_type) ||
net::SniffMimeTypeFromLocalData(&content[0], read_result,
&mime_type)) {
// Change base name for common mime types.
if (net::MatchesMimeType("image/*", mime_type)) {
dest_base_name = base::FilePath(FILE_PATH_LITERAL("image"));
} else if (net::MatchesMimeType("video/*", mime_type)) {
dest_base_name = base::FilePath(FILE_PATH_LITERAL("video"));
} else if (net::MatchesMimeType("audio/*", mime_type)) {
dest_base_name = base::FilePath(FILE_PATH_LITERAL("audio"));
}
// Estimate extension from mime type.
std::vector<base::FilePath::StringType> extensions;
base::FilePath::StringType extension;
if (net::GetPreferredExtensionForMimeType(mime_type, &extension))
extensions.push_back(extension);
else
net::GetExtensionsForMimeType(mime_type, &extensions);
// Add extension if possible.
if (!extensions.empty())
dest_base_name = dest_base_name.AddExtension(extensions[0]);
}
// Add file number to the file name and move.
const base::FilePath& dest_path = dest_directory.Append(dest_base_name)
.InsertBeforeExtensionASCII(base::StringPrintf("%08d", file_number++));
if (!base::CreateDirectory(dest_directory) ||
!base::Move(current, dest_path)) {
LOG(WARNING) << "Failed to move: " << current.value()
<< " to " << dest_path.value();
return false;
}
}
UMA_HISTOGRAM_COUNTS_1M("Drive.NumberOfCacheFilesRecoveredAfterDBCorruption",
file_number - 1);
return true;
}
FileError FileCache::MarkAsUnmounted(const base::FilePath& file_path) {
AssertOnSequencedWorkerPool();
DCHECK(IsUnderFileCacheDirectory(file_path));
std::string id = GetIdFromPath(file_path);
// Get the entry associated with the id.
ResourceEntry entry;
FileError error = storage_->GetEntry(id, &entry);
if (error != FILE_ERROR_OK)
return error;
std::set<std::string>::iterator it = mounted_files_.find(id);
if (it == mounted_files_.end())
return FILE_ERROR_INVALID_OPERATION;
mounted_files_.erase(it);
return FILE_ERROR_OK;
}
int64_t FileCache::GetAvailableSpace() {
int64_t free_space = 0;
if (free_disk_space_getter_)
free_space = free_disk_space_getter_->AmountOfFreeDiskSpace();
else
free_space = base::SysInfo::AmountOfFreeDiskSpace(cache_file_directory_);
// Subtract this as if this portion does not exist.
free_space -= drive::internal::kMinFreeSpaceInBytes;
return free_space;
}
bool FileCache::RenameCacheFilesToNewFormat() {
base::FileEnumerator enumerator(cache_file_directory_,
false, // not recursive
base::FileEnumerator::FILES);
for (base::FilePath current = enumerator.Next(); !current.empty();
current = enumerator.Next()) {
base::FilePath new_path = current.RemoveExtension();
if (!new_path.Extension().empty()) {
// Delete files with multiple extensions.
if (!base::DeleteFile(current, false /* recursive */))
return false;
continue;
}
const std::string& id = GetIdFromPath(new_path);
new_path = GetCacheFilePath(util::CanonicalizeResourceId(id));
if (new_path != current && !base::Move(current, new_path))
return false;
}
return true;
}
bool FileCache::FixMetadataAndFileAttributes() {
std::unique_ptr<ResourceMetadataStorage::Iterator> it =
storage_->GetIterator();
for (; !it->IsAtEnd(); it->Advance()) {
ResourceEntry entry = it->GetValue();
FileCacheEntry* file_cache_entry =
entry.mutable_file_specific_info()->mutable_cache_state();
const base::FilePath filepath = GetPathForId(cache_file_directory_,
entry.local_id());
if (base::PathExists(filepath)) {
if (file_cache_entry->is_present()) {
// Update file attribues for cryptohome.
if (file_cache_entry->is_pinned() || file_cache_entry->is_dirty()) {
if (!UnsetRemovable(filepath)) return false;
} else {
if (!SetRemovable(filepath)) return false;
}
} else {
// Delete file if the file is present but metadata says not.
// It happens only on abrupt shutdown.
LOG(WARNING)
<< "File is present but metadata's state was inconsistent.";
if (!base::DeleteFile(filepath, false /* recursive */))
return false;
}
} else {
// Update metatadata if there is no file but metadata says there is.
// It happens when cryptohome removed the file.
// We don't clear is_pinned here, so that file download is restarted on
// the following scenario:
// 1. The file was pinned but not present.
// 2. Then the file was downloaded and became present.
// 3. Unclean shutdown happens, metadata update was saved to the disk,
// but the file move was not.
if (file_cache_entry->is_present()) {
file_cache_entry->set_is_present(false);
file_cache_entry->set_is_dirty(false);
file_cache_entry->clear_md5();
if (storage_->PutEntry(entry) != FILE_ERROR_OK)
return false;
}
}
}
return MarkAsDriveCacheDir(cache_file_directory_);
}
void FileCache::CloseForWrite(const std::string& id) {
AssertOnSequencedWorkerPool();
std::map<std::string, int>::iterator it = write_opened_files_.find(id);
if (it == write_opened_files_.end())
return;
DCHECK_LT(0, it->second);
--it->second;
if (it->second == 0)
write_opened_files_.erase(it);
// Update last modified date.
ResourceEntry entry;
FileError error = storage_->GetEntry(id, &entry);
if (error != FILE_ERROR_OK) {
LOG(ERROR) << "Failed to get entry: " << id << ", "
<< FileErrorToString(error);
return;
}
int64_t now = base::Time::Now().ToInternalValue();
entry.mutable_file_info()->set_last_modified(now);
entry.set_last_modified_by_me(now);
error = storage_->PutEntry(entry);
if (error != FILE_ERROR_OK) {
LOG(ERROR) << "Failed to put entry: " << id << ", "
<< FileErrorToString(error);
}
}
bool FileCache::IsEvictable(const std::string& id, const ResourceEntry& entry) {
return IsPresent(entry) &&
!entry.file_specific_info().cache_state().is_pinned() &&
!entry.file_specific_info().cache_state().is_dirty() &&
!mounted_files_.count(id);
}
} // namespace internal
} // namespace drive