Обновление. После этого я написал еще одно пошаговое руководство о том, как загружать файлы с помощью Cognito вместо заранее заданных URL-адресов. Ссылка выше.

Если вы хотите создать одностраничное приложение с возможностью загрузки файлов в S3 без раскрытия конфиденциальных переменных среды, вы можете сделать это, используя лямбда-функцию для создания заранее подписанного URL-адреса. Этот сгенерированный URL-адрес может быть возвращен в ваш SPA, что дает ему возможность (с некоторыми ограничениями, такими как ограничение по времени и файлу) загружать непосредственно с клиента. Это полезно не только для безопасности, но и по другой причине; клиент делает всю работу по загрузке файла.

В этом пошаговом руководстве мы настроим приложение с помощью AWS appsync и ampify, а также будем использовать Vue.js для нашего клиентского SPA.

Поэтому убедитесь, что у вас установлены vue-cli (я использую версию 3.1.3) и ampify-cli (я использую версию 0.2.1-multienv.22).

Начните новый проект vue, набрав следующее и выбрав свой пресет:

vue create presigned-url-lambda

Перейдите в эту недавно созданную папку и инициализируйте ее как проект расширения.

> cd presigned-url-lambda
> amplify init

Ответьте на вопросы в командной строке в соответствии с вашими предпочтениями, и после завершения процесса инициализации добавьте api с помощью следующей команды:

amplify add api

В командной строке выберите параметр graphql для API, а затем выберите все ответы по умолчанию для других вопросов (возможно, вам придется изменить имя API, если по умолчанию используются запрещенные символы).

Как только вы ответите «да» на редактирование схемы, ваш текстовый редактор откроется с файлом schema.graphql. Измените его так:

type Modform @model {
  id: ID!
  files: [String]
  site: String
  page: String
  changes: String
}

type File @model {
  name: String
  url: String
  filetype: String
}

После сохранения схемы вернитесь в окно консоли и нажмите Enter, чтобы продолжить.

Затем нам нужно добавить нашу лямбда-функцию. В консоли введите:

amplify add function

И снова вам будет предложено предоставить некоторые детали. Мы просто будем использовать значения по умолчанию и ответим «да», чтобы отредактировать локальную лямбда-функцию.

Откроется index.js файл, который вы должны отредактировать, чтобы он выглядел так:

const AWS = require('aws-sdk');
// if you are using an eu region, you will have to set the signature
// version to v4 by passing this into the S3 constructor -
// {signatureVersion: 'v4' }
const s3 = new AWS.S3();

exports.handler = function (event, context) { 

  const bucket = process.env['s3_bucket']
  
  if (!bucket) {
    console.log('bucket not set:') 
    context.done(new Error(`S3 bucket not set`))
  }

  const key = `my-location/${event.input.name}`

  if (!key) {
    console.log('key missing:')
    context.done(new Error('S3 object key missing'))
    return;
  }

  const params = {
    'Bucket': bucket,
    'Key': key,
    ContentType: event.input.filetype
  };

  s3.getSignedUrl('putObject', params, (error, url) => {
    if (error) {
      console.log('error:', error)
      context.done(error)
    } else {
      context.done(null, {
        url: url, 
        name: key, 
        filetype: event.input.filetype
      });
    }
  })

}

Сохраните это, вернитесь на локальную консоль и нажмите Enter.

Эта функция в основном берет некоторую информацию из запроса (mime-тип файла и имя файла. Она создает ключ с именем файла (по сути, путь к файлу в S3) и использует методS3.getSignedUrl() для получения заранее подписанного URL-адреса. возвращается из функции.

Теперь, когда мы настроили наш API и функцию Lambda на нашей локальной машине, нам нужно передать эти данные в AWS:

amplify push

Это настроит все ресурсы, которые мы определили, в облаке. Перед началом процесса нам будет показана таблица в консоли с указанием отложенных операций. У нас должна быть ожидающая операция «Create» как для API, так и для функции. Ответьте «да», чтобы продолжить, а затем предоставьте ответ по умолчанию на следующие несколько вопросов, чтобы сгенерировать код для нашего API.

Когда этот процесс завершится, вам будет показана конечная точка GraphQL и ключ API. Вам нужно будет поместить их в наш src/main.js файл.

Но прежде чем мы это сделаем, нам нужно установить еще несколько пакетов:

npm install --save [email protected] [email protected] [email protected] [email protected]

Откройте файл src/main.js в нашем проекте и измените его на следующий код, разместив свой ключ API и конечную точку GraphQL там, где указано:

import Vue from 'vue'
import App from './App.vue'

// vue-apollo makes it easier for your vue app to interact with the
// apollo-client inside the aws-appsync package, which, in turn,
// coordinates the data exchanges between the front end store and
// the backend store and deals with caching etc.
import VueApollo from 'vue-apollo'
import AWSAppSyncClient from 'aws-appsync'

const config = {
 url: <YOUR_GRAPHQL_ENDPOINT>,
 region: 'us-east-1',
 auth: {
  type: 'API_KEY',
  apiKey: <YOUR_API_KEY>
 }
}

// The default fetchPolicy is cache-first. This means that if data
// is returned from the cache, no network request will be sent. If
// a new item is in a list, this will not be realised. So here we
// change the policy so that network requests are always sent after // data is returned from the cache.
const options = {
 defaultOptions: {
  watchQuery: {
    fetchPolicy: 'cache-and-network'
  }
 }
}
const client = new AWSAppSyncClient(config, options)
const appsyncProvider = new VueApollo({
 defaultClient: client
})
Vue.use(VueApollo)
Vue.config.productionTip = false
new Vue({
 render: h => h(App),
 apolloProvider: appsyncProvider
}).$mount('#app')

А затем измените файл src/App.vue, чтобы он выглядел так:

<template>
  <div id="app">
    <demo-page />
  </div>
</template>

<script>
import DemoPage from '@/components/DemoPage'
export default {
  name: 'app',
  components: {
    DemoPage
  }
}
</script>

<style>
  body {
    margin: 0;
    padding: 0;
    width: 100vw;
    height: 100vh;
  }
  #app {
    padding: 40px;
  }
</style>

Создайте следующую демонстрационную страницу на src/components/DemoPage.vue:

<template>
  <div class="container">
    <form class="form" @submit.prevent="handleSubmit">
      <h3>Modform</h3>
      <label>Site:</label>
      <input
        type="text"
        placeholder="Your site"
        v-model="model.site"
      />
      <label>Page:</label>
      <input
        type="text"
        placeholder="Your page"
        v-model="model.page"
      />
      <label>Changes:</label>
      <input
        type="text"
        placeholder="Your changes"
        v-model="model.changes"
      />
      <label>Files:</label>
      <input
        type="file"
        placeholder="Your files"
        @change="addFilenameToModel"
        multiple
      />
      <div v-if="images.length" class="image-container">
        <img v-for="img in images" :src="img" alt="pic" />
      </div>
      <input type="submit" class="btn-submit" :disabled="uploading">
    </form>
  </div>

</template>

<script>
import gql from 'graphql-tag'
import axios from 'axios'
import { createModform } from '@/graphql/mutations'
import { listModforms } from '@/graphql/queries'
import { createFile } from '@/graphql/mutations'

const BASE_URL = 'https://presigned-demo-images.s3.amazonaws.com'

export default {
  name: 'DemoPage',
  data () {
    return {
      uploading: false,
      images: [],
      model: {
        files: []
      }
    }
  },
  methods: {
    addFilenameToModel ({target}) {
        console.log('Loading...')
        this.uploading = true

      this.$apollo.mutate({
        mutation: gql(createFile),
        variables: {
          input: {
            name: target.files[0].name,
            filetype: target.files[0].type,
            // idOfSomeForm: like user id
          }
        }
      })
      .then(async ({data}) => {

        try {

          await axios.put(data.createFile.url, target.files[0], {
            headers: { 'Content-Type': target.files[0].type }
          })

          console.log('Loaded')
          this.uploading = false

          this.images.push(`${BASE_URL}/${data.createFile.name}`)
          this.model.files.push(`${BASE_URL}/${data.createFile.name}`)

        } catch (e) {

          // do we need to remove anything from the model here?
          console.log('Upload failed!', e)

        }
      })
    },
    handleSubmit () {
      this.$apollo.mutate({
        mutation: gql(createModform),
        variables: { input: this.model }
      })
      .then(a => {
        console.log('Form stored in database')
      })
    },
  }
}
</script>

<style lang="css" scoped>
  .form {
    max-width: 500px;
    min-height: 310px;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    border: 1px solid lightgrey;
    padding: 40px;
    border-radius: 5px;
  }

  .form input {
    font-size: 1em;
    padding-left: 3px;
    margin-bottom: 10px;
  }

  .btn-submit {
    background-color: #7979de;
    border-radius: 5px;
    color: white;
    margin-top: 10px;
  }
  
  .btn-submit:disabled {
    opacity: .5;
  }

  .container {
    display: flex;
    justify-content: space-around;
  }

  .image-container {
    display: flex;
    margin: 5px;
    justify-content: center;
  }

  img {
    max-height: 60px;
    max-width: 100px;
    margin: 3px;
    border: solid grey 1px;
  }
</style>

Внешний интерфейс настроен, и вы сможете запускать приложение с npm run serve. Когда вы это сделаете, вы увидите форму с некоторыми полями для ввода. Откройте инструменты разработчика и попробуйте добавить файл, используя ввод файла.

Вы увидите сообщение об ошибке: TypeError: cannot read property 'protocol' of null

Это связано с тем, что в настоящий момент наш преобразователь api графа приложений appsync не настроен должным образом для нашего варианта использования. Вам нужно перейти в раздел AWS Appsync консоли AWS с активной вкладкой «Источники данных». Нажмите кнопку «Создать источник данных».

Заполните поля ввода соответствующим образом, но убедитесь, что вы установили тип источника данных как «AWS Lambda function» и убедитесь, что вы указываете на функцию, которую мы создали с помощью ampify. Новая роль автоматически даст нужное разрешение.

Затем, все еще находясь в разделе AWS Appsync, перейдите на вкладку «Схема», найдите свою мутацию createFile () в окне «Решатели» и щелкните сопоставитель, с которым он в настоящее время связан. Вместо того, чтобы передавать данные в DynamoDB, мы хотим, чтобы они перешли к нашей функции Lambda, которая запросит для нас заранее подписанный URL-адрес и вернет его.

Выберите только что настроенный источник данных и настройте шаблоны сопоставления так, чтобы они пересылали аргументы и возвращали результат. Убедитесь, что вы нажали сохранить!

Нам действительно нужно создать корзину, поэтому перейдите в раздел S3 консоли AWS и нажмите кнопку «Создать корзину».

Заполните поля ввода Bucket Name и Region. Убедитесь, что регион соответствует вашей лямбда-функции. Нажмите «Далее» и продолжайте следующие шаги, не внося никаких изменений, пока не дойдете до кнопки «Создать сегмент». Щелкните по нему.

Теперь, если мы снова запустим наше приложение и попытаемся добавить файл, мы получим следующую ошибку: GraphQL error: S3 bucket not set

Это связано с тем, что в настроенной нами функции мы ссылаемся на еще не существующую переменную среды. Итак, перейдем в раздел Lambda нашей консоли AWS на вкладку «functions». Выделена функция, которую я создал, у вашей будет другое имя. Щелкните по нему и прокрутите вниз до панели «Переменные среды». Мы хотим установить это так, чтобы оно указывало на наше хранилище S3.

Теперь, когда вы попытаетесь запустить приложение и прикрепить файл, вы получите ошибку 403 (Запрещено) и ошибку CORS. Нам нужно настроить разрешения нашего s3.

Перейдите в раздел S3 консоли AWS и щелкните только что созданную корзину. Затем щелкните вкладку разрешений и кнопку «Конфигурация CORS». Вы можете найти примеры политик CORS, щелкнув ссылку на документацию. Мы будем использовать следующую политику:

В основном нам нужно было разрешить HTTP-методы PUT и GET и разрешить наше происхождение (предположительно localhost: 8080 при запуске вашего приложения vue с npm run serve). Итак, вы можете вырезать, изменять и вставлять, вот оно снова:

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>http://localhost:8080</AllowedOrigin>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>GET</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

Это устранит ошибку CORS, с которой мы столкнулись. Затем нам нужно перейти в «Настройки публичного доступа» и отредактировать политики публичного сегмента, как показано на изображении.

Это позволит нам создать новую политику.

Нажмите кнопку политики корзины.

Вы можете использовать генератор политик для создания политики в соответствии с вашими требованиями. Нам нужно разрешить действия GetObject и PutObject, и поскольку мы хотим, чтобы клиентское приложение (и все, кто его использует) выполняли эти операции, мы устанавливаем для Принципала подстановочный знак.

{
    "Id": "Policy1547200240036",
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1547200205482",
            "Action": [
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:s3:::presigned-demo-2/*",
            "Principal": "*"
        }
    ]
}

Если вы не можете сохранить политику, и отображается ошибка типа «Действие не применяется ни к одному ресурсу (ам) в заявлении», добавьте подстановочный знак в конце имени ресурса, потому что вам нужно применить эта политика распространяется на все ресурсы в корзине. Убедитесь, что вы нажали кнопку сохранения.

Теперь вы должны иметь возможность запускать приложение, добавлять изображения в форму и сохранять модформу с массивом URL-адресов изображений. Ура!

* Если вы обнаружите, что можете ПОСТАВИТЬ файлы в S3, но не можете ПОЛУЧИТЬ их, убедитесь, что BASE_URL на вашей демонстрационной странице установлен правильно, чтобы указывать на правильный источник S3.