blob: e9f8afffb4144e640ba0e6e8561ae0c36d9d114a [file] [log] [blame]
// Copyright 2018 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 "components/download/internal/common/download_db_cache.h"
#include "base/bind.h"
#include "components/download/database/download_db.h"
#include "components/download/database/download_db_conversions.h"
#include "components/download/database/download_db_entry.h"
#include "components/download/public/common/download_features.h"
#include "components/download/public/common/download_stats.h"
#include "components/download/public/common/download_utils.h"
namespace download {
namespace {
// Interval between download DB updates.
// TODO(qinmin): make this finch configurable.
const int kUpdateDBIntervalMs = 10000;
enum class ShouldUpdateDownloadDBResult {
NO_UPDATE,
UPDATE,
UPDATE_IMMEDIATELY,
};
ShouldUpdateDownloadDBResult ShouldUpdateDownloadDB(
base::Optional<DownloadDBEntry> previous,
const DownloadDBEntry& current) {
if (!previous)
return ShouldUpdateDownloadDBResult::UPDATE_IMMEDIATELY;
base::Optional<InProgressInfo> previous_info;
if (previous->download_info)
previous_info = previous->download_info->in_progress_info;
base::FilePath previous_path =
previous_info ? previous_info->current_path : base::FilePath();
bool previous_paused = previous_info ? previous_info->paused : false;
base::Optional<InProgressInfo> current_info;
if (current.download_info)
current_info = current.download_info->in_progress_info;
base::FilePath current_path =
current_info ? current_info->current_path : base::FilePath();
bool paused = current_info ? current_info->paused : false;
// When download path is determined, Chrome should commit the history
// immediately. Otherwise the file will be left permanently on the external
// storage if Chrome crashes right away.
if (current_path != previous_path || paused != previous_paused)
return ShouldUpdateDownloadDBResult::UPDATE_IMMEDIATELY;
if (previous.value() == current)
return ShouldUpdateDownloadDBResult::NO_UPDATE;
return ShouldUpdateDownloadDBResult::UPDATE;
}
void CleanUpInProgressEntry(DownloadDBEntry* entry) {
if (!entry->download_info)
return;
base::Optional<InProgressInfo>& in_progress_info =
entry->download_info->in_progress_info;
if (!in_progress_info)
return;
if (in_progress_info->state == DownloadItem::DownloadState::IN_PROGRESS) {
in_progress_info->state = DownloadItem::DownloadState::INTERRUPTED;
in_progress_info->interrupt_reason =
download::DOWNLOAD_INTERRUPT_REASON_CRASH;
}
}
void OnDownloadDBUpdated(bool success) {
// TODO(qinmin): handle the case that update fails.
if (!success)
LOG(ERROR) << "Unable to update DB entries";
}
// Check if a DownloadDBEntry represents an in progress download.
bool IsInProgressEntry(base::Optional<DownloadDBEntry> entry) {
if (!entry || !entry->download_info ||
!entry->download_info->in_progress_info)
return false;
return entry->download_info->in_progress_info->state ==
DownloadItem::DownloadState::IN_PROGRESS;
}
} // namespace
DownloadDBCache::DownloadDBCache(std::unique_ptr<DownloadDB> download_db)
: initialized_(false), download_db_(std::move(download_db)) {
DCHECK(download_db_);
}
DownloadDBCache::~DownloadDBCache() = default;
void DownloadDBCache::Initialize(InitializeCallback callback) {
DCHECK(!initialized_);
download_db_->Initialize(
base::BindOnce(&DownloadDBCache::OnDownloadDBInitialized,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
base::Optional<DownloadDBEntry> DownloadDBCache::RetrieveEntry(
const std::string& guid) {
auto iter = cached_entries_.find(guid);
if (iter != cached_entries_.end())
return iter->second;
return base::nullopt;
}
void DownloadDBCache::AddOrReplaceEntry(const DownloadDBEntry& entry) {
if (!entry.download_info)
return;
const std::string& guid = entry.download_info->guid;
ShouldUpdateDownloadDBResult result =
ShouldUpdateDownloadDB(RetrieveEntry(guid), entry);
if (result == ShouldUpdateDownloadDBResult::NO_UPDATE)
return;
if (!update_timer_.IsRunning() &&
result == ShouldUpdateDownloadDBResult::UPDATE) {
update_timer_.Start(FROM_HERE,
base::TimeDelta::FromMilliseconds(kUpdateDBIntervalMs),
this, &DownloadDBCache::UpdateDownloadDB);
}
cached_entries_[guid] = entry;
updated_guids_.emplace(guid);
if (result == ShouldUpdateDownloadDBResult::UPDATE_IMMEDIATELY) {
UpdateDownloadDB();
update_timer_.Stop();
}
}
void DownloadDBCache::RemoveEntry(const std::string& guid) {
cached_entries_.erase(guid);
updated_guids_.erase(guid);
if (initialized_)
download_db_->Remove(guid);
}
void DownloadDBCache::UpdateDownloadDB() {
if (updated_guids_.empty())
return;
std::vector<DownloadDBEntry> entries;
for (const auto& guid : updated_guids_) {
base::Optional<DownloadDBEntry> entry = RetrieveEntry(guid);
DCHECK(entry);
entries.emplace_back(entry.value());
// If the entry is no longer in-progress, remove it from the cache as it may
// not update again soon.
if (!IsInProgressEntry(entry))
cached_entries_.erase(guid);
}
updated_guids_.clear();
if (initialized_) {
download_db_->AddOrReplaceEntries(entries,
base::BindOnce(&OnDownloadDBUpdated));
}
}
void DownloadDBCache::OnDownloadUpdated(DownloadItem* download) {
// TODO(crbug.com/778425): Properly handle fail/resume/retry for downloads
// that are in the INTERRUPTED state for a long time.
if (!base::FeatureList::IsEnabled(features::kDownloadDBForNewDownloads)) {
// If history service is still managing in-progress download, we can safely
// remove a download from the in-progress DB whenever it is in the terminal
// state. Otherwise, we need to propagate the completed download to
// history service before we can remove it.
if (download->IsDone()) {
OnDownloadRemoved(download);
return;
}
}
DownloadDBEntry entry = CreateDownloadDBEntryFromItem(
*(static_cast<DownloadItemImpl*>(download)));
AddOrReplaceEntry(entry);
}
void DownloadDBCache::OnDownloadRemoved(DownloadItem* download) {
RemoveEntry(download->GetGuid());
}
void DownloadDBCache::OnDownloadDBInitialized(
InitializeCallback callback,
bool success) {
if (success) {
RecordInProgressDBCount(kInitializationSucceededCount);
download_db_->LoadEntries(
base::BindOnce(&DownloadDBCache::OnDownloadDBEntriesLoaded,
weak_factory_.GetWeakPtr(), std::move(callback)));
} else {
RecordInProgressDBCount(kInitializationFailedCount);
std::move(callback).Run(false,
std::make_unique<std::vector<DownloadDBEntry>>());
}
}
void DownloadDBCache::OnDownloadDBEntriesLoaded(
InitializeCallback callback,
bool success,
std::unique_ptr<std::vector<DownloadDBEntry>> entries) {
initialized_ = success;
RecordInProgressDBCount(success ? kLoadSucceededCount : kLoadFailedCount);
for (auto& entry : *entries) {
// If the entry is from the metadata cache migration, just remove it from
// DB as the data is not being cleaned up properly.
if (entry.download_info->id < 0)
RemoveEntry(entry.download_info->guid);
else
CleanUpInProgressEntry(&entry);
}
std::move(callback).Run(success, std::move(entries));
}
void DownloadDBCache::SetTimerTaskRunnerForTesting(
scoped_refptr<base::SequencedTaskRunner> task_runner) {
update_timer_.SetTaskRunner(task_runner);
}
} // namespace download