blob: 69365abfe4c44fcd93a886eb0f8bfcc8eaba41a2 [file] [log] [blame]
// Copyright 2014 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/enhanced_bookmarks/persistent_image_store.h"
#include "base/files/file.h"
#include "base/logging.h"
#include "components/enhanced_bookmarks/image_store_util.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "ui/gfx/geometry/size.h"
#include "url/gurl.h"
namespace {
// Current version number. Databases are written at the "current" version
// number, but any previous version that can read the "compatible" one can make
// do with our database without *too* many bad effects.
const int kCurrentVersionNumber = 2;
const int kCompatibleVersionNumber = 1;
bool InitTables(sql::Connection& db) {
const char kTableSql[] =
"CREATE TABLE IF NOT EXISTS images_by_url ("
"page_url LONGVARCHAR NOT NULL,"
"image_url LONGVARCHAR NOT NULL,"
"image_data BLOB,"
"width INTEGER,"
"height INTEGER,"
"image_dominant_color INTEGER"
")";
if (!db.Execute(kTableSql))
return false;
return true;
}
bool InitIndices(sql::Connection& db) {
const char kIndexSql[] =
"CREATE INDEX IF NOT EXISTS images_by_url_idx ON images_by_url(page_url)";
if (!db.Execute(kIndexSql))
return false;
return true;
}
// V1 didn't store the dominant color of an image. Creates the column to store
// a dominant color in the database. The value will be filled when queried for a
// one time cost.
bool MigrateImagesWithNoDominantColor(sql::Connection& db) {
if (!db.DoesTableExist("images_by_url")) {
NOTREACHED() << "images_by_url table should exist before migration.";
return false;
}
if (!db.DoesColumnExist("images_by_url", "image_dominant_color")) {
// The initial version doesn't have the image_dominant_color column, it is
// added to the table here.
if (!db.Execute(
"ALTER TABLE images_by_url "
"ADD COLUMN image_dominant_color INTEGER")) {
return false;
}
}
return true;
}
sql::InitStatus EnsureCurrentVersion(sql::Connection& db,
sql::MetaTable& meta_table) {
// 1- Newer databases than designed for can't be read.
if (meta_table.GetCompatibleVersionNumber() > kCurrentVersionNumber) {
LOG(WARNING) << "Image DB is too new.";
return sql::INIT_TOO_NEW;
}
int cur_version = meta_table.GetVersionNumber();
// 2- Put migration code here.
if (cur_version == 1) {
if (!MigrateImagesWithNoDominantColor(db)) {
LOG(WARNING) << "Unable to update image DB to version 1.";
return sql::INIT_FAILURE;
}
++cur_version;
meta_table.SetVersionNumber(cur_version);
meta_table.SetCompatibleVersionNumber(
std::min(cur_version, kCompatibleVersionNumber));
}
// 3- When the version is too old, just try to continue anyway, there should
// not be a released product that makes a database too old to handle.
LOG_IF(WARNING, cur_version < kCurrentVersionNumber)
<< "Image DB version " << cur_version << " is too old to handle.";
return sql::INIT_OK;
}
sql::InitStatus OpenDatabaseImpl(sql::Connection& db,
sql::MetaTable& meta_table,
const base::FilePath& db_path) {
DCHECK(!db.is_open());
db.set_histogram_tag("BookmarkImages");
// TODO(noyau): Set page and cache sizes?
// TODO(noyau): Set error callback?
// Run the database in exclusive mode. Nobody else should be accessing the
// database while running, and this will give somewhat improved performance.
db.set_exclusive_locking();
if (!db.Open(db_path))
return sql::INIT_FAILURE;
// Scope initialization in a transaction so it can't be partially initialized.
sql::Transaction transaction(&db);
if (!transaction.Begin())
return sql::INIT_FAILURE;
// Initialize the meta table.
int cur_version = meta_table.DoesTableExist(&db)
? kCurrentVersionNumber
: 1; // Only v1 didn't have a meta table.
if (!meta_table.Init(&db, cur_version,
std::min(cur_version, kCompatibleVersionNumber)))
return sql::INIT_FAILURE;
// Create the tables.
if (!InitTables(db) || !InitIndices(db))
return sql::INIT_FAILURE;
// Check the version.
sql::InitStatus version_status = EnsureCurrentVersion(db, meta_table);
if (version_status != sql::INIT_OK)
return version_status;
// Initialization is complete.
if (!transaction.Commit())
return sql::INIT_FAILURE;
return sql::INIT_OK;
}
} // namespace
PersistentImageStore::PersistentImageStore(const base::FilePath& path)
: ImageStore(),
path_(path.Append(
base::FilePath::FromUTF8Unsafe("BookmarkImageAndUrlStore.db"))) {
}
bool PersistentImageStore::HasKey(const GURL& page_url) {
DCHECK(sequence_checker_.CalledOnValidSequencedThread());
if (OpenDatabase() != sql::INIT_OK)
return false;
sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE,
"SELECT COUNT(*) FROM images_by_url WHERE page_url = ?"));
statement.BindString(0, page_url.possibly_invalid_spec());
int count = statement.Step() ? statement.ColumnInt(0) : 0;
return !!count;
}
void PersistentImageStore::Insert(
const GURL& page_url,
scoped_refptr<enhanced_bookmarks::ImageRecord> record) {
DCHECK(sequence_checker_.CalledOnValidSequencedThread());
if (OpenDatabase() != sql::INIT_OK)
return;
Erase(page_url); // Remove previous image for this url, if any.
sql::Statement statement(db_.GetCachedStatement(
SQL_FROM_HERE,
"INSERT INTO images_by_url "
"(page_url, image_url, image_data, width, height, image_dominant_color)"
"VALUES (?, ?, ?, ?, ?, ?)"));
statement.BindString(0, page_url.possibly_invalid_spec());
statement.BindString(1, record->url.possibly_invalid_spec());
scoped_refptr<base::RefCountedMemory> image_bytes =
enhanced_bookmarks::BytesForImage(*record->image);
// Insert an empty image in case encoding fails.
if (!image_bytes.get())
image_bytes = enhanced_bookmarks::BytesForImage(gfx::Image());
CHECK(image_bytes.get());
statement.BindBlob(2, image_bytes->front(), (int)image_bytes->size());
statement.BindInt(3, record->image->Size().width());
statement.BindInt(4, record->image->Size().height());
statement.BindInt(5, record->dominant_color);
statement.Run();
}
void PersistentImageStore::Erase(const GURL& page_url) {
DCHECK(sequence_checker_.CalledOnValidSequencedThread());
if (OpenDatabase() != sql::INIT_OK)
return;
sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE,
"DELETE FROM images_by_url WHERE page_url = ?"));
statement.BindString(0, page_url.possibly_invalid_spec());
statement.Run();
}
scoped_refptr<enhanced_bookmarks::ImageRecord> PersistentImageStore::Get(
const GURL& page_url) {
DCHECK(sequence_checker_.CalledOnValidSequencedThread());
scoped_refptr<enhanced_bookmarks::ImageRecord> image_record(
new enhanced_bookmarks::ImageRecord());
if (OpenDatabase() != sql::INIT_OK)
return image_record;
bool stored_image_record_needs_update = false;
// Scope the SELECT statement.
{
sql::Statement statement(db_.GetCachedStatement(
SQL_FROM_HERE,
"SELECT image_data, image_url, image_dominant_color FROM images_by_url "
"WHERE page_url = ?"));
statement.BindString(0, page_url.possibly_invalid_spec());
if (!statement.Step())
return image_record;
// Image.
if (statement.ColumnByteLength(0) > 0) {
scoped_refptr<base::RefCountedBytes> data(new base::RefCountedBytes());
statement.ColumnBlobAsVector(0, &data->data());
*image_record->image = enhanced_bookmarks::ImageForBytes(data);
}
// URL.
image_record->url = GURL(statement.ColumnString(1));
// Dominant color.
if (statement.ColumnType(2) != sql::COLUMN_TYPE_NULL) {
image_record->dominant_color = SkColor(statement.ColumnInt(2));
} else {
// The dominant color was not computed when the image was first
// stored.
// Compute it now.
image_record->dominant_color =
enhanced_bookmarks::DominantColorForImage(*image_record->image);
stored_image_record_needs_update = true;
}
// Make sure there is only one record for page_url.
DCHECK(!statement.Step());
}
if (stored_image_record_needs_update) {
Erase(page_url);
Insert(page_url, image_record);
}
return image_record;
}
gfx::Size PersistentImageStore::GetSize(const GURL& page_url) {
DCHECK(sequence_checker_.CalledOnValidSequencedThread());
if (OpenDatabase() != sql::INIT_OK)
return gfx::Size();
sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE,
"SELECT width, height FROM images_by_url WHERE page_url = ?"));
statement.BindString(0, page_url.possibly_invalid_spec());
while (statement.Step()) {
if (statement.ColumnByteLength(0) > 0) {
int width = statement.ColumnInt(0);
int height = statement.ColumnInt(1);
return gfx::Size(width, height);
}
}
return gfx::Size();
}
void PersistentImageStore::GetAllPageUrls(std::set<GURL>* urls) {
DCHECK(sequence_checker_.CalledOnValidSequencedThread());
DCHECK(urls->empty());
if (OpenDatabase() != sql::INIT_OK)
return;
sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE,
"SELECT page_url FROM images_by_url"));
while (statement.Step())
urls->insert(GURL(statement.ColumnString(0)));
}
void PersistentImageStore::ClearAll() {
DCHECK(sequence_checker_.CalledOnValidSequencedThread());
if (OpenDatabase() != sql::INIT_OK)
return;
sql::Statement statement(db_.GetCachedStatement(
SQL_FROM_HERE, "DELETE FROM images_by_url"));
statement.Run();
}
int64 PersistentImageStore::GetStoreSizeInBytes() {
base::File file(path_, base::File::FLAG_OPEN | base::File::FLAG_READ);
return file.IsValid() ? file.GetLength() : -1;
}
PersistentImageStore::~PersistentImageStore() {
DCHECK(sequence_checker_.CalledOnValidSequencedThread());
}
sql::InitStatus PersistentImageStore::OpenDatabase() {
DCHECK(sequence_checker_.CalledOnValidSequencedThread());
if (db_.is_open())
return sql::INIT_OK;
const size_t kAttempts = 2;
sql::InitStatus status = sql::INIT_FAILURE;
for (size_t i = 0; i < kAttempts; ++i) {
status = OpenDatabaseImpl(db_, meta_table_, path_);
if (status == sql::INIT_OK)
return status;
// Can't open, raze().
if (db_.is_open())
db_.Raze();
db_.Close();
}
DCHECK(false) << "Can't open image DB";
return sql::INIT_FAILURE;
}