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/change_list_processor.h"
6
7#include "base/metrics/histogram.h"
8#include "base/strings/string_number_conversions.h"
9#include "chrome/browser/chromeos/drive/drive.pb.h"
10#include "chrome/browser/chromeos/drive/file_system_util.h"
11#include "chrome/browser/chromeos/drive/resource_entry_conversion.h"
12#include "chrome/browser/chromeos/drive/resource_metadata.h"
13#include "google_apis/drive/drive_api_parser.h"
14#include "google_apis/drive/gdata_wapi_parser.h"
15
16namespace drive {
17namespace internal {
18
19namespace {
20
21class ChangeListToEntryMapUMAStats {
22 public:
23  ChangeListToEntryMapUMAStats()
24    : num_regular_files_(0),
25      num_hosted_documents_(0),
26      num_shared_with_me_entries_(0) {
27  }
28
29  // Increments number of files.
30  void IncrementNumFiles(bool is_hosted_document) {
31    is_hosted_document ? num_hosted_documents_++ : num_regular_files_++;
32  }
33
34  // Increments number of shared-with-me entries.
35  void IncrementNumSharedWithMeEntries() {
36    num_shared_with_me_entries_++;
37  }
38
39  // Updates UMA histograms with file counts.
40  void UpdateFileCountUmaHistograms() {
41    const int num_total_files = num_hosted_documents_ + num_regular_files_;
42    UMA_HISTOGRAM_COUNTS("Drive.NumberOfRegularFiles", num_regular_files_);
43    UMA_HISTOGRAM_COUNTS("Drive.NumberOfHostedDocuments",
44                         num_hosted_documents_);
45    UMA_HISTOGRAM_COUNTS("Drive.NumberOfTotalFiles", num_total_files);
46    UMA_HISTOGRAM_COUNTS("Drive.NumberOfSharedWithMeEntries",
47                         num_shared_with_me_entries_);
48  }
49
50 private:
51  int num_regular_files_;
52  int num_hosted_documents_;
53  int num_shared_with_me_entries_;
54};
55
56}  // namespace
57
58std::string DirectoryFetchInfo::ToString() const {
59  return ("resource_id: " + resource_id_ +
60          ", changestamp: " + base::Int64ToString(changestamp_));
61}
62
63ChangeList::ChangeList() {}
64
65ChangeList::ChangeList(const google_apis::ResourceList& resource_list)
66    : largest_changestamp_(resource_list.largest_changestamp()) {
67  resource_list.GetNextFeedURL(&next_url_);
68
69  entries_.resize(resource_list.entries().size());
70  parent_resource_ids_.resize(resource_list.entries().size());
71  size_t entries_index = 0;
72  for (size_t i = 0; i < resource_list.entries().size(); ++i) {
73    if (ConvertToResourceEntry(*resource_list.entries()[i],
74                               &entries_[entries_index],
75                               &parent_resource_ids_[entries_index]))
76      ++entries_index;
77  }
78  entries_.resize(entries_index);
79  parent_resource_ids_.resize(entries_index);
80}
81
82ChangeList::~ChangeList() {}
83
84ChangeListProcessor::ChangeListProcessor(ResourceMetadata* resource_metadata)
85  : resource_metadata_(resource_metadata) {
86}
87
88ChangeListProcessor::~ChangeListProcessor() {
89}
90
91FileError ChangeListProcessor::Apply(
92    scoped_ptr<google_apis::AboutResource> about_resource,
93    ScopedVector<ChangeList> change_lists,
94    bool is_delta_update) {
95  DCHECK(about_resource);
96
97  int64 largest_changestamp = 0;
98  if (is_delta_update) {
99    if (!change_lists.empty()) {
100      // The changestamp appears in the first page of the change list.
101      // The changestamp does not appear in the full resource list.
102      largest_changestamp = change_lists[0]->largest_changestamp();
103      DCHECK_GE(change_lists[0]->largest_changestamp(), 0);
104    }
105  } else {
106    largest_changestamp = about_resource->largest_change_id();
107
108    DVLOG(1) << "Root folder ID is " << about_resource->root_folder_id();
109    DCHECK(!about_resource->root_folder_id().empty());
110  }
111
112  // Convert ChangeList to map.
113  ChangeListToEntryMapUMAStats uma_stats;
114  for (size_t i = 0; i < change_lists.size(); ++i) {
115    ChangeList* change_list = change_lists[i];
116
117    std::vector<ResourceEntry>* entries = change_list->mutable_entries();
118    for (size_t i = 0; i < entries->size(); ++i) {
119      ResourceEntry* entry = &(*entries)[i];
120
121      // Count the number of files.
122      if (!entry->file_info().is_directory()) {
123        uma_stats.IncrementNumFiles(
124            entry->file_specific_info().is_hosted_document());
125        if (entry->shared_with_me())
126          uma_stats.IncrementNumSharedWithMeEntries();
127      }
128      parent_resource_id_map_[entry->resource_id()] =
129          change_list->parent_resource_ids()[i];
130      entry_map_[entry->resource_id()].Swap(entry);
131      LOG_IF(WARNING, !entry->resource_id().empty())
132          << "Found duplicated file: " << entry->base_name();
133    }
134  }
135
136  // Add the largest changestamp for directories.
137  for (ResourceEntryMap::iterator it = entry_map_.begin();
138       it != entry_map_.end(); ++it) {
139    if (it->second.file_info().is_directory()) {
140      it->second.mutable_directory_specific_info()->set_changestamp(
141          largest_changestamp);
142    }
143  }
144
145  FileError error = ApplyEntryMap(largest_changestamp, about_resource.Pass());
146  if (error != FILE_ERROR_OK) {
147    DLOG(ERROR) << "ApplyEntryMap failed: " << FileErrorToString(error);
148    return error;
149  }
150
151  // Update changestamp.
152  error = resource_metadata_->SetLargestChangestamp(largest_changestamp);
153  if (error != FILE_ERROR_OK) {
154    DLOG(ERROR) << "SetLargestChangeStamp failed: " << FileErrorToString(error);
155    return error;
156  }
157
158  // Shouldn't record histograms when processing delta update.
159  if (!is_delta_update)
160    uma_stats.UpdateFileCountUmaHistograms();
161
162  return FILE_ERROR_OK;
163}
164
165FileError ChangeListProcessor::ApplyEntryMap(
166    int64 changestamp,
167    scoped_ptr<google_apis::AboutResource> about_resource) {
168  DCHECK(about_resource);
169
170  // Create the entry for "My Drive" directory with the latest changestamp.
171  ResourceEntry root;
172  FileError error = resource_metadata_->GetResourceEntryByPath(
173      util::GetDriveMyDriveRootPath(), &root);
174  switch (error) {
175    case FILE_ERROR_OK:  // Refresh the existing My Drive root entry.
176      root.mutable_directory_specific_info()->set_changestamp(changestamp);
177      error = resource_metadata_->RefreshEntry(root);
178      break;
179    case FILE_ERROR_NOT_FOUND: {  // Add the My Drive root directory.
180      changed_dirs_.insert(util::GetDriveGrandRootPath());
181      changed_dirs_.insert(util::GetDriveMyDriveRootPath());
182
183      root = util::CreateMyDriveRootEntry(about_resource->root_folder_id());
184      root.mutable_directory_specific_info()->set_changestamp(changestamp);
185      std::string local_id;
186      error = resource_metadata_->AddEntry(root, &local_id);
187      break;
188    }
189    default:
190      break;
191  }
192  if (error != FILE_ERROR_OK) {
193    LOG(ERROR) << "Failed to update root entry: " << FileErrorToString(error);
194    return error;
195  }
196
197  // Gather the set of changes in the old path.
198  // Note that we want to notify the change in both old and new paths (suppose
199  // /a/b/c is moved to /x/y/c. We want to notify both "/a/b" and "/x/y".)
200  // The old paths must be calculated before we apply any actual changes.
201  // The new paths are calculated after each change is applied. It correctly
202  // sets the new path because we apply changes in such an order (see below).
203  for (ResourceEntryMap::iterator it = entry_map_.begin();
204       it != entry_map_.end(); ++it) {
205    UpdateChangedDirs(it->second);
206  }
207
208  // Apply all entries except deleted ones to the metadata.
209  std::vector<std::string> deleted_resource_ids;
210  while (!entry_map_.empty()) {
211    ResourceEntryMap::iterator it = entry_map_.begin();
212
213    // Process deleted entries later to avoid deleting moved entries under it.
214    if (it->second.deleted()) {
215      deleted_resource_ids.push_back(it->first);
216      entry_map_.erase(it);
217      continue;
218    }
219
220    // Start from entry_map_.begin() and traverse ancestors using the
221    // parent-child relationships in the result (after this apply) tree.
222    // Then apply the topmost change first.
223    //
224    // By doing this, assuming the result tree does not contain any cycles, we
225    // can guarantee that no cycle is made during this apply (i.e. no entry gets
226    // moved under any of its descendants) because the following conditions are
227    // always satisfied in any move:
228    // - The new parent entry is not a descendant of the moved entry.
229    // - The new parent and its ancestors will no longer move during this apply.
230    std::vector<ResourceEntryMap::iterator> entries;
231    for (ResourceEntryMap::iterator it = entry_map_.begin();
232         it != entry_map_.end();) {
233      entries.push_back(it);
234
235      const std::string& parent_resource_id =
236          parent_resource_id_map_[it->first];
237      DCHECK(!parent_resource_id.empty()) << it->first;
238
239      ResourceEntryMap::iterator it_parent =
240          entry_map_.find(parent_resource_id);
241      if (it_parent == entry_map_.end()) {
242        // Current entry's parent is already updated or not going to be updated,
243        // get the parent from the local tree.
244        std::string parent_local_id;
245        FileError error = resource_metadata_->GetIdByResourceId(
246            parent_resource_id, &parent_local_id);
247        if (error != FILE_ERROR_OK) {
248          LOG(ERROR) << "Failed to get local ID: " << parent_resource_id
249                     << ", error = " << FileErrorToString(error);
250          break;
251        }
252        ResourceEntry parent_entry;
253        while (it_parent == entry_map_.end() && !parent_local_id.empty()) {
254          error = resource_metadata_->GetResourceEntryById(
255              parent_local_id, &parent_entry);
256          if (error != FILE_ERROR_OK) {
257            LOG(ERROR) << "Failed to get local entry: "
258                       << FileErrorToString(error);
259            break;
260          }
261          it_parent = entry_map_.find(parent_entry.resource_id());
262          parent_local_id = parent_entry.parent_local_id();
263        }
264      }
265      it = it_parent;
266    }
267
268    // Apply the parent first.
269    std::reverse(entries.begin(), entries.end());
270    for (size_t i = 0; i < entries.size(); ++i) {
271      // Skip root entry in the change list. We don't expect servers to send
272      // root entry, but we should better be defensive (see crbug.com/297259).
273      ResourceEntryMap::iterator it = entries[i];
274      if (it->first != root.resource_id()) {
275        // TODO(hashimoto): Handle ApplyEntry errors correctly.
276        FileError error = ApplyEntry(it->second);
277        DLOG_IF(WARNING, error != FILE_ERROR_OK)
278            << "ApplyEntry failed: " << FileErrorToString(error)
279            << ", title = " << it->second.title();
280      }
281      entry_map_.erase(it);
282    }
283  }
284
285  // Apply deleted entries.
286  for (size_t i = 0; i < deleted_resource_ids.size(); ++i) {
287    std::string local_id;
288    FileError error = resource_metadata_->GetIdByResourceId(
289        deleted_resource_ids[i], &local_id);
290    if (error == FILE_ERROR_OK)
291      error = resource_metadata_->RemoveEntry(local_id);
292
293    DLOG_IF(WARNING, error != FILE_ERROR_OK && error != FILE_ERROR_NOT_FOUND)
294        << "Failed to delete: " << FileErrorToString(error)
295        << ", resource_id = " << deleted_resource_ids[i];
296  }
297
298  return FILE_ERROR_OK;
299}
300
301FileError ChangeListProcessor::ApplyEntry(const ResourceEntry& entry) {
302  DCHECK(!entry.deleted());
303
304  const std::string& parent_resource_id =
305      parent_resource_id_map_[entry.resource_id()];
306  DCHECK(!parent_resource_id.empty()) << entry.resource_id();
307
308  std::string parent_local_id;
309  FileError error = resource_metadata_->GetIdByResourceId(
310      parent_resource_id, &parent_local_id);
311  if (error != FILE_ERROR_OK)
312    return error;
313
314  // Lookup the entry.
315  std::string local_id;
316  error = resource_metadata_->GetIdByResourceId(entry.resource_id(), &local_id);
317
318  ResourceEntry existing_entry;
319  if (error == FILE_ERROR_OK)
320    error = resource_metadata_->GetResourceEntryById(local_id, &existing_entry);
321
322  ResourceEntry new_entry(entry);
323  new_entry.set_parent_local_id(parent_local_id);
324
325  switch (error) {
326    case FILE_ERROR_OK:  // Entry exists and needs to be refreshed.
327      new_entry.set_local_id(local_id);
328      error = resource_metadata_->RefreshEntry(new_entry);
329      break;
330    case FILE_ERROR_NOT_FOUND: {  // Adding a new entry.
331      std::string local_id;
332      error = resource_metadata_->AddEntry(new_entry, &local_id);
333      break;
334    }
335    default:
336      return error;
337  }
338  if (error != FILE_ERROR_OK)
339    return error;
340
341  UpdateChangedDirs(entry);
342  return FILE_ERROR_OK;
343}
344
345// static
346FileError ChangeListProcessor::RefreshDirectory(
347    ResourceMetadata* resource_metadata,
348    const DirectoryFetchInfo& directory_fetch_info,
349    ScopedVector<ChangeList> change_lists,
350    base::FilePath* out_file_path) {
351  DCHECK(!directory_fetch_info.empty());
352
353  std::string directory_local_id;
354  FileError error = resource_metadata->GetIdByResourceId(
355      directory_fetch_info.resource_id(), &directory_local_id);
356  if (error != FILE_ERROR_OK)
357    return error;
358
359  ResourceEntry directory;
360  error = resource_metadata->GetResourceEntryById(directory_local_id,
361                                                  &directory);
362  if (error != FILE_ERROR_OK)
363    return error;
364
365  if (!directory.file_info().is_directory())
366    return FILE_ERROR_NOT_A_DIRECTORY;
367
368  for (size_t i = 0; i < change_lists.size(); ++i) {
369    ChangeList* change_list = change_lists[i];
370    std::vector<ResourceEntry>* entries = change_list->mutable_entries();
371    for (size_t i = 0; i < entries->size(); ++i) {
372      ResourceEntry* entry = &(*entries)[i];
373      const std::string& parent_resource_id =
374          change_list->parent_resource_ids()[i];
375
376      // Skip if the parent resource ID does not match. This is needed to
377      // handle entries with multiple parents. For such entries, the first
378      // parent is picked and other parents are ignored, hence some entries may
379      // have a parent resource ID which does not match the target directory's.
380      if (parent_resource_id != directory_fetch_info.resource_id()) {
381        DVLOG(1) << "Wrong-parent entry rejected: " << entry->resource_id();
382        continue;
383      }
384
385      entry->set_parent_local_id(directory_local_id);
386
387      std::string local_id;
388      error = resource_metadata->GetIdByResourceId(entry->resource_id(),
389                                                   &local_id);
390      if (error == FILE_ERROR_OK) {
391        entry->set_local_id(local_id);
392        error = resource_metadata->RefreshEntry(*entry);
393      }
394
395      if (error == FILE_ERROR_NOT_FOUND) {  // If refreshing fails, try adding.
396        std::string local_id;
397        entry->clear_local_id();
398        error = resource_metadata->AddEntry(*entry, &local_id);
399      }
400
401      if (error != FILE_ERROR_OK)
402        return error;
403    }
404  }
405
406  directory.mutable_directory_specific_info()->set_changestamp(
407      directory_fetch_info.changestamp());
408  error = resource_metadata->RefreshEntry(directory);
409  if (error != FILE_ERROR_OK)
410    return error;
411
412  *out_file_path = resource_metadata->GetFilePath(directory_local_id);
413  return FILE_ERROR_OK;
414}
415
416void ChangeListProcessor::UpdateChangedDirs(const ResourceEntry& entry) {
417  DCHECK(!entry.resource_id().empty());
418
419  std::string local_id;
420  base::FilePath file_path;
421  if (resource_metadata_->GetIdByResourceId(
422          entry.resource_id(), &local_id) == FILE_ERROR_OK)
423    file_path = resource_metadata_->GetFilePath(local_id);
424
425  if (!file_path.empty()) {
426    // Notify parent.
427    changed_dirs_.insert(file_path.DirName());
428
429    if (entry.file_info().is_directory()) {
430      // Notify self if entry is a directory.
431      changed_dirs_.insert(file_path);
432
433      // Notify all descendants if it is a directory deletion.
434      if (entry.deleted()) {
435        std::set<base::FilePath> sub_directories;
436        resource_metadata_->GetSubDirectoriesRecursively(local_id,
437                                                         &sub_directories);
438        changed_dirs_.insert(sub_directories.begin(), sub_directories.end());
439      }
440    }
441  }
442}
443
444}  // namespace internal
445}  // namespace drive
446