UNPKG

uppy

Version:

Extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:

233 lines (210 loc) 6.57 kB
const prettyBytes = require('prettier-bytes') 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