Build a basic UI for uploading data to s3 using AWS Amplify

Disclaimer: I am not a web developer and I do not have knowledge of JavaScript. The code snippet is mostly taken from different sources and tweaked for my specific use case. 🙂

I was looking for a way to enable a user to upload bunch of small size (< 10 MB) csv files to a specific S3 bucket. Exploring the ways I came across AWS Amplify service and I felt AWS Amplify makes it easy to deploy a full-stack for someone like me who does not have much knowledge of web app development. Though in the end I created limited console access for the user and taught how to use aws console to upload the data to s3 bucket.

This blog is mostly an attempt by me to show how I experimented and created a Web-UI using AWS Amplify Framework.

So follow along the steps and build a very basic UI for uploading data to S3 using AWS Amplify –

1. Create a AWS Cloud 9 IDE environment –

  • AWS Console > AWS Cloud9 > Environments > Create Environment
  • Enter Environment Name and description
  • Choose the environment type – Creates a new EC2 instance for environment (direct access)
  • Choose Instance type – m5.large (8 GiB RAM + 2 vCPU)
  • Platform : Amazon Linux 2 (recommended)
  • Cost-saving setting : After a week
  • IAM Role : AWS Cloud9 creates a service-linked role
  • Review the summary and create environment

2. Open IDE and add a configuration file to specify the default AWS region.

cat <<END > ~/.aws/config
[default]
region=us-east-1
END

3. Create React app – Create a new React project using React cli

npx create-react-app amplify-s3uploader
Success! Created amplify-s3uploader at /home/ec2-user/environment/amplify-s3uploader
Inside that directory, you can run several commands:

  npm start
    Starts the development server.

  npm run build
    Bundles the app into static files for production.

  npm test
    Starts the test runner.

  npm run eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:

  cd amplify-s3uploader
  npm start

Happy hacking!

Creating the app creates the below folder structure –

Folder Structure

4. Install and configure the AWS Amplify CLI

npm install -g @aws-amplify/cli

anand:~/environment/amplify-s3uploader (master) $ which amplify
~/.nvm/versions/node/v10.24.0/bin/amplify

5. Install the required Amplify libraries

npm install --save aws-amplify aws-amplify-react @aws-amplify/ui-react @aws-amplify/ui-components  @reach/router uuid semantic-ui-react semantic-ui-css

npm install --save @reach/router uuid semantic-ui-react semantic-ui-css

You can check the currently installed libraries in Package.json file:

anand:~/environment/amplify-s3uploader (master) $ cat package.json 
{
  "name": "amplify-s3uploader",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@aws-amplify/ui-components": "^1.1.1",
    "@aws-amplify/ui-react": "^1.0.6",
    "@reach/router": "^1.3.4",
    "@testing-library/jest-dom": "^5.11.10",
    "@testing-library/react": "^11.2.5",
    "@testing-library/user-event": "^12.8.3",
    "aws-amplify": "^3.3.26",
    "aws-amplify-react": "^4.2.30",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-scripts": "4.0.3",
    "semantic-ui-css": "^2.4.1",
    "semantic-ui-react": "^2.0.3",
    "uuid": "^8.3.2",
    "web-vitals": "^1.1.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

anand:~/environment/amplify-s3uploader (master) $ 

6. Initialize a new backend using amplify init

anand:~/environment/amplify-s3uploader (master) $ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project amplifys3uploader
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using react
? Source Directory Path:  src
? Distribution Directory Path: build
? Build Command:  npm run-script build
? Start Command: npm run-script start
Using default provider  awscloudformation
? Select the authentication method you want to use: AWS access keys
? accessKeyId:  ********************
? secretAccessKey:  ****************************************
? region:  us-west-2
Adding backend environment dev to AWS Amplify Console app: db81kl0z2v353
⠼ Initializing project in the cloud...

Initializing the projects creates CloudFormation template to deploy the project.

7. Configuring the React Application

To configure the app, open src/index.js in your IDE and add the following four lines (prefixed with +) of code just after the last import statement (do not include the ‘+’ signs):

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

+ import 'semantic-ui-css/semantic.min.css'
+ import Amplify from 'aws-amplify'
+ import awsconfig from './aws-exports'

+ Amplify.configure(awsconfig)

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

With the above changes made start the React development server. Once the web server is running, click the “Preview” menu and select “Preview Running Application”.

anand:~/environment/amplify-s3uploader (master) $ npm start

8. Add authentication to Amplify project using amplify add auth

anand:~/environment/amplify-s3uploader (master) $ amplify add auth
Using service: Cognito, provided by: awscloudformation
 
 The current configured provider is Amazon Cognito. 
 
 Do you want to use the default authentication and security configuration? Default configuration
 Warning: you will not be able to edit these selections. 
 How do you want users to be able to sign in? Username
 Do you want to configure advanced settings? No, I am done.
Successfully added auth resource amplifys3uploader4144b6d8 locally

Some next steps:
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

anand:~/environment/amplify-s3uploader (master) $ amplify push
✔ Successfully pulled backend environment dev from the cloud.

Current Environment: dev

| Category | Resource name             | Operation | Provider plugin   |
| -------- | ------------------------- | --------- | ----------------- |
| Auth     | amplifys3uploader4144b6d8 | Create    | awscloudformation |
? Are you sure you want to continue? Yes
⠏ Updating resources in the cloud. This may take a few minutes...

This creates a nested CloudFormation stack to create UserPool and other resources in Amazon Cognito.

anand:~/environment/amplify-s3uploader (master) $ cat src/aws-exports.js
/* eslint-disable */
// WARNING: DO NOT EDIT. This file is automatically generated by AWS Amplify. It will be overwritten.

const awsmobile = {
    "aws_project_region": "us-east-1",
    "aws_cognito_identity_pool_id": "us-east-1:1552f126-7060-4c2f-88c8-dd2cea7d1775",
    "aws_cognito_region": "us-east-1",
    "aws_user_pools_id": "us-east-1_MVBQDuBmU",
    "aws_user_pools_web_client_id": "4thkttca9vkc2pg9dkd65tum4r",
    "oauth": {}
};

export default awsmobile;
anand:~/environment/amplify-s3uploader (master) $ 

Now, that we have our user pool configured we can add authentication. To do so, open src/App.js and edit as mentioned below –

Add the below import:

import { withAuthenticator } from '@aws-amplify/ui-react';

Replace the last line in src/App.js with:

export default withAuthenticator(App);

With these changes the preview of web page has the “Sign in to your account” authentication page.

To access the user name and other information about the currently logged in user add a new file in the src directory named useAmplifyAuth.js

src/useAmplifyAuth.js
import { useReducer, useState, useEffect } from 'react';
import { Auth, Hub } from 'aws-amplify';

const initalState = {
  isLoading: true,
  error: false,
  user: null
}

function reducer(state, action) {
  switch(action.type) {
    case 'init':
      return { ...state, isLoading: true, error: false }
    case 'success':
      return { ...state, isLoading: false, error: false, user: action.user }
    case 'reset':
      return { ...state, user: null }
    case 'error':
      return { ...state, isLoading: false, error: true }
    default:
      new Error();
  }
}

function useAmplifyAuth() {
  const [state, dispatch] = useReducer(reducer, initalState);
  const [fetchTrigger, setFetchTrigger] = useState(false);

  useEffect(() => {
    let isMounted = true;

    const fetchUser = async () => {
      if (isMounted) {
        dispatch({ type: 'init' });
      }

      try {
        if (isMounted) {
          const authData = await Auth.currentUserInfo();
          if (authData) {
            dispatch({ type: 'success', user: authData });
          }
        }
      } catch (error) {
        if (isMounted) {
          console.error('[ERROR - useAmplifyAuth]', error);
          dispatch({ type: 'error' });
        }
      }
    };

    const HubListener = () => {
      Hub.listen('auth', data => {
        const { payload } = data;
        onAuthEvent(payload);
      });
    };

    const onAuthEvent = (payload) => {
      switch(payload.event) {
        case 'signIn':
          // on signin, we want to rerun effect, trigger via flag
          if (isMounted) { setFetchTrigger(true); }
          break;
        default:
          // ignore anything else
          return;
      }
    };

    HubListener();
    fetchUser();

    // on tear down...
    return () => {
      Hub.remove('auth');
      isMounted = false;
    }
  }, [fetchTrigger]);

  const onSignOut = async () => {
    try {
      await Auth.signOut();
      setFetchTrigger(false);
      dispatch({ type: 'reset' })
    } catch (error) {
      console.error('[ERROR - useAmplifyAuth]', error);
    }
  };

  return { state, onSignOut };
}

export default useAmplifyAuth;

Next, update the application to use this new functionality and remove the boilerplate starter page. Open src/App.js and replace its contents with the following:

src/App.js
import './App.css';

import React from 'react';
import { Container, Menu } from 'semantic-ui-react';
import { Link } from "@reach/router";

import { withAuthenticator } from '@aws-amplify/ui-react';
import useAmplifyAuth from './useAmplifyAuth';

export const UserContext = React.createContext();

function App() {
  const { state: { user }, onSignOut } = useAmplifyAuth();

  function UserData(props) {
    return !user ? (
      <div></div>
    ) : (
      <div>Welcome {user.username} (<Link to="/" onClick={onSignOut}>Sign Out</Link>)</div>
    );
  }

  return (
    <div>
      <Menu fixed='top' borderless inverted>
        <Container>
          <Menu.Item as={Link} to='/' header>
            Amplify S3 Uploader
          </Menu.Item>

          <Menu.Menu position='right'>
            <Menu.Item>
              <UserData></UserData>
            </Menu.Item>
          </Menu.Menu>
        </Container>
      </Menu>

      <Container text style={{ marginTop: '5em' }}>
        <UserContext.Provider user={ user }>
          <p>To be updated...</p>
        </UserContext.Provider>
      </Container>    
    </div>
  );
}

export default withAuthenticator(App);

9. Add Storage to the application using amplify add storage

Let’s add storage to the application. In this case I will add a new S3 bucket to store the files uploaded by user. Amplify Storage module will do the trick here. The way we added Authentication module similarly we can add the storage module using the amplify add command.

anand:~/environment/amplify-s3uploader (master) $ amplify add storage
? Please select from one of the below mentioned services: Content (Images, audio, video, etc.)
? Please provide a friendly name for your resource that will be used to label this category in the project: amplifys3uploader
? Please provide bucket name: anand-amplifys3uploader
? Who should have access: Auth users only
? What kind of access do you want for Authenticated users? create/update, read, delete
? Do you want to add a Lambda Trigger for your S3 Bucket? No
Successfully added resource amplifys3uploader locally

If a user is part of a user pool group, run "amplify update storage" to enable IAM group policies for CRUD operations
Some next steps:
"amplify push" builds all of your local backend resources and provisions them in the cloud
"amplify publish" builds all of your local backend and front-end resources (if you added hosting category) and provisions them in the cloud

anand:~/environment/amplify-s3uploader (master) $ amplify status

Current Environment: dev

| Category | Resource name             | Operation | Provider plugin   |
| -------- | ------------------------- | --------- | ----------------- |
| Storage  | amplifys3uploader         | Create    | awscloudformation |
| Auth     | amplifys3uploader4144b6d8 | No Change | awscloudformation |

As a next step, executing the amplify push command, will create and execute nested CloudFormation template to create the S3 bucket. The S3 bucket will have CORS ( Cross Origin Resource Sharing ) enabled. This is needed for Amplify to interact with this bucket through the S3 Rest API endpoints.

Update src/App.js file to have the functionality to browse and upload the file. Below is the full source code of src/App.js file –

import React, { useState } from "react";
import { Container, Menu } from 'semantic-ui-react';
import { Link } from "@reach/router";

import { withAuthenticator } from '@aws-amplify/ui-react';
import { Storage } from "aws-amplify";
import useAmplifyAuth from './useAmplifyAuth';

export const UserContext = React.createContext();

function App() {
    const { state: { user }, onSignOut } = useAmplifyAuth();

    function UserData(props) {
        return !user ? (
            <div></div>
        ) : (
                <div>Welcome {user.username} (<Link to="/" onClick={onSignOut}>Sign Out</Link>)</div>
            );
    }

    const [loading, setLoading] = useState(false);

    const handleChange = async (e) => {
        const fileContent = e.target.files[0]
        /*const fileName = e.target.files[0].name*/
        const fileType = e.target.files[0].type

        let ext = fileContent.name.split(".").pop().toLowerCase();
        let fileFormats = ["csv"];
        if (!fileFormats.includes(ext)) {
            console.log("Invalid file format");
            return false;
        }

        let fileName =
            "data/" +
            fileContent.name.substr(0, fileContent.name.indexOf(ext) - 1) +
            "." +
            ext;

        try {
            setLoading(true);
            // Upload the file to s3 with private access level.
            await Storage.put(fileName, fileContent, {
                contentType: fileType,
                level: 'private',
                progressCallback(progress) {
                    console.log(`Uploaded: ${progress.loaded}/${progress.total}`);
                },
            })
            setLoading(false);
        } catch (err) {
            console.log(err);
        }
    }
    return (
        <div>
            <Menu fixed='top' borderless inverted>
                <Container>
                    <Menu.Item as={Link} to='/' header>
                        Amplify S3 Uploader
            </Menu.Item>

                    <Menu.Menu position='right'>
                        <Menu.Item>
                            <UserData></UserData>
                        </Menu.Item>
                    </Menu.Menu>
                </Container>
            </Menu>

            <Container text style={{ marginTop: '5em' }}>
                <UserContext.Provider user={user}>
                    <div className="App">
                        <h1> Upload CSV File to S3 </h1>
                        {loading ? <h3>Uploading...</h3> : <input
                            type="file" accept="text/csv"
                            onChange={(evt) => handleChange(evt)}
                        />}
                    </div>
                </UserContext.Provider>
            </Container>
        </div>

    );
}

// withAuthenticator wraps your App with a Login component
export default withAuthenticator(App);

After updating the src/App.js file, start the React server using npm start command. Clicking on the pane highlighted in red, the web page will open in a new tab.

Test uploading a .csv file. Here I chose to upload anand.csv file.

After the upload is complete you can view the file in S3 bucket. The Amplify Storage module provides mechanism to manage the user content in public, protected or private storage buckets. In my case the file is uploaded to private prefix in the bucket.

10. Hosting the app

To host the app we will create a Git repository to manage application source code and deploy the application using Amplify Console. I will be using AWS CodeCommit to host Git repository which is integrated well with AWS Amplify. Enter the below commands to create the repository and commit the project to source control –

anand:~/environment/amplify-s3uploader (master) $ git config --global credential.helper '!aws codecommit credential-helper $@'

anand:~/environment/amplify-s3uploader (master) $ git config --global credential.UseHttpPath true

anand:~/environment/amplify-s3uploader (master) $ export REPO=$(aws codecommit create-repository --repository-name AmplifyS3Uploader \
>         --repository-description "Upload csv files to s3 using AWS Amplify" \
>         --query 'repositoryMetadata.cloneUrlHttp' \
>         --output text)
anand:~/environment/amplify-s3uploader (master) $ git remote add origin $REPO
anand:~/environment/amplify-s3uploader (master) $ git add .
anand:~/environment/amplify-s3uploader (master) $ git commit -m "Initial AmplifyS3Uploader commit"
anand:~/environment/amplify-s3uploader (master) $ git push origin master
Enumerating objects: 52, done.
Counting objects: 100% (52/52), done.
Delta compression using up to 2 threads
Compressing objects: 100% (48/48), done.
Writing objects: 100% (52/52), 218.45 KiB | 8.09 MiB/s, done.
Total 52 (delta 5), reused 0 (delta 0)
To https://git-codecommit.us-east-1.amazonaws.com/v1/repos/AmplifyS3Uploader
 * [new branch]      master -> master
anand:~/environment/amplify-s3uploader (master) $ 

Going to Console > CodeCommit > Repositories, will list the repository we created.

Next, navigate to Amplify Console and click the “Get Started” button under Deploy. Select CodeCommit and click on Continue.

Select the newly created repository (AmplifyS3Uploader) and branch (master). Click “Next”.

Choose “dev” as backend environment and create a new service role. Click the “Create new role” button and follow the IAM role creation flow.

Finally review the settings and click “Save and Deploy”

After few minutes, Amplify will have the build deployed and provide a link to view the application.

Open the web app and test to upload the file.

Authentication window

Choose file to upload to S3

Uploaded file in S3

You can go a step further and use Amplify Analytics module which is powered by Amazon Pinpoint to understand and measure user engagement. Let’s take a quick look –

11. Add Analytics module using amplify add analytics

anand:~/environment/amplify-s3uploader (master) $ amplify add analytics
? Select an Analytics provider Amazon Pinpoint
? Provide your pinpoint resource name: amplifys3uploader
Auth configuration is required to allow unauthenticated users, but it is not configured properly.
Adding analytics would add the Auth category to the project if not already added.
? Apps need authorization to send analytics events. Do you want to allow guests and unauthenticated users to send analytics eve
nts? (we recommend you allow this when getting started) Yes
Successfully updated auth resource locally.
Successfully added resource amplifys3uploader locally

Some next steps:
"amplify push" builds all of your local backend resources and provisions them in the cloud
"amplify publish" builds all your local backend and front-end resources (if you have hosting category added) and provisions them in the cloud

anand:~/environment/amplify-s3uploader (master) $ 
anand:~/environment/amplify-s3uploader (master) $  amplify push

Enable tracking by modifying src/index.js. Below is the copy of index.js –

mport React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

import 'semantic-ui-css/semantic.min.css'
import Amplify, { Analytics } from 'aws-amplify';
import awsconfig from './aws-exports'
Amplify.configure(awsconfig)
Analytics.autoTrack('pageView', { enable: true, type: 'SPA' });

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Below is the screenshot from AWS Pinpoint service

To conclude, AWS Amplify service made developing and deploying a full stack serverless app simple and easy.

References –

https://github.com/amazon-archives/aws-reinvent-2019-mobile-workshops/tree/master/MOB303

https://docs.amplify.aws/lib/storage/getting-started/q/platform/js

https://medium.com/@anjanava.biswas/uploading-files-to-aws-s3-from-react-app-using-aws-amplify-b286dbad2dd7

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s