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:

274 lines (236 loc) 9.09 kB
const Plugin = require('../../core/Plugin') const ServiceWorkerStore = require('./ServiceWorkerStore') const IndexedDBStore = require('./IndexedDBStore') const MetaDataStore = require('./MetaDataStore') /** * The Golden Retriever plugin — restores selected files and resumes uploads * after a closed tab or a browser crash! * * Uses localStorage, IndexedDB and ServiceWorker to do its magic, read more: * https://uppy.io/blog/2017/07/golden-retriever/ */ module.exports = class GoldenRetriever extends Plugin { constructor (uppy, opts) { super(uppy, opts) this.type = 'debugger' this.id = 'GoldenRetriever' this.title = 'Golden Retriever' const defaultOptions = { expires: 24 * 60 * 60 * 1000, // 24 hours serviceWorker: false } this.opts = Object.assign({}, defaultOptions, opts) this.MetaDataStore = new MetaDataStore({ expires: this.opts.expires, storeName: uppy.getID() }) this.ServiceWorkerStore = null if (this.opts.serviceWorker) { this.ServiceWorkerStore = new ServiceWorkerStore({ storeName: uppy.getID() }) } this.IndexedDBStore = new IndexedDBStore(Object.assign( { expires: this.opts.expires }, opts.indexedDB || {}, { storeName: uppy.getID() })) this.saveFilesStateToLocalStorage = this.saveFilesStateToLocalStorage.bind(this) this.loadFilesStateFromLocalStorage = this.loadFilesStateFromLocalStorage.bind(this) this.loadFileBlobsFromServiceWorker = this.loadFileBlobsFromServiceWorker.bind(this) this.loadFileBlobsFromIndexedDB = this.loadFileBlobsFromIndexedDB.bind(this) this.onBlobsLoaded = this.onBlobsLoaded.bind(this) } loadFilesStateFromLocalStorage () { const savedState = this.MetaDataStore.load() if (savedState) { this.uppy.log('[GoldenRetriever] Recovered some state from Local Storage') this.uppy.setState({ currentUploads: savedState.currentUploads || {}, files: savedState.files || {} }) this.savedPluginData = savedState.pluginData } } /** * Get file objects that are currently waiting: they've been selected, * but aren't yet being uploaded. */ getWaitingFiles () { const waitingFiles = {} const allFiles = this.uppy.state.files Object.keys(allFiles).forEach((fileID) => { const file = this.uppy.getFile(fileID) if (!file.progress || !file.progress.uploadStarted) { waitingFiles[fileID] = file } }) return waitingFiles } /** * Get file objects that are currently being uploaded. If a file has finished * uploading, but the other files in the same batch have not, the finished * file is also returned. */ getUploadingFiles () { const uploadingFiles = {} const { currentUploads } = this.uppy.state if (currentUploads) { const uploadIDs = Object.keys(currentUploads) uploadIDs.forEach((uploadID) => { const filesInUpload = currentUploads[uploadID].fileIDs filesInUpload.forEach((fileID) => { uploadingFiles[fileID] = this.uppy.getFile(fileID) }) }) } return uploadingFiles } saveFilesStateToLocalStorage () { const filesToSave = Object.assign( this.getWaitingFiles(), this.getUploadingFiles() ) const pluginData = {} // TODO Find a better way to do this? // Other plugins can attach a restore:get-data listener that receives this callback. // Plugins can then use this callback (sync) to provide data to be stored. this.uppy.emit('restore:get-data', (data) => { Object.assign(pluginData, data) }) this.MetaDataStore.save({ currentUploads: this.uppy.state.currentUploads, files: filesToSave, pluginData: pluginData }) } loadFileBlobsFromServiceWorker () { this.ServiceWorkerStore.list().then((blobs) => { const numberOfFilesRecovered = Object.keys(blobs).length const numberOfFilesTryingToRecover = Object.keys(this.uppy.state.files).length if (numberOfFilesRecovered === numberOfFilesTryingToRecover) { this.uppy.log(`[GoldenRetriever] Successfully recovered ${numberOfFilesRecovered} blobs from Service Worker!`) this.uppy.info(`Successfully recovered ${numberOfFilesRecovered} files`, 'success', 3000) return this.onBlobsLoaded(blobs) } this.uppy.log('[GoldenRetriever] No blobs found in Service Worker, trying IndexedDB now...') return this.loadFileBlobsFromIndexedDB() }).catch((err) => { this.uppy.log('[GoldenRetriever] Failed to recover blobs from Service Worker', 'warning') this.uppy.log(err) }) } loadFileBlobsFromIndexedDB () { this.IndexedDBStore.list().then((blobs) => { const numberOfFilesRecovered = Object.keys(blobs).length if (numberOfFilesRecovered > 0) { this.uppy.log(`[GoldenRetriever] Successfully recovered ${numberOfFilesRecovered} blobs from IndexedDB!`) this.uppy.info(`Successfully recovered ${numberOfFilesRecovered} files`, 'success', 3000) return this.onBlobsLoaded(blobs) } this.uppy.log('[GoldenRetriever] No blobs found in IndexedDB') }).catch((err) => { this.uppy.log('[GoldenRetriever] Failed to recover blobs from IndexedDB', 'warning') this.uppy.log(err) }) } onBlobsLoaded (blobs) { const obsoleteBlobs = [] const updatedFiles = Object.assign({}, this.uppy.state.files) Object.keys(blobs).forEach((fileID) => { const originalFile = this.uppy.getFile(fileID) if (!originalFile) { obsoleteBlobs.push(fileID) return } const cachedData = blobs[fileID] const updatedFileData = { data: cachedData, isRestored: true } const updatedFile = Object.assign({}, originalFile, updatedFileData) updatedFiles[fileID] = updatedFile this.uppy.generatePreview(updatedFile) }) this.uppy.setState({ files: updatedFiles }) this.uppy.emit('restored', this.savedPluginData) if (obsoleteBlobs.length) { this.deleteBlobs(obsoleteBlobs).then(() => { this.uppy.log(`[GoldenRetriever] Cleaned up ${obsoleteBlobs.length} old files`) }).catch((err) => { this.uppy.log(`[GoldenRetriever] Could not clean up ${obsoleteBlobs.length} old files`, 'warning') this.uppy.log(err) }) } } deleteBlobs (fileIDs) { const promises = [] fileIDs.forEach((id) => { if (this.ServiceWorkerStore) { promises.push(this.ServiceWorkerStore.delete(id)) } if (this.IndexedDBStore) { promises.push(this.IndexedDBStore.delete(id)) } }) return Promise.all(promises) } install () { this.loadFilesStateFromLocalStorage() if (Object.keys(this.uppy.state.files).length > 0) { if (this.ServiceWorkerStore) { this.uppy.log('[GoldenRetriever] Attempting to load files from Service Worker...') this.loadFileBlobsFromServiceWorker() } else { this.uppy.log('[GoldenRetriever] Attempting to load files from Indexed DB...') this.loadFileBlobsFromIndexedDB() } } else { this.uppy.log('[GoldenRetriever] No files need to be loaded, only restoring processing state...') this.onBlobsLoaded([]) } this.uppy.on('file-added', (file) => { if (file.isRemote) return if (this.ServiceWorkerStore) { this.ServiceWorkerStore.put(file).catch((err) => { this.uppy.log('[GoldenRetriever] Could not store file', 'warning') this.uppy.log(err) }) } this.IndexedDBStore.put(file).catch((err) => { this.uppy.log('[GoldenRetriever] Could not store file', 'warning') this.uppy.log(err) }) }) this.uppy.on('file-removed', (file) => { if (this.ServiceWorkerStore) { this.ServiceWorkerStore.delete(file.id).catch((err) => { this.uppy.log('[GoldenRetriever] Failed to remove file', 'warning') this.uppy.log(err) }) } this.IndexedDBStore.delete(file.id).catch((err) => { this.uppy.log('[GoldenRetriever] Failed to remove file', 'warning') this.uppy.log(err) }) }) this.uppy.on('complete', ({ successful }) => { const fileIDs = successful.map((file) => file.id) this.deleteBlobs(fileIDs).then(() => { this.uppy.log(`[GoldenRetriever] Removed ${successful.length} files that finished uploading`) }).catch((err) => { this.uppy.log(`[GoldenRetriever] Could not remove ${successful.length} files that finished uploading`, 'warning') this.uppy.log(err) }) }) this.uppy.on('state-update', this.saveFilesStateToLocalStorage) this.uppy.on('restored', () => { // start all uploads again when file blobs are restored const { currentUploads } = this.uppy.getState() if (currentUploads) { Object.keys(currentUploads).forEach((uploadId) => { this.uppy.restore(uploadId, currentUploads[uploadId]) }) } }) } }