index.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. const Plugin = require('../Plugin')
  2. const Client = require('./Client')
  3. const StatusSocket = require('./Socket')
  4. /**
  5. * Upload files to Transloadit using Tus.
  6. */
  7. module.exports = class Transloadit extends Plugin {
  8. constructor (core, opts) {
  9. super(core, opts)
  10. this.type = 'uploader'
  11. this.id = 'Transloadit'
  12. this.title = 'Transloadit'
  13. const defaultLocale = {
  14. strings: {
  15. creatingAssembly: 'Preparing upload...',
  16. creatingAssemblyFailed: 'Transloadit: Could not create assembly',
  17. encoding: 'Encoding...'
  18. }
  19. }
  20. const defaultOptions = {
  21. waitForEncoding: false,
  22. waitForMetadata: false,
  23. signature: null,
  24. params: null,
  25. fields: {},
  26. locale: defaultLocale
  27. }
  28. this.opts = Object.assign({}, defaultOptions, opts)
  29. this.locale = Object.assign({}, defaultLocale, this.opts.locale)
  30. this.locale.strings = Object.assign({}, defaultLocale.strings, this.opts.locale.strings)
  31. this.prepareUpload = this.prepareUpload.bind(this)
  32. this.afterUpload = this.afterUpload.bind(this)
  33. if (!this.opts.params) {
  34. throw new Error('Transloadit: The `params` option is required.')
  35. }
  36. let params = this.opts.params
  37. if (typeof params === 'string') {
  38. try {
  39. params = JSON.parse(params)
  40. } catch (err) {
  41. // Tell the user that this is not an Uppy bug!
  42. err.message = 'Transloadit: The `params` option is a malformed JSON string: ' +
  43. err.message
  44. throw err
  45. }
  46. }
  47. if (!params.auth || !params.auth.key) {
  48. throw new Error('Transloadit: The `params.auth.key` option is required. ' +
  49. 'You can find your Transloadit API key at https://transloadit.com/accounts/credentials.')
  50. }
  51. this.client = new Client()
  52. }
  53. createAssembly () {
  54. this.core.log('Transloadit: create assembly')
  55. const files = this.core.state.files
  56. const expectedFiles = Object.keys(files).reduce((count, fileID) => {
  57. if (!files[fileID].progress.uploadStarted || files[fileID].isRemote) {
  58. return count + 1
  59. }
  60. return count
  61. }, 0)
  62. return this.client.createAssembly({
  63. params: this.opts.params,
  64. fields: this.opts.fields,
  65. expectedFiles,
  66. signature: this.opts.signature
  67. }).then((assembly) => {
  68. this.updateState({ assembly })
  69. function attachAssemblyMetadata (file, assembly) {
  70. // Attach meta parameters for the Tus plugin. See:
  71. // https://github.com/tus/tusd/wiki/Uploading-to-Transloadit-using-tus#uploading-using-tus
  72. // TODO Should this `meta` be moved to a `tus.meta` property instead?
  73. // If the MetaData plugin can add eg. resize parameters, it doesn't
  74. // make much sense to set those as upload-metadata for tus.
  75. const meta = Object.assign({}, file.meta, {
  76. assembly_url: assembly.assembly_url,
  77. filename: file.name,
  78. fieldname: 'file'
  79. })
  80. // Add assembly-specific Tus endpoint.
  81. const tus = Object.assign({}, file.tus, {
  82. endpoint: assembly.tus_url
  83. })
  84. return Object.assign(
  85. {},
  86. file,
  87. { meta, tus }
  88. )
  89. }
  90. const filesObj = this.core.state.files
  91. const files = {}
  92. Object.keys(filesObj).forEach((id) => {
  93. files[id] = attachAssemblyMetadata(filesObj[id], assembly)
  94. })
  95. this.core.setState({ files })
  96. return this.connectSocket()
  97. }).then(() => {
  98. this.core.log('Transloadit: Created assembly')
  99. }).catch((err) => {
  100. this.core.emit('informer', this.opts.locale.strings.creatingAssemblyFailed, 'error', 0)
  101. // Reject the promise.
  102. throw err
  103. })
  104. }
  105. shouldWait () {
  106. return this.opts.waitForEncoding || this.opts.waitForMetadata
  107. }
  108. // TODO if/when the transloadit API returns tus upload metadata in the
  109. // file objects in the assembly status, change this to use a unique ID
  110. // instead of checking the file name and size.
  111. findFile ({ name, size }) {
  112. const files = this.core.state.files
  113. for (const id in files) {
  114. if (!files.hasOwnProperty(id)) {
  115. continue
  116. }
  117. if (files[id].name === name && files[id].size === size) {
  118. return files[id]
  119. }
  120. }
  121. }
  122. onFileUploadComplete (uploadedFile) {
  123. const file = this.findFile(uploadedFile)
  124. this.updateState({
  125. files: Object.assign({}, this.state.files, {
  126. [uploadedFile.id]: {
  127. id: file.id,
  128. uploadedFile
  129. }
  130. })
  131. })
  132. this.core.bus.emit('transloadit:upload', uploadedFile)
  133. }
  134. onResult (stepName, result) {
  135. const file = this.state.files[result.original_id]
  136. // The `file` may not exist if an import robot was used instead of a file upload.
  137. result.localId = file ? file.id : null
  138. this.updateState({
  139. results: this.state.results.concat(result)
  140. })
  141. this.core.bus.emit('transloadit:result', stepName, result)
  142. }
  143. connectSocket () {
  144. this.socket = new StatusSocket(
  145. this.state.assembly.websocket_url,
  146. this.state.assembly
  147. )
  148. this.socket.on('upload', this.onFileUploadComplete.bind(this))
  149. if (this.opts.waitForEncoding) {
  150. this.socket.on('result', this.onResult.bind(this))
  151. }
  152. this.assemblyReady = new Promise((resolve, reject) => {
  153. if (this.opts.waitForEncoding) {
  154. this.socket.on('finished', resolve)
  155. } else if (this.opts.waitForMetadata) {
  156. this.socket.on('metadata', resolve)
  157. }
  158. this.socket.on('error', reject)
  159. })
  160. return new Promise((resolve, reject) => {
  161. this.socket.on('connect', resolve)
  162. this.socket.on('error', reject)
  163. }).then(() => {
  164. this.core.log('Transloadit: Socket is ready')
  165. })
  166. }
  167. prepareUpload () {
  168. this.core.emit('informer', this.opts.locale.strings.creatingAssembly, 'info', 0)
  169. return this.createAssembly().then(() => {
  170. this.core.emit('informer:hide')
  171. })
  172. }
  173. afterUpload () {
  174. // If we don't have to wait for encoding metadata or results, we can close
  175. // the socket immediately and finish the upload.
  176. if (!this.shouldWait()) {
  177. this.socket.close()
  178. return
  179. }
  180. this.core.emit('informer', this.opts.locale.strings.encoding, 'info', 0)
  181. return this.assemblyReady.then(() => {
  182. return this.client.getAssemblyStatus(this.state.assembly.assembly_ssl_url)
  183. }).then((assembly) => {
  184. this.updateState({ assembly })
  185. // TODO set the `file.uploadURL` to a result?
  186. // We will probably need an option here so the plugin user can tell us
  187. // which result to pick…?
  188. this.core.emit('informer:hide')
  189. }).catch((err) => {
  190. // Always hide the Informer
  191. this.core.emit('informer:hide')
  192. throw err
  193. })
  194. }
  195. install () {
  196. this.core.addPreProcessor(this.prepareUpload)
  197. this.core.addPostProcessor(this.afterUpload)
  198. this.updateState({
  199. assembly: null,
  200. files: {},
  201. results: []
  202. })
  203. }
  204. uninstall () {
  205. this.core.removePreProcessor(this.prepareUpload)
  206. this.core.removePostProcessor(this.afterUpload)
  207. }
  208. get state () {
  209. return this.core.state.transloadit || {}
  210. }
  211. updateState (newState) {
  212. const transloadit = Object.assign({}, this.state, newState)
  213. this.core.setState({ transloadit })
  214. }
  215. }