123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 |
- const prettyBytes = require('@uppy/utils/lib/prettyBytes')
- const indexedDB = typeof window !== 'undefined' &&
- (window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.OIndexedDB || window.msIndexedDB)
- const isSupported = !!indexedDB
- const DB_NAME = 'uppy-blobs'
- const STORE_NAME = 'files' // maybe have a thumbnail store in the future
- const DEFAULT_EXPIRY = 24 * 60 * 60 * 1000 // 24 hours
- const DB_VERSION = 3
- // Set default `expires` dates on existing stored blobs.
- function migrateExpiration (store) {
- const request = store.openCursor()
- request.onsuccess = (event) => {
- const cursor = event.target.result
- if (!cursor) {
- return
- }
- const entry = cursor.value
- entry.expires = Date.now() + DEFAULT_EXPIRY
- cursor.update(entry)
- }
- }
- function connect (dbName) {
- const request = indexedDB.open(dbName, DB_VERSION)
- return new Promise((resolve, reject) => {
- request.onupgradeneeded = (event) => {
- const db = event.target.result
- const transaction = event.currentTarget.transaction
- if (event.oldVersion < 2) {
- // Added in v2: DB structure changed to a single shared object store
- const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' })
- store.createIndex('store', 'store', { unique: false })
- }
- if (event.oldVersion < 3) {
- // Added in v3
- const store = transaction.objectStore(STORE_NAME)
- store.createIndex('expires', 'expires', { unique: false })
- migrateExpiration(store)
- }
- transaction.oncomplete = () => {
- resolve(db)
- }
- }
- request.onsuccess = (event) => {
- resolve(event.target.result)
- }
- request.onerror = reject
- })
- }
- function waitForRequest (request) {
- return new Promise((resolve, reject) => {
- request.onsuccess = (event) => {
- resolve(event.target.result)
- }
- request.onerror = reject
- })
- }
- let cleanedUp = false
- class IndexedDBStore {
- constructor (opts) {
- this.opts = Object.assign({
- dbName: DB_NAME,
- storeName: 'default',
- expires: DEFAULT_EXPIRY, // 24 hours
- maxFileSize: 10 * 1024 * 1024, // 10 MB
- maxTotalSize: 300 * 1024 * 1024 // 300 MB
- }, opts)
- this.name = this.opts.storeName
- const createConnection = () => {
- return connect(this.opts.dbName)
- }
- if (!cleanedUp) {
- cleanedUp = true
- this.ready = IndexedDBStore.cleanup()
- .then(createConnection, createConnection)
- } else {
- this.ready = createConnection()
- }
- }
- key (fileID) {
- return `${this.name}!${fileID}`
- }
- /**
- * List all file blobs currently in the store.
- */
- list () {
- return this.ready.then((db) => {
- const transaction = db.transaction([STORE_NAME], 'readonly')
- const store = transaction.objectStore(STORE_NAME)
- const request = store.index('store')
- .getAll(IDBKeyRange.only(this.name))
- return waitForRequest(request)
- }).then((files) => {
- const result = {}
- files.forEach((file) => {
- result[file.fileID] = file.data
- })
- return result
- })
- }
- /**
- * Get one file blob from the store.
- */
- get (fileID) {
- return this.ready.then((db) => {
- const transaction = db.transaction([STORE_NAME], 'readonly')
- const request = transaction.objectStore(STORE_NAME)
- .get(this.key(fileID))
- return waitForRequest(request)
- }).then((result) => ({
- id: result.data.fileID,
- data: result.data.data
- }))
- }
- /**
- * Get the total size of all stored files.
- *
- * @private
- */
- getSize () {
- return this.ready.then((db) => {
- const transaction = db.transaction([STORE_NAME], 'readonly')
- const store = transaction.objectStore(STORE_NAME)
- const request = store.index('store')
- .openCursor(IDBKeyRange.only(this.name))
- return new Promise((resolve, reject) => {
- let size = 0
- request.onsuccess = (event) => {
- const cursor = event.target.result
- if (cursor) {
- size += cursor.value.data.size
- cursor.continue()
- } else {
- resolve(size)
- }
- }
- request.onerror = () => {
- reject(new Error('Could not retrieve stored blobs size'))
- }
- })
- })
- }
- /**
- * Save a file in the store.
- */
- put (file) {
- if (file.data.size > this.opts.maxFileSize) {
- return Promise.reject(new Error('File is too big to store.'))
- }
- return this.getSize().then((size) => {
- if (size > this.opts.maxTotalSize) {
- return Promise.reject(new Error('No space left'))
- }
- return this.ready
- }).then((db) => {
- const transaction = db.transaction([STORE_NAME], 'readwrite')
- const request = transaction.objectStore(STORE_NAME).add({
- id: this.key(file.id),
- fileID: file.id,
- store: this.name,
- expires: Date.now() + this.opts.expires,
- data: file.data
- })
- return waitForRequest(request)
- })
- }
- /**
- * Delete a file blob from the store.
- */
- delete (fileID) {
- return this.ready.then((db) => {
- const transaction = db.transaction([STORE_NAME], 'readwrite')
- const request = transaction.objectStore(STORE_NAME)
- .delete(this.key(fileID))
- return waitForRequest(request)
- })
- }
- /**
- * Delete all stored blobs that have an expiry date that is before Date.now().
- * This is a static method because it deletes expired blobs from _all_ Uppy instances.
- */
- static cleanup () {
- return connect(DB_NAME).then((db) => {
- const transaction = db.transaction([STORE_NAME], 'readwrite')
- const store = transaction.objectStore(STORE_NAME)
- const request = store.index('expires')
- .openCursor(IDBKeyRange.upperBound(Date.now()))
- return new Promise((resolve, reject) => {
- request.onsuccess = (event) => {
- const cursor = event.target.result
- if (cursor) {
- const entry = cursor.value
- console.log(
- '[IndexedDBStore] Deleting record', entry.fileID,
- 'of size', prettyBytes(entry.data.size),
- '- expired on', new Date(entry.expires))
- cursor.delete() // Ignoring return value … it's not terrible if this goes wrong.
- cursor.continue()
- } else {
- resolve(db)
- }
- }
- request.onerror = reject
- })
- }).then((db) => {
- db.close()
- })
- }
- }
- IndexedDBStore.isSupported = isSupported
- module.exports = IndexedDBStore
|