123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- const Plugin = require('@uppy/core/lib/Plugin')
- const ServiceWorkerStore = require('./ServiceWorkerStore')
- const IndexedDBStore = require('./IndexedDBStore')
- const MetaDataStore = require('./MetaDataStore')
- /**
- * The GoldenRetriever 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 = {}
- this.uppy.getFiles().forEach((file) => {
- if (!file.progress || !file.progress.uploadStarted) {
- waitingFiles[file.id] = 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.getState()
- 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)
- })
- const { currentUploads } = this.uppy.getState()
- this.MetaDataStore.save({
- currentUploads: currentUploads,
- files: filesToSave,
- pluginData: pluginData
- })
- }
- loadFileBlobsFromServiceWorker () {
- this.ServiceWorkerStore.list().then((blobs) => {
- const numberOfFilesRecovered = Object.keys(blobs).length
- const numberOfFilesTryingToRecover = this.uppy.getFiles().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.getState().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.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 (this.uppy.getFiles().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])
- })
- }
- })
- }
- }
|