UNPKG

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