| // 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.path(), 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 |