Sándor Képiró

Maker of things, Träumer und Technoliebhaber

Freitag, 8. Februar

How-to: Upload direkt aus dem Frontend einer Vue App in ein AWS Bucket mit SignedURLs

Will man in seiner Vue App einen Upload für Nutzer bereitstellen, geht dies meist über das eigene Backend und ein damit verbundenes Daten-Handling. Mithilfe eines AWS Buckets und den SignedURLs ist dies relativ simpel zu erreichen, ohne große Last auf das eigene Backend zu legen.

Ich berichte von meinen Erfahrungen und Learnings bei der Umsetzung dieser Lösung. Vielleicht hilft es dem einen oder anderen Leser etwas schneller ans Ziel zu kommen.

Der Blogpost zeigt, mit welchem Setup der Upload zu AWS eingerichtet werden kann, und man komplett auf einen Backend-Server hierbei verzichten kann. Die Lernkurve bei AWS ist etwas steil, aber sobald das Grundsetup eingerichtet ist, lässt der Upload schnell realisieren.

Das brauchst du zum Starten

Setup deiner AWS Konsole

Schritt 1: Account anlegen

Im ersten Schritt erstellt man ein AWS Konto. AWS fragt bei einem neuen Account immer die Kreditkartendaten ab, obwohl wir später das Setup so einstellen, dass vorerst keine Kosten anfallen. Nach der erfolgreichen Anmeldung, haben wir Zugriff auf die zum Konto gehörenden Keys. Die angezeigten Schlüssel sind die Root-Schlüssel, die wir vorerst aber nicht nutzen werden, da einen IAM User für den Upload erstellen werden. Solltest du später diese Schlüssel benutzen wollen, kannst du dir einfach jederzeit neue Schlüssel unter “My Security Credentials” generieren lassen.

Beachte: Deine Schlüssel solltest du niemals öffentlich teilen, da es wegen des “Pay as you go” Preismodells schnell teuer für dich werden könnte. Auf dem sichersten Weg bist du, wenn du dir die Schlüssel als Variablen in einer .env-Datei in deinem Projekt hinterlegst und diese in deinem Git-Repository ignorierst

Schritt 2: Erstelle einen Bucket

Ist der Account angelegt, kann es mit der Bucket-Erstellung weitergehen. Ein Bucket lässt sich relativ einfach erstellen, indem du einen Name vergibst und alle Genehmigung vorerst deaktivierst. Dies ist wichtig, da sich erst danach die Bucket Policy auf public setzen lässt, um die Bilder für alle erreichbar zu machen.

Erstelle ein öffentliches Bucket (letzte Genehmigung auch ausschalten)

So sollten deine Bucket Policy und die CORS Einstellungen aussehen:

 {
    "Version": "2012-10-17",
    "Id": "public policy example",
    "Statement": [
        {
            "Sid": "Allow get requests",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::YOUR_BUCKET/*"
        }
    ]
}
<?xml version="1.0" encoding="UTF-8"?><CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><CORSRule>    <AllowedOrigin>*</AllowedOrigin>    <AllowedMethod>POST</AllowedMethod>    <AllowedMethod>GET</AllowedMethod>    <AllowedMethod>PUT</AllowedMethod>    <AllowedMethod>DELETE</AllowedMethod>    <AllowedMethod>HEAD</AllowedMethod>    
<AllowedHeader>*</AllowedHeader>
</CORSRule></CORSConfiguration>

Scritt 3: Erstelle einen neuen IAM Benutzer

Haben wir auch das Bucket angelegt, können wir uns nun der Erstellung eines IAM Benutzers widmen. Die richtig Seite für die Erstellung findest du am schnellsten über die Suche innerhalb der AWS Konsole.

Der neue Nutzer sollte nur Zugriff zum Get-, Put- und DeleteObject haben. Auch hier spielt die Sicherheit eine große Rolle. Sollten einmal Unbefugte Zugriff zu dem Schlüssel bekommen, haben diese dennoch keinen Vollzugriff auf das Bucket. Es empfiehlt sich deshalb eine neue Policy zu erstellen.

Hier ist ein Beispiel für eine Policy. Alle Schritte zum Erstellen eines neuen IAM Nutzers findest du in folgender Liste:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::YOUR_BUCKET/*"
        }
    ]
}
  • Navigiere zum User – der Link ist in der Sidebar, klicke auf Add user
  • Vergib einen Name eingeben und und wählen Programmatic access Dies erstellt einen eigenen Zugriffsschlüssel für den Nutzer, den wir später beim Upload nutzen.
  • Wähle “Attach existing policies directly” aus, um eine neue Policy unter “Create policy” erstellen zu können. Der Button wird einen neuen Tab öffnen.
  • Füge hier unter JSON einfach den Code von oben ein und klick “review Policy”. Notiz: Statt YOUR_BUCKET muss man natürlich den Name seines eigenen Buckets benutzen.
  • Vergib einen Namen für die Policy, und schließe den Tab mit “create policy”
  • Aktualisiere die Policies und wähle die neuerstellte Policy aus
  • Klicke “Next: Tags”, die du aber übergehen kannst, da wir Tags nicht brauchen
  • Nach der Erstellung des Users kannst du den Schlüssel einsehen. Man kann entweder den Text kopieren oder eine CSV Datei herunterladen. Diesen Schlüssel werden wir in der nächsten Schritt brauchen!

Das AWS JS SDK

Nach dem erfolgreichem Setup unserer AWS Konsole können wir endlich mit Coding anfangen. Ich habe die AWS Methoden aus Gründen der Wiederverwendbarkeit getrennt in einem aws.js File untergebracht.

Erste Schritt: Erstellen einer S3 Instance

Zu allerst wollen wir die aws-sdk Bibliothek zu unserem Projekt hinzuzufügen, um die AWS Methode zu benutzenzu können. Zusätzlich für Requests nutzen wir auch das axios Package.

Installiere aws-sdk und axios in deinem Projekt yarn:

yarn add aws-sdk axios

Sobald die Installation erfolgreich war, können wir mit der Initialisierung einer S3-Instanz starten. Wie du siehst nutzen wir unsere Zugriffsschlüssel aus der .env-Datei sowie eine Regionsvariable. Mit new aws.S3() lässt sich eine S3-Instanz intialisieren. Wie du siehst habe ich auch die Option signatureVersion angeben, um Dateien zu dem Server hochladen zu können. Wenn du einen amerikanischen Server benutzt, kannst du dir diese Option sparen.

const aws = require('aws-sdk')

aws.config.update({
  secretAccessKey: process.env.VUE_APP_AWS_SECRET_ACCESS_KEY,
  accessKeyId: process.env.VUE_APP_AWS_ACCESS_KEY,
})

export const s3 = new aws.S3({
  signatureVersion: 'v4',
  region: process.env.VUE_APP_AWS_REGION,
})

Notiz: wenn du das Code kopierst, vergiss nicht in deinen .env Variablen VUE_APP_AWS_ACCESS_KEY, VUE_APP_AWS_SECRET_ACCESS_KEY und VUE_APP_AWS_REGION definieren.

Zweiter Schritt: SignedURL

Wenn die Konfiguration abgeschlossen ist, können wir unsere singleUpload Methode mit signedURL anlegen. Ein kurzer Ausflug zum Thema signedURLs und warum es gut ist sie zu benutzen?

  • Die signedURL kann nur für einzelne Datei-Uploads benutzt werden – der Benutzer kann das Bucket nicht ungewollt weiter befüllen
  • Die signedURL verschlüsselt den Filename und Filetype – der Benutzer kann nur die jeweils angemeldete Datei hochladen
  • Die signedURL ist zeitlich beschränkt – dies schützt vor Exploits z.B.: Benutzer mit bösen Absichten, die versuchen, die signedURL von einem anderen Benutzer zu benutzen
  • Die signedURL ist von der Vue.js App generiert und lässt sich nicht selbst erstellen
  • Die signedURL funktioniert nur mit dem festgelegtem Bucket – Benutzer können kein andere Bucket sehen oder darauf zugreifen
export const singleUpload = (file, folder) => {
  const key = folder + '/' + Date.now() + '-' + file.name.replace(/\s/g, '-')
  const params = {
    Bucket: process.env.VUE_APP_AWS_BUCKET,
    Key: key,
    Expires: 10,
    ContentType: file.type,
  }
  const url = s3.getSignedUrl('putObject', params)
  return axios
    .put(url, file, {
      headers: {
        'Content-Type': file.type,
      },
    })
    .then(result => {
      const bucketUrl = decodeURIComponent(result.request.responseURL).split(
        key
      )[0]
      result.key = key
      result.fullPath = bucketUrl + key
      return result
    })
    .catch(err => {
      // TODO: error handling
      console.log(err)
    })
}

In Zeile 2 generieren wir einen spezifischen Dateinamen bei AWS als Key bezeichnet. Zusätzlich muss der Dateiname auch den Ordner enthalten in dem die Datei liegen soll, beispielsweise ein Album oder Team. Wir können den Ordnernamen mit Schrägstrich abgrenzen. Um einen einzigartigen Dateinamen zu generieren nutzen wir Date.now(). Die replace Methode ersetzt die Whitespaces gegen einen Bindestrich (-). Es wäre sogar möglich nur mit Date.now() zu arbeiten. Dies liegt bei dir, welche Struktur du in deinem Bucket aufbauen möchtest.

Wie ich oben schon erwähnt habe, beschränkt das “Expires” Attribut die URL zeitlich. Wenn du mehr über getSignedUrl erfahren willst, klicke auf dem Link.

Sobald die Datei hochgeladen ist, erhalten wir den Key und den Link zur Datei, diese geben wir zurück, um sie beispielsweise in unserer Datenbank mit abzulegen.

Dritte Schritt: Löschen die Datei

Das Löschen einer hochgeladenen Datei lässt sich ebenso einfach umsetzen. Man braucht nur das Bucket und den Name der Datei. Wenn man mehrere Buckets benutzt, dann speichert man lieber den Bucketnamen auch in der Datenbank mit ab. Beide Namen lassen sich dann aus der Datenbank ziehen. Nach dem erfolgreichen Löschen im Bucket, musst du natürlich die Datei auch aus deiner Datenbank löschen. 

export const deleteObjectByKey = key => {
  const params = {
    Bucket: process.env.VUE_APP_AWS_BUCKET,
    Key: key,
  }
  const data = s3.deleteObject(params).promise()

  return data
}

Upload Komponente in Vue mit Filepond

Wenn du deinen File-Upload nicht selbst stylen möchtest ist filepond sehr zu empfehlen. Mit der Bibliothek kannst in wenigen Minuten ein professionelles UI für den Upload implementieren.


Schritt 1: FilePond Komponente

Um die Bibliotheken nutzen zu können, fügen wir sie mit yarn wieder zu den Projekt Dependencies hinzu.

yarn add vue-filepont filepond-plugin-file-validate-type filepond-plugin-image-preview filepond-plugin-image-crop filepond-plugin-image-transform

Nach erfolgreichem Hinzufügen, kannst du vue-filepond in der gewünschten Vue-Komponente importieren.

import vueFilePond from 'vue-filepond'
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type'
import FilePondPluginImagePreview from 'filepond-plugin-image-preview'
import FilePondPluginImageCrop from 'filepond-plugin-image-crop'
import FilePondPluginImageTransform from 'filepond-plugin-image-transform'
  <FilePond
    ref="pond"
      :server="{
      process: (fieldName, file, metadata, load, error, progress, abort) => {
        uploadFile(file, metadata, load, error, progress, abort)
      },
    }"
    @removefile="onRemoveFile"
  />

Nun zur FilePond Komponente: Ref wird benötigt, um die Methode wie processFiles, addFile usw. mit den Komponente zu verbinden. Wenn die Datei bearbeitet wird, dann wird unsere uploadImages Methode mit den Parametern ausgeführt. Wichtig, die AWS Methoden müssen ebenso aus der aws.js importiert werden.

import { singleUpload, deleteObjectByKey } from '@/aws.js'

Schritt 2: So geht’s mit dem File-Upload

Der File-Upload in unserer Vue-App lässt sich nun recht einfach umsetzen. Wir rufen unsere uploadFile Methode mit der Datei und dem gewünschten Ordner als Parameter auf. Wenn der Upload erfolgreich war, erhalten wir eine Antwort mit dem Status 200.

async uploadFile(file, metadata, load, error, progress, abort){
      const result = await singleUpload(
        file,
        this.$route.params.teamSlug // folder of the file, you should change it to your variable or a string
      )
      if (result.status === 200) {
        // Handle storing it to your database here
        load(file) // Let FilePond know the processing is done
      } else {
        error() // Let FilePond know the upload was unsuccessful
      }
      return {
        abort: () => {
          // This function is entered if the user has tapped the cancel button
          // Let FilePond know the request has been cancelled
          abort()
        },
      }
},

Schritt 3: Rendern der Bilder

Um Dateien in seiner Vue-App später anzuzeigen, müssen spezifischen Datei-Daten wie Key und Url in der Datenbank gespeichert werden. Wenn man nur Bilder speichert, dann reicht auch nur der Key, da die URL in einem Computed Objekt generiert werden kann.

computed: {
  imgSrcArray: () => {
    return this.keys.map(url => 'https://s3.eu-central-1.amazonaws.com/vue-fileupload-example/' + url)
  },
},

Wichtig: Tausche eu-central-1 gegen deine Bucket-region und vue-fileupload-example gegen euren Bucket-name! Dann kannst du mir v-for beispielsweise eine Liste von Bilder rendern.

<img v-for="src in imgSrcArray" :src="src"/>

Schritt 4: Entfernung von Dateien

Im Schritt 1 habt ihr schon wahrscheinlich den v-on remove bemerkt. Jetzt zeige ich euch die Methode, die beim Löschen ausgeführt werden wird.

async onRemoveFile(event) {
      let url = this.$route.params.teamSlug + '/' + event.file.name // event.file.name has only the name after the slash / so the actual filename
      const res = await deleteObjectByKey(url)
      if (
        res.$response.httpResponse.statusCode >= 200 &&
        res.$response.httpResponse.statusCode < 300
      ){
        // here remove data from database too
      }
    },

Der StatusCode der Antwort zwischen 200 und 300 heißt, dass die Datei entweder gelöscht worden ist oder diese gar nicht existiert.

Resumé

Mithilfe eines AWS Buckets und der signedURL Funktion ist es relativ einfach einen Datei-Upload ohne große Einbindung des Backends zu realisieren. So wird das Backend nicht unnötig unter Last gesetzt. In Verbindung mit Vue und Filepond ist der gewünschte Upload über das Frontend einsatzbereit.