index.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859
  1. const Translator = require('../../core/Translator')
  2. const Plugin = require('../../core/Plugin')
  3. const Tus = require('../Tus')
  4. const Client = require('./Client')
  5. const StatusSocket = require('./Socket')
  6. function defaultGetAssemblyOptions (file, options) {
  7. return {
  8. params: options.params,
  9. signature: options.signature,
  10. fields: options.fields
  11. }
  12. }
  13. const UPPY_SERVER = 'https://api2.transloadit.com/uppy-server'
  14. // Regex used to check if an uppy-server address is run by Transloadit.
  15. const TL_UPPY_SERVER = /https?:\/\/api2(?:-\w+)?\.transloadit\.com\/uppy-server/
  16. /**
  17. * Upload files to Transloadit using Tus.
  18. */
  19. module.exports = class Transloadit extends Plugin {
  20. constructor (uppy, opts) {
  21. super(uppy, opts)
  22. this.type = 'uploader'
  23. this.id = 'Transloadit'
  24. this.title = 'Transloadit'
  25. const defaultLocale = {
  26. strings: {
  27. creatingAssembly: 'Preparing upload...',
  28. creatingAssemblyFailed: 'Transloadit: Could not create Assembly',
  29. encoding: 'Encoding...'
  30. }
  31. }
  32. const defaultOptions = {
  33. service: 'https://api2.transloadit.com',
  34. waitForEncoding: false,
  35. waitForMetadata: false,
  36. alwaysRunAssembly: false,
  37. importFromUploadURLs: false,
  38. signature: null,
  39. params: null,
  40. fields: {},
  41. getAssemblyOptions: defaultGetAssemblyOptions,
  42. locale: defaultLocale
  43. }
  44. this.opts = Object.assign({}, defaultOptions, opts)
  45. this.locale = Object.assign({}, defaultLocale, this.opts.locale)
  46. this.locale.strings = Object.assign({}, defaultLocale.strings, this.opts.locale.strings)
  47. this.translator = new Translator({ locale: this.locale })
  48. this.i18n = this.translator.translate.bind(this.translator)
  49. this.prepareUpload = this.prepareUpload.bind(this)
  50. this.afterUpload = this.afterUpload.bind(this)
  51. this.handleError = this.handleError.bind(this)
  52. this.onFileUploadURLAvailable = this.onFileUploadURLAvailable.bind(this)
  53. this.onRestored = this.onRestored.bind(this)
  54. this.getPersistentData = this.getPersistentData.bind(this)
  55. if (this.opts.params) {
  56. this.validateParams(this.opts.params)
  57. }
  58. this.client = new Client({
  59. service: this.opts.service
  60. })
  61. this.sockets = {}
  62. }
  63. validateParams (params) {
  64. if (!params) {
  65. throw new Error('Transloadit: The `params` option is required.')
  66. }
  67. if (typeof params === 'string') {
  68. try {
  69. params = JSON.parse(params)
  70. } catch (err) {
  71. // Tell the user that this is not an Uppy bug!
  72. err.message = 'Transloadit: The `params` option is a malformed JSON string: ' +
  73. err.message
  74. throw err
  75. }
  76. }
  77. if (!params.auth || !params.auth.key) {
  78. throw new Error('Transloadit: The `params.auth.key` option is required. ' +
  79. 'You can find your Transloadit API key at https://transloadit.com/accounts/credentials.')
  80. }
  81. }
  82. getAssemblyOptions (fileIDs) {
  83. const options = this.opts
  84. const normalizeAssemblyOptions = (file, assemblyOptions) => {
  85. if (Array.isArray(assemblyOptions.fields)) {
  86. const fieldNames = assemblyOptions.fields
  87. assemblyOptions.fields = {}
  88. fieldNames.forEach((fieldName) => {
  89. assemblyOptions.fields[fieldName] = file.meta[fieldName]
  90. })
  91. }
  92. if (!assemblyOptions.fields) {
  93. assemblyOptions.fields = {}
  94. }
  95. return assemblyOptions
  96. }
  97. return Promise.all(
  98. fileIDs.map((fileID) => {
  99. const file = this.uppy.getFile(fileID)
  100. const promise = Promise.resolve()
  101. .then(() => options.getAssemblyOptions(file, options))
  102. .then((assemblyOptions) => normalizeAssemblyOptions(file, assemblyOptions))
  103. return promise.then((assemblyOptions) => {
  104. this.validateParams(assemblyOptions.params)
  105. return {
  106. fileIDs: [fileID],
  107. options: assemblyOptions
  108. }
  109. })
  110. })
  111. )
  112. }
  113. dedupeAssemblyOptions (list) {
  114. const dedupeMap = Object.create(null)
  115. list.forEach(({ fileIDs, options }) => {
  116. const id = JSON.stringify(options)
  117. if (dedupeMap[id]) {
  118. dedupeMap[id].fileIDs.push(...fileIDs)
  119. } else {
  120. dedupeMap[id] = {
  121. options,
  122. fileIDs: [...fileIDs]
  123. }
  124. }
  125. })
  126. return Object.keys(dedupeMap).map((id) => dedupeMap[id])
  127. }
  128. createAssembly (fileIDs, uploadID, options) {
  129. const pluginOptions = this.opts
  130. this.uppy.log('[Transloadit] create Assembly')
  131. return this.client.createAssembly({
  132. params: options.params,
  133. fields: options.fields,
  134. expectedFiles: fileIDs.length,
  135. signature: options.signature
  136. }).then((assembly) => {
  137. // Store the list of assemblies related to this upload.
  138. const state = this.getPluginState()
  139. const assemblyList = state.uploadsAssemblies[uploadID]
  140. const uploadsAssemblies = Object.assign({}, state.uploadsAssemblies, {
  141. [uploadID]: assemblyList.concat([ assembly.assembly_id ])
  142. })
  143. this.setPluginState({
  144. assemblies: Object.assign(state.assemblies, {
  145. [assembly.assembly_id]: assembly
  146. }),
  147. uploadsAssemblies
  148. })
  149. function attachAssemblyMetadata (file, assembly) {
  150. // Attach meta parameters for the Tus plugin. See:
  151. // https://github.com/tus/tusd/wiki/Uploading-to-Transloadit-using-tus#uploading-using-tus
  152. const tlMeta = {
  153. assembly_url: assembly.assembly_url,
  154. filename: file.name,
  155. fieldname: 'file'
  156. }
  157. const meta = Object.assign({}, file.meta, tlMeta)
  158. // Add assembly-specific Tus endpoint.
  159. const tus = Object.assign({}, file.tus, {
  160. endpoint: assembly.tus_url
  161. })
  162. // Set uppy server location. We only add this, if 'file' has the attribute
  163. // remote, because this is the criteria to identify remote files.
  164. // We only replace the hostname for Transloadit's uppy-servers, so that
  165. // people can self-host them while still using Transloadit for encoding.
  166. let remote = file.remote
  167. if (file.remote && TL_UPPY_SERVER.test(file.remote)) {
  168. let newHost = assembly.uppyserver_url
  169. let path = file.remote.url.replace(file.remote.host, '')
  170. // remove tailing slash
  171. newHost = newHost.replace(/\/$/, '')
  172. // remove leading slash
  173. path = path.replace(/^\//, '')
  174. remote = Object.assign({}, file.remote, {
  175. host: newHost,
  176. url: `${newHost}/${path}`
  177. })
  178. }
  179. const transloadit = {
  180. assembly: assembly.assembly_id
  181. }
  182. const newFile = Object.assign({}, file, { transloadit })
  183. // Only configure the Tus plugin if we are uploading straight to Transloadit (the default).
  184. if (!pluginOptions.importFromUploadURLs) {
  185. Object.assign(newFile, { meta, tus, remote })
  186. }
  187. return newFile
  188. }
  189. const files = Object.assign({}, this.uppy.getState().files)
  190. fileIDs.forEach((id) => {
  191. files[id] = attachAssemblyMetadata(files[id], assembly)
  192. })
  193. this.uppy.setState({ files })
  194. this.uppy.emit('transloadit:assembly-created', assembly, fileIDs)
  195. return this.connectSocket(assembly)
  196. .then(() => assembly)
  197. }).then((assembly) => {
  198. this.uppy.log(`[Transloadit] Created Assembly ${assembly.assembly_id}`)
  199. return assembly
  200. }).catch((err) => {
  201. this.uppy.info(this.i18n('creatingAssemblyFailed'), 'error', 0)
  202. // Reject the promise.
  203. throw err
  204. })
  205. }
  206. shouldWait () {
  207. return this.opts.waitForEncoding || this.opts.waitForMetadata
  208. }
  209. /**
  210. * Used when `importFromUploadURLs` is enabled: reserves all files in
  211. * the assembly.
  212. */
  213. reserveFiles (assembly, fileIDs) {
  214. return Promise.all(fileIDs.map((fileID) => {
  215. const file = this.uppy.getFile(fileID)
  216. return this.client.reserveFile(assembly, file)
  217. }))
  218. }
  219. /**
  220. * Used when `importFromUploadURLs` is enabled: adds files to the assembly
  221. * once they have been fully uploaded.
  222. */
  223. onFileUploadURLAvailable (file) {
  224. if (!file || !file.transloadit || !file.transloadit.assembly) {
  225. return
  226. }
  227. const state = this.getPluginState()
  228. const assembly = state.assemblies[file.transloadit.assembly]
  229. this.client.addFile(assembly, file).catch((err) => {
  230. this.uppy.log(err)
  231. this.uppy.emit('transloadit:import-error', assembly, file.id, err)
  232. })
  233. }
  234. findFile (uploadedFile) {
  235. const files = this.uppy.getFiles()
  236. for (let i = 0; i < files.length; i++) {
  237. const file = files[i]
  238. // Completed file upload.
  239. if (file.uploadURL === uploadedFile.tus_upload_url) {
  240. return file
  241. }
  242. // In-progress file upload.
  243. if (file.tus && file.tus.uploadUrl === uploadedFile.tus_upload_url) {
  244. return file
  245. }
  246. if (!uploadedFile.is_tus_file) {
  247. // Fingers-crossed check for non-tus uploads, eg imported from S3.
  248. if (file.name === uploadedFile.name && file.size === uploadedFile.size) {
  249. return file
  250. }
  251. }
  252. }
  253. }
  254. onFileUploadComplete (assemblyId, uploadedFile) {
  255. const state = this.getPluginState()
  256. const file = this.findFile(uploadedFile)
  257. if (!file) {
  258. this.uppy.log('[Transloadit] Couldn’t file the file, it was likely removed in the process')
  259. return
  260. }
  261. this.setPluginState({
  262. files: Object.assign({}, state.files, {
  263. [uploadedFile.id]: {
  264. assembly: assemblyId,
  265. id: file.id,
  266. uploadedFile
  267. }
  268. })
  269. })
  270. this.uppy.emit('transloadit:upload', uploadedFile, this.getAssembly(assemblyId))
  271. }
  272. onResult (assemblyId, stepName, result) {
  273. const state = this.getPluginState()
  274. const file = state.files[result.original_id]
  275. // The `file` may not exist if an import robot was used instead of a file upload.
  276. result.localId = file ? file.id : null
  277. const entry = {
  278. result,
  279. stepName,
  280. id: result.id,
  281. assembly: assemblyId
  282. }
  283. this.setPluginState({
  284. results: [...state.results, entry]
  285. })
  286. this.uppy.emit('transloadit:result', stepName, result, this.getAssembly(assemblyId))
  287. }
  288. onAssemblyFinished (url) {
  289. this.client.getAssemblyStatus(url).then((assembly) => {
  290. const state = this.getPluginState()
  291. this.setPluginState({
  292. assemblies: Object.assign({}, state.assemblies, {
  293. [assembly.assembly_id]: assembly
  294. })
  295. })
  296. this.uppy.emit('transloadit:complete', assembly)
  297. })
  298. }
  299. getPersistentData (setData) {
  300. const state = this.getPluginState()
  301. const assemblies = state.assemblies
  302. const uploadsAssemblies = state.uploadsAssemblies
  303. const uploads = Object.keys(state.files)
  304. const results = state.results.map((result) => result.id)
  305. setData({
  306. [this.id]: {
  307. assemblies,
  308. uploadsAssemblies,
  309. uploads,
  310. results
  311. }
  312. })
  313. }
  314. /**
  315. * Emit the necessary events that must have occured to get from the `prevState`,
  316. * to the current state.
  317. * For completed uploads, `transloadit:upload` is emitted.
  318. * For new results, `transloadit:result` is emitted.
  319. * For completed or errored assemblies, `transloadit:complete` or `transloadit:assembly-error` is emitted.
  320. */
  321. emitEventsDiff (prevState) {
  322. const opts = this.opts
  323. const state = this.getPluginState()
  324. const emitMissedEvents = () => {
  325. // Emit events for completed uploads and completed results
  326. // that we've missed while we were away.
  327. const newUploads = Object.keys(state.files).filter((fileID) => {
  328. return !prevState.files.hasOwnProperty(fileID)
  329. }).map((fileID) => state.files[fileID])
  330. const newResults = state.results.filter((result) => {
  331. return !prevState.results.some((prev) => prev.id === result.id)
  332. })
  333. this.uppy.log('[Transloadit] New fully uploaded files since restore:')
  334. this.uppy.log(newUploads)
  335. newUploads.forEach(({ assembly, uploadedFile }) => {
  336. this.uppy.log(`[Transloadit] emitting transloadit:upload ${uploadedFile.id}`)
  337. this.uppy.emit('transloadit:upload', uploadedFile, this.getAssembly(assembly))
  338. })
  339. this.uppy.log('[Transloadit] New results since restore:')
  340. this.uppy.log(newResults)
  341. newResults.forEach(({ assembly, stepName, result, id }) => {
  342. this.uppy.log(`[Transloadit] emitting transloadit:result ${stepName}, ${id}`)
  343. this.uppy.emit('transloadit:result', stepName, result, this.getAssembly(assembly))
  344. })
  345. const newAssemblies = state.assemblies
  346. const previousAssemblies = prevState.assemblies
  347. this.uppy.log('[Transloadit] Current Assembly status after restore')
  348. this.uppy.log(newAssemblies)
  349. this.uppy.log('[Transloadit] Assembly status before restore')
  350. this.uppy.log(previousAssemblies)
  351. Object.keys(newAssemblies).forEach((assemblyId) => {
  352. const oldAssembly = previousAssemblies[assemblyId]
  353. diffAssemblyStatus(oldAssembly, newAssemblies[assemblyId])
  354. })
  355. }
  356. // Emit events for assemblies that have completed or errored while we were away.
  357. const diffAssemblyStatus = (prev, next) => {
  358. this.uppy.log('[Transloadit] Diff assemblies')
  359. this.uppy.log(prev)
  360. this.uppy.log(next)
  361. if (opts.waitForEncoding && next.ok === 'ASSEMBLY_COMPLETED' && prev.ok !== 'ASSEMBLY_COMPLETED') {
  362. this.uppy.log(`[Transloadit] Emitting transloadit:complete for ${next.assembly_id}`)
  363. this.uppy.log(next)
  364. this.uppy.emit('transloadit:complete', next)
  365. } else if (opts.waitForMetadata && next.upload_meta_data_extracted && !prev.upload_meta_data_extracted) {
  366. this.uppy.log(`[Transloadit] Emitting transloadit:complete after metadata extraction for ${next.assembly_id}`)
  367. this.uppy.log(next)
  368. this.uppy.emit('transloadit:complete', next)
  369. }
  370. if (next.error && !prev.error) {
  371. this.uppy.log(`[Transloadit] !!! Emitting transloadit:assembly-error for ${next.assembly_id}`)
  372. this.uppy.log(next)
  373. this.uppy.emit('transloadit:assembly-error', next, new Error(next.message))
  374. }
  375. }
  376. emitMissedEvents()
  377. }
  378. onRestored (pluginData) {
  379. const savedState = pluginData && pluginData[this.id] ? pluginData[this.id] : {}
  380. const knownUploads = savedState.files || []
  381. const knownResults = savedState.results || []
  382. const previousAssemblies = savedState.assemblies || {}
  383. const uploadsAssemblies = savedState.uploadsAssemblies || {}
  384. if (Object.keys(uploadsAssemblies).length === 0) {
  385. // Nothing to restore.
  386. return
  387. }
  388. // Fetch up-to-date assembly statuses.
  389. const loadAssemblies = () => {
  390. const assemblyIDs = []
  391. Object.keys(uploadsAssemblies).forEach((uploadID) => {
  392. assemblyIDs.push(...uploadsAssemblies[uploadID])
  393. })
  394. return Promise.all(
  395. assemblyIDs.map((assemblyID) => {
  396. const url = `https://api2.transloadit.com/assemblies/${assemblyID}`
  397. return this.client.getAssemblyStatus(url)
  398. })
  399. )
  400. }
  401. const reconnectSockets = (assemblies) => {
  402. return Promise.all(assemblies.map((assembly) => {
  403. // No need to connect to the socket if the assembly has completed by now.
  404. if (assembly.ok === 'ASSEMBLY_COMPLETE') {
  405. return null
  406. }
  407. return this.connectSocket(assembly)
  408. }))
  409. }
  410. // Convert loaded assembly statuses to a Transloadit plugin state object.
  411. const restoreState = (assemblies) => {
  412. const assembliesById = {}
  413. const files = {}
  414. const results = []
  415. assemblies.forEach((assembly) => {
  416. assembliesById[assembly.assembly_id] = assembly
  417. assembly.uploads.forEach((uploadedFile) => {
  418. const file = this.findFile(uploadedFile)
  419. files[uploadedFile.id] = {
  420. id: file.id,
  421. assembly: assembly.assembly_id,
  422. uploadedFile
  423. }
  424. })
  425. const state = this.getPluginState()
  426. Object.keys(assembly.results).forEach((stepName) => {
  427. assembly.results[stepName].forEach((result) => {
  428. const file = state.files[result.original_id]
  429. result.localId = file ? file.id : null
  430. results.push({
  431. id: result.id,
  432. result,
  433. stepName,
  434. assembly: assembly.assembly_id
  435. })
  436. })
  437. })
  438. })
  439. this.setPluginState({
  440. assemblies: assembliesById,
  441. files: files,
  442. results: results,
  443. uploadsAssemblies: uploadsAssemblies
  444. })
  445. }
  446. // Restore all assembly state.
  447. this.restored = Promise.resolve()
  448. .then(loadAssemblies)
  449. .then((assemblies) => {
  450. restoreState(assemblies)
  451. return reconnectSockets(assemblies)
  452. })
  453. .then(() => {
  454. // Return a callback that will be called by `afterUpload`
  455. // once it has attached event listeners etc.
  456. const newState = this.getPluginState()
  457. const previousFiles = {}
  458. knownUploads.forEach((id) => {
  459. previousFiles[id] = newState.files[id]
  460. })
  461. return () => this.emitEventsDiff({
  462. assemblies: previousAssemblies,
  463. files: previousFiles,
  464. results: newState.results.filter(({ id }) => knownResults.indexOf(id) !== -1),
  465. uploadsAssemblies
  466. })
  467. })
  468. this.restored.then(() => {
  469. this.restored = null
  470. })
  471. }
  472. connectSocket (assembly) {
  473. const socket = new StatusSocket(
  474. assembly.websocket_url,
  475. assembly
  476. )
  477. this.sockets[assembly.assembly_id] = socket
  478. socket.on('upload', this.onFileUploadComplete.bind(this, assembly.assembly_id))
  479. socket.on('error', (error) => {
  480. this.uppy.emit('transloadit:assembly-error', assembly, error)
  481. })
  482. socket.on('executing', () => {
  483. this.uppy.emit('transloadit:assembly-executing', assembly)
  484. })
  485. if (this.opts.waitForEncoding) {
  486. socket.on('result', this.onResult.bind(this, assembly.assembly_id))
  487. }
  488. if (this.opts.waitForEncoding) {
  489. socket.on('finished', () => {
  490. this.onAssemblyFinished(assembly.assembly_ssl_url)
  491. })
  492. } else if (this.opts.waitForMetadata) {
  493. socket.on('metadata', () => {
  494. this.onAssemblyFinished(assembly.assembly_ssl_url)
  495. })
  496. }
  497. return new Promise((resolve, reject) => {
  498. socket.on('connect', resolve)
  499. socket.on('error', reject)
  500. }).then(() => {
  501. this.uppy.log('[Transloadit] Socket is ready')
  502. })
  503. }
  504. prepareUpload (fileIDs, uploadID) {
  505. // Only use files without errors
  506. fileIDs = fileIDs.filter((file) => !file.error)
  507. fileIDs.forEach((fileID) => {
  508. const file = this.uppy.getFile(fileID)
  509. this.uppy.emit('preprocess-progress', file, {
  510. mode: 'indeterminate',
  511. message: this.i18n('creatingAssembly')
  512. })
  513. })
  514. const createAssembly = ({ fileIDs, options }) => {
  515. return this.createAssembly(fileIDs, uploadID, options).then((assembly) => {
  516. if (this.opts.importFromUploadURLs) {
  517. return this.reserveFiles(assembly, fileIDs)
  518. }
  519. }).then(() => {
  520. fileIDs.forEach((fileID) => {
  521. const file = this.uppy.getFile(fileID)
  522. this.uppy.emit('preprocess-complete', file)
  523. })
  524. }).catch((err) => {
  525. // Clear preprocessing state when the assembly could not be created,
  526. // otherwise the UI gets confused about the lingering progress keys
  527. fileIDs.forEach((fileID) => {
  528. const file = this.uppy.getFile(fileID)
  529. this.uppy.emit('preprocess-complete', file)
  530. this.uppy.emit('upload-error', file, err)
  531. })
  532. throw err
  533. })
  534. }
  535. const state = this.getPluginState()
  536. const uploadsAssemblies = Object.assign({},
  537. state.uploadsAssemblies,
  538. { [uploadID]: [] })
  539. this.setPluginState({ uploadsAssemblies })
  540. let optionsPromise
  541. if (fileIDs.length > 0) {
  542. optionsPromise = Promise.resolve(this.getAssemblyOptions(fileIDs))
  543. .then((allOptions) => this.dedupeAssemblyOptions(allOptions))
  544. } else if (this.opts.alwaysRunAssembly) {
  545. optionsPromise = Promise.resolve(
  546. this.opts.getAssemblyOptions(null, this.opts)
  547. ).then((options) => {
  548. this.validateParams(options.params)
  549. return [
  550. { fileIDs, options }
  551. ]
  552. })
  553. } else {
  554. // If there are no files and we do not `alwaysRunAssembly`,
  555. // don't do anything.
  556. return Promise.resolve()
  557. }
  558. return optionsPromise.then(
  559. (assemblies) => Promise.all(
  560. assemblies.map(createAssembly)
  561. ),
  562. // If something went wrong before any assemblies could be created,
  563. // clear all processing state.
  564. (err) => {
  565. fileIDs.forEach((fileID) => {
  566. const file = this.uppy.getFile(fileID)
  567. this.uppy.emit('preprocess-complete', file)
  568. this.uppy.emit('upload-error', file, err)
  569. })
  570. throw err
  571. }
  572. )
  573. }
  574. afterUpload (fileIDs, uploadID) {
  575. // Only use files without errors
  576. fileIDs = fileIDs.filter((file) => !file.error)
  577. const state = this.getPluginState()
  578. // If we're still restoring state, wait for that to be done.
  579. if (this.restored) {
  580. return this.restored.then((emitMissedEvents) => {
  581. const promise = this.afterUpload(fileIDs, uploadID)
  582. emitMissedEvents()
  583. return promise
  584. })
  585. }
  586. const assemblyIDs = state.uploadsAssemblies[uploadID]
  587. // If we don't have to wait for encoding metadata or results, we can close
  588. // the socket immediately and finish the upload.
  589. if (!this.shouldWait()) {
  590. assemblyIDs.forEach((assemblyID) => {
  591. const socket = this.sockets[assemblyID]
  592. socket.close()
  593. })
  594. const assemblies = assemblyIDs.map((id) => this.getAssembly(id))
  595. this.uppy.addResultData(uploadID, { transloadit: assemblies })
  596. return Promise.resolve()
  597. }
  598. // If no assemblies were created for this upload, we also do not have to wait.
  599. // There's also no sockets or anything to close, so just return immediately.
  600. if (assemblyIDs.length === 0) {
  601. this.uppy.addResultData(uploadID, { transloadit: [] })
  602. return Promise.resolve()
  603. }
  604. let finishedAssemblies = 0
  605. return new Promise((resolve, reject) => {
  606. fileIDs.forEach((fileID) => {
  607. const file = this.uppy.getFile(fileID)
  608. this.uppy.emit('postprocess-progress', file, {
  609. mode: 'indeterminate',
  610. message: this.i18n('encoding')
  611. })
  612. })
  613. const onAssemblyFinished = (assembly) => {
  614. // An assembly for a different upload just finished. We can ignore it.
  615. if (assemblyIDs.indexOf(assembly.assembly_id) === -1) {
  616. this.uppy.log(`[Transloadit] afterUpload(): Ignoring finished Assembly ${assembly.assembly_id}`)
  617. return
  618. }
  619. this.uppy.log(`[Transloadit] afterUpload(): Got Assembly finish ${assembly.assembly_id}`)
  620. // TODO set the `file.uploadURL` to a result?
  621. // We will probably need an option here so the plugin user can tell us
  622. // which result to pick…?
  623. const files = this.getAssemblyFiles(assembly.assembly_id)
  624. files.forEach((file) => {
  625. this.uppy.emit('postprocess-complete', file)
  626. })
  627. checkAllComplete()
  628. }
  629. const onAssemblyError = (assembly, error) => {
  630. // An assembly for a different upload just errored. We can ignore it.
  631. if (assemblyIDs.indexOf(assembly.assembly_id) === -1) {
  632. this.uppy.log(`[Transloadit] afterUpload(): Ignoring errored Assembly ${assembly.assembly_id}`)
  633. return
  634. }
  635. this.uppy.log(`[Transloadit] afterUpload(): Got Assembly error ${assembly.assembly_id}`)
  636. this.uppy.log(error)
  637. // Clear postprocessing state for all our files.
  638. const files = this.getAssemblyFiles(assembly.assembly_id)
  639. files.forEach((file) => {
  640. // TODO Maybe make a postprocess-error event here?
  641. this.uppy.emit('upload-error', file, error)
  642. this.uppy.emit('postprocess-complete', file)
  643. })
  644. checkAllComplete()
  645. }
  646. const onImportError = (assembly, fileID, error) => {
  647. if (assemblyIDs.indexOf(assembly.assembly_id) === -1) {
  648. return
  649. }
  650. // Not sure if we should be doing something when it's just one file failing.
  651. // ATM, the only options are 1) ignoring or 2) failing the entire upload.
  652. // I think failing the upload is better than silently ignoring.
  653. // In the future we should maybe have a way to resolve uploads with some failures,
  654. // like returning an object with `{ successful, failed }` uploads.
  655. onAssemblyError(assembly, error)
  656. }
  657. const checkAllComplete = () => {
  658. finishedAssemblies += 1
  659. if (finishedAssemblies === assemblyIDs.length) {
  660. // We're done, these listeners can be removed
  661. removeListeners()
  662. const assemblies = assemblyIDs.map((id) => this.getAssembly(id))
  663. this.uppy.addResultData(uploadID, { transloadit: assemblies })
  664. resolve()
  665. }
  666. }
  667. const removeListeners = () => {
  668. this.uppy.off('transloadit:complete', onAssemblyFinished)
  669. this.uppy.off('transloadit:assembly-error', onAssemblyError)
  670. this.uppy.off('transloadit:import-error', onImportError)
  671. }
  672. this.uppy.on('transloadit:complete', onAssemblyFinished)
  673. this.uppy.on('transloadit:assembly-error', onAssemblyError)
  674. this.uppy.on('transloadit:import-error', onImportError)
  675. }).then((result) => {
  676. // Clean up uploadID → assemblyIDs, they're no longer going to be used anywhere.
  677. const state = this.getPluginState()
  678. const uploadsAssemblies = Object.assign({}, state.uploadsAssemblies)
  679. delete uploadsAssemblies[uploadID]
  680. this.setPluginState({ uploadsAssemblies })
  681. return result
  682. })
  683. }
  684. handleError (err, uploadID) {
  685. this.uppy.log('[Transloadit] handleError')
  686. this.uppy.log(err)
  687. this.uppy.log(uploadID)
  688. const state = this.getPluginState()
  689. const assemblyIDs = state.uploadsAssemblies[uploadID]
  690. assemblyIDs.forEach((assemblyID) => {
  691. if (this.sockets[assemblyID]) {
  692. this.sockets[assemblyID].close()
  693. }
  694. })
  695. }
  696. install () {
  697. this.uppy.addPreProcessor(this.prepareUpload)
  698. this.uppy.addPostProcessor(this.afterUpload)
  699. // We may need to close socket.io connections on error.
  700. this.uppy.on('error', this.handleError)
  701. if (this.opts.importFromUploadURLs) {
  702. // No uploader needed when importing; instead we take the upload URL from an existing uploader.
  703. this.uppy.on('upload-success', this.onFileUploadURLAvailable)
  704. } else {
  705. this.uppy.use(Tus, {
  706. // Disable tus-js-client fingerprinting, otherwise uploading the same file at different times
  707. // will upload to the same assembly.
  708. resume: false,
  709. // Disable Uppy Server's retry optimisation; we need to change the endpoint on retry
  710. // so it can't just reuse the same tus.Upload instance server-side.
  711. useFastRemoteRetry: false,
  712. // Only send assembly metadata to the tus endpoint.
  713. metaFields: ['assembly_url', 'filename', 'fieldname']
  714. })
  715. }
  716. this.uppy.on('restore:get-data', this.getPersistentData)
  717. this.uppy.on('restored', this.onRestored)
  718. this.setPluginState({
  719. // Contains assembly status objects, indexed by their ID.
  720. assemblies: {},
  721. // Contains arrays of assembly IDs, indexed by the upload ID that they belong to.
  722. uploadsAssemblies: {},
  723. // Contains file data from Transloadit, indexed by their Transloadit-assigned ID.
  724. files: {},
  725. // Contains result data from Transloadit.
  726. results: []
  727. })
  728. }
  729. uninstall () {
  730. this.uppy.removePreProcessor(this.prepareUpload)
  731. this.uppy.removePostProcessor(this.afterUpload)
  732. this.uppy.off('error', this.handleError)
  733. if (this.opts.importFromUploadURLs) {
  734. this.uppy.off('upload-success', this.onFileUploadURLAvailable)
  735. }
  736. }
  737. getAssembly (id) {
  738. const state = this.getPluginState()
  739. return state.assemblies[id]
  740. }
  741. getAssemblyFiles (assemblyID) {
  742. return this.uppy.getFiles().filter((file) => {
  743. return file && file.transloadit && file.transloadit.assembly === assemblyID
  744. })
  745. }
  746. }
  747. module.exports.UPPY_SERVER = UPPY_SERVER