Source: lib/offline/indexeddb/storage_mechanism.js

/**
 * @license
 * Copyright 2016 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

goog.provide('shaka.offline.indexeddb.StorageMechanism');

goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.offline.StorageMuxer');
goog.require('shaka.offline.indexeddb.EmeSessionStorageCell');
goog.require('shaka.offline.indexeddb.V1StorageCell');
goog.require('shaka.offline.indexeddb.V2StorageCell');
goog.require('shaka.util.Error');
goog.require('shaka.util.PublicPromise');


/**
 * A storage mechanism to manage storage cells for an indexed db instance.
 * The cells are just for interacting with the stores that are found in the
 * database instance. The mechanism is responsible for creating new stores
 * when opening the database. If the database is too old of a version, a
 * cell will be added for the old stores but the cell won't support add
 * operations. The mechanism will create the new versions of the stores and
 * will allow add operations for those stores.
 *
 * @implements {shaka.extern.StorageMechanism}
 */
shaka.offline.indexeddb.StorageMechanism = class {
  constructor() {
    /** @private {IDBDatabase} */
    this.db_ = null;

    /** @private {shaka.extern.StorageCell} */
    this.v1_ = null;
    /** @private {shaka.extern.StorageCell} */
    this.v2_ = null;
    /** @private {shaka.extern.StorageCell} */
    this.v3_ = null;
    /** @private {shaka.extern.EmeSessionStorageCell} */
    this.sessions_ = null;
  }

  /**
   * @override
   */
  init() {
    const name = shaka.offline.indexeddb.StorageMechanism.DB_NAME;
    const version = shaka.offline.indexeddb.StorageMechanism.VERSION;

    let p = new shaka.util.PublicPromise();
    let open = window.indexedDB.open(name, version);
    open.onsuccess = (event) => {
      let db = event.target.result;
      this.db_ = db;
      this.v1_ = shaka.offline.indexeddb.StorageMechanism.createV1_(db);
      this.v2_ = shaka.offline.indexeddb.StorageMechanism.createV2_(db);
      this.v3_ = shaka.offline.indexeddb.StorageMechanism.createV3_(db);
      this.sessions_ =
          shaka.offline.indexeddb.StorageMechanism.createEmeSession_(db);
      p.resolve();
    };
    open.onupgradeneeded = (event) => {
      // Add object stores for the latest version only.
      this.createStores_(event.target.result);
    };
    open.onerror = (event) => {
      p.reject(new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.STORAGE,
          shaka.util.Error.Code.INDEXED_DB_ERROR,
          open.error));

      // Firefox will raise an error on the main thread unless we stop it here.
      event.preventDefault();
    };

    return p;
  }

  /**
   * @override
   */
  async destroy() {
    if (this.v1_) { await this.v1_.destroy(); }
    if (this.v2_) { await this.v2_.destroy(); }
    if (this.v3_) { await this.v3_.destroy(); }
    if (this.sessions_) { await this.sessions_.destroy(); }

    // If we were never initialized, then |db_| will still be null.
    if (this.db_) { this.db_.close(); }
  }

  /**
   * @override
   */
  getCells() {
    const map = new Map();

    if (this.v1_) { map.set('v1', this.v1_); }
    if (this.v2_) { map.set('v2', this.v2_); }
    if (this.v3_) { map.set('v3', this.v3_); }

    return map;
  }

  /**
   * @override
   */
  getEmeSessionCell() {
    goog.asserts.assert(this.sessions_, 'Cannot be destroyed.');
    return this.sessions_;
  }

  /**
   * @override
   */
  async erase() {
    // Not all cells may have been created, so only destroy the ones that
    // were created.
    if (this.v1_) { await this.v1_.destroy(); }
    if (this.v2_) { await this.v2_.destroy(); }
    if (this.v3_) { await this.v3_.destroy(); }

    // |db_| will only be null if the muxer was not initialized. We need to
    // close the connection in order delete the database without it being
    // blocked.
    if (this.db_) { this.db_.close(); }

    await shaka.offline.indexeddb.StorageMechanism.deleteAll_();

    // Reset before initializing.
    this.db_ = null;
    this.v1_ = null;
    this.v2_ = null;
    this.v3_ = null;

    await this.init();
  }

  /**
   * @param {!IDBDatabase} db
   * @return {shaka.extern.StorageCell}
   * @private
   */
  static createV1_(db) {
    const StorageMechanism = shaka.offline.indexeddb.StorageMechanism;
    const segmentStore = StorageMechanism.V1_SEGMENT_STORE;
    const manifestStore = StorageMechanism.V1_MANIFEST_STORE;
    const stores = db.objectStoreNames;
    if (stores.contains(manifestStore) && stores.contains(segmentStore)) {
      shaka.log.debug('Mounting v1 idb storage cell');

      return new shaka.offline.indexeddb.V1StorageCell(
          db,
          segmentStore,
          manifestStore);
    }
    return null;
  }

  /**
   * @param {!IDBDatabase} db
   * @return {shaka.extern.StorageCell}
   * @private
   */
  static createV2_(db) {
    const StorageMechanism = shaka.offline.indexeddb.StorageMechanism;
    const segmentStore = StorageMechanism.V2_SEGMENT_STORE;
    const manifestStore = StorageMechanism.V2_MANIFEST_STORE;
    const stores = db.objectStoreNames;
    if (stores.contains(manifestStore) && stores.contains(segmentStore)) {
      shaka.log.debug('Mounting v2 idb storage cell');

      return new shaka.offline.indexeddb.V2StorageCell(
          db,
          segmentStore,
          manifestStore,
          true);  // Are keys locked? Yes, this means no new additions.
    }
    return null;
  }

  /**
   * @param {!IDBDatabase} db
   * @return {shaka.extern.StorageCell}
   * @private
   */
  static createV3_(db) {
    const StorageMechanism = shaka.offline.indexeddb.StorageMechanism;
    const segmentStore = StorageMechanism.V3_SEGMENT_STORE;
    const manifestStore = StorageMechanism.V3_MANIFEST_STORE;
    const stores = db.objectStoreNames;
    if (stores.contains(manifestStore) && stores.contains(segmentStore)) {
      shaka.log.debug('Mounting v3 idb storage cell');

      // Version 3 uses the same structure as version 2, so we can use the same
      // cells but it can support new entries.
      return new shaka.offline.indexeddb.V2StorageCell(
          db,
          segmentStore,
          manifestStore,
          false); // Are keys locked? No, this means we can add new entries.
    }
    return null;
  }

  /**
   * @param {!IDBDatabase} db
   * @return {shaka.extern.EmeSessionStorageCell}
   * @private
   */
  static createEmeSession_(db) {
    const StorageMechanism = shaka.offline.indexeddb.StorageMechanism;
    const store = StorageMechanism.SESSION_ID_STORE;
    if (db.objectStoreNames.contains(store)) {
      shaka.log.debug('Mounting session ID idb storage cell');
      return new shaka.offline.indexeddb.EmeSessionStorageCell(db, store);
    }
    return null;
  }

  /**
   * @param {!IDBDatabase} db
   * @private
   */
  createStores_(db) {
    const storeNames = [
      shaka.offline.indexeddb.StorageMechanism.V3_SEGMENT_STORE,
      shaka.offline.indexeddb.StorageMechanism.V3_MANIFEST_STORE,
      shaka.offline.indexeddb.StorageMechanism.SESSION_ID_STORE,
    ];

    for (const name of storeNames) {
      if (!db.objectStoreNames.contains(name)) {
        db.createObjectStore(name, {autoIncrement: true});
      }
    }
  }

  /**
   * Delete the indexed db instance so that all stores are deleted and cleared.
   * This will force the database to a like-new state next time it opens.
   *
   * @return {!Promise}
   * @private
   */
  static deleteAll_() {
    const name = shaka.offline.indexeddb.StorageMechanism.DB_NAME;

    let p = new shaka.util.PublicPromise();

    let del = window.indexedDB.deleteDatabase(name);
    del.onblocked = (event) => {
      shaka.log.warning('Deleting', name, 'is being blocked');
    };
    del.onsuccess = (event) => {
      p.resolve();
    };
    del.onerror = (event) => {
      p.reject(new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.STORAGE,
          shaka.util.Error.Code.INDEXED_DB_ERROR,
          del.error));

      // Firefox will raise an error on the main thread unless we stop it here.
      event.preventDefault();
    };

    return p;
  }
};

/** @const {string} */
shaka.offline.indexeddb.StorageMechanism.DB_NAME = 'shaka_offline_db';
/** @const {number} */
shaka.offline.indexeddb.StorageMechanism.VERSION = 4;
/** @const {string} */
shaka.offline.indexeddb.StorageMechanism.V1_SEGMENT_STORE = 'segment';
/** @const {string} */
shaka.offline.indexeddb.StorageMechanism.V2_SEGMENT_STORE = 'segment-v2';
/** @const {string} */
shaka.offline.indexeddb.StorageMechanism.V3_SEGMENT_STORE = 'segment-v3';
/** @const {string} */
shaka.offline.indexeddb.StorageMechanism.V1_MANIFEST_STORE = 'manifest';
/** @const {string} */
shaka.offline.indexeddb.StorageMechanism.V2_MANIFEST_STORE = 'manifest-v2';
/** @const {string} */
shaka.offline.indexeddb.StorageMechanism.V3_MANIFEST_STORE = 'manifest-v3';
/** @const {string} */
shaka.offline.indexeddb.StorageMechanism.SESSION_ID_STORE = 'session-ids';


// Since this may be called before the polyfills remove indexeddb support from
// some platforms (looking at you Chromecast), we need to check for support
// when we create the mechanism.
//
// Thankfully the storage muxer api allows us to return a null mechanism
// to indicate that the mechanism is not supported on this platform.
shaka.offline.StorageMuxer.register(
    'idb',
    () => {
      // Offline storage is not supported on the Chromecast platform.
      if (shaka.util.Platform.isChromecast()) {
        return null;
      }
      // Offline storage requires the IndexedDB API.
      if (!window.indexedDB) {
        return null;
      }
      return new shaka.offline.indexeddb.StorageMechanism();
    });