hunk.code_window: ### `getUploadParameters(file)`
   > Note: When using [uppy-server][uppy-server docs] to sign S3 uploads, do not define this option.
   A function returning upload parameters for a file.
   Parameters should be returned as an object, or a Promise for an object, with keys `{ method, url, fields }`.
   The `method` field is the HTTP method to use for the upload.
   This should be one of `PUT` or `POST`, depending on the type of upload used.
  
hunk.labels: keep keep keep keep keep replace keep keep keep
hunk.after_edit: Parameters should be returned as an object, or a Promise for an object, with keys `{ method, url, fields, headers }`.
  
hunk.file_path: website/src/docs/aws-s3.md
hunk.type: replace
file: const resolveUrl = require('resolve-url')
  const Plugin = require('../../core/Plugin')
  const Translator = require('../../core/Translator')
  const { limitPromises } = require('../../core/Utils')
  const XHRUpload = require('../XHRUpload')
  
  function isXml (xhr) {
    const contentType = xhr.headers ? xhr.headers['content-type'] : xhr.getResponseHeader('Content-Type')
    return typeof contentType === 'string' && contentType.toLowerCase() === 'application/xml'
  }
  
  module.exports = class AwsS3 extends Plugin {
    constructor (uppy, opts) {
      super(uppy, opts)
      this.type = 'uploader'
      this.id = 'AwsS3'
      this.title = 'AWS S3'
  
      const defaultLocale = {
        strings: {
          preparingUpload: 'Preparing upload...'
        }
      }
  
      const defaultOptions = {
        timeout: 30 * 1000,
        limit: 0,
        getUploadParameters: this.getUploadParameters.bind(this),
        locale: defaultLocale
      }
  
      this.opts = Object.assign({}, defaultOptions, opts)
      this.locale = Object.assign({}, defaultLocale, this.opts.locale)
      this.locale.strings = Object.assign({}, defaultLocale.strings, this.opts.locale.strings)
  
      this.translator = new Translator({ locale: this.locale })
      this.i18n = this.translator.translate.bind(this.translator)
  
      this.prepareUpload = this.prepareUpload.bind(this)
  
      if (typeof this.opts.limit === 'number' && this.opts.limit !== 0) {
        this.limitRequests = limitPromises(this.opts.limit)
      } else {
        this.limitRequests = (fn) => fn
      }
    }
  
    getUploadParameters (file) {
      if (!this.opts.host) {
        throw new Error('Expected a `host` option containing an uppy-server address.')
      }
  
      const filename = encodeURIComponent(file.name)
      const type = encodeURIComponent(file.type)
      return fetch(`${this.opts.host}/s3/params?filename=${filename}&type=${type}`, {
        method: 'get',
        headers: { accept: 'application/json' }
      }).then((response) => response.json())
    }
  
    validateParameters (file, params) {
      const valid = typeof params === 'object' && params &&
        typeof params.url === 'string' &&
        (typeof params.fields === 'object' || params.fields == null) &&
        (params.method == null || /^(put|post)$/i.test(params.method))
  
      if (!valid) {
        const err = new TypeError(`AwsS3: got incorrect result from 'getUploadParameters()' for file '${file.name}', expected an object '{ url, method, fields }'.\nSee https://uppy.io/docs/aws-s3/#getUploadParameters-file for more on the expected format.`)
        console.error(err)
        throw err
      }
  
      return params
    }
  
    prepareUpload (fileIDs) {
      fileIDs.forEach((id) => {
        const file = this.uppy.getFile(id)
        this.uppy.emit('preprocess-progress', file, {
          mode: 'determinate',
          message: this.i18n('preparingUpload'),
          value: 0
        })
      })
  
      const getUploadParameters = this.limitRequests(this.opts.getUploadParameters)
  
      return Promise.all(
        fileIDs.map((id) => {
          const file = this.uppy.getFile(id)
          const paramsPromise = Promise.resolve()
            .then(() => getUploadParameters(file))
          return paramsPromise.then((params) => {
            return this.validateParameters(file, params)
          }).then((params) => {
            this.uppy.emit('preprocess-progress', file, {
              mode: 'determinate',
              message: this.i18n('preparingUpload'),
              value: 1
            })
            return params
          }).catch((error) => {
            this.uppy.emit('upload-error', file, error)
          })
        })
      ).then((responses) => {
        const updatedFiles = {}
        fileIDs.forEach((id, index) => {
          const file = this.uppy.getFile(id)
          if (file.error) {
            return
          }
  
          const {
            method = 'post',
            url,
            fields,
            headers
          } = responses[index]
          const xhrOpts = {
            method,
            formData: method.toLowerCase() === 'post',
            endpoint: url,
            metaFields: Object.keys(fields)
          }
  
          if (headers) {
            xhrOpts.headers = headers
          }
  
          const updatedFile = Object.assign({}, file, {
            meta: Object.assign({}, file.meta, fields),
            xhrUpload: xhrOpts
          })
  
          updatedFiles[id] = updatedFile
        })
  
        this.uppy.setState({
          files: Object.assign({}, this.uppy.getState().files, updatedFiles)
        })
  
        fileIDs.forEach((id) => {
          const file = this.uppy.getFile(id)
          this.uppy.emit('preprocess-complete', file)
        })
      })
    }
  
    install () {
      this.uppy.addPreProcessor(this.prepareUpload)
  
      this.uppy.use(XHRUpload, {
        fieldName: 'file',
        responseUrlFieldName: 'location',
        timeout: this.opts.timeout,
        limit: this.opts.limit,
        getResponseData (content, xhr) {
          // If no response, we've hopefully done a PUT request to the file
          // in the bucket on its full URL.
          if (!isXml(xhr)) {
            return { location: xhr.responseURL }
          }
  
          let getValue = () => ''
          if (xhr.responseXML) {
            getValue = (key) => {
              const el = xhr.responseXML.querySelector(key)
              return el ? el.textContent : ''
            }
          }
  
          if (xhr.responseText) {
            getValue = (key) => {
              const start = xhr.responseText.indexOf(`<${key}>`)
              const end = xhr.responseText.indexOf(`</${key}>`)
              return start !== -1 && end !== -1
                ? xhr.responseText.slice(start + key.length + 2, end)
                : ''
            }
          }
  
          return {
            // Some S3 alternatives do not reply with an absolute URL.
            // Eg DigitalOcean Spaces uses /$bucketName/xyz
            location: resolveUrl(xhr.responseURL, getValue('Location')),
            bucket: getValue('Bucket'),
            key: getValue('Key'),
            etag: getValue('ETag')
          }
        },
        getResponseError (content, xhr) {
          // If no response, we don't have a specific error message, use the default.
          if (!isXml(xhr)) {
            return
          }
          const error = xhr.responseXML.querySelector('Error > Message')
          return new Error(error.textContent)
        }
      })
    }
  
    uninstall () {
      const uploader = this.uppy.getPlugin('XHRUpload')
      this.uppy.removePlugin(uploader)
  
      this.uppy.removePreProcessor(this.prepareUpload)
    }
  }
  