Quellcode durchsuchen

Merge pull request #250 from goto-bus-stop/feature/tl-dynamic-params

Dynamic Transloadit assembly options
Artur Paikin vor 7 Jahren
Ursprung
Commit
2d112167f3
2 geänderte Dateien mit 193 neuen und 31 gelöschten Zeilen
  1. 78 27
      src/plugins/Transloadit/index.js
  2. 115 4
      test/unit/Transloadit.spec.js

+ 78 - 27
src/plugins/Transloadit/index.js

@@ -26,6 +26,13 @@ module.exports = class Transloadit extends Plugin {
       signature: null,
       params: null,
       fields: {},
+      getAssemblyOptions (file, options) {
+        return {
+          params: options.params,
+          signature: options.signature,
+          fields: options.fields
+        }
+      },
       locale: defaultLocale
     }
 
@@ -37,11 +44,19 @@ module.exports = class Transloadit extends Plugin {
     this.prepareUpload = this.prepareUpload.bind(this)
     this.afterUpload = this.afterUpload.bind(this)
 
-    if (!this.opts.params) {
+    if (this.opts.params) {
+      this.validateParams(this.opts.params)
+    }
+
+    this.client = new Client()
+    this.sockets = {}
+  }
+
+  validateParams (params) {
+    if (!params) {
       throw new Error('Transloadit: The `params` option is required.')
     }
 
-    let params = this.opts.params
     if (typeof params === 'string') {
       try {
         params = JSON.parse(params)
@@ -57,19 +72,51 @@ module.exports = class Transloadit extends Plugin {
       throw new Error('Transloadit: The `params.auth.key` option is required. ' +
         'You can find your Transloadit API key at https://transloadit.com/accounts/credentials.')
     }
+  }
 
-    this.client = new Client()
-    this.sockets = {}
+  getAssemblyOptions (fileIDs) {
+    const options = this.opts
+    return Promise.all(
+      fileIDs.map((fileID) => {
+        const file = this.getFile(fileID)
+        const promise = Promise.resolve(options.getAssemblyOptions(file, options))
+        return promise.then((assemblyOptions) => {
+          this.validateParams(assemblyOptions.params)
+
+          return {
+            fileIDs: [fileID],
+            options: assemblyOptions
+          }
+        })
+      })
+    )
   }
 
-  createAssembly (filesToUpload) {
+  dedupeAssemblyOptions (list) {
+    const dedupeMap = Object.create(null)
+    list.forEach(({ fileIDs, options }) => {
+      const id = JSON.stringify(options)
+      if (dedupeMap[id]) {
+        dedupeMap[id].fileIDs.push(...fileIDs)
+      } else {
+        dedupeMap[id] = {
+          options,
+          fileIDs: [...fileIDs]
+        }
+      }
+    })
+
+    return Object.keys(dedupeMap).map((id) => dedupeMap[id])
+  }
+
+  createAssembly (fileIDs, options) {
     this.core.log('Transloadit: create assembly')
 
     return this.client.createAssembly({
-      params: this.opts.params,
-      fields: this.opts.fields,
-      expectedFiles: Object.keys(filesToUpload).length,
-      signature: this.opts.signature
+      params: options.params,
+      fields: options.fields,
+      expectedFiles: fileIDs.length,
+      signature: options.signature
     }).then((assembly) => {
       this.updateState({
         assemblies: Object.assign(this.state.assemblies, {
@@ -103,13 +150,13 @@ module.exports = class Transloadit extends Plugin {
       }
 
       const files = Object.assign({}, this.core.state.files)
-      Object.keys(filesToUpload).forEach((id) => {
+      fileIDs.forEach((id) => {
         files[id] = attachAssemblyMetadata(files[id], assembly)
       })
 
       this.core.setState({ files })
 
-      this.core.emit('transloadit:assembly', assembly, Object.keys(filesToUpload))
+      this.core.emit('transloadit:assembly', assembly, fileIDs)
 
       return this.connectSocket(assembly)
     }).then(() => {
@@ -208,27 +255,31 @@ module.exports = class Transloadit extends Plugin {
     })
   }
 
-  prepareUpload (fileIDs) {
-    const filesToUpload = fileIDs.map(getFile, this).reduce(intoFileMap, {})
-    function getFile (fileID) {
-      return this.core.state.files[fileID]
-    }
-    function intoFileMap (map, file) {
-      map[file.id] = file
-      return map
-    }
+  getFile (fileID) {
+    return this.core.state.files[fileID]
+  }
 
+  prepareUpload (fileIDs) {
     fileIDs.forEach((fileID) => {
       this.core.emit('core:preprocess-progress', fileID, {
         mode: 'indeterminate',
         message: this.opts.locale.strings.creatingAssembly
       })
     })
-    return this.createAssembly(filesToUpload).then(() => {
-      fileIDs.forEach((fileID) => {
-        this.core.emit('core:preprocess-complete', fileID)
+
+    const createAssembly = ({ fileIDs, options }) => {
+      return this.createAssembly(fileIDs, options).then(() => {
+        fileIDs.forEach((fileID) => {
+          this.core.emit('core:preprocess-complete', fileID)
+        })
       })
-    })
+    }
+
+    return this.getAssemblyOptions(fileIDs)
+      .then((allOptions) => this.dedupeAssemblyOptions(allOptions))
+      .then((assemblies) => Promise.all(
+        assemblies.map(createAssembly)
+      ))
   }
 
   afterUpload (fileIDs) {
@@ -238,7 +289,7 @@ module.exports = class Transloadit extends Plugin {
     // If we don't have to wait for encoding metadata or results, we can close
     // the socket immediately and finish the upload.
     if (!this.shouldWait()) {
-      const file = this.core.getState().files[fileID]
+      const file = this.getFile(fileID)
       const socket = this.socket[file.assembly]
       socket.close()
       return
@@ -253,7 +304,7 @@ module.exports = class Transloadit extends Plugin {
       })
 
       const onAssemblyFinished = (assembly) => {
-        const file = this.core.state.files[fileID]
+        const file = this.getFile(fileID)
         // An assembly for a different upload just finished. We can ignore it.
         if (assembly.assembly_id !== file.transloadit.assembly) {
           return
@@ -273,7 +324,7 @@ module.exports = class Transloadit extends Plugin {
       }
 
       const onAssemblyError = (assembly, error) => {
-        const file = this.core.state.files[fileID]
+        const file = this.getFile(fileID)
         // An assembly for a different upload just errored. We can ignore it.
         if (assembly.assembly_id !== file.transloadit.assembly) {
           return

+ 115 - 4
test/unit/Transloadit.spec.js

@@ -5,10 +5,6 @@ import Transloadit from '../../src/plugins/Transloadit'
 test('Transloadit: Throws errors if options are missing', (t) => {
   const uppy = new Core()
 
-  t.throws(() => {
-    uppy.use(Transloadit, {})
-  }, /The `params` option is required/)
-
   t.throws(() => {
     uppy.use(Transloadit, { params: {} })
   }, /The `params\.auth\.key` option is required/)
@@ -38,3 +34,118 @@ test('Transloadit: Accepts a JSON string as `params` for signature authenticatio
 
   t.end()
 })
+
+test('Transloadit: Validates response from getAssemblyOptions()', (t) => {
+  const uppy = new Core({ autoProceed: false })
+
+  uppy.use(Transloadit, {
+    getAssemblyOptions: (file) => {
+      t.equal(file.name, 'testfile')
+      return {
+        params: '{"some":"json"}'
+      }
+    }
+  })
+
+  const data = Buffer.alloc(4000)
+  data.size = data.byteLength
+  uppy.addFile({
+    name: 'testfile',
+    data
+  }).then(() => {
+    uppy.upload().then(t.fail, (err) => {
+      t.ok(/The `params\.auth\.key` option is required/.test(err.message), 'should reject invalid dynamic params')
+      t.end()
+    })
+  }, t.fail)
+})
+
+test('Transloadit: Uses different assemblies for different params', (t) => {
+  t.plan(5)
+
+  const uppy = new Core({ autoProceed: false })
+
+  uppy.use(Transloadit, {
+    getAssemblyOptions: (file) => ({
+      params: {
+        auth: { key: 'fake key' },
+        steps: {
+          fake_step: { data: file.name }
+        }
+      }
+    })
+  })
+
+  const tl = uppy.getPlugin('Transloadit')
+  const files = ['a.png', 'b.png', 'c.png', 'd.png']
+  let i = 0
+  tl.client.createAssembly = (opts) => {
+    t.equal(opts.params.steps.fake_step.data, files[i], `assembly for file ${files[i]}`)
+    i++
+    // Short-circuit upload
+    return Promise.reject('short-circuit')
+  }
+
+  const data = Buffer.alloc(10)
+  data.size = data.byteLength
+
+  Promise.all([
+    uppy.addFile({ name: 'a.png', data }),
+    uppy.addFile({ name: 'b.png', data }),
+    uppy.addFile({ name: 'c.png', data }),
+    uppy.addFile({ name: 'd.png', data })
+  ]).then(() => {
+    uppy.upload().then(t.fail, () => {
+      t.equal(i, 4, 'created 4 assemblies')
+      t.end()
+    })
+  }, t.fail)
+})
+
+test('Transloadit: Should merge files with same parameters into one assembly', (t) => {
+  t.plan(3)
+
+  const uppy = new Core({ autoProceed: false })
+
+  uppy.use(Transloadit, {
+    getAssemblyOptions: (file) => ({
+      params: {
+        auth: { key: 'fake key' },
+        steps: {
+          fake_step: { data: file.size }
+        }
+      }
+    })
+  })
+
+  const tl = uppy.getPlugin('Transloadit')
+  const assemblies = [
+    { data: 10, files: ['a.png', 'b.png', 'c.png'] },
+    { data: 20, files: ['d.png'] }
+  ]
+  let i = 0
+  tl.client.createAssembly = (opts) => {
+    const assembly = assemblies[i]
+    t.equal(opts.params.steps.fake_step.data, assembly.data, `assembly for files ${assembly.files.join(',')}`)
+    i++
+    // Short-circuit upload
+    return Promise.reject('short-circuit')
+  }
+
+  const data = Buffer.alloc(10)
+  data.size = data.byteLength
+  const data2 = Buffer.alloc(20)
+  data2.size = data2.byteLength
+
+  Promise.all([
+    uppy.addFile({ name: 'a.png', data }),
+    uppy.addFile({ name: 'b.png', data }),
+    uppy.addFile({ name: 'c.png', data }),
+    uppy.addFile({ name: 'd.png', data: data2 })
+  ]).then(() => {
+    uppy.upload().then(t.fail, () => {
+      t.equal(i, 2, 'created two assemblies')
+      t.end()
+    })
+  }, t.fail)
+})