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/resource_metadata.h" | 5 #include "chrome/browser/chromeos/drive/resource_metadata.h" |
6 | 6 |
7 #include "base/strings/string_number_conversions.h" | 7 #include "base/strings/string_number_conversions.h" |
8 #include "base/strings/stringprintf.h" | 8 #include "base/strings/stringprintf.h" |
9 #include "base/sys_info.h" | 9 #include "base/sys_info.h" |
10 #include "chrome/browser/chromeos/drive/drive.pb.h" | 10 #include "chrome/browser/chromeos/drive/drive.pb.h" |
(...skipping 107 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
118 | 118 |
119 EntryInfoPairResult::EntryInfoPairResult() { | 119 EntryInfoPairResult::EntryInfoPairResult() { |
120 } | 120 } |
121 | 121 |
122 EntryInfoPairResult::~EntryInfoPairResult() { | 122 EntryInfoPairResult::~EntryInfoPairResult() { |
123 } | 123 } |
124 | 124 |
125 namespace internal { | 125 namespace internal { |
126 | 126 |
127 ResourceMetadata::ResourceMetadata( | 127 ResourceMetadata::ResourceMetadata( |
128 const base::FilePath& data_directory_path, | 128 ResourceMetadataStorage* storage, |
129 scoped_refptr<base::SequencedTaskRunner> blocking_task_runner) | 129 scoped_refptr<base::SequencedTaskRunner> blocking_task_runner) |
130 : data_directory_path_(data_directory_path), | 130 : blocking_task_runner_(blocking_task_runner), |
131 blocking_task_runner_(blocking_task_runner), | 131 storage_(storage), |
132 storage_(new ResourceMetadataStorage(data_directory_path)), | |
133 weak_ptr_factory_(this) { | 132 weak_ptr_factory_(this) { |
134 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 133 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
135 } | 134 } |
136 | 135 |
137 FileError ResourceMetadata::Initialize() { | 136 FileError ResourceMetadata::Initialize() { |
138 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); | 137 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); |
139 | 138 |
140 if (!EnoughDiskSpaceIsAvailableForDBOperation(data_directory_path_)) | 139 if (!EnoughDiskSpaceIsAvailableForDBOperation(storage_->directory_path())) |
141 return FILE_ERROR_NO_SPACE; | 140 return FILE_ERROR_NO_SPACE; |
142 | 141 |
143 // Initialize the storage. | |
144 if (!storage_->Initialize()) | |
145 return FILE_ERROR_FAILED; | |
146 | |
147 if (!SetUpDefaultEntries()) | 142 if (!SetUpDefaultEntries()) |
148 return FILE_ERROR_FAILED; | 143 return FILE_ERROR_FAILED; |
149 | 144 |
150 return FILE_ERROR_OK; | 145 return FILE_ERROR_OK; |
151 } | 146 } |
152 | 147 |
153 void ResourceMetadata::Destroy() { | 148 void ResourceMetadata::Destroy() { |
154 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 149 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
155 | 150 |
156 weak_ptr_factory_.InvalidateWeakPtrs(); | 151 weak_ptr_factory_.InvalidateWeakPtrs(); |
(...skipping 10 matching lines...) Expand all Loading... |
167 base::PostTaskAndReplyWithResult( | 162 base::PostTaskAndReplyWithResult( |
168 blocking_task_runner_.get(), | 163 blocking_task_runner_.get(), |
169 FROM_HERE, | 164 FROM_HERE, |
170 base::Bind(&ResourceMetadata::Reset, base::Unretained(this)), | 165 base::Bind(&ResourceMetadata::Reset, base::Unretained(this)), |
171 callback); | 166 callback); |
172 } | 167 } |
173 | 168 |
174 FileError ResourceMetadata::Reset() { | 169 FileError ResourceMetadata::Reset() { |
175 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); | 170 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); |
176 | 171 |
177 if (!EnoughDiskSpaceIsAvailableForDBOperation(data_directory_path_)) | 172 if (!EnoughDiskSpaceIsAvailableForDBOperation(storage_->directory_path())) |
178 return FILE_ERROR_NO_SPACE; | 173 return FILE_ERROR_NO_SPACE; |
179 | 174 |
180 if (!storage_->SetLargestChangestamp(0) || | 175 if (!storage_->SetLargestChangestamp(0) || |
181 !RemoveEntryRecursively(util::kDriveGrandRootSpecialResourceId) || | 176 !RemoveEntryRecursively(util::kDriveGrandRootSpecialResourceId) || |
182 !SetUpDefaultEntries()) | 177 !SetUpDefaultEntries()) |
183 return FILE_ERROR_FAILED; | 178 return FILE_ERROR_FAILED; |
184 | 179 |
185 return FILE_ERROR_OK; | 180 return FILE_ERROR_OK; |
186 } | 181 } |
187 | 182 |
(...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
242 } | 237 } |
243 | 238 |
244 int64 ResourceMetadata::GetLargestChangestamp() { | 239 int64 ResourceMetadata::GetLargestChangestamp() { |
245 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); | 240 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); |
246 return storage_->GetLargestChangestamp(); | 241 return storage_->GetLargestChangestamp(); |
247 } | 242 } |
248 | 243 |
249 FileError ResourceMetadata::SetLargestChangestamp(int64 value) { | 244 FileError ResourceMetadata::SetLargestChangestamp(int64 value) { |
250 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); | 245 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); |
251 | 246 |
252 if (!EnoughDiskSpaceIsAvailableForDBOperation(data_directory_path_)) | 247 if (!EnoughDiskSpaceIsAvailableForDBOperation(storage_->directory_path())) |
253 return FILE_ERROR_NO_SPACE; | 248 return FILE_ERROR_NO_SPACE; |
254 | 249 |
255 storage_->SetLargestChangestamp(value); | 250 storage_->SetLargestChangestamp(value); |
256 return FILE_ERROR_OK; | 251 return FILE_ERROR_OK; |
257 } | 252 } |
258 | 253 |
259 void ResourceMetadata::AddEntryOnUIThread(const ResourceEntry& entry, | 254 void ResourceMetadata::AddEntryOnUIThread(const ResourceEntry& entry, |
260 const FileMoveCallback& callback) { | 255 const FileMoveCallback& callback) { |
261 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 256 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
262 DCHECK(!callback.is_null()); | 257 DCHECK(!callback.is_null()); |
263 | 258 |
264 PostFileMoveTask( | 259 PostFileMoveTask( |
265 blocking_task_runner_.get(), | 260 blocking_task_runner_.get(), |
266 base::Bind(&AddEntryWithFilePath, base::Unretained(this), entry), | 261 base::Bind(&AddEntryWithFilePath, base::Unretained(this), entry), |
267 callback); | 262 callback); |
268 } | 263 } |
269 | 264 |
270 FileError ResourceMetadata::AddEntry(const ResourceEntry& entry) { | 265 FileError ResourceMetadata::AddEntry(const ResourceEntry& entry) { |
271 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); | 266 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); |
272 | 267 |
273 if (!EnoughDiskSpaceIsAvailableForDBOperation(data_directory_path_)) | 268 if (!EnoughDiskSpaceIsAvailableForDBOperation(storage_->directory_path())) |
274 return FILE_ERROR_NO_SPACE; | 269 return FILE_ERROR_NO_SPACE; |
275 | 270 |
276 ResourceEntry existing_entry; | 271 ResourceEntry existing_entry; |
277 if (storage_->GetEntry(entry.resource_id(), &existing_entry)) | 272 if (storage_->GetEntry(entry.resource_id(), &existing_entry)) |
278 return FILE_ERROR_EXISTS; | 273 return FILE_ERROR_EXISTS; |
279 | 274 |
280 ResourceEntry parent; | 275 ResourceEntry parent; |
281 if (!storage_->GetEntry(entry.parent_resource_id(), &parent) || | 276 if (!storage_->GetEntry(entry.parent_resource_id(), &parent) || |
282 !parent.file_info().is_directory()) | 277 !parent.file_info().is_directory()) |
283 return FILE_ERROR_NOT_FOUND; | 278 return FILE_ERROR_NOT_FOUND; |
(...skipping 29 matching lines...) Expand all Loading... |
313 base::Bind(&ResourceMetadata::RenameEntry, | 308 base::Bind(&ResourceMetadata::RenameEntry, |
314 base::Unretained(this), | 309 base::Unretained(this), |
315 file_path, | 310 file_path, |
316 new_name), | 311 new_name), |
317 callback); | 312 callback); |
318 } | 313 } |
319 | 314 |
320 FileError ResourceMetadata::RemoveEntry(const std::string& resource_id) { | 315 FileError ResourceMetadata::RemoveEntry(const std::string& resource_id) { |
321 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); | 316 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); |
322 | 317 |
323 if (!EnoughDiskSpaceIsAvailableForDBOperation(data_directory_path_)) | 318 if (!EnoughDiskSpaceIsAvailableForDBOperation(storage_->directory_path())) |
324 return FILE_ERROR_NO_SPACE; | 319 return FILE_ERROR_NO_SPACE; |
325 | 320 |
326 // Disallow deletion of special entries "/drive" and "/drive/other". | 321 // Disallow deletion of special entries "/drive" and "/drive/other". |
327 if (util::IsSpecialResourceId(resource_id)) | 322 if (util::IsSpecialResourceId(resource_id)) |
328 return FILE_ERROR_ACCESS_DENIED; | 323 return FILE_ERROR_ACCESS_DENIED; |
329 | 324 |
330 ResourceEntry entry; | 325 ResourceEntry entry; |
331 if (!storage_->GetEntry(resource_id, &entry)) | 326 if (!storage_->GetEntry(resource_id, &entry)) |
332 return FILE_ERROR_NOT_FOUND; | 327 return FILE_ERROR_NOT_FOUND; |
333 | 328 |
(...skipping 97 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
431 if (!storage_->GetEntry(children[i], &entries[i])) | 426 if (!storage_->GetEntry(children[i], &entries[i])) |
432 return FILE_ERROR_FAILED; | 427 return FILE_ERROR_FAILED; |
433 } | 428 } |
434 out_entries->swap(entries); | 429 out_entries->swap(entries); |
435 return FILE_ERROR_OK; | 430 return FILE_ERROR_OK; |
436 } | 431 } |
437 | 432 |
438 FileError ResourceMetadata::RefreshEntry(const ResourceEntry& entry) { | 433 FileError ResourceMetadata::RefreshEntry(const ResourceEntry& entry) { |
439 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); | 434 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); |
440 | 435 |
441 if (!EnoughDiskSpaceIsAvailableForDBOperation(data_directory_path_)) | 436 if (!EnoughDiskSpaceIsAvailableForDBOperation(storage_->directory_path())) |
442 return FILE_ERROR_NO_SPACE; | 437 return FILE_ERROR_NO_SPACE; |
443 | 438 |
444 ResourceEntry old_entry; | 439 ResourceEntry old_entry; |
445 if (!storage_->GetEntry(entry.resource_id(), &old_entry)) | 440 if (!storage_->GetEntry(entry.resource_id(), &old_entry)) |
446 return FILE_ERROR_NOT_FOUND; | 441 return FILE_ERROR_NOT_FOUND; |
447 | 442 |
448 if (old_entry.parent_resource_id().empty() || // Reject root. | 443 if (old_entry.parent_resource_id().empty() || // Reject root. |
449 old_entry.file_info().is_directory() != // Reject incompatible input. | 444 old_entry.file_info().is_directory() != // Reject incompatible input. |
450 entry.file_info().is_directory()) | 445 entry.file_info().is_directory()) |
451 return FILE_ERROR_INVALID_OPERATION; | 446 return FILE_ERROR_INVALID_OPERATION; |
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
522 | 517 |
523 FileError ResourceMetadata::MoveEntryToDirectory( | 518 FileError ResourceMetadata::MoveEntryToDirectory( |
524 const base::FilePath& file_path, | 519 const base::FilePath& file_path, |
525 const base::FilePath& directory_path, | 520 const base::FilePath& directory_path, |
526 base::FilePath* out_file_path) { | 521 base::FilePath* out_file_path) { |
527 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); | 522 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); |
528 DCHECK(!directory_path.empty()); | 523 DCHECK(!directory_path.empty()); |
529 DCHECK(!file_path.empty()); | 524 DCHECK(!file_path.empty()); |
530 DCHECK(out_file_path); | 525 DCHECK(out_file_path); |
531 | 526 |
532 if (!EnoughDiskSpaceIsAvailableForDBOperation(data_directory_path_)) | 527 if (!EnoughDiskSpaceIsAvailableForDBOperation(storage_->directory_path())) |
533 return FILE_ERROR_NO_SPACE; | 528 return FILE_ERROR_NO_SPACE; |
534 | 529 |
535 ResourceEntry entry, destination; | 530 ResourceEntry entry, destination; |
536 if (!FindEntryByPathSync(file_path, &entry) || | 531 if (!FindEntryByPathSync(file_path, &entry) || |
537 !FindEntryByPathSync(directory_path, &destination)) | 532 !FindEntryByPathSync(directory_path, &destination)) |
538 return FILE_ERROR_NOT_FOUND; | 533 return FILE_ERROR_NOT_FOUND; |
539 if (!destination.file_info().is_directory()) | 534 if (!destination.file_info().is_directory()) |
540 return FILE_ERROR_NOT_A_DIRECTORY; | 535 return FILE_ERROR_NOT_A_DIRECTORY; |
541 | 536 |
542 entry.set_parent_resource_id(destination.resource_id()); | 537 entry.set_parent_resource_id(destination.resource_id()); |
543 | 538 |
544 FileError error = RefreshEntry(entry); | 539 FileError error = RefreshEntry(entry); |
545 if (error == FILE_ERROR_OK) | 540 if (error == FILE_ERROR_OK) |
546 *out_file_path = GetFilePath(entry.resource_id()); | 541 *out_file_path = GetFilePath(entry.resource_id()); |
547 return error; | 542 return error; |
548 } | 543 } |
549 | 544 |
550 FileError ResourceMetadata::RenameEntry( | 545 FileError ResourceMetadata::RenameEntry( |
551 const base::FilePath& file_path, | 546 const base::FilePath& file_path, |
552 const std::string& new_name, | 547 const std::string& new_name, |
553 base::FilePath* out_file_path) { | 548 base::FilePath* out_file_path) { |
554 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); | 549 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); |
555 DCHECK(!file_path.empty()); | 550 DCHECK(!file_path.empty()); |
556 DCHECK(!new_name.empty()); | 551 DCHECK(!new_name.empty()); |
557 DCHECK(out_file_path); | 552 DCHECK(out_file_path); |
558 | 553 |
559 DVLOG(1) << "RenameEntry " << file_path.value() << " to " << new_name; | 554 DVLOG(1) << "RenameEntry " << file_path.value() << " to " << new_name; |
560 | 555 |
561 if (!EnoughDiskSpaceIsAvailableForDBOperation(data_directory_path_)) | 556 if (!EnoughDiskSpaceIsAvailableForDBOperation(storage_->directory_path())) |
562 return FILE_ERROR_NO_SPACE; | 557 return FILE_ERROR_NO_SPACE; |
563 | 558 |
564 ResourceEntry entry; | 559 ResourceEntry entry; |
565 if (!FindEntryByPathSync(file_path, &entry)) | 560 if (!FindEntryByPathSync(file_path, &entry)) |
566 return FILE_ERROR_NOT_FOUND; | 561 return FILE_ERROR_NOT_FOUND; |
567 | 562 |
568 if (base::FilePath::FromUTF8Unsafe(new_name) == file_path.BaseName()) | 563 if (base::FilePath::FromUTF8Unsafe(new_name) == file_path.BaseName()) |
569 return FILE_ERROR_EXISTS; | 564 return FILE_ERROR_EXISTS; |
570 | 565 |
571 entry.set_title(new_name); | 566 entry.set_title(new_name); |
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
624 callback)); | 619 callback)); |
625 } | 620 } |
626 | 621 |
627 FileError ResourceMetadata::RefreshDirectory( | 622 FileError ResourceMetadata::RefreshDirectory( |
628 const DirectoryFetchInfo& directory_fetch_info, | 623 const DirectoryFetchInfo& directory_fetch_info, |
629 const ResourceEntryMap& entry_map, | 624 const ResourceEntryMap& entry_map, |
630 base::FilePath* out_file_path) { | 625 base::FilePath* out_file_path) { |
631 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); | 626 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); |
632 DCHECK(!directory_fetch_info.empty()); | 627 DCHECK(!directory_fetch_info.empty()); |
633 | 628 |
634 if (!EnoughDiskSpaceIsAvailableForDBOperation(data_directory_path_)) | 629 if (!EnoughDiskSpaceIsAvailableForDBOperation(storage_->directory_path())) |
635 return FILE_ERROR_NO_SPACE; | 630 return FILE_ERROR_NO_SPACE; |
636 | 631 |
637 ResourceEntry directory; | 632 ResourceEntry directory; |
638 if (!storage_->GetEntry(directory_fetch_info.resource_id(), &directory)) | 633 if (!storage_->GetEntry(directory_fetch_info.resource_id(), &directory)) |
639 return FILE_ERROR_NOT_FOUND; | 634 return FILE_ERROR_NOT_FOUND; |
640 | 635 |
641 if (!directory.file_info().is_directory()) | 636 if (!directory.file_info().is_directory()) |
642 return FILE_ERROR_NOT_A_DIRECTORY; | 637 return FILE_ERROR_NOT_A_DIRECTORY; |
643 | 638 |
644 directory.mutable_directory_specific_info()->set_changestamp( | 639 directory.mutable_directory_specific_info()->set_changestamp( |
645 directory_fetch_info.changestamp()); | 640 directory_fetch_info.changestamp()); |
646 storage_->PutEntry(directory); | 641 storage_->PutEntry(directory); |
647 | 642 |
648 // First, go through the entry map. We'll handle existing entries and new | 643 // First, go through the entry map. We'll handle existing entries and new |
649 // entries in the loop. We'll process deleted entries afterwards. | 644 // entries in the loop. We'll process deleted entries afterwards. |
650 for (ResourceEntryMap::const_iterator it = entry_map.begin(); | 645 for (ResourceEntryMap::const_iterator it = entry_map.begin(); |
651 it != entry_map.end(); ++it) { | 646 it != entry_map.end(); ++it) { |
652 if (!EnoughDiskSpaceIsAvailableForDBOperation(data_directory_path_)) | 647 if (!EnoughDiskSpaceIsAvailableForDBOperation(storage_->directory_path())) |
653 return FILE_ERROR_NO_SPACE; | 648 return FILE_ERROR_NO_SPACE; |
654 | 649 |
655 const ResourceEntry& entry = it->second; | 650 const ResourceEntry& entry = it->second; |
656 // Skip if the parent resource ID does not match. This is needed to | 651 // Skip if the parent resource ID does not match. This is needed to |
657 // handle entries with multiple parents. For such entries, the first | 652 // handle entries with multiple parents. For such entries, the first |
658 // parent is picked and other parents are ignored, hence some entries may | 653 // parent is picked and other parents are ignored, hence some entries may |
659 // have a parent resource ID which does not match the target directory's. | 654 // have a parent resource ID which does not match the target directory's. |
660 // | 655 // |
661 // TODO(satorux): Move the filtering logic to somewhere more appropriate. | 656 // TODO(satorux): Move the filtering logic to somewhere more appropriate. |
662 // crbug.com/193525. | 657 // crbug.com/193525. |
663 if (entry.parent_resource_id() != | 658 if (entry.parent_resource_id() != |
664 directory_fetch_info.resource_id()) { | 659 directory_fetch_info.resource_id()) { |
665 DVLOG(1) << "Wrong-parent entry rejected: " << entry.resource_id(); | 660 DVLOG(1) << "Wrong-parent entry rejected: " << entry.resource_id(); |
666 continue; | 661 continue; |
667 } | 662 } |
668 | 663 |
669 if (!PutEntryUnderDirectory(CreateEntryWithProperBaseName(entry))) | 664 if (!PutEntryUnderDirectory(CreateEntryWithProperBaseName(entry))) |
670 return FILE_ERROR_FAILED; | 665 return FILE_ERROR_FAILED; |
671 } | 666 } |
672 | 667 |
673 // Go through the existing entries and remove deleted entries. | 668 // Go through the existing entries and remove deleted entries. |
674 std::vector<std::string> children; | 669 std::vector<std::string> children; |
675 storage_->GetChildren(directory.resource_id(), &children); | 670 storage_->GetChildren(directory.resource_id(), &children); |
676 for (size_t i = 0; i < children.size(); ++i) { | 671 for (size_t i = 0; i < children.size(); ++i) { |
677 if (!EnoughDiskSpaceIsAvailableForDBOperation(data_directory_path_)) | 672 if (!EnoughDiskSpaceIsAvailableForDBOperation(storage_->directory_path())) |
678 return FILE_ERROR_NO_SPACE; | 673 return FILE_ERROR_NO_SPACE; |
679 | 674 |
680 if (entry_map.count(children[i]) == 0) { | 675 if (entry_map.count(children[i]) == 0) { |
681 if (!RemoveEntryRecursively(children[i])) | 676 if (!RemoveEntryRecursively(children[i])) |
682 return FILE_ERROR_FAILED; | 677 return FILE_ERROR_FAILED; |
683 } | 678 } |
684 } | 679 } |
685 | 680 |
686 if (out_file_path) | 681 if (out_file_path) |
687 *out_file_path = GetFilePath(directory.resource_id()); | 682 *out_file_path = GetFilePath(directory.resource_id()); |
(...skipping 99 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
787 for (size_t i = 0; i < children.size(); ++i) { | 782 for (size_t i = 0; i < children.size(); ++i) { |
788 if (!RemoveEntryRecursively(children[i])) | 783 if (!RemoveEntryRecursively(children[i])) |
789 return false; | 784 return false; |
790 } | 785 } |
791 } | 786 } |
792 return storage_->RemoveEntry(resource_id); | 787 return storage_->RemoveEntry(resource_id); |
793 } | 788 } |
794 | 789 |
795 } // namespace internal | 790 } // namespace internal |
796 } // namespace drive | 791 } // namespace drive |
OLD | NEW |