| // Copyright 2021 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/file_manager/zip_io_task.h" |
| |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include "base/check_op.h" |
| #include "base/files/file.h" |
| #include "base/files/file_error_or.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/callback.h" |
| #include "base/logging.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/system/sys_info.h" |
| #include "base/task/bind_post_task.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/thread_pool.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/ash/drive/drive_integration_service.h" |
| #include "chrome/browser/ash/drive/file_system_util.h" |
| #include "chrome/browser/ash/file_manager/fileapi_util.h" |
| #include "chrome/browser/ash/file_manager/filesystem_api_util.h" |
| #include "chrome/browser/ash/file_manager/io_task.h" |
| #include "chrome/browser/ash/file_manager/office_file_tasks.h" |
| #include "chrome/browser/ash/fileapi/file_system_backend.h" |
| #include "chrome/browser/file_util_service.h" |
| #include "chrome/services/file_util/public/cpp/zip_file_creator.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "storage/browser/file_system/file_system_context.h" |
| #include "storage/browser/file_system/file_system_url.h" |
| |
| namespace file_manager { |
| namespace io_task { |
| |
| namespace { |
| |
| int64_t ComputeSize(base::FilePath src_dir, |
| std::vector<base::FilePath> src_files) { |
| VLOG(1) << ">>> Computing total size of " << src_files.size() << " items..."; |
| int64_t total_bytes = 0; |
| base::File::Info info; |
| for (const base::FilePath& relative_path : src_files) { |
| const base::FilePath absolute_path = src_dir.Append(relative_path); |
| |
| if (base::GetFileInfo(absolute_path, &info)) { |
| total_bytes += info.is_directory |
| ? base::ComputeDirectorySize(absolute_path) |
| : info.size; |
| } |
| } |
| VLOG(1) << "<<< Total size is " << total_bytes << " bytes"; |
| return total_bytes; |
| } |
| |
| } // namespace |
| |
| ZipIOTask::ZipIOTask( |
| std::vector<storage::FileSystemURL> source_urls, |
| storage::FileSystemURL parent_folder, |
| Profile* profile, |
| scoped_refptr<storage::FileSystemContext> file_system_context, |
| bool show_notification) |
| : IOTask(show_notification), |
| profile_(profile), |
| file_system_context_(file_system_context) { |
| progress_.state = State::kQueued; |
| progress_.type = OperationType::kZip; |
| progress_.SetDestinationFolder(std::move(parent_folder), profile); |
| progress_.bytes_transferred = 0; |
| progress_.total_bytes = 0; |
| |
| for (auto& url : source_urls) { |
| progress_.sources.emplace_back(std::move(url), std::nullopt); |
| } |
| } |
| |
| ZipIOTask::~ZipIOTask() { |
| if (zip_file_creator_) { |
| zip_file_creator_->Stop(); |
| } |
| } |
| |
| void ZipIOTask::Execute(IOTask::ProgressCallback progress_callback, |
| IOTask::CompleteCallback complete_callback) { |
| progress_callback_ = std::move(progress_callback); |
| complete_callback_ = std::move(complete_callback); |
| |
| start_time_ = base::TimeTicks::Now(); |
| |
| if (progress_.sources.size() == 0) { |
| Complete(State::kSuccess); |
| return; |
| } |
| progress_.state = State::kInProgress; |
| |
| // Convert the destination folder URL to absolute path. |
| source_dir_ = progress_.GetDestinationFolder().path(); |
| if (!ash::FileSystemBackend::CanHandleURL(progress_.GetDestinationFolder()) || |
| source_dir_.empty()) { |
| progress_.outputs.emplace_back(progress_.GetDestinationFolder(), |
| base::File::FILE_ERROR_NOT_FOUND); |
| Complete(State::kError); |
| return; |
| } |
| |
| // Convert source file URLs to relative paths. |
| for (EntryStatus& source : progress_.sources) { |
| const base::FilePath absolute_path = source.url.path(); |
| if (!ash::FileSystemBackend::CanHandleURL(source.url) || |
| absolute_path.empty()) { |
| source.error = base::File::FILE_ERROR_NOT_FOUND; |
| Complete(State::kError); |
| return; |
| } |
| |
| base::FilePath relative_path; |
| if (!source_dir_.AppendRelativePath(absolute_path, &relative_path)) { |
| source.error = base::File::FILE_ERROR_INVALID_OPERATION; |
| Complete(State::kError); |
| return; |
| } |
| source_relative_paths_.push_back(std::move(relative_path)); |
| |
| if (file_manager::util::IsDriveLocalPath(profile_, absolute_path) && |
| file_manager::file_tasks::IsOfficeFile(absolute_path)) { |
| UMA_HISTOGRAM_ENUMERATION( |
| file_manager::file_tasks::kUseOutsideDriveMetricName, |
| file_manager::file_tasks::OfficeFilesUseOutsideDriveHook::ZIP); |
| auto* drive_service = |
| drive::util::GetIntegrationServiceByProfile(profile_); |
| if (drive_service) { |
| drive_service->ForceReSyncFile( |
| absolute_path, base::BindOnce(&ZipIOTask::OnFilePreprocessed, |
| weak_ptr_factory_.GetWeakPtr())); |
| continue; |
| } |
| } |
| OnFilePreprocessed(); |
| } |
| } |
| |
| void ZipIOTask::OnFilePreprocessed() { |
| DCHECK_LT(files_preprocessed_, progress_.sources.size()); |
| files_preprocessed_++; |
| if (files_preprocessed_ < progress_.sources.size()) { |
| return; |
| } |
| |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE}, |
| base::BindOnce(&ComputeSize, source_dir_, source_relative_paths_), |
| base::BindOnce(&ZipIOTask::GenerateZipNameAfterGotTotalBytes, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ZipIOTask::Cancel() { |
| progress_.state = State::kCancelled; |
| // Any inflight operation will be cancelled when the task is destroyed. |
| } |
| |
| // Calls the completion callback for the task. |progress_| should not be |
| // accessed after calling this. |
| void ZipIOTask::Complete(State state) { |
| progress_.state = state; |
| if (state == State::kSuccess) { |
| base::UmaHistogramTimes("FileBrowser.ZipTask.Time", |
| base::TimeTicks::Now() - start_time_); |
| } |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(complete_callback_), std::move(progress_))); |
| } |
| |
| // Generates the destination url for the ZIP file. |
| void ZipIOTask::GenerateZipNameAfterGotTotalBytes(int64_t total_bytes) { |
| progress_.total_bytes = total_bytes; |
| speedometer_.SetTotalBytes(progress_.total_bytes); |
| |
| // TODO(crbug.com/1238237) Localize the name. |
| base::FilePath zip_name("Archive.zip"); |
| if (source_relative_paths_.size() == 1) { |
| zip_name = source_relative_paths_[0].BaseName().ReplaceExtension("zip"); |
| } |
| util::GenerateUnusedFilename( |
| progress_.GetDestinationFolder(), zip_name, file_system_context_, |
| base::BindOnce(&ZipIOTask::ZipItems, weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| // Starts the zip operation. |
| void ZipIOTask::ZipItems( |
| base::FileErrorOr<storage::FileSystemURL> destination_result) { |
| if (!destination_result.has_value()) { |
| progress_.outputs.emplace_back(progress_.GetDestinationFolder(), |
| destination_result.error()); |
| Complete(State::kError); |
| return; |
| } |
| progress_.outputs.emplace_back(destination_result.value(), std::nullopt); |
| progress_callback_.Run(progress_); |
| |
| zip_file_creator_ = base::MakeRefCounted<ZipFileCreator>( |
| std::move(source_dir_), std::move(source_relative_paths_), |
| std::move(destination_result->path())); |
| zip_file_creator_->SetProgressCallback(base::BindOnce( |
| &ZipIOTask::OnZipProgress, weak_ptr_factory_.GetWeakPtr())); |
| zip_file_creator_->SetCompletionCallback( |
| BindPostTaskToCurrentDefault(base::BindOnce( |
| &ZipIOTask::OnZipComplete, weak_ptr_factory_.GetWeakPtr()))); |
| zip_file_creator_->Start(LaunchFileUtilService()); |
| } |
| |
| void ZipIOTask::OnZipProgress() { |
| DCHECK(zip_file_creator_); |
| progress_.bytes_transferred = zip_file_creator_->GetProgress().bytes; |
| if (speedometer_.Update(progress_.bytes_transferred)) { |
| const base::TimeDelta remaining_time = speedometer_.GetRemainingTime(); |
| |
| // Speedometer can produce infinite result which can't be serialized to JSON |
| // when sending the status via private API. |
| if (!remaining_time.is_inf()) { |
| progress_.remaining_seconds = remaining_time.InSecondsF(); |
| } |
| } |
| |
| progress_callback_.Run(progress_); |
| if (zip_file_creator_->GetResult() == ZipFileCreator::kInProgress) { |
| zip_file_creator_->SetProgressCallback(base::BindOnce( |
| &ZipIOTask::OnZipProgress, weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| |
| void ZipIOTask::OnZipComplete() { |
| DCHECK(zip_file_creator_); |
| progress_.bytes_transferred = zip_file_creator_->GetProgress().bytes; |
| switch (zip_file_creator_->GetResult()) { |
| case ZipFileCreator::kSuccess: |
| progress_.outputs.back().error = base::File::FILE_OK; |
| Complete(State::kSuccess); |
| break; |
| case ZipFileCreator::kError: |
| progress_.outputs.back().error = base::File::FILE_ERROR_FAILED; |
| LOG(ERROR) << "Cannot create Zip archive: " |
| << zip_file_creator_->GetResult(); |
| Complete(State::kError); |
| break; |
| case ZipFileCreator::kCancelled: |
| // Cancelled state already gets reported so don't call Complete(). |
| break; |
| case ZipFileCreator::kInProgress: |
| NOTREACHED(); |
| } |
| zip_file_creator_.reset(); |
| } |
| |
| } // namespace io_task |
| } // namespace file_manager |