// 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/time.h" #include "base/string_util.h" #include "chrome/browser/history/url_database.h" #include "chrome/common/jpeg_codec.h" #include "chrome/common/sqlite_utils.h" #include "chrome/common/thumbnail_score.h" #include "skia/include/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) { } ThumbnailDatabase::~ThumbnailDatabase() { // The DBCloseScoper will delete the DB and the cache. } InitStatus ThumbnailDatabase::Init(const std::wstring& db_name) { // Open the thumbnail database, using the narrow version of open so that // the DB is in UTF-8. if (sqlite3_open(WideToUTF8(db_name).c_str(), &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 data; statement.column_blob_as_vector(1, &data); char filename[256]; sprintf(filename, "<<< YOUR PATH HERE >>>\\%d.jpeg", idx); if (!data.empty()) { file_util::WriteFile(ASCIIToWide(std::string(filename)), reinterpret_cast(data.data()), 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( URLID id, const SkBitmap& thumbnail, const ThumbnailScore& score) { if (!thumbnail.isNull()) { bool add_thumbnail = true; ThumbnailScore current_score; if (ThumbnailScoreForId(id, ¤t_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 jpeg_data; SkAutoLockPixels thumbnail_lock(thumbnail); bool encoded = JPEGCodec::Encode( reinterpret_cast(thumbnail.getAddr32(0, 0)), JPEGCodec::FORMAT_BGRA, thumbnail.width(), thumbnail.height(), static_cast(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(jpeg_data.size())); if (statement->step() != SQLITE_DONE) DLOG(WARNING) << "Unable to insert thumbnail"; } } } else { if ( !DeleteThumbnail(id) ) DLOG(WARNING) << "Unable to delete thumbnail"; } } bool ThumbnailDatabase::GetPageThumbnail(URLID id, std::vector* 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& 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(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* 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_text(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