1 | const prettierBytes = require('@transloadit/prettier-bytes')
|
2 | const indexedDB = typeof window !== 'undefined' &&
|
3 | (window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.OIndexedDB || window.msIndexedDB)
|
4 |
|
5 | const isSupported = !!indexedDB
|
6 |
|
7 | const DB_NAME = 'uppy-blobs'
|
8 | const STORE_NAME = 'files'
|
9 | const DEFAULT_EXPIRY = 24 * 60 * 60 * 1000
|
10 | const DB_VERSION = 3
|
11 |
|
12 |
|
13 | function 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 |
|
26 | function 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 |
|
35 | const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' })
|
36 | store.createIndex('store', 'store', { unique: false })
|
37 | }
|
38 |
|
39 | if (event.oldVersion < 3) {
|
40 |
|
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 |
|
58 | function 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 |
|
67 | let cleanedUp = false
|
68 | class IndexedDBStore {
|
69 | constructor (opts) {
|
70 | this.opts = Object.assign({
|
71 | dbName: DB_NAME,
|
72 | storeName: 'default',
|
73 | expires: DEFAULT_EXPIRY,
|
74 | maxFileSize: 10 * 1024 * 1024,
|
75 | maxTotalSize: 300 * 1024 * 1024
|
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 |
|
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 |
|
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 |
|
133 |
|
134 |
|
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 |
|
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 |
|
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 |
|
199 |
|
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()
|
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 |
|
230 | IndexedDBStore.isSupported = isSupported
|
231 |
|
232 | module.exports = IndexedDBStore
|