Tus10.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. const Plugin = require('./Plugin')
  2. const tus = require('tus-js-client')
  3. const settle = require('promise-settle')
  4. const UppySocket = require('../core/UppySocket')
  5. const Utils = require('../core/Utils')
  6. require('whatwg-fetch')
  7. // Extracted from https://github.com/tus/tus-js-client/blob/master/lib/upload.js#L13
  8. // excepted we removed 'fingerprint' key to avoid adding more dependencies
  9. const tusDefaultOptions = {
  10. endpoint: '',
  11. resume: true,
  12. onProgress: null,
  13. onChunkComplete: null,
  14. onSuccess: null,
  15. onError: null,
  16. headers: {},
  17. chunkSize: Infinity,
  18. withCredentials: false,
  19. uploadUrl: null,
  20. uploadSize: null,
  21. overridePatchMethod: false,
  22. retryDelays: null
  23. }
  24. /**
  25. * Tus resumable file uploader
  26. *
  27. */
  28. module.exports = class Tus10 extends Plugin {
  29. constructor (core, opts) {
  30. super(core, opts)
  31. this.type = 'uploader'
  32. this.id = 'Tus'
  33. this.title = 'Tus'
  34. // set default options
  35. const defaultOptions = {
  36. resume: true,
  37. autoRetry: true,
  38. retryDelays: [0, 1000, 3000, 5000]
  39. }
  40. // merge default options with the ones set by user
  41. this.opts = Object.assign({}, defaultOptions, opts)
  42. this.handlePauseAll = this.handlePauseAll.bind(this)
  43. this.handleResumeAll = this.handleResumeAll.bind(this)
  44. this.handleResetProgress = this.handleResetProgress.bind(this)
  45. this.handleUpload = this.handleUpload.bind(this)
  46. }
  47. pauseResume (action, fileID) {
  48. const updatedFiles = Object.assign({}, this.core.getState().files)
  49. const inProgressUpdatedFiles = Object.keys(updatedFiles).filter((file) => {
  50. return !updatedFiles[file].progress.uploadComplete &&
  51. updatedFiles[file].progress.uploadStarted
  52. })
  53. switch (action) {
  54. case 'toggle':
  55. if (updatedFiles[fileID].uploadComplete) return
  56. const wasPaused = updatedFiles[fileID].isPaused || false
  57. const isPaused = !wasPaused
  58. let updatedFile
  59. if (wasPaused) {
  60. updatedFile = Object.assign({}, updatedFiles[fileID], {
  61. isPaused: false
  62. })
  63. } else {
  64. updatedFile = Object.assign({}, updatedFiles[fileID], {
  65. isPaused: true
  66. })
  67. }
  68. updatedFiles[fileID] = updatedFile
  69. this.core.setState({files: updatedFiles})
  70. return isPaused
  71. case 'pauseAll':
  72. inProgressUpdatedFiles.forEach((file) => {
  73. const updatedFile = Object.assign({}, updatedFiles[file], {
  74. isPaused: true
  75. })
  76. updatedFiles[file] = updatedFile
  77. })
  78. this.core.setState({files: updatedFiles})
  79. return
  80. case 'resumeAll':
  81. inProgressUpdatedFiles.forEach((file) => {
  82. const updatedFile = Object.assign({}, updatedFiles[file], {
  83. isPaused: false
  84. })
  85. updatedFiles[file] = updatedFile
  86. })
  87. this.core.setState({files: updatedFiles})
  88. return
  89. }
  90. }
  91. handlePauseAll () {
  92. this.pauseResume('pauseAll')
  93. }
  94. handleResumeAll () {
  95. this.pauseResume('resumeAll')
  96. }
  97. handleResetProgress () {
  98. const files = Object.assign({}, this.core.state.files)
  99. Object.keys(files).forEach((fileID) => {
  100. // Only clone the file object if it has a Tus `uploadUrl` attached.
  101. if (files[fileID].tus && files[fileID].tus.uploadUrl) {
  102. const tusState = Object.assign({}, files[fileID].tus)
  103. delete tusState.uploadUrl
  104. files[fileID] = Object.assign({}, files[fileID], { tus: tusState })
  105. }
  106. })
  107. this.core.setState({ files })
  108. }
  109. /**
  110. * Create a new Tus upload
  111. *
  112. * @param {object} file for use with upload
  113. * @param {integer} current file in a queue
  114. * @param {integer} total number of files in a queue
  115. * @returns {Promise}
  116. */
  117. upload (file, current, total) {
  118. this.core.log(`uploading ${current} of ${total}`)
  119. // Create a new tus upload
  120. return new Promise((resolve, reject) => {
  121. const optsTus = Object.assign(
  122. {},
  123. tusDefaultOptions,
  124. this.opts,
  125. // Install file-specific upload overrides.
  126. file.tus || {}
  127. )
  128. optsTus.onError = (err) => {
  129. this.core.log(err)
  130. this.core.emit('core:upload-error', file.id, err)
  131. reject('Failed because: ' + err)
  132. }
  133. optsTus.onProgress = (bytesUploaded, bytesTotal) => {
  134. this.onReceiveUploadUrl(file, upload.url)
  135. this.core.emit('core:upload-progress', {
  136. uploader: this,
  137. id: file.id,
  138. bytesUploaded: bytesUploaded,
  139. bytesTotal: bytesTotal
  140. })
  141. }
  142. optsTus.onSuccess = () => {
  143. this.core.emit('core:upload-success', file.id, upload, upload.url)
  144. if (upload.url) {
  145. this.core.log('Download ' + upload.file.name + ' from ' + upload.url)
  146. }
  147. resolve(upload)
  148. }
  149. optsTus.metadata = file.meta
  150. const upload = new tus.Upload(file.data, optsTus)
  151. this.onFileRemove(file.id, (targetFileID) => {
  152. // this.core.log(`removing file: ${targetFileID}`)
  153. upload.abort()
  154. resolve(`upload ${targetFileID} was removed`)
  155. })
  156. this.onPause(file.id, (isPaused) => {
  157. isPaused ? upload.abort() : upload.start()
  158. })
  159. this.onPauseAll(file.id, () => {
  160. upload.abort()
  161. })
  162. this.onResumeAll(file.id, () => {
  163. upload.start()
  164. })
  165. this.core.on('core:retry-started', () => {
  166. const files = this.core.getState().files
  167. if (files[file.id].progress.uploadComplete ||
  168. !files[file.id].progress.uploadStarted ||
  169. files[file.id].isPaused
  170. ) {
  171. return
  172. }
  173. upload.start()
  174. })
  175. upload.start()
  176. this.core.emit('core:upload-started', file.id, upload)
  177. })
  178. }
  179. uploadRemote (file, current, total) {
  180. return new Promise((resolve, reject) => {
  181. this.core.log(file.remote.url)
  182. if (file.serverToken) {
  183. this.connectToServerSocket(file)
  184. } else {
  185. let endpoint = this.opts.endpoint
  186. if (file.tus && file.tus.endpoint) {
  187. endpoint = file.tus.endpoint
  188. }
  189. this.core.emitter.emit('core:upload-started', file.id)
  190. fetch(file.remote.url, {
  191. method: 'post',
  192. credentials: 'include',
  193. headers: {
  194. 'Accept': 'application/json',
  195. 'Content-Type': 'application/json'
  196. },
  197. body: JSON.stringify(Object.assign({}, file.remote.body, {
  198. endpoint,
  199. protocol: 'tus',
  200. size: file.data.size,
  201. metadata: file.meta
  202. }))
  203. })
  204. .then((res) => {
  205. if (res.status < 200 && res.status > 300) {
  206. return reject(res.statusText)
  207. }
  208. res.json().then((data) => {
  209. const token = data.token
  210. file = this.getFile(file.id)
  211. file.serverToken = token
  212. this.updateFile(file)
  213. this.connectToServerSocket(file)
  214. resolve()
  215. })
  216. })
  217. }
  218. })
  219. }
  220. connectToServerSocket (file) {
  221. const token = file.serverToken
  222. const host = Utils.getSocketHost(file.remote.host)
  223. const socket = new UppySocket({ target: `${host}/api/${token}` })
  224. this.onFileRemove(file.id, () => socket.send('pause', {}))
  225. this.onPause(file.id, (isPaused) => {
  226. isPaused ? socket.send('pause', {}) : socket.send('resume', {})
  227. })
  228. this.onPauseAll(file.id, () => socket.send('pause', {}))
  229. this.onResumeAll(file.id, () => socket.send('resume', {}))
  230. socket.on('progress', (progressData) => Utils.emitSocketProgress(this, progressData, file))
  231. socket.on('success', (data) => {
  232. this.core.emitter.emit('core:upload-success', file.id, data, data.url)
  233. socket.close()
  234. })
  235. }
  236. getFile (fileID) {
  237. return this.core.state.files[fileID]
  238. }
  239. updateFile (file) {
  240. const files = Object.assign({}, this.core.state.files, {
  241. [file.id]: file
  242. })
  243. this.core.setState({ files })
  244. }
  245. onReceiveUploadUrl (file, uploadURL) {
  246. const currentFile = this.getFile(file.id)
  247. if (!currentFile) return
  248. // Only do the update if we didn't have an upload URL yet.
  249. if (!currentFile.tus || currentFile.tus.uploadUrl !== uploadURL) {
  250. const newFile = Object.assign({}, currentFile, {
  251. tus: Object.assign({}, currentFile.tus, {
  252. uploadUrl: uploadURL
  253. })
  254. })
  255. this.updateFile(newFile)
  256. }
  257. }
  258. onFileRemove (fileID, cb) {
  259. this.core.on('core:file-removed', (targetFileID) => {
  260. if (fileID === targetFileID) cb(targetFileID)
  261. })
  262. }
  263. onPause (fileID, cb) {
  264. this.core.on('core:upload-pause', (targetFileID) => {
  265. if (fileID === targetFileID) {
  266. const isPaused = this.pauseResume('toggle', fileID)
  267. cb(isPaused)
  268. }
  269. })
  270. }
  271. onPauseAll (fileID, cb) {
  272. this.core.on('core:pause-all', () => {
  273. if (!this.core.getFile(fileID)) return
  274. cb()
  275. })
  276. }
  277. onResumeAll (fileID, cb) {
  278. this.core.on('core:resume-all', () => {
  279. if (!this.core.getFile(fileID)) return
  280. cb()
  281. })
  282. }
  283. uploadFiles (files) {
  284. return settle(files.map((file, index) => {
  285. const current = parseInt(index, 10) + 1
  286. const total = files.length
  287. if (!file.isRemote) {
  288. return this.upload(file, current, total)
  289. } else {
  290. return this.uploadRemote(file, current, total)
  291. }
  292. }))
  293. }
  294. handleUpload (fileIDs) {
  295. if (fileIDs.length === 0) {
  296. this.core.log('Tus: no files to upload!')
  297. return Promise.resolve()
  298. }
  299. this.core.log('Tus is uploading...')
  300. const filesToUpload = fileIDs.map((fileID) => this.core.getFile(fileID))
  301. return this.uploadFiles(filesToUpload)
  302. }
  303. actions () {
  304. this.core.on('core:pause-all', this.handlePauseAll)
  305. this.core.on('core:resume-all', this.handleResumeAll)
  306. this.core.on('core:reset-progress', this.handleResetProgress)
  307. if (this.opts.autoRetry) {
  308. this.core.on('back-online', () => {
  309. this.core.emit('core:retry-started')
  310. })
  311. }
  312. }
  313. addResumableUploadsCapabilityFlag () {
  314. const newCapabilities = Object.assign({}, this.core.getState().capabilities)
  315. newCapabilities.resumableUploads = true
  316. this.core.setState({
  317. capabilities: newCapabilities
  318. })
  319. }
  320. install () {
  321. this.addResumableUploadsCapabilityFlag()
  322. this.core.addUploader(this.handleUpload)
  323. this.actions()
  324. }
  325. uninstall () {
  326. this.core.removeUploader(this.handleUpload)
  327. this.core.off('core:pause-all', this.handlePauseAll)
  328. this.core.off('core:resume-all', this.handleResumeAll)
  329. }
  330. }