DEV Community

Cover image for Event Based System with Localstack (Elixir Edition): Uploading files to S3 with PresignedURL's
Nicol Acosta
Nicol Acosta

Posted on • Edited on

Event Based System with Localstack (Elixir Edition): Uploading files to S3 with PresignedURL's

Introduction

If you are reading this maybe you know what its Phoenix, but let me explain a few things first:

  • Localstack: Its a local version of AWS, yes, S3, SQS, SNS, DynamoDB and more all available from in your local machine

We will create an a platform that uses a ReactJS Frontend that calls the api in Phoenix and Uploads files to S3 and then notifies the file upload to a SQS Queue and then we process the messages and notifies using PhoenixChannels to all the connected frontends

Complex? sounds like but its not a big deal, lets start it!

The real work

First steps

You need to create your base, an a Frontend and a Backend in Phoenix, maybe you have it so lets move to the important part

You need Docker and Compose to use this examples, strongly recommend because makes easy with almost 0 config on the Localstack side, so all the code must assume that you have your project dockerized and configured with compose

Adding Localstack

This is the main config on the docker-compose.yml file

 localstack: image: localstack/localstack:latest ports: - 4566:4566 environment: # own env vars BUCKET_NAME: files DEFAULT_REGION: us-west-2 AWS_ACCESS_KEY_ID: test AWS_SECRET_ACCESS_KEY: test # service env vars SERVICES: s3 DISABLE_CORS_CHECKS: 1 PROVIDER_OVERRIDE_S3: asf S3_SKIP_SIGNATURE_VALIDATION: 1 volumes: - ./.localstack:/var/lib/localstack - ./init_localstack.sh:/etc/localstack/init/ready.d/init_localstack.sh 
Enter fullscreen mode Exit fullscreen mode

And this is the init_localstack.sh file content, a unique thing about localstack its that you can move all strings like an aws-cli tool, also the container deletes all the content and config once the container stops, so the script file must create all the resources that you need from Localstack

Basically the init file creates the bucket and add cors to ensure the calls must be allowed

#!/bin/bash echo "########### Create S3 bucket ###########" awslocal s3api create-bucket\ --region $DEFAULT_REGION\ --bucket $BUCKET_NAME\ --create-bucket-configuration LocationConstraint=$DEFAULT_REGION echo "########### Adding cors bucket ###########" awslocal s3api put-bucket-cors\ --bucket $BUCKET_NAME\ --cors-configuration '{ "CORSRules": [ { "AllowedHeaders": ["*"], "AllowedMethods" : ["HEAD", "GET", "POST", "PUT", "DELETE"], "AllowedOrigins" : [ "http://localhost:4000", "http://localhost:3000" ], "ExposeHeaders": [] } ] }' echo "########### List S3 bucket ###########" awslocal s3api list-buckets 
Enter fullscreen mode Exit fullscreen mode

In this examples we will use a feature of aws named presigned url's that its a url with signatures that will allow to the read/write from any caller, to ensure your browser can reach the Localstack container, must add this line to your /etc/hosts file

127.0.0.1 localstack 
Enter fullscreen mode Exit fullscreen mode

Adding AWS to Phoenix

We will use the library ex_aws to manage all the AWS resources

Add this to your mix.exs

{:ex_aws, "~> 2.1"}, {:ex_aws_s3, "~> 2.0"} 
Enter fullscreen mode Exit fullscreen mode

And add this to your config files (adjust in dev.exs and runtime.exs)

config :ex_aws, :s3, scheme: "http://", region: System.get_env("AWS_S3_REGION"), host: System.get_env("AWS_S3_URL"), port: System.get_env("AWS_S3_PORT"), bucket: System.get_env("AWS_S3_BUCKET") 
Enter fullscreen mode Exit fullscreen mode
  • Generating the Presigned URL

This function will generate the Presigned URL

 def presigned_url_upload(object_key, opts \\ []) do get_presigned_url(:put, object_key, opts) end defp get_presigned_url(http_method, object_key, opts) do # this its for a configuration issue with localhost opts = if is_prod() do opts else opts |> Keyword.put(:virtual_host, is_prod()) |> Keyword.put(:bucket_as_host, true) end ExAws.S3.presigned_url( config(), http_method, s3_bucket(), object_key, opts ) end 
Enter fullscreen mode Exit fullscreen mode

Full code here

and sharing the file

 # on some controller # 30 mins @presigned_upload_url_max_age 60 * 30 # 10 MB @presigned_upload_url_max_file_size 10 * 1_000_000 @presigned_url_opts [ ["starts-with", "$Content-Type", ""], {:expires_in, @presigned_upload_url_max_age}, {:content_length_range, [1, @presigned_upload_url_max_file_size]} ] def get_presigned_url(conn, _) do {:ok, url} = S3Client.presigned_url_upload("some_file_name", @presigned_url_opts) conn |> put_status(200) |> json(%{url: url}) end 
Enter fullscreen mode Exit fullscreen mode

Uploading files from Frontend

First lets add the form component, something simple

 <input type="file" onChange={handleFileChange} /> {uploaded && <span className="uploaded"> Uploaded </span> } 
Enter fullscreen mode Exit fullscreen mode

And upload it when a file its selected, this function example must check the if its a valid file, then get the presigned url and upload to this presigned url using fetch on a put method

 const [uploaded, setUploaded] = useState(false); const handleFileChange = async (e) => { if (!e.target.files) { return; } // get the presigned url let response = await api.getPresignedUrl(); if (response.status < 200 && response.status > 300) { // show error getting presigned url return; } const file = e.target.files[0]; const {url} = response.body; // uploading the file with put request to the presigned url response = await fetch(url, { body: file, method: "PUT" }); if (response.ok && response.status >= 200 && response.status < 300) { setUploaded(true); setTimeout(setUploaded.bind(this, false), 5000); // will remove the  } }; 
Enter fullscreen mode Exit fullscreen mode

And its all, you are pushing files to s3, the config its more extense than the real code but, a lot of times integrate some library or platforms to our code its more config than realworld code

The full code of this project its here and also includes a project structure front/back in same repo with docker compose config, just check the Makefile to see the available commands

In the next post we will:

  • Add SQS Queues and add a Worker per Queue using Broadway
  • Configure S3 Notification to SQS Queue When a file its uploaded
  • Post Messages to a Queue using Elixir

Part 2 here!

See you in the next posts

Top comments (0)