chromium/chrome/browser/ash/fileapi/recent_disk_source.cc

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/fileapi/recent_disk_source.h"

#include <utility>

#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "net/base/mime_util.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "storage/browser/file_system/file_system_context.h"
#include "storage/browser/file_system/file_system_operation.h"
#include "storage/browser/file_system/file_system_operation_runner.h"
#include "storage/browser/file_system/file_system_url.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "ui/file_manager/file_types_data.h"
#include "url/origin.h"

using content::BrowserThread;

namespace ash {

namespace {

constexpr char kAudioMimeType[] = "audio/*";
constexpr char kImageMimeType[] = "image/*";
constexpr char kVideoMimeType[] = "video/*";

void OnReadDirectoryOnIOThread(
    const storage::FileSystemOperation::ReadDirectoryCallback& callback,
    base::File::Error result,
    storage::FileSystemOperation::FileEntryList entries,
    bool has_more) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(callback, result, std::move(entries), has_more));
}

void ReadDirectoryOnIOThread(
    scoped_refptr<storage::FileSystemContext> file_system_context,
    const storage::FileSystemURL& url,
    const storage::FileSystemOperation::ReadDirectoryCallback& callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  file_system_context->operation_runner()->ReadDirectory(
      url, base::BindRepeating(&OnReadDirectoryOnIOThread, callback));
}

void OnGetMetadataOnIOThread(
    storage::FileSystemOperation::GetMetadataCallback callback,
    base::File::Error result,
    const base::File::Info& info) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), result, info));
}

void GetMetadataOnIOThread(
    scoped_refptr<storage::FileSystemContext> file_system_context,
    const storage::FileSystemURL& url,
    storage::FileSystemOperation::GetMetadataFieldSet fields,
    storage::FileSystemOperation::GetMetadataCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  file_system_context->operation_runner()->GetMetadata(
      url, fields,
      base::BindOnce(&OnGetMetadataOnIOThread, std::move(callback)));
}

}  // namespace

RecentDiskSource::RecentDiskSource::CallContext::CallContext(
    const Params& params,
    GetRecentFilesCallback callback)
    : params(params),
      callback(std::move(callback)),
      build_start_time(base::TimeTicks::Now()),
      accumulator(params.max_files()) {}

RecentDiskSource::RecentDiskSource::CallContext::CallContext(
    CallContext&& context)
    : params(context.params),
      callback(std::move(context.callback)),
      build_start_time(context.build_start_time),
      inflight_readdirs(context.inflight_readdirs),
      inflight_stats(context.inflight_stats),
      accumulator(std::move(context.accumulator)) {}

RecentDiskSource::RecentDiskSource::CallContext::~CallContext() = default;

RecentDiskSource::RecentDiskSource(
    extensions::api::file_manager_private::VolumeType volume_type,
    std::string mount_point_name,
    bool ignore_dotfiles,
    int max_depth,
    std::string uma_histogram_name)
    : RecentSource(volume_type),
      mount_point_name_(std::move(mount_point_name)),
      ignore_dotfiles_(ignore_dotfiles),
      max_depth_(max_depth),
      uma_histogram_name_(std::move(uma_histogram_name)) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
}

RecentDiskSource::~RecentDiskSource() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
}

void RecentDiskSource::GetRecentFiles(const Params& params,
                                      GetRecentFilesCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(context_map_.Lookup(params.call_id()) == nullptr);

  // Return immediately if mount point does not exist.
  storage::ExternalMountPoints* mount_points =
      storage::ExternalMountPoints::GetSystemInstance();
  base::FilePath path;
  if (!mount_points->GetRegisteredPath(mount_point_name_, &path)) {
    std::move(callback).Run({});
    return;
  }

  // Create a unique context for this call.
  auto context = std::make_unique<CallContext>(params, std::move(callback));
  context_map_.AddWithID(std::move(context), params.call_id());

  ScanDirectory(params.call_id(), base::FilePath(), 1);
}

std::vector<RecentFile> RecentDiskSource::Stop(const int32_t call_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  CallContext* context = context_map_.Lookup(call_id);
  if (context == nullptr) {
    // The Stop method was called after we already responded. Just return empty
    // list of files.
    return {};
  }
  // Proper stop; get the files and erase the context.
  const std::vector<RecentFile> files = context->accumulator.Get();
  context_map_.Remove(call_id);
  return files;
}

void RecentDiskSource::ScanDirectory(const int32_t call_id,
                                     const base::FilePath& path,
                                     int depth) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  // If context is gone, that is Stop() has been called, exit immediately.
  CallContext* context = context_map_.Lookup(call_id);
  if (context == nullptr) {
    return;
  }

  storage::FileSystemURL url = BuildDiskURL(context->params, path);

  ++context->inflight_readdirs;
  content::GetIOThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(
          &ReadDirectoryOnIOThread,
          base::WrapRefCounted(context->params.file_system_context()), url,
          base::BindRepeating(&RecentDiskSource::OnReadDirectory,
                              weak_ptr_factory_.GetWeakPtr(), call_id, path,
                              depth)));
}

void RecentDiskSource::OnReadDirectory(
    const int32_t call_id,
    const base::FilePath& path,
    const int depth,
    base::File::Error result,
    storage::FileSystemOperation::FileEntryList entries,
    bool has_more) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // If context is gone, that is Stop() has been called, exit immediately.
  CallContext* context = context_map_.Lookup(call_id);
  if (context == nullptr) {
    return;
  }

  const std::u16string q16 = base::UTF8ToUTF16(context->params.query());
  for (const auto& entry : entries) {
    // Ignore directories and files that start with dot.
    if (ignore_dotfiles_ &&
        base::StartsWith(entry.name.value(), ".",
                         base::CompareCase::INSENSITIVE_ASCII)) {
      continue;
    }
    base::FilePath subpath = path.Append(entry.name);

    if (entry.type == filesystem::mojom::FsFileType::DIRECTORY) {
      if ((max_depth_ > 0 && depth >= max_depth_) || context->params.IsLate()) {
        continue;
      }
      ScanDirectory(call_id, subpath, depth + 1);
    } else {
      if (!MatchesFileType(entry.name, context->params.file_type())) {
        continue;
      }
      if (!FileNameMatches(base::UTF8ToUTF16(entry.name.value()), q16)) {
        continue;
      }
      storage::FileSystemURL url = BuildDiskURL(context->params, subpath);
      ++context->inflight_stats;
      content::GetIOThreadTaskRunner({})->PostTask(
          FROM_HERE,
          base::BindOnce(
              &GetMetadataOnIOThread,
              base::WrapRefCounted(context->params.file_system_context()), url,
              storage::FileSystemOperation::GetMetadataFieldSet(
                  {storage::FileSystemOperation::GetMetadataField::
                       kLastModified}),
              base::BindOnce(&RecentDiskSource::OnGotMetadata,
                             weak_ptr_factory_.GetWeakPtr(), call_id, url)));
    }
  }

  if (has_more) {
    return;
  }

  --context->inflight_readdirs;
  if (context->inflight_stats == 0 && context->inflight_readdirs == 0) {
    OnReadOrStatFinished(call_id);
  }
}

void RecentDiskSource::OnGotMetadata(const int32_t call_id,
                                     const storage::FileSystemURL& url,
                                     base::File::Error result,
                                     const base::File::Info& info) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // If context is gone, that is Stop() has been called, exit immediately.
  CallContext* context = context_map_.Lookup(call_id);
  if (context == nullptr) {
    return;
  }

  if (result == base::File::FILE_OK &&
      info.last_modified >= context->params.cutoff_time()) {
    context->accumulator.Add(RecentFile(url, info.last_modified));
  }

  --context->inflight_stats;
  if (context->inflight_stats == 0 && context->inflight_readdirs == 0) {
    OnReadOrStatFinished(call_id);
  }
}

void RecentDiskSource::OnReadOrStatFinished(const int32_t call_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  CallContext* context = context_map_.Lookup(call_id);
  // If context is gone, that is Stop() has been called, exit immediately.
  if (context == nullptr) {
    return;
  }

  DCHECK(context->inflight_stats == 0);
  DCHECK(context->inflight_readdirs == 0);
  DCHECK(!context->build_start_time.is_null());

  // All reads/scans completed.
  UmaHistogramTimes(uma_histogram_name_,
                    base::TimeTicks::Now() - context->build_start_time);

  std::move(context->callback).Run(context->accumulator.Get());
  context_map_.Remove(call_id);
}

storage::FileSystemURL RecentDiskSource::BuildDiskURL(
    const Params& params,
    const base::FilePath& path) const {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  storage::ExternalMountPoints* mount_points =
      storage::ExternalMountPoints::GetSystemInstance();
  return mount_points->CreateExternalFileSystemURL(
      blink::StorageKey::CreateFirstParty(url::Origin::Create(params.origin())),
      mount_point_name_, path);
}

bool RecentDiskSource::MatchesFileType(const base::FilePath& path,
                                       RecentSource::FileType file_type) {
  if (file_type == RecentSource::FileType::kAll) {
    return true;
  }

  // File type for |path| is guessed by data generated from file_types.json5.
  // It guesses mime types based on file extensions, but it has a limited set
  // of file extensions.
  // TODO(fukino): It is better to have better coverage of file extensions to be
  // consistent with file-type detection on Android system. crbug.com/1034874.
  const auto ext = base::ToLowerASCII(path.Extension());
  if (!file_types_data::kExtensionToMIME.contains(ext)) {
    return false;
  }
  std::string mime_type = file_types_data::kExtensionToMIME.at(ext);

  switch (file_type) {
    case RecentSource::FileType::kAudio:
      return net::MatchesMimeType(kAudioMimeType, mime_type);
    case RecentSource::FileType::kImage:
      return net::MatchesMimeType(kImageMimeType, mime_type);
    case RecentSource::FileType::kVideo:
      return net::MatchesMimeType(kVideoMimeType, mime_type);
    case RecentSource::FileType::kDocument:
      return file_types_data::kDocumentMIMETypes.contains(mime_type);
    default:
      return false;
  }
}

}  // namespace ash