├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .graphqlconfig.yml ├── LICENSE ├── Makefile ├── NOTICE.txt ├── README.md ├── Workshop ├── README.md ├── additional-steps.md ├── clean-up.md ├── images │ ├── 0-resource-setup.png │ ├── 0a-cfn-create-change-set.png │ ├── 0a-cfn-execute-change-set.png │ ├── 0b-cfn-create-change-set.png │ ├── 0b-cfn-execute-change-set.png │ ├── 0b-cfn-outputs.png │ ├── 0b-code-pipeline.png │ ├── 0b-webapp.png │ ├── 1-auto-IAM-role.png │ ├── 1a-step-easy-name.png │ ├── 1a-step-lambda-details.png │ ├── 1b-pick-state-role.png │ ├── 1b-step-console-preview.png │ ├── 1c-dashboard.png │ ├── 1c-execution.png │ ├── 1c-input-old.png │ ├── 1c-input.png │ ├── 1c-start-new-execution.png │ ├── 1d-edit.png │ ├── 1d-output-w-resultpath.png │ ├── 1d-start-execution.png │ ├── 1d-updated-new-execution.png │ ├── 2-branching-logic-state-machine.png │ ├── 2a-reimport-step-easy.png │ ├── 2a-step-easy-Paralelldetails.png │ ├── 2a-step-easy-canvas.png │ ├── 2a-step-easy-choice-ImageTypeCheckChoicedetails.png │ ├── 2a-step-easy-choice-ImageTypeCheckconfigurecondition.png │ ├── 2a-step-easy-choice-ImageTypeCheckdetails.png │ ├── 2a-step-easy-fail-notsupportedimage.png │ ├── 2a-step-easy-fail-notsupportedimagecatchers.png │ ├── 2a-step-easy-fail-notsupportedimagedetails.png │ ├── 2b-step-console-update-preview.png │ ├── 2c-test-catch-failed.png │ ├── 2c-test-choice-test-failed.png │ ├── 2c-test-choice-test-succeeded.png │ ├── 3-create-statemachine-select-role.png │ ├── 3-state-machine-parallel-with-output.png │ ├── 3-state-machine-parallel.png │ ├── 4-add-step.png │ ├── 4-final-status.png │ ├── 4b-step-console-preview.png │ ├── 4c-ddb-imagemetadata-item.png │ ├── 4c-ddb-imagemetadata.png │ ├── 4c-test-choice-test-succeeded.png │ ├── 5a-enviroment-variables.png │ ├── 5a-state-machine-arn-new.png │ ├── 5a-state-machine-arn-newer.png │ ├── 5a-state-machine-arn.png │ ├── 5b-s3-event-configuration.png │ ├── 5b-s3-events.png │ ├── 5b-s3-incoming-folder.png │ ├── 5c-s3-object-metadata.png │ ├── 5c-state-machine-execution.png │ ├── additional-step-inappropiate.png │ ├── bucket-sync-state-machine.png │ ├── bulk-import-album1.png │ ├── bulk-import-albumlist.png │ ├── bulk-import-user.png │ ├── cfn-launch-stack.png │ ├── cloudwatch-state-machine-metrics.png │ ├── dynamo-screenshot.png │ ├── single-step-state-machine.png │ ├── step-easy-intro.png │ └── web-app-screenshot.png ├── step-0.md ├── step-1.md ├── step-2.md ├── step-3.md ├── step-4.md ├── step-5.md └── step-6.md ├── amplify.yml ├── amplify ├── .config │ └── project-config.json ├── README.md ├── backend │ ├── api │ │ └── photoshare │ │ │ ├── parameters.json │ │ │ ├── resolvers │ │ │ ├── Mutation.startSfnExecution.req.vtl │ │ │ ├── Mutation.startSfnExecution.res.vtl │ │ │ ├── Query.checkSfnStatus.req.vtl │ │ │ └── Query.checkSfnStatus.res.vtl │ │ │ ├── schema.graphql │ │ │ ├── stacks │ │ │ └── CustomResources.json │ │ │ └── transform.conf.json │ ├── auth │ │ └── photoshareb143529b │ │ │ ├── parameters.json │ │ │ └── photoshareb143529b-cloudformation-template.yml │ ├── backend-config.json │ ├── function │ │ └── S3Trigger984fb593 │ │ │ ├── S3Trigger984fb593-cloudformation-template.json │ │ │ ├── amplify.state │ │ │ ├── function-parameters.json │ │ │ ├── parameters.json │ │ │ └── src │ │ │ ├── event.json │ │ │ ├── index.js │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── reference-index.js │ └── storage │ │ └── photostorage │ │ ├── parameters.json │ │ ├── s3-cloudformation-template.json │ │ └── storage-params.json └── team-provider-info.json ├── cloudformation ├── image-processing.serverless.yaml └── state-machine.asl.json ├── images ├── amplify-select-role.png ├── app-create-album.png ├── app-screenshot.png ├── app-signup-screenshot.png ├── example-album.png ├── example-analyzed.png ├── example-processing.png ├── photo-processing-backend-diagram.png └── step-function-execution.png ├── lambda-functions ├── extract-image-metadata │ ├── index.js │ ├── lambda-sample-output.json │ ├── local-testing.sh │ ├── package-lock.json │ ├── package.json │ ├── step-sample-input.json │ └── step-sample-output.json ├── rekognition │ ├── index.js │ ├── package-lock.json │ ├── package.json │ ├── step-sample-input.json │ └── step-sample-output.json ├── store-image-metadata │ ├── index.js │ ├── package-lock.json │ ├── package.json │ └── step-sample-input.json ├── thumbnail │ ├── index.js │ ├── lambda-sample-input.json │ ├── package-lock.json │ └── package.json └── transform-metadata │ ├── index.js │ ├── lambda-sample-input.json │ ├── lambda-sample-output.json │ ├── local-testing.sh │ ├── package-lock.json │ └── package.json ├── package-lock.json └── src └── react-frontend ├── .eslintcache ├── .gitignore ├── .graphqlconfig.yml ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── components ├── Album.js ├── AlbumDetail.js └── PhotoList.js ├── graphql ├── mutations.js ├── queries.js ├── schema.json └── subscriptions.js ├── index.css ├── index.js ├── logo.svg ├── serviceWorker.js ├── setupTests.js └── utils.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [amplify/**.json] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "src/react-frontend" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "npm" 9 | directory: "lambda-functions/extract-image-metadata" 10 | schedule: 11 | interval: "daily" 12 | 13 | - package-ecosystem: "npm" 14 | directory: "lambda-functions/rekognition" 15 | schedule: 16 | interval: "daily" 17 | 18 | - package-ecosystem: "npm" 19 | directory: "lambda-functions/store-image-metadata" 20 | schedule: 21 | interval: "daily" 22 | 23 | - package-ecosystem: "npm" 24 | directory: "lambda-functions/thumbnail" 25 | schedule: 26 | interval: "daily" 27 | 28 | - package-ecosystem: "npm" 29 | directory: "lambda-functions/transform-metadata" 30 | schedule: 31 | interval: "daily" 32 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '21 9 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies files to intentionally ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | .idea 5 | .DS_Store 6 | 7 | **/node_modules/ 8 | */java/ 9 | *.iml 10 | 11 | #amplify 12 | amplify/\#current-cloud-backend 13 | amplify/.config/local-* 14 | amplify/mock-data 15 | amplify/backend/amplify-meta.json 16 | amplify/backend/awscloudformation 17 | build/ 18 | dist/ 19 | node_modules/ 20 | aws-exports.js 21 | awsconfiguration.json 22 | amplifyconfiguration.json 23 | amplify-build-config.json 24 | amplify-gradle-config.json 25 | amplifyxc.config 26 | .vscode/targets.log 27 | .vscode/dryrun.log 28 | .vscode/configurationCache.log -------------------------------------------------------------------------------- /.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | photoshare: 3 | schemaPath: src/react-frontend/src/graphql/schema.json 4 | includes: 5 | - src/graphql/**/*.js 6 | excludes: 7 | - ./amplify/** 8 | extensions: 9 | amplify: 10 | codeGenTarget: javascript 11 | generatedFileName: '' 12 | docsFilePath: src/react-frontend/src/graphql 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | AWS_BRANCH ?= dev 2 | STACK_NAME ?= photo-sharing 3 | GRAPHQL_API_ID ?= "UNDEFINED" 4 | APPSYNC_URL ?= "UNDEFINED" 5 | PHOTO_BUCKET ?= "UNDEFINED" 6 | TEMPLATE_NAME = image-processing 7 | 8 | target: 9 | $(info ${HELP_MESSAGE}) 10 | @exit 0 11 | 12 | init: ##=> Install OS deps and dev tools 13 | $(info [*] Initializing...) 14 | @$(MAKE) _install_os_packages 15 | 16 | 17 | deploy: ##=> Deploy services 18 | $(info [*] Deploying backend...) 19 | 20 | # cd lambda-functions/thumbnail && npm install && \ 21 | # cd ../extract-image-metadata && npm install && \ 22 | # cd ../store-image-metadata && npm install && \ 23 | 24 | cd cloudformation/ && \ 25 | sam build --template ${TEMPLATE_NAME}.serverless.yaml && \ 26 | sam package \ 27 | --s3-bucket ${DEPLOYMENT_BUCKET_NAME} \ 28 | --s3-prefix photo-sharing-app/lambda/ \ 29 | --output-template-file packaged.yaml && \ 30 | sam deploy \ 31 | --template-file packaged.yaml \ 32 | --stack-name ${STACK_NAME}-backend-${AWS_BRANCH} \ 33 | --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND \ 34 | --parameter-overrides \ 35 | PhotoRepoS3Bucket=${PHOTO_BUCKET} \ 36 | GraphQLEndPoint=${APPSYNC_URL} \ 37 | GraphQLAPIId=${GRAPHQL_API_ID} \ 38 | Stage=${AWS_BRANCH} \ 39 | --no-fail-on-empty-changeset 40 | 41 | ############# 42 | # Helpers # 43 | ############# 44 | 45 | _install_os_packages: 46 | $(info [*] Installing jq...) 47 | yum install jq -y 48 | $(info [*] Upgrading Python SAM CLI and CloudFormation linter to the latest version...) 49 | python3 -m pip install --upgrade --user cfn-lint aws-sam-cli 50 | 51 | 52 | define HELP_MESSAGE 53 | 54 | Environment variables: 55 | 56 | These variables are automatically filled at CI time except STRIPE_SECRET_KEY 57 | If doing a dirty/individual/non-ci deployment locally you'd need them to be set 58 | 59 | AWS_BRANCH: "dev" 60 | Description: Feature branch name used as part of stacks name; added by Amplify Console by default 61 | DEPLOYMENT_BUCKET_NAME: "a_valid_bucket_name" 62 | Description: S3 Bucket name used for deployment artifacts 63 | GRAPHQL_API_ID: "hnxochcn4vfdbgp6zaopgcxk2a" 64 | Description: AppSync GraphQL ID already deployed 65 | PHOTO_BUCKET: "" 66 | 67 | Common usage: 68 | 69 | ...::: Bootstraps environment with necessary tools like SAM CLI, cfn-lint, etc. :::... 70 | $ make init 71 | 72 | ...::: Deploy all SAM based services :::... 73 | $ make deploy 74 | 75 | ...::: Delete all SAM based services :::... 76 | $ make delete 77 | 78 | ...::: Export parameter and its value to System Manager Parameter Store :::... 79 | $ make export.parameter NAME="/env/service/amplify/api/id" VALUE="xzklsdio234" 80 | endef 81 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | AWS Serverless Reference Architecture: Image Recognition and Processing Backend 2 | Copyright 2017 Amazon Web Services, Inc. or its affiliates. All Rights Reserved. 3 | 4 | ********************** 5 | THIRD PARTY COMPONENTS 6 | ********************** 7 | The web app used React (https://reactjs.org/) which is under the MIT license. 8 | 9 | 10 | The licenses for these third party components are included in LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Reference Architecture: Image Recognition and Processing Backend 2 | 3 | The Image Recognition and Processing Backend demonstrates how to use [AWS Step Functions](https://aws.amazon.com/step-functions/) to orchestrate a serverless processing workflow using [AWS Lambda](http://aws.amazon.com/lambda/), [Amazon S3](http://aws.amazon.com/s3/), [Amazon DynamoDB](http://aws.amazon.com/dynamodb/) and [Amazon Rekognition](https://aws.amazon.com/rekognition/). This workflow processes photos uploaded to Amazon S3 and extracts metadata from the image such as geolocation, size/format, time, etc. It then uses image recognition to tag objects in the photo. In parallel, it also produces a thumbnail of the photo. 4 | 5 | This repository contains sample code for all the Lambda functions depicted in the diagram below as well as an AWS CloudFormation template for creating the functions and related resources. There is also a test web app that you can run to interact with the backend. 6 | 7 | ![architecture diagram with an Amplify based frontend and a backend processing pipeline orchestrated using Step Functions](images/photo-processing-backend-diagram.png) 8 | 9 | ### Walkthrough of the architecture 10 | 1. An image is uploaded to the `PhotoRepo` S3 bucket under the `private/{userid}/uploads` prefix 11 | 2. The S3 upload event triggers the `S3Trigger` Lambda function, which kicks off an execution of the `ImageProcStateMachine` in AWS Step Functions, passing in the S3 bucket and object key as input parameters. 12 | 3. The `ImageProcStateMachine` has the following sub-steps: 13 | * Read the file from S3 and extract image metadata (format, EXIF data, size, etc.) 14 | * Based on output from previous step, validate if the file uploaded is a supported file format (png or jpg). If not, throw `NotSupportedImageType` error and end execution. 15 | * Store the extracted metadata in the `ImageMetadata` DynamoDB table 16 | * In parallel, kick off two processes simultaneously: 17 | * Call Amazon Rekognition to detect objects in the image file. If detected, store the tags in the `ImageMetadata` DynamoDB table 18 | * Generate a thumbnail and store it under the `private/{userid}/resized` prefix in the `PhotoRepo` S3 bucket 19 | 20 | 21 | ### How to deploy 22 | Follow these instructions to deploy the application (both backend and frontend): 23 | 24 | [![One-click deployment](https://oneclick.amplifyapp.com/button.svg)](https://console.aws.amazon.com/amplify/home#/deploy?repo=https://github.com/aws-samples/lambda-refarch-imagerecognition) 25 | 26 | 1. Use **1-click deployment** button above. Amplify Console will fork this repository in your GitHub account, and deploy the backend and frontend application. 27 | - Note: If you forked and changed the repository first, you can use the Amplify console and select "**Connect App**" to connect to your forked repo. 28 | 1. For IAM Service Role, create one if you don't have one or select an existing role. (This is required because the Amplify Console needs permissions to deploy backend resources on your behalf. More [info](https://docs.aws.amazon.com/amplify/latest/userguide/how-to-service-role-amplify-console.html)) 29 | ![amplify console select role or create new role](images/amplify-select-role.png) 30 | 1. Within your new app in Amplify Console, wait for deployment to complete (this may take a while) 31 | 1. Once the deployment is complete, you can test out the application! 32 | 33 | If you want to make changes to the code locally: 34 | 35 | 1. Clone the repo in your Github account that Amplify created 36 | 1. In the Amplify console, choose **Backend environments**, and toggle "Edit backend" on the environment with categories added 37 | 1. Under **Edit backend**, copy the `amplify pull --appId --envName ` command displayed 38 | - If you don't see this command and instead see `amplify init --appId`, try refreshing the backend environment tab after waiting a few minutes (cloudformation could still be provisioning resources) 39 | 1. Within your forked repository locally, run the command you copied and follow the instructions 40 | 41 | ``` 42 | - This command synchronizes what's deployed to your local Amplify environment 43 | - Do you want to use an AWS profile: Yes 44 | - default 45 | - Choose your default editor: Visutal Studio Code 46 | - Choose the type of app that you're building: javascript 47 | - What javascript framework are you using: react 48 | - Source Directory Path: src/react-frontend/src 49 | - Distribution Directory Path: src/react-frontend/build 50 | - Build Command: npm.cmd run-script build 51 | - Start Command: npm.cmd run-script start 52 | - Do you plan on modifying this backend? (Yes) 53 | ``` 54 | 55 | If at anytime you want to change these options. Look into `amplify/.config/project-config.json` and make your changes there. 56 | 57 | ### Using the test web app 58 | 59 | You can use the test web app to upload images and explore the image recognition and processing workflow. 60 | ![screenshot of the photo sharing app: a photo album showing 4 photos and their respective extracted metadata](images/app-screenshot.png) 61 | 62 | #### Sign up and log in 63 | 64 | 1. Go to the URL of the Amplify app that was deployed 65 | 1. In the login page, click on "**Create account**" 66 | 1. Register an account by following the sign up instructions 67 | 68 | sign up 69 | 70 | 1. After confirming the account, sign in 71 | 72 | ##### Album list 73 | 74 | 1. create albums using the "Add a new album" 75 | ![screenshot of the photo sharing app: album list and create album controls](images/app-create-album.png) 76 | 1. You may need to referresh 77 | 78 | ##### Photo gallery 79 | 80 | 1. Click into an album you created 81 | 1. Upload a photo 82 | ![screenshot of the photo sharing app: showing after upload, a status message showing processing in prgress and a link to the Step Functions state machine execution](images/example-processing.png) 83 | 1. You can follow the Step Functions execution link to review the details of the workflow execution 84 | Below is the diagram of the state machine being executed every time a new image is uploaded 85 | (you can explore this in the Step Functions [Console](https://console.aws.amazon.com/states/home)): 86 | 87 | state machine diagram 88 | 1. When the processing finishes, the photo and extracted information is added to the display 89 | ![screenshot of the photo sharing app: displaying photo metadata extracted from the processing pipeline](images/example-analyzed.png) 90 | 91 | ## Cleaning Up the Application Resources 92 | 93 | To remove all resources created by this example, do the following: 94 | 1. Go to [AWS CloudFormation console](https://console.aws.amazon.com/cloudformation/home), delete the 2 stacks with name "amplify-photoshare-" 95 | 1. Go to the [AWS Amplify console](https://console.aws.amazon.com/amplify/home) and delete the app. 96 | 97 | ## License 98 | 99 | This reference architecture sample is licensed under Apache 2.0. 100 | 101 | 102 | -------------------------------------------------------------------------------- /Workshop/README.md: -------------------------------------------------------------------------------- 1 | 2 | # This workshop is not actively maintained. 3 | 4 | Refer to https://image-processing.serverlessworkshops.io/ for an active serverless workshop using Step Functions to address image processing use case 5 | Below is kept for archiving purposes only 6 | 7 | # [archived] Workshop: Serverless Image Processing Workflow with AWS Step Functions 8 | 9 | In this workshop, you will learn to build a serverless image processing workflow step-by-step using AWS Step Functions. 10 | 11 | As can be seen in the diagram below, this workflow processes photos uploaded to Amazon S3 and extracts metadata from the image such as geolocation, size/format, time, etc. It then uses image recognition to tag objects in the photo. In parallel, it also produces a thumbnail of the photo. AWS Step Functions acts as the orchestration to coordinate the various steps involved. 12 | 13 | ![pick IAM role for state machine](../images/photo-processing-backend-diagram.png) 14 | 15 | ## Pre-requisites 16 | 17 | - Administrative access to an AWS account 18 | - Code editor of choice (e.g. Sublime Text, PyCharm, etc...) 19 | 20 | ## Instructions 21 | 22 | * [Step 0: Set up resources](step-0.md) 23 | * [Step 1: Adding first Lambda step to a AWS Step Functions state machine](step-1.md) 24 | * [Step 2: Add branching logic to state machine](step-2.md) 25 | * [Step 3: Add parallel processing to the workflow](step-3.md) 26 | * [Step 4: Persisting labels and image metadata](step-4.md) 27 | * [Step 5: Start execution from an S3 event](step-5.md) 28 | * [Step 6: Build and Launch the web application](step-6.md) 29 | * [Extra credit options](additional-steps.md) 30 | * [Resource clean-up](clean-up.md) 31 | -------------------------------------------------------------------------------- /Workshop/clean-up.md: -------------------------------------------------------------------------------- 1 | # Resource clean-up 2 | 3 | To clean up all the resources created in this workshop: 4 | 5 | * Go to [Step Functions Console](https://console.aws.amazon.com/states/home), delete the `ImageProcessing` Step Functions state machine 6 | * Go to [CloudFormation Console](https://console.aws.amazon.com/cloudformation/home), delete the `sfn-workshop-setup-webapp` CloudFormation stack 7 | * Delete the `sfn-workshop-setup` CloudFormation stack -------------------------------------------------------------------------------- /Workshop/images/0-resource-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/0-resource-setup.png -------------------------------------------------------------------------------- /Workshop/images/0a-cfn-create-change-set.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/0a-cfn-create-change-set.png -------------------------------------------------------------------------------- /Workshop/images/0a-cfn-execute-change-set.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/0a-cfn-execute-change-set.png -------------------------------------------------------------------------------- /Workshop/images/0b-cfn-create-change-set.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/0b-cfn-create-change-set.png -------------------------------------------------------------------------------- /Workshop/images/0b-cfn-execute-change-set.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/0b-cfn-execute-change-set.png -------------------------------------------------------------------------------- /Workshop/images/0b-cfn-outputs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/0b-cfn-outputs.png -------------------------------------------------------------------------------- /Workshop/images/0b-code-pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/0b-code-pipeline.png -------------------------------------------------------------------------------- /Workshop/images/0b-webapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/0b-webapp.png -------------------------------------------------------------------------------- /Workshop/images/1-auto-IAM-role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/1-auto-IAM-role.png -------------------------------------------------------------------------------- /Workshop/images/1a-step-easy-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/1a-step-easy-name.png -------------------------------------------------------------------------------- /Workshop/images/1a-step-lambda-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/1a-step-lambda-details.png -------------------------------------------------------------------------------- /Workshop/images/1b-pick-state-role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/1b-pick-state-role.png -------------------------------------------------------------------------------- /Workshop/images/1b-step-console-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/1b-step-console-preview.png -------------------------------------------------------------------------------- /Workshop/images/1c-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/1c-dashboard.png -------------------------------------------------------------------------------- /Workshop/images/1c-execution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/1c-execution.png -------------------------------------------------------------------------------- /Workshop/images/1c-input-old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/1c-input-old.png -------------------------------------------------------------------------------- /Workshop/images/1c-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/1c-input.png -------------------------------------------------------------------------------- /Workshop/images/1c-start-new-execution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/1c-start-new-execution.png -------------------------------------------------------------------------------- /Workshop/images/1d-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/1d-edit.png -------------------------------------------------------------------------------- /Workshop/images/1d-output-w-resultpath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/1d-output-w-resultpath.png -------------------------------------------------------------------------------- /Workshop/images/1d-start-execution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/1d-start-execution.png -------------------------------------------------------------------------------- /Workshop/images/1d-updated-new-execution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/1d-updated-new-execution.png -------------------------------------------------------------------------------- /Workshop/images/2-branching-logic-state-machine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/2-branching-logic-state-machine.png -------------------------------------------------------------------------------- /Workshop/images/2a-reimport-step-easy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/2a-reimport-step-easy.png -------------------------------------------------------------------------------- /Workshop/images/2a-step-easy-Paralelldetails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/2a-step-easy-Paralelldetails.png -------------------------------------------------------------------------------- /Workshop/images/2a-step-easy-canvas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/2a-step-easy-canvas.png -------------------------------------------------------------------------------- /Workshop/images/2a-step-easy-choice-ImageTypeCheckChoicedetails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/2a-step-easy-choice-ImageTypeCheckChoicedetails.png -------------------------------------------------------------------------------- /Workshop/images/2a-step-easy-choice-ImageTypeCheckconfigurecondition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/2a-step-easy-choice-ImageTypeCheckconfigurecondition.png -------------------------------------------------------------------------------- /Workshop/images/2a-step-easy-choice-ImageTypeCheckdetails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/2a-step-easy-choice-ImageTypeCheckdetails.png -------------------------------------------------------------------------------- /Workshop/images/2a-step-easy-fail-notsupportedimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/2a-step-easy-fail-notsupportedimage.png -------------------------------------------------------------------------------- /Workshop/images/2a-step-easy-fail-notsupportedimagecatchers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/2a-step-easy-fail-notsupportedimagecatchers.png -------------------------------------------------------------------------------- /Workshop/images/2a-step-easy-fail-notsupportedimagedetails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/2a-step-easy-fail-notsupportedimagedetails.png -------------------------------------------------------------------------------- /Workshop/images/2b-step-console-update-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/2b-step-console-update-preview.png -------------------------------------------------------------------------------- /Workshop/images/2c-test-catch-failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/2c-test-catch-failed.png -------------------------------------------------------------------------------- /Workshop/images/2c-test-choice-test-failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/2c-test-choice-test-failed.png -------------------------------------------------------------------------------- /Workshop/images/2c-test-choice-test-succeeded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/2c-test-choice-test-succeeded.png -------------------------------------------------------------------------------- /Workshop/images/3-create-statemachine-select-role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/3-create-statemachine-select-role.png -------------------------------------------------------------------------------- /Workshop/images/3-state-machine-parallel-with-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/3-state-machine-parallel-with-output.png -------------------------------------------------------------------------------- /Workshop/images/3-state-machine-parallel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/3-state-machine-parallel.png -------------------------------------------------------------------------------- /Workshop/images/4-add-step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/4-add-step.png -------------------------------------------------------------------------------- /Workshop/images/4-final-status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/4-final-status.png -------------------------------------------------------------------------------- /Workshop/images/4b-step-console-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/4b-step-console-preview.png -------------------------------------------------------------------------------- /Workshop/images/4c-ddb-imagemetadata-item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/4c-ddb-imagemetadata-item.png -------------------------------------------------------------------------------- /Workshop/images/4c-ddb-imagemetadata.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/4c-ddb-imagemetadata.png -------------------------------------------------------------------------------- /Workshop/images/4c-test-choice-test-succeeded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/4c-test-choice-test-succeeded.png -------------------------------------------------------------------------------- /Workshop/images/5a-enviroment-variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/5a-enviroment-variables.png -------------------------------------------------------------------------------- /Workshop/images/5a-state-machine-arn-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/5a-state-machine-arn-new.png -------------------------------------------------------------------------------- /Workshop/images/5a-state-machine-arn-newer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/5a-state-machine-arn-newer.png -------------------------------------------------------------------------------- /Workshop/images/5a-state-machine-arn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/5a-state-machine-arn.png -------------------------------------------------------------------------------- /Workshop/images/5b-s3-event-configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/5b-s3-event-configuration.png -------------------------------------------------------------------------------- /Workshop/images/5b-s3-events.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/5b-s3-events.png -------------------------------------------------------------------------------- /Workshop/images/5b-s3-incoming-folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/5b-s3-incoming-folder.png -------------------------------------------------------------------------------- /Workshop/images/5c-s3-object-metadata.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/5c-s3-object-metadata.png -------------------------------------------------------------------------------- /Workshop/images/5c-state-machine-execution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/5c-state-machine-execution.png -------------------------------------------------------------------------------- /Workshop/images/additional-step-inappropiate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/additional-step-inappropiate.png -------------------------------------------------------------------------------- /Workshop/images/bucket-sync-state-machine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/bucket-sync-state-machine.png -------------------------------------------------------------------------------- /Workshop/images/bulk-import-album1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/bulk-import-album1.png -------------------------------------------------------------------------------- /Workshop/images/bulk-import-albumlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/bulk-import-albumlist.png -------------------------------------------------------------------------------- /Workshop/images/bulk-import-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/bulk-import-user.png -------------------------------------------------------------------------------- /Workshop/images/cfn-launch-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/cfn-launch-stack.png -------------------------------------------------------------------------------- /Workshop/images/cloudwatch-state-machine-metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/cloudwatch-state-machine-metrics.png -------------------------------------------------------------------------------- /Workshop/images/dynamo-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/dynamo-screenshot.png -------------------------------------------------------------------------------- /Workshop/images/single-step-state-machine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/single-step-state-machine.png -------------------------------------------------------------------------------- /Workshop/images/step-easy-intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/step-easy-intro.png -------------------------------------------------------------------------------- /Workshop/images/web-app-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/Workshop/images/web-app-screenshot.png -------------------------------------------------------------------------------- /Workshop/step-0.md: -------------------------------------------------------------------------------- 1 | ## Step 0: Set up resources 2 | 3 | ### Step 0A: Set up Lambda functions and data stores 4 | 5 | The AWS Step Functions state machine you will create in this workshop coordinates a number of Lambda functions that implement the logic for each step. Some of those Lambda functions rely on the existance of AWS resources and data stores, such as an Amazon S3 bucket or an Amazon DynamoDB table. 6 | 7 | In this section, you will use a cloudformation template to provision the AWS Lambda functions plus all the resources that those require. 8 | 9 | To help you understand what resources are set up in this stage, refer to the diagram below. In this workshop, you will build a Step Functions state machine (greyed out in the middle) to orchestrate the lambda functions that does the processing work: 10 |
11 | 12 | 13 | 14 |
15 |
16 | 17 | Region| Code | Launch 18 | ------|------|------- 19 | US West (Oregon) | us-west-2 | [![Launch Step 0A in us-west-2](images/cfn-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=sfn-workshop-setup&templateURL=https://s3-us-west-2.amazonaws.com/image-processing-step-functions-workshop-us-west-2/cloudformation/step0-sam.yaml) 20 | 21 | 22 |
23 | CloudFormation launch instructions (expand for details)

24 | 25 | 1. Click the **Launch Stack** link above for the region of your choice. 26 | 27 | 1. Click **Next** on the Select Template page. 28 | 29 | 1. On the ```Specify stack details``` page, leave all the defaults and click **Next**. 30 | 31 | 1. On the ```Configure stack options``` page, also leave all the defaults and click **Next**. 32 | 33 | 1. On the ```Review page```, check all the boxes to acknowledge that CloudFormation will create IAM resources and CAPABILITY_AUTO_EXPAND and click **Create Stack**. 34 | 35 | ![Acknowledge IAM Screenshot](./images/0a-cfn-create-change-set.png) 36 | 37 | This template creates a number of IAM roles to grant the Lambda fuctions proper permissions on the resources they have to deal with. 38 | 39 | 1. Wait for the `sfn-workshop-setup` stack to reach a status of `CREATE_COMPLETE` (you might need to click the refresh button to see the stack being created). 40 |

41 | 42 | 43 | ### Next step 44 | You are now ready to move on to [Step 1](step-1.md)! 45 | -------------------------------------------------------------------------------- /Workshop/step-3.md: -------------------------------------------------------------------------------- 1 | ## Step 3: Add parallel processing to the workflow 2 | 3 | After extracting and checking the metadata, we are now ready to add a few more steps to our state machine: thumb-nailing, image recognition and persisting/indexing the metadata. Thumb-nailing and image recognition does not depend on each other and can happen in parallel, and we can use a [parallel state](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-parallel-state.html) in Step Functions. 4 | 5 | 6 | 7 | ### Step 3A: Update the state machine definition 8 | 9 | Now you acquired some experience with incrementally adding steps to the state machine, deploying, and testing it, give it a shot to add a parallel step to our current state machine (remember to export it or take it from the Final JSON of the last step) in step-easy that performs thumb-nailing and image recognition in parallel. 10 | 11 | Take a look at these documentation if you need help with the syntax: 12 | 13 | - [Parallel State](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-parallel-state.html) 14 | - [Task State](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-task-state.html) 15 | - [Amazon States Language Spec](https://states-language.net/spec.html) 16 | 17 | We suggest now to edit the JSON directly, so that links to the documentation referred above are easier to follow. 18 | 19 |
20 | Expand to get some hints

21 | 22 | - The first step is to change the type of the *Parallel* state from **Pass** to **Parallel**. 23 | 24 | - Parallel acitivies within a **Parallel** state are specified as an array of objects, each of one is, in turn, a self-contained state machine object. 25 | 26 | ```JSON 27 | "Parallel": { 28 | "Type": "Parallel", 29 | "Branches": [, ..., ], 30 | "End": true 31 | } 32 | ``` 33 | 34 | - For each branch create a state machine object with one **Task** state that triggers the corresponding Lambda function: 35 | - ``sfn-workshop-setup-DetectLabel``, that leverages the deep learning-based image analyis [Amazon Rekognition](https://aws.amazon.com/rekognition/) service and, in particular, its [DetectLabels API](http://docs.aws.amazon.com/rekognition/latest/dg/API_DetectLabels.html), to obtain metadata about what objects and concepts appear on the processed image. 36 | 37 | - ``sfn-workshop-setup-Thumbnail``, that relies on [GraphicsMagick for node.js](http://aheckmann.github.io/gm/docs.html) library to generate the thumbnails 38 | 39 |

40 | 41 | Now is a good time to remember Step 1D, where we learned how to merge the input of a state with its output, so that both become available to states further down the line using the [AWS Step Functions Paths feature]((https://docs.aws.amazon.com/step-functions/latest/dg/awl-ref-paths.html)). 42 | 43 | In particular use **ResultPath** in the *Parallel* state to make sure the array of parallel state machine outputs are made available to downstream states. Use the `parallelResults` attribute for that. 44 | 45 |
46 | Expand to get some hints

47 | 48 | ```JSON 49 | "Parallel": { 50 | "Type": "Parallel", 51 | "Branches": [, ..., ], 52 | "ResultPath": "$.parallelResults", 53 | "End": true 54 | } 55 | ``` 56 |

57 | 58 | 59 | Now, test your state machine with the images you want! 60 | 61 | - Make sure that both labels detected by Amazon Rekognition and the S3 location of the thumbnails are available in the output. 62 | 63 | 64 | 65 | - Verify that the tumbnails are in the S3 bucket, under the **Thumbnail** folder 66 | 67 | 68 | ### Final JSON 69 |
70 | Expand to see JSON definition

71 | 72 | ```JSON 73 | { 74 | "StartAt": "ExtractImageMetadata", 75 | "Comment": "Image Processing State Machine - step 3 final", 76 | "States": { 77 | "ExtractImageMetadata": { 78 | "Type": "Task", 79 | "Resource": "arn:aws:lambda:us-west-2:012345678901:function:sfn-workshop-setup-ExtractMetadata", 80 | "Catch": [ 81 | { 82 | "ErrorEquals": [ 83 | "ImageIdentifyError" 84 | ], 85 | "ResultPath": "$.error", 86 | "Next": "NotSupportedImageType" 87 | } 88 | ], 89 | "ResultPath": "$.extractedMetadata", 90 | "Next": "ImageTypeCheck" 91 | }, 92 | "ImageTypeCheck": { 93 | "Type": "Choice", 94 | "Choices": [ 95 | { 96 | "Or": [ 97 | { 98 | "Variable": "$.extractedMetadata.format", 99 | "StringEquals": "JPEG" 100 | }, 101 | { 102 | "Variable": "$.extractedMetadata.format", 103 | "StringEquals": "PNG" 104 | } 105 | ], 106 | "Next": "Parallel" 107 | } 108 | ], 109 | "Default": "NotSupportedImageType" 110 | }, 111 | "NotSupportedImageType": { 112 | "Type": "Fail", 113 | "Cause": "Image type not supported!", 114 | "Error": "FileTypeNotSupported" 115 | }, 116 | "Parallel": { 117 | "Type": "Parallel", 118 | "Branches": [ 119 | { 120 | "StartAt": "DetectLabelsRekognition", 121 | "States": { 122 | "DetectLabelsRekognition": { 123 | "Type": "Task", 124 | "Resource": "arn:aws:lambda:us-west-2:012345678901:function:sfn-workshop-setup-DetectLabel", 125 | "End": true 126 | } 127 | } 128 | }, 129 | { 130 | "StartAt": "Thumbnail", 131 | "States": { 132 | "Thumbnail": { 133 | "Type": "Task", 134 | "Resource": "arn:aws:lambda:us-west-2:012345678901:function:sfn-workshop-setup-Thumbnail", 135 | "End": true 136 | } 137 | } 138 | } 139 | ], 140 | "ResultPath": "$.parallelResults", 141 | "End": true 142 | } 143 | } 144 | } 145 | 146 | ``` 147 |

148 | 149 | ### Next step 150 | You are now ready to move on to [Step 4](step-4.md)! 151 | 152 | -------------------------------------------------------------------------------- /Workshop/step-4.md: -------------------------------------------------------------------------------- 1 | ## Step 4: Persisting labels and image metadata 2 | 3 | At this point, we should have our state machine generating Thumbnails and metadata, as well as detecting what objects and concepts are in the images (labels). 4 | 5 | 6 | ### Step 4A: Add a step to store the metadata in DynamoDB 7 | 8 | For this part we will again edit the JSON manually. Go to your editor of choice used previously and paste in the final JSON you ended up with on Step 3. From there add one more step to your state machine, to execute the Lambda function `sfn-workshop-setup-PersistDDB` that takes care of storing everything in DynamoDB. 9 | 10 | Take a look at this documentation if you need help with the syntax: 11 | 12 | - [Task State](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-task-state.html) 13 | - [Amazon States Language Spec](https://states-language.net/spec.html) 14 | 15 | > To find the ARN of the Lambda function, in the AWS CloudFormation Console, go to the **sfn-workshop-setup** stack and look in the Outputs section for **StoreImageMetadataFunction** 16 | > 17 | > It should look something like `arn:aws:lambda:us-west-2::function:sfn-workshop-setup-PersistDDB` 18 | 19 | 1. Make the new step you've just added, be the next step after the Parallel step. 20 | 21 | 1. Copy the produced JSON. If you're having trouble, you can find the JSON at the bottom of this page. 22 | 23 | 24 | ### Step 4B: Create an AWS Step Functions state machine to test the result 25 | 26 | 1. Go to [AWS Step Functions management console](http://console.aws.amazon.com/states/home). 27 | 28 | 1. Select the `ImageProcessing` state machine. Click on **Edit state machine** 29 | 30 | 1. Paste in the JSON from your editor produced in Step 4A 31 | 32 | 1. Click on the ↺ icon next to **Visual Workflow** to refresh the visual representation of the state machine: 33 | 34 | 35 | 36 | 1. Click **Update and start execution** 37 | 38 | 39 | ### Step 4C: Test the state machine execution 40 | 41 | 1. Use the following input you have been using so far to test the execution 42 | 43 | ```JSON 44 | { 45 | "s3Bucket": "FILL_IN_YOUR_VALUE", 46 | "s3Key": "tests/1_cactus.jpg" 47 | } 48 | ``` 49 | 50 | 51 | 1. Verify that everything came out fine and the workflow succesfully executed all the steps: 52 | 53 | 54 | 55 | 1. You may also want to check what DynamoDB tables and items stored in them look like. Navigate to the [Amazon DynamoDB management console](https://console.aws.amazon.com/dynamodb/home?#tables:). You should see two tables there with names similar to the following ones: 56 | 57 | - `sfn-workshop-setup-AlbumMetadataDDBTable` 58 | - `sfn-workshop-setup-ImageMetadataDDBTable` 59 | 60 | 1. Click on the `sfn-workshop-setup-ImageMetadataDDBTable` and the click on the **Items** folder. Verify there is one entry for each sucessfully processed image: 61 | 62 | 63 | 64 | 65 | 1. Click on an item and have a look at its structure: 66 | 67 | 68 | 69 | 70 | ### Final JSON 71 |
72 | Expand to see JSON definition

73 | 74 | ```JSON 75 | { 76 | "StartAt": "ExtractImageMetadata", 77 | "Comment": "Image Processing State Machine", 78 | "States": { 79 | "ExtractImageMetadata": { 80 | "Type": "Task", 81 | "Resource": "arn:aws:lambda:us-west-2:012345678901:function:sfn-workshop-setup-ExtractMetadata", 82 | "Catch": [ 83 | { 84 | "ErrorEquals": [ 85 | "ImageIdentifyError" 86 | ], 87 | "ResultPath": "$.error", 88 | "Next": "NotSupportedImageType" 89 | } 90 | ], 91 | "ResultPath": "$.extractedMetadata", 92 | "Next": "ImageTypeCheck" 93 | }, 94 | "NotSupportedImageType": { 95 | "Type": "Fail", 96 | "Cause": "Image type not supported!", 97 | "Error": "FileNotSupported" 98 | }, 99 | "ImageTypeCheck": { 100 | "Type": "Choice", 101 | "Choices": [ 102 | { 103 | "Or": [ 104 | { 105 | "Variable": "$.extractedMetadata.format", 106 | "StringEquals": "JPEG" 107 | }, 108 | { 109 | "Variable": "$.extractedMetadata.format", 110 | "StringEquals": "PNG" 111 | } 112 | ], 113 | "Next": "Parallel" 114 | } 115 | ], 116 | "Default": "NotSupportedImageType" 117 | }, 118 | "Parallel": { 119 | "Type": "Parallel", 120 | "Branches": [ 121 | { 122 | "StartAt": "DetectLabelsRekognition", 123 | "States": { 124 | "DetectLabelsRekognition": { 125 | "Type": "Task", 126 | "Resource": "arn:aws:lambda:us-west-2:012345678901:function:sfn-workshop-setup-DetectLabel", 127 | "End": true 128 | } 129 | } 130 | }, 131 | { 132 | "StartAt": "Thumbnail", 133 | "States": { 134 | "Thumbnail": { 135 | "Type": "Task", 136 | "Resource": "arn:aws:lambda:us-west-2:012345678901:function:sfn-workshop-setup-Thumbnail", 137 | "End": true 138 | } 139 | } 140 | } 141 | ], 142 | "ResultPath": "$.parallelResults", 143 | "Next": "PersistDDB" 144 | }, 145 | "PersistDDB": { 146 | "Type": "Task", 147 | "Resource": "arn:aws:lambda:us-west-2:012345678901:function:sfn-workshop-setup-PersistDDB", 148 | "End": true 149 | } 150 | } 151 | } 152 | ``` 153 |

154 | 155 | ### Next step 156 | You are now ready to move on to [Step 5](step-5.md)! 157 | -------------------------------------------------------------------------------- /Workshop/step-5.md: -------------------------------------------------------------------------------- 1 | ## Step 5: Start execution from an S3 event 2 | 3 | Finally, we have our state machine up and running! It's time to automate its execution. 4 | As images are uploaded to the landing S3 bucket we want the state machine to be triggered. 5 | 6 | We have two possibilities here: 7 | 8 | 1. Use CloudWatch Events to monitor activity on the S3 bucket and select Step Functions as the target for its execution 9 | 10 | 1. Use a Lambda Function that: 11 | - Invokes the state machine 12 | - Is triggered by an S3 event 13 | 14 | The first step requires some extra configuration as it needs CloudTrail to be enabled. For brevity, we will go with Option 2. 15 | 16 | ### Step 5A: Instruct StartExecution Lambda function what Step Functions state machine it is to be triggered 17 | 18 | 1. Go to [AWS Step Functions management console](http://console.aws.amazon.com/states/home). Make sure the AWS Region selection matches the one you have been working with so far. 19 | 20 | 1. On the Dashboard locate your state machine. If you followed instructions on prior steps it should be *ImageProcessing* 21 | 22 | 1. On the **Details** box, find and copy the **ARN**: 23 | 24 | 25 | 26 | 1. Navigate to the [AWS Lambda management console](https://console.aws.amazon.com/lambda/home) and find the Lambda function with a name that ends with `StartExecution`. That is the one that triggers the state machine. Select it by clicking on the link on its name. 27 | 28 | 1. The specific AWS Step Function state machine it launches is passed to the Lambda function as a configurable environment variable. Scroll down to the **Environment variables** section. Expand it and change the placeholder value `FILL_WITH_YOUR_VALUE` to the ARN of your state machine. 29 | 30 | 31 | 32 | 1. Scroll up to the top of the page and click **Save** 33 | 34 | ### Step 5B: Set up S3 event to trigger StartExecution Lambda function 35 | 36 | The Lambda function now knows what state machine we want it to run. Now we need to set up the event that will, in turn, trigger the Lambda function. Since we want it to be launched automatically whenever a new object is uploaded to the landing bucket, we need the Lambda funtion to be triggered by an S3 event. 37 | 38 | 1. Go to the S3 management console and select the landing bucket: 39 | 40 | ``` 41 | sfn-workshop-setup-photorepos3bucket-xxxxxxxxxxxxx 42 | ``` 43 | 44 | 1. Create a folder and call it *Incoming* 45 | 46 | 47 | 48 | 1. Click on the **Properties** tab 49 | 50 | 1. Click on **Events** 51 | 52 | 53 | 54 | 1. Click on **Add notification** 55 | 56 | 1. Enter the following parameters: 57 | - **Name**: ExecuteStateMachine 58 | - **Events**: All object create events 59 | - **Prefix**: Incoming/ 60 | - **Send to**: Lambda Function 61 | - **Lambda**: sfn-workshop-setup-StartExecution 62 | 63 | > **Note:** the **Prefix** parameter is critical: this limits the event trigger to only trigger processing workflows when an image file lands in the "Incoming/" prefix. Because the thumbnail generation process uploads the thumbnails to the same S3 bucket, without limiting the prefix, the thumbnail upload will trigger another workflow and causes an infinite loop. 64 | 65 | 66 | 67 | 1. Click **Save** 68 | 69 | ### Step 5C: Test the event trigger by uploading a photo to S3 70 | 71 | Now you are ready to test your event! Just upload an image within the S3 bucket into the "Incoming" folder and check the execution in the Step Functions console! 72 | 73 | 1. On the S3 management console go to the `Incoming/` prefix on the landing bucket and click **Upload**. Select an image with a supported format (JPEG or PNG). Click **Next** 74 | 75 | 76 | 1. Click **Next** and **Upload** 77 | 78 | 1. Verify that the state machine is triggered and it executes successfully. Verify the metadata of the new image processed is stored in DynamoDB. 79 | 80 | 81 | 82 | 83 | ### Next step 84 | You are now ready to move on to [Step 6](step-6.md)! 85 | 86 | 87 | -------------------------------------------------------------------------------- /Workshop/step-6.md: -------------------------------------------------------------------------------- 1 | # Step 6: Build and Launch the web application 2 | 3 | To see the processing workflow in action, we will use an AngularJS web application to create photo albums and upload pictures to them. 4 | 5 | ### Step 6A: Set up the Photo Processing Web Application 6 | 7 | To complete the workshop tests, you will use an AngularJS web application to create photo albums and upload pictures to them. 8 | 9 | In this section, you will use a cloudformation template to package and deploy the web application on an S3 bucket configured for website hosting. 10 | 11 | > This cloudformation template leverages AWS CodeBuild and AWS CodePipeline to automate building, packaging and deploying the web application 12 | 13 | Region| Code | Launch 14 | ------|------|------- 15 | US East (Ohio)| us-east-2 | [![Launch Step 6 in us-east-2](images/cfn-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-2#/stacks/new?stackName=sfn-workshop-setup-webapp&templateURL=https://s3-us-east-2.amazonaws.com/image-processing-step-functions-workshop-us-east-2/cloudformation/step0-webapp.yaml) 16 | US East (N. Virginia) | us-east-1 | [![Launch Step 6 in us-east-1](images/cfn-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=sfn-workshop-setup-webapp&templateURL=https://s3.amazonaws.com/image-processing-step-functions-workshop-us-east-1/cloudformation/step0-webapp.yaml) 17 | US West (Oregon) | us-west-2 | [![Launch Step 6 in us-west-2](images/cfn-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=sfn-workshop-setup-webapp&templateURL=https://s3-us-west-2.amazonaws.com/image-processing-step-functions-workshop-us-west-2/cloudformation/step0-webapp.yaml) 18 | EU (Ireland) | eu-west-1 | [![Launch Step 6 in eu-west-1](images/cfn-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=eu-west-1#/stacks/new?stackName=sfn-workshop-setup-webapp&templateURL=https://s3-eu-west-1.amazonaws.com/image-processing-step-functions-workshop-eu-west-1/cloudformation/step0-webapp.yaml) 19 | Tokyo | ap-northeast-1 | [![Launch Step 6 in ap-northeast-1](images/cfn-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/stacks/new?stackName=sfn-workshop-setup-webapp&templateURL=https://s3-ap-northeast-1.amazonaws.com/image-processing-step-functions-workshop-ap-northeast-1/cloudformation/step0-webapp.yaml) 20 | Sydney | ap-southeast-2 | [![Launch Step 6 in ap-southeast-2](images/cfn-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-southeast-2#/stacks/new?stackName=sfn-workshop-setup-webapp&templateURL=https://s3-ap-southeast-2.amazonaws.com/image-processing-step-functions-workshop-ap-southeast-2/cloudformation/step0-webapp.yaml) 21 | Mumbai | ap-south-1 | [![Launch Step 6 in ap-south-1](images/cfn-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-south-1#/stacks/new?stackName=sfn-workshop-setup-webapp&templateURL=https://s3-ap-south-1.amazonaws.com/image-processing-step-functions-workshop-ap-south-1/cloudformation/step0-webapp.yaml) 22 | 23 | #### CloudFormation launch instructions 24 | 25 | 1. Click the **Launch Stack** link above for the region of your choice (make sure to pick the same region you have launched your other resources in) 26 | 27 | 1. Click **Next** on the Select Template page. 28 | 29 | 1. On the ```Specify stack details``` page, leave all the defaults and click **Next**. 30 | 31 | 1. On the ```Configure stack options``` page, also leave all the defaults and click **Next**. 32 | 33 | 1. On the ```Review page```, check all the boxes to acknowledge that CloudFormation will create IAM resources and CAPABILITY_AUTO_EXPAND and click **Create Stack**. 34 | 35 | ![Acknowledge IAM Screenshot](./images/0a-cfn-create-change-set.png) 36 | 37 | 1. Wait for the `sfn-workshop-setup-webapp` stack to reach a status of `CREATE_COMPLETE` (you might need to click the refresh button to see the stack being created). 38 | 39 | The stack will take a minute of so to complete. As part ot that, it copies the web application sources over to the target bucket and that will trigger the delivery pipleline. 40 | 41 | 1. Go to the [AWS CodePipeline management console](http://console.aws.amazon.com/codepipeline/home). Click on the pipeline identified by `sfn-workshop-pipeline` and verify it finishes succesfully. 42 | Pipeline Screenshot 43 | 44 | 1. Go to the [AWS CloudFormation management console](http://console.aws.amazon.com/cloudformation/home). With the stack `sfn-workshop-setup-webapp` selected click on the Outputs tab. The `WebsiteURL` key points at the link to access the **Image Sharing demo Web Application** 45 | 46 | ![CloudFormation Outputs Screenshot](./images/0b-cfn-outputs.png) 47 | 48 | 1. Click on that link to navigate to the **Media Sharing Web Application** 49 | 50 | **Note: Make sure to use Chrome browser to open the web application.** 51 | 52 | ![Media Sharing Web Application Screenshot](./images/0b-webapp.png) 53 | 54 |

55 | 56 | ### Step 6B: Test the end-to-end workflow using the sample web app 57 | ##### Login 58 | Pick any username to log in (This is a test app to showcase the backend so it's not using real user authentication. In an actual app, you can use Amazon Cognito to manage user sign-up and login.) 59 | 60 | The username will be used in storing ownership metadata of the uploaded images. 61 | 62 | ##### Album list 63 | Create new or select existing albums to upload images to. 64 | 65 | ##### Photo gallery 66 | Upload images and see status updates when: 67 | 68 | 1. Upload to S3 bucket succeeds 69 | 2. The AWS Step Function execution is started. The execution ARN is provided in the UI so you can easily look up its details in the Step Functions [Console](https://console.aws.amazon.com/states/home) 70 | 3. The AWS Step Function execution completes 71 | 72 | A sample set of extracted image metadata and recognized tags, along with the thumbnail generated in the Step Function execution is displayed for each uploaded image. 73 | 74 | Pipeline Screenshot 75 | 76 | 77 | ### Next step: extra credits 78 | 79 | Now you have built a end-to-end image processing workflow using Step Functions, ready for some fun challenges? 80 | 81 | See the [extra credits](./additional-steps.md) section 82 | 83 | If you are ready to clean up resources created for this workshop, see the [cleanup](./clean-up.md) instructions 84 | -------------------------------------------------------------------------------- /amplify.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | backend: 3 | phases: 4 | preBuild: 5 | commands: 6 | - npm install -g @aws-amplify/cli@latest 7 | - make init # install OS packages and dev tools (awscli, sam-cli, linters, etc.) 8 | build: 9 | commands: 10 | - "# Execute Amplify CLI with the helper script" 11 | - amplifyPush -e $AWS_BRANCH --simple 12 | ## 13 | # Extract Environment data 14 | ## 15 | - export STACK_NAME=$(jq -r '.providers.awscloudformation.StackName' ./amplify/#current-cloud-backend/amplify-meta.json) 16 | - export DEPLOYMENT_BUCKET_NAME=$(jq -r '.providers.awscloudformation.DeploymentBucketName' ./amplify/#current-cloud-backend/amplify-meta.json) 17 | - export AWS_DEFAULT_REGION=$(jq -r '.providers.awscloudformation.Region' amplify/#current-cloud-backend/amplify-meta.json) 18 | - export GRAPHQL_API_ID=$(jq -r '.api[(.api | keys)[0]].output.GraphQLAPIIdOutput' ./amplify/#current-cloud-backend/amplify-meta.json) 19 | - export APPSYNC_URL=$(jq -r '.api[(.api | keys)[0]].output.GraphQLAPIEndpointOutput' ./amplify/#current-cloud-backend/amplify-meta.json) 20 | - export GRAPHQL_API_ID=$(jq -r '.api[(.api | keys)[0]].output.GraphQLAPIIdOutput' ./amplify/#current-cloud-backend/amplify-meta.json) 21 | - export COGNITO_USER_POOL_ID=$(jq -r '.auth[(.auth | keys)[0]].output.UserPoolId' ./amplify/#current-cloud-backend/amplify-meta.json) 22 | - export COGNITO_USER_POOL_ARN=$(aws cognito-idp describe-user-pool --user-pool-id ${COGNITO_USER_POOL_ID} --query 'UserPool.Arn' --output text) 23 | - export COGNITO_USER_POOL_CLIENT_ID=$(jq -r '.auth[(.auth | keys)[0]].output.AppClientIDWeb' ./amplify/#current-cloud-backend/amplify-meta.json) 24 | - export PHOTO_BUCKET=$(jq -r '.storage[(.storage | keys)[0]].output.BucketName' ./amplify/#current-cloud-backend/amplify-meta.json) 25 | ## 26 | # Deploy SAM based back-end 27 | ## 28 | - make deploy 29 | frontend: 30 | phases: 31 | preBuild: 32 | commands: 33 | - cd src/react-frontend 34 | - npm install 35 | build: 36 | commands: 37 | - npm run build 38 | artifacts: 39 | baseDirectory: src/react-frontend/build 40 | files: 41 | - "**/*" 42 | cache: 43 | paths: 44 | - node_modules/**/* 45 | - src/react-frontend/node_modules/**/* 46 | -------------------------------------------------------------------------------- /amplify/.config/project-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "photoshare", 3 | "version": "3.0", 4 | "frontend": "javascript", 5 | "javascript": { 6 | "framework": "react", 7 | "config": { 8 | "SourceDir": "src/react-frontend/src", 9 | "DistributionDir": "src/react-frontend/build", 10 | "BuildCommand": "npm run-script build", 11 | "StartCommand": "npm run-script start" 12 | } 13 | }, 14 | "providers": [ 15 | "awscloudformation" 16 | ] 17 | } -------------------------------------------------------------------------------- /amplify/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Amplify CLI 2 | This directory was generated by [Amplify CLI](https://docs.amplify.aws/cli). 3 | 4 | Helpful resources: 5 | - Amplify documentation: https://docs.amplify.aws 6 | - Amplify CLI documentation: https://docs.amplify.aws/cli 7 | - More details on this folder & generated files: https://docs.amplify.aws/cli/reference/files 8 | - Join Amplify's community: https://amplify.aws/community/ 9 | -------------------------------------------------------------------------------- /amplify/backend/api/photoshare/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSyncApiName": "photoshare", 3 | "DynamoDBBillingMode": "PAY_PER_REQUEST", 4 | "DynamoDBEnableServerSideEncryption": "false", 5 | "AuthCognitoUserPoolId": { 6 | "Fn::GetAtt": [ 7 | "authphotoshareb143529b", 8 | "Outputs.UserPoolId" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /amplify/backend/api/photoshare/resolvers/Mutation.startSfnExecution.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2018-05-29", 3 | "method": "POST", 4 | "resourcePath": "/", 5 | "params": { 6 | "headers": { 7 | "content-type": "application/x-amz-json-1.0", 8 | "x-amz-target":"AWSStepFunctions.StartExecution" 9 | }, 10 | "body":$util.toJson($ctx.arguments.input) 11 | } 12 | } -------------------------------------------------------------------------------- /amplify/backend/api/photoshare/resolvers/Mutation.startSfnExecution.res.vtl: -------------------------------------------------------------------------------- 1 | $ctx.result.body -------------------------------------------------------------------------------- /amplify/backend/api/photoshare/resolvers/Query.checkSfnStatus.req.vtl: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2018-05-29", 3 | "method": "POST", 4 | "resourcePath": "/", 5 | "params": { 6 | "headers": { 7 | "content-type": "application/x-amz-json-1.0", 8 | "x-amz-target":"AWSStepFunctions.DescribeExecution" 9 | }, 10 | "body": $util.toJson($ctx.arguments.input) 11 | } 12 | } -------------------------------------------------------------------------------- /amplify/backend/api/photoshare/resolvers/Query.checkSfnStatus.res.vtl: -------------------------------------------------------------------------------- 1 | $ctx.result.body -------------------------------------------------------------------------------- /amplify/backend/api/photoshare/schema.graphql: -------------------------------------------------------------------------------- 1 | type Album 2 | @model 3 | @auth(rules: [ 4 | {allow: owner}, 5 | {allow: private, provider: iam} 6 | ]) { 7 | id: ID! 8 | name: String! 9 | owner: String 10 | photos: [Photo] @connection(keyName: "byAlbumUploadTime", fields: ["id"]) 11 | } 12 | 13 | type Photo 14 | @model 15 | @key(name: "byAlbumUploadTime", fields: ["albumId", "uploadTime"], queryField: "listPhotosByAlbumUploadTime") 16 | @auth(rules: [ 17 | {allow: owner}, 18 | {allow: private, provider: iam} 19 | ]) { 20 | id: ID! 21 | albumId: ID! 22 | owner: String 23 | uploadTime: AWSDateTime! 24 | album: Album @connection(fields: ["albumId"]) 25 | bucket: String! 26 | fullsize: PhotoS3Info 27 | thumbnail: PhotoS3Info 28 | format: String 29 | exifMake: String 30 | exitModel: String 31 | SfnExecutionArn: String 32 | ProcessingStatus: Status 33 | objectDetected: [String] 34 | geoLocation: GeoCoordinates 35 | } 36 | 37 | enum GeoCoordinateDirection{ 38 | N, 39 | W, 40 | S, 41 | E 42 | } 43 | 44 | type GeoCoordinates @aws_iam @aws_cognito_user_pools{ 45 | Latitude: Latitude! 46 | Longtitude: Longtitude! 47 | } 48 | 49 | type Latitude @aws_iam @aws_cognito_user_pools{ 50 | D: Float! 51 | M: Float! 52 | S: Float! 53 | Direction: GeoCoordinateDirection! 54 | } 55 | 56 | type Longtitude @aws_iam @aws_cognito_user_pools{ 57 | D: Float! 58 | M: Float! 59 | S: Float! 60 | Direction: GeoCoordinateDirection! 61 | 62 | } 63 | 64 | type PhotoS3Info @aws_iam @aws_cognito_user_pools{ 65 | key: String! 66 | width: Int 67 | height: Int 68 | } 69 | 70 | type Mutation { 71 | startSfnExecution(input: StartSfnExecutionInput!): StartSfnExecutionResult @aws_iam 72 | } 73 | 74 | type Query { 75 | checkSfnStatus(input: checkSfnStatusInput!): SfnStatusResult 76 | } 77 | 78 | input checkSfnStatusInput{ 79 | executionArn: String! 80 | } 81 | 82 | type SfnStatusResult { 83 | startDate: Float, 84 | stopDate: Float, 85 | status: Status 86 | } 87 | 88 | enum Status { 89 | PENDING 90 | RUNNING 91 | SUCCEEDED 92 | FAILED 93 | TIMED_OUT 94 | ABORTED 95 | } 96 | 97 | input StartSfnExecutionInput { 98 | input: String! 99 | stateMachineArn: String! 100 | } 101 | 102 | type StartSfnExecutionResult @aws_iam{ 103 | executionArn: String! 104 | startDate: Float 105 | } 106 | -------------------------------------------------------------------------------- /amplify/backend/api/photoshare/stacks/CustomResources.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "An auto-generated nested stack.", 4 | "Metadata": {}, 5 | "Parameters": { 6 | "AppSyncApiId": { 7 | "Type": "String", 8 | "Description": "The id of the AppSync API associated with this project." 9 | }, 10 | "AppSyncApiName": { 11 | "Type": "String", 12 | "Description": "The name of the AppSync API", 13 | "Default": "AppSyncSimpleTransform" 14 | }, 15 | "env": { 16 | "Type": "String", 17 | "Description": "The environment name. e.g. Dev, Test, or Production", 18 | "Default": "NONE" 19 | }, 20 | "S3DeploymentBucket": { 21 | "Type": "String", 22 | "Description": "The S3 bucket containing all deployment assets for the project." 23 | }, 24 | "S3DeploymentRootKey": { 25 | "Type": "String", 26 | "Description": "An S3 key relative to the S3DeploymentBucket that points to the root\nof the deployment directory." 27 | } 28 | }, 29 | "Resources": { 30 | "EmptyResource": { 31 | "Type": "Custom::EmptyResource", 32 | "Condition": "AlwaysFalse" 33 | }, 34 | "StartSfnResolver": { 35 | "Type": "AWS::AppSync::Resolver", 36 | "Properties": { 37 | "ApiId": { 38 | "Ref": "AppSyncApiId" 39 | }, 40 | "DataSourceName": { 41 | "Fn::GetAtt": [ 42 | "SfnProxyDataSource", 43 | "Name" 44 | ] 45 | }, 46 | "TypeName": "Mutation", 47 | "FieldName": "startSfnExecution", 48 | "RequestMappingTemplateS3Location": { 49 | "Fn::Sub": [ 50 | "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Mutation.startSfnExecution.req.vtl", 51 | { 52 | "S3DeploymentBucket": { 53 | "Ref": "S3DeploymentBucket" 54 | }, 55 | "S3DeploymentRootKey": { 56 | "Ref": "S3DeploymentRootKey" 57 | } 58 | } 59 | ] 60 | }, 61 | "ResponseMappingTemplateS3Location": { 62 | "Fn::Sub": [ 63 | "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Mutation.startSfnExecution.res.vtl", 64 | { 65 | "S3DeploymentBucket": { 66 | "Ref": "S3DeploymentBucket" 67 | }, 68 | "S3DeploymentRootKey": { 69 | "Ref": "S3DeploymentRootKey" 70 | } 71 | } 72 | ] 73 | } 74 | } 75 | }, 76 | "SfnStatusResolver": { 77 | "Type": "AWS::AppSync::Resolver", 78 | "Properties": { 79 | "ApiId": { 80 | "Ref": "AppSyncApiId" 81 | }, 82 | "DataSourceName": { 83 | "Fn::GetAtt": [ 84 | "SfnProxyDataSource", 85 | "Name" 86 | ] 87 | }, 88 | "TypeName": "Query", 89 | "FieldName": "checkSfnStatus", 90 | "RequestMappingTemplateS3Location": { 91 | "Fn::Sub": [ 92 | "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.checkSfnStatus.req.vtl", 93 | { 94 | "S3DeploymentBucket": { 95 | "Ref": "S3DeploymentBucket" 96 | }, 97 | "S3DeploymentRootKey": { 98 | "Ref": "S3DeploymentRootKey" 99 | } 100 | } 101 | ] 102 | }, 103 | "ResponseMappingTemplateS3Location": { 104 | "Fn::Sub": [ 105 | "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Query.checkSfnStatus.res.vtl", 106 | { 107 | "S3DeploymentBucket": { 108 | "Ref": "S3DeploymentBucket" 109 | }, 110 | "S3DeploymentRootKey": { 111 | "Ref": "S3DeploymentRootKey" 112 | } 113 | } 114 | ] 115 | } 116 | } 117 | }, 118 | "SfnProxyDataSource": { 119 | "Type": "AWS::AppSync::DataSource", 120 | "Properties": { 121 | "ApiId": { 122 | "Ref": "AppSyncApiId" 123 | }, 124 | "Name": "SfnProxy", 125 | "Description": "HTTP proxy for Step Function Service", 126 | "Type": "HTTP", 127 | "ServiceRoleArn": { 128 | "Fn::GetAtt": [ 129 | "SfnProxyDataSourceRole", 130 | "Arn" 131 | ] 132 | }, 133 | "HttpConfig": { 134 | "Endpoint": { 135 | "Fn::Sub": "https://states.${AWS::Region}.amazonaws.com/" 136 | }, 137 | "AuthorizationConfig": { 138 | "AuthorizationType": "AWS_IAM", 139 | "AwsIamConfig": { 140 | "SigningRegion": { 141 | "Fn::Sub": "${AWS::Region}" 142 | }, 143 | "SigningServiceName": "states" 144 | } 145 | } 146 | } 147 | } 148 | }, 149 | "SfnProxyDataSourceRole": { 150 | "Type": "AWS::IAM::Role", 151 | "Properties": { 152 | "RoleName": { 153 | "Fn::Sub": [ 154 | "SfnProxyDataSourceRole-${env}", 155 | { 156 | "env": { 157 | "Ref": "env" 158 | } 159 | } 160 | ] 161 | }, 162 | "AssumeRolePolicyDocument": { 163 | "Version": "2012-10-17", 164 | "Statement": [ 165 | { 166 | "Effect": "Allow", 167 | "Principal": { 168 | "Service": "appsync.amazonaws.com" 169 | }, 170 | "Action": "sts:AssumeRole" 171 | } 172 | ] 173 | }, 174 | "Policies": [ 175 | { 176 | "PolicyName": "Sfn", 177 | "PolicyDocument": { 178 | "Version": "2012-10-17", 179 | "Statement": [ 180 | { 181 | "Effect": "Allow", 182 | "Action": [ 183 | "states:StartExecution", 184 | "states:DescribeExecution" 185 | ], 186 | "Resource": "*" 187 | } 188 | ] 189 | } 190 | } 191 | ] 192 | } 193 | } 194 | }, 195 | "Conditions": { 196 | "HasEnvironmentParameter": { 197 | "Fn::Not": [ 198 | { 199 | "Fn::Equals": [ 200 | { 201 | "Ref": "env" 202 | }, 203 | "NONE" 204 | ] 205 | } 206 | ] 207 | }, 208 | "AlwaysFalse": { 209 | "Fn::Equals": [ 210 | "true", 211 | "false" 212 | ] 213 | } 214 | }, 215 | "Outputs": { 216 | "EmptyOutput": { 217 | "Description": "An empty output. You may delete this if you have at least one resource above.", 218 | "Value": "" 219 | } 220 | } 221 | } -------------------------------------------------------------------------------- /amplify/backend/api/photoshare/transform.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 5, 3 | "ElasticsearchWarning": true 4 | } -------------------------------------------------------------------------------- /amplify/backend/auth/photoshareb143529b/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "identityPoolName": "photoshareb143529b_identitypool_b143529b", 3 | "allowUnauthenticatedIdentities": false, 4 | "resourceNameTruncated": "photosb143529b", 5 | "userPoolName": "photoshareb143529b_userpool_b143529b", 6 | "autoVerifiedAttributes": [ 7 | "email" 8 | ], 9 | "mfaConfiguration": "OFF", 10 | "mfaTypes": [ 11 | "SMS Text Message" 12 | ], 13 | "smsAuthenticationMessage": "Your authentication code is {####}", 14 | "smsVerificationMessage": "Your verification code is {####}", 15 | "emailVerificationSubject": "Your verification code", 16 | "emailVerificationMessage": "Your verification code is {####}", 17 | "defaultPasswordPolicy": false, 18 | "passwordPolicyMinLength": 8, 19 | "passwordPolicyCharacters": [], 20 | "requiredAttributes": [ 21 | "email" 22 | ], 23 | "userpoolClientGenerateSecret": true, 24 | "userpoolClientRefreshTokenValidity": 30, 25 | "userpoolClientWriteAttributes": [ 26 | "email" 27 | ], 28 | "userpoolClientReadAttributes": [ 29 | "email" 30 | ], 31 | "userpoolClientLambdaRole": "photosb143529b_userpoolclient_lambda_role", 32 | "userpoolClientSetAttributes": false, 33 | "resourceName": "photoshareb143529b", 34 | "authSelections": "identityPoolAndUserPool", 35 | "authRoleArn": { 36 | "Fn::GetAtt": [ 37 | "AuthRole", 38 | "Arn" 39 | ] 40 | }, 41 | "unauthRoleArn": { 42 | "Fn::GetAtt": [ 43 | "UnauthRole", 44 | "Arn" 45 | ] 46 | }, 47 | "useDefault": "default", 48 | "userPoolGroupList": [], 49 | "dependsOn": [] 50 | } -------------------------------------------------------------------------------- /amplify/backend/backend-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth": { 3 | "photoshareb143529b": { 4 | "service": "Cognito", 5 | "providerPlugin": "awscloudformation", 6 | "dependsOn": [], 7 | "customAuth": false 8 | } 9 | }, 10 | "api": { 11 | "photoshare": { 12 | "service": "AppSync", 13 | "providerPlugin": "awscloudformation", 14 | "output": { 15 | "authConfig": { 16 | "additionalAuthenticationProviders": [ 17 | { 18 | "authenticationType": "AWS_IAM" 19 | } 20 | ], 21 | "defaultAuthentication": { 22 | "authenticationType": "AMAZON_COGNITO_USER_POOLS", 23 | "userPoolConfig": { 24 | "userPoolId": "authphotoshareb143529b" 25 | } 26 | } 27 | } 28 | } 29 | } 30 | }, 31 | "function": { 32 | "S3Trigger984fb593": { 33 | "service": "Lambda", 34 | "providerPlugin": "awscloudformation", 35 | "build": true, 36 | "dependsOn": [ 37 | { 38 | "category": "api", 39 | "resourceName": "photoshare", 40 | "attributes": [ 41 | "GraphQLAPIIdOutput", 42 | "GraphQLAPIEndpointOutput" 43 | ] 44 | } 45 | ] 46 | } 47 | }, 48 | "storage": { 49 | "photostorage": { 50 | "service": "S3", 51 | "providerPlugin": "awscloudformation", 52 | "dependsOn": [ 53 | { 54 | "category": "function", 55 | "resourceName": "S3Trigger984fb593", 56 | "attributes": [ 57 | "Name", 58 | "Arn", 59 | "LambdaExecutionRole" 60 | ] 61 | } 62 | ] 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /amplify/backend/function/S3Trigger984fb593/S3Trigger984fb593-cloudformation-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Lambda resource stack creation using Amplify CLI", 4 | "Parameters": { 5 | "env": { 6 | "Type": "String" 7 | }, 8 | "SfnStateMachineName": { 9 | "Type": "String" 10 | }, 11 | "apiphotoshareGraphQLAPIIdOutput": { 12 | "Type": "String", 13 | "Default": "apiphotoshareGraphQLAPIIdOutput" 14 | }, 15 | "apiphotoshareGraphQLAPIEndpointOutput": { 16 | "Type": "String", 17 | "Default": "apiphotoshareGraphQLAPIEndpointOutput" 18 | } 19 | }, 20 | "Conditions": { 21 | "ShouldNotCreateEnvResources": { 22 | "Fn::Equals": [ 23 | { 24 | "Ref": "env" 25 | }, 26 | "NONE" 27 | ] 28 | } 29 | }, 30 | "Resources": { 31 | "LambdaFunction": { 32 | "Type": "AWS::Lambda::Function", 33 | "Metadata": { 34 | "aws:asset:path": "./src", 35 | "aws:asset:property": "Code" 36 | }, 37 | "Properties": { 38 | "Handler": "index.handler", 39 | "FunctionName": { 40 | "Fn::If": [ 41 | "ShouldNotCreateEnvResources", 42 | "S3Trigger984fb593", 43 | { 44 | "Fn::Join": [ 45 | "", 46 | [ 47 | "S3Trigger984fb593", 48 | "-", 49 | { 50 | "Ref": "env" 51 | } 52 | ] 53 | ] 54 | } 55 | ] 56 | }, 57 | "Environment": { 58 | "Variables": { 59 | "ENV": { 60 | "Ref": "env" 61 | }, 62 | "STATE_MACHINE_ARN": { 63 | "Fn::Sub": "arn:aws:states:${AWS::Region}:${AWS::AccountId}:stateMachine:${SfnStateMachineName}-main" 64 | }, 65 | "REGION": { 66 | "Ref": "AWS::Region" 67 | }, 68 | "API_PHOTOSHARE_GRAPHQLAPIIDOUTPUT": { 69 | "Ref": "apiphotoshareGraphQLAPIIdOutput" 70 | }, 71 | "API_PHOTOSHARE_GRAPHQLAPIENDPOINTOUTPUT": { 72 | "Ref": "apiphotoshareGraphQLAPIEndpointOutput" 73 | } 74 | } 75 | }, 76 | "Role": { 77 | "Fn::GetAtt": [ 78 | "LambdaExecutionRole", 79 | "Arn" 80 | ] 81 | }, 82 | "Runtime": "nodejs12.x", 83 | "Timeout": "25", 84 | "ReservedConcurrentExecutions": "10", 85 | "Code": { 86 | "S3Bucket": "amplify-photoshare-master-183417-deployment", 87 | "S3Key": "amplify-builds/S3Trigger984fb593-4a786c424331774d6135-build.zip" 88 | } 89 | } 90 | }, 91 | "LambdaExecutionRole": { 92 | "Type": "AWS::IAM::Role", 93 | "Properties": { 94 | "RoleName": { 95 | "Fn::If": [ 96 | "ShouldNotCreateEnvResources", 97 | "S3Trigger984fb593LambdaRole984fb593", 98 | { 99 | "Fn::Join": [ 100 | "", 101 | [ 102 | "S3Trigger984fb593LambdaRole984fb593", 103 | "-", 104 | { 105 | "Ref": "env" 106 | } 107 | ] 108 | ] 109 | } 110 | ] 111 | }, 112 | "AssumeRolePolicyDocument": { 113 | "Version": "2012-10-17", 114 | "Statement": [ 115 | { 116 | "Effect": "Allow", 117 | "Principal": { 118 | "Service": [ 119 | "lambda.amazonaws.com" 120 | ] 121 | }, 122 | "Action": [ 123 | "sts:AssumeRole" 124 | ] 125 | } 126 | ] 127 | } 128 | } 129 | }, 130 | "lambdaexecutionpolicy": { 131 | "DependsOn": [ 132 | "LambdaExecutionRole" 133 | ], 134 | "Type": "AWS::IAM::Policy", 135 | "Properties": { 136 | "PolicyName": "lambda-execution-policy", 137 | "Roles": [ 138 | { 139 | "Ref": "LambdaExecutionRole" 140 | } 141 | ], 142 | "PolicyDocument": { 143 | "Version": "2012-10-17", 144 | "Statement": [ 145 | { 146 | "Effect": "Allow", 147 | "Action": [ 148 | "logs:CreateLogGroup", 149 | "logs:CreateLogStream", 150 | "logs:PutLogEvents" 151 | ], 152 | "Resource": { 153 | "Fn::Sub": [ 154 | "arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:*", 155 | { 156 | "region": { 157 | "Ref": "AWS::Region" 158 | }, 159 | "account": { 160 | "Ref": "AWS::AccountId" 161 | }, 162 | "lambda": { 163 | "Ref": "LambdaFunction" 164 | } 165 | } 166 | ] 167 | } 168 | } 169 | ] 170 | } 171 | } 172 | }, 173 | "AmplifyResourcesPolicy": { 174 | "DependsOn": [ 175 | "LambdaExecutionRole" 176 | ], 177 | "Type": "AWS::IAM::Policy", 178 | "Properties": { 179 | "PolicyName": "amplify-lambda-appsync-invoke", 180 | "Roles": [ 181 | { 182 | "Ref": "LambdaExecutionRole" 183 | } 184 | ], 185 | "PolicyDocument": { 186 | "Version": "2012-10-17", 187 | "Statement": [ 188 | { 189 | "Effect": "Allow", 190 | "Action": [ 191 | "appsync:GraphQL" 192 | ], 193 | "Resource": [ 194 | { 195 | "Fn::Join": [ 196 | "", 197 | [ 198 | "arn:aws:appsync:", 199 | { 200 | "Ref": "AWS::Region" 201 | }, 202 | ":", 203 | { 204 | "Ref": "AWS::AccountId" 205 | }, 206 | ":apis/", 207 | { 208 | "Ref": "apiphotoshareGraphQLAPIIdOutput" 209 | }, 210 | "/*" 211 | ] 212 | ] 213 | } 214 | ] 215 | } 216 | ] 217 | } 218 | } 219 | } 220 | }, 221 | "Outputs": { 222 | "Name": { 223 | "Value": { 224 | "Ref": "LambdaFunction" 225 | } 226 | }, 227 | "Arn": { 228 | "Value": { 229 | "Fn::GetAtt": [ 230 | "LambdaFunction", 231 | "Arn" 232 | ] 233 | } 234 | }, 235 | "Region": { 236 | "Value": { 237 | "Ref": "AWS::Region" 238 | } 239 | }, 240 | "LambdaExecutionRole": { 241 | "Value": { 242 | "Ref": "LambdaExecutionRole" 243 | } 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /amplify/backend/function/S3Trigger984fb593/amplify.state: -------------------------------------------------------------------------------- 1 | { 2 | "pluginId": "amplify-nodejs-function-runtime-provider", 3 | "functionRuntime": "nodejs", 4 | "useLegacyBuild": true 5 | } -------------------------------------------------------------------------------- /amplify/backend/function/S3Trigger984fb593/function-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "api": { 4 | "photoshare": [ 5 | "create", 6 | "read", 7 | "update", 8 | "delete" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /amplify/backend/function/S3Trigger984fb593/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "SfnStateMachineName": "PhotoProcessingWorkflow" 3 | } -------------------------------------------------------------------------------- /amplify/backend/function/S3Trigger984fb593/src/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventVersion": "2.0", 5 | "eventSource": "aws:s3", 6 | "awsRegion": "us-east-1", 7 | "eventTime": "1970-01-01T00:00:00.000Z", 8 | "eventName": "ObjectCreated:Put", 9 | "userIdentity": { 10 | "principalId": "EXAMPLE" 11 | }, 12 | "requestParameters": { 13 | "sourceIPAddress": "127.0.0.1" 14 | }, 15 | "responseElements": { 16 | "x-amz-request-id": "EXAMPLE123456789", 17 | "x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH" 18 | }, 19 | "s3": { 20 | "s3SchemaVersion": "1.0", 21 | "configurationId": "testConfigRule", 22 | "bucket": { 23 | "name": "photo-sharee7fa5f1b25be4c63a81417becda53512master-master", 24 | "ownerIdentity": { 25 | "principalId": "EXAMPLE" 26 | }, 27 | "arn": "arn:aws:s3:::example-bucket" 28 | }, 29 | "object": { 30 | "key": "private/us-east-1:4f182a97-9165-46d8-a6de-f29b16927761/uploads/e2d9705b-e39a-4b28-aeea-93d0bb922291.jpg", 31 | "size": 1024, 32 | "eTag": "0123456789abcdef0123456789abcdef", 33 | "sequencer": "0A1B2C3D4E5F678901" 34 | } 35 | } 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /amplify/backend/function/S3Trigger984fb593/src/index.js: -------------------------------------------------------------------------------- 1 | /* Amplify Params - DO NOT EDIT 2 | API_PHOTOSHARE_GRAPHQLAPIENDPOINTOUTPUT 3 | API_PHOTOSHARE_GRAPHQLAPIIDOUTPUT 4 | ENV 5 | REGION 6 | Amplify Params - DO NOT EDIT *//* Amplify Params - DO NOT EDIT 7 | You can access the following resource attributes as environment variables from your Lambda function 8 | var environment = process.env.ENV 9 | var region = process.env.REGION 10 | var apiPhotoshareGraphQLAPIIdOutput = process.env.API_PHOTOSHARE_GRAPHQLAPIIDOUTPUT 11 | var apiPhotoshareGraphQLAPIEndpointOutput = process.env.API_PHOTOSHARE_GRAPHQLAPIENDPOINTOUTPUT 12 | 13 | Amplify Params - DO NOT EDIT */ 14 | require('dotenv').config(); 15 | require('isomorphic-fetch'); 16 | const AWS = require('aws-sdk'); 17 | const AUTH_TYPE = require('aws-appsync').AUTH_TYPE; 18 | const AWSAppSyncClient = require('aws-appsync').default; 19 | const gql = require('graphql-tag') 20 | const stateMachineArn = process.env.STATE_MACHINE_ARN; 21 | 22 | console.log(process.env.STATE_MACHINE_ARN) 23 | console.log(process.env.API_PHOTOSHARE_GRAPHQLAPIENDPOINTOUTPUT) 24 | 25 | let client = new AWSAppSyncClient({ 26 | url: process.env.API_PHOTOSHARE_GRAPHQLAPIENDPOINTOUTPUT, 27 | region: process.env.REGION, 28 | auth: { 29 | type: AUTH_TYPE.AWS_IAM, 30 | credentials: AWS.config.credentials 31 | }, 32 | disableOffline: true 33 | }); 34 | 35 | const UPDATE_PHOTO_MUTATION = gql` 36 | mutation UpdatePhoto( 37 | $input: UpdatePhotoInput! 38 | $condition: ModelPhotoConditionInput 39 | ) { 40 | updatePhoto(input: $input, condition: $condition) { 41 | id 42 | albumId 43 | owner 44 | uploadTime 45 | bucket 46 | SfnExecutionArn 47 | ProcessingStatus 48 | } 49 | } 50 | ` 51 | 52 | 53 | const START_WORKFLOW_MUTATION = gql` 54 | mutation StartSfnExecution( 55 | $input: StartSfnExecutionInput! 56 | ) { 57 | startSfnExecution(input: $input) { 58 | executionArn 59 | startDate 60 | } 61 | } 62 | ` 63 | 64 | async function startSfnExecution(bucketName, key, id) { 65 | const sfnInput = { 66 | s3Bucket: bucketName, 67 | s3Key: key, 68 | objectID: id 69 | }; 70 | const startWorkflowInput = { 71 | input: JSON.stringify(sfnInput), 72 | stateMachineArn: stateMachineArn 73 | } 74 | const startWorkflowResult = await client.mutate({ 75 | mutation: START_WORKFLOW_MUTATION, 76 | variables: {input: startWorkflowInput}, 77 | fetchPolicy: 'no-cache' 78 | }) 79 | 80 | let executionArn = startWorkflowResult.data.startSfnExecution.executionArn 81 | return executionArn 82 | } 83 | 84 | async function processRecord(record) { 85 | const bucketName = record.s3.bucket.name; 86 | const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " ")); 87 | const id = key.split('/').pop().split(".")[0] 88 | console.log('processRecord', JSON.stringify(record)) 89 | 90 | if (!key.includes('upload')) { 91 | console.log('Does not look like an upload from user'); 92 | return; 93 | } 94 | 95 | const SfnExecutionArn = await startSfnExecution(bucketName, key, id); 96 | console.log(`Sfn started. Execution: ${SfnExecutionArn}`) 97 | 98 | const item = { 99 | id, 100 | SfnExecutionArn, 101 | ProcessingStatus: 'RUNNING' 102 | } 103 | console.log('update photo item: ', JSON.stringify(item, null, 2)) 104 | 105 | const result = await client.mutate({ 106 | mutation: UPDATE_PHOTO_MUTATION, 107 | variables: {input: item}, 108 | fetchPolicy: 'no-cache' 109 | }) 110 | 111 | console.log('result', JSON.stringify(result)) 112 | return result 113 | } 114 | 115 | exports.handler = async (event, context, callback) => { 116 | console.log('Received S3 event:', JSON.stringify(event, null, 2)); 117 | 118 | try { 119 | for (let i in event.Records) { 120 | await processRecord(event.Records[i]) 121 | } 122 | callback(null, {status: 'Photo Processed'}); 123 | } catch (err) { 124 | console.error(err); 125 | callback(err); 126 | } 127 | }; -------------------------------------------------------------------------------- /amplify/backend/function/S3Trigger984fb593/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "S3TriggerPhotoProcessor", 3 | "version": "1.0.0", 4 | "description": "Photo uploads trigger that kicks off the Step Functions workflow", 5 | "main": "index.js", 6 | "dependencies": { 7 | "aws-appsync": "^3.0.2", 8 | "dotenv": "^8.2.0", 9 | "graphql-tag": "^2.10.1", 10 | "isomorphic-fetch": "^2.2.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /amplify/backend/function/S3Trigger984fb593/src/reference-index.js: -------------------------------------------------------------------------------- 1 | /* Amplify Params - DO NOT EDIT 2 | You can access the following resource attributes as environment variables from your Lambda function 3 | var environment = process.env.ENV 4 | var region = process.env.REGION 5 | var apiPhotoalbumsGraphQLAPIIdOutput = process.env.API_PHOTOALBUMS_GRAPHQLAPIIDOUTPUT 6 | var apiPhotoalbumsGraphQLAPIEndpointOutput = process.env.API_PHOTOALBUMS_GRAPHQLAPIENDPOINTOUTPUT 7 | 8 | Amplify Params - DO NOT EDIT */// eslint-disable-next-line 9 | 10 | require('es6-promise').polyfill(); 11 | require('isomorphic-fetch'); 12 | const AWS = require('aws-sdk'); 13 | const S3 = new AWS.S3({ signatureVersion: 'v4' }); 14 | const AUTH_TYPE = require('aws-appsync').AUTH_TYPE; 15 | const AWSAppSyncClient = require('aws-appsync').default; 16 | const uuidv4 = require('uuid/v4'); 17 | const gql = require('graphql-tag'); 18 | 19 | /* 20 | Note: Sharp requires native extensions to be installed in a way that is compatible 21 | with Amazon Linux (in order to run successfully in a Lambda execution environment). 22 | 23 | If you're not working in Cloud9, you can follow the instructions on http://sharp.pixelplumbing.com/en/stable/install/#aws-lambda how to install the module and native dependencies. 24 | */ 25 | const Sharp = require('sharp'); 26 | 27 | // We'll expect these environment variables to be defined when the Lambda function is deployed 28 | const THUMBNAIL_WIDTH = parseInt(process.env.THUMBNAIL_WIDTH || 80, 10); 29 | const THUMBNAIL_HEIGHT = parseInt(process.env.THUMBNAIL_HEIGHT || 80, 10); 30 | 31 | let client = null 32 | 33 | 34 | async function storePhotoInfo(item) { 35 | console.log('storePhotoItem', JSON.stringify(item)) 36 | const createPhoto = gql` 37 | mutation CreatePhoto( 38 | $input: CreatePhotoInput! 39 | $condition: ModelPhotoConditionInput 40 | ) { 41 | createPhoto(input: $input, condition: $condition) { 42 | id 43 | albumId 44 | owner 45 | bucket 46 | fullsize { 47 | key 48 | width 49 | height 50 | } 51 | thumbnail { 52 | key 53 | width 54 | height 55 | } 56 | album { 57 | id 58 | name 59 | owner 60 | } 61 | } 62 | } 63 | `; 64 | 65 | console.log('trying to createphoto with input', JSON.stringify(item)) 66 | const result = await client.mutate({ 67 | mutation: createPhoto, 68 | variables: { input: item }, 69 | fetchPolicy: 'no-cache' 70 | }) 71 | 72 | console.log('result', JSON.stringify(result)) 73 | return result 74 | } 75 | 76 | function thumbnailKey(keyPrefix, filename) { 77 | return `${keyPrefix}/resized/${filename}`; 78 | } 79 | 80 | function fullsizeKey(keyPrefix, filename) { 81 | return `${keyPrefix}/fullsize/${filename}`; 82 | } 83 | 84 | function makeThumbnail(photo) { 85 | return Sharp(photo).resize(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT).toBuffer(); 86 | } 87 | 88 | async function resize(photoBody, bucketName, key) { 89 | const keyPrefix = key.substr(0, key.indexOf('/upload/')) 90 | const originalPhotoName = key.substr(key.lastIndexOf('/') + 1) 91 | const originalPhotoDimensions = await Sharp(photoBody).metadata(); 92 | 93 | const thumbnail = await makeThumbnail(photoBody); 94 | 95 | await Promise.all([ 96 | S3.putObject({ 97 | Body: thumbnail, 98 | Bucket: bucketName, 99 | Key: thumbnailKey(keyPrefix, originalPhotoName), 100 | }).promise(), 101 | 102 | S3.copyObject({ 103 | Bucket: bucketName, 104 | CopySource: bucketName + '/' + key, 105 | Key: fullsizeKey(keyPrefix, originalPhotoName), 106 | }).promise(), 107 | ]); 108 | 109 | await S3.deleteObject({ 110 | Bucket: bucketName, 111 | Key: key 112 | }).promise(); 113 | 114 | return { 115 | photoId: originalPhotoName, 116 | 117 | thumbnail: { 118 | key: thumbnailKey(keyPrefix, originalPhotoName), 119 | width: THUMBNAIL_WIDTH, 120 | height: THUMBNAIL_HEIGHT 121 | }, 122 | 123 | fullsize: { 124 | key: fullsizeKey(keyPrefix, originalPhotoName), 125 | width: originalPhotoDimensions.width, 126 | height: originalPhotoDimensions.height 127 | } 128 | }; 129 | }; 130 | 131 | async function processRecord(record) { 132 | const bucketName = record.s3.bucket.name; 133 | const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " ")); 134 | 135 | console.log('processRecord', JSON.stringify(record)) 136 | 137 | if (record.eventName !== "ObjectCreated:Put") { console.log('Is not a new file'); return; } 138 | if (! key.includes('upload/')) { console.log('Does not look like an upload from user'); return; } 139 | 140 | const originalPhoto = await S3.getObject({ Bucket: bucketName, Key: key }).promise() 141 | 142 | const metadata = originalPhoto.Metadata 143 | console.log('metadata', JSON.stringify(metadata)) 144 | console.log('resize') 145 | const sizes = await resize(originalPhoto.Body, bucketName, key); 146 | console.log('sizes', JSON.stringify(sizes)) 147 | const id = uuidv4(); 148 | const item = { 149 | id: id, 150 | owner: metadata.owner, 151 | albumId: metadata.albumid, 152 | bucket: bucketName, 153 | thumbnail: { 154 | width: sizes.thumbnail.width, 155 | height: sizes.thumbnail.height, 156 | key: sizes.thumbnail.key, 157 | }, 158 | fullsize: { 159 | width: sizes.fullsize.width, 160 | height: sizes.fullsize.height, 161 | key: sizes.fullsize.key, 162 | } 163 | } 164 | 165 | console.log(JSON.stringify(metadata), JSON.stringify(sizes), JSON.stringify(item)) 166 | await storePhotoInfo(item); 167 | } 168 | 169 | 170 | exports.handler = async (event, context, callback) => { 171 | console.log('Received S3 event:', JSON.stringify(event, null, 2)); 172 | 173 | client = new AWSAppSyncClient({ 174 | url: process.env.API_PHOTOALBUMS_GRAPHQLAPIENDPOINTOUTPUT, 175 | region: process.env.REGION, 176 | auth: { 177 | type: AUTH_TYPE.AWS_IAM, 178 | credentials: AWS.config.credentials 179 | }, 180 | disableOffline: true 181 | }); 182 | 183 | try { 184 | event.Records.forEach(processRecord); 185 | callback(null, { status: 'Photo Processed' }); 186 | } 187 | catch (err) { 188 | console.error(err); 189 | callback(err); 190 | } 191 | }; 192 | 193 | -------------------------------------------------------------------------------- /amplify/backend/storage/photostorage/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "bucketName": "photo-sharee7fa5f1b25be4c63a81417becda53512", 3 | "authPolicyName": "s3_amplify_de0654c8", 4 | "unauthPolicyName": "s3_amplify_de0654c8", 5 | "authRoleName": { 6 | "Ref": "AuthRoleName" 7 | }, 8 | "unauthRoleName": { 9 | "Ref": "UnauthRoleName" 10 | }, 11 | "selectedGuestPermissions": [ 12 | "s3:GetObject", 13 | "s3:ListBucket" 14 | ], 15 | "selectedAuthenticatedPermissions": [ 16 | "s3:PutObject", 17 | "s3:GetObject", 18 | "s3:ListBucket", 19 | "s3:DeleteObject" 20 | ], 21 | "s3PermissionsAuthenticatedPublic": "s3:PutObject,s3:GetObject,s3:DeleteObject", 22 | "s3PublicPolicy": "Public_policy_a62b0ec0", 23 | "s3PermissionsAuthenticatedUploads": "s3:PutObject", 24 | "s3UploadsPolicy": "Uploads_policy_a62b0ec0", 25 | "s3PermissionsAuthenticatedProtected": "s3:PutObject,s3:GetObject,s3:DeleteObject", 26 | "s3ProtectedPolicy": "Protected_policy_a62b0ec0", 27 | "s3PermissionsAuthenticatedPrivate": "s3:PutObject,s3:GetObject,s3:DeleteObject", 28 | "s3PrivatePolicy": "Private_policy_a62b0ec0", 29 | "AuthenticatedAllowList": "ALLOW", 30 | "s3ReadPolicy": "read_policy_a62b0ec0", 31 | "s3PermissionsGuestPublic": "DISALLOW", 32 | "s3PermissionsGuestUploads": "DISALLOW", 33 | "GuestAllowList": "DISALLOW", 34 | "triggerFunction": "S3Trigger984fb593" 35 | } -------------------------------------------------------------------------------- /amplify/backend/storage/photostorage/storage-params.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /amplify/team-provider-info.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /cloudformation/image-processing.serverless.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Amazon Web Services, Inc. or its affiliates. All Rights Reserved. 2 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at 3 | # http://aws.amazon.com/apache2.0/ 4 | # or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | 6 | AWSTemplateFormatVersion: '2010-09-09' 7 | Description: Backend for photo sharing reference architecture. 8 | Transform: 'AWS::Serverless-2016-10-31' 9 | 10 | Globals: 11 | Function: 12 | Runtime: nodejs12.x 13 | Timeout: 30 14 | Environment: 15 | Variables: 16 | LOG_LEVEL: "INFO" 17 | GRAPHQL_API_ENDPOINT: !Ref GraphQLEndPoint 18 | 19 | Parameters: 20 | PhotoRepoS3Bucket: 21 | Description: s3 bucket for photo uploads 22 | Type: String 23 | GraphQLEndPoint: 24 | Description: graph QL endpoint 25 | Type: String 26 | GraphQLAPIId: 27 | Description: graph QL API ID 28 | Type: String 29 | Stage: 30 | Type: String 31 | Description: Environment stage or git branch 32 | 33 | Resources: 34 | # --------------------------------------------------------------------------------------------------------------------- 35 | # This group of Lambda functions below make up the Step Functions state machine to execute the image processing workflow 36 | # --------------------------------------------------------------------------------------------------------------------- 37 | ImageMagick: 38 | Type: AWS::Serverless::Application 39 | Properties: 40 | Location: 41 | ApplicationId: arn:aws:serverlessrepo:us-east-1:145266761615:applications/image-magick-lambda-layer 42 | SemanticVersion: 1.0.0 43 | 44 | ExtractImageMetadataFunction: 45 | Properties: 46 | CodeUri: ../lambda-functions/extract-image-metadata 47 | Description: "Extract image metadata such as format, size, geolocation, etc." 48 | Handler: index.handler 49 | MemorySize: 1024 50 | Timeout: 200 51 | Policies: 52 | - S3ReadPolicy: 53 | BucketName: 54 | !Ref PhotoRepoS3Bucket 55 | Layers: 56 | - !GetAtt ImageMagick.Outputs.LayerVersion 57 | Type: AWS::Serverless::Function 58 | 59 | TransformMetadataFunction: 60 | Properties: 61 | CodeUri: ../lambda-functions/transform-metadata 62 | Description: "massages JSON of extracted image metadata" 63 | Handler: index.handler 64 | MemorySize: 256 65 | Timeout: 60 66 | Type: AWS::Serverless::Function 67 | 68 | StoreImageMetadataFunction: 69 | Properties: 70 | CodeUri: ../lambda-functions/store-image-metadata 71 | Description: "Store image metadata into database" 72 | Handler: index.handler 73 | Policies: 74 | - Statement: 75 | - Sid: "AppSyncInvoke" 76 | Effect: Allow 77 | Action: 78 | - appsync:GraphQL 79 | Resource: !Sub "arn:aws:appsync:${AWS::Region}:${AWS::AccountId}:apis/${GraphQLAPIId}/*" 80 | Type: AWS::Serverless::Function 81 | 82 | RekognitionFunction: 83 | Properties: 84 | Description: "Use Amazon Rekognition to detect labels from image" 85 | Handler: index.handler 86 | MemorySize: 256 87 | Policies: 88 | - RekognitionDetectOnlyPolicy: {} 89 | - S3ReadPolicy: 90 | BucketName: 91 | !Ref PhotoRepoS3Bucket 92 | CodeUri: 93 | ../lambda-functions/rekognition 94 | Type: AWS::Serverless::Function 95 | 96 | GenerateThumbnailFunction: 97 | Properties: 98 | CodeUri: 99 | ../lambda-functions/thumbnail 100 | Description: "Generate thumbnails for images" 101 | Handler: index.handler 102 | MemorySize: 1536 103 | Timeout: 300 104 | Policies: 105 | - S3FullAccessPolicy: 106 | BucketName: 107 | !Ref PhotoRepoS3Bucket 108 | Layers: 109 | - !GetAtt ImageMagick.Outputs.LayerVersion 110 | Type: AWS::Serverless::Function 111 | 112 | # --------------------------------------------------------------------------------------------------------------------- 113 | # Step functions State Machine 114 | # --------------------------------------------------------------------------------------------------------------------- 115 | 116 | ImageProcStateMachine: 117 | Type: "AWS::Serverless::StateMachine" 118 | Properties: 119 | Name: !Sub PhotoProcessingWorkflow-${Stage} 120 | DefinitionUri: state-machine.asl.json 121 | DefinitionSubstitutions: 122 | ExtractImageMetadataFunction: !GetAtt ExtractImageMetadataFunction.Arn 123 | TransformMetadataFunction: !GetAtt TransformMetadataFunction.Arn 124 | GenerateThumbnailFunction: !GetAtt GenerateThumbnailFunction.Arn 125 | RekognitionFunction: !GetAtt RekognitionFunction.Arn 126 | StoreImageMetadataFunction: !GetAtt StoreImageMetadataFunction.Arn 127 | Policies: # Find out more about SAM policy templates: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-policy-templates.html 128 | - LambdaInvokePolicy: 129 | FunctionName: !Ref GenerateThumbnailFunction 130 | - LambdaInvokePolicy: 131 | FunctionName: !Ref RekognitionFunction 132 | - LambdaInvokePolicy: 133 | FunctionName: !Ref ExtractImageMetadataFunction 134 | - LambdaInvokePolicy: 135 | FunctionName: !Ref StoreImageMetadataFunction 136 | - LambdaInvokePolicy: 137 | FunctionName: !Ref TransformMetadataFunction 138 | 139 | 140 | Outputs: 141 | ProcessingStateMachine: 142 | Value: 143 | Ref: ImageProcStateMachine 144 | -------------------------------------------------------------------------------- /cloudformation/state-machine.asl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "Image Processing workflow", 3 | "StartAt": "ExtractImageMetadata", 4 | "States": { 5 | "ExtractImageMetadata": { 6 | "Type": "Task", 7 | "Resource": "${ExtractImageMetadataFunction}", 8 | "InputPath": "$", 9 | "ResultPath": "$.extractedMetadata", 10 | "Next": "ImageTypeCheck", 11 | "Catch": [ 12 | { 13 | "ErrorEquals": [ 14 | "ImageIdentifyError" 15 | ], 16 | "Next": "NotSupportedImageType" 17 | } 18 | ], 19 | "Retry": [ 20 | { 21 | "ErrorEquals": [ 22 | "ImageIdentifyError" 23 | ], 24 | "MaxAttempts": 0 25 | }, 26 | { 27 | "ErrorEquals": [ 28 | "States.ALL" 29 | ], 30 | "IntervalSeconds": 1, 31 | "MaxAttempts": 2, 32 | "BackoffRate": 1.5 33 | } 34 | ] 35 | }, 36 | "ImageTypeCheck": { 37 | "Type": "Choice", 38 | "Choices": [ 39 | { 40 | "Or": [ 41 | { 42 | "Variable": "$.extractedMetadata.format", 43 | "StringEquals": "JPEG" 44 | }, 45 | { 46 | "Variable": "$.extractedMetadata.format", 47 | "StringEquals": "PNG" 48 | } 49 | ], 50 | "Next": "TransformMetadata" 51 | } 52 | ], 53 | "Default": "NotSupportedImageType" 54 | }, 55 | "TransformMetadata": { 56 | "Type": "Task", 57 | "Resource": "${TransformMetadataFunction}", 58 | "InputPath": "$.extractedMetadata", 59 | "ResultPath": "$.extractedMetadata", 60 | "Retry": [ 61 | { 62 | "ErrorEquals": [ 63 | "States.ALL" 64 | ], 65 | "IntervalSeconds": 1, 66 | "MaxAttempts": 2, 67 | "BackoffRate": 1.5 68 | } 69 | ], 70 | "Next": "ParallelProcessing" 71 | }, 72 | "NotSupportedImageType": { 73 | "Type": "Fail", 74 | "Cause": "Image type not supported!", 75 | "Error": "FileTypeNotSupported" 76 | }, 77 | "ParallelProcessing": { 78 | "Type": "Parallel", 79 | "Branches": [ 80 | { 81 | "StartAt": "Rekognition", 82 | "States": { 83 | "Rekognition": { 84 | "Type": "Task", 85 | "Resource": "${RekognitionFunction}", 86 | "Retry": [ 87 | { 88 | "ErrorEquals": [ 89 | "States.ALL" 90 | ], 91 | "IntervalSeconds": 1, 92 | "MaxAttempts": 2, 93 | "BackoffRate": 1.5 94 | } 95 | ], 96 | "End": true 97 | } 98 | } 99 | }, 100 | { 101 | "StartAt": "Thumbnail", 102 | "States": { 103 | "Thumbnail": { 104 | "Type": "Task", 105 | "Resource": "${GenerateThumbnailFunction}", 106 | "Retry": [ 107 | { 108 | "ErrorEquals": [ 109 | "States.ALL" 110 | ], 111 | "IntervalSeconds": 1, 112 | "MaxAttempts": 2, 113 | "BackoffRate": 1.5 114 | } 115 | ], 116 | "End": true 117 | } 118 | } 119 | } 120 | ], 121 | "ResultPath": "$.parallelResults", 122 | "Next": "StoreImageMetadata" 123 | }, 124 | "StoreImageMetadata": { 125 | "Type": "Task", 126 | "Resource": "${StoreImageMetadataFunction}", 127 | "InputPath": "$", 128 | "ResultPath": "$.storeResult", 129 | "Retry": [ 130 | { 131 | "ErrorEquals": [ 132 | "States.ALL" 133 | ], 134 | "IntervalSeconds": 1, 135 | "MaxAttempts": 2, 136 | "BackoffRate": 1.5 137 | } 138 | ], 139 | "End": true 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /images/amplify-select-role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/images/amplify-select-role.png -------------------------------------------------------------------------------- /images/app-create-album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/images/app-create-album.png -------------------------------------------------------------------------------- /images/app-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/images/app-screenshot.png -------------------------------------------------------------------------------- /images/app-signup-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/images/app-signup-screenshot.png -------------------------------------------------------------------------------- /images/example-album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/images/example-album.png -------------------------------------------------------------------------------- /images/example-analyzed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/images/example-analyzed.png -------------------------------------------------------------------------------- /images/example-processing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/images/example-processing.png -------------------------------------------------------------------------------- /images/photo-processing-backend-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/images/photo-processing-backend-diagram.png -------------------------------------------------------------------------------- /images/step-function-execution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/images/step-function-execution.png -------------------------------------------------------------------------------- /lambda-functions/extract-image-metadata/index.js: -------------------------------------------------------------------------------- 1 | // dependencies 2 | const AWS = require('aws-sdk'); 3 | const gm = require('gm').subClass({ imageMagick: true }); // Enable ImageMagick integration. 4 | const util = require('util'); 5 | const Promise = require('bluebird'); 6 | Promise.promisifyAll(gm.prototype); 7 | 8 | // get reference to S3 client 9 | const s3 = new AWS.S3(); 10 | 11 | exports.handler = (event, context, callback) => { 12 | // Read input from the event. 13 | console.log("Reading input from event:\n", util.inspect(event, { depth: 5 })); 14 | const srcBucket = event.s3Bucket; 15 | // Object key may have spaces or unicode non-ASCII characters. 16 | const srcKey = decodeURIComponent(event.s3Key.replace(/\+/g, " ")); 17 | 18 | var getObjectPromise = s3.getObject({ 19 | Bucket: srcBucket, 20 | Key: srcKey 21 | }).promise(); 22 | 23 | getObjectPromise.then((getObjectResponse) => { 24 | gm(getObjectResponse.Body).identifyAsync().then((data) => { 25 | console.log("Identified metadata:\n", util.inspect(data, { depth: 5 })); 26 | callback(null, data); 27 | }).catch(function (err) { 28 | callback(new ImageIdentifyError(err)); 29 | 30 | }); 31 | }).catch(function (err) { 32 | callback(err); 33 | }); 34 | }; 35 | 36 | function ImageIdentifyError(message) { 37 | this.name = "ImageIdentifyError"; 38 | this.message = message; 39 | } 40 | ImageIdentifyError.prototype = new Error(); 41 | -------------------------------------------------------------------------------- /lambda-functions/extract-image-metadata/lambda-sample-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "Base filename": "-", 3 | "Format": "JPEG (Joint Photographic Experts Group JFIF format)", 4 | "format": "JPEG", 5 | "Class": "DirectClass", 6 | "Geometry": "4032x3024+0+0", 7 | "size": { 8 | "width": 4032, 9 | "height": 3024 10 | }, 11 | "Resolution": "72x72", 12 | "Print size": "56x42", 13 | "Units": "PixelsPerInch", 14 | "Type": "TrueColor", 15 | "Endianess": "Undefined", 16 | "Colorspace": "sRGB", 17 | "Depth": "8-bit", 18 | "depth": 8, 19 | "Channel depth": { 20 | "red": "8-bit", 21 | "green": "8-bit", 22 | "blue": "8-bit" 23 | }, 24 | "Channel statistics": { 25 | "Red": { 26 | "min": "0 (0)", 27 | "max": "239 (0.937255)", 28 | "mean": "92.2995 (0.361959)", 29 | "standard deviation": "56.6032 (0.221973)", 30 | "kurtosis": "-0.342865", 31 | "skewness": "0.833885" 32 | }, 33 | "Green": { 34 | "min": "0 (0)", 35 | "max": "248 (0.972549)", 36 | "mean": "121.324 (0.475779)", 37 | "standard deviation": "51.1459 (0.200572)", 38 | "kurtosis": "-0.401377", 39 | "skewness": "0.437793" 40 | }, 41 | "Blue": { 42 | "min": "0 (0)", 43 | "max": "255 (1)", 44 | "mean": "160.431 (0.629141)", 45 | "standard deviation": "44.2143 (0.173389)", 46 | "kurtosis": "0.123975", 47 | "skewness": "-0.413949" 48 | } 49 | }, 50 | "Image statistics": { 51 | "Overall": { 52 | "min": "0 (0)", 53 | "max": "255 (1)", 54 | "mean": "124.685 (0.48896)", 55 | "standard deviation": "50.9075 (0.199637)", 56 | "kurtosis": "0.452466", 57 | "skewness": "0.145731" 58 | } 59 | }, 60 | "Rendering intent": "Perceptual", 61 | "Gamma": "0.454545", 62 | "Chromaticity": { 63 | "red primary": "(0.64,0.33)", 64 | "green primary": "(0.3,0.6)", 65 | "blue primary": "(0.15,0.06)", 66 | "white point": "(0.3127,0.329)" 67 | }, 68 | "Interlace": "None", 69 | "Background color": "white", 70 | "Border color": "srgb(223,223,223)", 71 | "Matte color": "grey74", 72 | "Transparent color": "black", 73 | "Compose": "Over", 74 | "Page geometry": "4032x3024+0+0", 75 | "Dispose": "Undefined", 76 | "Iterations": "0", 77 | "Compression": "JPEG", 78 | "Quality": "94", 79 | "Orientation": "Undefined", 80 | "Properties": { 81 | "date:create": "2017-06-06T00:50:16+00:00", 82 | "date:modify": "2017-06-06T00:50:16+00:00", 83 | "exif:ApertureValue": "200/100", 84 | "exif:BrightnessValue": "0/100", 85 | "exif:ColorSpace": "1", 86 | "exif:ComponentsConfiguration": "1, 2, 3, 0", 87 | "exif:Compression": "6", 88 | "exif:DateTime": "2016:04:09 14:08:13", 89 | "exif:DateTimeDigitized": "2016:04:09 14:08:13", 90 | "exif:DateTimeOriginal": "2016:04:09 14:08:13", 91 | "exif:ExifImageLength": "3024", 92 | "exif:ExifImageWidth": "4032", 93 | "exif:ExifOffset": "228", 94 | "exif:ExifVersion": "48, 50, 50, 48", 95 | "exif:ExposureBiasValue": "0/1", 96 | "exif:ExposureMode": "0", 97 | "exif:ExposureProgram": "0", 98 | "exif:ExposureTime": "1/6568", 99 | "exif:Flash": "16", 100 | "exif:FlashPixVersion": "48, 49, 48, 48", 101 | "exif:FNumber": "200/100", 102 | "exif:FocalLength": "4670/1000", 103 | "exif:FocalLengthIn35mmFilm": "0", 104 | "exif:GPSAltitude": "272000/100", 105 | "exif:GPSAltitudeRef": "0", 106 | "exif:GPSDateStamp": "2016:04:09", 107 | "exif:GPSInfo": "744", 108 | "exif:GPSLatitude": "46/1, 49/1, 3243/100", 109 | "exif:GPSLatitudeRef": "N", 110 | "exif:GPSLongitude": "121/1, 43/1, 4188/100", 111 | "exif:GPSLongitudeRef": "W", 112 | "exif:GPSTimeStamp": "21/1, 8/1, 8/1", 113 | "exif:GPSVersionID": "2, 2, 0, 0", 114 | "exif:ImageUniqueID": "15f1a7fd7b5a23fd0000000000000000", 115 | "exif:InteroperabilityIndex": "R98", 116 | "exif:InteroperabilityOffset": "950", 117 | "exif:InteroperabilityVersion": "48, 49, 48, 48", 118 | "exif:ISOSpeedRatings": "63", 119 | "exif:JPEGInterchangeFormat": "1074", 120 | "exif:JPEGInterchangeFormatLength": "5366", 121 | "exif:Make": "LGE", 122 | "exif:MeteringMode": "0", 123 | "exif:Model": "Nexus 5X", 124 | "exif:ResolutionUnit": "2", 125 | "exif:SceneCaptureType": "0", 126 | "exif:SceneType": "0", 127 | "exif:SensingMethod": "0", 128 | "exif:ShutterSpeedValue": "12681/1000", 129 | "exif:Software": "bullhead-user 6.0.1 MHC19J 2595691 release-keys", 130 | "exif:SubSecTime": "525867", 131 | "exif:SubSecTimeDigitized": "525867", 132 | "exif:SubSecTimeOriginal": "525867", 133 | "exif:WhiteBalance": "0", 134 | "exif:XResolution": "72/1", 135 | "exif:YCbCrPositioning": "1", 136 | "exif:YResolution": "72/1", 137 | "jpeg:colorspace": "2", 138 | "jpeg:sampling-factor": "2x2,1x1,1x1", 139 | "signature": "6872beb50662324bac90a33163c5790d3f0c83d3f3fc7d28630406da55f17d2d" 140 | }, 141 | "Profiles": { 142 | "Profile-exif": "6446 bytes", 143 | "Profile-icc": { 144 | "Description": "sRGB IEC61966-2-1 black scaled", 145 | "Manufacturer": "sRGB IEC61966-2-1 black scaled", 146 | "Model": "IEC 61966-2-1 Default RGB Colour Space - sRGB", 147 | "Copyright": "Copyright International Color Consortium, 2009" 148 | }, 149 | "Profile-xmp": "259 bytes" 150 | }, 151 | "Artifacts": { 152 | "filename": "-", 153 | "verbose": "true" 154 | }, 155 | "Tainted": "False", 156 | "Filesize": "1.354MB", 157 | "Number pixels": "12.19M", 158 | "Pixels per second": "55.42MB", 159 | "User time": "0.130u", 160 | "Elapsed time": "0:01.219", 161 | "Version": "ImageMagick 6.7.8-9 2016-06-22 Q16 http://www.imagemagick.org", 162 | "path": "unknown.jpg" 163 | } 164 | -------------------------------------------------------------------------------- /lambda-functions/extract-image-metadata/local-testing.sh: -------------------------------------------------------------------------------- 1 | # use lambda-local (https://www.npmjs.com/package/lambda-local) to test lambda functions locally 2 | lambda-local -l index.js -h handler -e step-sample-input.json -t 60 -------------------------------------------------------------------------------- /lambda-functions/extract-image-metadata/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extract-image-metadata", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "extract-image-metadata", 9 | "version": "1.0.0", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "bluebird": "3.7.2", 13 | "gm": "^1.25.0" 14 | } 15 | }, 16 | "node_modules/array-parallel": { 17 | "version": "0.1.3", 18 | "resolved": "https://registry.npmjs.org/array-parallel/-/array-parallel-0.1.3.tgz", 19 | "integrity": "sha1-j3hTCJJu1apHjEfmTRszS2wMlH0=" 20 | }, 21 | "node_modules/array-series": { 22 | "version": "0.1.5", 23 | "resolved": "https://registry.npmjs.org/array-series/-/array-series-0.1.5.tgz", 24 | "integrity": "sha1-3103v8XC7wdV4qpPkv6ufUtaly8=" 25 | }, 26 | "node_modules/bluebird": { 27 | "version": "3.7.2", 28 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", 29 | "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" 30 | }, 31 | "node_modules/cross-spawn": { 32 | "version": "4.0.2", 33 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", 34 | "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", 35 | "dependencies": { 36 | "lru-cache": "^4.0.1", 37 | "which": "^1.2.9" 38 | } 39 | }, 40 | "node_modules/debug": { 41 | "version": "3.2.7", 42 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", 43 | "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", 44 | "dependencies": { 45 | "ms": "^2.1.1" 46 | } 47 | }, 48 | "node_modules/gm": { 49 | "version": "1.25.0", 50 | "resolved": "https://registry.npmjs.org/gm/-/gm-1.25.0.tgz", 51 | "integrity": "sha512-4kKdWXTtgQ4biIo7hZA396HT062nDVVHPjQcurNZ3o/voYN+o5FUC5kOwuORbpExp3XbTJ3SU7iRipiIhQtovw==", 52 | "dependencies": { 53 | "array-parallel": "~0.1.3", 54 | "array-series": "~0.1.5", 55 | "cross-spawn": "^4.0.0", 56 | "debug": "^3.1.0" 57 | }, 58 | "engines": { 59 | "node": ">=14" 60 | } 61 | }, 62 | "node_modules/isexe": { 63 | "version": "2.0.0", 64 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 65 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 66 | }, 67 | "node_modules/lru-cache": { 68 | "version": "4.1.5", 69 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", 70 | "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", 71 | "dependencies": { 72 | "pseudomap": "^1.0.2", 73 | "yallist": "^2.1.2" 74 | } 75 | }, 76 | "node_modules/ms": { 77 | "version": "2.1.3", 78 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 79 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 80 | }, 81 | "node_modules/pseudomap": { 82 | "version": "1.0.2", 83 | "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", 84 | "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" 85 | }, 86 | "node_modules/which": { 87 | "version": "1.3.1", 88 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 89 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 90 | "dependencies": { 91 | "isexe": "^2.0.0" 92 | }, 93 | "bin": { 94 | "which": "bin/which" 95 | } 96 | }, 97 | "node_modules/yallist": { 98 | "version": "2.1.2", 99 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", 100 | "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" 101 | } 102 | }, 103 | "dependencies": { 104 | "array-parallel": { 105 | "version": "0.1.3", 106 | "resolved": "https://registry.npmjs.org/array-parallel/-/array-parallel-0.1.3.tgz", 107 | "integrity": "sha1-j3hTCJJu1apHjEfmTRszS2wMlH0=" 108 | }, 109 | "array-series": { 110 | "version": "0.1.5", 111 | "resolved": "https://registry.npmjs.org/array-series/-/array-series-0.1.5.tgz", 112 | "integrity": "sha1-3103v8XC7wdV4qpPkv6ufUtaly8=" 113 | }, 114 | "bluebird": { 115 | "version": "3.7.2", 116 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", 117 | "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" 118 | }, 119 | "cross-spawn": { 120 | "version": "4.0.2", 121 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", 122 | "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", 123 | "requires": { 124 | "lru-cache": "^4.0.1", 125 | "which": "^1.2.9" 126 | } 127 | }, 128 | "debug": { 129 | "version": "3.2.7", 130 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", 131 | "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", 132 | "requires": { 133 | "ms": "^2.1.1" 134 | } 135 | }, 136 | "gm": { 137 | "version": "1.25.0", 138 | "resolved": "https://registry.npmjs.org/gm/-/gm-1.25.0.tgz", 139 | "integrity": "sha512-4kKdWXTtgQ4biIo7hZA396HT062nDVVHPjQcurNZ3o/voYN+o5FUC5kOwuORbpExp3XbTJ3SU7iRipiIhQtovw==", 140 | "requires": { 141 | "array-parallel": "~0.1.3", 142 | "array-series": "~0.1.5", 143 | "cross-spawn": "^4.0.0", 144 | "debug": "^3.1.0" 145 | } 146 | }, 147 | "isexe": { 148 | "version": "2.0.0", 149 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 150 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 151 | }, 152 | "lru-cache": { 153 | "version": "4.1.5", 154 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", 155 | "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", 156 | "requires": { 157 | "pseudomap": "^1.0.2", 158 | "yallist": "^2.1.2" 159 | } 160 | }, 161 | "ms": { 162 | "version": "2.1.3", 163 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 164 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 165 | }, 166 | "pseudomap": { 167 | "version": "1.0.2", 168 | "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", 169 | "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" 170 | }, 171 | "which": { 172 | "version": "1.3.1", 173 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 174 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 175 | "requires": { 176 | "isexe": "^2.0.0" 177 | } 178 | }, 179 | "yallist": { 180 | "version": "2.1.2", 181 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", 182 | "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /lambda-functions/extract-image-metadata/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extract-image-metadata", 3 | "version": "1.0.0", 4 | "description": "Extracts EXIF, size, format, etc. info from an image", 5 | "author": "Angela Wang", 6 | "license": "Apache-2.0", 7 | "dependencies": { 8 | "bluebird": "3.7.2", 9 | "gm": "^1.25.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lambda-functions/extract-image-metadata/step-sample-input.json: -------------------------------------------------------------------------------- 1 | { 2 | "s3Bucket": "refarch-photorepos3bucket-xvqjajtk05bo", 3 | "s3Key": "Incoming/1452120176941-bob/ba-IMG_20160409_140812.jpg", 4 | "objectID": "1452120176941-bob/ba-IMG_20160409_140812.jpg" 5 | } 6 | -------------------------------------------------------------------------------- /lambda-functions/extract-image-metadata/step-sample-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "s3Bucket": "refarch-photorepos3bucket-xvqjajtk05bo", 3 | "s3Key": "Incoming/1452120176941-bob/ba-IMG_20160409_140812.jpg", 4 | "objectID": "1452120176941-bob/ba-IMG_20160409_140812.jpg", 5 | "extractedMetadata": { 6 | "Base filename": "-", 7 | "Format": "JPEG (Joint Photographic Experts Group JFIF format)", 8 | "format": "JPEG", 9 | "Class": "DirectClass", 10 | "Geometry": "4032x3024+0+0", 11 | "size": { 12 | "width": 4032, 13 | "height": 3024 14 | }, 15 | "Resolution": "72x72", 16 | "Print size": "56x42", 17 | "Units": "PixelsPerInch", 18 | "Type": "TrueColor", 19 | "Endianess": "Undefined", 20 | "Colorspace": "sRGB", 21 | "Depth": "8-bit", 22 | "depth": 8, 23 | "Channel depth": { 24 | "red": "8-bit", 25 | "green": "8-bit", 26 | "blue": "8-bit" 27 | }, 28 | "Channel statistics": { 29 | "Red": { 30 | "min": "0 (0)", 31 | "max": "239 (0.937255)", 32 | "mean": "92.2995 (0.361959)", 33 | "standard deviation": "56.6032 (0.221973)", 34 | "kurtosis": "-0.342865", 35 | "skewness": "0.833885" 36 | }, 37 | "Green": { 38 | "min": "0 (0)", 39 | "max": "248 (0.972549)", 40 | "mean": "121.324 (0.475779)", 41 | "standard deviation": "51.1459 (0.200572)", 42 | "kurtosis": "-0.401377", 43 | "skewness": "0.437793" 44 | }, 45 | "Blue": { 46 | "min": "0 (0)", 47 | "max": "255 (1)", 48 | "mean": "160.431 (0.629141)", 49 | "standard deviation": "44.2143 (0.173389)", 50 | "kurtosis": "0.123975", 51 | "skewness": "-0.413949" 52 | } 53 | }, 54 | "Image statistics": { 55 | "Overall": { 56 | "min": "0 (0)", 57 | "max": "255 (1)", 58 | "mean": "124.685 (0.48896)", 59 | "standard deviation": "50.9075 (0.199637)", 60 | "kurtosis": "0.452466", 61 | "skewness": "0.145731" 62 | } 63 | }, 64 | "Rendering intent": "Perceptual", 65 | "Gamma": "0.454545", 66 | "Chromaticity": { 67 | "red primary": "(0.64,0.33)", 68 | "green primary": "(0.3,0.6)", 69 | "blue primary": "(0.15,0.06)", 70 | "white point": "(0.3127,0.329)" 71 | }, 72 | "Interlace": "None", 73 | "Background color": "white", 74 | "Border color": "srgb(223,223,223)", 75 | "Matte color": "grey74", 76 | "Transparent color": "black", 77 | "Compose": "Over", 78 | "Page geometry": "4032x3024+0+0", 79 | "Dispose": "Undefined", 80 | "Iterations": "0", 81 | "Compression": "JPEG", 82 | "Quality": "94", 83 | "Orientation": "Undefined", 84 | "Properties": { 85 | "date:create": "2017-06-06T00:50:16+00:00", 86 | "date:modify": "2017-06-06T00:50:16+00:00", 87 | "exif:ApertureValue": "200/100", 88 | "exif:BrightnessValue": "0/100", 89 | "exif:ColorSpace": "1", 90 | "exif:ComponentsConfiguration": "1, 2, 3, 0", 91 | "exif:Compression": "6", 92 | "exif:DateTime": "2016:04:09 14:08:13", 93 | "exif:DateTimeDigitized": "2016:04:09 14:08:13", 94 | "exif:DateTimeOriginal": "2016:04:09 14:08:13", 95 | "exif:ExifImageLength": "3024", 96 | "exif:ExifImageWidth": "4032", 97 | "exif:ExifOffset": "228", 98 | "exif:ExifVersion": "48, 50, 50, 48", 99 | "exif:ExposureBiasValue": "0/1", 100 | "exif:ExposureMode": "0", 101 | "exif:ExposureProgram": "0", 102 | "exif:ExposureTime": "1/6568", 103 | "exif:Flash": "16", 104 | "exif:FlashPixVersion": "48, 49, 48, 48", 105 | "exif:FNumber": "200/100", 106 | "exif:FocalLength": "4670/1000", 107 | "exif:FocalLengthIn35mmFilm": "0", 108 | "exif:GPSAltitude": "272000/100", 109 | "exif:GPSAltitudeRef": "0", 110 | "exif:GPSDateStamp": "2016:04:09", 111 | "exif:GPSInfo": "744", 112 | "exif:GPSLatitude": "46/1, 49/1, 3243/100", 113 | "exif:GPSLatitudeRef": "N", 114 | "exif:GPSLongitude": "121/1, 43/1, 4188/100", 115 | "exif:GPSLongitudeRef": "W", 116 | "exif:GPSTimeStamp": "21/1, 8/1, 8/1", 117 | "exif:GPSVersionID": "2, 2, 0, 0", 118 | "exif:ImageUniqueID": "15f1a7fd7b5a23fd0000000000000000", 119 | "exif:InteroperabilityIndex": "R98", 120 | "exif:InteroperabilityOffset": "950", 121 | "exif:InteroperabilityVersion": "48, 49, 48, 48", 122 | "exif:ISOSpeedRatings": "63", 123 | "exif:JPEGInterchangeFormat": "1074", 124 | "exif:JPEGInterchangeFormatLength": "5366", 125 | "exif:Make": "LGE", 126 | "exif:MeteringMode": "0", 127 | "exif:Model": "Nexus 5X", 128 | "exif:ResolutionUnit": "2", 129 | "exif:SceneCaptureType": "0", 130 | "exif:SceneType": "0", 131 | "exif:SensingMethod": "0", 132 | "exif:ShutterSpeedValue": "12681/1000", 133 | "exif:Software": "bullhead-user 6.0.1 MHC19J 2595691 release-keys", 134 | "exif:SubSecTime": "525867", 135 | "exif:SubSecTimeDigitized": "525867", 136 | "exif:SubSecTimeOriginal": "525867", 137 | "exif:WhiteBalance": "0", 138 | "exif:XResolution": "72/1", 139 | "exif:YCbCrPositioning": "1", 140 | "exif:YResolution": "72/1", 141 | "jpeg:colorspace": "2", 142 | "jpeg:sampling-factor": "2x2,1x1,1x1", 143 | "signature": "6872beb50662324bac90a33163c5790d3f0c83d3f3fc7d28630406da55f17d2d" 144 | }, 145 | "Profiles": { 146 | "Profile-exif": "6446 bytes", 147 | "Profile-icc": { 148 | "Description": "sRGB IEC61966-2-1 black scaled", 149 | "Manufacturer": "sRGB IEC61966-2-1 black scaled", 150 | "Model": "IEC 61966-2-1 Default RGB Colour Space - sRGB", 151 | "Copyright": "Copyright International Color Consortium, 2009" 152 | }, 153 | "Profile-xmp": "259 bytes" 154 | }, 155 | "Artifacts": { 156 | "filename": "-", 157 | "verbose": "true" 158 | }, 159 | "Tainted": "False", 160 | "Filesize": "1.354MB", 161 | "Number pixels": "12.19M", 162 | "Pixels per second": "55.42MB", 163 | "User time": "0.130u", 164 | "Elapsed time": "0:01.219", 165 | "Version": "ImageMagick 6.7.8-9 2016-06-22 Q16 http://www.imagemagick.org", 166 | "path": "unknown.jpg" 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /lambda-functions/rekognition/index.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const AWS = require('aws-sdk'); 3 | const rekognition = new AWS.Rekognition(); 4 | 5 | /** 6 | * Calls the Rekognition service to detect lables in an image. 7 | * @param event should contain "s3Bucket" and "s3Key" fields 8 | * @param context 9 | * @param callback 10 | */ 11 | exports.handler = async (event, context, callback) => { 12 | console.log("Reading input from event:\n", util.inspect(event, { depth: 5 })); 13 | 14 | const srcBucket = event.s3Bucket; 15 | // Object key may have spaces or unicode non-ASCII characters. 16 | const srcKey = decodeURIComponent(event.s3Key.replace(/\+/g, " ")); 17 | 18 | var params = { 19 | Image: { 20 | S3Object: { 21 | Bucket: srcBucket, 22 | Name: srcKey 23 | } 24 | }, 25 | MaxLabels: 10, 26 | MinConfidence: 60 27 | }; 28 | 29 | try { 30 | const result = await rekognition.detectLabels(params).promise(); 31 | callback(null, result.Labels); 32 | } catch (err) { 33 | callback(err); 34 | } 35 | 36 | }; 37 | -------------------------------------------------------------------------------- /lambda-functions/rekognition/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rekognition", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "rekognition", 9 | "version": "1.0.0", 10 | "license": "Apache-2.0" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lambda-functions/rekognition/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rekognition", 3 | "version": "1.0.0", 4 | "description": "call Amazon Rekognition to detect labels in an image stored in s3", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Angela Wang", 10 | "license": "Apache-2.0" 11 | } 12 | -------------------------------------------------------------------------------- /lambda-functions/rekognition/step-sample-input.json: -------------------------------------------------------------------------------- 1 | { 2 | "s3Bucket": "refarch-photorepos3bucket-xvqjajtk05bo", 3 | "s3Key": "Incoming/3165139876941-bob/ba-IMG_20160903_172634.jpg", 4 | "objectID": "3165139876941-bob/ba-IMG_20160903_172634.jpg", 5 | "extractedMetadata": { 6 | "creationTime": "2016:09:03 17:26:36", 7 | "geo": { 8 | "latitude": { 9 | "D": 47, 10 | "M": 37, 11 | "S": 15.11, 12 | "Direction": "N" 13 | }, 14 | "longitude": { 15 | "D": 122, 16 | "M": 20, 17 | "S": 59.96, 18 | "Direction": "W" 19 | } 20 | }, 21 | "exifMake": "LGE", 22 | "exifModel": "Nexus 5X", 23 | "dimensions": { 24 | "width": 4032, 25 | "height": 3024 26 | }, 27 | "fileSize": "1.983MB", 28 | "format": "JPEG" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lambda-functions/rekognition/step-sample-output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "Plant", 4 | "Confidence": 88.06138610839844 5 | }, 6 | { 7 | "Name": "Potted Plant", 8 | "Confidence": 88.06138610839844 9 | }, 10 | { 11 | "Name": "Blossom", 12 | "Confidence": 64.35401916503906 13 | }, 14 | { 15 | "Name": "Flora", 16 | "Confidence": 64.35401916503906 17 | }, 18 | { 19 | "Name": "Flower", 20 | "Confidence": 64.35401916503906 21 | }, 22 | { 23 | "Name": "Amaryllis", 24 | "Confidence": 60.95824432373047 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /lambda-functions/store-image-metadata/index.js: -------------------------------------------------------------------------------- 1 | // dependencies 2 | require('isomorphic-fetch'); 3 | const AWS = require('aws-sdk'); 4 | const AUTH_TYPE = require('aws-appsync').AUTH_TYPE; 5 | const AWSAppSyncClient = require('aws-appsync').default; 6 | const gql = require('graphql-tag') 7 | const util = require('util'); 8 | 9 | let client = new AWSAppSyncClient({ 10 | url: process.env.GRAPHQL_API_ENDPOINT, 11 | region: process.env.AWS_REGION, 12 | auth: { 13 | type: AUTH_TYPE.AWS_IAM, 14 | credentials: AWS.config.credentials 15 | }, 16 | disableOffline: true 17 | }); 18 | 19 | const UPDATE_PHOTO = gql` 20 | mutation UpdatePhoto( 21 | $input: UpdatePhotoInput! 22 | $condition: ModelPhotoConditionInput 23 | ) { 24 | updatePhoto(input: $input, condition: $condition) { 25 | id 26 | albumId 27 | uploadTime 28 | bucket 29 | fullsize { 30 | key 31 | width 32 | height 33 | } 34 | thumbnail { 35 | key 36 | width 37 | height 38 | } 39 | format 40 | exifMake 41 | exitModel 42 | objectDetected 43 | SfnExecutionArn 44 | ProcessingStatus 45 | geoLocation { 46 | Latitude { 47 | D 48 | M 49 | S 50 | Direction 51 | } 52 | Longtitude { 53 | D 54 | M 55 | S 56 | Direction 57 | } 58 | } 59 | owner 60 | } 61 | } 62 | `; 63 | 64 | 65 | exports.handler = async (event, context, callback) => { 66 | console.log("Reading input from event:\n", util.inspect(event, { depth: 5 })); 67 | const id = event.objectID 68 | let extractedMetadata = event.extractedMetadata; 69 | const fullsize = { 70 | key: event.s3Key, 71 | width: extractedMetadata.dimensions.width, 72 | height: extractedMetadata.dimensions.height 73 | } 74 | 75 | const updateInput = { 76 | id, 77 | fullsize, 78 | format: extractedMetadata.format, 79 | exifMake: extractedMetadata.exifMake || null, 80 | exitModel: extractedMetadata.exifModel || null, 81 | ProcessingStatus: "SUCCEEDED" 82 | 83 | } 84 | const thumbnailInfo = event.parallelResults[1]; 85 | 86 | if (thumbnailInfo) { 87 | updateInput['thumbnail'] = { 88 | key: thumbnailInfo.s3key, 89 | width: Math.round(thumbnailInfo.width), 90 | height: Math.round(thumbnailInfo.height) 91 | } 92 | } 93 | 94 | if (event.parallelResults[0]) { 95 | const labels = event.parallelResults[0]; 96 | let tags = [] 97 | for (let i in labels) { 98 | tags.push(labels[i]["Name"]) 99 | } 100 | updateInput["objectDetected"] = tags 101 | } 102 | 103 | 104 | if (event.extractedMetadata.geo) { 105 | updateInput['geoLocation'] = { 106 | Latitude: event.extractedMetadata.geo.latitude, 107 | Longtitude: event.extractedMetadata.geo.longitude 108 | } 109 | } 110 | 111 | console.log(JSON.stringify(updateInput, null, 2)); 112 | 113 | const result = await client.mutate({ 114 | mutation: UPDATE_PHOTO, 115 | variables: { input: updateInput }, 116 | fetchPolicy: 'no-cache' 117 | }) 118 | 119 | return { "Status": "Success" } 120 | } 121 | -------------------------------------------------------------------------------- /lambda-functions/store-image-metadata/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "store-image-metadata", 3 | "version": "1.0.0", 4 | "description": "Store extracted image info into backend database", 5 | "main": "index.js", 6 | "repository": "https://github.com/aws-samples/lambda-refarch-imagerecognition", 7 | "author": "Angela Wang", 8 | "license": "Apache-2", 9 | "dependencies": { 10 | "aws-appsync": "^4.1.9", 11 | "graphql": "^16.6.0", 12 | "graphql-tag": "^2.12.6", 13 | "isomorphic-fetch": "^3.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lambda-functions/store-image-metadata/step-sample-input.json: -------------------------------------------------------------------------------- 1 | { 2 | "s3Bucket": "photo-sharee7fa5f1b25be4c63a81417becda53512master-master", 3 | "s3Key": "private/us-east-1:4f182a97-9165-46d8-a6de-f29b16927761/uploads/1d0e9d92-b444-4ed2-a4e8-e1ee6071bf2a.png", 4 | "objectID": "private/us-east-1:4f182a97-9165-46d8-a6de-f29b16927761/uploads/1d0e9d92-b444-4ed2-a4e8-e1ee6071bf2a.png", 5 | "extractedMetadata": { 6 | "creationTime": "2016:07:06 09:48:21", 7 | "geo": { 8 | "latitude": { 9 | "D": 33, 10 | "M": 53, 11 | "S": 52.41, 12 | "Direction": "N" 13 | }, 14 | "longitude": { 15 | "D": 118, 16 | "M": 25, 17 | "S": 7.25, 18 | "Direction": "W" 19 | } 20 | }, 21 | "exifMake": "LGE", 22 | "exifModel": "Nexus 5X", 23 | "dimensions": { 24 | "width": 1402, 25 | "height": 783 26 | }, 27 | "fileSize": "88504B", 28 | "format": "PNG" 29 | }, 30 | "parallelResults": [ 31 | [ 32 | { 33 | "Name": "Word", 34 | "Confidence": 96.80172729492188, 35 | "Instances": [], 36 | "Parents": [] 37 | }, 38 | { 39 | "Name": "Text", 40 | "Confidence": 96.48905944824219, 41 | "Instances": [], 42 | "Parents": [] 43 | }, 44 | { 45 | "Name": "File", 46 | "Confidence": 77.9222183227539, 47 | "Instances": [], 48 | "Parents": [] 49 | }, 50 | { 51 | "Name": "Scoreboard", 52 | "Confidence": 74.76708984375, 53 | "Instances": [ 54 | { 55 | "BoundingBox": { 56 | "Width": 0.9764256477355957, 57 | "Height": 0.8571417331695557, 58 | "Left": 0.014487230218946934, 59 | "Top": 0.06307414174079895 60 | }, 61 | "Confidence": 74.76708984375 62 | } 63 | ], 64 | "Parents": [] 65 | }, 66 | { 67 | "Name": "Webpage", 68 | "Confidence": 70.3815689086914, 69 | "Instances": [], 70 | "Parents": [ 71 | { 72 | "Name": "File" 73 | } 74 | ] 75 | }, 76 | { 77 | "Name": "Symbol", 78 | "Confidence": 64.35458374023438, 79 | "Instances": [], 80 | "Parents": [] 81 | } 82 | ], 83 | { 84 | "width": 250, 85 | "height": 139.62196861626248, 86 | "s3key": "resized/private/us-east-1:4f182a97-9165-46d8-a6de-f29b16927761/uploads/1d0e9d92-b444-4ed2-a4e8-e1ee6071bf2a.png", 87 | "s3Bucket": "photo-sharee7fa5f1b25be4c63a81417becda53512master-master" 88 | } 89 | ] 90 | } -------------------------------------------------------------------------------- /lambda-functions/thumbnail/index.js: -------------------------------------------------------------------------------- 1 | // dependencies 2 | const S3 = require('aws-sdk/clients/s3'); 3 | const gm = require('gm').subClass({ imageMagick: true }); // Enable ImageMagick integration. 4 | const util = require('util'); 5 | 6 | // constants 7 | const MAX_WIDTH = 250; 8 | const MAX_HEIGHT = 250; 9 | 10 | const s3 = new S3(); 11 | 12 | function thumbnailKey(keyPrefix, filename) { 13 | return `${keyPrefix}/resized/${filename}`; 14 | } 15 | 16 | async function generateThumbnail(s3Bucket, srcKey, width, height, format) { 17 | let originalPhoto = await s3.getObject({ Bucket: s3Bucket, Key: srcKey }).promise(); 18 | 19 | const resizePromise = new Promise((resolve, reject) => { 20 | gm(originalPhoto.Body).resize(width, height).toBuffer(format, (err, buffer) => { 21 | if (err) { 22 | reject(err); 23 | } else { 24 | resolve(buffer); 25 | } 26 | }); 27 | }); 28 | return await resizePromise 29 | } 30 | 31 | /** 32 | * Generate a thumbnail of an image stored in s3 and store the thumbnail back in the same bucket under the "Thumbnail/" prefix 33 | * @param event 34 | * should have the following inputs: 35 | * { 36 | * "s3Bucket": "mybucket", 37 | * "s3Key": "mykey", 38 | * "objectID": "l3j4234-234", 39 | * "extractedMetadata": { 40 | * "dimensions": { 41 | * "width": 4567, 42 | * "height": 3456 43 | * } 44 | * } 45 | * } 46 | * @param context 47 | * @param callback 48 | */ 49 | exports.handler = async (event, context, callback) => { 50 | try { 51 | console.log("Reading input from event:\n", util.inspect(event, { depth: 5 })); 52 | const s3Bucket = event.s3Bucket; 53 | // Object key may have spaces or unicode non-ASCII characters. 54 | const srcKey = event.s3Key; 55 | 56 | const size = event.extractedMetadata.dimensions; 57 | const scalingFactor = Math.min( 58 | MAX_WIDTH / size.width, 59 | MAX_HEIGHT / size.height 60 | ); 61 | const width = scalingFactor * size.width; 62 | const height = scalingFactor * size.height; 63 | const format = event.extractedMetadata.format; // JPG or PNG 64 | 65 | const resizedBuffer = await generateThumbnail(s3Bucket, srcKey, width, height, format); 66 | console.log(`resized : ${width} x ${height}`) 67 | 68 | const keyPrefix = srcKey.substr(0, srcKey.indexOf('/uploads/')) 69 | const originalPhotoName = srcKey.substr(srcKey.lastIndexOf('/') + 1) 70 | const destKey = thumbnailKey(keyPrefix, originalPhotoName) 71 | const s3PutParams = { 72 | Bucket: s3Bucket, 73 | Key: destKey, 74 | ContentType: "image/" + format.toLowerCase(), 75 | Body: resizedBuffer 76 | }; 77 | await s3.upload(s3PutParams).promise() 78 | console.log(`uploaded : s3://${s3Bucket}/${destKey}`) 79 | 80 | let thumbnailInfo = { 81 | width, 82 | height, 83 | s3key: destKey, 84 | s3Bucket 85 | } 86 | 87 | callback(null, thumbnailInfo) 88 | } catch (err) { 89 | console.error(err); 90 | callback(err); 91 | } 92 | } 93 | 94 | 95 | -------------------------------------------------------------------------------- /lambda-functions/thumbnail/lambda-sample-input.json: -------------------------------------------------------------------------------- 1 | { 2 | "s3Bucket": "refarch-photorepos3bucket-xvqjajtk05bo", 3 | "s3Key": "Incoming/3165139876941-bob/ba-IMG_20160903_172634.jpg", 4 | "objectID": "3165139876941-bob/ba-IMG_20160903_172634.jpg", 5 | "extractedMetadata": { 6 | "creationTime": "2016:09:03 17:26:36", 7 | "geo": { 8 | "latitude": { 9 | "D": 47, 10 | "M": 37, 11 | "S": 15.11, 12 | "Direction": "N" 13 | }, 14 | "longitude": { 15 | "D": 122, 16 | "M": 20, 17 | "S": 59.96, 18 | "Direction": "W" 19 | } 20 | }, 21 | "exifMake": "LGE", 22 | "exifModel": "Nexus 5X", 23 | "dimensions": { 24 | "width": 4032, 25 | "height": 3024 26 | }, 27 | "fileSize": "1.983MB", 28 | "format": "JPEG" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lambda-functions/thumbnail/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generate-thumbnail", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "generate-thumbnail", 9 | "version": "1.0.0", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "gm": "^1.25.0" 13 | } 14 | }, 15 | "node_modules/array-parallel": { 16 | "version": "0.1.3", 17 | "resolved": "https://registry.npmjs.org/array-parallel/-/array-parallel-0.1.3.tgz", 18 | "integrity": "sha1-j3hTCJJu1apHjEfmTRszS2wMlH0=" 19 | }, 20 | "node_modules/array-series": { 21 | "version": "0.1.5", 22 | "resolved": "https://registry.npmjs.org/array-series/-/array-series-0.1.5.tgz", 23 | "integrity": "sha1-3103v8XC7wdV4qpPkv6ufUtaly8=" 24 | }, 25 | "node_modules/cross-spawn": { 26 | "version": "4.0.2", 27 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", 28 | "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", 29 | "dependencies": { 30 | "lru-cache": "^4.0.1", 31 | "which": "^1.2.9" 32 | } 33 | }, 34 | "node_modules/debug": { 35 | "version": "3.2.7", 36 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", 37 | "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", 38 | "dependencies": { 39 | "ms": "^2.1.1" 40 | } 41 | }, 42 | "node_modules/gm": { 43 | "version": "1.25.0", 44 | "resolved": "https://registry.npmjs.org/gm/-/gm-1.25.0.tgz", 45 | "integrity": "sha512-4kKdWXTtgQ4biIo7hZA396HT062nDVVHPjQcurNZ3o/voYN+o5FUC5kOwuORbpExp3XbTJ3SU7iRipiIhQtovw==", 46 | "dependencies": { 47 | "array-parallel": "~0.1.3", 48 | "array-series": "~0.1.5", 49 | "cross-spawn": "^4.0.0", 50 | "debug": "^3.1.0" 51 | }, 52 | "engines": { 53 | "node": ">=14" 54 | } 55 | }, 56 | "node_modules/isexe": { 57 | "version": "2.0.0", 58 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 59 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 60 | }, 61 | "node_modules/lru-cache": { 62 | "version": "4.1.5", 63 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", 64 | "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", 65 | "dependencies": { 66 | "pseudomap": "^1.0.2", 67 | "yallist": "^2.1.2" 68 | } 69 | }, 70 | "node_modules/ms": { 71 | "version": "2.1.3", 72 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 73 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 74 | }, 75 | "node_modules/pseudomap": { 76 | "version": "1.0.2", 77 | "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", 78 | "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" 79 | }, 80 | "node_modules/which": { 81 | "version": "1.3.1", 82 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 83 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 84 | "dependencies": { 85 | "isexe": "^2.0.0" 86 | }, 87 | "bin": { 88 | "which": "bin/which" 89 | } 90 | }, 91 | "node_modules/yallist": { 92 | "version": "2.1.2", 93 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", 94 | "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" 95 | } 96 | }, 97 | "dependencies": { 98 | "array-parallel": { 99 | "version": "0.1.3", 100 | "resolved": "https://registry.npmjs.org/array-parallel/-/array-parallel-0.1.3.tgz", 101 | "integrity": "sha1-j3hTCJJu1apHjEfmTRszS2wMlH0=" 102 | }, 103 | "array-series": { 104 | "version": "0.1.5", 105 | "resolved": "https://registry.npmjs.org/array-series/-/array-series-0.1.5.tgz", 106 | "integrity": "sha1-3103v8XC7wdV4qpPkv6ufUtaly8=" 107 | }, 108 | "cross-spawn": { 109 | "version": "4.0.2", 110 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", 111 | "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", 112 | "requires": { 113 | "lru-cache": "^4.0.1", 114 | "which": "^1.2.9" 115 | } 116 | }, 117 | "debug": { 118 | "version": "3.2.7", 119 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", 120 | "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", 121 | "requires": { 122 | "ms": "^2.1.1" 123 | } 124 | }, 125 | "gm": { 126 | "version": "1.25.0", 127 | "resolved": "https://registry.npmjs.org/gm/-/gm-1.25.0.tgz", 128 | "integrity": "sha512-4kKdWXTtgQ4biIo7hZA396HT062nDVVHPjQcurNZ3o/voYN+o5FUC5kOwuORbpExp3XbTJ3SU7iRipiIhQtovw==", 129 | "requires": { 130 | "array-parallel": "~0.1.3", 131 | "array-series": "~0.1.5", 132 | "cross-spawn": "^4.0.0", 133 | "debug": "^3.1.0" 134 | } 135 | }, 136 | "isexe": { 137 | "version": "2.0.0", 138 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 139 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 140 | }, 141 | "lru-cache": { 142 | "version": "4.1.5", 143 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", 144 | "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", 145 | "requires": { 146 | "pseudomap": "^1.0.2", 147 | "yallist": "^2.1.2" 148 | } 149 | }, 150 | "ms": { 151 | "version": "2.1.3", 152 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 153 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 154 | }, 155 | "pseudomap": { 156 | "version": "1.0.2", 157 | "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", 158 | "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" 159 | }, 160 | "which": { 161 | "version": "1.3.1", 162 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 163 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 164 | "requires": { 165 | "isexe": "^2.0.0" 166 | } 167 | }, 168 | "yallist": { 169 | "version": "2.1.2", 170 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", 171 | "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lambda-functions/thumbnail/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generate-thumbnail", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "gm": "^1.25.0" 6 | }, 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "Angela Wang", 11 | "license": "Apache-2.0" 12 | } 13 | -------------------------------------------------------------------------------- /lambda-functions/transform-metadata/index.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | 3 | exports.handler = (event, context, callback) => { 4 | console.log("Reading input from event:\n", util.inspect(event, { depth: 5 })); 5 | 6 | var result = {}; 7 | if (event.Properties) { 8 | if (event.Properties["exif:DateTimeOriginal"]) { 9 | result['creationTime'] = event.Properties["exif:DateTimeOriginal"]; 10 | } 11 | 12 | if (event.Properties["exif:GPSLatitude"] && event.Properties["exif:GPSLatitudeRef"] && event.Properties["exif:GPSLongitude"] && event.Properties["exif:GPSLongitudeRef"]) { 13 | try { 14 | const lat = parseCoordinate(event.Properties["exif:GPSLatitude"], event.Properties["exif:GPSLatitudeRef"]); 15 | const long = parseCoordinate(event.Properties["exif:GPSLongitude"], event.Properties["exif:GPSLongitudeRef"]); 16 | console.log("lat", lat); 17 | console.log("long", long); 18 | result.geo = { 19 | 'latitude': lat, "longitude": long 20 | } 21 | } catch (err) { 22 | // ignore failure in parsing coordinates 23 | console.log(err); 24 | } 25 | } 26 | if (event.Properties["exif:Make"]) { 27 | result['exifMake'] = event.Properties["exif:Make"]; 28 | } 29 | if (event.Properties["exif:Model"]) { 30 | result['exifModel'] = event.Properties["exif:Model"]; 31 | } 32 | result['dimensions'] = event["size"]; 33 | result['fileSize'] = event["Filesize"]; 34 | result['format'] = event["format"]; 35 | } 36 | callback(null, result); 37 | } 38 | 39 | /** 40 | * 41 | * @param coordinate in the format of "DDD/number, MM/number, SSSS/number" (e.g. "47/1, 44/1, 3598/100") 42 | * @param coordinateDirection coordinate direction (e.g. "N" "S" "E" "W" 43 | * @returns {{D: number, M: number, S: number, Direction: string}} 44 | */ 45 | function parseCoordinate(coordinate, coordinateDirection) { 46 | 47 | const degreeArray = coordinate.split(",")[0].trim().split("/"); 48 | const minuteArray = coordinate.split(",")[1].trim().split("/"); 49 | const secondArray = coordinate.split(",")[2].trim().split("/"); 50 | 51 | return { 52 | "D": parseInt(degreeArray[0]) / parseInt(degreeArray[1]), 53 | "M": parseInt(minuteArray[0]) / parseInt(minuteArray[1]), 54 | "S": parseInt(secondArray[0]) / parseInt(secondArray[1]), 55 | "Direction": coordinateDirection 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /lambda-functions/transform-metadata/lambda-sample-input.json: -------------------------------------------------------------------------------- 1 | { 2 | "Base filename": "-", 3 | "Format": "JPEG (Joint Photographic Experts Group JFIF format)", 4 | "format": "JPEG", 5 | "Class": "DirectClass", 6 | "Geometry": "4032x3024+0+0", 7 | "size": { 8 | "width": 4032, 9 | "height": 3024 10 | }, 11 | "Resolution": "72x72", 12 | "Print size": "56x42", 13 | "Units": "PixelsPerInch", 14 | "Type": "TrueColor", 15 | "Endianess": "Undefined", 16 | "Colorspace": "sRGB", 17 | "Depth": "8-bit", 18 | "depth": 8, 19 | "Channel depth": { 20 | "red": "8-bit", 21 | "green": "8-bit", 22 | "blue": "8-bit" 23 | }, 24 | "Channel statistics": { 25 | "Red": { 26 | "min": "0 (0)", 27 | "max": "239 (0.937255)", 28 | "mean": "92.2995 (0.361959)", 29 | "standard deviation": "56.6032 (0.221973)", 30 | "kurtosis": "-0.342865", 31 | "skewness": "0.833885" 32 | }, 33 | "Green": { 34 | "min": "0 (0)", 35 | "max": "248 (0.972549)", 36 | "mean": "121.324 (0.475779)", 37 | "standard deviation": "51.1459 (0.200572)", 38 | "kurtosis": "-0.401377", 39 | "skewness": "0.437793" 40 | }, 41 | "Blue": { 42 | "min": "0 (0)", 43 | "max": "255 (1)", 44 | "mean": "160.431 (0.629141)", 45 | "standard deviation": "44.2143 (0.173389)", 46 | "kurtosis": "0.123975", 47 | "skewness": "-0.413949" 48 | } 49 | }, 50 | "Image statistics": { 51 | "Overall": { 52 | "min": "0 (0)", 53 | "max": "255 (1)", 54 | "mean": "124.685 (0.48896)", 55 | "standard deviation": "50.9075 (0.199637)", 56 | "kurtosis": "0.452466", 57 | "skewness": "0.145731" 58 | } 59 | }, 60 | "Rendering intent": "Perceptual", 61 | "Gamma": "0.454545", 62 | "Chromaticity": { 63 | "red primary": "(0.64,0.33)", 64 | "green primary": "(0.3,0.6)", 65 | "blue primary": "(0.15,0.06)", 66 | "white point": "(0.3127,0.329)" 67 | }, 68 | "Interlace": "None", 69 | "Background color": "white", 70 | "Border color": "srgb(223,223,223)", 71 | "Matte color": "grey74", 72 | "Transparent color": "black", 73 | "Compose": "Over", 74 | "Page geometry": "4032x3024+0+0", 75 | "Dispose": "Undefined", 76 | "Iterations": "0", 77 | "Compression": "JPEG", 78 | "Quality": "94", 79 | "Orientation": "Undefined", 80 | "Properties": { 81 | "date:create": "2017-06-06T00:50:16+00:00", 82 | "date:modify": "2017-06-06T00:50:16+00:00", 83 | "exif:ApertureValue": "200/100", 84 | "exif:BrightnessValue": "0/100", 85 | "exif:ColorSpace": "1", 86 | "exif:ComponentsConfiguration": "1, 2, 3, 0", 87 | "exif:Compression": "6", 88 | "exif:DateTime": "2016:04:09 14:08:13", 89 | "exif:DateTimeDigitized": "2016:04:09 14:08:13", 90 | "exif:DateTimeOriginal": "2016:04:09 14:08:13", 91 | "exif:ExifImageLength": "3024", 92 | "exif:ExifImageWidth": "4032", 93 | "exif:ExifOffset": "228", 94 | "exif:ExifVersion": "48, 50, 50, 48", 95 | "exif:ExposureBiasValue": "0/1", 96 | "exif:ExposureMode": "0", 97 | "exif:ExposureProgram": "0", 98 | "exif:ExposureTime": "1/6568", 99 | "exif:Flash": "16", 100 | "exif:FlashPixVersion": "48, 49, 48, 48", 101 | "exif:FNumber": "200/100", 102 | "exif:FocalLength": "4670/1000", 103 | "exif:FocalLengthIn35mmFilm": "0", 104 | "exif:GPSAltitude": "272000/100", 105 | "exif:GPSAltitudeRef": "0", 106 | "exif:GPSDateStamp": "2016:04:09", 107 | "exif:GPSInfo": "744", 108 | "exif:GPSLatitude": "46/1, 49/1, 3243/100", 109 | "exif:GPSLatitudeRef": "N", 110 | "exif:GPSLongitude": "121/1, 43/1, 4188/100", 111 | "exif:GPSLongitudeRef": "W", 112 | "exif:GPSTimeStamp": "21/1, 8/1, 8/1", 113 | "exif:GPSVersionID": "2, 2, 0, 0", 114 | "exif:ImageUniqueID": "15f1a7fd7b5a23fd0000000000000000", 115 | "exif:InteroperabilityIndex": "R98", 116 | "exif:InteroperabilityOffset": "950", 117 | "exif:InteroperabilityVersion": "48, 49, 48, 48", 118 | "exif:ISOSpeedRatings": "63", 119 | "exif:JPEGInterchangeFormat": "1074", 120 | "exif:JPEGInterchangeFormatLength": "5366", 121 | "exif:Make": "LGE", 122 | "exif:MeteringMode": "0", 123 | "exif:Model": "Nexus 5X", 124 | "exif:ResolutionUnit": "2", 125 | "exif:SceneCaptureType": "0", 126 | "exif:SceneType": "0", 127 | "exif:SensingMethod": "0", 128 | "exif:ShutterSpeedValue": "12681/1000", 129 | "exif:Software": "bullhead-user 6.0.1 MHC19J 2595691 release-keys", 130 | "exif:SubSecTime": "525867", 131 | "exif:SubSecTimeDigitized": "525867", 132 | "exif:SubSecTimeOriginal": "525867", 133 | "exif:WhiteBalance": "0", 134 | "exif:XResolution": "72/1", 135 | "exif:YCbCrPositioning": "1", 136 | "exif:YResolution": "72/1", 137 | "jpeg:colorspace": "2", 138 | "jpeg:sampling-factor": "2x2,1x1,1x1", 139 | "signature": "6872beb50662324bac90a33163c5790d3f0c83d3f3fc7d28630406da55f17d2d" 140 | }, 141 | "Profiles": { 142 | "Profile-exif": "6446 bytes", 143 | "Profile-icc": { 144 | "Description": "sRGB IEC61966-2-1 black scaled", 145 | "Manufacturer": "sRGB IEC61966-2-1 black scaled", 146 | "Model": "IEC 61966-2-1 Default RGB Colour Space - sRGB", 147 | "Copyright": "Copyright International Color Consortium, 2009" 148 | }, 149 | "Profile-xmp": "259 bytes" 150 | }, 151 | "Artifacts": { 152 | "filename": "-", 153 | "verbose": "true" 154 | }, 155 | "Tainted": "False", 156 | "Filesize": "1.354MB", 157 | "Number pixels": "12.19M", 158 | "Pixels per second": "55.42MB", 159 | "User time": "0.130u", 160 | "Elapsed time": "0:01.219", 161 | "Version": "ImageMagick 6.7.8-9 2016-06-22 Q16 http://www.imagemagick.org", 162 | "path": "unknown.jpg" 163 | } -------------------------------------------------------------------------------- /lambda-functions/transform-metadata/lambda-sample-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "creationTime": "2016:04:09 14:08:13", 3 | "geo": { 4 | "latitude": { 5 | "D": 46, 6 | "M": 49, 7 | "S": 32.43, 8 | "Direction": "N" 9 | }, 10 | "longitude": { 11 | "D": 121, 12 | "M": 43, 13 | "S": 41.88, 14 | "Direction": "W" 15 | } 16 | }, 17 | "exifMake": "LGE", 18 | "exifModel": "Nexus 5X", 19 | "dimensions": { 20 | "width": 4032, 21 | "height": 3024 22 | }, 23 | "fileSize": "1.354MB", 24 | "format": "JPEG" 25 | } -------------------------------------------------------------------------------- /lambda-functions/transform-metadata/local-testing.sh: -------------------------------------------------------------------------------- 1 | # use lambda-local (https://www.npmjs.com/package/lambda-local) to test lambda functions locally 2 | lambda-local -l index.js -h handler -e lambda-sample-input.json -t 60 -------------------------------------------------------------------------------- /lambda-functions/transform-metadata/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transform-metadata", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "version": "1.0.0", 9 | "license": "Apache-2.0" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lambda-functions/transform-metadata/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transform-metadata", 3 | "version": "1.0.0", 4 | "description": "formats and filters the detected metadata JSON for easier processing in future steps", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Angela Wang", 10 | "license": "Apache-2.0" 11 | } 12 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda-refarch-imagerecognition", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /src/react-frontend/.eslintcache: -------------------------------------------------------------------------------- 1 | [{"/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/index.js":"1","/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/App.js":"2","/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/serviceWorker.js":"3","/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/components/Album.js":"4","/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/components/AlbumDetail.js":"5","/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/aws-exports.js":"6","/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/utils.js":"7","/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/components/PhotoList.js":"8","/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/graphql/queries.js":"9","/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/graphql/mutations.js":"10","/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/graphql/subscriptions.js":"11"},{"size":503,"mtime":1610375871906,"results":"12","hashOfConfig":"13"},{"size":1870,"mtime":1610375871901,"results":"14","hashOfConfig":"13"},{"size":5086,"mtime":1610375871906,"results":"15","hashOfConfig":"13"},{"size":2261,"mtime":1610375871902,"results":"16","hashOfConfig":"13"},{"size":4105,"mtime":1610375871902,"results":"17","hashOfConfig":"13"},{"size":833,"mtime":1610567705896,"results":"18","hashOfConfig":"13"},{"size":541,"mtime":1610375871907,"results":"19","hashOfConfig":"13"},{"size":6746,"mtime":1610375871902,"results":"20","hashOfConfig":"13"},{"size":3734,"mtime":1610567696205,"results":"21","hashOfConfig":"13"},{"size":4779,"mtime":1610567696276,"results":"22","hashOfConfig":"13"},{"size":4141,"mtime":1610567696336,"results":"23","hashOfConfig":"13"},{"filePath":"24","messages":"25","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"9ajg2s",{"filePath":"26","messages":"27","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"28","messages":"29","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"30","messages":"31","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"32","messages":"33","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"34","messages":"35","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"36","messages":"37","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"38","messages":"39","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"40","messages":"41","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"42","messages":"43","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"44","messages":"45","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/index.js",[],"/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/App.js",[],"/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/serviceWorker.js",[],"/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/components/Album.js",[],"/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/components/AlbumDetail.js",[],"/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/aws-exports.js",[],"/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/utils.js",[],"/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/components/PhotoList.js",[],"/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/graphql/queries.js",[],"/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/graphql/mutations.js",[],"/Users/ibakouch/Dev/lambda-refarch-imagerecognition/src/react-frontend/src/graphql/subscriptions.js",[]] -------------------------------------------------------------------------------- /src/react-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | -------------------------------------------------------------------------------- /src/react-frontend/.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | photoshare: 3 | schemaPath: src/graphql/schema.json 4 | includes: 5 | - src/graphql/**/*.js 6 | excludes: 7 | - ./amplify/** 8 | extensions: 9 | amplify: 10 | codeGenTarget: javascript 11 | generatedFileName: '' 12 | docsFilePath: src/graphql 13 | extensions: 14 | amplify: 15 | version: 3 16 | -------------------------------------------------------------------------------- /src/react-frontend/README.md: -------------------------------------------------------------------------------- 1 | This project was built on React and on top of https://github.com/aws-samples/amplify-photo-gallery-workshop 2 | 3 | 4 | ## Running locally 5 | 6 | You can run the front-end locally while targeting the back-end and auth deployed in your AWS account. 7 | 8 | If you followed [the deployment instructions](../../README.md), you should have `src/react-frontend/aws-exports.js` file. 9 | 10 | ### I don't have aws-exports, or deleted accidentally 11 | 12 | 1. Open up your deployed App in Amplify Console by running `amplify console` 13 | 2. At the bottom of the page under **Edit your backend**, copy and run the `amplify pull` command 14 | - e.g. `amplify pull --appId d34s789vnlqyw4 --envName master` 15 | 16 | > NOTE: **Aws-exports** is a configuration file for AWS Amplify library containing Cognito User Pools, AppSync API, and what authentication mechanism it should use along with its region. 17 | 18 | Once you're all set, install front-end dependencies and run a local copy: 19 | 20 | 1. `npm install` 21 | 2. `npm start` 22 | 23 | ### `npm start` 24 | 25 | Runs the app in the development mode.
26 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 27 | 28 | The page will reload if you make edits.
29 | You will also see any lint errors in the console. 30 | 31 | -------------------------------------------------------------------------------- /src/react-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "photo-sharing-webapp-react", 3 | "version": "0.1.1", 4 | "private": true, 5 | "dependencies": { 6 | "@aws-amplify/api": "^3.3.3", 7 | "@aws-amplify/storage": "^3.4.4", 8 | "@aws-amplify/ui-react": "^4.3.3", 9 | "@testing-library/jest-dom": "^5.16.5", 10 | "@testing-library/react": "^13.4.0", 11 | "@testing-library/user-event": "^14.4.3", 12 | "aws-amplify": "^5.0.9", 13 | "aws-amplify-react": "^5.1.9", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-router-dom": "^6.3.0", 17 | "react-scripts": "^5.0.1", 18 | "semantic-ui-react": "^2.1.3", 19 | "uuid": "^9.0.0" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/react-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/src/react-frontend/public/favicon.ico -------------------------------------------------------------------------------- /src/react-frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 19 | 20 | 29 | React App 30 | 31 | 32 | 33 |
34 | 44 | 45 | -------------------------------------------------------------------------------- /src/react-frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/src/react-frontend/public/logo192.png -------------------------------------------------------------------------------- /src/react-frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/lambda-refarch-imagerecognition/856a02a2c036a2e9f8222da10b1d79c45d5c5ed4/src/react-frontend/public/logo512.png -------------------------------------------------------------------------------- /src/react-frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/react-frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/react-frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/react-frontend/src/App.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Amazon Web Services, Inc. or its affiliates. All Rights Reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at 4 | 5 | // http://aws.amazon.com/apache2.0/ 6 | 7 | // or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 8 | 9 | import React from 'react'; 10 | import Amplify, { Auth } from 'aws-amplify'; 11 | import aws_exports from './aws-exports'; 12 | 13 | import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react'; 14 | import { Grid, Header, Menu } from 'semantic-ui-react' 15 | 16 | import '@aws-amplify/ui/dist/style.css'; 17 | 18 | import { BrowserRouter as Router, NavLink, Route } from 'react-router-dom'; 19 | 20 | import { AlbumList, NewAlbum } from './components/Album' 21 | import { AlbumDetails } from "./components/AlbumDetail"; 22 | 23 | Amplify.configure(aws_exports); 24 | 25 | function App() { 26 | return ( 27 | 28 | 29 | 30 | 32 |
Albums
33 |
34 | 35 | 36 | 37 | 38 | 39 |
40 | 41 | 42 | 43 | 44 | 45 | !Auth.currentAuthenticatedUser() ? null : } /> 46 | } /> 49 | 50 | 51 | 52 |
53 | 54 | ); 55 | } 56 | 57 | export default withAuthenticator(App); 58 | -------------------------------------------------------------------------------- /src/react-frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/react-frontend/src/components/Album.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import API, { graphqlOperation } from '@aws-amplify/api' 3 | import { Auth } from 'aws-amplify'; 4 | 5 | import { Header, Input, List, Segment } from 'semantic-ui-react' 6 | import { NavLink } from 'react-router-dom'; 7 | 8 | import * as queries from '../graphql/queries' 9 | import * as mutations from '../graphql/mutations' 10 | import * as subscriptions from '../graphql/subscriptions' 11 | 12 | import { makeComparator } from "../utils"; 13 | 14 | export const NewAlbum = () => { 15 | const [name, setName] = useState('') 16 | const handleSubmit = async (event) => { 17 | event.preventDefault(); 18 | await API.graphql(graphqlOperation(mutations.createAlbum, { input: { name } })) 19 | setName('') 20 | } 21 | return ( 22 | 23 |
Add a new album
24 | setName(e.target.value)} /> 32 |
33 | ) 34 | } 35 | 36 | export const AlbumList = () => { 37 | const [albums, setAlbums] = useState([]) 38 | useEffect(() => { 39 | async function fetchData() { 40 | const result = await API.graphql(graphqlOperation(queries.listAlbums, { limit: 999 })) 41 | setAlbums(result.data.listAlbums.items) 42 | } 43 | 44 | fetchData() 45 | }, []) 46 | 47 | useEffect(() => { 48 | let subscription 49 | 50 | async function setupSubscription() { 51 | const user = await Auth.currentAuthenticatedUser() 52 | subscription = API.graphql(graphqlOperation(subscriptions.onCreateAlbum, { owner: user.username })).subscribe({ 53 | next: (data) => { 54 | const album = data.value.data.onCreateAlbum 55 | setAlbums(a => a.concat([album].sort(makeComparator('name')))) 56 | } 57 | }) 58 | } 59 | 60 | setupSubscription() 61 | 62 | return () => subscription.unsubscribe(); 63 | }, []) 64 | 65 | const albumItems = () => { 66 | return albums 67 | .sort(makeComparator('name')) 68 | .map(album => 69 | {album.name} 70 | ); 71 | } 72 | 73 | return ( 74 |
My Albums
75 | 76 | {albumItems()} 77 | 78 |
79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /src/react-frontend/src/components/AlbumDetail.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import API, { graphqlOperation } from '@aws-amplify/api' 3 | 4 | import { Header, Form, Segment } from 'semantic-ui-react' 5 | 6 | import * as queries from '../graphql/queries' 7 | import * as subscriptions from '../graphql/subscriptions' 8 | import { Auth } from "aws-amplify"; 9 | import { PhotoList, S3ImageUpload } from "./PhotoList"; 10 | 11 | export const AlbumDetails = (props) => { 12 | const [album, setAlbum] = useState({ name: 'Loading...', photos: [] }) 13 | const [photos, setPhotos] = useState([]) 14 | const [hasMorePhotos, setHasMorePhotos] = useState(true) 15 | const [fetchingPhotos, setFetchingPhotos] = useState(false) 16 | const [nextPhotosToken, setNextPhotosToken] = useState(null) 17 | const [processingStatuses, setProcessingStatuses] = useState({}) 18 | 19 | useEffect(() => { 20 | const loadAlbumInfo = async () => { 21 | const results = await API.graphql(graphqlOperation(queries.getAlbum, { id: props.id })) 22 | setAlbum(results.data.getAlbum) 23 | } 24 | 25 | loadAlbumInfo() 26 | }, [props.id]) 27 | 28 | useEffect(() => { 29 | fetchNextPhotos() 30 | // eslint-disable-next-line react-hooks/exhaustive-deps 31 | }, []) 32 | 33 | useEffect(() => { 34 | let subscription 35 | 36 | async function setupSubscription() { 37 | const user = await Auth.currentAuthenticatedUser() 38 | subscription = API.graphql(graphqlOperation(subscriptions.onCreatePhoto, 39 | { owner: user.username })).subscribe({ 40 | next: (data) => { 41 | const photo = data.value.data.onCreatePhoto 42 | if (photo.albumId !== props.id) return 43 | setPhotos(p => p.concat([photo])) 44 | 45 | setProcessingStatuses((prevState => { 46 | prevState[photo.id] = { 47 | 'status': photo.ProcessingStatus, 48 | 'sfnArn': photo.SfnExecutionArn 49 | } 50 | return prevState 51 | })) 52 | 53 | } 54 | }) 55 | } 56 | 57 | setupSubscription(); 58 | return () => subscription.unsubscribe() 59 | }, [props.id]) 60 | 61 | useEffect(() => { 62 | let subscription 63 | 64 | async function setupSubscription() { 65 | const user = await Auth.currentAuthenticatedUser() 66 | subscription = API.graphql(graphqlOperation(subscriptions.onUpdatePhoto, 67 | { owner: user.username })).subscribe({ 68 | next: (data) => { 69 | const photo = data.value.data.onUpdatePhoto 70 | if (photo.albumId !== props.id) return 71 | setPhotos(p => { 72 | let newPhotos = p.slice() 73 | for (let i in newPhotos) { 74 | if (newPhotos[i].id === photo.id) { 75 | newPhotos[i] = photo 76 | } 77 | } 78 | setProcessingStatuses((prevState => { 79 | prevState[photo.id] = { 80 | 'status': photo.ProcessingStatus, 81 | 'sfnArn': photo.SfnExecutionArn 82 | } 83 | return prevState 84 | })) 85 | return newPhotos 86 | }) 87 | } 88 | }) 89 | } 90 | 91 | setupSubscription(); 92 | return () => subscription.unsubscribe() 93 | }, [props.id]) 94 | 95 | const fetchNextPhotos = async () => { 96 | const FETCH_LIMIT = 20 97 | setFetchingPhotos(true) 98 | let queryArgs = { 99 | albumId: props.id, 100 | limit: FETCH_LIMIT, 101 | nextToken: nextPhotosToken 102 | } 103 | if (!queryArgs.nextToken) delete queryArgs.nextToken 104 | const results = await API.graphql(graphqlOperation(queries.listPhotosByAlbumUploadTime, queryArgs)) 105 | setPhotos(p => p.concat(results.data.listPhotosByAlbumUploadTime.items)) 106 | setNextPhotosToken(results.data.listPhotosByAlbumUploadTime.nextToken) 107 | setHasMorePhotos(results.data.listPhotosByAlbumUploadTime.items.length === FETCH_LIMIT) 108 | setFetchingPhotos(false) 109 | } 110 | 111 | return ( 112 | 113 |
{album.name}
114 | setProcessingStatuses({})} 116 | processingStatuses={processingStatuses} /> 117 | 118 | { 119 | hasMorePhotos && 120 | fetchNextPhotos()} 122 | icon='refresh' 123 | disabled={fetchingPhotos} 124 | content={fetchingPhotos ? 'Loading...' : 'Load more photos'} 125 | /> 126 | } 127 |
128 | ) 129 | } 130 | -------------------------------------------------------------------------------- /src/react-frontend/src/components/PhotoList.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {S3Image} from 'aws-amplify-react' 3 | 4 | import {Card, Label, Divider, Form, Dimmer, Loader, Message} from 'semantic-ui-react' 5 | 6 | import {v4 as uuid} from 'uuid'; 7 | import * as mutations from '../graphql/mutations' 8 | import AWSConfig from '../aws-exports' 9 | import {Auth} from "aws-amplify"; 10 | import Storage from '@aws-amplify/storage' 11 | import API, {graphqlOperation} from "@aws-amplify/api"; 12 | 13 | export const S3ImageUpload = (props) => { 14 | const [uploading, setUploading] = useState(false) 15 | const [statuses, setStatuses] = useState({}) 16 | 17 | const ProcessingStatus = (props) => { 18 | setStatuses(prevState => { 19 | for (let id in props.processingStatuses) { 20 | if (id in prevState) { 21 | prevState[id]['status'] = props.processingStatuses[id]['status'] 22 | prevState[id]['sfnArn'] = props.processingStatuses[id]['sfnArn'] 23 | } 24 | } 25 | return prevState 26 | }); 27 | 28 | let sfnExecutionLink = ((sfnArn) => { 29 | if (sfnArn) { 30 | let arnComponets = sfnArn.split(":") 31 | let awsRegion = arnComponets[3] 32 | let executionId = arnComponets[arnComponets.length-1] 33 | let consoleUrl = `https://console.aws.amazon.com/states/home?region=${awsRegion}#/executions/details/${sfnArn}` 34 | return ( 35 | Step Function execution: {executionId} 36 | ) 37 | } else { 38 | return null 39 | } 40 | }) 41 | return (Object.keys(statuses).map((fileId) => { 42 | let status = statuses[fileId] 43 | return ( 44 | {/*{filename}*/} 45 |

46 | {status.filename} 47 | {(status.status === 'PENDING' && ' uploading to S3.')} 48 | {((status.status === 'UPLOADED' || status.status === 'RUNNING') && ' uploaded. Processing... ')} 49 | {(status.status === 'SUCCEEDED' && ' successfully processed. ')} 50 | {sfnExecutionLink(status.sfnArn)} 51 |

52 | 53 |
54 | ) 55 | } 56 | )) 57 | } 58 | 59 | const uploadFile = async (file) => { 60 | const imageId = uuid(); 61 | const fileName = 'uploads/' + imageId + '.' + file.name.split('.').pop(); 62 | const user = await Auth.currentAuthenticatedUser(); 63 | 64 | let createPhotoArg = { 65 | id: imageId, 66 | albumId: props.albumId, 67 | owner: user.username, 68 | uploadTime: new Date(), 69 | bucket: AWSConfig.aws_user_files_s3_bucket, 70 | ProcessingStatus: 'PENDING' 71 | } 72 | 73 | await API.graphql(graphqlOperation(mutations.createPhoto, {"input": createPhotoArg})) 74 | 75 | setStatuses(prevStatus => { 76 | prevStatus[imageId] = { 77 | 'filename': file.name, 78 | 'status': 'PENDING' 79 | } 80 | return prevStatus 81 | }) 82 | 83 | try { 84 | const result = await Storage.vault.put( 85 | fileName, 86 | file, 87 | { 88 | metadata: { 89 | // Discovered that Amplify has bug that doesn't persist this metadata when multipart upload is used 90 | // https://github.com/aws-amplify/amplify-js/issues/5432 91 | albumid: props.albumId, 92 | owner: user.username, 93 | } 94 | } 95 | ); 96 | 97 | setStatuses(prevStatus => { 98 | prevStatus[imageId]['status'] = 'UPLOADED'; 99 | return prevStatus 100 | }) 101 | 102 | console.log(`Uploaded ${file.name} to ${fileName}: `, result); 103 | } catch (e) { 104 | console.log('Failed to upload to s3.') 105 | } 106 | } 107 | 108 | const onFileSelectionChange = async (e) => { 109 | setUploading(true) 110 | 111 | let files = []; 112 | for (let i = 0; i < e.target.files.length; i++) { 113 | files.push(e.target.files.item(i)); 114 | } 115 | setStatuses({}) 116 | props.clearStatus() 117 | await Promise.all(files.map(f => uploadFile(f))); 118 | 119 | setUploading(false) 120 | } 121 | 122 | return ( 123 |
124 | document.getElementById('add-image-file-input').click()} 126 | disabled={uploading} 127 | icon='file image outline' 128 | content={uploading ? 'Uploading...' : 'Add Images'} 129 | /> 130 | 138 | 139 |
140 | ); 141 | } 142 | 143 | export const PhotoList = React.memo(props => { 144 | 145 | const PhotoItems = (props) => { 146 | const photoItem = (photo) => { 147 | if (photo.ProcessingStatus === "SUCCEEDED") { 148 | const DetectedLabels = () => { 149 | if (photo.objectDetected) { 150 | return photo.objectDetected.map((tag) => ( 151 | 154 | )) 155 | } else { 156 | return null; 157 | } 158 | } 159 | const GeoLocation = () => { 160 | if (photo.geoLocation) { 161 | const geo = photo.geoLocation 162 | return ( 163 |

Geolocation:  164 | {geo.Latitude.D}°{Math.round(geo.Latitude.M)}'{Math.round(geo.Latitude.S)}"{geo.Latitude.Direction}   165 | {geo.Longtitude.D}°{Math.round(geo.Longtitude.M)}'{Math.round(geo.Longtitude.S)}"{geo.Longtitude.Direction} 166 |

167 | ) 168 | } else { 169 | return null 170 | } 171 | } 172 | 173 | return ( 174 | 175 | 176 | 182 | 183 | 184 | 185 | Uploaded: {new Date(photo.uploadTime).toLocaleString()} 186 | 187 | 188 |

Detected labels:

189 | 190 |

Image size: {photo.fullsize.width} x {photo.fullsize.height}

191 | 192 | {(photo.exifMake || photo.exitModel) && 193 |

Device: {photo.exifMake} {photo.exitModel}

} 194 |
195 |
196 |
197 | ) 198 | } else if (photo.ProcessingStatus === "RUNNING" || photo.ProcessingStatus === "PENDING") { 199 | return ( 200 | 201 | Processing 202 | 203 | ) 204 | } 205 | } 206 | 207 | return ( {props.photos.map(photoItem)}); 208 | }; 209 | 210 | return ( 211 |
212 |
) 215 | }) 216 | -------------------------------------------------------------------------------- /src/react-frontend/src/graphql/mutations.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // this is an auto generated file. This will be overwritten 3 | 4 | export const startSfnExecution = /* GraphQL */ ` 5 | mutation StartSfnExecution($input: StartSfnExecutionInput!) { 6 | startSfnExecution(input: $input) { 7 | executionArn 8 | startDate 9 | } 10 | } 11 | `; 12 | export const createAlbum = /* GraphQL */ ` 13 | mutation CreateAlbum( 14 | $input: CreateAlbumInput! 15 | $condition: ModelAlbumConditionInput 16 | ) { 17 | createAlbum(input: $input, condition: $condition) { 18 | id 19 | name 20 | owner 21 | createdAt 22 | updatedAt 23 | photos { 24 | items { 25 | id 26 | albumId 27 | owner 28 | uploadTime 29 | bucket 30 | format 31 | exifMake 32 | exitModel 33 | SfnExecutionArn 34 | ProcessingStatus 35 | objectDetected 36 | createdAt 37 | updatedAt 38 | } 39 | nextToken 40 | } 41 | } 42 | } 43 | `; 44 | export const updateAlbum = /* GraphQL */ ` 45 | mutation UpdateAlbum( 46 | $input: UpdateAlbumInput! 47 | $condition: ModelAlbumConditionInput 48 | ) { 49 | updateAlbum(input: $input, condition: $condition) { 50 | id 51 | name 52 | owner 53 | createdAt 54 | updatedAt 55 | photos { 56 | items { 57 | id 58 | albumId 59 | owner 60 | uploadTime 61 | bucket 62 | format 63 | exifMake 64 | exitModel 65 | SfnExecutionArn 66 | ProcessingStatus 67 | objectDetected 68 | createdAt 69 | updatedAt 70 | } 71 | nextToken 72 | } 73 | } 74 | } 75 | `; 76 | export const deleteAlbum = /* GraphQL */ ` 77 | mutation DeleteAlbum( 78 | $input: DeleteAlbumInput! 79 | $condition: ModelAlbumConditionInput 80 | ) { 81 | deleteAlbum(input: $input, condition: $condition) { 82 | id 83 | name 84 | owner 85 | createdAt 86 | updatedAt 87 | photos { 88 | items { 89 | id 90 | albumId 91 | owner 92 | uploadTime 93 | bucket 94 | format 95 | exifMake 96 | exitModel 97 | SfnExecutionArn 98 | ProcessingStatus 99 | objectDetected 100 | createdAt 101 | updatedAt 102 | } 103 | nextToken 104 | } 105 | } 106 | } 107 | `; 108 | export const createPhoto = /* GraphQL */ ` 109 | mutation CreatePhoto( 110 | $input: CreatePhotoInput! 111 | $condition: ModelPhotoConditionInput 112 | ) { 113 | createPhoto(input: $input, condition: $condition) { 114 | id 115 | albumId 116 | owner 117 | uploadTime 118 | bucket 119 | fullsize { 120 | key 121 | width 122 | height 123 | } 124 | thumbnail { 125 | key 126 | width 127 | height 128 | } 129 | format 130 | exifMake 131 | exitModel 132 | SfnExecutionArn 133 | ProcessingStatus 134 | objectDetected 135 | geoLocation { 136 | Latitude { 137 | D 138 | M 139 | S 140 | Direction 141 | } 142 | Longtitude { 143 | D 144 | M 145 | S 146 | Direction 147 | } 148 | } 149 | createdAt 150 | updatedAt 151 | album { 152 | id 153 | name 154 | owner 155 | createdAt 156 | updatedAt 157 | photos { 158 | nextToken 159 | } 160 | } 161 | } 162 | } 163 | `; 164 | export const updatePhoto = /* GraphQL */ ` 165 | mutation UpdatePhoto( 166 | $input: UpdatePhotoInput! 167 | $condition: ModelPhotoConditionInput 168 | ) { 169 | updatePhoto(input: $input, condition: $condition) { 170 | id 171 | albumId 172 | owner 173 | uploadTime 174 | bucket 175 | fullsize { 176 | key 177 | width 178 | height 179 | } 180 | thumbnail { 181 | key 182 | width 183 | height 184 | } 185 | format 186 | exifMake 187 | exitModel 188 | SfnExecutionArn 189 | ProcessingStatus 190 | objectDetected 191 | geoLocation { 192 | Latitude { 193 | D 194 | M 195 | S 196 | Direction 197 | } 198 | Longtitude { 199 | D 200 | M 201 | S 202 | Direction 203 | } 204 | } 205 | createdAt 206 | updatedAt 207 | album { 208 | id 209 | name 210 | owner 211 | createdAt 212 | updatedAt 213 | photos { 214 | nextToken 215 | } 216 | } 217 | } 218 | } 219 | `; 220 | export const deletePhoto = /* GraphQL */ ` 221 | mutation DeletePhoto( 222 | $input: DeletePhotoInput! 223 | $condition: ModelPhotoConditionInput 224 | ) { 225 | deletePhoto(input: $input, condition: $condition) { 226 | id 227 | albumId 228 | owner 229 | uploadTime 230 | bucket 231 | fullsize { 232 | key 233 | width 234 | height 235 | } 236 | thumbnail { 237 | key 238 | width 239 | height 240 | } 241 | format 242 | exifMake 243 | exitModel 244 | SfnExecutionArn 245 | ProcessingStatus 246 | objectDetected 247 | geoLocation { 248 | Latitude { 249 | D 250 | M 251 | S 252 | Direction 253 | } 254 | Longtitude { 255 | D 256 | M 257 | S 258 | Direction 259 | } 260 | } 261 | createdAt 262 | updatedAt 263 | album { 264 | id 265 | name 266 | owner 267 | createdAt 268 | updatedAt 269 | photos { 270 | nextToken 271 | } 272 | } 273 | } 274 | } 275 | `; 276 | -------------------------------------------------------------------------------- /src/react-frontend/src/graphql/queries.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // this is an auto generated file. This will be overwritten 3 | 4 | export const checkSfnStatus = /* GraphQL */ ` 5 | query CheckSfnStatus($input: checkSfnStatusInput!) { 6 | checkSfnStatus(input: $input) { 7 | startDate 8 | stopDate 9 | status 10 | } 11 | } 12 | `; 13 | export const listAlbums = /* GraphQL */ ` 14 | query ListAlbums( 15 | $filter: ModelAlbumFilterInput 16 | $limit: Int 17 | $nextToken: String 18 | ) { 19 | listAlbums(filter: $filter, limit: $limit, nextToken: $nextToken) { 20 | items { 21 | id 22 | name 23 | owner 24 | createdAt 25 | updatedAt 26 | photos { 27 | nextToken 28 | } 29 | } 30 | nextToken 31 | } 32 | } 33 | `; 34 | export const getAlbum = /* GraphQL */ ` 35 | query GetAlbum($id: ID!) { 36 | getAlbum(id: $id) { 37 | id 38 | name 39 | owner 40 | createdAt 41 | updatedAt 42 | photos { 43 | items { 44 | id 45 | albumId 46 | owner 47 | uploadTime 48 | bucket 49 | format 50 | exifMake 51 | exitModel 52 | SfnExecutionArn 53 | ProcessingStatus 54 | objectDetected 55 | createdAt 56 | updatedAt 57 | } 58 | nextToken 59 | } 60 | } 61 | } 62 | `; 63 | export const getPhoto = /* GraphQL */ ` 64 | query GetPhoto($id: ID!) { 65 | getPhoto(id: $id) { 66 | id 67 | albumId 68 | owner 69 | uploadTime 70 | bucket 71 | fullsize { 72 | key 73 | width 74 | height 75 | } 76 | thumbnail { 77 | key 78 | width 79 | height 80 | } 81 | format 82 | exifMake 83 | exitModel 84 | SfnExecutionArn 85 | ProcessingStatus 86 | objectDetected 87 | geoLocation { 88 | Latitude { 89 | D 90 | M 91 | S 92 | Direction 93 | } 94 | Longtitude { 95 | D 96 | M 97 | S 98 | Direction 99 | } 100 | } 101 | createdAt 102 | updatedAt 103 | album { 104 | id 105 | name 106 | owner 107 | createdAt 108 | updatedAt 109 | photos { 110 | nextToken 111 | } 112 | } 113 | } 114 | } 115 | `; 116 | export const listPhotos = /* GraphQL */ ` 117 | query ListPhotos( 118 | $filter: ModelPhotoFilterInput 119 | $limit: Int 120 | $nextToken: String 121 | ) { 122 | listPhotos(filter: $filter, limit: $limit, nextToken: $nextToken) { 123 | items { 124 | id 125 | albumId 126 | owner 127 | uploadTime 128 | bucket 129 | fullsize { 130 | key 131 | width 132 | height 133 | } 134 | thumbnail { 135 | key 136 | width 137 | height 138 | } 139 | format 140 | exifMake 141 | exitModel 142 | SfnExecutionArn 143 | ProcessingStatus 144 | objectDetected 145 | createdAt 146 | updatedAt 147 | album { 148 | id 149 | name 150 | owner 151 | createdAt 152 | updatedAt 153 | } 154 | } 155 | nextToken 156 | } 157 | } 158 | `; 159 | export const listPhotosByAlbumUploadTime = /* GraphQL */ ` 160 | query ListPhotosByAlbumUploadTime( 161 | $albumId: ID 162 | $uploadTime: ModelStringKeyConditionInput 163 | $sortDirection: ModelSortDirection 164 | $filter: ModelPhotoFilterInput 165 | $limit: Int 166 | $nextToken: String 167 | ) { 168 | listPhotosByAlbumUploadTime( 169 | albumId: $albumId 170 | uploadTime: $uploadTime 171 | sortDirection: $sortDirection 172 | filter: $filter 173 | limit: $limit 174 | nextToken: $nextToken 175 | ) { 176 | items { 177 | id 178 | albumId 179 | owner 180 | uploadTime 181 | bucket 182 | fullsize { 183 | key 184 | width 185 | height 186 | } 187 | thumbnail { 188 | key 189 | width 190 | height 191 | } 192 | format 193 | exifMake 194 | exitModel 195 | SfnExecutionArn 196 | ProcessingStatus 197 | objectDetected 198 | createdAt 199 | updatedAt 200 | album { 201 | id 202 | name 203 | owner 204 | createdAt 205 | updatedAt 206 | } 207 | } 208 | nextToken 209 | } 210 | } 211 | `; 212 | -------------------------------------------------------------------------------- /src/react-frontend/src/graphql/subscriptions.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // this is an auto generated file. This will be overwritten 3 | 4 | export const onCreateAlbum = /* GraphQL */ ` 5 | subscription OnCreateAlbum($owner: String) { 6 | onCreateAlbum(owner: $owner) { 7 | id 8 | name 9 | owner 10 | createdAt 11 | updatedAt 12 | photos { 13 | items { 14 | id 15 | albumId 16 | owner 17 | uploadTime 18 | bucket 19 | format 20 | exifMake 21 | exitModel 22 | SfnExecutionArn 23 | ProcessingStatus 24 | objectDetected 25 | createdAt 26 | updatedAt 27 | } 28 | nextToken 29 | } 30 | } 31 | } 32 | `; 33 | export const onUpdateAlbum = /* GraphQL */ ` 34 | subscription OnUpdateAlbum($owner: String) { 35 | onUpdateAlbum(owner: $owner) { 36 | id 37 | name 38 | owner 39 | createdAt 40 | updatedAt 41 | photos { 42 | items { 43 | id 44 | albumId 45 | owner 46 | uploadTime 47 | bucket 48 | format 49 | exifMake 50 | exitModel 51 | SfnExecutionArn 52 | ProcessingStatus 53 | objectDetected 54 | createdAt 55 | updatedAt 56 | } 57 | nextToken 58 | } 59 | } 60 | } 61 | `; 62 | export const onDeleteAlbum = /* GraphQL */ ` 63 | subscription OnDeleteAlbum($owner: String) { 64 | onDeleteAlbum(owner: $owner) { 65 | id 66 | name 67 | owner 68 | createdAt 69 | updatedAt 70 | photos { 71 | items { 72 | id 73 | albumId 74 | owner 75 | uploadTime 76 | bucket 77 | format 78 | exifMake 79 | exitModel 80 | SfnExecutionArn 81 | ProcessingStatus 82 | objectDetected 83 | createdAt 84 | updatedAt 85 | } 86 | nextToken 87 | } 88 | } 89 | } 90 | `; 91 | export const onCreatePhoto = /* GraphQL */ ` 92 | subscription OnCreatePhoto($owner: String) { 93 | onCreatePhoto(owner: $owner) { 94 | id 95 | albumId 96 | owner 97 | uploadTime 98 | bucket 99 | fullsize { 100 | key 101 | width 102 | height 103 | } 104 | thumbnail { 105 | key 106 | width 107 | height 108 | } 109 | format 110 | exifMake 111 | exitModel 112 | SfnExecutionArn 113 | ProcessingStatus 114 | objectDetected 115 | geoLocation { 116 | Latitude { 117 | D 118 | M 119 | S 120 | Direction 121 | } 122 | Longtitude { 123 | D 124 | M 125 | S 126 | Direction 127 | } 128 | } 129 | createdAt 130 | updatedAt 131 | album { 132 | id 133 | name 134 | owner 135 | createdAt 136 | updatedAt 137 | photos { 138 | nextToken 139 | } 140 | } 141 | } 142 | } 143 | `; 144 | export const onUpdatePhoto = /* GraphQL */ ` 145 | subscription OnUpdatePhoto($owner: String) { 146 | onUpdatePhoto(owner: $owner) { 147 | id 148 | albumId 149 | owner 150 | uploadTime 151 | bucket 152 | fullsize { 153 | key 154 | width 155 | height 156 | } 157 | thumbnail { 158 | key 159 | width 160 | height 161 | } 162 | format 163 | exifMake 164 | exitModel 165 | SfnExecutionArn 166 | ProcessingStatus 167 | objectDetected 168 | geoLocation { 169 | Latitude { 170 | D 171 | M 172 | S 173 | Direction 174 | } 175 | Longtitude { 176 | D 177 | M 178 | S 179 | Direction 180 | } 181 | } 182 | createdAt 183 | updatedAt 184 | album { 185 | id 186 | name 187 | owner 188 | createdAt 189 | updatedAt 190 | photos { 191 | nextToken 192 | } 193 | } 194 | } 195 | } 196 | `; 197 | export const onDeletePhoto = /* GraphQL */ ` 198 | subscription OnDeletePhoto($owner: String) { 199 | onDeletePhoto(owner: $owner) { 200 | id 201 | albumId 202 | owner 203 | uploadTime 204 | bucket 205 | fullsize { 206 | key 207 | width 208 | height 209 | } 210 | thumbnail { 211 | key 212 | width 213 | height 214 | } 215 | format 216 | exifMake 217 | exitModel 218 | SfnExecutionArn 219 | ProcessingStatus 220 | objectDetected 221 | geoLocation { 222 | Latitude { 223 | D 224 | M 225 | S 226 | Direction 227 | } 228 | Longtitude { 229 | D 230 | M 231 | S 232 | Direction 233 | } 234 | } 235 | createdAt 236 | updatedAt 237 | album { 238 | id 239 | name 240 | owner 241 | createdAt 242 | updatedAt 243 | photos { 244 | nextToken 245 | } 246 | } 247 | } 248 | } 249 | `; 250 | -------------------------------------------------------------------------------- /src/react-frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/react-frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /src/react-frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/react-frontend/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/react-frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/react-frontend/src/utils.js: -------------------------------------------------------------------------------- 1 | function makeComparator(key, order = 'asc') { 2 | return (a, b) => { 3 | if (!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) 4 | return 0; 5 | 6 | const aVal = (typeof a[key] === 'string') 7 | ? a[key].toUpperCase() 8 | : a[key]; 9 | const bVal = (typeof b[key] === 'string') 10 | ? b[key].toUpperCase() 11 | : b[key]; 12 | 13 | let comparison = 0; 14 | if (aVal > bVal) 15 | comparison = 1; 16 | if (aVal < bVal) 17 | comparison = -1; 18 | 19 | return order === 'desc' 20 | ? (comparison * -1) 21 | : comparison 22 | }; 23 | } 24 | 25 | export { makeComparator } 26 | --------------------------------------------------------------------------------