blob: a15a488ee7f6852814266a8fe56e6ffd5c46eb6a [file] [log] [blame]
// Copyright (c) 2022 The Chromium Authors. All rights reserved.
// 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/extract_io_task.h"
#include <utility>
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/strcat.h"
#include "base/task/thread_pool.h"
#include "base/threading/platform_thread.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/path_util.h"
#include "chrome/browser/chromeos/fileapi/file_system_backend.h"
#include "chrome/browser/platform_util.h"
#include "components/services/unzip/content/unzip_service.h"
#include "components/services/unzip/public/mojom/unzipper.mojom.h"
#include "content/public/browser/browser_thread.h"
#include "third_party/cros_system_api/constants/cryptohome.h"
#include "third_party/zlib/google/redact.h"
namespace file_manager {
namespace io_task {
void RecordUmaExtractStatus(ExtractStatus status) {
UMA_HISTOGRAM_ENUMERATION(kExtractTaskStatusHistogramName, status);
}
ExtractIOTask::ExtractIOTask(
std::vector<storage::FileSystemURL> source_urls,
std::string password,
storage::FileSystemURL parent_folder,
Profile* profile,
scoped_refptr<storage::FileSystemContext> file_system_context,
bool show_notification)
: IOTask(show_notification),
source_urls_(std::move(source_urls)),
password_(std::move(password)),
parent_folder_(std::move(parent_folder)),
profile_(profile),
file_system_context_(std::move(file_system_context)) {
progress_.type = OperationType::kExtract;
progress_.state = State::kQueued;
progress_.destination_folder = parent_folder_;
progress_.bytes_transferred = 0;
progress_.total_bytes = 0;
// Store all the ZIP files in the selection so we have
// a proper count of how many need to be extracted.
for (const storage::FileSystemURL& source_url : source_urls_) {
const base::FilePath source_path = source_url.path();
if (source_path.MatchesExtension(".zip") &&
chromeos::FileSystemBackend::CanHandleURL(source_url)) {
progress_.sources.emplace_back(source_url, absl::nullopt);
}
}
sizingCount_ = extractCount_ = progress_.sources.size();
}
ExtractIOTask::~ExtractIOTask() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
}
void ExtractIOTask::ZipListenerCallback(uint64_t bytes) {
progress_.bytes_transferred += bytes;
speedometer_.Update(progress_.bytes_transferred);
const double remaining_seconds = speedometer_.GetRemainingSeconds();
// Speedometer can produce infinite result which can't be serialized to JSON
// when sending the status via private API.
if (std::isfinite(remaining_seconds)) {
progress_.remaining_seconds = remaining_seconds;
}
progress_callback_.Run(progress_);
}
void ExtractIOTask::FinishedExtraction(base::FilePath directory, bool success) {
if (success) {
// Open a new window to show the extracted content.
platform_util::ShowItemInFolder(profile_, directory);
}
// Release the unpacker parameters stored for the extraction.
auto unpacker = unpackers_[directory];
if (unpacker) {
unpacker->CleanUp();
// Wait for the task runner to clean up the UnpackParams object.
while (!unpacker->CleanUpDone()) {
// Yield until the cancellation tasks are done.
base::PlatformThread::Sleep(base::Microseconds(1));
}
}
DCHECK_GT(extractCount_, 0);
if (--extractCount_ == 0) {
progress_.state = success ? State::kSuccess : State::kError;
RecordUmaExtractStatus(progress_.state == State::kSuccess
? ExtractStatus::kSuccess
: ExtractStatus::kUnknownError);
Complete();
}
}
// Recursively walk directory and set 'u+rwx,g+x,o+x'.
bool SetDirectoryPermissions(base::FilePath directory, bool success) {
// Always set permissions in case of error mid-extract.
base::FileEnumerator traversal(directory, true,
base::FileEnumerator::DIRECTORIES);
for (base::FilePath current = traversal.Next(); !current.empty();
current = traversal.Next()) {
base::SetPosixFilePermissions(current,
base::FILE_PERMISSION_READ_BY_USER |
base::FILE_PERMISSION_WRITE_BY_USER |
base::FILE_PERMISSION_EXECUTE_BY_USER |
base::FILE_PERMISSION_EXECUTE_BY_GROUP |
base::FILE_PERMISSION_EXECUTE_BY_OTHERS);
}
return success;
}
void ExtractIOTask::ZipExtractCallback(base::FilePath destination_directory,
bool success) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(&SetDirectoryPermissions, destination_directory, success),
base::BindOnce(&ExtractIOTask::FinishedExtraction,
weak_ptr_factory_.GetWeakPtr(), destination_directory));
}
void ExtractIOTask::ExtractIntoNewDirectory(
base::FilePath destination_directory,
base::FilePath source_file,
bool created_ok) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (created_ok) {
unzip::mojom::UnzipOptionsPtr options =
unzip::mojom::UnzipOptions::New("auto", password_);
scoped_refptr<unzip::ZipFileUnpacker> unpacker =
base::MakeRefCounted<unzip::ZipFileUnpacker>();
unpacker->Unpack(
unzip::LaunchUnzipper(), source_file, destination_directory,
std::move(options),
base::BindRepeating(&ExtractIOTask::ZipListenerCallback,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&ExtractIOTask::ZipExtractCallback,
weak_ptr_factory_.GetWeakPtr(), destination_directory));
unpackers_.insert({destination_directory, std::move(unpacker)});
} else {
LOG(ERROR) << "Cannot create directory "
<< zip::Redact(destination_directory);
}
}
bool CreateExtractionDirectory(const base::FilePath& destination_directory) {
bool created_ok = base::CreateDirectory(destination_directory);
// Make sure the directory is world readable.
if (created_ok) {
created_ok = base::SetPosixFilePermissions(
destination_directory, base::FILE_PERMISSION_READ_BY_USER |
base::FILE_PERMISSION_WRITE_BY_USER |
base::FILE_PERMISSION_EXECUTE_BY_USER |
base::FILE_PERMISSION_EXECUTE_BY_GROUP |
base::FILE_PERMISSION_EXECUTE_BY_OTHERS);
}
return created_ok;
}
void ExtractIOTask::ExtractArchive(
size_t index,
base::FileErrorOr<storage::FileSystemURL> destination_result) {
DCHECK(index < progress_.sources.size());
const base::FilePath source_file = progress_.sources[index].url.path();
if (destination_result.is_error()) {
ZipExtractCallback(base::FilePath(), false);
} else {
const base::FilePath destination_directory =
destination_result.value().path();
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(&CreateExtractionDirectory, destination_directory),
base::BindOnce(&ExtractIOTask::ExtractIntoNewDirectory,
weak_ptr_factory_.GetWeakPtr(), destination_directory,
source_file));
}
}
void ExtractIOTask::ExtractAllSources() {
for (size_t index = 0; index < progress_.sources.size(); ++index) {
const EntryStatus& source = progress_.sources[index];
const base::FilePath source_file = source.url.path().BaseName();
util::GenerateUnusedFilename(
parent_folder_, source_file.RemoveExtension(), file_system_context_,
base::BindOnce(&ExtractIOTask::ExtractArchive,
weak_ptr_factory_.GetWeakPtr(), index));
}
}
void ExtractIOTask::GotFreeDiskSpace(int64_t free_space) {
auto* drive_integration_service =
drive::util::GetIntegrationServiceByProfile(profile_);
if (progress_.destination_folder.filesystem_id() ==
util::GetDownloadsMountPointName(profile_) ||
(drive_integration_service &&
drive_integration_service->GetMountPointPath().IsParent(
progress_.destination_folder.path()))) {
free_space -= cryptohome::kMinFreeSpaceInBytes;
}
if (progress_.total_bytes > free_space) {
progress_.outputs.emplace_back(progress_.destination_folder,
base::File::FILE_ERROR_NO_SPACE);
progress_.state = State::kError;
RecordUmaExtractStatus(ExtractStatus::kInsufficientDiskSpace);
Complete();
return;
}
if (have_encrypted_content_ && password_.empty()) {
if (uses_aes_encryption_) {
RecordUmaExtractStatus(ExtractStatus::kAesEncrypted);
} else {
RecordUmaExtractStatus(ExtractStatus::kPasswordError);
}
progress_.state = State::kNeedPassword;
Complete();
return;
}
speedometer_.SetTotalBytes(progress_.total_bytes);
ExtractAllSources();
}
void ExtractIOTask::ZipInfoCallback(unzip::mojom::InfoPtr info) {
DCHECK_GT(extractCount_, 0);
if (info->size_is_valid) {
progress_.total_bytes += info->size;
}
have_encrypted_content_ = have_encrypted_content_ || info->is_encrypted;
uses_aes_encryption_ = info->uses_aes_encryption;
if (--sizingCount_ == 0) {
// After getting the size of all the ZIPs, check if we have
// enough available disk space, and if so, extract them.
if (util::IsNonNativeFileSystemType(parent_folder_.type())) {
// Destination is a virtual filesystem, so skip the size check.
ExtractAllSources();
} else {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&base::SysInfo::AmountOfFreeDiskSpace,
parent_folder_.path()),
base::BindOnce(&ExtractIOTask::GotFreeDiskSpace,
weak_ptr_factory_.GetWeakPtr()));
}
}
}
void ExtractIOTask::GetExtractedSize(base::FilePath source_file) {
unzip::GetExtractedInfo(unzip::LaunchUnzipper(), source_file,
base::BindOnce(&ExtractIOTask::ZipInfoCallback,
weak_ptr_factory_.GetWeakPtr()));
}
void ExtractIOTask::CheckSizeThenExtract() {
for (const EntryStatus& source : progress_.sources) {
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(&ExtractIOTask::GetExtractedSize,
weak_ptr_factory_.GetWeakPtr(), source.url.path()));
}
}
void ExtractIOTask::Execute(IOTask::ProgressCallback progress_callback,
IOTask::CompleteCallback complete_callback) {
progress_callback_ = std::move(progress_callback);
complete_callback_ = std::move(complete_callback);
DVLOG(1) << "Executing EXTRACT_ARCHIVE IO task";
progress_.state = State::kInProgress;
progress_callback_.Run(progress_);
// If the backend can't handle the folder to unpack into or
// there are no files to extract, finish the operation with an error.
if (!chromeos::FileSystemBackend::CanHandleURL(parent_folder_) ||
sizingCount_ == 0) {
progress_.state = State::kError;
RecordUmaExtractStatus(ExtractStatus::kUnknownError);
Complete();
} else {
CheckSizeThenExtract();
}
}
void ExtractIOTask::Cancel() {
progress_.state = State::kCancelled;
RecordUmaExtractStatus(ExtractStatus::kCancelled);
// Run through all existing extraction instances and cancel them all.
for (auto unpacker : unpackers_) {
if (unpacker.second) {
unpacker.second->Stop();
while (!unpacker.second->CleanUpDone()) {
// Yield until the UnpackParams objects have been released.
base::PlatformThread::Sleep(base::Microseconds(1));
}
}
}
}
// Calls the completion callback for the task. |progress_| should not be
// accessed after calling this.
void ExtractIOTask::Complete() {
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(std::move(complete_callback_), std::move(progress_)));
}
} // namespace io_task
} // namespace file_manager