blob: f3bdeca6dccb63273c96abecd12ebbc67e6e2b78 [file] [log] [blame]
// Copyright (c) 2006-2008 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/history/thumbnail_database.h"
#include "base/file_util.h"
#include "base/gfx/jpeg_codec.h"
#include "base/time.h"
#include "base/string_util.h"
#include "chrome/browser/history/history_publisher.h"
#include "chrome/browser/history/url_database.h"
#include "chrome/common/sqlite_utils.h"
#include "chrome/common/thumbnail_score.h"
#include "third_party/skia/include/core/SkBitmap.h"
using base::Time;
namespace history {
// Version number of the database.
static const int kCurrentVersionNumber = 3;
static const int kCompatibleVersionNumber = 3;
ThumbnailDatabase::ThumbnailDatabase()
: db_(NULL),
statement_cache_(NULL),
transaction_nesting_(0),
history_publisher_(NULL) {
}
ThumbnailDatabase::~ThumbnailDatabase() {
// The DBCloseScoper will delete the DB and the cache.
}
InitStatus ThumbnailDatabase::Init(const FilePath& db_name,
const HistoryPublisher* history_publisher) {
history_publisher_ = history_publisher;
// OpenSqliteDb uses the narrow version of open, so the resulting DB is in
// UTF-8.
if (OpenSqliteDb(db_name, &db_) != SQLITE_OK)
return INIT_FAILURE;
// Set the database page size to something larger to give us
// better performance (we're typically seek rather than bandwidth limited).
// This only has an effect before any tables have been created, otherwise
// this is a NOP. Must be a power of 2 and a max of 8192. We use a bigger
// one because we're storing larger data (4-16K) in it, so we want a few
// blocks per element.
sqlite3_exec(db_, "PRAGMA page_size=4096", NULL, NULL, NULL);
// The UI is generally designed to work well when the thumbnail database is
// slow, so we can tolerate much less caching. The file is also very large
// and so caching won't save a significant percentage of it for us,
// reducing the benefit of caching in the first place. With the default cache
// size of 2000 pages, it will take >8MB of memory, so reducing it can be a
// big savings.
sqlite3_exec(db_, "PRAGMA cache_size=64", NULL, NULL, NULL);
// Run the database in exclusive mode. Nobody else should be accessing the
// database while we're running, and this will give somewhat improved perf.
sqlite3_exec(db_, "PRAGMA locking_mode=EXCLUSIVE", NULL, NULL, NULL);
statement_cache_ = new SqliteStatementCache;
DBCloseScoper scoper(&db_, &statement_cache_);
// Scope initialization in a transaction so we can't be partially initialized.
SQLTransaction transaction(db_);
transaction.Begin();
// Create the tables.
if (!meta_table_.Init(std::string(), kCurrentVersionNumber,
kCompatibleVersionNumber, db_) ||
!InitThumbnailTable() ||
!InitFavIconsTable(false))
return INIT_FAILURE;
InitFavIconsIndex();
// Version check. We should not encounter a database too old for us to handle
// in the wild, so we try to continue in that case.
if (meta_table_.GetCompatibleVersionNumber() > kCurrentVersionNumber) {
LOG(WARNING) << "Thumbnail database is too new.";
return INIT_TOO_NEW;
}
int cur_version = meta_table_.GetVersionNumber();
if (cur_version == 2) {
if (!UpgradeToVersion3()) {
LOG(WARNING) << "Unable to update to thumbnail database to version 3.";
return INIT_FAILURE;
}
++cur_version;
}
LOG_IF(WARNING, cur_version < kCurrentVersionNumber) <<
"Thumbnail database version " << cur_version << " is too old to handle.";
// Initialization is complete.
if (transaction.Commit() != SQLITE_OK)
return INIT_FAILURE;
// Initialize the statement cache and the scoper to automatically close it.
statement_cache_->set_db(db_);
scoper.Detach();
close_scoper_.Attach(&db_, &statement_cache_);
// The following code is useful in debugging the thumbnail database. Upon
// startup, it will spit out a file for each thumbnail in the database so you
// can open them in an external viewer. Insert the path name on your system
// into the string below (I recommend using a blank directory since there may
// be a lot of files).
#if 0
SQLStatement statement;
statement.prepare(db_, "SELECT id, image_data FROM favicons");
while (statement.step() == SQLITE_ROW) {
int idx = statement.column_int(0);
std::vector<unsigned char> data;
statement.column_blob_as_vector(1, &data);
char filename[256];
sprintf(filename, "<<< YOUR PATH HERE >>>\\%d.png", idx);
if (!data.empty()) {
file_util::WriteFile(ASCIIToWide(std::string(filename)),
reinterpret_cast<char*>(&data[0]),
data.size());
}
}
#endif
return INIT_OK;
}
bool ThumbnailDatabase::InitThumbnailTable() {
if (!DoesSqliteTableExist(db_, "thumbnails")) {
if (sqlite3_exec(db_, "CREATE TABLE thumbnails ("
"url_id INTEGER PRIMARY KEY,"
"boring_score DOUBLE DEFAULT 1.0,"
"good_clipping INTEGER DEFAULT 0,"
"at_top INTEGER DEFAULT 0,"
"last_updated INTEGER DEFAULT 0,"
"data BLOB)", NULL, NULL, NULL) != SQLITE_OK)
return false;
}
return true;
}
bool ThumbnailDatabase::UpgradeToVersion3() {
// sqlite doesn't like the "ALTER TABLE xxx ADD (column_one, two,
// three)" syntax, so list out the commands we need to execute:
const char* alterations[] = {
"ALTER TABLE thumbnails ADD boring_score DOUBLE DEFAULT 1.0",
"ALTER TABLE thumbnails ADD good_clipping INTEGER DEFAULT 0",
"ALTER TABLE thumbnails ADD at_top INTEGER DEFAULT 0",
"ALTER TABLE thumbnails ADD last_updated INTEGER DEFAULT 0",
NULL
};
for (int i = 0; alterations[i] != NULL; ++i) {
if (sqlite3_exec(db_, alterations[i],
NULL, NULL, NULL) != SQLITE_OK) {
NOTREACHED();
return false;
}
}
meta_table_.SetVersionNumber(3);
meta_table_.SetCompatibleVersionNumber(std::min(3, kCompatibleVersionNumber));
return true;
}
bool ThumbnailDatabase::RecreateThumbnailTable() {
if (sqlite3_exec(db_, "DROP TABLE thumbnails", NULL, NULL, NULL) != SQLITE_OK)
return false;
return InitThumbnailTable();
}
bool ThumbnailDatabase::InitFavIconsTable(bool is_temporary) {
// Note: if you update the schema, don't forget to update
// CopyToTemporaryFaviconTable as well.
const char* name = is_temporary ? "temp_favicons" : "favicons";
if (!DoesSqliteTableExist(db_, name)) {
std::string sql;
sql.append("CREATE TABLE ");
sql.append(name);
sql.append("("
"id INTEGER PRIMARY KEY,"
"url LONGVARCHAR NOT NULL,"
"last_updated INTEGER DEFAULT 0,"
"image_data BLOB)");
if (sqlite3_exec(db_, sql.c_str(), NULL, NULL, NULL) != SQLITE_OK)
return false;
}
return true;
}
void ThumbnailDatabase::InitFavIconsIndex() {
// Add an index on the url column. We ignore errors. Since this is always
// called during startup, the index will normally already exist.
sqlite3_exec(db_, "CREATE INDEX favicons_url ON favicons(url)",
NULL, NULL, NULL);
}
void ThumbnailDatabase::BeginTransaction() {
DCHECK(db_);
if (transaction_nesting_ == 0) {
int rv = sqlite3_exec(db_, "BEGIN TRANSACTION", NULL, NULL, NULL);
DCHECK(rv == SQLITE_OK) << "Failed to begin transaction";
}
transaction_nesting_++;
}
void ThumbnailDatabase::CommitTransaction() {
DCHECK(db_);
DCHECK(transaction_nesting_ > 0) << "Committing too many transaction";
transaction_nesting_--;
if (transaction_nesting_ == 0) {
int rv = sqlite3_exec(db_, "COMMIT", NULL, NULL, NULL);
DCHECK(rv == SQLITE_OK) << "Failed to commit transaction";
}
}
void ThumbnailDatabase::Vacuum() {
DCHECK(transaction_nesting_ == 0) <<
"Can not have a transaction when vacuuming.";
sqlite3_exec(db_, "VACUUM", NULL, NULL, NULL);
}
void ThumbnailDatabase::SetPageThumbnail(
const GURL& url,
URLID id,
const SkBitmap& thumbnail,
const ThumbnailScore& score,
const Time& time) {
if (!thumbnail.isNull()) {
bool add_thumbnail = true;
ThumbnailScore current_score;
if (ThumbnailScoreForId(id, &current_score)) {
add_thumbnail = ShouldReplaceThumbnailWith(current_score, score);
}
if (add_thumbnail) {
SQLITE_UNIQUE_STATEMENT(
statement, *statement_cache_,
"INSERT OR REPLACE INTO thumbnails "
"(url_id, boring_score, good_clipping, at_top, last_updated, data) "
"VALUES (?,?,?,?,?,?)");
if (!statement.is_valid())
return;
// We use 90 quality (out of 100) which is pretty high, because
// we're very sensitive to artifacts for these small sized,
// highly detailed images.
std::vector<unsigned char> jpeg_data;
SkAutoLockPixels thumbnail_lock(thumbnail);
bool encoded = JPEGCodec::Encode(
reinterpret_cast<unsigned char*>(thumbnail.getAddr32(0, 0)),
JPEGCodec::FORMAT_BGRA, thumbnail.width(),
thumbnail.height(),
static_cast<int>(thumbnail.rowBytes()), 90,
&jpeg_data);
if (encoded) {
statement->bind_int64(0, id);
statement->bind_double(1, score.boring_score);
statement->bind_bool(2, score.good_clipping);
statement->bind_bool(3, score.at_top);
statement->bind_int64(4, score.time_at_snapshot.ToTimeT());
statement->bind_blob(5, &jpeg_data[0],
static_cast<int>(jpeg_data.size()));
if (statement->step() != SQLITE_DONE)
DLOG(WARNING) << "Unable to insert thumbnail";
}
// Publish the thumbnail to any indexers listening to us.
// The tests may send an invalid url. Hence avoid publishing those.
if (url.is_valid() && history_publisher_ != NULL)
history_publisher_->PublishPageThumbnail(jpeg_data, url, time);
}
} else {
if ( !DeleteThumbnail(id) )
DLOG(WARNING) << "Unable to delete thumbnail";
}
}
bool ThumbnailDatabase::GetPageThumbnail(URLID id,
std::vector<unsigned char>* data) {
SQLITE_UNIQUE_STATEMENT(
statement, *statement_cache_,
"SELECT data FROM thumbnails WHERE url_id=?");
if (!statement.is_valid())
return false;
statement->bind_int64(0, id);
if (statement->step() != SQLITE_ROW)
return false; // don't have a thumbnail for this ID
return statement->column_blob_as_vector(0, data);
}
bool ThumbnailDatabase::DeleteThumbnail(URLID id) {
SQLITE_UNIQUE_STATEMENT(
statement, *statement_cache_,
"DELETE FROM thumbnails WHERE url_id = ?");
if (!statement.is_valid())
return false;
statement->bind_int64(0, id);
return statement->step() == SQLITE_DONE;
}
bool ThumbnailDatabase::ThumbnailScoreForId(
URLID id,
ThumbnailScore* score) {
// Fetch the current thumbnail's information to make sure we
// aren't replacing a good thumbnail with one that's worse.
SQLITE_UNIQUE_STATEMENT(
select_statement, *statement_cache_,
"SELECT boring_score, good_clipping, at_top, last_updated "
"FROM thumbnails WHERE url_id=?");
if (!select_statement.is_valid()) {
NOTREACHED() << "Couldn't build select statement!";
} else {
select_statement->bind_int64(0, id);
if (select_statement->step() == SQLITE_ROW) {
double current_boring_score = select_statement->column_double(0);
bool current_clipping = select_statement->column_bool(1);
bool current_at_top = select_statement->column_bool(2);
Time last_updated = Time::FromTimeT(select_statement->column_int64(3));
*score = ThumbnailScore(current_boring_score, current_clipping,
current_at_top, last_updated);
return true;
}
}
return false;
}
bool ThumbnailDatabase::SetFavIcon(URLID icon_id,
const std::vector<unsigned char>& icon_data,
Time time) {
DCHECK(icon_id);
if (icon_data.size()) {
SQLITE_UNIQUE_STATEMENT(
statement, *statement_cache_,
"UPDATE favicons SET image_data=?, last_updated=? WHERE id=?");
if (!statement.is_valid())
return 0;
statement->bind_blob(0, &icon_data.front(),
static_cast<int>(icon_data.size()));
statement->bind_int64(1, time.ToTimeT());
statement->bind_int64(2, icon_id);
return statement->step() == SQLITE_DONE;
} else {
SQLITE_UNIQUE_STATEMENT(
statement, *statement_cache_,
"UPDATE favicons SET image_data=NULL, last_updated=? WHERE id=?");
if (!statement.is_valid())
return 0;
statement->bind_int64(0, time.ToTimeT());
statement->bind_int64(1, icon_id);
return statement->step() == SQLITE_DONE;
}
}
bool ThumbnailDatabase::SetFavIconLastUpdateTime(FavIconID icon_id,
const Time& time) {
SQLITE_UNIQUE_STATEMENT(
statement, *statement_cache_,
"UPDATE favicons SET last_updated=? WHERE id=?");
if (!statement.is_valid())
return 0;
statement->bind_int64(0, time.ToTimeT());
statement->bind_int64(1, icon_id);
return statement->step() == SQLITE_DONE;
}
FavIconID ThumbnailDatabase::GetFavIconIDForFavIconURL(const GURL& icon_url) {
SQLITE_UNIQUE_STATEMENT(statement, *statement_cache_,
"SELECT id FROM favicons WHERE url=?");
if (!statement.is_valid())
return 0;
statement->bind_string(0, URLDatabase::GURLToDatabaseURL(icon_url));
if (statement->step() != SQLITE_ROW)
return 0; // not cached
return statement->column_int64(0);
}
bool ThumbnailDatabase::GetFavIcon(
FavIconID icon_id,
Time* last_updated,
std::vector<unsigned char>* png_icon_data,
GURL* icon_url) {
DCHECK(icon_id);
SQLITE_UNIQUE_STATEMENT(statement, *statement_cache_,
"SELECT last_updated, image_data, url FROM favicons WHERE id=?");
if (!statement.is_valid())
return 0;
statement->bind_int64(0, icon_id);
if (statement->step() != SQLITE_ROW) {
// No entry for the id.
return false;
}
*last_updated = Time::FromTimeT(statement->column_int64(0));
if (statement->column_bytes(1) > 0)
statement->column_blob_as_vector(1, png_icon_data);
if (icon_url)
*icon_url = GURL(statement->column_string(2));
return true;
}
FavIconID ThumbnailDatabase::AddFavIcon(const GURL& icon_url) {
SQLITE_UNIQUE_STATEMENT(statement, *statement_cache_,
"INSERT INTO favicons (url) VALUES (?)");
if (!statement.is_valid())
return 0;
statement->bind_string(0, URLDatabase::GURLToDatabaseURL(icon_url));
if (statement->step() != SQLITE_DONE)
return 0;
return sqlite3_last_insert_rowid(db_);
}
bool ThumbnailDatabase::DeleteFavIcon(FavIconID id) {
SQLITE_UNIQUE_STATEMENT(statement, *statement_cache_,
"DELETE FROM favicons WHERE id = ?");
if (!statement.is_valid())
return false;
statement->bind_int64(0, id);
return statement->step() == SQLITE_DONE;
}
FavIconID ThumbnailDatabase::CopyToTemporaryFavIconTable(FavIconID source) {
SQLITE_UNIQUE_STATEMENT(statement, *statement_cache_,
"INSERT INTO temp_favicons (url, last_updated, image_data)"
"SELECT url, last_updated, image_data "
"FROM favicons WHERE id = ?");
if (!statement.is_valid())
return 0;
statement->bind_int64(0, source);
if (statement->step() != SQLITE_DONE)
return 0;
// We return the ID of the newly inserted favicon.
return sqlite3_last_insert_rowid(db_);
}
bool ThumbnailDatabase::CommitTemporaryFavIconTable() {
// Delete the old favicons table.
if (sqlite3_exec(db_, "DROP TABLE favicons", NULL, NULL, NULL) != SQLITE_OK)
return false;
// Rename the temporary one.
if (sqlite3_exec(db_, "ALTER TABLE temp_favicons RENAME TO favicons",
NULL, NULL, NULL) != SQLITE_OK)
return false;
// The renamed table needs the index (the temporary table doesn't have one).
InitFavIconsIndex();
return true;
}
} // namespace history