blob: 37c0e87ff9e96ba26a55fcd178ce18cae960bb68 [file] [log] [blame]
[email protected]3653146a2012-05-29 13:41:471// Copyright (c) 2012 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.
4
yawano3513e142016-04-20 00:42:425#include "components/drive/chromeos/file_cache.h"
[email protected]3653146a2012-05-29 13:41:476
okac6aac502016-05-23 12:16:587#include <linux/fs.h>
8#include <sys/ioctl.h>
9#include <sys/xattr.h>
yawano1c325bf2016-04-20 06:37:0310
yawano4c36b9a02015-10-23 06:17:2311#include <queue>
[email protected]3653146a2012-05-29 13:41:4712#include <vector>
13
lukasza037c10b12015-06-12 04:21:2514#include "base/bind.h"
15#include "base/bind_helpers.h"
[email protected]8b03ab3a2014-01-15 17:52:4516#include "base/callback_helpers.h"
okac6aac502016-05-23 12:16:5817#include "base/files/file.h"
[email protected]25a4c1c2013-06-08 04:53:3618#include "base/files/file_enumerator.h"
thestig18dfb7a52014-08-26 10:44:0419#include "base/files/file_util.h"
lukasza037c10b12015-06-12 04:21:2520#include "base/location.h"
[email protected]3653146a2012-05-29 13:41:4721#include "base/logging.h"
[email protected]d105b3ad2013-11-01 05:33:1322#include "base/metrics/histogram.h"
okac6aac502016-05-23 12:16:5823#include "base/stl_util.h"
[email protected]5c073322013-06-11 08:03:3024#include "base/strings/string_util.h"
25#include "base/strings/stringprintf.h"
[email protected]a321b9632012-06-14 03:29:1726#include "base/sys_info.h"
avibc5337b2015-12-25 23:16:3327#include "build/build_config.h"
lukasza01b9d55a2015-07-21 15:19:2528#include "components/drive/drive.pb.h"
lukasza8acc4eb2015-07-20 20:57:2029#include "components/drive/drive_api_util.h"
lukasza6364a022015-08-21 01:13:2430#include "components/drive/file_system_core_util.h"
31#include "components/drive/resource_metadata_storage.h"
[email protected]8b03ab3a2014-01-15 17:52:4532#include "google_apis/drive/task_util.h"
[email protected]d96cf752014-04-09 04:05:2833#include "net/base/filename_util.h"
[email protected]b7af4f12013-10-31 06:57:4534#include "net/base/mime_sniffer.h"
35#include "net/base/mime_util.h"
[email protected]7986b8d2012-06-14 15:05:1436
[email protected]d9d04df2012-10-12 07:06:3537namespace drive {
[email protected]59c7cdec2013-05-07 04:17:1338namespace internal {
[email protected]3653146a2012-05-29 13:41:4739namespace {
40
okac6aac502016-05-23 12:16:5841typedef std::pair<base::File::Info, ResourceEntry> CacheInfo;
42typedef long FileAttributes; // NOLINT(runtime/int)
43
[email protected]c9e4738d2013-08-26 03:04:0744// Returns ID extracted from the path.
45std::string GetIdFromPath(const base::FilePath& path) {
[email protected]91a464e62013-07-10 09:30:0646 return util::UnescapeCacheFileName(path.BaseName().AsUTF8Unsafe());
47}
48
yawano9fd1e632016-02-04 09:00:0649base::FilePath GetPathForId(const base::FilePath& cache_directory,
50 const std::string& id) {
51 return cache_directory.Append(
52 base::FilePath::FromUTF8Unsafe(util::EscapeCacheFileName(id)));
53}
54
okac6aac502016-05-23 12:16:5855// Sets extended file attribute as |name| |value| pair.
56bool SetExtendedFileAttributes(const base::FilePath& path,
57 const std::string& name, const std::string& value) {
58 return setxattr(path.value().c_str(), name.c_str(), value.c_str(),
59 value.size() + 1, 0) == 0;
60}
61
62// Changes attributes of the file with |flags|, e.g. FS_NODUMP_FL (cryptohome
63// will remove Drive caches with this attribute).
64// See linux/fs.h for available flags, and chattr(1) which does similar thing.
65// Returns whether operation succeeded.
66bool SetFileAttributes(const base::FilePath& path, FileAttributes flags) {
67 base::File file(path, base::File::FLAG_OPEN | base::File::FLAG_READ);
68 if (!file.IsValid()) {
69 PLOG(ERROR) << "Failed to open file: " << path.value();
70 return false;
71 }
72 if (ioctl(file.GetPlatformFile(), FS_IOC_SETFLAGS, &flags) < 0) {
73 PLOG(ERROR) << "ioctl: " << path.value();
74 return false;
75 }
76 return true;
77}
78
79// Gets file attributes similarly to lsattr(1). Returns flags or -1 on error.
80// See linux/fs.h for the definition of the returned flags e.g. FS_NODUMP_FL.
81FileAttributes GetFileAttributes(const base::FilePath& path) {
82 base::File file(path, base::File::FLAG_OPEN | base::File::FLAG_READ);
83 if (!file.IsValid()) {
84 PLOG(ERROR) << "Failed to open file: " << path.value();
85 return -1;
86 }
87 FileAttributes flags = 0;
88 if (ioctl(file.GetPlatformFile(), FS_IOC_GETFLAGS, &flags) < 0) {
89 PLOG(ERROR) << "ioctl: " << path.value();
90 return -1;
91 }
92 return flags;
93}
94
95// Marks the cache file to be removable by cryptohome.
96bool SetRemovable(const base::FilePath& path) {
97 FileAttributes flags = GetFileAttributes(path);
98 if (flags < 0) return false;
99 if ((flags & FS_NODUMP_FL) == FS_NODUMP_FL) return true;
100
101 return SetFileAttributes(path, flags | FS_NODUMP_FL);
102}
103
104// Marks the cache file to be unremovable by cryptohome.
105bool UnsetRemovable(const base::FilePath& path) {
106 FileAttributes flags = GetFileAttributes(path);
107 if (flags < 0) return false;
108 if ((flags & FS_NODUMP_FL) == 0) return true;
109
110 return SetFileAttributes(path, flags & ~FS_NODUMP_FL);
111}
112
113// Marks |path| as drive cache dir.
114// Returns if the operation succeeded.
115bool MarkAsDriveCacheDir(const base::FilePath& path) {
116 return SetRemovable(path)
117 && SetExtendedFileAttributes(path, FileCache::kGCacheFilesAttribute, "");
118}
yawano4c36b9a02015-10-23 06:17:23119
120class CacheInfoLatestCompare {
121 public:
122 bool operator()(const CacheInfo& info_a, const CacheInfo& info_b) {
123 return info_a.first.last_accessed < info_b.first.last_accessed;
124 }
125};
126
127const size_t kMaxNumOfEvictedCacheFiles = 30000;
128
[email protected]a321b9632012-06-14 03:29:17129} // namespace
[email protected]32a7fc852012-06-08 17:25:50130
okac6aac502016-05-23 12:16:58131// static
132const char FileCache::kGCacheFilesAttribute[] = "user.GCacheFiles";
133
[email protected]2df61e12013-06-21 16:00:09134FileCache::FileCache(ResourceMetadataStorage* storage,
[email protected]e07f7b7b2013-06-19 03:43:12135 const base::FilePath& cache_file_directory,
[email protected]eca3fc92013-05-01 03:53:40136 base::SequencedTaskRunner* blocking_task_runner,
137 FreeDiskSpaceGetterInterface* free_disk_space_getter)
[email protected]2df61e12013-06-21 16:00:09138 : cache_file_directory_(cache_file_directory),
[email protected]ddbf2052012-07-13 15:07:02139 blocking_task_runner_(blocking_task_runner),
[email protected]2df61e12013-06-21 16:00:09140 storage_(storage),
[email protected]f6fd98a2012-12-14 00:04:02141 free_disk_space_getter_(free_disk_space_getter),
yawano4c36b9a02015-10-23 06:17:23142 max_num_of_evicted_cache_files_(kMaxNumOfEvictedCacheFiles),
[email protected]9c009092013-05-01 03:14:09143 weak_ptr_factory_(this) {
[email protected]144b6c42013-06-14 07:30:38144 DCHECK(blocking_task_runner_.get());
[email protected]3653146a2012-05-29 13:41:47145}
146
[email protected]eca3fc92013-05-01 03:53:40147FileCache::~FileCache() {
[email protected]17196ee2012-12-13 06:23:51148 // Must be on the sequenced worker pool, as |metadata_| must be deleted on
149 // the sequenced worker pool.
[email protected]73f9c742012-06-15 07:37:13150 AssertOnSequencedWorkerPool();
[email protected]3653146a2012-05-29 13:41:47151}
152
yawano4c36b9a02015-10-23 06:17:23153void FileCache::SetMaxNumOfEvictedCacheFilesForTest(
154 size_t max_num_of_evicted_cache_files) {
155 max_num_of_evicted_cache_files_ = max_num_of_evicted_cache_files;
156}
157
[email protected]c9e4738d2013-08-26 03:04:07158base::FilePath FileCache::GetCacheFilePath(const std::string& id) const {
yawano9fd1e632016-02-04 09:00:06159 return GetPathForId(cache_file_directory_, id);
[email protected]32a7fc852012-06-08 17:25:50160}
161
[email protected]eca3fc92013-05-01 03:53:40162void FileCache::AssertOnSequencedWorkerPool() {
[email protected]8e37b9b2013-12-11 09:06:02163 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
[email protected]fcc92a52012-06-08 22:54:16164}
165
[email protected]eca3fc92013-05-01 03:53:40166bool FileCache::IsUnderFileCacheDirectory(const base::FilePath& path) const {
[email protected]e07f7b7b2013-06-19 03:43:12167 return cache_file_directory_.IsParent(path);
[email protected]01ba15f72012-06-09 00:41:05168}
169
avibc5337b2015-12-25 23:16:33170bool FileCache::FreeDiskSpaceIfNeededFor(int64_t num_bytes) {
[email protected]ec514362013-05-27 17:52:22171 AssertOnSequencedWorkerPool();
172
173 // Do nothing and return if we have enough space.
yawano4c36b9a02015-10-23 06:17:23174 if (GetAvailableSpace() >= num_bytes)
[email protected]ec514362013-05-27 17:52:22175 return true;
176
177 // Otherwise, try to free up the disk space.
178 DVLOG(1) << "Freeing up disk space for " << num_bytes;
[email protected]f8b1a532013-06-06 08:35:08179
[email protected]9d147582013-06-14 06:25:45180 // Remove all files which have no corresponding cache entries.
[email protected]e07f7b7b2013-06-19 03:43:12181 base::FileEnumerator enumerator(cache_file_directory_,
[email protected]9d147582013-06-14 06:25:45182 false, // not recursive
183 base::FileEnumerator::FILES);
[email protected]cd8fd37f2014-05-20 15:45:21184 ResourceEntry entry;
[email protected]9d147582013-06-14 06:25:45185 for (base::FilePath current = enumerator.Next(); !current.empty();
186 current = enumerator.Next()) {
yawano4c36b9a02015-10-23 06:17:23187 const std::string id = GetIdFromPath(current);
188 const FileError error = storage_->GetEntry(id, &entry);
189
190 if (error == FILE_ERROR_NOT_FOUND)
[email protected]dd3aa792013-07-16 19:10:23191 base::DeleteFile(current, false /* recursive */);
[email protected]996139412014-05-10 06:19:50192 else if (error != FILE_ERROR_OK)
193 return false;
[email protected]9d147582013-06-14 06:25:45194 }
[email protected]ec514362013-05-27 17:52:22195
yawano4c36b9a02015-10-23 06:17:23196 // Check available space again. If we have enough space here, do nothing.
avibc5337b2015-12-25 23:16:33197 const int64_t available_space = GetAvailableSpace();
yawano4c36b9a02015-10-23 06:17:23198 if (available_space >= num_bytes)
199 return true;
200
avibc5337b2015-12-25 23:16:33201 const int64_t requested_space = num_bytes - available_space;
yawano4c36b9a02015-10-23 06:17:23202
203 // Put all entries in priority queue where latest entry becomes top.
204 std::priority_queue<CacheInfo, std::vector<CacheInfo>, CacheInfoLatestCompare>
205 cache_info_queue;
dchengf42750232016-04-12 04:12:27206 std::unique_ptr<ResourceMetadataStorage::Iterator> it =
207 storage_->GetIterator();
yawano4c36b9a02015-10-23 06:17:23208 for (; !it->IsAtEnd(); it->Advance()) {
209 if (IsEvictable(it->GetID(), it->GetValue())) {
210 const ResourceEntry& entry = it->GetValue();
211
212 const base::FilePath& cache_path = GetCacheFilePath(entry.local_id());
213 base::File::Info info;
214 // If it fails to get file info of |cache_path|, use default value as its
215 // file info. i.e. the file becomes least recently used one.
216 base::GetFileInfo(cache_path, &info);
217
218 CacheInfo cache_info = std::make_pair(info, entry);
219
220 if (cache_info_queue.size() < max_num_of_evicted_cache_files_) {
221 cache_info_queue.push(cache_info);
222 } else if (cache_info_queue.size() >= max_num_of_evicted_cache_files_ &&
223 cache_info.first.last_accessed <
224 cache_info_queue.top().first.last_accessed) {
225 // Do not enqueue more than max_num_of_evicted_cache_files_ not to use
226 // up memory with this queue.
227 cache_info_queue.pop();
228 cache_info_queue.push(cache_info);
229 }
230 }
231 }
232 if (it->HasError())
233 return false;
234
235 // Copy entries to the vector. This becomes last-accessed desc order.
236 std::vector<CacheInfo> cache_info_list;
237 while (!cache_info_queue.empty()) {
238 cache_info_list.push_back(cache_info_queue.top());
239 cache_info_queue.pop();
240 }
241
242 // Update DB and delete files with accessing to the vector in ascending order.
avibc5337b2015-12-25 23:16:33243 int64_t evicted_cache_size = 0;
yawano4c36b9a02015-10-23 06:17:23244 auto iter = cache_info_list.rbegin();
245 while (evicted_cache_size < requested_space &&
246 iter != cache_info_list.rend()) {
247 const CacheInfo& cache_info = *iter;
248
249 // Update DB.
250 ResourceEntry entry = cache_info.second;
251 entry.mutable_file_specific_info()->clear_cache_state();
252 storage_->PutEntry(entry);
253
254 // Delete cache file.
255 const base::FilePath& path = GetCacheFilePath(entry.local_id());
256
257 if (base::DeleteFile(path, false /* recursive */))
258 evicted_cache_size += cache_info.first.size;
259
260 ++iter;
261 }
262
[email protected]ec514362013-05-27 17:52:22263 // Check the disk space again.
yawano4c36b9a02015-10-23 06:17:23264 return GetAvailableSpace() >= num_bytes;
[email protected]ec514362013-05-27 17:52:22265}
266
yawano8578abf2015-08-26 09:15:50267uint64_t FileCache::CalculateEvictableCacheSize() {
268 AssertOnSequencedWorkerPool();
269
270 uint64_t evictable_cache_size = 0;
battrea5f64bd2015-08-26 10:47:41271 int64_t cache_size = 0;
yawano8578abf2015-08-26 09:15:50272
dchengf42750232016-04-12 04:12:27273 std::unique_ptr<ResourceMetadataStorage::Iterator> it =
274 storage_->GetIterator();
yawano8578abf2015-08-26 09:15:50275 for (; !it->IsAtEnd(); it->Advance()) {
276 if (IsEvictable(it->GetID(), it->GetValue()) &&
277 base::GetFileSize(GetCacheFilePath(it->GetID()), &cache_size)) {
278 DCHECK_GE(cache_size, 0);
279 evictable_cache_size += cache_size;
280 }
281 }
282
283 if (it->HasError())
284 return 0;
285
286 return evictable_cache_size;
287}
288
[email protected]c9e4738d2013-08-26 03:04:07289FileError FileCache::GetFile(const std::string& id,
[email protected]ec514362013-05-27 17:52:22290 base::FilePath* cache_file_path) {
291 AssertOnSequencedWorkerPool();
292 DCHECK(cache_file_path);
293
[email protected]cd8fd37f2014-05-20 15:45:21294 ResourceEntry entry;
295 FileError error = storage_->GetEntry(id, &entry);
[email protected]996139412014-05-10 06:19:50296 if (error != FILE_ERROR_OK)
297 return error;
[email protected]cd8fd37f2014-05-20 15:45:21298 if (!entry.file_specific_info().cache_state().is_present())
[email protected]ec514362013-05-27 17:52:22299 return FILE_ERROR_NOT_FOUND;
300
[email protected]c9e4738d2013-08-26 03:04:07301 *cache_file_path = GetCacheFilePath(id);
[email protected]ec514362013-05-27 17:52:22302 return FILE_ERROR_OK;
303}
304
[email protected]c9e4738d2013-08-26 03:04:07305FileError FileCache::Store(const std::string& id,
[email protected]82c4eb92013-05-21 11:25:23306 const std::string& md5,
307 const base::FilePath& source_path,
308 FileOperationType file_operation_type) {
309 AssertOnSequencedWorkerPool();
[email protected]d8546c92013-05-02 05:09:59310
[email protected]cd8fd37f2014-05-20 15:45:21311 ResourceEntry entry;
312 FileError error = storage_->GetEntry(id, &entry);
313 if (error != FILE_ERROR_OK)
314 return error;
315
avibc5337b2015-12-25 23:16:33316 int64_t file_size = 0;
[email protected]8e37b9b2013-12-11 09:06:02317 if (file_operation_type == FILE_OPERATION_COPY) {
318 if (!base::GetFileSize(source_path, &file_size)) {
319 LOG(WARNING) << "Couldn't get file size for: " << source_path.value();
320 return FILE_ERROR_FAILED;
321 }
322 }
323 if (!FreeDiskSpaceIfNeededFor(file_size))
324 return FILE_ERROR_NO_LOCAL_SPACE;
[email protected]73f9c742012-06-15 07:37:13325
[email protected]1d0786a2014-02-06 12:37:08326 // If file is mounted, return error.
327 if (mounted_files_.count(id))
[email protected]8e37b9b2013-12-11 09:06:02328 return FILE_ERROR_IN_USE;
329
330 base::FilePath dest_path = GetCacheFilePath(id);
331 bool success = false;
332 switch (file_operation_type) {
333 case FILE_OPERATION_MOVE:
334 success = base::Move(source_path, dest_path);
335 break;
336 case FILE_OPERATION_COPY:
337 success = base::CopyFile(source_path, dest_path);
338 break;
339 default:
340 NOTREACHED();
341 }
342
343 if (!success) {
344 LOG(ERROR) << "Failed to store: "
345 << "source_path = " << source_path.value() << ", "
346 << "dest_path = " << dest_path.value() << ", "
347 << "file_operation_type = " << file_operation_type;
348 return FILE_ERROR_FAILED;
349 }
350
351 // Now that file operations have completed, update metadata.
[email protected]cd8fd37f2014-05-20 15:45:21352 FileCacheEntry* cache_state =
353 entry.mutable_file_specific_info()->mutable_cache_state();
354 cache_state->set_md5(md5);
355 cache_state->set_is_present(true);
[email protected]bae99ae52014-01-29 01:13:14356 if (md5.empty())
[email protected]cd8fd37f2014-05-20 15:45:21357 cache_state->set_is_dirty(true);
okac6aac502016-05-23 12:16:58358
359 if (!cache_state->is_pinned() && !cache_state->is_dirty()) {
360 if (!SetRemovable(dest_path))
361 return FILE_ERROR_FAILED;
362 } else {
363 if (!UnsetRemovable(dest_path))
364 return FILE_ERROR_FAILED;
365 }
366
[email protected]cd8fd37f2014-05-20 15:45:21367 return storage_->PutEntry(entry);
[email protected]73f9c742012-06-15 07:37:13368}
369
[email protected]c9e4738d2013-08-26 03:04:07370FileError FileCache::Pin(const std::string& id) {
[email protected]f8b1a532013-06-06 08:35:08371 AssertOnSequencedWorkerPool();
372
[email protected]cd8fd37f2014-05-20 15:45:21373 ResourceEntry entry;
374 FileError error = storage_->GetEntry(id, &entry);
375 if (error != FILE_ERROR_OK)
[email protected]996139412014-05-10 06:19:50376 return error;
okac6aac502016-05-23 12:16:58377
[email protected]cd8fd37f2014-05-20 15:45:21378 entry.mutable_file_specific_info()->mutable_cache_state()->set_is_pinned(
379 true);
okac6aac502016-05-23 12:16:58380
381 base::FilePath file_path = GetCacheFilePath(entry.local_id());
382 // Cache file can be created later.
383 if (entry.file_specific_info().cache_state().is_present()) {
384 if (!UnsetRemovable(file_path))
385 return FILE_ERROR_FAILED;
386 }
387
[email protected]cd8fd37f2014-05-20 15:45:21388 return storage_->PutEntry(entry);
[email protected]f8b1a532013-06-06 08:35:08389}
390
[email protected]c9e4738d2013-08-26 03:04:07391FileError FileCache::Unpin(const std::string& id) {
[email protected]ec514362013-05-27 17:52:22392 AssertOnSequencedWorkerPool();
393
394 // Unpinning a file means its entry must exist in cache.
[email protected]cd8fd37f2014-05-20 15:45:21395 ResourceEntry entry;
396 FileError error = storage_->GetEntry(id, &entry);
[email protected]996139412014-05-10 06:19:50397 if (error != FILE_ERROR_OK)
398 return error;
[email protected]ec514362013-05-27 17:52:22399
[email protected]ec514362013-05-27 17:52:22400 // Now that file operations have completed, update metadata.
[email protected]cd8fd37f2014-05-20 15:45:21401 if (entry.file_specific_info().cache_state().is_present()) {
402 entry.mutable_file_specific_info()->mutable_cache_state()->set_is_pinned(
403 false);
okac6aac502016-05-23 12:16:58404 if (!entry.file_specific_info().cache_state().is_dirty()) {
405 if (!SetRemovable(GetCacheFilePath(entry.local_id())))
406 return FILE_ERROR_FAILED;
407 }
[email protected]ec514362013-05-27 17:52:22408 } else {
409 // Remove the existing entry if we are unpinning a non-present file.
[email protected]cd8fd37f2014-05-20 15:45:21410 entry.mutable_file_specific_info()->clear_cache_state();
[email protected]ec514362013-05-27 17:52:22411 }
[email protected]cd8fd37f2014-05-20 15:45:21412 error = storage_->PutEntry(entry);
413 if (error != FILE_ERROR_OK)
414 return error;
[email protected]bd2254d2013-06-12 16:00:47415
[email protected]9d147582013-06-14 06:25:45416 // Now it's a chance to free up space if needed.
[email protected]bd2254d2013-06-12 16:00:47417 FreeDiskSpaceIfNeededFor(0);
418
[email protected]ec514362013-05-27 17:52:22419 return FILE_ERROR_OK;
420}
421
[email protected]c3f65642013-08-28 02:04:33422FileError FileCache::MarkAsMounted(const std::string& id,
423 base::FilePath* cache_file_path) {
424 AssertOnSequencedWorkerPool();
425 DCHECK(cache_file_path);
426
427 // Get cache entry associated with the id and md5
[email protected]cd8fd37f2014-05-20 15:45:21428 ResourceEntry entry;
429 FileError error = storage_->GetEntry(id, &entry);
[email protected]996139412014-05-10 06:19:50430 if (error != FILE_ERROR_OK)
431 return error;
[email protected]cd8fd37f2014-05-20 15:45:21432 if (!entry.file_specific_info().cache_state().is_present())
433 return FILE_ERROR_NOT_FOUND;
[email protected]c3f65642013-08-28 02:04:33434
435 if (mounted_files_.count(id))
436 return FILE_ERROR_INVALID_OPERATION;
437
[email protected]c3f65642013-08-28 02:04:33438 base::FilePath path = GetCacheFilePath(id);
lukasza6364a022015-08-21 01:13:24439
440#if defined(OS_CHROMEOS)
441 // Ensure the file is readable to cros_disks. See crbug.com/236994.
[email protected]b264eab2013-11-27 23:22:08442 if (!base::SetPosixFilePermissions(
[email protected]c3f65642013-08-28 02:04:33443 path,
[email protected]b264eab2013-11-27 23:22:08444 base::FILE_PERMISSION_READ_BY_USER |
445 base::FILE_PERMISSION_WRITE_BY_USER |
446 base::FILE_PERMISSION_READ_BY_GROUP |
447 base::FILE_PERMISSION_READ_BY_OTHERS))
[email protected]c3f65642013-08-28 02:04:33448 return FILE_ERROR_FAILED;
lukasza6364a022015-08-21 01:13:24449#endif
[email protected]c3f65642013-08-28 02:04:33450
451 mounted_files_.insert(id);
452
453 *cache_file_path = path;
454 return FILE_ERROR_OK;
455}
456
[email protected]8b03ab3a2014-01-15 17:52:45457FileError FileCache::OpenForWrite(
458 const std::string& id,
dchengf42750232016-04-12 04:12:27459 std::unique_ptr<base::ScopedClosureRunner>* file_closer) {
[email protected]b568b882013-06-10 04:38:07460 AssertOnSequencedWorkerPool();
461
[email protected]b568b882013-06-10 04:38:07462 // Marking a file dirty means its entry and actual file blob must exist in
463 // cache.
[email protected]cd8fd37f2014-05-20 15:45:21464 ResourceEntry entry;
465 FileError error = storage_->GetEntry(id, &entry);
[email protected]996139412014-05-10 06:19:50466 if (error != FILE_ERROR_OK)
467 return error;
[email protected]cd8fd37f2014-05-20 15:45:21468 if (!entry.file_specific_info().cache_state().is_present()) {
[email protected]c9e4738d2013-08-26 03:04:07469 LOG(WARNING) << "Can't mark dirty a file that wasn't cached: " << id;
[email protected]b568b882013-06-10 04:38:07470 return FILE_ERROR_NOT_FOUND;
471 }
472
[email protected]cd8fd37f2014-05-20 15:45:21473 entry.mutable_file_specific_info()->mutable_cache_state()->set_is_dirty(true);
okac6aac502016-05-23 12:16:58474 if (!UnsetRemovable(GetCacheFilePath(entry.local_id())))
475 return FILE_ERROR_FAILED;
476
[email protected]cd8fd37f2014-05-20 15:45:21477 entry.mutable_file_specific_info()->mutable_cache_state()->clear_md5();
478 error = storage_->PutEntry(entry);
[email protected]996139412014-05-10 06:19:50479 if (error != FILE_ERROR_OK)
480 return error;
[email protected]b568b882013-06-10 04:38:07481
[email protected]8b03ab3a2014-01-15 17:52:45482 write_opened_files_[id]++;
483 file_closer->reset(new base::ScopedClosureRunner(
[email protected]c929c932014-07-23 06:06:05484 base::Bind(&google_apis::RunTaskWithTaskRunner,
[email protected]8b03ab3a2014-01-15 17:52:45485 blocking_task_runner_,
486 base::Bind(&FileCache::CloseForWrite,
487 weak_ptr_factory_.GetWeakPtr(),
488 id))));
489 return FILE_ERROR_OK;
490}
491
492bool FileCache::IsOpenedForWrite(const std::string& id) {
493 AssertOnSequencedWorkerPool();
lukasza81be9752015-06-17 00:14:35494 return write_opened_files_.count(id) != 0;
[email protected]b568b882013-06-10 04:38:07495}
496
[email protected]b1bf19a2014-01-21 04:45:19497FileError FileCache::UpdateMd5(const std::string& id) {
[email protected]fcf8eafe02013-05-28 11:15:39498 AssertOnSequencedWorkerPool();
[email protected]eca3fc92013-05-01 03:53:40499
[email protected]b1bf19a2014-01-21 04:45:19500 if (IsOpenedForWrite(id))
501 return FILE_ERROR_IN_USE;
502
[email protected]cd8fd37f2014-05-20 15:45:21503 ResourceEntry entry;
504 FileError error = storage_->GetEntry(id, &entry);
[email protected]996139412014-05-10 06:19:50505 if (error != FILE_ERROR_OK)
506 return error;
[email protected]cd8fd37f2014-05-20 15:45:21507 if (!entry.file_specific_info().cache_state().is_present())
[email protected]b1bf19a2014-01-21 04:45:19508 return FILE_ERROR_NOT_FOUND;
509
hashimoto246e4a82015-04-17 07:44:49510 const std::string& md5 =
511 util::GetMd5Digest(GetCacheFilePath(id), &in_shutdown_);
512 if (in_shutdown_.IsSet())
513 return FILE_ERROR_ABORT;
[email protected]b1bf19a2014-01-21 04:45:19514 if (md5.empty())
515 return FILE_ERROR_NOT_FOUND;
516
[email protected]cd8fd37f2014-05-20 15:45:21517 entry.mutable_file_specific_info()->mutable_cache_state()->set_md5(md5);
518 return storage_->PutEntry(entry);
[email protected]b1bf19a2014-01-21 04:45:19519}
520
521FileError FileCache::ClearDirty(const std::string& id) {
522 AssertOnSequencedWorkerPool();
523
524 if (IsOpenedForWrite(id))
525 return FILE_ERROR_IN_USE;
526
[email protected]fcf8eafe02013-05-28 11:15:39527 // Clearing a dirty file means its entry and actual file blob must exist in
528 // cache.
[email protected]cd8fd37f2014-05-20 15:45:21529 ResourceEntry entry;
530 FileError error = storage_->GetEntry(id, &entry);
[email protected]996139412014-05-10 06:19:50531 if (error != FILE_ERROR_OK)
532 return error;
[email protected]cd8fd37f2014-05-20 15:45:21533 if (!entry.file_specific_info().cache_state().is_present()) {
[email protected]fcf8eafe02013-05-28 11:15:39534 LOG(WARNING) << "Can't clear dirty state of a file that wasn't cached: "
[email protected]c9e4738d2013-08-26 03:04:07535 << id;
[email protected]fcf8eafe02013-05-28 11:15:39536 return FILE_ERROR_NOT_FOUND;
537 }
538
[email protected]8b03ab3a2014-01-15 17:52:45539 // If a file is not dirty (it should have been marked dirty via OpenForWrite),
540 // clearing its dirty state is an invalid operation.
[email protected]cd8fd37f2014-05-20 15:45:21541 if (!entry.file_specific_info().cache_state().is_dirty()) {
[email protected]c9e4738d2013-08-26 03:04:07542 LOG(WARNING) << "Can't clear dirty state of a non-dirty file: " << id;
[email protected]fcf8eafe02013-05-28 11:15:39543 return FILE_ERROR_INVALID_OPERATION;
544 }
545
[email protected]cd8fd37f2014-05-20 15:45:21546 entry.mutable_file_specific_info()->mutable_cache_state()->set_is_dirty(
547 false);
okac6aac502016-05-23 12:16:58548 if (!entry.file_specific_info().cache_state().is_pinned()) {
549 if (!SetRemovable(GetCacheFilePath(entry.local_id())))
550 return FILE_ERROR_FAILED;
551 }
552
[email protected]cd8fd37f2014-05-20 15:45:21553 return storage_->PutEntry(entry);
[email protected]73f9c742012-06-15 07:37:13554}
555
[email protected]c9e4738d2013-08-26 03:04:07556FileError FileCache::Remove(const std::string& id) {
[email protected]3361a542013-05-22 17:38:27557 AssertOnSequencedWorkerPool();
558
[email protected]cd8fd37f2014-05-20 15:45:21559 ResourceEntry entry;
[email protected]3361a542013-05-22 17:38:27560
[email protected]4b60a25f2013-06-17 09:43:11561 // If entry doesn't exist, nothing to do.
[email protected]cd8fd37f2014-05-20 15:45:21562 FileError error = storage_->GetEntry(id, &entry);
[email protected]996139412014-05-10 06:19:50563 if (error == FILE_ERROR_NOT_FOUND)
[email protected]3361a542013-05-22 17:38:27564 return FILE_ERROR_OK;
[email protected]996139412014-05-10 06:19:50565 if (error != FILE_ERROR_OK)
566 return error;
[email protected]cd8fd37f2014-05-20 15:45:21567 if (!entry.file_specific_info().has_cache_state())
568 return FILE_ERROR_OK;
[email protected]3361a542013-05-22 17:38:27569
[email protected]d1ad8fa2013-07-11 13:23:20570 // Cannot delete a mounted file.
[email protected]c9e4738d2013-08-26 03:04:07571 if (mounted_files_.count(id))
[email protected]4b60a25f2013-06-17 09:43:11572 return FILE_ERROR_IN_USE;
573
[email protected]91a464e62013-07-10 09:30:06574 // Delete the file.
[email protected]c9e4738d2013-08-26 03:04:07575 base::FilePath path = GetCacheFilePath(id);
[email protected]dd3aa792013-07-16 19:10:23576 if (!base::DeleteFile(path, false /* recursive */))
[email protected]91a464e62013-07-10 09:30:06577 return FILE_ERROR_FAILED;
[email protected]3361a542013-05-22 17:38:27578
579 // Now that all file operations have completed, remove from metadata.
[email protected]cd8fd37f2014-05-20 15:45:21580 entry.mutable_file_specific_info()->clear_cache_state();
581 return storage_->PutEntry(entry);
[email protected]3361a542013-05-22 17:38:27582}
583
[email protected]823ca9712013-09-13 10:09:09584bool FileCache::ClearAll() {
585 AssertOnSequencedWorkerPool();
[email protected]f861b392012-08-03 20:41:12586
[email protected]823ca9712013-09-13 10:09:09587 // Remove files.
588 base::FileEnumerator enumerator(cache_file_directory_,
589 false, // not recursive
590 base::FileEnumerator::FILES);
591 for (base::FilePath file = enumerator.Next(); !file.empty();
592 file = enumerator.Next())
593 base::DeleteFile(file, false /* recursive */);
594
595 return true;
[email protected]f861b392012-08-03 20:41:12596}
597
[email protected]34a1bbf32013-06-17 07:24:02598bool FileCache::Initialize() {
[email protected]ca5f6da2012-06-18 12:54:59599 AssertOnSequencedWorkerPool();
600
[email protected]b1bf19a2014-01-21 04:45:19601 // Older versions do not clear MD5 when marking entries dirty.
602 // Clear MD5 of all dirty entries to deal with old data.
dchengf42750232016-04-12 04:12:27603 std::unique_ptr<ResourceMetadataStorage::Iterator> it =
604 storage_->GetIterator();
[email protected]b1bf19a2014-01-21 04:45:19605 for (; !it->IsAtEnd(); it->Advance()) {
[email protected]cd8fd37f2014-05-20 15:45:21606 if (it->GetValue().file_specific_info().cache_state().is_dirty()) {
607 ResourceEntry new_entry(it->GetValue());
608 new_entry.mutable_file_specific_info()->mutable_cache_state()->
609 clear_md5();
610 if (storage_->PutEntry(new_entry) != FILE_ERROR_OK)
[email protected]b1bf19a2014-01-21 04:45:19611 return false;
612 }
613 }
[email protected]996139412014-05-10 06:19:50614 if (it->HasError())
615 return false;
[email protected]b1bf19a2014-01-21 04:45:19616
[email protected]f2731d12013-10-22 03:23:15617 if (!RenameCacheFilesToNewFormat())
618 return false;
okac6aac502016-05-23 12:16:58619
620 // Run this every time to resolve inconsistency between metadata
621 // and file attributes which possibly occurs on abrupt power failure.
622 if (!FixMetadataAndFileAttributes()) {
623 return false;
624 }
625
[email protected]e8842b192013-06-11 04:05:14626 return true;
[email protected]ca5f6da2012-06-18 12:54:59627}
628
[email protected]34a1bbf32013-06-17 07:24:02629void FileCache::Destroy() {
lukasza037c10b12015-06-12 04:21:25630 DCHECK(thread_checker_.CalledOnValidThread());
[email protected]34a1bbf32013-06-17 07:24:02631
hashimoto246e4a82015-04-17 07:44:49632 in_shutdown_.Set();
633
[email protected]34a1bbf32013-06-17 07:24:02634 // Destroy myself on the blocking pool.
635 // Note that base::DeletePointer<> cannot be used as the destructor of this
636 // class is private.
637 blocking_task_runner_->PostTask(
638 FROM_HERE,
639 base::Bind(&FileCache::DestroyOnBlockingPool, base::Unretained(this)));
640}
641
[email protected]eca3fc92013-05-01 03:53:40642void FileCache::DestroyOnBlockingPool() {
[email protected]73f9c742012-06-15 07:37:13643 AssertOnSequencedWorkerPool();
644 delete this;
645}
646
[email protected]b7af4f12013-10-31 06:57:45647bool FileCache::RecoverFilesFromCacheDirectory(
[email protected]760abc32013-11-01 05:13:01648 const base::FilePath& dest_directory,
[email protected]026d4a522013-11-05 14:22:18649 const ResourceMetadataStorage::RecoveredCacheInfoMap&
650 recovered_cache_info) {
[email protected]b7af4f12013-10-31 06:57:45651 int file_number = 1;
652
653 base::FileEnumerator enumerator(cache_file_directory_,
654 false, // not recursive
655 base::FileEnumerator::FILES);
656 for (base::FilePath current = enumerator.Next(); !current.empty();
657 current = enumerator.Next()) {
658 const std::string& id = GetIdFromPath(current);
[email protected]cd8fd37f2014-05-20 15:45:21659 ResourceEntry entry;
660 FileError error = storage_->GetEntry(id, &entry);
661 if (error != FILE_ERROR_OK && error != FILE_ERROR_NOT_FOUND)
662 return false;
663 if (error == FILE_ERROR_OK &&
664 entry.file_specific_info().cache_state().is_present()) {
[email protected]b7af4f12013-10-31 06:57:45665 // This file is managed by FileCache, no need to recover it.
666 continue;
667 }
668
[email protected]760abc32013-11-01 05:13:01669 // If a cache entry which is non-dirty and has matching MD5 is found in
670 // |recovered_cache_entries|, it means the current file is already uploaded
671 // to the server. Just delete it instead of recovering it.
[email protected]026d4a522013-11-05 14:22:18672 ResourceMetadataStorage::RecoveredCacheInfoMap::const_iterator it =
673 recovered_cache_info.find(id);
674 if (it != recovered_cache_info.end()) {
675 // Due to the DB corruption, cache info might be recovered from old
676 // revision. Perform MD5 check even when is_dirty is false just in case.
677 if (!it->second.is_dirty &&
hashimoto246e4a82015-04-17 07:44:49678 it->second.md5 == util::GetMd5Digest(current, &in_shutdown_)) {
[email protected]760abc32013-11-01 05:13:01679 base::DeleteFile(current, false /* recursive */);
680 continue;
681 }
682 }
683
[email protected]b7af4f12013-10-31 06:57:45684 // Read file contents to sniff mime type.
685 std::vector<char> content(net::kMaxBytesToSniff);
686 const int read_result =
[email protected]7600d0b2013-12-08 21:43:30687 base::ReadFile(current, &content[0], content.size());
[email protected]b7af4f12013-10-31 06:57:45688 if (read_result < 0) {
689 LOG(WARNING) << "Cannot read: " << current.value();
690 return false;
691 }
692 if (read_result == 0) // Skip empty files.
693 continue;
694
[email protected]026d4a522013-11-05 14:22:18695 // Use recovered file name if available, otherwise decide file name with
696 // sniffed mime type.
[email protected]b7af4f12013-10-31 06:57:45697 base::FilePath dest_base_name(FILE_PATH_LITERAL("file"));
698 std::string mime_type;
[email protected]026d4a522013-11-05 14:22:18699 if (it != recovered_cache_info.end() && !it->second.title.empty()) {
700 // We can use a file name recovered from the trashed DB.
701 dest_base_name = base::FilePath::FromUTF8Unsafe(it->second.title);
702 } else if (net::SniffMimeType(&content[0], read_result,
703 net::FilePathToFileURL(current),
704 std::string(), &mime_type) ||
705 net::SniffMimeTypeFromLocalData(&content[0], read_result,
706 &mime_type)) {
[email protected]b7af4f12013-10-31 06:57:45707 // Change base name for common mime types.
708 if (net::MatchesMimeType("image/*", mime_type)) {
709 dest_base_name = base::FilePath(FILE_PATH_LITERAL("image"));
710 } else if (net::MatchesMimeType("video/*", mime_type)) {
711 dest_base_name = base::FilePath(FILE_PATH_LITERAL("video"));
712 } else if (net::MatchesMimeType("audio/*", mime_type)) {
713 dest_base_name = base::FilePath(FILE_PATH_LITERAL("audio"));
714 }
715
716 // Estimate extension from mime type.
717 std::vector<base::FilePath::StringType> extensions;
718 base::FilePath::StringType extension;
719 if (net::GetPreferredExtensionForMimeType(mime_type, &extension))
720 extensions.push_back(extension);
721 else
722 net::GetExtensionsForMimeType(mime_type, &extensions);
723
724 // Add extension if possible.
725 if (!extensions.empty())
726 dest_base_name = dest_base_name.AddExtension(extensions[0]);
727 }
728
729 // Add file number to the file name and move.
730 const base::FilePath& dest_path = dest_directory.Append(dest_base_name)
731 .InsertBeforeExtensionASCII(base::StringPrintf("%08d", file_number++));
[email protected]426d1c92013-12-03 20:08:54732 if (!base::CreateDirectory(dest_directory) ||
[email protected]b7af4f12013-10-31 06:57:45733 !base::Move(current, dest_path)) {
734 LOG(WARNING) << "Failed to move: " << current.value()
735 << " to " << dest_path.value();
736 return false;
737 }
738 }
[email protected]d105b3ad2013-11-01 05:33:13739 UMA_HISTOGRAM_COUNTS("Drive.NumberOfCacheFilesRecoveredAfterDBCorruption",
740 file_number - 1);
[email protected]b7af4f12013-10-31 06:57:45741 return true;
742}
743
[email protected]54ba37502013-05-09 08:43:40744FileError FileCache::MarkAsUnmounted(const base::FilePath& file_path) {
[email protected]9564c1502012-11-28 12:12:16745 AssertOnSequencedWorkerPool();
[email protected]eca3fc92013-05-01 03:53:40746 DCHECK(IsUnderFileCacheDirectory(file_path));
[email protected]9564c1502012-11-28 12:12:16747
[email protected]c9e4738d2013-08-26 03:04:07748 std::string id = GetIdFromPath(file_path);
[email protected]9564c1502012-11-28 12:12:16749
[email protected]cd8fd37f2014-05-20 15:45:21750 // Get the entry associated with the id.
751 ResourceEntry entry;
752 FileError error = storage_->GetEntry(id, &entry);
[email protected]996139412014-05-10 06:19:50753 if (error != FILE_ERROR_OK)
754 return error;
[email protected]9564c1502012-11-28 12:12:16755
[email protected]c9e4738d2013-08-26 03:04:07756 std::set<std::string>::iterator it = mounted_files_.find(id);
[email protected]4b60a25f2013-06-17 09:43:11757 if (it == mounted_files_.end())
[email protected]78a158b2013-04-23 06:57:49758 return FILE_ERROR_INVALID_OPERATION;
[email protected]9564c1502012-11-28 12:12:16759
[email protected]4b60a25f2013-06-17 09:43:11760 mounted_files_.erase(it);
[email protected]78a158b2013-04-23 06:57:49761 return FILE_ERROR_OK;
[email protected]9564c1502012-11-28 12:12:16762}
763
avibc5337b2015-12-25 23:16:33764int64_t FileCache::GetAvailableSpace() {
765 int64_t free_space = 0;
[email protected]f6fd98a2012-12-14 00:04:02766 if (free_disk_space_getter_)
767 free_space = free_disk_space_getter_->AmountOfFreeDiskSpace();
768 else
yawano4c36b9a02015-10-23 06:17:23769 free_space = base::SysInfo::AmountOfFreeDiskSpace(cache_file_directory_);
[email protected]f6fd98a2012-12-14 00:04:02770
771 // Subtract this as if this portion does not exist.
lukasza3fb22622015-08-27 21:04:34772 free_space -= drive::internal::kMinFreeSpaceInBytes;
yawano4c36b9a02015-10-23 06:17:23773 return free_space;
[email protected]f6fd98a2012-12-14 00:04:02774}
775
[email protected]f2731d12013-10-22 03:23:15776bool FileCache::RenameCacheFilesToNewFormat() {
777 base::FileEnumerator enumerator(cache_file_directory_,
778 false, // not recursive
779 base::FileEnumerator::FILES);
780 for (base::FilePath current = enumerator.Next(); !current.empty();
781 current = enumerator.Next()) {
782 base::FilePath new_path = current.RemoveExtension();
783 if (!new_path.Extension().empty()) {
784 // Delete files with multiple extensions.
785 if (!base::DeleteFile(current, false /* recursive */))
786 return false;
787 continue;
788 }
789 const std::string& id = GetIdFromPath(new_path);
790 new_path = GetCacheFilePath(util::CanonicalizeResourceId(id));
791 if (new_path != current && !base::Move(current, new_path))
792 return false;
[email protected]91a464e62013-07-10 09:30:06793 }
[email protected]f2731d12013-10-22 03:23:15794 return true;
[email protected]91a464e62013-07-10 09:30:06795}
796
okac6aac502016-05-23 12:16:58797bool FileCache::FixMetadataAndFileAttributes() {
dchengf42750232016-04-12 04:12:27798 std::unique_ptr<ResourceMetadataStorage::Iterator> it =
okac6aac502016-05-23 12:16:58799 storage_->GetIterator();
800
yawano9fd1e632016-02-04 09:00:06801 for (; !it->IsAtEnd(); it->Advance()) {
okac6aac502016-05-23 12:16:58802 ResourceEntry entry = it->GetValue();
803 FileCacheEntry* file_cache_entry =
804 entry.mutable_file_specific_info()->mutable_cache_state();
yawano9fd1e632016-02-04 09:00:06805
okac6aac502016-05-23 12:16:58806 const base::FilePath filepath = GetPathForId(cache_file_directory_,
807 entry.local_id());
yawano9fd1e632016-02-04 09:00:06808
okac6aac502016-05-23 12:16:58809 if (base::PathExists(filepath)) {
810 if (file_cache_entry->is_present()) {
811 // Update file attribues for cryptohome.
812 if (file_cache_entry->is_pinned() || file_cache_entry->is_dirty()) {
813 if (!UnsetRemovable(filepath)) return false;
814 } else {
815 if (!SetRemovable(filepath)) return false;
816 }
817 } else {
818 // Delete file if the file is present but metadata says not.
819 // It happens only on abrupt shutdown.
820 LOG(WARNING)
821 << "File is present but metadata's state was inconsistent.";
822
823 if (!base::DeleteFile(filepath, false /* recursive */))
824 return false;
yawano1c325bf2016-04-20 06:37:03825 }
okac6aac502016-05-23 12:16:58826 } else {
827 // Update metatadata if there is no file but metadata says there is.
828 // It happens when cryptohome removed the file.
829 // We don't clear is_pinned here, so that file download is restarted on
830 // the following scenario:
831 // 1. The file was pinned but not present.
832 // 2. Then the file was downloaded and became present.
833 // 3. Unclean shutdown happens, metadata update was saved to the disk,
834 // but the file move was not.
835 if (file_cache_entry->is_present()) {
836 file_cache_entry->set_is_present(false);
837 file_cache_entry->set_is_dirty(false);
838 file_cache_entry->clear_md5();
839 if (storage_->PutEntry(entry) != FILE_ERROR_OK)
840 return false;
841 }
yawano9fd1e632016-02-04 09:00:06842 }
yawano9fd1e632016-02-04 09:00:06843 }
844
okac6aac502016-05-23 12:16:58845 return MarkAsDriveCacheDir(cache_file_directory_);
yawano9fd1e632016-02-04 09:00:06846}
847
[email protected]8b03ab3a2014-01-15 17:52:45848void FileCache::CloseForWrite(const std::string& id) {
849 AssertOnSequencedWorkerPool();
850
851 std::map<std::string, int>::iterator it = write_opened_files_.find(id);
852 if (it == write_opened_files_.end())
853 return;
854
855 DCHECK_LT(0, it->second);
856 --it->second;
857 if (it->second == 0)
858 write_opened_files_.erase(it);
[email protected]f92367ae2014-06-02 07:35:43859
860 // Update last modified date.
861 ResourceEntry entry;
862 FileError error = storage_->GetEntry(id, &entry);
863 if (error != FILE_ERROR_OK) {
864 LOG(ERROR) << "Failed to get entry: " << id << ", "
865 << FileErrorToString(error);
866 return;
867 }
868 entry.mutable_file_info()->set_last_modified(
869 base::Time::Now().ToInternalValue());
870 error = storage_->PutEntry(entry);
871 if (error != FILE_ERROR_OK) {
872 LOG(ERROR) << "Failed to put entry: " << id << ", "
873 << FileErrorToString(error);
874 }
[email protected]8b03ab3a2014-01-15 17:52:45875}
876
yawano8578abf2015-08-26 09:15:50877bool FileCache::IsEvictable(const std::string& id, const ResourceEntry& entry) {
878 return entry.file_specific_info().has_cache_state() &&
879 !entry.file_specific_info().cache_state().is_pinned() &&
880 !entry.file_specific_info().cache_state().is_dirty() &&
881 !mounted_files_.count(id);
882}
883
[email protected]59c7cdec2013-05-07 04:17:13884} // namespace internal
[email protected]d9d04df2012-10-12 07:06:35885} // namespace drive