IndexedDBStore.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. const prettyBytes = require('@uppy/utils/lib/prettyBytes')
  2. const indexedDB = typeof window !== 'undefined' &&
  3. (window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.OIndexedDB || window.msIndexedDB)
  4. const isSupported = !!indexedDB
  5. const DB_NAME = 'uppy-blobs'
  6. const STORE_NAME = 'files' // maybe have a thumbnail store in the future
  7. const DEFAULT_EXPIRY = 24 * 60 * 60 * 1000 // 24 hours
  8. const DB_VERSION = 3
  9. // Set default `expires` dates on existing stored blobs.
  10. function migrateExpiration (store) {
  11. const request = store.openCursor()
  12. request.onsuccess = (event) => {
  13. const cursor = event.target.result
  14. if (!cursor) {
  15. return
  16. }
  17. const entry = cursor.value
  18. entry.expires = Date.now() + DEFAULT_EXPIRY
  19. cursor.update(entry)
  20. }
  21. }
  22. function connect (dbName) {
  23. const request = indexedDB.open(dbName, DB_VERSION)
  24. return new Promise((resolve, reject) => {
  25. request.onupgradeneeded = (event) => {
  26. const db = event.target.result
  27. const transaction = event.currentTarget.transaction
  28. if (event.oldVersion < 2) {
  29. // Added in v2: DB structure changed to a single shared object store
  30. const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' })
  31. store.createIndex('store', 'store', { unique: false })
  32. }
  33. if (event.oldVersion < 3) {
  34. // Added in v3
  35. const store = transaction.objectStore(STORE_NAME)
  36. store.createIndex('expires', 'expires', { unique: false })
  37. migrateExpiration(store)
  38. }
  39. transaction.oncomplete = () => {
  40. resolve(db)
  41. }
  42. }
  43. request.onsuccess = (event) => {
  44. resolve(event.target.result)
  45. }
  46. request.onerror = reject
  47. })
  48. }
  49. function waitForRequest (request) {
  50. return new Promise((resolve, reject) => {
  51. request.onsuccess = (event) => {
  52. resolve(event.target.result)
  53. }
  54. request.onerror = reject
  55. })
  56. }
  57. let cleanedUp = false
  58. class IndexedDBStore {
  59. constructor (opts) {
  60. this.opts = Object.assign({
  61. dbName: DB_NAME,
  62. storeName: 'default',
  63. expires: DEFAULT_EXPIRY, // 24 hours
  64. maxFileSize: 10 * 1024 * 1024, // 10 MB
  65. maxTotalSize: 300 * 1024 * 1024 // 300 MB
  66. }, opts)
  67. this.name = this.opts.storeName
  68. const createConnection = () => {
  69. return connect(this.opts.dbName)
  70. }
  71. if (!cleanedUp) {
  72. cleanedUp = true
  73. this.ready = IndexedDBStore.cleanup()
  74. .then(createConnection, createConnection)
  75. } else {
  76. this.ready = createConnection()
  77. }
  78. }
  79. key (fileID) {
  80. return `${this.name}!${fileID}`
  81. }
  82. /**
  83. * List all file blobs currently in the store.
  84. */
  85. list () {
  86. return this.ready.then((db) => {
  87. const transaction = db.transaction([STORE_NAME], 'readonly')
  88. const store = transaction.objectStore(STORE_NAME)
  89. const request = store.index('store')
  90. .getAll(IDBKeyRange.only(this.name))
  91. return waitForRequest(request)
  92. }).then((files) => {
  93. const result = {}
  94. files.forEach((file) => {
  95. result[file.fileID] = file.data
  96. })
  97. return result
  98. })
  99. }
  100. /**
  101. * Get one file blob from the store.
  102. */
  103. get (fileID) {
  104. return this.ready.then((db) => {
  105. const transaction = db.transaction([STORE_NAME], 'readonly')
  106. const request = transaction.objectStore(STORE_NAME)
  107. .get(this.key(fileID))
  108. return waitForRequest(request)
  109. }).then((result) => ({
  110. id: result.data.fileID,
  111. data: result.data.data
  112. }))
  113. }
  114. /**
  115. * Get the total size of all stored files.
  116. *
  117. * @private
  118. */
  119. getSize () {
  120. return this.ready.then((db) => {
  121. const transaction = db.transaction([STORE_NAME], 'readonly')
  122. const store = transaction.objectStore(STORE_NAME)
  123. const request = store.index('store')
  124. .openCursor(IDBKeyRange.only(this.name))
  125. return new Promise((resolve, reject) => {
  126. let size = 0
  127. request.onsuccess = (event) => {
  128. const cursor = event.target.result
  129. if (cursor) {
  130. size += cursor.value.data.size
  131. cursor.continue()
  132. } else {
  133. resolve(size)
  134. }
  135. }
  136. request.onerror = () => {
  137. reject(new Error('Could not retrieve stored blobs size'))
  138. }
  139. })
  140. })
  141. }
  142. /**
  143. * Save a file in the store.
  144. */
  145. put (file) {
  146. if (file.data.size > this.opts.maxFileSize) {
  147. return Promise.reject(new Error('File is too big to store.'))
  148. }
  149. return this.getSize().then((size) => {
  150. if (size > this.opts.maxTotalSize) {
  151. return Promise.reject(new Error('No space left'))
  152. }
  153. return this.ready
  154. }).then((db) => {
  155. const transaction = db.transaction([STORE_NAME], 'readwrite')
  156. const request = transaction.objectStore(STORE_NAME).add({
  157. id: this.key(file.id),
  158. fileID: file.id,
  159. store: this.name,
  160. expires: Date.now() + this.opts.expires,
  161. data: file.data
  162. })
  163. return waitForRequest(request)
  164. })
  165. }
  166. /**
  167. * Delete a file blob from the store.
  168. */
  169. delete (fileID) {
  170. return this.ready.then((db) => {
  171. const transaction = db.transaction([STORE_NAME], 'readwrite')
  172. const request = transaction.objectStore(STORE_NAME)
  173. .delete(this.key(fileID))
  174. return waitForRequest(request)
  175. })
  176. }
  177. /**
  178. * Delete all stored blobs that have an expiry date that is before Date.now().
  179. * This is a static method because it deletes expired blobs from _all_ Uppy instances.
  180. */
  181. static cleanup () {
  182. return connect(DB_NAME).then((db) => {
  183. const transaction = db.transaction([STORE_NAME], 'readwrite')
  184. const store = transaction.objectStore(STORE_NAME)
  185. const request = store.index('expires')
  186. .openCursor(IDBKeyRange.upperBound(Date.now()))
  187. return new Promise((resolve, reject) => {
  188. request.onsuccess = (event) => {
  189. const cursor = event.target.result
  190. if (cursor) {
  191. const entry = cursor.value
  192. console.log(
  193. '[IndexedDBStore] Deleting record', entry.fileID,
  194. 'of size', prettyBytes(entry.data.size),
  195. '- expired on', new Date(entry.expires))
  196. cursor.delete() // Ignoring return value … it's not terrible if this goes wrong.
  197. cursor.continue()
  198. } else {
  199. resolve(db)
  200. }
  201. }
  202. request.onerror = reject
  203. })
  204. }).then((db) => {
  205. db.close()
  206. })
  207. }
  208. }
  209. IndexedDBStore.isSupported = isSupported
  210. module.exports = IndexedDBStore