| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/chromeos/drive/change_list_processor.h" |
| |
| #include "base/metrics/histogram.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "chrome/browser/chromeos/drive/drive.pb.h" |
| #include "chrome/browser/chromeos/drive/file_system_util.h" |
| #include "chrome/browser/chromeos/drive/resource_entry_conversion.h" |
| #include "chrome/browser/chromeos/drive/resource_metadata.h" |
| #include "chrome/browser/google_apis/drive_api_parser.h" |
| #include "chrome/browser/google_apis/gdata_wapi_parser.h" |
| |
| namespace drive { |
| namespace internal { |
| |
| std::string DirectoryFetchInfo::ToString() const { |
| return ("resource_id: " + resource_id_ + |
| ", changestamp: " + base::Int64ToString(changestamp_)); |
| } |
| |
| ChangeList::ChangeList(const google_apis::ResourceList& resource_list) |
| : largest_changestamp_(resource_list.largest_changestamp()) { |
| resource_list.GetNextFeedURL(&next_url_); |
| |
| entries_.resize(resource_list.entries().size()); |
| size_t entries_index = 0; |
| for (size_t i = 0; i < resource_list.entries().size(); ++i) { |
| if (ConvertToResourceEntry(*resource_list.entries()[i], |
| &entries_[entries_index])) |
| ++entries_index; |
| } |
| entries_.resize(entries_index); |
| } |
| |
| ChangeList::~ChangeList() {} |
| |
| class ChangeListProcessor::ChangeListToEntryMapUMAStats { |
| public: |
| ChangeListToEntryMapUMAStats() |
| : num_regular_files_(0), |
| num_hosted_documents_(0), |
| num_shared_with_me_entries_(0) { |
| } |
| |
| // Increments number of files. |
| void IncrementNumFiles(bool is_hosted_document) { |
| is_hosted_document ? num_hosted_documents_++ : num_regular_files_++; |
| } |
| |
| // Increments number of shared-with-me entries. |
| void IncrementNumSharedWithMeEntries() { |
| num_shared_with_me_entries_++; |
| } |
| |
| // Updates UMA histograms with file counts. |
| void UpdateFileCountUmaHistograms() { |
| const int num_total_files = num_hosted_documents_ + num_regular_files_; |
| UMA_HISTOGRAM_COUNTS("Drive.NumberOfRegularFiles", num_regular_files_); |
| UMA_HISTOGRAM_COUNTS("Drive.NumberOfHostedDocuments", |
| num_hosted_documents_); |
| UMA_HISTOGRAM_COUNTS("Drive.NumberOfTotalFiles", num_total_files); |
| UMA_HISTOGRAM_COUNTS("Drive.NumberOfSharedWithMeEntries", |
| num_shared_with_me_entries_); |
| } |
| |
| private: |
| int num_regular_files_; |
| int num_hosted_documents_; |
| int num_shared_with_me_entries_; |
| }; |
| |
| ChangeListProcessor::ChangeListProcessor(ResourceMetadata* resource_metadata) |
| : resource_metadata_(resource_metadata) { |
| } |
| |
| ChangeListProcessor::~ChangeListProcessor() { |
| } |
| |
| void ChangeListProcessor::Apply( |
| scoped_ptr<google_apis::AboutResource> about_resource, |
| ScopedVector<ChangeList> change_lists, |
| bool is_delta_update) { |
| DCHECK(is_delta_update || about_resource.get()); |
| |
| int64 largest_changestamp = 0; |
| if (is_delta_update) { |
| if (!change_lists.empty()) { |
| // The changestamp appears in the first page of the change list. |
| // The changestamp does not appear in the full resource list. |
| largest_changestamp = change_lists[0]->largest_changestamp(); |
| DCHECK_GE(change_lists[0]->largest_changestamp(), 0); |
| } |
| } else if (about_resource.get()) { |
| largest_changestamp = about_resource->largest_change_id(); |
| |
| DVLOG(1) << "Root folder ID is " << about_resource->root_folder_id(); |
| DCHECK(!about_resource->root_folder_id().empty()); |
| } else { |
| // A full update without AboutResouce will have no effective changestamp. |
| NOTREACHED(); |
| } |
| |
| ChangeListToEntryMapUMAStats uma_stats; |
| ConvertToMap(change_lists.Pass(), &entry_map_, &uma_stats); |
| |
| // Add the largest changestamp for directories. |
| for (ResourceEntryMap::iterator it = entry_map_.begin(); |
| it != entry_map_.end(); ++it) { |
| if (it->second.file_info().is_directory()) { |
| it->second.mutable_directory_specific_info()->set_changestamp( |
| largest_changestamp); |
| } |
| } |
| |
| ApplyEntryMap(is_delta_update, about_resource.Pass()); |
| |
| // Update the root entry and finish. |
| UpdateRootEntry(largest_changestamp); |
| |
| // Update changestamp. |
| FileError error = resource_metadata_->SetLargestChangestamp( |
| largest_changestamp); |
| DLOG_IF(ERROR, error != FILE_ERROR_OK) << "SetLargestChangeStamp failed: " |
| << FileErrorToString(error); |
| |
| // Shouldn't record histograms when processing delta update. |
| if (!is_delta_update) |
| uma_stats.UpdateFileCountUmaHistograms(); |
| } |
| |
| void ChangeListProcessor::ApplyEntryMap( |
| bool is_delta_update, |
| scoped_ptr<google_apis::AboutResource> about_resource) { |
| if (!is_delta_update) { // Full update. |
| DCHECK(about_resource); |
| |
| FileError error = resource_metadata_->Reset(); |
| |
| LOG_IF(ERROR, error != FILE_ERROR_OK) << "Failed to reset: " |
| << FileErrorToString(error); |
| |
| changed_dirs_.insert(util::GetDriveGrandRootPath()); |
| changed_dirs_.insert(util::GetDriveMyDriveRootPath()); |
| |
| // Create the MyDrive root directory. |
| ApplyEntry(util::CreateMyDriveRootEntry(about_resource->root_folder_id())); |
| } |
| |
| // Gather the set of changes in the old path. |
| // Note that we want to notify the change in both old and new paths (suppose |
| // /a/b/c is moved to /x/y/c. We want to notify both "/a/b" and "/x/y".) |
| // The old paths must be calculated before we apply any actual changes. |
| // The new paths are calculated after each change is applied. It correctly |
| // sets the new path because we apply changes in such an order (see below). |
| for (ResourceEntryMap::iterator it = entry_map_.begin(); |
| it != entry_map_.end(); ++it) { |
| UpdateChangedDirs(it->second); |
| } |
| |
| // Apply all entries except deleted ones to the metadata. |
| std::vector<ResourceEntry> deleted_entries; |
| deleted_entries.reserve(entry_map_.size()); |
| while (!entry_map_.empty()) { |
| ResourceEntryMap::iterator it = entry_map_.begin(); |
| |
| // Process deleted entries later to avoid deleting moved entries under it. |
| if (it->second.deleted()) { |
| deleted_entries.push_back(ResourceEntry()); |
| deleted_entries.back().Swap(&it->second); |
| entry_map_.erase(it); |
| continue; |
| } |
| |
| // Start from entry_map_.begin() and traverse ancestors using the |
| // parent-child relashonships in the result (after this apply) tree. |
| // Then apply the topmost change first. |
| // |
| // By doing this, assuming the result tree does not contain any cycles, we |
| // can guarantee that no cycle is made during this apply (i.e. no entry gets |
| // moved under any of its descendants) because the following conditions are |
| // always satisfied in any move: |
| // - The new parent entry is not a descendant of the moved entry. |
| // - The new parent and its ancestors will no longer move during this apply. |
| std::vector<ResourceEntryMap::iterator> entries; |
| entries.push_back(entry_map_.begin()); |
| for (std::string parent_id = entries.back()->second.parent_local_id(); |
| !parent_id.empty();) { |
| ResourceEntryMap::iterator it_parent = entry_map_.find(parent_id); |
| if (it_parent != entry_map_.end()) { |
| // This entry is going to be updated, get the parent from the new data. |
| entries.push_back(it_parent); |
| parent_id = it_parent->second.parent_local_id(); |
| } else { |
| // This entry is already updated or not going to be updated, get the |
| // parent from the current tree. |
| ResourceEntry parent_entry; |
| FileError error = |
| resource_metadata_->GetResourceEntryById(parent_id, &parent_entry); |
| if (error != FILE_ERROR_OK) { |
| LOG(ERROR) << "Cannot get the parent: " << FileErrorToString(error); |
| break; |
| } |
| parent_id = parent_entry.parent_local_id(); |
| } |
| } |
| |
| // Apply the parent first. |
| std::reverse(entries.begin(), entries.end()); |
| for (size_t i = 0; i < entries.size(); ++i) { |
| ResourceEntryMap::iterator it = entries[i]; |
| ApplyEntry(it->second); |
| entry_map_.erase(it); |
| } |
| } |
| |
| // Apply deleted entries. |
| for (size_t i = 0; i < deleted_entries.size(); ++i) |
| ApplyEntry(deleted_entries[i]); |
| } |
| |
| void ChangeListProcessor::ApplyEntry(const ResourceEntry& entry) { |
| // Lookup the entry. |
| ResourceEntry existing_entry; |
| FileError error = resource_metadata_->GetResourceEntryById( |
| entry.resource_id(), &existing_entry); |
| |
| if (error == FILE_ERROR_OK) { |
| if (entry.deleted()) { |
| // Deleted file/directory. |
| RemoveEntry(entry); |
| } else { |
| // Entry exists and needs to be refreshed. |
| RefreshEntry(entry); |
| } |
| } else if (error == FILE_ERROR_NOT_FOUND && !entry.deleted()) { |
| // Adding a new entry. |
| AddEntry(entry); |
| } |
| } |
| |
| void ChangeListProcessor::AddEntry(const ResourceEntry& entry) { |
| FileError error = resource_metadata_->AddEntry(entry); |
| |
| if (error == FILE_ERROR_OK) |
| UpdateChangedDirs(entry); |
| } |
| |
| void ChangeListProcessor::RemoveEntry(const ResourceEntry& entry) { |
| resource_metadata_->RemoveEntry(entry.resource_id()); |
| } |
| |
| void ChangeListProcessor::RefreshEntry(const ResourceEntry& entry) { |
| FileError error = |
| resource_metadata_->RefreshEntry(entry.resource_id(), entry); |
| |
| if (error == FILE_ERROR_OK) |
| UpdateChangedDirs(entry); |
| } |
| |
| // static |
| void ChangeListProcessor::ConvertToMap( |
| ScopedVector<ChangeList> change_lists, |
| ResourceEntryMap* entry_map, |
| ChangeListToEntryMapUMAStats* uma_stats) { |
| for (size_t i = 0; i < change_lists.size(); ++i) { |
| ChangeList* change_list = change_lists[i]; |
| |
| std::vector<ResourceEntry>* entries = change_list->mutable_entries(); |
| for (size_t i = 0; i < entries->size(); ++i) { |
| ResourceEntry* entry = &(*entries)[i]; |
| // Some document entries don't map into files (i.e. sites). |
| if (entry->resource_id().empty()) |
| continue; |
| |
| // Count the number of files. |
| if (uma_stats) { |
| if (!entry->file_info().is_directory()) { |
| uma_stats->IncrementNumFiles( |
| entry->file_specific_info().is_hosted_document()); |
| } |
| if (entry->shared_with_me()) |
| uma_stats->IncrementNumSharedWithMeEntries(); |
| } |
| |
| (*entry_map)[entry->resource_id()].Swap(entry); |
| LOG_IF(WARNING, !entry->resource_id().empty()) |
| << "Found duplicated file: " << entry->base_name(); |
| } |
| } |
| } |
| |
| // static |
| FileError ChangeListProcessor::RefreshDirectory( |
| ResourceMetadata* resource_metadata, |
| const DirectoryFetchInfo& directory_fetch_info, |
| const ResourceEntryMap& entry_map, |
| base::FilePath* out_file_path) { |
| DCHECK(!directory_fetch_info.empty()); |
| |
| ResourceEntry directory; |
| FileError error = resource_metadata->GetResourceEntryById( |
| directory_fetch_info.resource_id(), &directory); |
| if (error != FILE_ERROR_OK) |
| return error; |
| |
| if (!directory.file_info().is_directory()) |
| return FILE_ERROR_NOT_A_DIRECTORY; |
| |
| // Go through the entry map. Handle existing entries and new entries. |
| for (ResourceEntryMap::const_iterator it = entry_map.begin(); |
| it != entry_map.end(); ++it) { |
| const ResourceEntry& entry = it->second; |
| // Skip if the parent resource ID does not match. This is needed to |
| // handle entries with multiple parents. For such entries, the first |
| // parent is picked and other parents are ignored, hence some entries may |
| // have a parent resource ID which does not match the target directory's. |
| if (entry.parent_local_id() != directory_fetch_info.resource_id()) { |
| DVLOG(1) << "Wrong-parent entry rejected: " << entry.resource_id(); |
| continue; |
| } |
| |
| error = resource_metadata->RefreshEntry(it->first, entry); |
| if (error == FILE_ERROR_NOT_FOUND) // If refreshing fails, try adding. |
| error = resource_metadata->AddEntry(entry); |
| |
| if (error != FILE_ERROR_OK) |
| return error; |
| } |
| |
| directory.mutable_directory_specific_info()->set_changestamp( |
| directory_fetch_info.changestamp()); |
| error = resource_metadata->RefreshEntry(directory_fetch_info.resource_id(), |
| directory); |
| if (error != FILE_ERROR_OK) |
| return error; |
| |
| *out_file_path = resource_metadata->GetFilePath(directory.resource_id()); |
| return FILE_ERROR_OK; |
| } |
| |
| void ChangeListProcessor::UpdateRootEntry(int64 largest_changestamp) { |
| std::string root_local_id; |
| FileError error = resource_metadata_->GetIdByPath( |
| util::GetDriveMyDriveRootPath(), &root_local_id); |
| |
| ResourceEntry root; |
| if (error == FILE_ERROR_OK) |
| error = resource_metadata_->GetResourceEntryById(root_local_id, &root); |
| |
| if (error != FILE_ERROR_OK) { |
| // TODO(satorux): Need to trigger recovery if root is corrupt. |
| LOG(WARNING) << "Failed to get the entry for root directory"; |
| return; |
| } |
| |
| // The changestamp should always be updated. |
| root.mutable_directory_specific_info()->set_changestamp(largest_changestamp); |
| |
| error = resource_metadata_->RefreshEntry(root_local_id, root); |
| |
| LOG_IF(WARNING, error != FILE_ERROR_OK) << "Failed to refresh root directory"; |
| } |
| |
| void ChangeListProcessor::UpdateChangedDirs(const ResourceEntry& entry) { |
| base::FilePath file_path = |
| resource_metadata_->GetFilePath(entry.resource_id()); |
| |
| if (!file_path.empty()) { |
| // Notify parent. |
| changed_dirs_.insert(file_path.DirName()); |
| |
| if (entry.file_info().is_directory()) { |
| // Notify self if entry is a directory. |
| changed_dirs_.insert(file_path); |
| |
| // Notify all descendants if it is a directory deletion. |
| if (entry.deleted()) { |
| std::set<base::FilePath> sub_directories; |
| resource_metadata_->GetSubDirectoriesRecursively(entry.resource_id(), |
| &sub_directories); |
| changed_dirs_.insert(sub_directories.begin(), sub_directories.end()); |
| } |
| } |
| } |
| } |
| |
| } // namespace internal |
| } // namespace drive |