| OLD | NEW |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "chrome/browser/chromeos/drive/file_system/move_operation.h" | 5 #include "chrome/browser/chromeos/drive/file_system/move_operation.h" |
| 6 | 6 |
| 7 #include "chrome/browser/chromeos/drive/drive.pb.h" | 7 #include "chrome/browser/chromeos/drive/drive.pb.h" |
| 8 #include "chrome/browser/chromeos/drive/file_system/operation_observer.h" | 8 #include "chrome/browser/chromeos/drive/file_system/operation_observer.h" |
| 9 #include "chrome/browser/chromeos/drive/file_system_util.h" | 9 #include "chrome/browser/chromeos/drive/file_system_util.h" |
| 10 #include "chrome/browser/chromeos/drive/job_scheduler.h" | 10 #include "chrome/browser/chromeos/drive/job_scheduler.h" |
| 11 #include "content/public/browser/browser_thread.h" | 11 #include "content/public/browser/browser_thread.h" |
| 12 | 12 |
| 13 using content::BrowserThread; | 13 using content::BrowserThread; |
| 14 | 14 |
| 15 namespace drive { | 15 namespace drive { |
| 16 namespace file_system { | 16 namespace file_system { |
| 17 namespace { |
| 17 | 18 |
| 18 MoveOperation::MoveOperation(OperationObserver* observer, | 19 // Looks up ResourceEntry for source entry and the destination directory. |
| 20 FileError PrepareMove(internal::ResourceMetadata* metadata, |
| 21 const base::FilePath& src_path, |
| 22 const base::FilePath& dest_parent_path, |
| 23 ResourceEntry* src_entry, |
| 24 ResourceEntry* dest_parent_entry) { |
| 25 FileError error = metadata->GetResourceEntryByPath(src_path, src_entry); |
| 26 if (error != FILE_ERROR_OK) |
| 27 return error; |
| 28 |
| 29 return metadata->GetResourceEntryByPath(dest_parent_path, dest_parent_entry); |
| 30 } |
| 31 |
| 32 // Applies renaming to the local metadata. |
| 33 FileError RenameLocally(internal::ResourceMetadata* metadata, |
| 34 const std::string& resource_id, |
| 35 const std::string& new_title) { |
| 36 ResourceEntry entry; |
| 37 FileError error = metadata->GetResourceEntryById(resource_id, &entry); |
| 38 if (error != FILE_ERROR_OK) |
| 39 return error; |
| 40 |
| 41 entry.set_title(new_title); |
| 42 return metadata->RefreshEntry(entry); |
| 43 } |
| 44 |
| 45 // Applies directory-moving to the local metadata. |
| 46 FileError MoveDirectoryLocally(internal::ResourceMetadata* metadata, |
| 47 const std::string& resource_id, |
| 48 const std::string& parent_resource_id) { |
| 49 ResourceEntry entry; |
| 50 FileError error = metadata->GetResourceEntryById(resource_id, &entry); |
| 51 if (error != FILE_ERROR_OK) |
| 52 return error; |
| 53 |
| 54 // TODO(hidehiko,hashimoto): Set local id, instead of resource id. |
| 55 entry.set_parent_local_id(parent_resource_id); |
| 56 return metadata->RefreshEntry(entry); |
| 57 } |
| 58 |
| 59 } // namespace |
| 60 |
| 61 MoveOperation::MoveOperation(base::SequencedTaskRunner* blocking_task_runner, |
| 62 OperationObserver* observer, |
| 19 JobScheduler* scheduler, | 63 JobScheduler* scheduler, |
| 20 internal::ResourceMetadata* metadata) | 64 internal::ResourceMetadata* metadata) |
| 21 : observer_(observer), | 65 : blocking_task_runner_(blocking_task_runner), |
| 22 scheduler_(scheduler), | 66 observer_(observer), |
| 23 metadata_(metadata), | 67 scheduler_(scheduler), |
| 24 weak_ptr_factory_(this) { | 68 metadata_(metadata), |
| 69 weak_ptr_factory_(this) { |
| 25 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 70 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 26 } | 71 } |
| 27 | 72 |
| 28 MoveOperation::~MoveOperation() { | 73 MoveOperation::~MoveOperation() { |
| 29 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 74 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 30 } | 75 } |
| 31 | 76 |
| 32 void MoveOperation::Move(const base::FilePath& src_file_path, | 77 void MoveOperation::Move(const base::FilePath& src_file_path, |
| 33 const base::FilePath& dest_file_path, | 78 const base::FilePath& dest_file_path, |
| 34 const FileOperationCallback& callback) { | 79 const FileOperationCallback& callback) { |
| 35 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 80 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 36 DCHECK(!callback.is_null()); | 81 DCHECK(!callback.is_null()); |
| 37 | 82 |
| 38 metadata_->GetResourceEntryPairByPathsOnUIThread( | 83 scoped_ptr<ResourceEntry> src_entry(new ResourceEntry); |
| 39 src_file_path, | 84 scoped_ptr<ResourceEntry> dest_parent_entry(new ResourceEntry); |
| 40 dest_file_path.DirName(), | 85 ResourceEntry* src_entry_ptr = src_entry.get(); |
| 41 base::Bind(&MoveOperation::MoveAfterGetResourceEntryPair, | 86 ResourceEntry* dest_parent_entry_ptr = dest_parent_entry.get(); |
| 87 base::PostTaskAndReplyWithResult( |
| 88 blocking_task_runner_.get(), |
| 89 FROM_HERE, |
| 90 base::Bind(&PrepareMove, |
| 91 metadata_, src_file_path, dest_file_path.DirName(), |
| 92 src_entry_ptr, dest_parent_entry_ptr), |
| 93 base::Bind(&MoveOperation::MoveAfterPrepare, |
| 42 weak_ptr_factory_.GetWeakPtr(), | 94 weak_ptr_factory_.GetWeakPtr(), |
| 43 dest_file_path, | 95 src_file_path, dest_file_path, callback, |
| 44 callback)); | 96 base::Passed(&src_entry), |
| 45 } | 97 base::Passed(&dest_parent_entry))); |
| 46 | 98 } |
| 47 void MoveOperation::MoveAfterGetResourceEntryPair( | 99 |
| 100 void MoveOperation::MoveAfterPrepare( |
| 101 const base::FilePath& src_file_path, |
| 48 const base::FilePath& dest_file_path, | 102 const base::FilePath& dest_file_path, |
| 49 const FileOperationCallback& callback, | 103 const FileOperationCallback& callback, |
| 50 scoped_ptr<EntryInfoPairResult> src_dest_info) { | 104 scoped_ptr<ResourceEntry> src_entry, |
| 51 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 105 scoped_ptr<ResourceEntry> dest_parent_entry, |
| 52 DCHECK(!callback.is_null()); | 106 FileError error) { |
| 53 DCHECK(src_dest_info.get()); | 107 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 54 | 108 DCHECK(!callback.is_null()); |
| 55 if (src_dest_info->first.error != FILE_ERROR_OK) { | 109 |
| 56 callback.Run(src_dest_info->first.error); | 110 if (error != FILE_ERROR_OK) { |
| 57 return; | 111 callback.Run(error); |
| 58 } | 112 return; |
| 59 if (src_dest_info->second.error != FILE_ERROR_OK) { | 113 } |
| 60 callback.Run(src_dest_info->second.error); | 114 |
| 61 return; | 115 if (!dest_parent_entry->file_info().is_directory()) { |
| 62 } | 116 // The parent of the destination is not a directory. |
| 63 if (!src_dest_info->second.entry->file_info().is_directory()) { | |
| 64 callback.Run(FILE_ERROR_NOT_A_DIRECTORY); | 117 callback.Run(FILE_ERROR_NOT_A_DIRECTORY); |
| 65 return; | 118 return; |
| 66 } | 119 } |
| 67 | 120 |
| 68 const std::string& src_id = src_dest_info->first.entry->resource_id(); | 121 // Strip the extension for a hosted document if necessary. |
| 69 const base::FilePath& src_path = src_dest_info->first.path; | 122 const bool has_hosted_document_extension = |
| 70 const base::FilePath new_name = dest_file_path.BaseName(); | 123 src_entry->has_file_specific_info() && |
| 71 const bool new_name_has_hosted_extension = | 124 src_entry->file_specific_info().is_hosted_document() && |
| 72 src_dest_info->first.entry->has_file_specific_info() && | 125 dest_file_path.Extension() == |
| 73 src_dest_info->first.entry->file_specific_info().is_hosted_document() && | 126 src_entry->file_specific_info().document_extension(); |
| 74 new_name.Extension() == | 127 const std::string new_title = |
| 75 src_dest_info->first.entry->file_specific_info().document_extension(); | 128 has_hosted_document_extension ? |
| 76 | 129 dest_file_path.BaseName().RemoveExtension().AsUTF8Unsafe() : |
| 77 Rename(src_id, src_path, new_name, new_name_has_hosted_extension, | 130 dest_file_path.BaseName().AsUTF8Unsafe(); |
| 131 |
| 132 // TODO(hidehiko): On Drive API v2, we can move a resource by only one |
| 133 // server request. Implement here. crbug.com/241814 |
| 134 |
| 135 ResourceEntry* src_entry_ptr = src_entry.get(); |
| 136 Rename(*src_entry_ptr, new_title, |
| 78 base::Bind(&MoveOperation::MoveAfterRename, | 137 base::Bind(&MoveOperation::MoveAfterRename, |
| 79 weak_ptr_factory_.GetWeakPtr(), | 138 weak_ptr_factory_.GetWeakPtr(), |
| 80 callback, | 139 src_file_path, dest_file_path, callback, |
| 81 base::Passed(&src_dest_info))); | 140 base::Passed(&src_entry), |
| 141 base::Passed(&dest_parent_entry))); |
| 82 } | 142 } |
| 83 | 143 |
| 84 void MoveOperation::MoveAfterRename( | 144 void MoveOperation::MoveAfterRename( |
| 85 const FileOperationCallback& callback, | 145 const base::FilePath& src_file_path, |
| 86 scoped_ptr<EntryInfoPairResult> src_dest_info, | 146 const base::FilePath& dest_file_path, |
| 87 FileError error, | 147 const FileOperationCallback& callback, |
| 88 const base::FilePath& src_path) { | 148 scoped_ptr<ResourceEntry> src_entry, |
| 89 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 149 scoped_ptr<ResourceEntry> dest_parent_entry, |
| 90 | 150 FileError error) { |
| 91 if (error != FILE_ERROR_OK) { | 151 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 92 callback.Run(error); | 152 DCHECK(!callback.is_null()); |
| 93 return; | 153 |
| 94 } | 154 if (error != FILE_ERROR_OK) { |
| 95 | 155 callback.Run(error); |
| 96 const std::string& src_id = src_dest_info->first.entry->resource_id(); | 156 return; |
| 97 const std::string& dest_dir_id = src_dest_info->second.entry->resource_id(); | 157 } |
| 98 const base::FilePath& dest_dir_path = src_dest_info->second.path; | |
| 99 | 158 |
| 100 // The source and the destination directory are the same. Nothing more to do. | 159 // The source and the destination directory are the same. Nothing more to do. |
| 101 if (src_path.DirName() == dest_dir_path) { | 160 // TODO(hidehiko,hashimoto): Replace resource_id to local_id. |
| 102 observer_->OnDirectoryChangedByOperation(dest_dir_path); | 161 if (src_entry->parent_local_id() == dest_parent_entry->resource_id()) { |
| 162 observer_->OnDirectoryChangedByOperation(dest_file_path.DirName()); |
| 103 callback.Run(FILE_ERROR_OK); | 163 callback.Run(FILE_ERROR_OK); |
| 104 return; | 164 return; |
| 105 } | 165 } |
| 106 | 166 |
| 107 AddToDirectory(src_id, dest_dir_id, src_path, dest_dir_path, | 167 // TODO(hidehiko,hashimoto): For MoveAfterAddToDirectory, it will be |
| 168 // necessary to resolve local id to resource id. |
| 169 AddToDirectory(src_entry->resource_id(), |
| 170 dest_parent_entry->resource_id(), |
| 108 base::Bind(&MoveOperation::MoveAfterAddToDirectory, | 171 base::Bind(&MoveOperation::MoveAfterAddToDirectory, |
| 109 weak_ptr_factory_.GetWeakPtr(), | 172 weak_ptr_factory_.GetWeakPtr(), |
| 110 callback, | 173 src_file_path, dest_file_path, callback, |
| 111 base::Passed(&src_dest_info))); | 174 src_entry->resource_id(), |
| 175 src_entry->parent_local_id())); |
| 112 } | 176 } |
| 113 | 177 |
| 114 void MoveOperation::MoveAfterAddToDirectory( | 178 void MoveOperation::MoveAfterAddToDirectory( |
| 115 const FileOperationCallback& callback, | 179 const base::FilePath& src_file_path, |
| 116 scoped_ptr<EntryInfoPairResult> src_dest_info, | 180 const base::FilePath& dest_file_path, |
| 117 FileError error, | 181 const FileOperationCallback& callback, |
| 118 const base::FilePath& new_path) { | 182 const std::string& resource_id, |
| 119 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 183 const std::string& parent_resource_id, |
| 120 | 184 FileError error) { |
| 121 if (error != FILE_ERROR_OK) { | 185 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 122 callback.Run(error); | 186 DCHECK(!callback.is_null()); |
| 123 return; | 187 |
| 124 } | 188 if (error != FILE_ERROR_OK) { |
| 125 | 189 callback.Run(error); |
| 126 const base::FilePath& src_path = src_dest_info->first.path; | 190 return; |
| 127 observer_->OnDirectoryChangedByOperation(src_path.DirName()); | 191 } |
| 128 observer_->OnDirectoryChangedByOperation(new_path.DirName()); | 192 |
| 129 | 193 // Notify to the observers. |
| 130 RemoveFromDirectory(src_dest_info->first.entry->resource_id(), | 194 observer_->OnDirectoryChangedByOperation(src_file_path.DirName()); |
| 131 src_dest_info->first.entry->parent_local_id(), | 195 observer_->OnDirectoryChangedByOperation(dest_file_path.DirName()); |
| 132 callback); | 196 |
| 133 } | 197 RemoveFromDirectory(resource_id, parent_resource_id, callback); |
| 134 | 198 } |
| 135 void MoveOperation::Rename(const std::string& src_id, | 199 |
| 136 const base::FilePath& src_path, | 200 void MoveOperation::Rename(const ResourceEntry& entry, |
| 137 const base::FilePath& new_name, | 201 const std::string& new_title, |
| 138 bool new_name_has_hosted_extension, | 202 const FileOperationCallback& callback) { |
| 139 const FileMoveCallback& callback) { | 203 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 140 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 204 DCHECK(!callback.is_null()); |
| 141 | 205 |
| 142 // It is a no-op if the file is renamed to the same name. | 206 if (entry.title() == new_title) { |
| 143 if (src_path.BaseName() == new_name) { | 207 // We have nothing to do. |
| 144 callback.Run(FILE_ERROR_OK, src_path); | 208 callback.Run(FILE_ERROR_OK); |
| 145 return; | 209 return; |
| 146 } | 210 } |
| 147 | 211 |
| 148 // Drop the .g<something> extension from |new_name| if the file being | 212 // Send a rename request to the server. |
| 149 // renamed is a hosted document and |new_name| has the same .g<something> | 213 scheduler_->RenameResource( |
| 150 // extension as the file. | 214 entry.resource_id(), |
| 151 const std::string new_title = new_name_has_hosted_extension ? | 215 new_title, |
| 152 new_name.RemoveExtension().AsUTF8Unsafe() : | 216 base::Bind(&MoveOperation::RenameAfterRenameResource, |
| 153 new_name.AsUTF8Unsafe(); | 217 weak_ptr_factory_.GetWeakPtr(), |
| 154 | 218 entry.resource_id(), new_title, callback)); |
| 155 // Rename on the server. | 219 } |
| 156 scheduler_->RenameResource(src_id, | 220 |
| 157 new_title, | 221 void MoveOperation::RenameAfterRenameResource( |
| 158 base::Bind(&MoveOperation::RenameLocally, | 222 const std::string& resource_id, |
| 159 weak_ptr_factory_.GetWeakPtr(), | 223 const std::string& new_title, |
| 160 src_path, | 224 const FileOperationCallback& callback, |
| 161 new_title, | 225 google_apis::GDataErrorCode status) { |
| 162 callback)); | 226 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 163 } | 227 DCHECK(!callback.is_null()); |
| 164 | |
| 165 void MoveOperation::RenameLocally(const base::FilePath& src_path, | |
| 166 const std::string& new_title, | |
| 167 const FileMoveCallback& callback, | |
| 168 google_apis::GDataErrorCode status) { | |
| 169 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
| 170 | 228 |
| 171 const FileError error = GDataToFileError(status); | 229 const FileError error = GDataToFileError(status); |
| 172 if (error != FILE_ERROR_OK) { | 230 if (error != FILE_ERROR_OK) { |
| 173 callback.Run(error, base::FilePath()); | 231 callback.Run(error); |
| 174 return; | 232 return; |
| 175 } | 233 } |
| 176 metadata_->RenameEntryOnUIThread(src_path, new_title, callback); | 234 |
| 177 } | 235 // Server side renaming is done. Update the local metadata. |
| 178 | 236 base::PostTaskAndReplyWithResult( |
| 179 void MoveOperation::AddToDirectory(const std::string& src_id, | 237 blocking_task_runner_.get(), |
| 180 const std::string& dest_dir_id, | 238 FROM_HERE, |
| 181 const base::FilePath& src_path, | 239 base::Bind(&RenameLocally, metadata_, resource_id, new_title), |
| 182 const base::FilePath& dest_dir_path, | 240 callback); |
| 183 const FileMoveCallback& callback) { | 241 } |
| 184 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 242 |
| 243 void MoveOperation::AddToDirectory( |
| 244 const std::string& resource_id, |
| 245 const std::string& parent_resource_id, |
| 246 const FileOperationCallback& callback) { |
| 247 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 248 DCHECK(!callback.is_null()); |
| 185 | 249 |
| 186 scheduler_->AddResourceToDirectory( | 250 scheduler_->AddResourceToDirectory( |
| 187 dest_dir_id, src_id, | 251 parent_resource_id, resource_id, |
| 188 base::Bind(&MoveOperation::AddToDirectoryLocally, | 252 base::Bind(&MoveOperation::AddToDirectoryAfterAddResourceToDirectory, |
| 189 weak_ptr_factory_.GetWeakPtr(), | 253 weak_ptr_factory_.GetWeakPtr(), |
| 190 src_path, | 254 resource_id, parent_resource_id, callback)); |
| 191 dest_dir_path, | 255 } |
| 192 callback)); | 256 |
| 193 } | 257 void MoveOperation::AddToDirectoryAfterAddResourceToDirectory( |
| 194 | 258 const std::string& resource_id, |
| 195 void MoveOperation::AddToDirectoryLocally(const base::FilePath& src_path, | 259 const std::string& parent_resource_id, |
| 196 const base::FilePath& dest_dir_path, | 260 const FileOperationCallback& callback, |
| 197 const FileMoveCallback& callback, | 261 google_apis::GDataErrorCode status) { |
| 198 google_apis::GDataErrorCode status) { | 262 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 199 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 263 DCHECK(!callback.is_null()); |
| 200 | 264 |
| 201 const FileError error = GDataToFileError(status); | 265 const FileError error = GDataToFileError(status); |
| 202 if (error != FILE_ERROR_OK) { | 266 if (error != FILE_ERROR_OK) { |
| 203 callback.Run(error, base::FilePath()); | 267 callback.Run(error); |
| 204 return; | 268 return; |
| 205 } | 269 } |
| 206 metadata_->MoveEntryToDirectoryOnUIThread(src_path, dest_dir_path, callback); | 270 |
| 271 // Server side moving is done. Update the local metadata. |
| 272 base::PostTaskAndReplyWithResult( |
| 273 blocking_task_runner_.get(), |
| 274 FROM_HERE, |
| 275 base::Bind(&MoveDirectoryLocally, |
| 276 metadata_, resource_id, parent_resource_id), |
| 277 callback); |
| 207 } | 278 } |
| 208 | 279 |
| 209 void MoveOperation::RemoveFromDirectory( | 280 void MoveOperation::RemoveFromDirectory( |
| 210 const std::string& resource_id, | 281 const std::string& resource_id, |
| 211 const std::string& directory_resource_id, | 282 const std::string& directory_resource_id, |
| 212 const FileOperationCallback& callback) { | 283 const FileOperationCallback& callback) { |
| 213 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 284 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 285 DCHECK(!callback.is_null()); |
| 214 | 286 |
| 215 // Moving files out from "drive/other" special folder for storing orphan files | 287 // Moving files out from "drive/other" special folder for storing orphan |
| 216 // has no meaning in the server. Just skip the step. | 288 // files has no meaning in the server. Just skip the step. |
| 217 if (util::IsSpecialResourceId(directory_resource_id)) { | 289 if (util::IsSpecialResourceId(directory_resource_id)) { |
| 218 callback.Run(FILE_ERROR_OK); | 290 callback.Run(FILE_ERROR_OK); |
| 219 return; | 291 return; |
| 220 } | 292 } |
| 221 | 293 |
| 222 scheduler_->RemoveResourceFromDirectory( | 294 scheduler_->RemoveResourceFromDirectory( |
| 223 directory_resource_id, | 295 directory_resource_id, |
| 224 resource_id, | 296 resource_id, |
| 225 base::Bind(&MoveOperation::RemoveFromDirectoryCompleted, | 297 base::Bind( |
| 226 weak_ptr_factory_.GetWeakPtr(), | 298 &MoveOperation::RemoveFromDirectoryAfterRemoveResourceFromDirectory, |
| 227 callback)); | 299 weak_ptr_factory_.GetWeakPtr(), callback)); |
| 228 } | 300 } |
| 229 | 301 |
| 230 void MoveOperation::RemoveFromDirectoryCompleted( | 302 void MoveOperation::RemoveFromDirectoryAfterRemoveResourceFromDirectory( |
| 231 const FileOperationCallback& callback, | 303 const FileOperationCallback& callback, |
| 232 google_apis::GDataErrorCode status) { | 304 google_apis::GDataErrorCode status) { |
| 233 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 305 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 306 DCHECK(!callback.is_null()); |
| 234 callback.Run(GDataToFileError(status)); | 307 callback.Run(GDataToFileError(status)); |
| 235 } | 308 } |
| 236 | 309 |
| 237 } // namespace file_system | 310 } // namespace file_system |
| 238 } // namespace drive | 311 } // namespace drive |
| OLD | NEW |