1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "components/enhanced_bookmarks/persistent_image_store.h"
6
7#include "base/files/file.h"
8#include "components/enhanced_bookmarks/image_store_util.h"
9#include "sql/statement.h"
10#include "sql/transaction.h"
11#include "ui/gfx/geometry/size.h"
12#include "url/gurl.h"
13
14namespace {
15
16bool InitTables(sql::Connection& db) {
17  const char kTableSql[] =
18      "CREATE TABLE IF NOT EXISTS images_by_url ("
19      "page_url LONGVARCHAR NOT NULL,"
20      "image_url LONGVARCHAR NOT NULL,"
21      "image_data BLOB,"
22      "width INTEGER,"
23      "height INTEGER"
24      ")";
25  if (!db.Execute(kTableSql))
26    return false;
27  return true;
28}
29
30bool InitIndices(sql::Connection& db) {
31  const char kIndexSql[] =
32      "CREATE INDEX IF NOT EXISTS images_by_url_idx ON images_by_url(page_url)";
33  if (!db.Execute(kIndexSql))
34    return false;
35  return true;
36}
37
38sql::InitStatus OpenDatabaseImpl(sql::Connection& db,
39                                 const base::FilePath& db_path) {
40  DCHECK(!db.is_open());
41
42  db.set_histogram_tag("BookmarkImages");
43  // TODO(noyau): Set page and cache sizes?
44  // TODO(noyau): Set error callback?
45
46  // Run the database in exclusive mode. Nobody else should be accessing the
47  // database while we're running, and this will give somewhat improved perf.
48  db.set_exclusive_locking();
49
50  if (!db.Open(db_path))
51    return sql::INIT_FAILURE;
52
53  // Scope initialization in a transaction so we can't be partially initialized.
54  sql::Transaction transaction(&db);
55  if (!transaction.Begin())
56    return sql::INIT_FAILURE;
57
58  // Create the tables.
59  if (!InitTables(db) ||
60      !InitIndices(db)) {
61    return sql::INIT_FAILURE;
62  }
63
64  // Initialization is complete.
65  if (!transaction.Commit())
66    return sql::INIT_FAILURE;
67
68  return sql::INIT_OK;
69}
70
71}  // namespace
72
73PersistentImageStore::PersistentImageStore(const base::FilePath& path)
74    : ImageStore(),
75      path_(path.Append(
76          base::FilePath::FromUTF8Unsafe("BookmarkImageAndUrlStore.db"))) {
77}
78
79bool PersistentImageStore::HasKey(const GURL& page_url) {
80  DCHECK(sequence_checker_.CalledOnValidSequencedThread());
81  if (OpenDatabase() != sql::INIT_OK)
82    return false;
83
84  sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE,
85      "SELECT COUNT(*) FROM images_by_url WHERE page_url = ?"));
86  statement.BindString(0, page_url.possibly_invalid_spec());
87
88  int count = statement.Step() ? statement.ColumnInt(0) : 0;
89
90  return !!count;
91}
92
93void PersistentImageStore::Insert(const GURL& page_url,
94                                  const GURL& image_url,
95                                  const gfx::Image& image) {
96  DCHECK(sequence_checker_.CalledOnValidSequencedThread());
97  if (OpenDatabase() != sql::INIT_OK)
98    return;
99
100  Erase(page_url);  // Remove previous image for this url, if any.
101  sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE,
102      "INSERT INTO images_by_url "
103      "(page_url, image_url, image_data, width, height)"
104      "VALUES (?, ?, ?, ?, ?)"));
105
106  statement.BindString(0, page_url.possibly_invalid_spec());
107  statement.BindString(1, image_url.possibly_invalid_spec());
108
109  scoped_refptr<base::RefCountedMemory> image_bytes =
110        enhanced_bookmarks::BytesForImage(image);
111
112  // Insert an empty image in case encoding fails.
113  if (!image_bytes.get())
114    image_bytes = enhanced_bookmarks::BytesForImage(gfx::Image());
115
116  CHECK(image_bytes.get());
117
118  statement.BindBlob(2, image_bytes->front(), (int)image_bytes->size());
119
120  statement.BindInt(3, image.Size().width());
121  statement.BindInt(4, image.Size().height());
122  statement.Run();
123}
124
125void PersistentImageStore::Erase(const GURL& page_url) {
126  DCHECK(sequence_checker_.CalledOnValidSequencedThread());
127  if (OpenDatabase() != sql::INIT_OK)
128    return;
129
130  sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE,
131      "DELETE FROM images_by_url WHERE page_url = ?"));
132  statement.BindString(0, page_url.possibly_invalid_spec());
133  statement.Run();
134}
135
136std::pair<gfx::Image, GURL> PersistentImageStore::Get(const GURL& page_url) {
137  DCHECK(sequence_checker_.CalledOnValidSequencedThread());
138  if (OpenDatabase() != sql::INIT_OK)
139    return std::make_pair(gfx::Image(), GURL());
140
141  sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE,
142      "SELECT image_data, image_url FROM images_by_url WHERE page_url = ?"));
143
144  statement.BindString(0, page_url.possibly_invalid_spec());
145
146  while (statement.Step()) {
147    if (statement.ColumnByteLength(0) > 0) {
148      scoped_refptr<base::RefCountedBytes> data(new base::RefCountedBytes());
149      statement.ColumnBlobAsVector(0, &data->data());
150
151      return std::make_pair(enhanced_bookmarks::ImageForBytes(data),
152                            GURL(statement.ColumnString(1)));
153    }
154  }
155
156  return std::make_pair(gfx::Image(), GURL());
157}
158
159gfx::Size PersistentImageStore::GetSize(const GURL& page_url) {
160  DCHECK(sequence_checker_.CalledOnValidSequencedThread());
161  if (OpenDatabase() != sql::INIT_OK)
162    return gfx::Size();
163
164  sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE,
165      "SELECT width, height FROM images_by_url WHERE page_url = ?"));
166
167  statement.BindString(0, page_url.possibly_invalid_spec());
168
169  while (statement.Step()) {
170    if (statement.ColumnByteLength(0) > 0) {
171      int width = statement.ColumnInt(0);
172      int height = statement.ColumnInt(1);
173      return gfx::Size(width, height);
174    }
175  }
176  return gfx::Size();
177}
178
179void PersistentImageStore::GetAllPageUrls(std::set<GURL>* urls) {
180  DCHECK(sequence_checker_.CalledOnValidSequencedThread());
181  DCHECK(urls->empty());
182  if (OpenDatabase() != sql::INIT_OK)
183    return;
184
185  sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE,
186      "SELECT page_url FROM images_by_url"));
187  while (statement.Step())
188    urls->insert(GURL(statement.ColumnString(0)));
189}
190
191void PersistentImageStore::ClearAll() {
192  DCHECK(sequence_checker_.CalledOnValidSequencedThread());
193  if (OpenDatabase() != sql::INIT_OK)
194    return;
195
196  sql::Statement statement(db_.GetCachedStatement(
197      SQL_FROM_HERE, "DELETE FROM images_by_url"));
198  statement.Run();
199}
200
201int64 PersistentImageStore::GetStoreSizeInBytes() {
202  base::File file(path_, base::File::FLAG_OPEN | base::File::FLAG_READ);
203  return file.IsValid() ? file.GetLength() : -1;
204}
205
206PersistentImageStore::~PersistentImageStore() {
207  DCHECK(sequence_checker_.CalledOnValidSequencedThread());
208}
209
210sql::InitStatus PersistentImageStore::OpenDatabase() {
211  DCHECK(sequence_checker_.CalledOnValidSequencedThread());
212
213  if (db_.is_open())
214    return sql::INIT_OK;
215
216  const size_t kAttempts = 2;
217
218  sql::InitStatus status = sql::INIT_FAILURE;
219  for (size_t i = 0; i < kAttempts; ++i) {
220    status = OpenDatabaseImpl(db_, path_);
221    if (status == sql::INIT_OK)
222      return status;
223
224    // Can't open, raze().
225    if (db_.is_open())
226      db_.Raze();
227    db_.Close();
228  }
229
230  DCHECK(false) << "Can't open image DB";
231  return sql::INIT_FAILURE;
232}
233