chromium/chrome/browser/ash/file_manager/zip_io_task.cc

// 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_IN_MIGRATION();
  }
  zip_file_creator_.reset();
}

}  // namespace io_task
}  // namespace file_manager