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