Browse Source

Pass metadata to Companion `getKey()` option for S3 uploads (#1866)

* aws-s3: send metadata to companion

* companion: apply non-multipart s3 metadata to object

* companion: add metadata parameter to getKey() api

* docs: add `metadata` parameter for `getKey`
Renée Kooi 5 years ago
parent
commit
1afe6977af

+ 6 - 0
package-lock.json

@@ -6173,6 +6173,7 @@
         "@uppy/companion-client": "file:packages/@uppy/companion-client",
         "@uppy/utils": "file:packages/@uppy/utils",
         "@uppy/xhr-upload": "file:packages/@uppy/xhr-upload",
+        "qs-stringify": "^1.1.0",
         "url-parse": "^1.4.7"
       }
     },
@@ -27889,6 +27890,11 @@
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
       "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
     },
+    "qs-stringify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/qs-stringify/-/qs-stringify-1.1.0.tgz",
+      "integrity": "sha512-0/FY/zSBLOFrlrB/fQAjtX9IngoESeDCMzgDIMB4uvv2Ks7q9NfvbWix0BMBrVOvB853xImt+0s5p37gSDFtGw=="
+    },
     "query-string": {
       "version": "4.3.4",
       "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",

+ 2 - 1
packages/@uppy/aws-s3/package.json

@@ -25,7 +25,8 @@
     "@uppy/companion-client": "file:../companion-client",
     "@uppy/utils": "file:../utils",
     "@uppy/xhr-upload": "file:../xhr-upload",
-    "url-parse": "^1.4.7"
+    "url-parse": "^1.4.7",
+    "qs-stringify": "^1.1.0"
   },
   "peerDependencies": {
     "@uppy/core": "^1.0.0"

+ 11 - 1
packages/@uppy/aws-s3/src/index.js

@@ -5,6 +5,7 @@ const Translator = require('@uppy/utils/lib/Translator')
 const RateLimitedQueue = require('@uppy/utils/lib/RateLimitedQueue')
 const { RequestClient } = require('@uppy/companion-client')
 const XHRUpload = require('@uppy/xhr-upload')
+const qsStringify = require('qs-stringify')
 
 function resolveUrl (origin, link) {
   return new URL_(link, origin).toString()
@@ -63,6 +64,7 @@ module.exports = class AwsS3 extends Plugin {
     const defaultOptions = {
       timeout: 30 * 1000,
       limit: 0,
+      metaFields: [], // have to opt in
       getUploadParameters: this.getUploadParameters.bind(this)
     }
 
@@ -93,7 +95,15 @@ module.exports = class AwsS3 extends Plugin {
 
     const filename = encodeURIComponent(file.meta.name)
     const type = encodeURIComponent(file.meta.type)
-    return this.client.get(`s3/params?filename=${filename}&type=${type}`)
+    const metadata = {}
+    this.opts.metaFields.forEach((key) => {
+      if (file.meta[key] != null) {
+        metadata[key] = file.meta[key].toString()
+      }
+    })
+
+    const query = qsStringify({ filename, type, metadata })
+    return this.client.get(`s3/params?${query}`)
       .then(assertServerError)
   }
 

+ 11 - 2
packages/@uppy/companion/src/server/controllers/s3.js

@@ -16,6 +16,8 @@ module.exports = function s3 (config) {
    *  - filename - The name of the file, given to the `config.getKey`
    *    option to determine the object key name in the S3 bucket.
    *  - type - The MIME type of the file.
+   *  - metadata - Key/value pairs configuring S3 metadata. Both must be ASCII-safe.
+   *    Query parameters are formatted like `metadata[name]=value`.
    *
    * Response JSON:
    *  - method - The HTTP method to use to upload.
@@ -25,7 +27,8 @@ module.exports = function s3 (config) {
   function getUploadParameters (req, res, next) {
     // @ts-ignore The `companion` property is added by middleware before reaching here.
     const client = req.companion.s3Client
-    const key = config.getKey(req, req.query.filename)
+    const metadata = req.query.metadata || {}
+    const key = config.getKey(req, req.query.filename, metadata)
     if (typeof key !== 'string') {
       return res.status(500).json({ error: 's3: filename returned from `getKey` must be a string' })
     }
@@ -37,6 +40,10 @@ module.exports = function s3 (config) {
       'content-type': req.query.type
     }
 
+    Object.keys(metadata).forEach((key) => {
+      fields[`x-amz-meta-${key}`] = metadata[key]
+    })
+
     client.createPresignedPost({
       Bucket: config.bucket,
       Expires: ms('5 minutes') / 1000,
@@ -62,6 +69,8 @@ module.exports = function s3 (config) {
    *  - filename - The name of the file, given to the `config.getKey`
    *    option to determine the object key name in the S3 bucket.
    *  - type - The MIME type of the file.
+   *  - metadata - An object with the key/value pairs to set as metadata.
+   *    Keys and values must be ASCII-safe for S3.
    *
    * Response JSON:
    *  - key - The object key in the S3 bucket.
@@ -70,7 +79,7 @@ module.exports = function s3 (config) {
   function createMultipartUpload (req, res, next) {
     // @ts-ignore The `companion` property is added by middleware before reaching here.
     const client = req.companion.s3Client
-    const key = config.getKey(req, req.body.filename)
+    const key = config.getKey(req, req.body.filename, req.body.metadata || {})
     const { type, metadata } = req.body
     if (typeof key !== 'string') {
       return res.status(500).json({ error: 's3: filename returned from `getKey` must be a string' })

+ 7 - 0
website/src/docs/aws-s3.md

@@ -68,6 +68,13 @@ uppy.use(AwsS3, {
 
 Custom headers that should be sent along to [Companion][companion docs] on every request.
 
+### `metaFields: []`
+
+Pass an array of field names to specify the metadata fields that should be stored in S3 as Object Metadata. This takes values from each file's `file.meta` property.
+
+- Set this to `['name']` to only send the name field.
+- Set this to an empty array `[]` (the default) to not send any fields.
+
 ### `getUploadParameters(file)`
 
 > Note: When using [Companion][companion docs] to sign S3 uploads, do not define this option.

+ 15 - 5
website/src/docs/companion.md

@@ -241,7 +241,7 @@ See [env.example.sh](https://github.com/transloadit/uppy/blob/master/env.example
       secret: "***"
     },
     s3: {
-      getKey: (req, filename) => filename,
+      getKey: (req, filename, metadata) => filename,
       key: "***",
       secret: "***",
       bucket: "bucket-name",
@@ -292,21 +292,31 @@ See [env.example.sh](https://github.com/transloadit/uppy/blob/master/env.example
 
 The S3 uploader has some options in addition to the ones necessary for authentication.
 
-#### `s3.getKey(req, filename)`
+#### `s3.getKey(req, filename, metadata)`
+a
+Get the key name for a file. The key is the file path to which the file will be uploaded in your bucket. This option should be a function receiving three arguments:
+- `req`, the HTTP request, for _regular_ S3 uploads using the `@uppy/aws-s3` plugin. This parameter is _not_ available for multipart uploads using the `@uppy/aws-s3-multipart` plugin;
+- `filename`, the original name of the uploaded file;
+- `metadata`, user-provided metadata for the file. See the [`@uppy/aws-s3`](https://uppy.io/docs/aws-s3/#metaFields) docs. Currently, the `@uppy/aws-s3-multipart` plugin unconditionally sends all metadata fields, so all of them are available here.
 
-Get the key name for a file. The key is the file path to which the file will be uploaded in your bucket. This option should be a function receiving two arguments: `req`, the HTTP request, and the original `filename` of the uploaded file. It should return a string `key`. The `req` parameter can be used to upload to a user-specific folder in your bucket, for example:
+This function should return a string `key`. The `req` parameter can be used to upload to a user-specific folder in your bucket, for example:
 
 ```js
 app.use(authenticationMiddleware)
 app.use(uppy.app({
   s3: {
-    getKey: (req, filename) => `${req.user.id}/${filename}`,
+    getKey: (req, filename, metadata) => `${req.user.id}/${filename}`,
     /* auth options */
   }
 }))
 ```
 
-The default value simply returns `filename`, so all files will be uploaded to the root of the bucket as their original file name.
+The default implementation returns the `filename`, so all files will be uploaded to the root of the bucket as their original file name.
+```js
+({
+  getKey: (req, filename, metadata) => filename
+})
+```
 
 ### Running in Kubernetes