1// Copyright 2014 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/media_galleries/media_folder_finder.h" 6 7#include <algorithm> 8#include <set> 9 10#include "base/files/file_enumerator.h" 11#include "base/files/file_util.h" 12#include "base/path_service.h" 13#include "base/sequence_checker.h" 14#include "base/stl_util.h" 15#include "base/strings/string_util.h" 16#include "base/task_runner_util.h" 17#include "base/threading/sequenced_worker_pool.h" 18#include "chrome/browser/extensions/api/file_system/file_system_api.h" 19#include "chrome/browser/media_galleries/fileapi/media_path_filter.h" 20#include "chrome/common/chrome_paths.h" 21#include "components/storage_monitor/storage_monitor.h" 22#include "content/public/browser/browser_thread.h" 23 24#if defined(OS_CHROMEOS) 25#include "chrome/common/chrome_paths.h" 26#include "chromeos/dbus/cros_disks_client.h" 27#endif 28 29using storage_monitor::StorageInfo; 30using storage_monitor::StorageMonitor; 31 32typedef base::Callback<void(const std::vector<base::FilePath>& /*roots*/)> 33 DefaultScanRootsCallback; 34using content::BrowserThread; 35 36namespace { 37 38const int64 kMinimumImageSize = 200 * 1024; // 200 KB 39const int64 kMinimumAudioSize = 500 * 1024; // 500 KB 40const int64 kMinimumVideoSize = 1024 * 1024; // 1 MB 41 42const int kPrunedPaths[] = { 43#if defined(OS_WIN) 44 base::DIR_IE_INTERNET_CACHE, 45 base::DIR_PROGRAM_FILES, 46 base::DIR_PROGRAM_FILESX86, 47 base::DIR_WINDOWS, 48#endif 49#if defined(OS_MACOSX) && !defined(OS_IOS) 50 chrome::DIR_USER_APPLICATIONS, 51 chrome::DIR_USER_LIBRARY, 52#endif 53#if defined(OS_LINUX) 54 base::DIR_CACHE, 55#endif 56#if defined(OS_WIN) || defined(OS_LINUX) 57 base::DIR_TEMP, 58#endif 59}; 60 61bool IsValidScanPath(const base::FilePath& path) { 62 return !path.empty() && path.IsAbsolute(); 63} 64 65void CountScanResult(MediaGalleryScanFileType type, 66 MediaGalleryScanResult* scan_result) { 67 if (type & MEDIA_GALLERY_SCAN_FILE_TYPE_IMAGE) 68 scan_result->image_count += 1; 69 if (type & MEDIA_GALLERY_SCAN_FILE_TYPE_AUDIO) 70 scan_result->audio_count += 1; 71 if (type & MEDIA_GALLERY_SCAN_FILE_TYPE_VIDEO) 72 scan_result->video_count += 1; 73} 74 75bool FileMeetsSizeRequirement(MediaGalleryScanFileType type, int64 size) { 76 if (type & MEDIA_GALLERY_SCAN_FILE_TYPE_IMAGE) 77 if (size >= kMinimumImageSize) 78 return true; 79 if (type & MEDIA_GALLERY_SCAN_FILE_TYPE_AUDIO) 80 if (size >= kMinimumAudioSize) 81 return true; 82 if (type & MEDIA_GALLERY_SCAN_FILE_TYPE_VIDEO) 83 if (size >= kMinimumVideoSize) 84 return true; 85 return false; 86} 87 88// Return true if |path| should not be considered as the starting point for a 89// media scan. 90bool ShouldIgnoreScanRoot(const base::FilePath& path) { 91#if defined(OS_MACOSX) 92 // Scanning root is of little value. 93 return (path.value() == "/"); 94#elif defined(OS_CHROMEOS) 95 // Sanity check to make sure mount points are where they should be. 96 base::FilePath mount_point = 97 chromeos::CrosDisksClient::GetRemovableDiskMountPoint(); 98 return mount_point.IsParent(path); 99#elif defined(OS_LINUX) 100 // /media and /mnt are likely the only places with interesting mount points. 101 if (StartsWithASCII(path.value(), "/media", true) || 102 StartsWithASCII(path.value(), "/mnt", true)) { 103 return false; 104 } 105 return true; 106#elif defined(OS_WIN) 107 return false; 108#else 109 NOTIMPLEMENTED(); 110 return false; 111#endif 112} 113 114// Return a location that is likely to have user data to scan, if any. 115base::FilePath GetPlatformSpecificDefaultScanRoot() { 116 base::FilePath root; 117#if defined(OS_CHROMEOS) 118 PathService::Get(chrome::DIR_DEFAULT_DOWNLOADS_SAFE, &root); 119#elif defined(OS_MACOSX) || defined(OS_LINUX) 120 PathService::Get(base::DIR_HOME, &root); 121#elif defined(OS_WIN) 122 // Nothing to add. 123#else 124 NOTIMPLEMENTED(); 125#endif 126 return root; 127} 128 129// Find the likely locations with user media files and pass them to 130// |callback|. Locations are platform specific. 131void GetDefaultScanRoots(const DefaultScanRootsCallback& callback, 132 bool has_override, 133 const std::vector<base::FilePath>& override_paths) { 134 DCHECK_CURRENTLY_ON(BrowserThread::UI); 135 136 if (has_override) { 137 callback.Run(override_paths); 138 return; 139 } 140 141 StorageMonitor* monitor = StorageMonitor::GetInstance(); 142 DCHECK(monitor->IsInitialized()); 143 144 std::vector<base::FilePath> roots; 145 std::vector<StorageInfo> storages = monitor->GetAllAvailableStorages(); 146 for (size_t i = 0; i < storages.size(); ++i) { 147 StorageInfo::Type type; 148 if (!StorageInfo::CrackDeviceId(storages[i].device_id(), &type, NULL) || 149 (type != StorageInfo::FIXED_MASS_STORAGE && 150 type != StorageInfo::REMOVABLE_MASS_STORAGE_NO_DCIM)) { 151 continue; 152 } 153 base::FilePath path(storages[i].location()); 154 if (ShouldIgnoreScanRoot(path)) 155 continue; 156 roots.push_back(path); 157 } 158 159 base::FilePath platform_root = GetPlatformSpecificDefaultScanRoot(); 160 if (!platform_root.empty()) 161 roots.push_back(platform_root); 162 callback.Run(roots); 163} 164 165} // namespace 166 167MediaFolderFinder::WorkerReply::WorkerReply() {} 168 169MediaFolderFinder::WorkerReply::~WorkerReply() {} 170 171// The Worker is created on the UI thread, but does all its work on a blocking 172// SequencedTaskRunner. 173class MediaFolderFinder::Worker { 174 public: 175 explicit Worker(const std::vector<base::FilePath>& graylisted_folders); 176 ~Worker(); 177 178 // Scans |path| and return the results. 179 WorkerReply ScanFolder(const base::FilePath& path); 180 181 private: 182 void MakeFolderPathsAbsolute(); 183 184 bool folder_paths_are_absolute_; 185 std::vector<base::FilePath> graylisted_folders_; 186 std::vector<base::FilePath> pruned_folders_; 187 188 scoped_ptr<MediaPathFilter> filter_; 189 190 base::SequenceChecker sequence_checker_; 191 192 DISALLOW_COPY_AND_ASSIGN(Worker); 193}; 194 195MediaFolderFinder::Worker::Worker( 196 const std::vector<base::FilePath>& graylisted_folders) 197 : folder_paths_are_absolute_(false), 198 graylisted_folders_(graylisted_folders), 199 filter_(new MediaPathFilter) { 200 DCHECK_CURRENTLY_ON(BrowserThread::UI); 201 202 for (size_t i = 0; i < arraysize(kPrunedPaths); ++i) { 203 base::FilePath path; 204 if (PathService::Get(kPrunedPaths[i], &path)) 205 pruned_folders_.push_back(path); 206 } 207 208 sequence_checker_.DetachFromSequence(); 209} 210 211MediaFolderFinder::Worker::~Worker() { 212 DCHECK(sequence_checker_.CalledOnValidSequencedThread()); 213} 214 215MediaFolderFinder::WorkerReply MediaFolderFinder::Worker::ScanFolder( 216 const base::FilePath& path) { 217 DCHECK(sequence_checker_.CalledOnValidSequencedThread()); 218 CHECK(IsValidScanPath(path)); 219 220 if (!folder_paths_are_absolute_) 221 MakeFolderPathsAbsolute(); 222 223 WorkerReply reply; 224 bool folder_meets_size_requirement = false; 225 bool is_graylisted_folder = false; 226 base::FilePath abspath = base::MakeAbsoluteFilePath(path); 227 if (abspath.empty()) 228 return reply; 229 230 for (size_t i = 0; i < graylisted_folders_.size(); ++i) { 231 if (abspath == graylisted_folders_[i] || 232 abspath.IsParent(graylisted_folders_[i])) { 233 is_graylisted_folder = true; 234 break; 235 } 236 } 237 238 base::FileEnumerator enumerator( 239 path, 240 false, /* recursive? */ 241 base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES 242#if defined(OS_POSIX) 243 | base::FileEnumerator::SHOW_SYM_LINKS // show symlinks, not follow. 244#endif 245 ); // NOLINT 246 while (!enumerator.Next().empty()) { 247 base::FileEnumerator::FileInfo file_info = enumerator.GetInfo(); 248 base::FilePath full_path = path.Append(file_info.GetName()); 249 if (MediaPathFilter::ShouldSkip(full_path)) 250 continue; 251 252 // Enumerating a directory. 253 if (file_info.IsDirectory()) { 254 bool is_pruned_folder = false; 255 base::FilePath abs_full_path = base::MakeAbsoluteFilePath(full_path); 256 if (abs_full_path.empty()) 257 continue; 258 for (size_t i = 0; i < pruned_folders_.size(); ++i) { 259 if (abs_full_path == pruned_folders_[i]) { 260 is_pruned_folder = true; 261 break; 262 } 263 } 264 265 if (!is_pruned_folder) 266 reply.new_folders.push_back(full_path); 267 continue; 268 } 269 270 // Enumerating a file. 271 // 272 // Do not include scan results for graylisted folders. 273 if (is_graylisted_folder) 274 continue; 275 276 MediaGalleryScanFileType type = filter_->GetType(full_path); 277 if (type == MEDIA_GALLERY_SCAN_FILE_TYPE_UNKNOWN) 278 continue; 279 280 CountScanResult(type, &reply.scan_result); 281 if (!folder_meets_size_requirement) { 282 folder_meets_size_requirement = 283 FileMeetsSizeRequirement(type, file_info.GetSize()); 284 } 285 } 286 // Make sure there is at least 1 file above a size threshold. 287 if (!folder_meets_size_requirement) 288 reply.scan_result = MediaGalleryScanResult(); 289 return reply; 290} 291 292void MediaFolderFinder::Worker::MakeFolderPathsAbsolute() { 293 DCHECK(sequence_checker_.CalledOnValidSequencedThread()); 294 DCHECK(!folder_paths_are_absolute_); 295 folder_paths_are_absolute_ = true; 296 297 std::vector<base::FilePath> abs_paths; 298 for (size_t i = 0; i < graylisted_folders_.size(); ++i) { 299 base::FilePath path = base::MakeAbsoluteFilePath(graylisted_folders_[i]); 300 if (!path.empty()) 301 abs_paths.push_back(path); 302 } 303 graylisted_folders_ = abs_paths; 304 abs_paths.clear(); 305 for (size_t i = 0; i < pruned_folders_.size(); ++i) { 306 base::FilePath path = base::MakeAbsoluteFilePath(pruned_folders_[i]); 307 if (!path.empty()) 308 abs_paths.push_back(path); 309 } 310 pruned_folders_ = abs_paths; 311} 312 313MediaFolderFinder::MediaFolderFinder( 314 const MediaFolderFinderResultsCallback& callback) 315 : results_callback_(callback), 316 graylisted_folders_( 317 extensions::file_system_api::GetGrayListedDirectories()), 318 scan_state_(SCAN_STATE_NOT_STARTED), 319 worker_(new Worker(graylisted_folders_)), 320 has_roots_for_testing_(false), 321 weak_factory_(this) { 322 DCHECK_CURRENTLY_ON(BrowserThread::UI); 323 324 base::SequencedWorkerPool* pool = BrowserThread::GetBlockingPool(); 325 worker_task_runner_ = pool->GetSequencedTaskRunner(pool->GetSequenceToken()); 326} 327 328MediaFolderFinder::~MediaFolderFinder() { 329 DCHECK_CURRENTLY_ON(BrowserThread::UI); 330 331 worker_task_runner_->DeleteSoon(FROM_HERE, worker_); 332 333 if (scan_state_ == SCAN_STATE_FINISHED) 334 return; 335 336 MediaFolderFinderResults empty_results; 337 results_callback_.Run(false /* success? */, empty_results); 338} 339 340void MediaFolderFinder::StartScan() { 341 DCHECK_CURRENTLY_ON(BrowserThread::UI); 342 343 if (scan_state_ != SCAN_STATE_NOT_STARTED) 344 return; 345 346 scan_state_ = SCAN_STATE_STARTED; 347 GetDefaultScanRoots( 348 base::Bind(&MediaFolderFinder::OnInitialized, weak_factory_.GetWeakPtr()), 349 has_roots_for_testing_, 350 roots_for_testing_); 351} 352 353const std::vector<base::FilePath>& 354MediaFolderFinder::graylisted_folders() const { 355 return graylisted_folders_; 356} 357 358void MediaFolderFinder::SetRootsForTesting( 359 const std::vector<base::FilePath>& roots) { 360 DCHECK_CURRENTLY_ON(BrowserThread::UI); 361 DCHECK_EQ(SCAN_STATE_NOT_STARTED, scan_state_); 362 363 has_roots_for_testing_ = true; 364 roots_for_testing_ = roots; 365} 366 367void MediaFolderFinder::OnInitialized( 368 const std::vector<base::FilePath>& roots) { 369 DCHECK_EQ(SCAN_STATE_STARTED, scan_state_); 370 371 std::set<base::FilePath> valid_roots; 372 for (size_t i = 0; i < roots.size(); ++i) { 373 // Skip if |path| is invalid or redundant. 374 const base::FilePath& path = roots[i]; 375 if (!IsValidScanPath(path)) 376 continue; 377 if (ContainsKey(valid_roots, path)) 378 continue; 379 380 // Check for overlap. 381 bool valid_roots_contains_path = false; 382 std::vector<base::FilePath> overlapping_paths_to_remove; 383 for (std::set<base::FilePath>::iterator it = valid_roots.begin(); 384 it != valid_roots.end(); ++it) { 385 if (it->IsParent(path)) { 386 valid_roots_contains_path = true; 387 break; 388 } 389 const base::FilePath& other_path = *it; 390 if (path.IsParent(other_path)) 391 overlapping_paths_to_remove.push_back(other_path); 392 } 393 if (valid_roots_contains_path) 394 continue; 395 // Remove anything |path| overlaps from |valid_roots|. 396 for (size_t i = 0; i < overlapping_paths_to_remove.size(); ++i) 397 valid_roots.erase(overlapping_paths_to_remove[i]); 398 399 valid_roots.insert(path); 400 } 401 402 std::copy(valid_roots.begin(), valid_roots.end(), 403 std::back_inserter(folders_to_scan_)); 404 ScanFolder(); 405} 406 407void MediaFolderFinder::ScanFolder() { 408 DCHECK_CURRENTLY_ON(BrowserThread::UI); 409 DCHECK_EQ(SCAN_STATE_STARTED, scan_state_); 410 411 if (folders_to_scan_.empty()) { 412 scan_state_ = SCAN_STATE_FINISHED; 413 results_callback_.Run(true /* success? */, results_); 414 return; 415 } 416 417 base::FilePath folder_to_scan = folders_to_scan_.back(); 418 folders_to_scan_.pop_back(); 419 base::PostTaskAndReplyWithResult( 420 worker_task_runner_.get(), 421 FROM_HERE, 422 base::Bind( 423 &Worker::ScanFolder, base::Unretained(worker_), folder_to_scan), 424 base::Bind(&MediaFolderFinder::GotScanResults, 425 weak_factory_.GetWeakPtr(), 426 folder_to_scan)); 427} 428 429void MediaFolderFinder::GotScanResults(const base::FilePath& path, 430 const WorkerReply& reply) { 431 DCHECK_CURRENTLY_ON(BrowserThread::UI); 432 DCHECK_EQ(SCAN_STATE_STARTED, scan_state_); 433 DCHECK(!path.empty()); 434 CHECK(!ContainsKey(results_, path)); 435 436 if (!IsEmptyScanResult(reply.scan_result)) 437 results_[path] = reply.scan_result; 438 439 // Push new folders to the |folders_to_scan_| in reverse order. 440 std::copy(reply.new_folders.rbegin(), reply.new_folders.rend(), 441 std::back_inserter(folders_to_scan_)); 442 443 ScanFolder(); 444} 445