• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // 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 
5 #include "chrome/browser/chromeos/drive/file_cache.h"
6 
7 #include <vector>
8 
9 #include "base/callback_helpers.h"
10 #include "base/files/file_enumerator.h"
11 #include "base/files/file_util.h"
12 #include "base/logging.h"
13 #include "base/metrics/histogram.h"
14 #include "base/strings/string_util.h"
15 #include "base/strings/stringprintf.h"
16 #include "base/sys_info.h"
17 #include "chrome/browser/chromeos/drive/drive.pb.h"
18 #include "chrome/browser/chromeos/drive/file_system_util.h"
19 #include "chrome/browser/chromeos/drive/resource_metadata_storage.h"
20 #include "chrome/browser/drive/drive_api_util.h"
21 #include "chromeos/chromeos_constants.h"
22 #include "content/public/browser/browser_thread.h"
23 #include "google_apis/drive/task_util.h"
24 #include "net/base/filename_util.h"
25 #include "net/base/mime_sniffer.h"
26 #include "net/base/mime_util.h"
27 #include "third_party/cros_system_api/constants/cryptohome.h"
28 
29 using content::BrowserThread;
30 
31 namespace drive {
32 namespace internal {
33 namespace {
34 
35 // Returns ID extracted from the path.
GetIdFromPath(const base::FilePath & path)36 std::string GetIdFromPath(const base::FilePath& path) {
37   return util::UnescapeCacheFileName(path.BaseName().AsUTF8Unsafe());
38 }
39 
40 }  // namespace
41 
FileCache(ResourceMetadataStorage * storage,const base::FilePath & cache_file_directory,base::SequencedTaskRunner * blocking_task_runner,FreeDiskSpaceGetterInterface * free_disk_space_getter)42 FileCache::FileCache(ResourceMetadataStorage* storage,
43                      const base::FilePath& cache_file_directory,
44                      base::SequencedTaskRunner* blocking_task_runner,
45                      FreeDiskSpaceGetterInterface* free_disk_space_getter)
46     : cache_file_directory_(cache_file_directory),
47       blocking_task_runner_(blocking_task_runner),
48       storage_(storage),
49       free_disk_space_getter_(free_disk_space_getter),
50       weak_ptr_factory_(this) {
51   DCHECK(blocking_task_runner_.get());
52   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
53 }
54 
~FileCache()55 FileCache::~FileCache() {
56   // Must be on the sequenced worker pool, as |metadata_| must be deleted on
57   // the sequenced worker pool.
58   AssertOnSequencedWorkerPool();
59 }
60 
GetCacheFilePath(const std::string & id) const61 base::FilePath FileCache::GetCacheFilePath(const std::string& id) const {
62   return cache_file_directory_.Append(
63       base::FilePath::FromUTF8Unsafe(util::EscapeCacheFileName(id)));
64 }
65 
AssertOnSequencedWorkerPool()66 void FileCache::AssertOnSequencedWorkerPool() {
67   DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread());
68 }
69 
IsUnderFileCacheDirectory(const base::FilePath & path) const70 bool FileCache::IsUnderFileCacheDirectory(const base::FilePath& path) const {
71   return cache_file_directory_.IsParent(path);
72 }
73 
FreeDiskSpaceIfNeededFor(int64 num_bytes)74 bool FileCache::FreeDiskSpaceIfNeededFor(int64 num_bytes) {
75   AssertOnSequencedWorkerPool();
76 
77   // Do nothing and return if we have enough space.
78   if (HasEnoughSpaceFor(num_bytes, cache_file_directory_))
79     return true;
80 
81   // Otherwise, try to free up the disk space.
82   DVLOG(1) << "Freeing up disk space for " << num_bytes;
83 
84   // Remove all entries unless specially marked.
85   scoped_ptr<ResourceMetadataStorage::Iterator> it = storage_->GetIterator();
86   for (; !it->IsAtEnd(); it->Advance()) {
87     if (it->GetValue().file_specific_info().has_cache_state() &&
88         !it->GetValue().file_specific_info().cache_state().is_pinned() &&
89         !it->GetValue().file_specific_info().cache_state().is_dirty() &&
90         !mounted_files_.count(it->GetID())) {
91       ResourceEntry entry(it->GetValue());
92       entry.mutable_file_specific_info()->clear_cache_state();
93       storage_->PutEntry(entry);
94     }
95   }
96   if (it->HasError())
97     return false;
98 
99   // Remove all files which have no corresponding cache entries.
100   base::FileEnumerator enumerator(cache_file_directory_,
101                                   false,  // not recursive
102                                   base::FileEnumerator::FILES);
103   ResourceEntry entry;
104   for (base::FilePath current = enumerator.Next(); !current.empty();
105        current = enumerator.Next()) {
106     std::string id = GetIdFromPath(current);
107     FileError error = storage_->GetEntry(id, &entry);
108     if (error == FILE_ERROR_NOT_FOUND ||
109         (error == FILE_ERROR_OK &&
110          !entry.file_specific_info().cache_state().is_present()))
111       base::DeleteFile(current, false /* recursive */);
112     else if (error != FILE_ERROR_OK)
113       return false;
114   }
115 
116   // Check the disk space again.
117   return HasEnoughSpaceFor(num_bytes, cache_file_directory_);
118 }
119 
GetFile(const std::string & id,base::FilePath * cache_file_path)120 FileError FileCache::GetFile(const std::string& id,
121                              base::FilePath* cache_file_path) {
122   AssertOnSequencedWorkerPool();
123   DCHECK(cache_file_path);
124 
125   ResourceEntry entry;
126   FileError error = storage_->GetEntry(id, &entry);
127   if (error != FILE_ERROR_OK)
128     return error;
129   if (!entry.file_specific_info().cache_state().is_present())
130     return FILE_ERROR_NOT_FOUND;
131 
132   *cache_file_path = GetCacheFilePath(id);
133   return FILE_ERROR_OK;
134 }
135 
Store(const std::string & id,const std::string & md5,const base::FilePath & source_path,FileOperationType file_operation_type)136 FileError FileCache::Store(const std::string& id,
137                            const std::string& md5,
138                            const base::FilePath& source_path,
139                            FileOperationType file_operation_type) {
140   AssertOnSequencedWorkerPool();
141 
142   ResourceEntry entry;
143   FileError error = storage_->GetEntry(id, &entry);
144   if (error != FILE_ERROR_OK)
145     return error;
146 
147   int64 file_size = 0;
148   if (file_operation_type == FILE_OPERATION_COPY) {
149     if (!base::GetFileSize(source_path, &file_size)) {
150       LOG(WARNING) << "Couldn't get file size for: " << source_path.value();
151       return FILE_ERROR_FAILED;
152     }
153   }
154   if (!FreeDiskSpaceIfNeededFor(file_size))
155     return FILE_ERROR_NO_LOCAL_SPACE;
156 
157   // If file is mounted, return error.
158   if (mounted_files_.count(id))
159     return FILE_ERROR_IN_USE;
160 
161   base::FilePath dest_path = GetCacheFilePath(id);
162   bool success = false;
163   switch (file_operation_type) {
164     case FILE_OPERATION_MOVE:
165       success = base::Move(source_path, dest_path);
166       break;
167     case FILE_OPERATION_COPY:
168       success = base::CopyFile(source_path, dest_path);
169       break;
170     default:
171       NOTREACHED();
172   }
173 
174   if (!success) {
175     LOG(ERROR) << "Failed to store: "
176                << "source_path = " << source_path.value() << ", "
177                << "dest_path = " << dest_path.value() << ", "
178                << "file_operation_type = " << file_operation_type;
179     return FILE_ERROR_FAILED;
180   }
181 
182   // Now that file operations have completed, update metadata.
183   FileCacheEntry* cache_state =
184       entry.mutable_file_specific_info()->mutable_cache_state();
185   cache_state->set_md5(md5);
186   cache_state->set_is_present(true);
187   if (md5.empty())
188     cache_state->set_is_dirty(true);
189   return storage_->PutEntry(entry);
190 }
191 
Pin(const std::string & id)192 FileError FileCache::Pin(const std::string& id) {
193   AssertOnSequencedWorkerPool();
194 
195   ResourceEntry entry;
196   FileError error = storage_->GetEntry(id, &entry);
197   if (error != FILE_ERROR_OK)
198     return error;
199   entry.mutable_file_specific_info()->mutable_cache_state()->set_is_pinned(
200       true);
201   return storage_->PutEntry(entry);
202 }
203 
Unpin(const std::string & id)204 FileError FileCache::Unpin(const std::string& id) {
205   AssertOnSequencedWorkerPool();
206 
207   // Unpinning a file means its entry must exist in cache.
208   ResourceEntry entry;
209   FileError error = storage_->GetEntry(id, &entry);
210   if (error != FILE_ERROR_OK)
211     return error;
212 
213   // Now that file operations have completed, update metadata.
214   if (entry.file_specific_info().cache_state().is_present()) {
215     entry.mutable_file_specific_info()->mutable_cache_state()->set_is_pinned(
216         false);
217   } else {
218     // Remove the existing entry if we are unpinning a non-present file.
219     entry.mutable_file_specific_info()->clear_cache_state();
220   }
221   error = storage_->PutEntry(entry);
222   if (error != FILE_ERROR_OK)
223     return error;
224 
225   // Now it's a chance to free up space if needed.
226   FreeDiskSpaceIfNeededFor(0);
227 
228   return FILE_ERROR_OK;
229 }
230 
MarkAsMounted(const std::string & id,base::FilePath * cache_file_path)231 FileError FileCache::MarkAsMounted(const std::string& id,
232                                    base::FilePath* cache_file_path) {
233   AssertOnSequencedWorkerPool();
234   DCHECK(cache_file_path);
235 
236   // Get cache entry associated with the id and md5
237   ResourceEntry entry;
238   FileError error = storage_->GetEntry(id, &entry);
239   if (error != FILE_ERROR_OK)
240     return error;
241   if (!entry.file_specific_info().cache_state().is_present())
242     return FILE_ERROR_NOT_FOUND;
243 
244   if (mounted_files_.count(id))
245     return FILE_ERROR_INVALID_OPERATION;
246 
247   // Ensure the file is readable to cros_disks. See crbug.com/236994.
248   base::FilePath path = GetCacheFilePath(id);
249   if (!base::SetPosixFilePermissions(
250           path,
251           base::FILE_PERMISSION_READ_BY_USER |
252           base::FILE_PERMISSION_WRITE_BY_USER |
253           base::FILE_PERMISSION_READ_BY_GROUP |
254           base::FILE_PERMISSION_READ_BY_OTHERS))
255     return FILE_ERROR_FAILED;
256 
257   mounted_files_.insert(id);
258 
259   *cache_file_path = path;
260   return FILE_ERROR_OK;
261 }
262 
OpenForWrite(const std::string & id,scoped_ptr<base::ScopedClosureRunner> * file_closer)263 FileError FileCache::OpenForWrite(
264     const std::string& id,
265     scoped_ptr<base::ScopedClosureRunner>* file_closer) {
266   AssertOnSequencedWorkerPool();
267 
268   // Marking a file dirty means its entry and actual file blob must exist in
269   // cache.
270   ResourceEntry entry;
271   FileError error = storage_->GetEntry(id, &entry);
272   if (error != FILE_ERROR_OK)
273     return error;
274   if (!entry.file_specific_info().cache_state().is_present()) {
275     LOG(WARNING) << "Can't mark dirty a file that wasn't cached: " << id;
276     return FILE_ERROR_NOT_FOUND;
277   }
278 
279   entry.mutable_file_specific_info()->mutable_cache_state()->set_is_dirty(true);
280   entry.mutable_file_specific_info()->mutable_cache_state()->clear_md5();
281   error = storage_->PutEntry(entry);
282   if (error != FILE_ERROR_OK)
283     return error;
284 
285   write_opened_files_[id]++;
286   file_closer->reset(new base::ScopedClosureRunner(
287       base::Bind(&google_apis::RunTaskWithTaskRunner,
288                  blocking_task_runner_,
289                  base::Bind(&FileCache::CloseForWrite,
290                             weak_ptr_factory_.GetWeakPtr(),
291                             id))));
292   return FILE_ERROR_OK;
293 }
294 
IsOpenedForWrite(const std::string & id)295 bool FileCache::IsOpenedForWrite(const std::string& id) {
296   AssertOnSequencedWorkerPool();
297   return write_opened_files_.count(id);
298 }
299 
UpdateMd5(const std::string & id)300 FileError FileCache::UpdateMd5(const std::string& id) {
301   AssertOnSequencedWorkerPool();
302 
303   if (IsOpenedForWrite(id))
304     return FILE_ERROR_IN_USE;
305 
306   ResourceEntry entry;
307   FileError error = storage_->GetEntry(id, &entry);
308   if (error != FILE_ERROR_OK)
309     return error;
310   if (!entry.file_specific_info().cache_state().is_present())
311     return FILE_ERROR_NOT_FOUND;
312 
313   const std::string& md5 = util::GetMd5Digest(GetCacheFilePath(id));
314   if (md5.empty())
315     return FILE_ERROR_NOT_FOUND;
316 
317   entry.mutable_file_specific_info()->mutable_cache_state()->set_md5(md5);
318   return storage_->PutEntry(entry);
319 }
320 
ClearDirty(const std::string & id)321 FileError FileCache::ClearDirty(const std::string& id) {
322   AssertOnSequencedWorkerPool();
323 
324   if (IsOpenedForWrite(id))
325     return FILE_ERROR_IN_USE;
326 
327   // Clearing a dirty file means its entry and actual file blob must exist in
328   // cache.
329   ResourceEntry entry;
330   FileError error = storage_->GetEntry(id, &entry);
331   if (error != FILE_ERROR_OK)
332     return error;
333   if (!entry.file_specific_info().cache_state().is_present()) {
334     LOG(WARNING) << "Can't clear dirty state of a file that wasn't cached: "
335                  << id;
336     return FILE_ERROR_NOT_FOUND;
337   }
338 
339   // If a file is not dirty (it should have been marked dirty via OpenForWrite),
340   // clearing its dirty state is an invalid operation.
341   if (!entry.file_specific_info().cache_state().is_dirty()) {
342     LOG(WARNING) << "Can't clear dirty state of a non-dirty file: " << id;
343     return FILE_ERROR_INVALID_OPERATION;
344   }
345 
346   entry.mutable_file_specific_info()->mutable_cache_state()->set_is_dirty(
347       false);
348   return storage_->PutEntry(entry);
349 }
350 
Remove(const std::string & id)351 FileError FileCache::Remove(const std::string& id) {
352   AssertOnSequencedWorkerPool();
353 
354   ResourceEntry entry;
355 
356   // If entry doesn't exist, nothing to do.
357   FileError error = storage_->GetEntry(id, &entry);
358   if (error == FILE_ERROR_NOT_FOUND)
359     return FILE_ERROR_OK;
360   if (error != FILE_ERROR_OK)
361     return error;
362   if (!entry.file_specific_info().has_cache_state())
363     return FILE_ERROR_OK;
364 
365   // Cannot delete a mounted file.
366   if (mounted_files_.count(id))
367     return FILE_ERROR_IN_USE;
368 
369   // Delete the file.
370   base::FilePath path = GetCacheFilePath(id);
371   if (!base::DeleteFile(path, false /* recursive */))
372     return FILE_ERROR_FAILED;
373 
374   // Now that all file operations have completed, remove from metadata.
375   entry.mutable_file_specific_info()->clear_cache_state();
376   return storage_->PutEntry(entry);
377 }
378 
ClearAll()379 bool FileCache::ClearAll() {
380   AssertOnSequencedWorkerPool();
381 
382   // Remove files.
383   base::FileEnumerator enumerator(cache_file_directory_,
384                                   false,  // not recursive
385                                   base::FileEnumerator::FILES);
386   for (base::FilePath file = enumerator.Next(); !file.empty();
387        file = enumerator.Next())
388     base::DeleteFile(file, false /* recursive */);
389 
390   return true;
391 }
392 
Initialize()393 bool FileCache::Initialize() {
394   AssertOnSequencedWorkerPool();
395 
396   // Older versions do not clear MD5 when marking entries dirty.
397   // Clear MD5 of all dirty entries to deal with old data.
398   scoped_ptr<ResourceMetadataStorage::Iterator> it = storage_->GetIterator();
399   for (; !it->IsAtEnd(); it->Advance()) {
400     if (it->GetValue().file_specific_info().cache_state().is_dirty()) {
401       ResourceEntry new_entry(it->GetValue());
402       new_entry.mutable_file_specific_info()->mutable_cache_state()->
403           clear_md5();
404       if (storage_->PutEntry(new_entry) != FILE_ERROR_OK)
405         return false;
406     }
407   }
408   if (it->HasError())
409     return false;
410 
411   if (!RenameCacheFilesToNewFormat())
412     return false;
413   return true;
414 }
415 
Destroy()416 void FileCache::Destroy() {
417   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
418 
419   // Destroy myself on the blocking pool.
420   // Note that base::DeletePointer<> cannot be used as the destructor of this
421   // class is private.
422   blocking_task_runner_->PostTask(
423       FROM_HERE,
424       base::Bind(&FileCache::DestroyOnBlockingPool, base::Unretained(this)));
425 }
426 
DestroyOnBlockingPool()427 void FileCache::DestroyOnBlockingPool() {
428   AssertOnSequencedWorkerPool();
429   delete this;
430 }
431 
RecoverFilesFromCacheDirectory(const base::FilePath & dest_directory,const ResourceMetadataStorage::RecoveredCacheInfoMap & recovered_cache_info)432 bool FileCache::RecoverFilesFromCacheDirectory(
433     const base::FilePath& dest_directory,
434     const ResourceMetadataStorage::RecoveredCacheInfoMap&
435         recovered_cache_info) {
436   int file_number = 1;
437 
438   base::FileEnumerator enumerator(cache_file_directory_,
439                                   false,  // not recursive
440                                   base::FileEnumerator::FILES);
441   for (base::FilePath current = enumerator.Next(); !current.empty();
442        current = enumerator.Next()) {
443     const std::string& id = GetIdFromPath(current);
444     ResourceEntry entry;
445     FileError error = storage_->GetEntry(id, &entry);
446     if (error != FILE_ERROR_OK && error != FILE_ERROR_NOT_FOUND)
447       return false;
448     if (error == FILE_ERROR_OK &&
449         entry.file_specific_info().cache_state().is_present()) {
450       // This file is managed by FileCache, no need to recover it.
451       continue;
452     }
453 
454     // If a cache entry which is non-dirty and has matching MD5 is found in
455     // |recovered_cache_entries|, it means the current file is already uploaded
456     // to the server. Just delete it instead of recovering it.
457     ResourceMetadataStorage::RecoveredCacheInfoMap::const_iterator it =
458         recovered_cache_info.find(id);
459     if (it != recovered_cache_info.end()) {
460       // Due to the DB corruption, cache info might be recovered from old
461       // revision. Perform MD5 check even when is_dirty is false just in case.
462       if (!it->second.is_dirty &&
463           it->second.md5 == util::GetMd5Digest(current)) {
464         base::DeleteFile(current, false /* recursive */);
465         continue;
466       }
467     }
468 
469     // Read file contents to sniff mime type.
470     std::vector<char> content(net::kMaxBytesToSniff);
471     const int read_result =
472         base::ReadFile(current, &content[0], content.size());
473     if (read_result < 0) {
474       LOG(WARNING) << "Cannot read: " << current.value();
475       return false;
476     }
477     if (read_result == 0)  // Skip empty files.
478       continue;
479 
480     // Use recovered file name if available, otherwise decide file name with
481     // sniffed mime type.
482     base::FilePath dest_base_name(FILE_PATH_LITERAL("file"));
483     std::string mime_type;
484     if (it != recovered_cache_info.end() && !it->second.title.empty()) {
485       // We can use a file name recovered from the trashed DB.
486       dest_base_name = base::FilePath::FromUTF8Unsafe(it->second.title);
487     } else if (net::SniffMimeType(&content[0], read_result,
488                                   net::FilePathToFileURL(current),
489                                   std::string(), &mime_type) ||
490                net::SniffMimeTypeFromLocalData(&content[0], read_result,
491                                                &mime_type)) {
492       // Change base name for common mime types.
493       if (net::MatchesMimeType("image/*", mime_type)) {
494         dest_base_name = base::FilePath(FILE_PATH_LITERAL("image"));
495       } else if (net::MatchesMimeType("video/*", mime_type)) {
496         dest_base_name = base::FilePath(FILE_PATH_LITERAL("video"));
497       } else if (net::MatchesMimeType("audio/*", mime_type)) {
498         dest_base_name = base::FilePath(FILE_PATH_LITERAL("audio"));
499       }
500 
501       // Estimate extension from mime type.
502       std::vector<base::FilePath::StringType> extensions;
503       base::FilePath::StringType extension;
504       if (net::GetPreferredExtensionForMimeType(mime_type, &extension))
505         extensions.push_back(extension);
506       else
507         net::GetExtensionsForMimeType(mime_type, &extensions);
508 
509       // Add extension if possible.
510       if (!extensions.empty())
511         dest_base_name = dest_base_name.AddExtension(extensions[0]);
512     }
513 
514     // Add file number to the file name and move.
515     const base::FilePath& dest_path = dest_directory.Append(dest_base_name)
516         .InsertBeforeExtensionASCII(base::StringPrintf("%08d", file_number++));
517     if (!base::CreateDirectory(dest_directory) ||
518         !base::Move(current, dest_path)) {
519       LOG(WARNING) << "Failed to move: " << current.value()
520                    << " to " << dest_path.value();
521       return false;
522     }
523   }
524   UMA_HISTOGRAM_COUNTS("Drive.NumberOfCacheFilesRecoveredAfterDBCorruption",
525                        file_number - 1);
526   return true;
527 }
528 
MarkAsUnmounted(const base::FilePath & file_path)529 FileError FileCache::MarkAsUnmounted(const base::FilePath& file_path) {
530   AssertOnSequencedWorkerPool();
531   DCHECK(IsUnderFileCacheDirectory(file_path));
532 
533   std::string id = GetIdFromPath(file_path);
534 
535   // Get the entry associated with the id.
536   ResourceEntry entry;
537   FileError error = storage_->GetEntry(id, &entry);
538   if (error != FILE_ERROR_OK)
539     return error;
540 
541   std::set<std::string>::iterator it = mounted_files_.find(id);
542   if (it == mounted_files_.end())
543     return FILE_ERROR_INVALID_OPERATION;
544 
545   mounted_files_.erase(it);
546   return FILE_ERROR_OK;
547 }
548 
HasEnoughSpaceFor(int64 num_bytes,const base::FilePath & path)549 bool FileCache::HasEnoughSpaceFor(int64 num_bytes,
550                                   const base::FilePath& path) {
551   int64 free_space = 0;
552   if (free_disk_space_getter_)
553     free_space = free_disk_space_getter_->AmountOfFreeDiskSpace();
554   else
555     free_space = base::SysInfo::AmountOfFreeDiskSpace(path);
556 
557   // Subtract this as if this portion does not exist.
558   free_space -= cryptohome::kMinFreeSpaceInBytes;
559   return (free_space >= num_bytes);
560 }
561 
RenameCacheFilesToNewFormat()562 bool FileCache::RenameCacheFilesToNewFormat() {
563   base::FileEnumerator enumerator(cache_file_directory_,
564                                   false,  // not recursive
565                                   base::FileEnumerator::FILES);
566   for (base::FilePath current = enumerator.Next(); !current.empty();
567        current = enumerator.Next()) {
568     base::FilePath new_path = current.RemoveExtension();
569     if (!new_path.Extension().empty()) {
570       // Delete files with multiple extensions.
571       if (!base::DeleteFile(current, false /* recursive */))
572         return false;
573       continue;
574     }
575     const std::string& id = GetIdFromPath(new_path);
576     new_path = GetCacheFilePath(util::CanonicalizeResourceId(id));
577     if (new_path != current && !base::Move(current, new_path))
578       return false;
579   }
580   return true;
581 }
582 
CloseForWrite(const std::string & id)583 void FileCache::CloseForWrite(const std::string& id) {
584   AssertOnSequencedWorkerPool();
585 
586   std::map<std::string, int>::iterator it = write_opened_files_.find(id);
587   if (it == write_opened_files_.end())
588     return;
589 
590   DCHECK_LT(0, it->second);
591   --it->second;
592   if (it->second == 0)
593     write_opened_files_.erase(it);
594 
595   // Update last modified date.
596   ResourceEntry entry;
597   FileError error = storage_->GetEntry(id, &entry);
598   if (error != FILE_ERROR_OK) {
599     LOG(ERROR) << "Failed to get entry: " << id << ", "
600                << FileErrorToString(error);
601     return;
602   }
603   entry.mutable_file_info()->set_last_modified(
604       base::Time::Now().ToInternalValue());
605   error = storage_->PutEntry(entry);
606   if (error != FILE_ERROR_OK) {
607     LOG(ERROR) << "Failed to put entry: " << id << ", "
608                << FileErrorToString(error);
609   }
610 }
611 
612 }  // namespace internal
613 }  // namespace drive
614