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
JavaScript
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])
})
}
})
}
}