UNPKG

9.04 kBJavaScriptView Raw
1const { Plugin } = require('@uppy/core')
2const ServiceWorkerStore = require('./ServiceWorkerStore')
3const IndexedDBStore = require('./IndexedDBStore')
4const MetaDataStore = require('./MetaDataStore')
5
6/**
7 * The GoldenRetriever plugin — restores selected files and resumes uploads
8 * after a closed tab or a browser crash!
9 *
10 * Uses localStorage, IndexedDB and ServiceWorker to do its magic, read more:
11 * https://uppy.io/blog/2017/07/golden-retriever/
12 */
13module.exports = class GoldenRetriever extends Plugin {
14 static VERSION = require('../package.json').version
15
16 constructor (uppy, opts) {
17 super(uppy, opts)
18 this.type = 'debugger'
19 this.id = this.opts.id || 'GoldenRetriever'
20 this.title = 'Golden Retriever'
21
22 const defaultOptions = {
23 expires: 24 * 60 * 60 * 1000, // 24 hours
24 serviceWorker: false
25 }
26
27 this.opts = Object.assign({}, defaultOptions, opts)
28
29 this.MetaDataStore = new MetaDataStore({
30 expires: this.opts.expires,
31 storeName: uppy.getID()
32 })
33 this.ServiceWorkerStore = null
34 if (this.opts.serviceWorker) {
35 this.ServiceWorkerStore = new ServiceWorkerStore({ storeName: uppy.getID() })
36 }
37 this.IndexedDBStore = new IndexedDBStore(Object.assign(
38 { expires: this.opts.expires },
39 this.opts.indexedDB || {},
40 { storeName: uppy.getID() }))
41
42 this.saveFilesStateToLocalStorage = this.saveFilesStateToLocalStorage.bind(this)
43 this.loadFilesStateFromLocalStorage = this.loadFilesStateFromLocalStorage.bind(this)
44 this.loadFileBlobsFromServiceWorker = this.loadFileBlobsFromServiceWorker.bind(this)
45 this.loadFileBlobsFromIndexedDB = this.loadFileBlobsFromIndexedDB.bind(this)
46 this.onBlobsLoaded = this.onBlobsLoaded.bind(this)
47 }
48
49 loadFilesStateFromLocalStorage () {
50 const savedState = this.MetaDataStore.load()
51
52 if (savedState) {
53 this.uppy.log('[GoldenRetriever] Recovered some state from Local Storage')
54 this.uppy.setState({
55 currentUploads: savedState.currentUploads || {},
56 files: savedState.files || {}
57 })
58
59 this.savedPluginData = savedState.pluginData
60 }
61 }
62
63 /**
64 * Get file objects that are currently waiting: they've been selected,
65 * but aren't yet being uploaded.
66 */
67 getWaitingFiles () {
68 const waitingFiles = {}
69
70 this.uppy.getFiles().forEach((file) => {
71 if (!file.progress || !file.progress.uploadStarted) {
72 waitingFiles[file.id] = file
73 }
74 })
75
76 return waitingFiles
77 }
78
79 /**
80 * Get file objects that are currently being uploaded. If a file has finished
81 * uploading, but the other files in the same batch have not, the finished
82 * file is also returned.
83 */
84 getUploadingFiles () {
85 const uploadingFiles = {}
86
87 const { currentUploads } = this.uppy.getState()
88 if (currentUploads) {
89 const uploadIDs = Object.keys(currentUploads)
90 uploadIDs.forEach((uploadID) => {
91 const filesInUpload = currentUploads[uploadID].fileIDs
92 filesInUpload.forEach((fileID) => {
93 uploadingFiles[fileID] = this.uppy.getFile(fileID)
94 })
95 })
96 }
97
98 return uploadingFiles
99 }
100
101 saveFilesStateToLocalStorage () {
102 const filesToSave = Object.assign(
103 this.getWaitingFiles(),
104 this.getUploadingFiles()
105 )
106
107 const pluginData = {}
108 // TODO Find a better way to do this?
109 // Other plugins can attach a restore:get-data listener that receives this callback.
110 // Plugins can then use this callback (sync) to provide data to be stored.
111 this.uppy.emit('restore:get-data', (data) => {
112 Object.assign(pluginData, data)
113 })
114
115 const { currentUploads } = this.uppy.getState()
116 this.MetaDataStore.save({
117 currentUploads: currentUploads,
118 files: filesToSave,
119 pluginData: pluginData
120 })
121 }
122
123 loadFileBlobsFromServiceWorker () {
124 this.ServiceWorkerStore.list().then((blobs) => {
125 const numberOfFilesRecovered = Object.keys(blobs).length
126 const numberOfFilesTryingToRecover = this.uppy.getFiles().length
127 if (numberOfFilesRecovered === numberOfFilesTryingToRecover) {
128 this.uppy.log(`[GoldenRetriever] Successfully recovered ${numberOfFilesRecovered} blobs from Service Worker!`)
129 this.uppy.info(`Successfully recovered ${numberOfFilesRecovered} files`, 'success', 3000)
130 return this.onBlobsLoaded(blobs)
131 }
132 this.uppy.log('[GoldenRetriever] No blobs found in Service Worker, trying IndexedDB now...')
133 return this.loadFileBlobsFromIndexedDB()
134 }).catch((err) => {
135 this.uppy.log('[GoldenRetriever] Failed to recover blobs from Service Worker', 'warning')
136 this.uppy.log(err)
137 })
138 }
139
140 loadFileBlobsFromIndexedDB () {
141 this.IndexedDBStore.list().then((blobs) => {
142 const numberOfFilesRecovered = Object.keys(blobs).length
143
144 if (numberOfFilesRecovered > 0) {
145 this.uppy.log(`[GoldenRetriever] Successfully recovered ${numberOfFilesRecovered} blobs from IndexedDB!`)
146 this.uppy.info(`Successfully recovered ${numberOfFilesRecovered} files`, 'success', 3000)
147 return this.onBlobsLoaded(blobs)
148 }
149 this.uppy.log('[GoldenRetriever] No blobs found in IndexedDB')
150 }).catch((err) => {
151 this.uppy.log('[GoldenRetriever] Failed to recover blobs from IndexedDB', 'warning')
152 this.uppy.log(err)
153 })
154 }
155
156 onBlobsLoaded (blobs) {
157 const obsoleteBlobs = []
158 const updatedFiles = Object.assign({}, this.uppy.getState().files)
159 Object.keys(blobs).forEach((fileID) => {
160 const originalFile = this.uppy.getFile(fileID)
161 if (!originalFile) {
162 obsoleteBlobs.push(fileID)
163 return
164 }
165
166 const cachedData = blobs[fileID]
167
168 const updatedFileData = {
169 data: cachedData,
170 isRestored: true
171 }
172 const updatedFile = Object.assign({}, originalFile, updatedFileData)
173 updatedFiles[fileID] = updatedFile
174 })
175
176 this.uppy.setState({
177 files: updatedFiles
178 })
179
180 this.uppy.emit('restored', this.savedPluginData)
181
182 if (obsoleteBlobs.length) {
183 this.deleteBlobs(obsoleteBlobs).then(() => {
184 this.uppy.log(`[GoldenRetriever] Cleaned up ${obsoleteBlobs.length} old files`)
185 }).catch((err) => {
186 this.uppy.log(`[GoldenRetriever] Could not clean up ${obsoleteBlobs.length} old files`, 'warning')
187 this.uppy.log(err)
188 })
189 }
190 }
191
192 deleteBlobs (fileIDs) {
193 const promises = []
194 fileIDs.forEach((id) => {
195 if (this.ServiceWorkerStore) {
196 promises.push(this.ServiceWorkerStore.delete(id))
197 }
198 if (this.IndexedDBStore) {
199 promises.push(this.IndexedDBStore.delete(id))
200 }
201 })
202 return Promise.all(promises)
203 }
204
205 install () {
206 this.loadFilesStateFromLocalStorage()
207
208 if (this.uppy.getFiles().length > 0) {
209 if (this.ServiceWorkerStore) {
210 this.uppy.log('[GoldenRetriever] Attempting to load files from Service Worker...')
211 this.loadFileBlobsFromServiceWorker()
212 } else {
213 this.uppy.log('[GoldenRetriever] Attempting to load files from Indexed DB...')
214 this.loadFileBlobsFromIndexedDB()
215 }
216 } else {
217 this.uppy.log('[GoldenRetriever] No files need to be loaded, only restoring processing state...')
218 this.onBlobsLoaded([])
219 }
220
221 this.uppy.on('file-added', (file) => {
222 if (file.isRemote) return
223
224 if (this.ServiceWorkerStore) {
225 this.ServiceWorkerStore.put(file).catch((err) => {
226 this.uppy.log('[GoldenRetriever] Could not store file', 'warning')
227 this.uppy.log(err)
228 })
229 }
230
231 this.IndexedDBStore.put(file).catch((err) => {
232 this.uppy.log('[GoldenRetriever] Could not store file', 'warning')
233 this.uppy.log(err)
234 })
235 })
236
237 this.uppy.on('file-removed', (file) => {
238 if (this.ServiceWorkerStore) {
239 this.ServiceWorkerStore.delete(file.id).catch((err) => {
240 this.uppy.log('[GoldenRetriever] Failed to remove file', 'warning')
241 this.uppy.log(err)
242 })
243 }
244 this.IndexedDBStore.delete(file.id).catch((err) => {
245 this.uppy.log('[GoldenRetriever] Failed to remove file', 'warning')
246 this.uppy.log(err)
247 })
248 })
249
250 this.uppy.on('complete', ({ successful }) => {
251 const fileIDs = successful.map((file) => file.id)
252 this.deleteBlobs(fileIDs).then(() => {
253 this.uppy.log(`[GoldenRetriever] Removed ${successful.length} files that finished uploading`)
254 }).catch((err) => {
255 this.uppy.log(`[GoldenRetriever] Could not remove ${successful.length} files that finished uploading`, 'warning')
256 this.uppy.log(err)
257 })
258 })
259
260 this.uppy.on('state-update', this.saveFilesStateToLocalStorage)
261
262 this.uppy.on('restored', () => {
263 // start all uploads again when file blobs are restored
264 const { currentUploads } = this.uppy.getState()
265 if (currentUploads) {
266 Object.keys(currentUploads).forEach((uploadId) => {
267 this.uppy.restore(uploadId, currentUploads[uploadId])
268 })
269 }
270 })
271 }
272}