├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .env.sample ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .vscode └── launch.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── build-and-deploy-sample-job.yml ├── images ├── chrome-edge.jpg └── spring-flowers.jpg ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── scripts ├── az-group-create.sh ├── az-group-delete.sh ├── az-storage-cors-add.sh ├── az-storage-delete-resource.sh ├── az-storage-generate-sas.sh └── newStorageService.js └── src ├── App.css ├── App.jsx ├── ContainerImages.jsx ├── azure-storage-blob.js ├── index.css ├── index.jsx ├── logo.svg ├── reportWebVitals.js └── setupTests.js /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.145.0/containers/typescript-node/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Node.js version: 14, 12, 10 4 | ARG VARIANT="14-buster" 5 | FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} 6 | 7 | # [Optional] Uncomment this section to install additional OS packages. 8 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | # && apt-get -y install --no-install-recommends 10 | 11 | # [Optional] Uncomment if you want to install an additional version of node using nvm 12 | # ARG EXTRA_NODE_VERSION=10 13 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 14 | 15 | # [Optional] Uncomment if you want to install more global node packages 16 | # RUN sudo -u node npm install -g 17 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Node.js & TypeScript", 3 | "build": { 4 | "dockerfile": "Dockerfile", 5 | // Update 'VARIANT' to pick a Node version: 10, 12, 14 6 | "args": { "VARIANT": "14" } 7 | }, 8 | 9 | // Set *default* container specific settings.json values on container create. 10 | "settings": { 11 | "terminal.integrated.shell.linux": "/bin/bash" 12 | }, 13 | 14 | // Add the IDs of extensions you want installed when the container is created. 15 | "extensions": [ 16 | "dbaeumer.vscode-eslint", 17 | "ms-vscode.vscode-typescript-tslint-plugin" 18 | ] 19 | 20 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 21 | ,"forwardPorts": [3000], 22 | 23 | // Use 'postCreateCommand' to run commands after the container is created. 24 | "postCreateCommand": "yarn install", 25 | 26 | // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. 27 | // "remoteUser": "node" 28 | } -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | REACT_APP_AZURE_STORAGE_SAS_TOKEN="sv=..." 2 | REACT_APP_AZURE_STORAGE_RESOURCE_NAME= -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /.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: '27 6 * * 1' 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@v2 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@v2 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@v2 68 | -------------------------------------------------------------------------------- /.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 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "NPM create server", 9 | "request": "launch", 10 | "runtimeArgs": [ 11 | "run-script", 12 | "server:create" 13 | ], 14 | "runtimeExecutable": "npm", 15 | "skipFiles": [ 16 | "/**" 17 | ], 18 | "type": "pwa-node" 19 | }, 20 | { 21 | "type": "node", 22 | "request": "launch", 23 | "name": "Launch Program", 24 | "skipFiles": [ 25 | "/**" 26 | ], 27 | "program": "${workspaceFolder}\\start", 28 | "outFiles": [ 29 | "${workspaceFolder}/**/*.js" 30 | ] 31 | }, 32 | { 33 | "type": "chrome", 34 | "request": "launch", 35 | "name": "Launch Chrome against localhost", 36 | "url": "http://localhost:3000", 37 | "webRoot": "${workspaceFolder}" 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 | 4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - javascript 5 | - typescript 6 | - nodejs 7 | name: "JavaScript end-to-end client file upload to Azure Storage Blobs" 8 | description: "Use an Azure Static Web App (client-side React app) to upload an image file to an Azure Storage blob using an Azure Storage @azure/storage-blob npm package and an Azure Storage SAS token." 9 | products: 10 | - azure 11 | - azure-storage 12 | - azure-portal 13 | - vs-code 14 | - azure-app-service-static 15 | --- 16 | 17 | **This sample has been archived. There is a current version available in [https://github.com/Azure-Samples/azure-typescript-e2e-apps](https://github.com/Azure-Samples/azure-typescript-e2e-apps).** 18 | 19 | # JavaScript end-to-end client file upload to Azure Storage Blobs 20 | 21 | This sample project is a TypeScript React (create-react-app) framework client app with an HTML form to select a file for upload to Azure Storage Blobs. 22 | 23 | TypeScript [version](https://github.com/Azure-Samples/ts-e2e-browser-file-upload-storage-blob) 24 | 25 | The user: 26 | * selects an image from the file system 27 | * uploads the image to Storage Blobs 28 | 29 | * [Read Tutorial](https://docs.microsoft.com/azure/developer/javascript/tutorial/browser-file-upload-azure-storage-blob) - The tutorial demonstrates how to load and run the project locally with VSCode. The tutorial includes creating a Storage resource, SAS token and CORS configuration. 30 | 31 | 32 | ## Sample application 33 | 34 | The React (create-react-app) client app consists of the following elements: 35 | 36 | * **React** app hosted on port 3000 37 | * uploadToBlob.ts using **@azure/storage-blob** client library to create Blob container and upload file 38 | 39 | ## Features 40 | 41 | This project framework provides the following features: 42 | 43 | * Create Azure Storage resource 44 | * Generate SAS token for Storage resource 45 | * Set Storage resource CORS 46 | * Select and upload file to Azure Storage Blob Container 47 | 48 | ## Getting Started 49 | 50 | 1. Clone or download repo. 51 | 1. Create Azure Storage resource - using /scripts/newStorageService.js. This resource name is the `storageAccountName`. 52 | 1. Generate SAS Token for Storage resource - using /scripts/az-storage-generte-sas.sh. This value is the `sasToken`. 53 | 1. Configure CORS for browser - using /scripts/az-storage-cors-add.sh 54 | 55 | Settings for CORS: 56 | * Allowed origins: `*` 57 | * Allowed methods: `DELETE, GET, HEAD, MERGE, POST, OPTIONS, and PUT` 58 | * Allowed headers: `*` 59 | * Exposed headers: `*` 60 | * Max age: `86400` 61 | 1. Install dependencies: 62 | 63 | ```javascript 64 | npm install 65 | ``` 66 | 67 | To run the React app, you need the following Azure SDK client npm packages: 68 | * @azure/identity 69 | * @azure/storage-blob 70 | 71 | A third Azure package, @azure/arm-storage, is listed in the `package.json` strictly for use by the `scripts/newStorageService.js` file to create a new Azure Storage resource. 72 | 73 | 1. Create a file name `.env` at the root of the project. 74 | 1. Add two required variables with their storage values: 75 | 76 | ```text 77 | REACT_APP_AZURE_STORAGE_SAS_TOKEN= 78 | REACT_APP_AZURE_STORAGE_RESOURCE_NAME= 79 | ``` 80 | 81 | React builds the static files with these variables. 82 | 83 | 1. If the token begins with a question mark, remove the `?`. The code file provides the `?` for you so you don't need it in the token. 84 | 85 | 1. Start project: 86 | 87 | ```javascript 88 | npm start 89 | ``` 90 | 91 | 1. View project in browser, `http://localhost:3000`. 92 | 93 | 1. Select image then select `Upload!`. 94 | 95 | Page displays images in container. 96 | 97 | ## Prerequisites 98 | 99 | - Git, if cloning 100 | - Node.js and NPM 101 | - Web browser 102 | - Azure subscription to create resource on 103 | 104 | ## Installation 105 | 106 | 1. Install the sample's dependencies: 107 | 108 | ```javascript 109 | npm install 110 | ``` 111 | 112 | 1. Run the command to run the web app. 113 | 114 | ```javascript 115 | npm start 116 | ``` 117 | 118 | 1. Open a web browser and use the following url to view the client app on your local computer. 119 | 120 | ```url 121 | http://localhost:3000/ 122 | ``` 123 | 124 | ## Troubleshooting 125 | 126 | If you received an error or your file doesn't upload to the container, check the following: 127 | 128 | * Recreate your SAS token, making sure that your token is created at the Storage resource level and not the container level. Copy the new token into the code at the correct location. 129 | * Check that the token string you copied into the code doesn't contain the `?` (question mark) at the beginning of the string. 130 | * Verify your CORS setting for your Storage resource. 131 | 132 | ## Additional scripts 133 | 134 | * Create Azure Storage Blob from JavaScript file: scripts/newStorageService.js 135 | * Set CORS for service using Azure CLI script: scripts/az-storage-cors-add.sh 136 | * Generate SAS Token using Azure CLI script: scripts/az-storage-generate-sas.sh 137 | 138 | ## Images 139 | 140 | The /images folder includes images for upload. 141 | -------------------------------------------------------------------------------- /build-and-deploy-sample-job.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | build_and_deploy_job: 3 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 4 | runs-on: ubuntu-latest 5 | name: Build and Deploy Job 6 | steps: 7 | - uses: actions/checkout@v2 8 | with: 9 | submodules: true 10 | - name: Build And Deploy 11 | id: builddeploy 12 | uses: Azure/static-web-apps-deploy@v1 13 | with: 14 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_123456 }} 15 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 16 | action: "upload" 17 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### 18 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 19 | app_location: "/" # App source code path 20 | api_location: "api" # Api source code path - optional 21 | output_location: "build" # Built app content directory - optional 22 | ###### End of Repository/Build Configurations ###### 23 | env: 24 | REACT_APP_AZURE_STORAGE_SAS_TOKEN: ${{secrets.REACT_APP_AZURE_STORAGE_SAS_TOKEN}} 25 | REACT_APP_AZURE_STORAGE_RESOURCE_NAME: ${{secrets.REACT_APP_AZURE_STORAGE_RESOURCE_NAME}} 26 | -------------------------------------------------------------------------------- /images/chrome-edge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/js-e2e-browser-file-upload-storage-blob/313e83f758a5c7685c460f84f530e9499033782c/images/chrome-edge.jpg -------------------------------------------------------------------------------- /images/spring-flowers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/js-e2e-browser-file-upload-storage-blob/313e83f758a5c7685c460f84f530e9499033782c/images/spring-flowers.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-e2e-browser-file-upload-storage-blob", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@azure/storage-blob": "^12.12.0", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "path-browserify": "^1.0.1", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-scripts": "5.0.1", 14 | "web-vitals": "^2.1.4" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "devDependencies": { 41 | "@azure/arm-storage": "^18.1.0", 42 | "@azure/identity": "^3.1.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/js-e2e-browser-file-upload-storage-blob/313e83f758a5c7685c460f84f530e9499033782c/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/js-e2e-browser-file-upload-storage-blob/313e83f758a5c7685c460f84f530e9499033782c/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/js-e2e-browser-file-upload-storage-blob/313e83f758a5c7685c460f84f530e9499033782c/public/logo512.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /scripts/az-group-create.sh: -------------------------------------------------------------------------------- 1 | # https://docs.microsoft.com/cli/azure/group?view=azure-cli-latest#az_group_create 2 | az group create --location westus 3 | --name rg-YOUR-NAME 4 | --subscription YOUR-SUBSCRIPTION-ID -------------------------------------------------------------------------------- /scripts/az-group-delete.sh: -------------------------------------------------------------------------------- 1 | # https://docs.microsoft.com/en-us/cli/azure/group?view=azure-cli-latest#az_group_delete 2 | az group delete --name 3 | --no-wait 4 | --subscription YOUR-SUBSCRIPTION-ID 5 | --yes -------------------------------------------------------------------------------- /scripts/az-storage-cors-add.sh: -------------------------------------------------------------------------------- 1 | # https://docs.microsoft.com/cli/azure/storage/cors?view=azure-cli-latest#az_storage_cors_add 2 | az storage cors add --methods DELETE GET HEAD MERGE OPTIONS POST PUT \ 3 | --origins "*" \ 4 | --allowed-headers "*" \ 5 | --exposed-headers "*" \ 6 | --services b \ 7 | --max-age 86400 \ 8 | --timeout 86400 \ 9 | --account-key YOUR-RESOURCE-PRIMARY-KEY \ 10 | --account-name YOUR-RESOURCE-NAME \ 11 | --subscription YOUR-SUBSCRIPTION-ID \ 12 | --sas-token "YOUR-SAS-TOKEN" -------------------------------------------------------------------------------- /scripts/az-storage-delete-resource.sh: -------------------------------------------------------------------------------- 1 | # https://docs.microsoft.com/cli/azure/storage/account?view=azure-cli-latest#az_storage_account_delete 2 | # If you used the Azure Storage extension for Visual Studio Code to create your resource, the resource name 3 | # and resource group name are the same. 4 | az storage account delete --name YOUR-RESOURCE-NAME 5 | --resource-group YOUR-RESOURCE-NAME 6 | --subscription YOUR-SUBSCRIPTION-ID 7 | --yes -------------------------------------------------------------------------------- /scripts/az-storage-generate-sas.sh: -------------------------------------------------------------------------------- 1 | # https://docs.microsoft.com/cli/azure/storage/account?view=azure-cli-latest#az_storage_account_generate_sas 2 | az storage account generate-sas --expiry 2021-12-30T12:00Z \ 3 | --permissions rwdlac \ 4 | --resource-types sco \ 5 | --services b \ 6 | --https-only \ 7 | --account-key YOUR-RESOURCE-PRIMARY-KEY \ 8 | --account-name YOUR-RESOURCE-NAME \ 9 | --subscription YOUR-SUBSCRIPTION-ID 10 | 11 | # Save the result - this is your SAS Token -------------------------------------------------------------------------------- /scripts/newStorageService.js: -------------------------------------------------------------------------------- 1 | //import DeviceCodeCredential from "@azure/identity"; 2 | //import StorageManagementClient from "@azure/arm-storage"; 3 | const { DeviceCodeCredential } = require("@azure/identity"); 4 | const { StorageManagementClient } = require("@azure/arm-storage"); 5 | 6 | // DON'T DO THIS IN PRODUCTION CODE 7 | const subscriptionId = "REPLACE-WITH-YOUR-SUBSCRIPTION"; 8 | const existingResourceGroup = "REPLACE-WITH-YOUR-RESOURCE-GROUP"; 9 | const storageResourceName = "REPLACE-WITH-YOUR-RESOURCE-NAME"; 10 | 11 | // 24 chars - no space, dash, or uppercase 12 | const longStorageAccountName = storageResourceName + Math.random().toString().replace(/0\./, ''); 13 | const accountName = longStorageAccountName.substring(0, 24); 14 | 15 | const creds = new DeviceCodeCredential(); 16 | 17 | async function main() { 18 | const client = new StorageManagementClient(creds, subscriptionId); 19 | 20 | const blobServiceOptions = { 21 | accessTier: 'Hot', 22 | allowBlobPublicAccess: true, 23 | customDomain: { 24 | name: "", 25 | useSubDomainName: false 26 | }, 27 | enableHttpsTrafficOnly: true, 28 | isHnsEnabled: false, 29 | kind: 'StorageV2', 30 | largeFileSharesState: 'Disabled', 31 | location: 'westus', 32 | sku: { 33 | name: 'Standard_RAGRS', 34 | tier: 'Standard' 35 | } 36 | 37 | } 38 | const createStorageAccountResponse = await client.storageAccounts.beginCreateAndWait(existingResourceGroup, accountName, blobServiceOptions); 39 | 40 | console.log(createStorageAccountResponse); 41 | 42 | } 43 | 44 | main().catch((err) => { 45 | console.log("An error occurred:"); 46 | console.error(err); 47 | }); 48 | -------------------------------------------------------------------------------- /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/App.jsx: -------------------------------------------------------------------------------- 1 | // ./src/App.tsx 2 | 3 | import React, { useState, useEffect } from 'react'; 4 | import uploadFileToBlob, { isStorageConfigured, getBlobsInContainer } from './azure-storage-blob'; 5 | import DisplayImagesFromContainer from './ContainerImages'; 6 | const storageConfigured = isStorageConfigured(); 7 | 8 | const App = () => { 9 | // all blobs in container 10 | const [blobList, setBlobList] = useState([]); 11 | 12 | // current file to upload into container 13 | const [fileSelected, setFileSelected] = useState(); 14 | const [fileUploaded, setFileUploaded] = useState(''); 15 | 16 | // UI/form management 17 | const [uploading, setUploading] = useState(false); 18 | const [inputKey, setInputKey] = useState(Math.random().toString(36)); 19 | 20 | // *** GET FILES IN CONTAINER *** 21 | useEffect(() => { 22 | getBlobsInContainer().then((list) =>{ 23 | // prepare UI for results 24 | setBlobList(list); 25 | }) 26 | }, [fileUploaded]); 27 | 28 | const onFileChange = (event) => { 29 | // capture file into state 30 | setFileSelected(event.target.files[0]); 31 | }; 32 | 33 | const onFileUpload = async () => { 34 | 35 | if(fileSelected && fileSelected?.name){ 36 | // prepare UI 37 | setUploading(true); 38 | 39 | // *** UPLOAD TO AZURE STORAGE *** 40 | await uploadFileToBlob(fileSelected); 41 | 42 | // reset state/form 43 | setFileSelected(null); 44 | setFileUploaded(fileSelected.name); 45 | setUploading(false); 46 | setInputKey(Math.random().toString(36)); 47 | 48 | } 49 | 50 | }; 51 | 52 | // display form 53 | const DisplayForm = () => ( 54 |
55 | 56 | 59 |
60 | ) 61 | 62 | return ( 63 |
64 |

Upload file to Azure Blob Storage

65 | {storageConfigured && !uploading && DisplayForm()} 66 | {storageConfigured && uploading &&
Uploading
} 67 |
68 | {storageConfigured && blobList.length > 0 && } 69 | {!storageConfigured &&
Storage is not configured.
} 70 |
71 | ); 72 | }; 73 | 74 | export default App; 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/ContainerImages.jsx: -------------------------------------------------------------------------------- 1 | 2 | const DisplayImagesFromContainer = ({blobList}) => ( 3 |
4 |

Container items

5 |
    6 | {blobList.map((item) => { 7 | return ( 8 |
  • 9 |
    10 | {item.name} 11 |
    12 | {item.name} 13 |
    14 |
  • 15 | ); 16 | })} 17 |
18 |
19 | ); 20 | 21 | export default DisplayImagesFromContainer; -------------------------------------------------------------------------------- /src/azure-storage-blob.js: -------------------------------------------------------------------------------- 1 | // ./src/azure-storage-blob.ts 2 | 3 | // 4 | // THIS IS SAMPLE CODE ONLY - NOT MEANT FOR PRODUCTION USE 5 | import { BlobServiceClient } from "@azure/storage-blob"; 6 | 7 | const containerName = `uploaded`; 8 | const sasToken = process.env.REACT_APP_AZURE_STORAGE_SAS_TOKEN; 9 | const storageAccountName = process.env.REACT_APP_AZURE_STORAGE_RESOURCE_NAME; 10 | // 11 | 12 | // 13 | const uploadUrl = `https://${storageAccountName}.blob.core.windows.net/?${sasToken}`; 14 | console.log(uploadUrl); 15 | 16 | // get BlobService = notice `?` is pulled out of sasToken - if created in Azure portal 17 | const blobService = new BlobServiceClient(uploadUrl); 18 | 19 | // get Container - full public read access 20 | const containerClient = 21 | blobService.getContainerClient(containerName); 22 | // 23 | 24 | // 25 | // Feature flag - disable storage feature to app if not configured 26 | export const isStorageConfigured = () => { 27 | return !storageAccountName || !sasToken ? false : true; 28 | }; 29 | // 30 | 31 | // 32 | // return list of blobs in container to display 33 | export const getBlobsInContainer = async () => { 34 | const returnedBlobUrls = []; 35 | 36 | // get list of blobs in container 37 | // eslint-disable-next-line 38 | for await (const blob of containerClient.listBlobsFlat()) { 39 | console.log(`${blob.name}`); 40 | 41 | const blobItem = { 42 | url: `https://${storageAccountName}.blob.core.windows.net/${containerName}/${blob.name}?${sasToken}`, 43 | name: blob.name 44 | } 45 | 46 | // if image is public, just construct URL 47 | returnedBlobUrls.push(blobItem); 48 | } 49 | 50 | return returnedBlobUrls; 51 | }; 52 | // 53 | 54 | // 55 | const createBlobInContainer = async (file) => { 56 | // create blobClient for container 57 | const blobClient = containerClient.getBlockBlobClient(file.name); 58 | 59 | // set mimetype as determined from browser with file upload control 60 | const options = { blobHTTPHeaders: { blobContentType: file.type } }; 61 | 62 | // upload file 63 | await blobClient.uploadData(file, options); 64 | }; 65 | // 66 | 67 | // 68 | const uploadFileToBlob = async (file) => { 69 | if (!file) return; 70 | 71 | // upload file 72 | await createBlobInContainer(file); 73 | }; 74 | // 75 | 76 | export default uploadFileToBlob; 77 | -------------------------------------------------------------------------------- /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/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') 9 | ); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /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'; 6 | --------------------------------------------------------------------------------