diff --git a/README.md b/README.md index 9da200911..3fa1a5520 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ const Dashboard = require('uppy/lib/plugins/Dashboard') const Webcam = require('uppy/lib/plugins/Webcam') const GoogleDrive = require('uppy/lib/plugins/GoogleDrive') const Tus10 = require('uppy/lib/plugins/Tus10') - + const uppy = Uppy({ autoProceed: false }) .use(Dashboard, { trigger: '#select-files' }) .use(Webcam, { target: Dashboard }) @@ -106,7 +106,7 @@ If you like, you can also use a pre-built bundle, for example from [unpkg CDN](h - `ProgressBar` — minimal progress bar that fills itself when upload progresses - `StatusBar` — more detailed progress, pause/resume/cancel buttons, percentage, speed, uploaded/total sizes (included by default with `Dashboard`) - `Informer` — send notifications like “smile” before taking a selfie or “upload failed” when all is lost (also included by default with `Dashboard`) -- `RestoreFiles` — restores files after a browser crash, like it’s nothing +- `GoldenRetriever` — restores files after a browser crash, like it’s nothing - `ReduxDevTools` — for your emerging [time traveling](https://github.com/gaearon/redux-devtools) needs - `GoogleDrive` — select files from [Google Drive](https://www.google.com/drive/) - `Dropbox` — select files from [Dropbox](https://www.dropbox.com/) diff --git a/src/plugins/Tus10.js b/Tus.js similarity index 100% rename from src/plugins/Tus10.js rename to Tus.js diff --git a/examples/bundled-example/main.js b/examples/bundled-example/main.js index 08502d578..b2c620b46 100644 --- a/examples/bundled-example/main.js +++ b/examples/bundled-example/main.js @@ -1,17 +1,17 @@ -const Uppy = require('../../src/core/Core.js') +const Uppy = require('../../src/core') const Dashboard = require('../../src/plugins/Dashboard') // const GoogleDrive = require('../../src/plugins/GoogleDrive') const Dropbox = require('../../src/plugins/Dropbox') const Instagram = require('../../src/plugins/Instagram') // const Webcam = require('../../src/plugins/Webcam') const Tus10 = require('../../src/plugins/Tus10') -// const Multipart = require('../../src/plugins/Multipart') +// const XHRUpload = require('../../src/plugins/XHRUpload') // const FileInput = require('../../src/plugins/FileInput') const MetaData = require('../../src/plugins/MetaData') // const Informer = require('../../src/plugins/Informer') // const StatusBar = require('../../src/plugins/StatusBar') // const DragDrop = require('../../src/plugins/DragDrop') -// const RestoreFiles = require('../../src/plugins/RestoreFiles') +// const GoldenRetriever = require('../../src/plugins/GoldenRetriever') const PROTOCOL = location.protocol === 'https:' ? 'https' : 'http' const TUS_ENDPOINT = PROTOCOL + '://master.tus.io/files/' @@ -68,7 +68,7 @@ const uppy = Uppy({ { id: 'caption', name: 'Caption', value: 'none', placeholder: 'describe what the image is about' } ] }) - // .use(RestoreFiles, {serviceWorker: true}) + // .use(GoldenRetriever, {serviceWorker: true}) .run() uppy.on('core:success', (fileList) => { diff --git a/examples/multiple-instances/main.js b/examples/multiple-instances/main.js index b4bda290b..4c46d228a 100644 --- a/examples/multiple-instances/main.js +++ b/examples/multiple-instances/main.js @@ -1,8 +1,8 @@ const Uppy = require('uppy/lib/core') const Dashboard = require('uppy/lib/plugins/Dashboard') -const RestoreFiles = require('uppy/lib/plugins/RestoreFiles') +const GoldenRetriever = require('uppy/lib/plugins/GoldenRetriever') -// Initialise two Uppy instances with the RestoreFiles plugin, +// Initialise two Uppy instances with the GoldenRetriever plugin, // but with different `id`s. const a = Uppy({ id: 'a', @@ -13,7 +13,7 @@ const a = Uppy({ inline: true, maxWidth: 400 }) - .use(RestoreFiles, { serviceWorker: false }) + .use(GoldenRetriever, { serviceWorker: false }) .run() const b = Uppy({ @@ -25,7 +25,7 @@ const b = Uppy({ inline: true, maxWidth: 400 }) - .use(RestoreFiles, { serviceWorker: false }) + .use(GoldenRetriever, { serviceWorker: false }) .run() window.a = a diff --git a/src/index.js b/src/index.js index fe1fa0e05..7b39766dd 100644 --- a/src/index.js +++ b/src/index.js @@ -30,7 +30,7 @@ const Transloadit = require('./plugins/Transloadit') const AwsS3 = require('./plugins/AwsS3') // Other? -const RestoreFiles = require('./plugins/RestoreFiles') +const GoldenRetriever = require('./plugins/GoldenRetriever') const ReduxDevTools = require('./plugins/ReduxDevTools') const ReduxStore = require('./plugins/Redux') @@ -53,7 +53,22 @@ module.exports = { Dashboard, MetaData, Webcam, - RestoreFiles, + GoldenRetriever, ReduxDevTools, ReduxStore } + +Object.defineProperty(module.exports, 'RestoreFiles', { + enumerable: true, + configurable: true, + get: () => { + console.warn('Uppy.RestoreFiles is deprecated and will be removed in v0.22. Use Uppy.GoldenRetriever instead.') + Object.defineProperty(module.exports, 'RestoreFiles', { + enumerable: true, + configurable: true, + writable: true, + value: GoldenRetriever + }) + return GoldenRetriever + } +}) diff --git a/src/plugins/RestoreFiles/IndexedDBStore.js b/src/plugins/GoldenRetriever/IndexedDBStore.js similarity index 100% rename from src/plugins/RestoreFiles/IndexedDBStore.js rename to src/plugins/GoldenRetriever/IndexedDBStore.js diff --git a/src/plugins/RestoreFiles/MetaDataStore.js b/src/plugins/GoldenRetriever/MetaDataStore.js similarity index 100% rename from src/plugins/RestoreFiles/MetaDataStore.js rename to src/plugins/GoldenRetriever/MetaDataStore.js diff --git a/src/plugins/RestoreFiles/ServiceWorker.js b/src/plugins/GoldenRetriever/ServiceWorker.js similarity index 100% rename from src/plugins/RestoreFiles/ServiceWorker.js rename to src/plugins/GoldenRetriever/ServiceWorker.js diff --git a/src/plugins/RestoreFiles/ServiceWorkerStore.js b/src/plugins/GoldenRetriever/ServiceWorkerStore.js similarity index 100% rename from src/plugins/RestoreFiles/ServiceWorkerStore.js rename to src/plugins/GoldenRetriever/ServiceWorkerStore.js diff --git a/src/plugins/RestoreFiles/cleanup.js b/src/plugins/GoldenRetriever/cleanup.js similarity index 100% rename from src/plugins/RestoreFiles/cleanup.js rename to src/plugins/GoldenRetriever/cleanup.js diff --git a/src/plugins/GoldenRetriever/index.js b/src/plugins/GoldenRetriever/index.js new file mode 100644 index 000000000..b84c8487f --- /dev/null +++ b/src/plugins/GoldenRetriever/index.js @@ -0,0 +1,234 @@ +const Plugin = require('../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 (core, opts) { + super(core, opts) + this.type = 'debugger' + this.id = 'GoldenRetriever' + this.title = 'Restore Files' + + 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: core.getID() + }) + this.ServiceWorkerStore = null + if (this.opts.serviceWorker) { + this.ServiceWorkerStore = new ServiceWorkerStore({ storeName: core.getID() }) + } + this.IndexedDBStore = new IndexedDBStore(Object.assign( + { expires: this.opts.expires }, + opts.indexedDB || {}, + { storeName: core.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.core.log('Recovered some state from Local Storage') + this.core.setState(savedState) + } + } + + /** + * Get file objects that are currently waiting: they've been selected, + * but aren't yet being uploaded. + */ + getWaitingFiles () { + const waitingFiles = {} + + const allFiles = this.core.state.files + Object.keys(allFiles).forEach((fileID) => { + const file = this.core.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.core.state + if (currentUploads) { + const uploadIDs = Object.keys(currentUploads) + uploadIDs.forEach((uploadID) => { + const filesInUpload = currentUploads[uploadID].fileIDs + filesInUpload.forEach((fileID) => { + uploadingFiles[fileID] = this.core.getFile(fileID) + }) + }) + } + + return uploadingFiles + } + + saveFilesStateToLocalStorage () { + const filesToSave = Object.assign( + this.getWaitingFiles(), + this.getUploadingFiles() + ) + + this.MetaDataStore.save({ + currentUploads: this.core.state.currentUploads, + files: filesToSave + }) + } + + loadFileBlobsFromServiceWorker () { + this.ServiceWorkerStore.list().then((blobs) => { + const numberOfFilesRecovered = Object.keys(blobs).length + const numberOfFilesTryingToRecover = Object.keys(this.core.state.files).length + if (numberOfFilesRecovered === numberOfFilesTryingToRecover) { + this.core.log(`Successfully recovered ${numberOfFilesRecovered} blobs from Service Worker!`) + this.core.info(`Successfully recovered ${numberOfFilesRecovered} files`, 'success', 3000) + this.onBlobsLoaded(blobs) + } else { + this.core.log('Failed to recover blobs from Service Worker, trying IndexedDB now...') + this.loadFileBlobsFromIndexedDB() + } + }) + } + + loadFileBlobsFromIndexedDB () { + this.IndexedDBStore.list().then((blobs) => { + const numberOfFilesRecovered = Object.keys(blobs).length + + if (numberOfFilesRecovered > 0) { + this.core.log(`Successfully recovered ${numberOfFilesRecovered} blobs from Indexed DB!`) + this.core.info(`Successfully recovered ${numberOfFilesRecovered} files`, 'success', 3000) + return this.onBlobsLoaded(blobs) + } + this.core.log('Couldn’t recover anything from IndexedDB :(') + }) + } + + onBlobsLoaded (blobs) { + const obsoleteBlobs = [] + const updatedFiles = Object.assign({}, this.core.state.files) + Object.keys(blobs).forEach((fileID) => { + const originalFile = this.core.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.core.generatePreview(updatedFile) + }) + this.core.setState({ + files: updatedFiles + }) + this.core.emit('core:restored') + + if (obsoleteBlobs.length) { + this.deleteBlobs(obsoleteBlobs).then(() => { + this.core.log(`[GoldenRetriever] cleaned up ${obsoleteBlobs.length} old files`) + }) + } + } + + 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.core.state.files).length > 0) { + if (this.ServiceWorkerStore) { + this.core.log('Attempting to load files from Service Worker...') + this.loadFileBlobsFromServiceWorker() + } else { + this.core.log('Attempting to load files from Indexed DB...') + this.loadFileBlobsFromIndexedDB() + } + } + + this.core.on('core:file-added', (file) => { + if (file.isRemote) return + + if (this.ServiceWorkerStore) { + this.ServiceWorkerStore.put(file).catch((err) => { + this.core.log('Could not store file', 'error') + this.core.log(err) + }) + } + + this.IndexedDBStore.put(file).catch((err) => { + this.core.log('Could not store file', 'error') + this.core.log(err) + }) + }) + + this.core.on('core:file-removed', (fileID) => { + if (this.ServiceWorkerStore) this.ServiceWorkerStore.delete(fileID) + this.IndexedDBStore.delete(fileID) + }) + + this.core.on('core:success', (fileIDs) => { + this.deleteBlobs(fileIDs).then(() => { + this.core.log(`[GoldenRetriever] removed ${fileIDs.length} files that finished uploading`) + }) + }) + + this.core.on('core:state-update', this.saveFilesStateToLocalStorage) + + this.core.on('core:restored', () => { + // start all uploads again when file blobs are restored + const { currentUploads } = this.core.getState() + if (currentUploads) { + Object.keys(currentUploads).forEach((uploadId) => { + this.core.restore(uploadId, currentUploads[uploadId]) + }) + } + }) + } +} diff --git a/src/plugins/RestoreFiles/index.js b/src/plugins/RestoreFiles/index.js index cda55dbfc..ddbc19b7b 100644 --- a/src/plugins/RestoreFiles/index.js +++ b/src/plugins/RestoreFiles/index.js @@ -1,234 +1,5 @@ -const Plugin = require('../Plugin') -const ServiceWorkerStore = require('./ServiceWorkerStore') -const IndexedDBStore = require('./IndexedDBStore') -const MetaDataStore = require('./MetaDataStore') +const GoldenRetriever = require('../GoldenRetriever') -/** -* Restore Files 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 RestoreFiles extends Plugin { - constructor (core, opts) { - super(core, opts) - this.type = 'debugger' - this.id = 'RestoreFiles' - this.title = 'Restore Files' +console.warn('Using `uppy/lib/plugins/RestoreFiles` is deprecated and will be removed in v0.22. Please use `uppy/lib/plugins/GoldenRetriever` instead.') - 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: core.getID() - }) - this.ServiceWorkerStore = null - if (this.opts.serviceWorker) { - this.ServiceWorkerStore = new ServiceWorkerStore({ storeName: core.getID() }) - } - this.IndexedDBStore = new IndexedDBStore(Object.assign( - { expires: this.opts.expires }, - opts.indexedDB || {}, - { storeName: core.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.core.log('Recovered some state from Local Storage') - this.core.setState(savedState) - } - } - - /** - * Get file objects that are currently waiting: they've been selected, - * but aren't yet being uploaded. - */ - getWaitingFiles () { - const waitingFiles = {} - - const allFiles = this.core.state.files - Object.keys(allFiles).forEach((fileID) => { - const file = this.core.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.core.state - if (currentUploads) { - const uploadIDs = Object.keys(currentUploads) - uploadIDs.forEach((uploadID) => { - const filesInUpload = currentUploads[uploadID].fileIDs - filesInUpload.forEach((fileID) => { - uploadingFiles[fileID] = this.core.getFile(fileID) - }) - }) - } - - return uploadingFiles - } - - saveFilesStateToLocalStorage () { - const filesToSave = Object.assign( - this.getWaitingFiles(), - this.getUploadingFiles() - ) - - this.MetaDataStore.save({ - currentUploads: this.core.state.currentUploads, - files: filesToSave - }) - } - - loadFileBlobsFromServiceWorker () { - this.ServiceWorkerStore.list().then((blobs) => { - const numberOfFilesRecovered = Object.keys(blobs).length - const numberOfFilesTryingToRecover = Object.keys(this.core.state.files).length - if (numberOfFilesRecovered === numberOfFilesTryingToRecover) { - this.core.log(`Successfully recovered ${numberOfFilesRecovered} blobs from Service Worker!`) - this.core.info(`Successfully recovered ${numberOfFilesRecovered} files`, 'success', 3000) - this.onBlobsLoaded(blobs) - } else { - this.core.log('Failed to recover blobs from Service Worker, trying IndexedDB now...') - this.loadFileBlobsFromIndexedDB() - } - }) - } - - loadFileBlobsFromIndexedDB () { - this.IndexedDBStore.list().then((blobs) => { - const numberOfFilesRecovered = Object.keys(blobs).length - - if (numberOfFilesRecovered > 0) { - this.core.log(`Successfully recovered ${numberOfFilesRecovered} blobs from Indexed DB!`) - this.core.info(`Successfully recovered ${numberOfFilesRecovered} files`, 'success', 3000) - return this.onBlobsLoaded(blobs) - } - this.core.log('Couldn’t recover anything from IndexedDB :(') - }) - } - - onBlobsLoaded (blobs) { - const obsoleteBlobs = [] - const updatedFiles = Object.assign({}, this.core.state.files) - Object.keys(blobs).forEach((fileID) => { - const originalFile = this.core.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.core.generatePreview(updatedFile) - }) - this.core.setState({ - files: updatedFiles - }) - this.core.emit('core:restored') - - if (obsoleteBlobs.length) { - this.deleteBlobs(obsoleteBlobs).then(() => { - this.core.log(`RestoreFiles: cleaned up ${obsoleteBlobs.length} old files`) - }) - } - } - - 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.core.state.files).length > 0) { - if (this.ServiceWorkerStore) { - this.core.log('Attempting to load files from Service Worker...') - this.loadFileBlobsFromServiceWorker() - } else { - this.core.log('Attempting to load files from Indexed DB...') - this.loadFileBlobsFromIndexedDB() - } - } - - this.core.on('core:file-added', (file) => { - if (file.isRemote) return - - if (this.ServiceWorkerStore) { - this.ServiceWorkerStore.put(file).catch((err) => { - this.core.log('Could not store file', 'error') - this.core.log(err) - }) - } - - this.IndexedDBStore.put(file).catch((err) => { - this.core.log('Could not store file', 'error') - this.core.log(err) - }) - }) - - this.core.on('core:file-removed', (fileID) => { - if (this.ServiceWorkerStore) this.ServiceWorkerStore.delete(fileID) - this.IndexedDBStore.delete(fileID) - }) - - this.core.on('core:success', (fileIDs) => { - this.deleteBlobs(fileIDs).then(() => { - this.core.log(`RestoreFiles: removed ${fileIDs.length} files that finished uploading`) - }) - }) - - this.core.on('core:state-update', this.saveFilesStateToLocalStorage) - - this.core.on('core:restored', () => { - // start all uploads again when file blobs are restored - const { currentUploads } = this.core.getState() - if (currentUploads) { - Object.keys(currentUploads).forEach((uploadId) => { - this.core.restore(uploadId, currentUploads[uploadId]) - }) - } - }) - } -} +module.exports = GoldenRetriever diff --git a/website/src/docs/golden-retriever.md b/website/src/docs/golden-retriever.md index 66eb100af..8c15e9bfa 100644 --- a/website/src/docs/golden-retriever.md +++ b/website/src/docs/golden-retriever.md @@ -5,22 +5,23 @@ permalink: docs/golden-retriever/ order: 40 --- -Golden Retriever plugin, also known and used as `RestoreFiles`, saves selected files in your browser cache (Local Storage for metadata, then Service Worker for all blobs + IndexedDB for small blobs), so that if the browser crashes, Uppy can restore everything and continue uploading like nothing happened. Read more about it [on the blog](https://uppy.io/blog/2017/07/golden-retriever/). +Golden Retriever plugin saves selected files in your browser cache (Local Storage for metadata, then Service Worker for all blobs + IndexedDB for small blobs), so that if the browser crashes, Uppy can restore everything and continue uploading like nothing happened. Read more about it [on the blog](https://uppy.io/blog/2017/07/golden-retriever/). 1\. Bundle your own service worker `sw.js` file with Uppy Golden Retriever’s service worker. If you’re using Browserify, just bundle it separately, for Webpack there is a plugin [serviceworker-webpack-plugin](https://github.com/oliviertassinari/serviceworker-webpack-plugin). ```js // sw.js -require('uppy/lib/RestoreFiles/ServiceWorker.js') +require('uppy/lib/plugins/GoldenRetriever/ServiceWorker.js') ``` 2\. Register it in your app entry point: ```js // you app.js entry point +const GoldenRetriever = require('uppy/lib/plugins/GoldenRetriever') -uppy.use(RestoreFiles, {serviceWorker: true}) +uppy.use(GoldenRetriever, {serviceWorker: true}) uppy.run() if ('serviceWorker' in navigator) {