Snap pinch zoom gestures near the screen edge.
[chromium-blink-merge.git] / ui / file_manager / image_loader / cache.js
blob7b8a3f23d157026cc53467fded9318d46eb21186
1 // Copyright 2013 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.
5 /**
6  * Persistent cache storing images in an indexed database on the hard disk.
7  * @constructor
8  */
9 function ImageCache() {
10   /**
11    * IndexedDB database handle.
12    * @type {IDBDatabase}
13    * @private
14    */
15   this.db_ = null;
18 /**
19  * Cache database name.
20  * @type {string}
21  * @const
22  */
23 ImageCache.DB_NAME = 'image-loader';
25 /**
26  * Cache database version.
27  * @type {number}
28  * @const
29  */
30 ImageCache.DB_VERSION = 12;
32 /**
33  * Memory limit for images data in bytes.
34  *
35  * @const
36  * @type {number}
37  */
38 ImageCache.MEMORY_LIMIT = 250 * 1024 * 1024;  // 250 MB.
40 /**
41  * Minimal amount of memory freed per eviction. Used to limit number of
42  * evictions which are expensive.
43  *
44  * @const
45  * @type {number}
46  */
47 ImageCache.EVICTION_CHUNK_SIZE = 50 * 1024 * 1024;  // 50 MB.
49 /**
50  * Creates a cache key.
51  *
52  * @param {Object} request Request options.
53  * @return {?string} Cache key. It may be null if the cache does not support
54  *     |request|. e.g. Data URI.
55  */
56 ImageCache.createKey = function(request) {
57   if (/^data:/i.test(request.url))
58     return null;
59   return JSON.stringify({
60     url: request.url,
61     scale: request.scale,
62     width: request.width,
63     height: request.height,
64     maxWidth: request.maxWidth,
65     maxHeight: request.maxHeight});
68 /**
69  * Initializes the cache database.
70  * @param {function()} callback Completion callback.
71  */
72 ImageCache.prototype.initialize = function(callback) {
73   // Establish a connection to the database or (re)create it if not available
74   // or not up to date. After changing the database's schema, increment
75   // ImageCache.DB_VERSION to force database recreating.
76   var openRequest = window.indexedDB.open(
77       ImageCache.DB_NAME, ImageCache.DB_VERSION);
79   openRequest.onsuccess = function(e) {
80     this.db_ = e.target.result;
81     callback();
82   }.bind(this);
84   openRequest.onerror = callback;
86   openRequest.onupgradeneeded = function(e) {
87     console.info('Cache database creating or upgrading.');
88     var db = e.target.result;
89     if (db.objectStoreNames.contains('metadata'))
90       db.deleteObjectStore('metadata');
91     if (db.objectStoreNames.contains('data'))
92       db.deleteObjectStore('data');
93     if (db.objectStoreNames.contains('settings'))
94       db.deleteObjectStore('settings');
95     db.createObjectStore('metadata', {keyPath: 'key'});
96     db.createObjectStore('data', {keyPath: 'key'});
97     db.createObjectStore('settings', {keyPath: 'key'});
98   };
102  * Sets size of the cache.
104  * @param {number} size Size in bytes.
105  * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
106  *     provided, then a new one is created.
107  * @private
108  */
109 ImageCache.prototype.setCacheSize_ = function(size, opt_transaction) {
110   var transaction = opt_transaction ||
111       this.db_.transaction(['settings'], 'readwrite');
112   var settingsStore = transaction.objectStore('settings');
114   settingsStore.put({key: 'size', value: size});  // Update asynchronously.
118  * Fetches current size of the cache.
120  * @param {function(number)} onSuccess Callback to return the size.
121  * @param {function()} onFailure Failure callback.
122  * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
123  *     provided, then a new one is created.
124  * @private
125  */
126 ImageCache.prototype.fetchCacheSize_ = function(
127     onSuccess, onFailure, opt_transaction) {
128   var transaction = opt_transaction ||
129       this.db_.transaction(['settings', 'metadata', 'data'], 'readwrite');
130   var settingsStore = transaction.objectStore('settings');
131   var sizeRequest = settingsStore.get('size');
133   sizeRequest.onsuccess = function(e) {
134     if (e.target.result)
135       onSuccess(e.target.result.value);
136     else
137       onSuccess(0);
138   };
140   sizeRequest.onerror = function() {
141     console.error('Failed to fetch size from the database.');
142     onFailure();
143   };
147  * Evicts the least used elements in cache to make space for a new image and
148  * updates size of the cache taking into account the upcoming item.
150  * @param {number} size Requested size.
151  * @param {function()} onSuccess Success callback.
152  * @param {function()} onFailure Failure callback.
153  * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
154  *     provided, then a new one is created.
155  * @private
156  */
157 ImageCache.prototype.evictCache_ = function(
158     size, onSuccess, onFailure, opt_transaction) {
159   var transaction = opt_transaction ||
160       this.db_.transaction(['settings', 'metadata', 'data'], 'readwrite');
162   // Check if the requested size is smaller than the cache size.
163   if (size > ImageCache.MEMORY_LIMIT) {
164     onFailure();
165     return;
166   }
168   var onCacheSize = function(cacheSize) {
169     if (size < ImageCache.MEMORY_LIMIT - cacheSize) {
170       // Enough space, no need to evict.
171       this.setCacheSize_(cacheSize + size, transaction);
172       onSuccess();
173       return;
174     }
176     var bytesToEvict = Math.max(size, ImageCache.EVICTION_CHUNK_SIZE);
178     // Fetch all metadata.
179     var metadataEntries = [];
180     var metadataStore = transaction.objectStore('metadata');
181     var dataStore = transaction.objectStore('data');
183     var onEntriesFetched = function() {
184       metadataEntries.sort(function(a, b) {
185         return b.lastLoadTimestamp - a.lastLoadTimestamp;
186       });
188       var totalEvicted = 0;
189       while (bytesToEvict > 0) {
190         var entry = metadataEntries.pop();
191         totalEvicted += entry.size;
192         bytesToEvict -= entry.size;
193         metadataStore.delete(entry.key);  // Remove asynchronously.
194         dataStore.delete(entry.key);  // Remove asynchronously.
195       }
197       this.setCacheSize_(cacheSize - totalEvicted + size, transaction);
198     }.bind(this);
200     metadataStore.openCursor().onsuccess = function(e) {
201       var cursor = e.target.result;
202       if (cursor) {
203         metadataEntries.push(cursor.value);
204         cursor.continue();
205       } else {
206         onEntriesFetched();
207       }
208     };
209   }.bind(this);
211   this.fetchCacheSize_(onCacheSize, onFailure, transaction);
215  * Saves an image in the cache.
217  * @param {string} key Cache key.
218  * @param {string} data Image data.
219  * @param {number} width Image width.
220  * @param {number} height Image height.
221  * @param {number} timestamp Last modification timestamp. Used to detect
222  *     if the cache entry becomes out of date.
223  */
224 ImageCache.prototype.saveImage = function(key, data, width, height, timestamp) {
225   if (!this.db_) {
226     console.warn('Cache database not available.');
227     return;
228   }
230   var onNotFoundInCache = function() {
231     var metadataEntry = {
232       key: key,
233       timestamp: timestamp,
234       width: width,
235       height: height,
236       size: data.length,
237       lastLoadTimestamp: Date.now()};
238     var dataEntry = {key: key, data: data};
240     var transaction = this.db_.transaction(['settings', 'metadata', 'data'],
241                                            'readwrite');
242     var metadataStore = transaction.objectStore('metadata');
243     var dataStore = transaction.objectStore('data');
245     var onCacheEvicted = function() {
246       metadataStore.put(metadataEntry);  // Add asynchronously.
247       dataStore.put(dataEntry);  // Add asynchronously.
248     };
250     // Make sure there is enough space in the cache.
251     this.evictCache_(data.length, onCacheEvicted, function() {}, transaction);
252   }.bind(this);
254   // Check if the image is already in cache. If not, then save it to cache.
255   this.loadImage(key, timestamp, function() {}, onNotFoundInCache);
259  * Loads an image from the cache (if available) or returns null.
261  * @param {string} key Cache key.
262  * @param {number} timestamp Last modification timestamp. If different
263  *     that the one in cache, then the entry will be invalidated.
264  * @param {function(string, number, number)} onSuccess Success callback with
265  *     the image's data, width, height.
266  * @param {function()} onFailure Failure callback.
267  */
268 ImageCache.prototype.loadImage = function(
269     key, timestamp, onSuccess, onFailure) {
270   if (!this.db_) {
271     console.warn('Cache database not available.');
272     onFailure();
273     return;
274   }
276   var transaction = this.db_.transaction(['settings', 'metadata', 'data'],
277                                          'readwrite');
278   var metadataStore = transaction.objectStore('metadata');
279   var dataStore = transaction.objectStore('data');
280   var metadataRequest = metadataStore.get(key);
281   var dataRequest = dataStore.get(key);
283   var metadataEntry = null;
284   var metadataReceived = false;
285   var dataEntry = null;
286   var dataReceived = false;
288   var onPartialSuccess = function() {
289     // Check if all sub-requests have finished.
290     if (!metadataReceived || !dataReceived)
291       return;
293     // Check if both entries are available or both unavailable.
294     if (!!metadataEntry != !!dataEntry) {
295       console.warn('Inconsistent cache database.');
296       onFailure();
297       return;
298     }
300     // Process the responses.
301     if (!metadataEntry) {
302       // The image not found.
303       onFailure();
304     } else if (metadataEntry.timestamp != timestamp) {
305       // The image is not up to date, so remove it.
306       this.removeImage(key, function() {}, function() {}, transaction);
307       onFailure();
308     } else {
309       // The image is available. Update the last load time and return the
310       // image data.
311       metadataEntry.lastLoadTimestamp = Date.now();
312       metadataStore.put(metadataEntry);  // Added asynchronously.
313       onSuccess(dataEntry.data, metadataEntry.width, metadataEntry.height);
314     }
315   }.bind(this);
317   metadataRequest.onsuccess = function(e) {
318     if (e.target.result)
319       metadataEntry = e.target.result;
320     metadataReceived = true;
321     onPartialSuccess();
322   };
324   dataRequest.onsuccess = function(e) {
325     if (e.target.result)
326       dataEntry = e.target.result;
327     dataReceived = true;
328     onPartialSuccess();
329   };
331   metadataRequest.onerror = function() {
332     console.error('Failed to fetch metadata from the database.');
333     metadataReceived = true;
334     onPartialSuccess();
335   };
337   dataRequest.onerror = function() {
338     console.error('Failed to fetch image data from the database.');
339     dataReceived = true;
340     onPartialSuccess();
341   };
345  * Removes the image from the cache.
347  * @param {string} key Cache key.
348  * @param {function()=} opt_onSuccess Success callback.
349  * @param {function()=} opt_onFailure Failure callback.
350  * @param {IDBTransaction=} opt_transaction Transaction to be reused. If not
351  *     provided, then a new one is created.
352  */
353 ImageCache.prototype.removeImage = function(
354     key, opt_onSuccess, opt_onFailure, opt_transaction) {
355   if (!this.db_) {
356     console.warn('Cache database not available.');
357     return;
358   }
360   var transaction = opt_transaction ||
361       this.db_.transaction(['settings', 'metadata', 'data'], 'readwrite');
362   var metadataStore = transaction.objectStore('metadata');
363   var dataStore = transaction.objectStore('data');
365   var cacheSize = null;
366   var cacheSizeReceived = false;
367   var metadataEntry = null;
368   var metadataReceived = false;
370   var onPartialSuccess = function() {
371     if (!cacheSizeReceived || !metadataReceived)
372       return;
374     // If either cache size or metadata entry is not available, then it is
375     // an error.
376     if (cacheSize === null || !metadataEntry) {
377       if (opt_onFailure)
378         opt_onFailure();
379       return;
380     }
382     if (opt_onSuccess)
383       opt_onSuccess();
385     this.setCacheSize_(cacheSize - metadataEntry.size, transaction);
386     metadataStore.delete(key);  // Delete asynchronously.
387     dataStore.delete(key);  // Delete asynchronously.
388   }.bind(this);
390   var onCacheSizeFailure = function() {
391     cacheSizeReceived = true;
392   };
394   var onCacheSizeSuccess = function(result) {
395     cacheSize = result;
396     cacheSizeReceived = true;
397     onPartialSuccess();
398   };
400   // Fetch the current cache size.
401   this.fetchCacheSize_(onCacheSizeSuccess, onCacheSizeFailure, transaction);
403   // Receive image's metadata.
404   var metadataRequest = metadataStore.get(key);
406   metadataRequest.onsuccess = function(e) {
407     if (e.target.result)
408       metadataEntry = e.target.result;
409     metadataReceived = true;
410     onPartialSuccess();
411   };
413   metadataRequest.onerror = function() {
414     console.error('Failed to remove an image.');
415     metadataReceived = true;
416     onPartialSuccess();
417   };