index.js 8.8 KB

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