├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── THIRD-PARTY-LICENSES ├── docs ├── arch-overview.png ├── home-screenshot-1.png ├── home-screenshot-2.png └── home-screenshot-3.png ├── sam ├── .gitignore ├── seed_s3_data │ ├── __init__.py │ ├── app.py │ ├── poetry.lock │ ├── pyproject.toml │ ├── requirements.txt │ └── website.zip ├── template.yaml └── tests │ ├── __init__.py │ ├── poetry.lock │ ├── pyproject.toml │ ├── requirements.txt │ └── unit │ ├── __init__.py │ └── test_seed_s3_data.py └── website ├── css ├── bootstrap-grid.css ├── bootstrap-grid.css.map ├── bootstrap-grid.min.css ├── bootstrap-grid.min.css.map ├── bootstrap-grid.rtl.css ├── bootstrap-grid.rtl.css.map ├── bootstrap-grid.rtl.min.css ├── bootstrap-grid.rtl.min.css.map ├── bootstrap-icons.css ├── bootstrap-icons.json ├── bootstrap-icons.min.css ├── bootstrap-icons.scss ├── bootstrap-reboot.css ├── bootstrap-reboot.css.map ├── bootstrap-reboot.min.css ├── bootstrap-reboot.min.css.map ├── bootstrap-reboot.rtl.css ├── bootstrap-reboot.rtl.css.map ├── bootstrap-reboot.rtl.min.css ├── bootstrap-reboot.rtl.min.css.map ├── bootstrap-utilities.css ├── bootstrap-utilities.css.map ├── bootstrap-utilities.min.css ├── bootstrap-utilities.min.css.map ├── bootstrap-utilities.rtl.css ├── bootstrap-utilities.rtl.css.map ├── bootstrap-utilities.rtl.min.css ├── bootstrap-utilities.rtl.min.css.map ├── bootstrap.css ├── bootstrap.css.map ├── bootstrap.min.css ├── bootstrap.min.css.map ├── bootstrap.rtl.css ├── bootstrap.rtl.css.map ├── bootstrap.rtl.min.css ├── bootstrap.rtl.min.css.map ├── fonts │ ├── bootstrap-icons.woff │ └── bootstrap-icons.woff2 └── main.css ├── icon ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── mstile-144x144.png ├── mstile-150x150.png ├── mstile-310x150.png ├── mstile-310x310.png ├── mstile-70x70.png ├── safari-pinned-tab.svg └── site.webmanifest ├── index.html └── js ├── aws-sdk-js-v2.1560.0.min.js ├── bootstrap.bundle.js ├── bootstrap.bundle.js.map ├── bootstrap.bundle.min.js ├── bootstrap.bundle.min.js.map ├── bootstrap.esm.js ├── bootstrap.esm.js.map ├── bootstrap.esm.min.js ├── bootstrap.esm.min.js.map ├── bootstrap.js ├── bootstrap.js.map ├── bootstrap.min.js ├── bootstrap.min.js.map ├── jquery-3.7.1.min.js ├── luxon.min.js └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### OSX ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Thumbnails 25 | ._* 26 | 27 | # Files that might appear in the root of a volume 28 | .DocumentRevisions-V100 29 | .fseventsd 30 | .Spotlight-V100 31 | .TemporaryItems 32 | .Trashes 33 | .VolumeIcon.icns 34 | .com.apple.timemachine.donotpresent 35 | 36 | # Directories potentially created on remote AFP share 37 | .AppleDB 38 | .AppleDesktop 39 | Network Trash Folder 40 | Temporary Items 41 | .apdisk 42 | 43 | ### PyCharm ### 44 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 45 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 46 | 47 | # User-specific stuff: 48 | .idea/**/workspace.xml 49 | .idea/**/tasks.xml 50 | .idea/dictionaries 51 | 52 | # Sensitive or high-churn files: 53 | .idea/**/dataSources/ 54 | .idea/**/dataSources.ids 55 | .idea/**/dataSources.xml 56 | .idea/**/dataSources.local.xml 57 | .idea/**/sqlDataSources.xml 58 | .idea/**/dynamic.xml 59 | .idea/**/uiDesigner.xml 60 | 61 | # Gradle: 62 | .idea/**/gradle.xml 63 | .idea/**/libraries 64 | 65 | # CMake 66 | cmake-build-debug/ 67 | 68 | # Mongo Explorer plugin: 69 | .idea/**/mongoSettings.xml 70 | 71 | ## File-based project format: 72 | *.iws 73 | 74 | ## Plugin-specific files: 75 | 76 | # IntelliJ 77 | /out/ 78 | 79 | # mpeltonen/sbt-idea plugin 80 | .idea_modules/ 81 | 82 | # JIRA plugin 83 | atlassian-ide-plugin.xml 84 | 85 | # Cursive Clojure plugin 86 | .idea/replstate.xml 87 | 88 | # Ruby plugin and RubyMine 89 | /.rakeTasks 90 | 91 | # Crashlytics plugin (for Android Studio and IntelliJ) 92 | com_crashlytics_export_strings.xml 93 | crashlytics.properties 94 | crashlytics-build.properties 95 | fabric.properties 96 | 97 | ### PyCharm Patch ### 98 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 99 | 100 | # *.iml 101 | # modules.xml 102 | # .idea/misc.xml 103 | # *.ipr 104 | 105 | # Sonarlint plugin 106 | .idea/sonarlint 107 | 108 | ### Python ### 109 | # Byte-compiled / optimized / DLL files 110 | __pycache__/ 111 | *.py[cod] 112 | *$py.class 113 | 114 | # C extensions 115 | *.so 116 | 117 | # Distribution / packaging 118 | .Python 119 | build/ 120 | develop-eggs/ 121 | dist/ 122 | downloads/ 123 | eggs/ 124 | .eggs/ 125 | lib/ 126 | lib64/ 127 | parts/ 128 | sdist/ 129 | var/ 130 | wheels/ 131 | *.egg-info/ 132 | .installed.cfg 133 | *.egg 134 | 135 | # PyInstaller 136 | # Usually these files are written by a python script from a template 137 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 138 | *.manifest 139 | *.spec 140 | 141 | # Installer logs 142 | pip-log.txt 143 | pip-delete-this-directory.txt 144 | 145 | # Unit test / coverage reports 146 | htmlcov/ 147 | .tox/ 148 | .coverage 149 | .coverage.* 150 | .cache 151 | .pytest_cache/ 152 | nosetests.xml 153 | coverage.xml 154 | *.cover 155 | .hypothesis/ 156 | 157 | # Translations 158 | *.mo 159 | *.pot 160 | 161 | # Flask stuff: 162 | instance/ 163 | .webassets-cache 164 | 165 | # Scrapy stuff: 166 | .scrapy 167 | 168 | # Sphinx documentation 169 | docs/_build/ 170 | 171 | # PyBuilder 172 | target/ 173 | 174 | # Jupyter Notebook 175 | .ipynb_checkpoints 176 | 177 | # pyenv 178 | .python-version 179 | 180 | # celery beat schedule file 181 | celerybeat-schedule.* 182 | 183 | # SageMath parsed files 184 | *.sage.py 185 | 186 | # Environments 187 | .env 188 | .venv 189 | env/ 190 | venv/ 191 | ENV/ 192 | env.bak/ 193 | venv.bak/ 194 | 195 | # Spyder project settings 196 | .spyderproject 197 | .spyproject 198 | 199 | # Rope project settings 200 | .ropeproject 201 | 202 | # mkdocs documentation 203 | /site 204 | 205 | # mypy 206 | .mypy_cache/ 207 | 208 | ### VisualStudioCode ### 209 | .vscode/* 210 | !.vscode/settings.json 211 | !.vscode/tasks.json 212 | !.vscode/launch.json 213 | !.vscode/extensions.json 214 | .history 215 | 216 | ### Windows ### 217 | # Windows thumbnail cache files 218 | Thumbs.db 219 | ehthumbs.db 220 | ehthumbs_vista.db 221 | 222 | # Folder config file 223 | Desktop.ini 224 | 225 | # Recycle Bin used on file shares 226 | $RECYCLE.BIN/ 227 | 228 | # Windows Installer files 229 | *.cab 230 | *.msi 231 | *.msm 232 | *.msp 233 | 234 | # Windows shortcuts 235 | *.lnk 236 | 237 | # Build folder 238 | 239 | */build/* 240 | 241 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 242 | .aws-sam/* 243 | samconfig.toml 244 | .idea/* 245 | ash_cf2cdk_output/* 246 | aggregated_results.txt 247 | sam/.idea/** 248 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | - Nothing 9 | 10 | ## [1.0.0] - 2024-02-20 11 | - Initial Release 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Public File Browser for Amazon S3 2 | 3 | AWS Storage Blog: [Creating a simple public file repository on Amazon S3](https://aws.amazon.com/blogs/storage/creating-a-simple-public-file-repository-on-amazon-s3/) 4 | 5 | This AWS Samples code allows customers to create a simple PUBLIC file repository using Amazon S3 and Amazon CloudFront. 6 | This sample code deploys a website and a public files S3 bucket which can be loaded with any files they wish to publish 7 | publicly online. 8 | 9 | ![Example Interface Screenshot](./docs/home-screenshot-3.png "Example Interface Screenshot") 10 | 11 | ## Architecture 12 | 13 | ![Overall Architecture Diagram](./docs/arch-overview.png "Overall Architecture Diagram") 14 | 15 | 1. User accesses static website via CloudFront CDN (contents of S3 "***Website***" Bucket) 16 | 2. Static website loads and browser obtains temporary credentials from Cognito 17 | 3. Cognito credentials are used to list files in the public S3 "Files" Bucket 18 | 4. Directory and file tree is rendered in the browser 19 | 5. User clicks any file to download via CloudFront CDN (contents of S3 "***Files***" Bucket) 20 | 21 | ## Deployment Instructions 22 | 23 | The solution is packaged as an automated deployment via the [AWS Serverless Application Model (SAM)](https://aws.amazon.com/serverless/sam/) CLI. 24 | 25 | ### Prerequisites 26 | 27 | For this walkthrough, you need to have the following prerequisites: 28 | 29 | - An [AWS account](https://portal.aws.amazon.com/billing/signup) 30 | - [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) installed and set up with [credentials](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-getting-started-set-up-credentials.html) 31 | - Python 3.11 Installed and in your PATH variable 32 | - Choose a region where the required services below are supported. Most AWS commercial regions are supported, but consult the [AWS Services by Region](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/) page for details. 33 | - Amazon CloudFront (including [Standard Log support](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html#access-logs-choosing-s3-bucket)) 34 | - Amazon S3 35 | - Amazon Cognito 36 | - AWS CloudFormation 37 | - AWS Lambda 38 | - As of March 2024, these are the supported regions: 39 | - US East (N. Virginia) 40 | - US East (Ohio) 41 | - US West (N. California) 42 | - US West (Oregon) 43 | - Canada (Central) 44 | - Europe (Frankfurt) 45 | - Europe (Ireland) 46 | - Europe (London) 47 | - Europe (Paris) 48 | - Europe (Stockholm) 49 | - Asia Pacific (Mumbai) 50 | - Asia Pacific (Osaka) 51 | - Asia Pacific (Seoul) 52 | - Asia Pacific (Singapore) 53 | - Asia Pacific (Sydney) 54 | - Asia Pacific (Tokyo) 55 | - South America (São Paulo) 56 | 57 | 58 | ### Build and Deploy 59 | 60 | [Download](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) the source code and extract locally: [AWS Samples - Public File Browser for Amazon S3](https://github.com/aws-samples/public-file-browser-for-amazon-s3) 61 | 62 | 1. In a terminal, navigate to the `./sam/` directory 63 | 2. Run the following command to build and package the project for deployment:\ 64 | `sam build` 65 | 3. Deploy the SAM template to your account. The wizard will guide you through the process of deploying the SAM [AWS CloudFormation](https://aws.amazon.com/cloudformation/). Details on this process are found in the [sam build documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-build.html). 66 | 1. Run the following command: \ 67 | `sam deploy --guided --capabilities CAPABILITY_NAMED_IAM` 68 | 2. Select the supported AWS Region you chose in the prerequisites section. 69 | 3. Enter values for the deployment parameters 70 | 1. Stack Name - Choose a unique CloudFormation stack name. End users do not see this. 71 | 2. AWS Region - The supported AWS Region you chose in the Prerequisites section. 72 | 3. `SiteName` - Publicly visible title for the site, shown on the top of the page and in the title bar. 73 | 4. `FilesOpenTabMode` - How do you want the browser to react when a user selects a file in the following interface values. `In New Tab` is recommended over `In Same Tab`. 74 | 5. `VisibleStorageClasses` - Comma delimited list of storage classes to show. Recommend keeping this default. 75 | 6. `CrossOriginRestriction` - Browser security setting, set to `*` for first deployment, then see Step 6. 76 | 4. Select the default inputs for the remaining items with the last prompt before deployment being:\ 77 | `Deploy this changeset?` 78 | 4. Wait for the deployment to complete. This process takes approximately five minutes with a final prompt stating:\ 79 | `Successfully created/updated stack - [STACK-NAME] in [REGION]` 80 | 5. Once the deployment completes, note the following entries in the Outputs section. 81 | 1. `FileBrowserURL` - This URL is for the public web interface. Needed in Step 6. 82 | 2. `PublicFilesBucket` - The name of the S3 bucket for storing PUBLICLY ACCESSIBLE files that display in the user browser. 83 | 3. `WebInterfaceAppBucket` - The name of the S3 bucket that stores the code that runs the file browser web interface. 84 | 6. IMPORTANT: Complete Steps 3 and 4 again, keeping all values the same except for the `CrossOriginRestrictio`n parameter, and input the value from the `FileBrowserURL` output in Step 5. For example:\ 85 | `Parameter CrossOriginRestriction [*]: https://d111111abcdef8.cloudfront.net` 86 | 87 | This concludes the deployment of the Public File Browser for Amazon S3 web application. AWS SAM CLI uses [AWS CloudFormation](https://aws.amazon.com/cloudformation/) to orchestrate the deployment of the front-end static website and public file storage bucket. The entire application is deployed. 88 | 89 | ### Usage 90 | 91 | To add files to the public interface simply move files into the S3 Bucket indicated in the `PublicFilesBucket` output from the above SAM Deploy command. The bucket name should start with `public-file-browser-files-` followed by a random string. 92 | 93 | Note that this solution has [Amazon S3 Versioning](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html) enabled for all data, which could result in deleted data being retained and incurring costs. See the Security section below for details. 94 | 95 | ## Frequently Asked Questions 96 | 97 | ### How much does this solution cost to operate? 98 | 99 | This is an entirely serverless solution. Therefore, costs are directly related to usage, both in data storage and data 100 | retrieval by end users. Reference below and consult the [S3](https://aws.amazon.com/s3/pricing/) and 101 | [CloudFront](https://aws.amazon.com/cloudfront/pricing/) pricing pages. 102 | 103 | - Static Costs 104 | - A few cents monthly to store the website source code in S3 Standard 105 | - S3 data storage costs for the public files and logging buckets 106 | - Free Tier: 5GB Per Month for First 12 Months 107 | - Costs for End User Access 108 | - CloudFront data transfer 109 | - Free Tier: 1TB Per Month 110 | - S3 LIST/GET request costs 111 | - Free Tier: 2,000 Page Views and 20,000 Downloads for First 12 Months 112 | - Note: There is NO CHARGE for S3 Data Transfer to CloudFront 113 | 114 | ### How are objects listed in the interface? 115 | 116 | S3 object/prefix lists are ordered lexicographically (UTF-8 byte order). For this to make sense in most use cases the 117 | solution automatically switches between two modes: 118 | - Less than or equal to 1000 Objects/Prefixes 119 | - Sort how most filesystems do (lexicographically with folders always on top) 120 | - This makes the system make intuitive sense for 99% of listings and views 121 | - Greater than 1000 Objects 122 | - Strictly lexicographically so folders may be interspersed 123 | - While this is less intuitive it is consistent without listing the entire bucket. Listing the entire bucket would 124 | inflate costs and load times unnecessarily. The alternative would be to take each page and treat it as above but 125 | this leads to odd ordering that almost seems random since the top of one page is not always the next object of the 126 | previous page (it is all the next folders lexicographically). 127 | 128 | ### How do I move files into the `public-file-browser-files-[...]` quickly in bulk? 129 | 130 | Sync from a local system: 131 | - [How-To Guide: Batch Upload Files to Amazon S3 Using the AWS CLI](https://aws.amazon.com/getting-started/hands-on/backup-to-s3-cli/) 132 | - [Use high-level (s3) commands with the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-services-s3-commands.html) 133 | - [Synchronizing your data to Amazon S3 using AWS DataSync](https://aws.amazon.com/blogs/storage/synchronizing-your-data-to-amazon-s3-using-aws-datasync/) 134 | 135 | For large uploads: 136 | - [How do I use the AWS CLI to upload a large file in multiple parts to Amazon S3?](https://repost.aws/knowledge-center/s3-multipart-upload-cli) 137 | - [How can I optimize performance when I upload large files to Amazon S3?](https://repost.aws/knowledge-center/s3-upload-large-files) 138 | 139 | If your files are already in a different Amazon S3 bucket: 140 | - [How can I copy all objects from one Amazon S3 bucket to another bucket?](https://repost.aws/knowledge-center/move-objects-s3-bucket) 141 | - For large Amazon S3 buckets with millions of files, consider using [S3 Batch Operations](https://docs.aws.amazon.com/AmazonS3/latest/userguide/batch-ops-examples-copy.html) 142 | 143 | ### How do I use this solution with my own DNS domain name (such as `publicfiles.example.com`) instead of the CloudFront name? 144 | 145 | See [How can I configure CloudFront to serve my content using an alternate domain name over HTTPS?](https://repost.aws/knowledge-center/cloudfront-https-content) 146 | - Note that you have to [update the Cross-Origin Resource Sharing (CORS) rules](https://repost.aws/knowledge-center/s3-configure-cors) in the Amazon S3 configuration of BOTH the files and website S3 buckets. 147 | - See also: [Using custom URLs by adding alternate domain names (CNAMEs)](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/CNAMEs.html) 148 | 149 | ### How do I modify the public user interface, such as to add text, change the style, or add my logo? 150 | 151 | The public website files are located in the `public-file-browser-website-[...]` bucket. These files can be downloaded, modified, and re-uploaded containing customizations. Note that CloudFront caches these files, so you must create an invalidation to clear the cache when a file is updated. See [How do I remove a cached file from CloudFront](https://repost.aws/knowledge-center/cloudfront-clear-cache)? 152 | 153 | If you choose to update the files in the `./website/` directory of the source code repository for future deployments, then you must update the `./sam/seed_s3_data/website.zip` by following the instructions in the repository’s `README.md` file. 154 | 155 | 156 | ## Security 157 | 158 | As a best practice, the solution enables the following features: 159 | 160 | - [Amazon CloudWatch distribution access logs](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html) 161 | - Stored in an S3 bucket: `public-file-browser-logging-[...]` 162 | - [Amazon S3 bucket access logs (static website and uploaded images)](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerLogs.html) 163 | - Stored in an S3 bucket: `public-file-browser-logging-[...]` 164 | - [Amazon S3 Versioning](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html) 165 | - This feature is enabled for all S3 buckets, such as the primary `public-file-browser-files-[...]` bucket used to store public files. This means all data written to the S3 bucket is retained as a “previous version”, even if overwritten or deleted. You incur charges for storing previous versions of objects. 166 | - If you expect to replace or update files with the same name frequently, then you should configure the S3 bucket with a [Lifecycle Configuration](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html) to expire noncurrent object versions. See [How do I create Amazon S3 lifecycle configuration rules for noncurrent object versions?](https://repost.aws/knowledge-center/s3-lifecycle-rule-non-current-version) 167 | - [Default Encryption-at-Rest for all objects stored in Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/default-encryption-faq.html) 168 | - [Default TLS Version Options (TLSv1)](https://repost.aws/knowledge-center/cloudfront-security-protocols-ciphers) 169 | - To simplify deployment, this solution uses the default CloudFront domain and certificate that limits the available TLS version options to TLSv1. To use another [supported TLS version](https://repost.aws/knowledge-center/cloudfront-security-protocols-ciphers) follow the instructions above to use a custom domain, [new certificate](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cnames-and-https-requirements.html), and configure the [Security Policy](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/secure-connections-supported-viewer-protocols-ciphers.html) on the CloudFront distribution. 170 | 171 | 172 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 173 | 174 | ## Development 175 | 176 | Run the following from the website directory using [local-web-server](https://www.npmjs.com/package/local-web-server): 177 | 178 | ```bash 179 | > ws -r '/ -> index.html' '/pfb_for_s3/(.*) -> /$1' --log.format dev 180 | ``` 181 | 182 | ### Automatic Deployment Note 183 | The file `./sam/seed_s3_data/website.zip` contains a statically zipped copy of the `./website/` directory. This 184 | zip file is used to automatically load the `public-file-browser-website-[...]` bucket with the actual website code 185 | during deployment. Before re-deploying you will need to re-create `./sam/seed_s3_data/website.zip` using the command 186 | below from the root of the repository. You may then follow the [DEPLOYMENT](./docs/DEPLOYMENT.md) guide. 187 | 188 | ```bash 189 | > zip -FS -x "*.DS_Store" -r ./sam/seed_s3_data/website.zip website 190 | ``` 191 | 192 | ## License 193 | 194 | This library is licensed under the MIT-0 License. See the [LICENSE](LICENSE) file. 195 | -------------------------------------------------------------------------------- /THIRD-PARTY-LICENSES: -------------------------------------------------------------------------------- 1 | ** aws-sdk-js; version 2.1560.0 -- https://aws.amazon.com/sdk-for-javascript/ 2 | 3 | 4 | Apache License 5 | Version 2.0, January 2004 6 | http://www.apache.org/licenses/ 7 | 8 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 9 | 10 | 1. Definitions. 11 | 12 | "License" shall mean the terms and conditions for use, reproduction, 13 | and distribution as defined by Sections 1 through 9 of this document. 14 | 15 | "Licensor" shall mean the copyright owner or entity authorized by 16 | the copyright owner that is granting the License. 17 | 18 | "Legal Entity" shall mean the union of the acting entity and all 19 | other entities that control, are controlled by, or are under common 20 | control with that entity. For the purposes of this definition, 21 | "control" means (i) the power, direct or indirect, to cause the 22 | direction or management of such entity, whether by contract or 23 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 24 | outstanding shares, or (iii) beneficial ownership of such entity. 25 | 26 | "You" (or "Your") shall mean an individual or Legal Entity 27 | exercising permissions granted by this License. 28 | 29 | "Source" form shall mean the preferred form for making modifications, 30 | including but not limited to software source code, documentation 31 | source, and configuration files. 32 | 33 | "Object" form shall mean any form resulting from mechanical 34 | transformation or translation of a Source form, including but 35 | not limited to compiled object code, generated documentation, 36 | and conversions to other media types. 37 | 38 | "Work" shall mean the work of authorship, whether in Source or 39 | Object form, made available under the License, as indicated by a 40 | copyright notice that is included in or attached to the work 41 | (an example is provided in the Appendix below). 42 | 43 | "Derivative Works" shall mean any work, whether in Source or Object 44 | form, that is based on (or derived from) the Work and for which the 45 | editorial revisions, annotations, elaborations, or other modifications 46 | represent, as a whole, an original work of authorship. For the purposes 47 | of this License, Derivative Works shall not include works that remain 48 | separable from, or merely link (or bind by name) to the interfaces of, 49 | the Work and Derivative Works thereof. 50 | 51 | "Contribution" shall mean any work of authorship, including 52 | the original version of the Work and any modifications or additions 53 | to that Work or Derivative Works thereof, that is intentionally 54 | submitted to Licensor for inclusion in the Work by the copyright owner 55 | or by an individual or Legal Entity authorized to submit on behalf of 56 | the copyright owner. For the purposes of this definition, "submitted" 57 | means any form of electronic, verbal, or written communication sent 58 | to the Licensor or its representatives, including but not limited to 59 | communication on electronic mailing lists, source code control systems, 60 | and issue tracking systems that are managed by, or on behalf of, the 61 | Licensor for the purpose of discussing and improving the Work, but 62 | excluding communication that is conspicuously marked or otherwise 63 | designated in writing by the copyright owner as "Not a Contribution." 64 | 65 | "Contributor" shall mean Licensor and any individual or Legal Entity 66 | on behalf of whom a Contribution has been received by Licensor and 67 | subsequently incorporated within the Work. 68 | 69 | 2. Grant of Copyright License. Subject to the terms and conditions of 70 | this License, each Contributor hereby grants to You a perpetual, 71 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 72 | copyright license to reproduce, prepare Derivative Works of, 73 | publicly display, publicly perform, sublicense, and distribute the 74 | Work and such Derivative Works in Source or Object form. 75 | 76 | 3. Grant of Patent License. Subject to the terms and conditions of 77 | this License, each Contributor hereby grants to You a perpetual, 78 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 79 | (except as stated in this section) patent license to make, have made, 80 | use, offer to sell, sell, import, and otherwise transfer the Work, 81 | where such license applies only to those patent claims licensable 82 | by such Contributor that are necessarily infringed by their 83 | Contribution(s) alone or by combination of their Contribution(s) 84 | with the Work to which such Contribution(s) was submitted. If You 85 | institute patent litigation against any entity (including a 86 | cross-claim or counterclaim in a lawsuit) alleging that the Work 87 | or a Contribution incorporated within the Work constitutes direct 88 | or contributory patent infringement, then any patent licenses 89 | granted to You under this License for that Work shall terminate 90 | as of the date such litigation is filed. 91 | 92 | 4. Redistribution. You may reproduce and distribute copies of the 93 | Work or Derivative Works thereof in any medium, with or without 94 | modifications, and in Source or Object form, provided that You 95 | meet the following conditions: 96 | 97 | (a) You must give any other recipients of the Work or 98 | Derivative Works a copy of this License; and 99 | 100 | (b) You must cause any modified files to carry prominent notices 101 | stating that You changed the files; and 102 | 103 | (c) You must retain, in the Source form of any Derivative Works 104 | that You distribute, all copyright, patent, trademark, and 105 | attribution notices from the Source form of the Work, 106 | excluding those notices that do not pertain to any part of 107 | the Derivative Works; and 108 | 109 | (d) If the Work includes a "NOTICE" text file as part of its 110 | distribution, then any Derivative Works that You distribute must 111 | include a readable copy of the attribution notices contained 112 | within such NOTICE file, excluding those notices that do not 113 | pertain to any part of the Derivative Works, in at least one 114 | of the following places: within a NOTICE text file distributed 115 | as part of the Derivative Works; within the Source form or 116 | documentation, if provided along with the Derivative Works; or, 117 | within a display generated by the Derivative Works, if and 118 | wherever such third-party notices normally appear. The contents 119 | of the NOTICE file are for informational purposes only and 120 | do not modify the License. You may add Your own attribution 121 | notices within Derivative Works that You distribute, alongside 122 | or as an addendum to the NOTICE text from the Work, provided 123 | that such additional attribution notices cannot be construed 124 | as modifying the License. 125 | 126 | You may add Your own copyright statement to Your modifications and 127 | may provide additional or different license terms and conditions 128 | for use, reproduction, or distribution of Your modifications, or 129 | for any such Derivative Works as a whole, provided Your use, 130 | reproduction, and distribution of the Work otherwise complies with 131 | the conditions stated in this License. 132 | 133 | 5. Submission of Contributions. Unless You explicitly state otherwise, 134 | any Contribution intentionally submitted for inclusion in the Work 135 | by You to the Licensor shall be under the terms and conditions of 136 | this License, without any additional terms or conditions. 137 | Notwithstanding the above, nothing herein shall supersede or modify 138 | the terms of any separate license agreement you may have executed 139 | with Licensor regarding such Contributions. 140 | 141 | 6. Trademarks. This License does not grant permission to use the trade 142 | names, trademarks, service marks, or product names of the Licensor, 143 | except as required for reasonable and customary use in describing the 144 | origin of the Work and reproducing the content of the NOTICE file. 145 | 146 | 7. Disclaimer of Warranty. Unless required by applicable law or 147 | agreed to in writing, Licensor provides the Work (and each 148 | Contributor provides its Contributions) on an "AS IS" BASIS, 149 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 150 | implied, including, without limitation, any warranties or conditions 151 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 152 | PARTICULAR PURPOSE. You are solely responsible for determining the 153 | appropriateness of using or redistributing the Work and assume any 154 | risks associated with Your exercise of permissions under this License. 155 | 156 | 8. Limitation of Liability. In no event and under no legal theory, 157 | whether in tort (including negligence), contract, or otherwise, 158 | unless required by applicable law (such as deliberate and grossly 159 | negligent acts) or agreed to in writing, shall any Contributor be 160 | liable to You for damages, including any direct, indirect, special, 161 | incidental, or consequential damages of any character arising as a 162 | result of this License or out of the use or inability to use the 163 | Work (including but not limited to damages for loss of goodwill, 164 | work stoppage, computer failure or malfunction, or any and all 165 | other commercial damages or losses), even if such Contributor 166 | has been advised of the possibility of such damages. 167 | 168 | 9. Accepting Warranty or Additional Liability. While redistributing 169 | the Work or Derivative Works thereof, You may choose to offer, 170 | and charge a fee for, acceptance of support, warranty, indemnity, 171 | or other liability obligations and/or rights consistent with this 172 | License. However, in accepting such obligations, You may act only 173 | on Your own behalf and on Your sole responsibility, not on behalf 174 | of any other Contributor, and only if You agree to indemnify, 175 | defend, and hold each Contributor harmless for any liability 176 | incurred by, or claims asserted against, such Contributor by reason 177 | of your accepting any such warranty or additional liability. 178 | 179 | END OF TERMS AND CONDITIONS 180 | 181 | APPENDIX: How to apply the Apache License to your work. 182 | 183 | To apply the Apache License to your work, attach the following 184 | boilerplate notice, with the fields enclosed by brackets "[]" 185 | replaced with your own identifying information. (Don't include 186 | the brackets!) The text should be enclosed in the appropriate 187 | comment syntax for the file format. We also recommend that a 188 | file or class name and description of purpose be included on the 189 | same "printed page" as the copyright notice for easier 190 | identification within third-party archives. 191 | 192 | Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 193 | 194 | Licensed under the Apache License, Version 2.0 (the "License"); 195 | you may not use this file except in compliance with the License. 196 | You may obtain a copy of the License at 197 | 198 | http://www.apache.org/licenses/LICENSE-2.0 199 | 200 | Unless required by applicable law or agreed to in writing, software 201 | distributed under the License is distributed on an "AS IS" BASIS, 202 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 203 | See the License for the specific language governing permissions and 204 | limitations under the License. 205 | * For aws-sdk-js see also this required NOTICE: 206 | AWS SDK for JavaScript 207 | Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 208 | 209 | This product includes software developed at 210 | Amazon Web Services, Inc. (http://aws.amazon.com/). 211 | 212 | ------ 213 | 214 | ** bootstrap-icons; version 1.11.3 -- https://icons.getbootstrap.com/ 215 | Copyright (c) 2019-2024 The Bootstrap Authors 216 | ** bootstrap; version 5.3.4 -- https://getbootstrap.com/ 217 | Copyright (c) 2011-2023 The Bootstrap Authors 218 | 219 | The MIT License (MIT) 220 | 221 | Permission is hereby granted, free of charge, to any person obtaining a copy 222 | of this software and associated documentation files (the "Software"), to deal 223 | in the Software without restriction, including without limitation the rights 224 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 225 | copies of the Software, and to permit persons to whom the Software is 226 | furnished to do so, subject to the following conditions: 227 | 228 | The above copyright notice and this permission notice shall be included in 229 | all copies or substantial portions of the Software. 230 | 231 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 232 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 233 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 234 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 235 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 236 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 237 | THE SOFTWARE. 238 | 239 | ------ 240 | 241 | ** jquery; version 3.7.1 -- https://jquery.com/ 242 | Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 243 | ** luxon; version 3.4.4 -- https://moment.github.io/luxon/ 244 | Copyright 2019 JS Foundation and other contributors 245 | 246 | Permission is hereby granted, free of charge, to any person obtaining 247 | a copy of this software and associated documentation files (the 248 | "Software"), to deal in the Software without restriction, including 249 | without limitation the rights to use, copy, modify, merge, publish, 250 | distribute, sublicense, and/or sell copies of the Software, and to 251 | permit persons to whom the Software is furnished to do so, subject to 252 | the following conditions: 253 | 254 | The above copyright notice and this permission notice shall be 255 | included in all copies or substantial portions of the Software. 256 | 257 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 258 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 259 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 260 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 261 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 262 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 263 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 264 | -------------------------------------------------------------------------------- /docs/arch-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/public-file-browser-for-amazon-s3/9fc848a72c6f85a7cdbde7d85340c2e1bf2f5b8c/docs/arch-overview.png -------------------------------------------------------------------------------- /docs/home-screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/public-file-browser-for-amazon-s3/9fc848a72c6f85a7cdbde7d85340c2e1bf2f5b8c/docs/home-screenshot-1.png -------------------------------------------------------------------------------- /docs/home-screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/public-file-browser-for-amazon-s3/9fc848a72c6f85a7cdbde7d85340c2e1bf2f5b8c/docs/home-screenshot-2.png -------------------------------------------------------------------------------- /docs/home-screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/public-file-browser-for-amazon-s3/9fc848a72c6f85a7cdbde7d85340c2e1bf2f5b8c/docs/home-screenshot-3.png -------------------------------------------------------------------------------- /sam/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### OSX ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | ### PyCharm ### 47 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 48 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 49 | 50 | # User-specific stuff: 51 | .idea/**/workspace.xml 52 | .idea/**/tasks.xml 53 | .idea/dictionaries 54 | 55 | # Sensitive or high-churn files: 56 | .idea/**/dataSources/ 57 | .idea/**/dataSources.ids 58 | .idea/**/dataSources.xml 59 | .idea/**/dataSources.local.xml 60 | .idea/**/sqlDataSources.xml 61 | .idea/**/dynamic.xml 62 | .idea/**/uiDesigner.xml 63 | 64 | # Gradle: 65 | .idea/**/gradle.xml 66 | .idea/**/libraries 67 | 68 | # CMake 69 | cmake-build-debug/ 70 | 71 | # Mongo Explorer plugin: 72 | .idea/**/mongoSettings.xml 73 | 74 | ## File-based project format: 75 | *.iws 76 | 77 | ## Plugin-specific files: 78 | 79 | # IntelliJ 80 | /out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Cursive Clojure plugin 89 | .idea/replstate.xml 90 | 91 | # Ruby plugin and RubyMine 92 | /.rakeTasks 93 | 94 | # Crashlytics plugin (for Android Studio and IntelliJ) 95 | com_crashlytics_export_strings.xml 96 | crashlytics.properties 97 | crashlytics-build.properties 98 | fabric.properties 99 | 100 | ### PyCharm Patch ### 101 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 102 | 103 | # *.iml 104 | # modules.xml 105 | # .idea/misc.xml 106 | # *.ipr 107 | 108 | # Sonarlint plugin 109 | .idea/sonarlint 110 | 111 | ### Python ### 112 | # Byte-compiled / optimized / DLL files 113 | __pycache__/ 114 | *.py[cod] 115 | *$py.class 116 | 117 | # C extensions 118 | *.so 119 | 120 | # Distribution / packaging 121 | .Python 122 | build/ 123 | develop-eggs/ 124 | dist/ 125 | downloads/ 126 | eggs/ 127 | .eggs/ 128 | lib/ 129 | lib64/ 130 | parts/ 131 | sdist/ 132 | var/ 133 | wheels/ 134 | *.egg-info/ 135 | .installed.cfg 136 | *.egg 137 | 138 | # PyInstaller 139 | # Usually these files are written by a python script from a template 140 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 141 | *.manifest 142 | *.spec 143 | 144 | # Installer logs 145 | pip-log.txt 146 | pip-delete-this-directory.txt 147 | 148 | # Unit test / coverage reports 149 | htmlcov/ 150 | .tox/ 151 | .coverage 152 | .coverage.* 153 | .cache 154 | .pytest_cache/ 155 | nosetests.xml 156 | coverage.xml 157 | *.cover 158 | .hypothesis/ 159 | 160 | # Translations 161 | *.mo 162 | *.pot 163 | 164 | # Flask stuff: 165 | instance/ 166 | .webassets-cache 167 | 168 | # Scrapy stuff: 169 | .scrapy 170 | 171 | # Sphinx documentation 172 | docs/_build/ 173 | 174 | # PyBuilder 175 | target/ 176 | 177 | # Jupyter Notebook 178 | .ipynb_checkpoints 179 | 180 | # pyenv 181 | .python-version 182 | 183 | # celery beat schedule file 184 | celerybeat-schedule.* 185 | 186 | # SageMath parsed files 187 | *.sage.py 188 | 189 | # Environments 190 | .env 191 | .venv 192 | env/ 193 | venv/ 194 | ENV/ 195 | env.bak/ 196 | venv.bak/ 197 | 198 | # Spyder project settings 199 | .spyderproject 200 | .spyproject 201 | 202 | # Rope project settings 203 | .ropeproject 204 | 205 | # mkdocs documentation 206 | /site 207 | 208 | # mypy 209 | .mypy_cache/ 210 | 211 | ### VisualStudioCode ### 212 | .vscode/* 213 | !.vscode/settings.json 214 | !.vscode/tasks.json 215 | !.vscode/launch.json 216 | !.vscode/extensions.json 217 | .history 218 | 219 | ### Windows ### 220 | # Windows thumbnail cache files 221 | Thumbs.db 222 | ehthumbs.db 223 | ehthumbs_vista.db 224 | 225 | # Folder config file 226 | Desktop.ini 227 | 228 | # Recycle Bin used on file shares 229 | $RECYCLE.BIN/ 230 | 231 | # Windows Installer files 232 | *.cab 233 | *.msi 234 | *.msm 235 | *.msp 236 | 237 | # Windows shortcuts 238 | *.lnk 239 | 240 | # Build folder 241 | 242 | */build/* 243 | 244 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 245 | .aws-sam/* 246 | samconfig.toml 247 | -------------------------------------------------------------------------------- /sam/seed_s3_data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/public-file-browser-for-amazon-s3/9fc848a72c6f85a7cdbde7d85340c2e1bf2f5b8c/sam/seed_s3_data/__init__.py -------------------------------------------------------------------------------- /sam/seed_s3_data/app.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | import json 4 | import mimetypes 5 | import os 6 | import zipfile 7 | 8 | import boto3 9 | import simplejson as json 10 | from crhelper import CfnResource 11 | from loguru import logger 12 | 13 | helper = CfnResource() 14 | 15 | 16 | @helper.create 17 | def seed_data(event, _): 18 | logger.debug('Event: ' + json.dumps(event)) 19 | logger.debug(f"Retrieving S3 Public Website Contents...") 20 | s3 = boto3.client('s3') 21 | response = s3.list_objects_v2( 22 | Bucket=event['ResourceProperties']['PublicWebsiteBucket'], 23 | MaxKeys=1 24 | ) 25 | if response['KeyCount'] > 0: 26 | logger.debug(f"Public Website Bucket already has contents, skipping...") 27 | return 28 | with zipfile.ZipFile('website.zip', 'r') as zip_ref: 29 | zip_ref.extractall('/tmp/website/') # nosec hardcoded_tmp_directory 30 | path = '/tmp/website/website' # nosec hardcoded_tmp_directory 31 | # Replace placeholder config values with Lambda inputs 32 | for file_name in ['index.html', 'icon/site.webmanifest']: 33 | config_path = os.path.join(path, file_name) 34 | logger.debug(f"Modifying Website Config {config_path}...") 35 | with open(config_path, 'r') as file: 36 | config_data = file.read() 37 | config_data = config_data.replace('###REPLACE_ME_SITE_NAME###', event['ResourceProperties']['SiteName']) 38 | config_data = config_data.replace('###REPLACE_ME_IDENTITY_POOL_ID###', event['ResourceProperties']['IdentityPoolId']) 39 | config_data = config_data.replace('###REPLACE_ME_BUCKET_NAME###', event['ResourceProperties']['FilesBucketName']) 40 | if event['ResourceProperties']['FilesOpenMode'] == 'In New Tab': 41 | config_data = config_data.replace('###REPLACE_ME_FILES_OPEN_MODE###', 'true') 42 | elif event['ResourceProperties']['FilesOpenMode'] == 'In Same Tab': 43 | config_data = config_data.replace('###REPLACE_ME_FILES_OPEN_MODE###', 'false') 44 | config_data = config_data.replace('###REPLACE_ME_VISIBLE_STORAGE_CLASSES###', event['ResourceProperties']['VisibleStorageClasses']) 45 | with open(config_path, 'w') as file: 46 | file.write(config_data) 47 | # Upload the website data 48 | logger.debug(f"Uploading Website Data...") 49 | for subdir, dirs, files in os.walk(path): 50 | for file in files: 51 | full_path = os.path.join(subdir, file) 52 | with open(full_path, 'rb') as data: 53 | object_key = full_path[len(path) + 1:] 54 | logger.debug( 55 | f"Uploading: {full_path} -> s3://{event['ResourceProperties']['PublicWebsiteBucket']}/pfb_for_s3/{object_key}") 56 | s3.put_object( 57 | Bucket=event['ResourceProperties']['PublicWebsiteBucket'], 58 | Key='pfb_for_s3/' + object_key, 59 | Body=data, 60 | ContentType=mimetypes.guess_type(full_path)[0] or 'application/octet-stream' 61 | ) 62 | 63 | 64 | @helper.delete 65 | def delete_data(event, _): 66 | logger.debug('Event: ' + json.dumps(event)) 67 | s3 = boto3.client('s3') 68 | bucket_list = [ 69 | event['ResourceProperties']['PublicWebsiteBucket'] 70 | ] 71 | for bucket in bucket_list: 72 | logger.debug(f"Deleting S3 Bucket Contents: {bucket}") 73 | key_count = 1 74 | while key_count > 0: 75 | response = s3.list_objects_v2( 76 | Bucket=bucket, 77 | MaxKeys=1000 78 | ) 79 | key_count = response['KeyCount'] 80 | if key_count > 0: 81 | delete_dict = [{'Key': x['Key']} for x in response['Contents']] 82 | for key_obj in delete_dict: 83 | logger.debug( 84 | f"Queueing S3 Object Deletion: {key_obj['Key']}") 85 | s3.delete_objects( 86 | Bucket=bucket, 87 | Delete={'Objects': delete_dict} 88 | ) 89 | 90 | 91 | @helper.update 92 | def no_op(_, __): 93 | # No Operation 94 | pass 95 | 96 | 97 | def handler(event, context): 98 | helper(event, context) 99 | -------------------------------------------------------------------------------- /sam/seed_s3_data/poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "boto3" 5 | version = "1.34.48" 6 | description = "The AWS SDK for Python" 7 | optional = false 8 | python-versions = ">= 3.8" 9 | files = [ 10 | {file = "boto3-1.34.48-py3-none-any.whl", hash = "sha256:adc785ff05aec9fc93f82d507420b320203cd4fd011c67eb369b3aa2b5aeb298"}, 11 | {file = "boto3-1.34.48.tar.gz", hash = "sha256:f9873c3f03de546d7297475c6acd771840c385521caadb8c121a1ac38bc59cd4"}, 12 | ] 13 | 14 | [package.dependencies] 15 | botocore = ">=1.34.48,<1.35.0" 16 | jmespath = ">=0.7.1,<2.0.0" 17 | s3transfer = ">=0.10.0,<0.11.0" 18 | 19 | [package.extras] 20 | crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] 21 | 22 | [[package]] 23 | name = "botocore" 24 | version = "1.34.48" 25 | description = "Low-level, data-driven core of boto 3." 26 | optional = false 27 | python-versions = ">= 3.8" 28 | files = [ 29 | {file = "botocore-1.34.48-py3-none-any.whl", hash = "sha256:f3e1c84fa75fd6921dfbfb4b2f803bcc424b9b866982fe80e08edbd26ca9861c"}, 30 | {file = "botocore-1.34.48.tar.gz", hash = "sha256:eabdde36309274b76bb79ae9bdfa10c1fd91a2c9b3343cfa15b8a91f8e1ec224"}, 31 | ] 32 | 33 | [package.dependencies] 34 | jmespath = ">=0.7.1,<2.0.0" 35 | python-dateutil = ">=2.1,<3.0.0" 36 | urllib3 = {version = ">=1.25.4,<2.1", markers = "python_version >= \"3.10\""} 37 | 38 | [package.extras] 39 | crt = ["awscrt (==0.19.19)"] 40 | 41 | [[package]] 42 | name = "colorama" 43 | version = "0.4.6" 44 | description = "Cross-platform colored terminal text." 45 | optional = false 46 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 47 | files = [ 48 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 49 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 50 | ] 51 | 52 | [[package]] 53 | name = "crhelper" 54 | version = "2.0.11" 55 | description = "crhelper simplifies authoring CloudFormation Custom Resources" 56 | optional = false 57 | python-versions = "*" 58 | files = [ 59 | {file = "crhelper-2.0.11-py3-none-any.whl", hash = "sha256:0c1f703a830722379d205d58ca4f0da768c0b10670ddce46af31ba9661bf2d5a"}, 60 | {file = "crhelper-2.0.11.tar.gz", hash = "sha256:da9efe4fb57d86f0567fddc999ae1c242ea9602c95b165b09e00d435c3845ef0"}, 61 | ] 62 | 63 | [[package]] 64 | name = "jmespath" 65 | version = "1.0.1" 66 | description = "JSON Matching Expressions" 67 | optional = false 68 | python-versions = ">=3.7" 69 | files = [ 70 | {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, 71 | {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, 72 | ] 73 | 74 | [[package]] 75 | name = "loguru" 76 | version = "0.7.2" 77 | description = "Python logging made (stupidly) simple" 78 | optional = false 79 | python-versions = ">=3.5" 80 | files = [ 81 | {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, 82 | {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, 83 | ] 84 | 85 | [package.dependencies] 86 | colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} 87 | win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} 88 | 89 | [package.extras] 90 | dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] 91 | 92 | [[package]] 93 | name = "python-dateutil" 94 | version = "2.8.2" 95 | description = "Extensions to the standard Python datetime module" 96 | optional = false 97 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 98 | files = [ 99 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 100 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 101 | ] 102 | 103 | [package.dependencies] 104 | six = ">=1.5" 105 | 106 | [[package]] 107 | name = "s3transfer" 108 | version = "0.10.0" 109 | description = "An Amazon S3 Transfer Manager" 110 | optional = false 111 | python-versions = ">= 3.8" 112 | files = [ 113 | {file = "s3transfer-0.10.0-py3-none-any.whl", hash = "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e"}, 114 | {file = "s3transfer-0.10.0.tar.gz", hash = "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b"}, 115 | ] 116 | 117 | [package.dependencies] 118 | botocore = ">=1.33.2,<2.0a.0" 119 | 120 | [package.extras] 121 | crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] 122 | 123 | [[package]] 124 | name = "simplejson" 125 | version = "3.19.2" 126 | description = "Simple, fast, extensible JSON encoder/decoder for Python" 127 | optional = false 128 | python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*" 129 | files = [ 130 | {file = "simplejson-3.19.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3471e95110dcaf901db16063b2e40fb394f8a9e99b3fe9ee3acc6f6ef72183a2"}, 131 | {file = "simplejson-3.19.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3194cd0d2c959062b94094c0a9f8780ffd38417a5322450a0db0ca1a23e7fbd2"}, 132 | {file = "simplejson-3.19.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:8a390e56a7963e3946ff2049ee1eb218380e87c8a0e7608f7f8790ba19390867"}, 133 | {file = "simplejson-3.19.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1537b3dd62d8aae644f3518c407aa8469e3fd0f179cdf86c5992792713ed717a"}, 134 | {file = "simplejson-3.19.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a8617625369d2d03766413bff9e64310feafc9fc4f0ad2b902136f1a5cd8c6b0"}, 135 | {file = "simplejson-3.19.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:2c433a412e96afb9a3ce36fa96c8e61a757af53e9c9192c97392f72871e18e69"}, 136 | {file = "simplejson-3.19.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f1c70249b15e4ce1a7d5340c97670a95f305ca79f376887759b43bb33288c973"}, 137 | {file = "simplejson-3.19.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:287e39ba24e141b046812c880f4619d0ca9e617235d74abc27267194fc0c7835"}, 138 | {file = "simplejson-3.19.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6f0a0b41dd05eefab547576bed0cf066595f3b20b083956b1405a6f17d1be6ad"}, 139 | {file = "simplejson-3.19.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f98d918f7f3aaf4b91f2b08c0c92b1774aea113334f7cde4fe40e777114dbe6"}, 140 | {file = "simplejson-3.19.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d74beca677623481810c7052926365d5f07393c72cbf62d6cce29991b676402"}, 141 | {file = "simplejson-3.19.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7f2398361508c560d0bf1773af19e9fe644e218f2a814a02210ac2c97ad70db0"}, 142 | {file = "simplejson-3.19.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ad331349b0b9ca6da86064a3599c425c7a21cd41616e175ddba0866da32df48"}, 143 | {file = "simplejson-3.19.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:332c848f02d71a649272b3f1feccacb7e4f7e6de4a2e6dc70a32645326f3d428"}, 144 | {file = "simplejson-3.19.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25785d038281cd106c0d91a68b9930049b6464288cea59ba95b35ee37c2d23a5"}, 145 | {file = "simplejson-3.19.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18955c1da6fc39d957adfa346f75226246b6569e096ac9e40f67d102278c3bcb"}, 146 | {file = "simplejson-3.19.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:11cc3afd8160d44582543838b7e4f9aa5e97865322844b75d51bf4e0e413bb3e"}, 147 | {file = "simplejson-3.19.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b01fda3e95d07a6148702a641e5e293b6da7863f8bc9b967f62db9461330562c"}, 148 | {file = "simplejson-3.19.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:778331444917108fa8441f59af45886270d33ce8a23bfc4f9b192c0b2ecef1b3"}, 149 | {file = "simplejson-3.19.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9eb117db8d7ed733a7317c4215c35993b815bf6aeab67523f1f11e108c040672"}, 150 | {file = "simplejson-3.19.2-cp310-cp310-win32.whl", hash = "sha256:39b6d79f5cbfa3eb63a869639cfacf7c41d753c64f7801efc72692c1b2637ac7"}, 151 | {file = "simplejson-3.19.2-cp310-cp310-win_amd64.whl", hash = "sha256:5675e9d8eeef0aa06093c1ff898413ade042d73dc920a03e8cea2fb68f62445a"}, 152 | {file = "simplejson-3.19.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed628c1431100b0b65387419551e822987396bee3c088a15d68446d92f554e0c"}, 153 | {file = "simplejson-3.19.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:adcb3332979cbc941b8fff07181f06d2b608625edc0a4d8bc3ffc0be414ad0c4"}, 154 | {file = "simplejson-3.19.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:08889f2f597ae965284d7b52a5c3928653a9406d88c93e3161180f0abc2433ba"}, 155 | {file = "simplejson-3.19.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef7938a78447174e2616be223f496ddccdbf7854f7bf2ce716dbccd958cc7d13"}, 156 | {file = "simplejson-3.19.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a970a2e6d5281d56cacf3dc82081c95c1f4da5a559e52469287457811db6a79b"}, 157 | {file = "simplejson-3.19.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554313db34d63eac3b3f42986aa9efddd1a481169c12b7be1e7512edebff8eaf"}, 158 | {file = "simplejson-3.19.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d36081c0b1c12ea0ed62c202046dca11438bee48dd5240b7c8de8da62c620e9"}, 159 | {file = "simplejson-3.19.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a3cd18e03b0ee54ea4319cdcce48357719ea487b53f92a469ba8ca8e39df285e"}, 160 | {file = "simplejson-3.19.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:66e5dc13bfb17cd6ee764fc96ccafd6e405daa846a42baab81f4c60e15650414"}, 161 | {file = "simplejson-3.19.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:972a7833d4a1fcf7a711c939e315721a88b988553fc770a5b6a5a64bd6ebeba3"}, 162 | {file = "simplejson-3.19.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3e74355cb47e0cd399ead3477e29e2f50e1540952c22fb3504dda0184fc9819f"}, 163 | {file = "simplejson-3.19.2-cp311-cp311-win32.whl", hash = "sha256:1dd4f692304854352c3e396e9b5f0a9c9e666868dd0bdc784e2ac4c93092d87b"}, 164 | {file = "simplejson-3.19.2-cp311-cp311-win_amd64.whl", hash = "sha256:9300aee2a8b5992d0f4293d88deb59c218989833e3396c824b69ba330d04a589"}, 165 | {file = "simplejson-3.19.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b8d940fd28eb34a7084877747a60873956893e377f15a32ad445fe66c972c3b8"}, 166 | {file = "simplejson-3.19.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4969d974d9db826a2c07671273e6b27bc48e940738d768fa8f33b577f0978378"}, 167 | {file = "simplejson-3.19.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c594642d6b13d225e10df5c16ee15b3398e21a35ecd6aee824f107a625690374"}, 168 | {file = "simplejson-3.19.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2f5a398b5e77bb01b23d92872255e1bcb3c0c719a3be40b8df146570fe7781a"}, 169 | {file = "simplejson-3.19.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:176a1b524a3bd3314ed47029a86d02d5a95cc0bee15bd3063a1e1ec62b947de6"}, 170 | {file = "simplejson-3.19.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3c7363a8cb8c5238878ec96c5eb0fc5ca2cb11fc0c7d2379863d342c6ee367a"}, 171 | {file = "simplejson-3.19.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:346820ae96aa90c7d52653539a57766f10f33dd4be609206c001432b59ddf89f"}, 172 | {file = "simplejson-3.19.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de9a2792612ec6def556d1dc621fd6b2073aff015d64fba9f3e53349ad292734"}, 173 | {file = "simplejson-3.19.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1c768e7584c45094dca4b334af361e43b0aaa4844c04945ac7d43379eeda9bc2"}, 174 | {file = "simplejson-3.19.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:9652e59c022e62a5b58a6f9948b104e5bb96d3b06940c6482588176f40f4914b"}, 175 | {file = "simplejson-3.19.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9c1a4393242e321e344213a90a1e3bf35d2f624aa8b8f6174d43e3c6b0e8f6eb"}, 176 | {file = "simplejson-3.19.2-cp312-cp312-win32.whl", hash = "sha256:7cb98be113911cb0ad09e5523d0e2a926c09a465c9abb0784c9269efe4f95917"}, 177 | {file = "simplejson-3.19.2-cp312-cp312-win_amd64.whl", hash = "sha256:6779105d2fcb7fcf794a6a2a233787f6bbd4731227333a072d8513b252ed374f"}, 178 | {file = "simplejson-3.19.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:061e81ea2d62671fa9dea2c2bfbc1eec2617ae7651e366c7b4a2baf0a8c72cae"}, 179 | {file = "simplejson-3.19.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4280e460e51f86ad76dc456acdbfa9513bdf329556ffc8c49e0200878ca57816"}, 180 | {file = "simplejson-3.19.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11c39fbc4280d7420684494373b7c5904fa72a2b48ef543a56c2d412999c9e5d"}, 181 | {file = "simplejson-3.19.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bccb3e88ec26ffa90f72229f983d3a5d1155e41a1171190fa723d4135523585b"}, 182 | {file = "simplejson-3.19.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bb5b50dc6dd671eb46a605a3e2eb98deb4a9af787a08fcdddabe5d824bb9664"}, 183 | {file = "simplejson-3.19.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d94245caa3c61f760c4ce4953cfa76e7739b6f2cbfc94cc46fff6c050c2390c5"}, 184 | {file = "simplejson-3.19.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d0e5ffc763678d48ecc8da836f2ae2dd1b6eb2d27a48671066f91694e575173c"}, 185 | {file = "simplejson-3.19.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d222a9ed082cd9f38b58923775152003765016342a12f08f8c123bf893461f28"}, 186 | {file = "simplejson-3.19.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8434dcdd347459f9fd9c526117c01fe7ca7b016b6008dddc3c13471098f4f0dc"}, 187 | {file = "simplejson-3.19.2-cp36-cp36m-win32.whl", hash = "sha256:c9ac1c2678abf9270e7228133e5b77c6c3c930ad33a3c1dfbdd76ff2c33b7b50"}, 188 | {file = "simplejson-3.19.2-cp36-cp36m-win_amd64.whl", hash = "sha256:92c4a4a2b1f4846cd4364855cbac83efc48ff5a7d7c06ba014c792dd96483f6f"}, 189 | {file = "simplejson-3.19.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0d551dc931638e2102b8549836a1632e6e7cf620af3d093a7456aa642bff601d"}, 190 | {file = "simplejson-3.19.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73a8a4653f2e809049999d63530180d7b5a344b23a793502413ad1ecea9a0290"}, 191 | {file = "simplejson-3.19.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40847f617287a38623507d08cbcb75d51cf9d4f9551dd6321df40215128325a3"}, 192 | {file = "simplejson-3.19.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be893258d5b68dd3a8cba8deb35dc6411db844a9d35268a8d3793b9d9a256f80"}, 193 | {file = "simplejson-3.19.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9eb3cff1b7d71aa50c89a0536f469cb8d6dcdd585d8f14fb8500d822f3bdee4"}, 194 | {file = "simplejson-3.19.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d0f402e787e6e7ee7876c8b05e2fe6464820d9f35ba3f172e95b5f8b699f6c7f"}, 195 | {file = "simplejson-3.19.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fbbcc6b0639aa09b9649f36f1bcb347b19403fe44109948392fbb5ea69e48c3e"}, 196 | {file = "simplejson-3.19.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:2fc697be37585eded0c8581c4788fcfac0e3f84ca635b73a5bf360e28c8ea1a2"}, 197 | {file = "simplejson-3.19.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b0a3eb6dd39cce23801a50c01a0976971498da49bc8a0590ce311492b82c44b"}, 198 | {file = "simplejson-3.19.2-cp37-cp37m-win32.whl", hash = "sha256:49f9da0d6cd17b600a178439d7d2d57c5ef01f816b1e0e875e8e8b3b42db2693"}, 199 | {file = "simplejson-3.19.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c87c22bd6a987aca976e3d3e23806d17f65426191db36d40da4ae16a6a494cbc"}, 200 | {file = "simplejson-3.19.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9e4c166f743bb42c5fcc60760fb1c3623e8fda94f6619534217b083e08644b46"}, 201 | {file = "simplejson-3.19.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0a48679310e1dd5c9f03481799311a65d343748fe86850b7fb41df4e2c00c087"}, 202 | {file = "simplejson-3.19.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0521e0f07cb56415fdb3aae0bbd8701eb31a9dfef47bb57206075a0584ab2a2"}, 203 | {file = "simplejson-3.19.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d2d5119b1d7a1ed286b8af37357116072fc96700bce3bec5bb81b2e7057ab41"}, 204 | {file = "simplejson-3.19.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c1467d939932901a97ba4f979e8f2642415fcf02ea12f53a4e3206c9c03bc17"}, 205 | {file = "simplejson-3.19.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49aaf4546f6023c44d7e7136be84a03a4237f0b2b5fb2b17c3e3770a758fc1a0"}, 206 | {file = "simplejson-3.19.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60848ab779195b72382841fc3fa4f71698a98d9589b0a081a9399904487b5832"}, 207 | {file = "simplejson-3.19.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0436a70d8eb42bea4fe1a1c32d371d9bb3b62c637969cb33970ad624d5a3336a"}, 208 | {file = "simplejson-3.19.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:49e0e3faf3070abdf71a5c80a97c1afc059b4f45a5aa62de0c2ca0444b51669b"}, 209 | {file = "simplejson-3.19.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ff836cd4041e16003549449cc0a5e372f6b6f871eb89007ab0ee18fb2800fded"}, 210 | {file = "simplejson-3.19.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3848427b65e31bea2c11f521b6fc7a3145d6e501a1038529da2391aff5970f2f"}, 211 | {file = "simplejson-3.19.2-cp38-cp38-win32.whl", hash = "sha256:3f39bb1f6e620f3e158c8b2eaf1b3e3e54408baca96a02fe891794705e788637"}, 212 | {file = "simplejson-3.19.2-cp38-cp38-win_amd64.whl", hash = "sha256:0405984f3ec1d3f8777c4adc33eac7ab7a3e629f3b1c05fdded63acc7cf01137"}, 213 | {file = "simplejson-3.19.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:445a96543948c011a3a47c8e0f9d61e9785df2544ea5be5ab3bc2be4bd8a2565"}, 214 | {file = "simplejson-3.19.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4a8c3cc4f9dfc33220246760358c8265dad6e1104f25f0077bbca692d616d358"}, 215 | {file = "simplejson-3.19.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af9c7e6669c4d0ad7362f79cb2ab6784d71147503e62b57e3d95c4a0f222c01c"}, 216 | {file = "simplejson-3.19.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:064300a4ea17d1cd9ea1706aa0590dcb3be81112aac30233823ee494f02cb78a"}, 217 | {file = "simplejson-3.19.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9453419ea2ab9b21d925d0fd7e3a132a178a191881fab4169b6f96e118cc25bb"}, 218 | {file = "simplejson-3.19.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e038c615b3906df4c3be8db16b3e24821d26c55177638ea47b3f8f73615111c"}, 219 | {file = "simplejson-3.19.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16ca9c90da4b1f50f089e14485db8c20cbfff2d55424062791a7392b5a9b3ff9"}, 220 | {file = "simplejson-3.19.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1018bd0d70ce85f165185d2227c71e3b1e446186f9fa9f971b69eee223e1e3cd"}, 221 | {file = "simplejson-3.19.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e8dd53a8706b15bc0e34f00e6150fbefb35d2fd9235d095b4f83b3c5ed4fa11d"}, 222 | {file = "simplejson-3.19.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:2d022b14d7758bfb98405672953fe5c202ea8a9ccf9f6713c5bd0718eba286fd"}, 223 | {file = "simplejson-3.19.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:febffa5b1eda6622d44b245b0685aff6fb555ce0ed734e2d7b1c3acd018a2cff"}, 224 | {file = "simplejson-3.19.2-cp39-cp39-win32.whl", hash = "sha256:4edcd0bf70087b244ba77038db23cd98a1ace2f91b4a3ecef22036314d77ac23"}, 225 | {file = "simplejson-3.19.2-cp39-cp39-win_amd64.whl", hash = "sha256:aad7405c033d32c751d98d3a65801e2797ae77fac284a539f6c3a3e13005edc4"}, 226 | {file = "simplejson-3.19.2-py3-none-any.whl", hash = "sha256:bcedf4cae0d47839fee7de344f96b5694ca53c786f28b5f773d4f0b265a159eb"}, 227 | {file = "simplejson-3.19.2.tar.gz", hash = "sha256:9eb442a2442ce417801c912df68e1f6ccfcd41577ae7274953ab3ad24ef7d82c"}, 228 | ] 229 | 230 | [[package]] 231 | name = "six" 232 | version = "1.16.0" 233 | description = "Python 2 and 3 compatibility utilities" 234 | optional = false 235 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 236 | files = [ 237 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 238 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 239 | ] 240 | 241 | [[package]] 242 | name = "urllib3" 243 | version = "2.0.7" 244 | description = "HTTP library with thread-safe connection pooling, file post, and more." 245 | optional = false 246 | python-versions = ">=3.7" 247 | files = [ 248 | {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, 249 | {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, 250 | ] 251 | 252 | [package.extras] 253 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 254 | secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] 255 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 256 | zstd = ["zstandard (>=0.18.0)"] 257 | 258 | [[package]] 259 | name = "win32-setctime" 260 | version = "1.1.0" 261 | description = "A small Python utility to set file creation time on Windows" 262 | optional = false 263 | python-versions = ">=3.5" 264 | files = [ 265 | {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, 266 | {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, 267 | ] 268 | 269 | [package.extras] 270 | dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] 271 | 272 | [metadata] 273 | lock-version = "2.0" 274 | python-versions = "~3.11" 275 | content-hash = "7fbdacab996eb7c69779fb67fd2169f30e6ec90598d47b6795f47de604d064c4" 276 | -------------------------------------------------------------------------------- /sam/seed_s3_data/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "public_file_browser_for_amazon_s3_seed_ddb_data" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Caesar Kabalan "] 6 | license = "MIT-0" 7 | 8 | [tool.poetry.dependencies] 9 | python = "~3.11" 10 | boto3 = "^1.34.23" 11 | loguru = "^0.7.2" 12 | simplejson = "^3.19.2" 13 | crhelper = "^2.0.11" 14 | 15 | [tool.poetry.dev-dependencies] 16 | 17 | [build-system] 18 | requires = ["poetry-core>=1.0.0"] 19 | build-backend = "poetry.core.masonry.api" 20 | -------------------------------------------------------------------------------- /sam/seed_s3_data/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.34.48 ; python_version >= "3.11" and python_version < "3.12" 2 | botocore==1.34.48 ; python_version >= "3.11" and python_version < "3.12" 3 | colorama==0.4.6 ; python_version >= "3.11" and python_version < "3.12" and sys_platform == "win32" 4 | crhelper==2.0.11 ; python_version >= "3.11" and python_version < "3.12" 5 | jmespath==1.0.1 ; python_version >= "3.11" and python_version < "3.12" 6 | loguru==0.7.2 ; python_version >= "3.11" and python_version < "3.12" 7 | python-dateutil==2.8.2 ; python_version >= "3.11" and python_version < "3.12" 8 | s3transfer==0.10.0 ; python_version >= "3.11" and python_version < "3.12" 9 | simplejson==3.19.2 ; python_version >= "3.11" and python_version < "3.12" 10 | six==1.16.0 ; python_version >= "3.11" and python_version < "3.12" 11 | urllib3==2.0.7 ; python_version >= "3.11" and python_version < "3.12" 12 | win32-setctime==1.1.0 ; python_version >= "3.11" and python_version < "3.12" and sys_platform == "win32" 13 | -------------------------------------------------------------------------------- /sam/seed_s3_data/website.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/public-file-browser-for-amazon-s3/9fc848a72c6f85a7cdbde7d85340c2e1bf2f5b8c/sam/seed_s3_data/website.zip -------------------------------------------------------------------------------- /sam/template.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | --- 4 | AWSTemplateFormatVersion: '2010-09-09' 5 | Description: > 6 | Public File Browser for Amazon S3 7 | **WARNING** This template creates resources which incur charges. You will be billed for the AWS resources used if you create a stack from this template. 8 | Parameters: 9 | SiteName: 10 | Type: String 11 | Description: Friendly Site Name displayed in the header and title of the website. 12 | Default: AnyCompany Public Files 13 | FilesOpenTabMode: 14 | Type: String 15 | Description: How do files open when clicked 16 | AllowedValues: 17 | - In New Tab 18 | - In Same Tab 19 | Default: In New Tab 20 | VisibleStorageClasses: 21 | Type: String 22 | Description: Comma-delimited list of storage classes to SHOW in directory listings. See S3 GetObject API Reference for possible values. 23 | Default: STANDARD,STANDARD_IA,ONEZONE_IA,REDUCED_REDUNDANCY 24 | CrossOriginRestriction: 25 | Type: String 26 | Description: First deployment set to "*", all subsequent deployments set to value of the FileBrowserURL CloudFormation output 27 | Default: '*' 28 | Transform: AWS::Serverless-2016-10-31 29 | Resources: 30 | CloudFrontOriginAccessControl: 31 | Type: AWS::CloudFront::OriginAccessControl 32 | Properties: 33 | OriginAccessControlConfig: 34 | Description: Public File Browser for Amazon S3 35 | Name: !Sub 36 | - public-file-browser-${Unique} 37 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 38 | OriginAccessControlOriginType: s3 39 | SigningBehavior: always 40 | SigningProtocol: sigv4 41 | PublicWebsiteBucket: 42 | Type: AWS::S3::Bucket 43 | Metadata: 44 | cdk_nag: 45 | rules_to_suppress: 46 | - id: AwsSolutions-S10 47 | reason: aws:SecureTransport policy is properly implemented in the Bucket Policy 48 | Properties: 49 | BucketName: !Sub 50 | - public-file-browser-website-${Unique} 51 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 52 | AccessControl: Private 53 | LoggingConfiguration: 54 | DestinationBucketName: !Ref LoggingBucket 55 | LogFilePrefix: s3-website/ 56 | PublicAccessBlockConfiguration: 57 | BlockPublicAcls: true 58 | BlockPublicPolicy: true 59 | IgnorePublicAcls: true 60 | RestrictPublicBuckets: true 61 | BucketEncryption: 62 | ServerSideEncryptionConfiguration: 63 | - ServerSideEncryptionByDefault: 64 | SSEAlgorithm: AES256 65 | VersioningConfiguration: 66 | Status: Enabled 67 | LifecycleConfiguration: 68 | Rules: 69 | - Id: DeleteOldVersionAfter90Days 70 | Status: Enabled 71 | NoncurrentVersionExpiration: 72 | NoncurrentDays: 90 73 | PublicFilesBucket: 74 | Type: AWS::S3::Bucket 75 | Metadata: 76 | cdk_nag: 77 | rules_to_suppress: 78 | - id: AwsSolutions-S10 79 | reason: aws:SecureTransport policy is properly implemented in the Bucket Policy 80 | Properties: 81 | BucketName: !Sub 82 | - public-file-browser-files-${Unique} 83 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 84 | AccessControl: Private 85 | LoggingConfiguration: 86 | DestinationBucketName: !Ref LoggingBucket 87 | LogFilePrefix: s3-files/ 88 | PublicAccessBlockConfiguration: 89 | BlockPublicAcls: true 90 | BlockPublicPolicy: true 91 | IgnorePublicAcls: true 92 | RestrictPublicBuckets: true 93 | BucketEncryption: 94 | ServerSideEncryptionConfiguration: 95 | - ServerSideEncryptionByDefault: 96 | SSEAlgorithm: AES256 97 | VersioningConfiguration: 98 | Status: Enabled 99 | LifecycleConfiguration: 100 | Rules: 101 | - Id: DeleteOldVersionAfter90Days 102 | Status: Enabled 103 | NoncurrentVersionExpiration: 104 | NoncurrentDays: 90 105 | CorsConfiguration: 106 | CorsRules: 107 | - AllowedHeaders: 108 | - '*' 109 | AllowedMethods: 110 | - HEAD 111 | - GET 112 | AllowedOrigins: 113 | # This is an unfortunate reality of circular dependencies, we get around this 114 | # by leaving the CORS configuration open until the user re-runs the deployment 115 | # with an updated phase. 116 | - !Ref CrossOriginRestriction 117 | ExposedHeaders: 118 | - ETag 119 | Id: CORSRule 120 | MaxAge: 3600 121 | PublicWebsiteBucketBucketPolicy: 122 | Type: AWS::S3::BucketPolicy 123 | Metadata: 124 | cdk_nag: 125 | rules_to_suppress: 126 | - id: AwsSolutions-S10 127 | reason: aws:SecureTransport policy is properly implemented in the Bucket Policy 128 | DependsOn: PublicWebsiteBucket 129 | Properties: 130 | Bucket: !Sub 131 | - public-file-browser-website-${Unique} 132 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 133 | PolicyDocument: 134 | Version: '2012-10-17' 135 | Id: WebAccess 136 | Statement: 137 | - Sid: CloudFrontReadForGetBucketObjects 138 | Principal: 139 | Service: cloudfront.amazonaws.com 140 | Effect: Allow 141 | Action: 142 | - s3:GetObject 143 | - s3:GetObjectVersion 144 | Resource: !Sub 145 | - arn:${AWS::Partition}:s3:::public-file-browser-website-${Unique}/* 146 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 147 | Condition: 148 | StringEquals: 149 | AWS:SourceArn: !Sub arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${CloudFront} 150 | - Sid: DenyPlaintextAccess 151 | Principal: '*' 152 | Effect: Deny 153 | Action: s3:* 154 | Resource: 155 | - !Sub 156 | - arn:${AWS::Partition}:s3:::public-file-browser-website-${Unique} 157 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 158 | - !Sub 159 | - arn:${AWS::Partition}:s3:::public-file-browser-website-${Unique}/* 160 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 161 | Condition: 162 | Bool: 163 | aws:SecureTransport: 'false' 164 | PublicFilesBucketBucketPolicy: 165 | Type: AWS::S3::BucketPolicy 166 | Metadata: 167 | cdk_nag: 168 | rules_to_suppress: 169 | - id: AwsSolutions-S10 170 | reason: aws:SecureTransport policy is properly implemented in the Bucket Policy 171 | DependsOn: PublicFilesBucket 172 | Properties: 173 | Bucket: !Sub 174 | - public-file-browser-files-${Unique} 175 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 176 | PolicyDocument: 177 | Version: '2012-10-17' 178 | Id: WebAccess 179 | Statement: 180 | - Sid: CloudFrontReadForGetBucketObjects 181 | Principal: 182 | Service: cloudfront.amazonaws.com 183 | Effect: Allow 184 | Action: 185 | - s3:GetObject 186 | - s3:GetObjectVersion 187 | Resource: !Sub 188 | - arn:${AWS::Partition}:s3:::public-file-browser-files-${Unique}/* 189 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 190 | Condition: 191 | StringEquals: 192 | AWS:SourceArn: !Sub arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${CloudFront} 193 | - Sid: DenyPlaintextAccess 194 | Principal: '*' 195 | Effect: Deny 196 | Action: s3:* 197 | Resource: 198 | - !Sub 199 | - arn:${AWS::Partition}:s3:::public-file-browser-files-${Unique} 200 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 201 | - !Sub 202 | - arn:${AWS::Partition}:s3:::public-file-browser-files-${Unique}/* 203 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 204 | Condition: 205 | Bool: 206 | aws:SecureTransport: 'false' 207 | LoggingBucket: 208 | Type: AWS::S3::Bucket 209 | DeletionPolicy: Retain 210 | Metadata: 211 | cfn_nag: 212 | rules_to_suppress: 213 | - id: W35 214 | reason: Unnecessary to log access to the logging bucket. 215 | cdk_nag: 216 | rules_to_suppress: 217 | - id: AwsSolutions-S10 218 | reason: aws:SecureTransport policy is properly implemented in the Bucket Policy 219 | checkov: 220 | skip: 221 | - id: CKV_AWS_18 222 | comment: Unnecessary to log access to the logging bucket. 223 | Properties: 224 | BucketName: !Sub 225 | - public-file-browser-logging-${Unique} 226 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 227 | AccessControl: Private 228 | PublicAccessBlockConfiguration: 229 | BlockPublicAcls: true 230 | BlockPublicPolicy: true 231 | IgnorePublicAcls: true 232 | RestrictPublicBuckets: true 233 | OwnershipControls: 234 | Rules: 235 | - ObjectOwnership: BucketOwnerPreferred 236 | BucketEncryption: 237 | ServerSideEncryptionConfiguration: 238 | - ServerSideEncryptionByDefault: 239 | SSEAlgorithm: AES256 240 | VersioningConfiguration: 241 | Status: Enabled 242 | LifecycleConfiguration: 243 | Rules: 244 | - Id: DeleteOldVersionAfter90Days 245 | Status: Enabled 246 | NoncurrentVersionExpiration: 247 | NoncurrentDays: 90 248 | LoggingBucketBucketPolicy: 249 | Type: AWS::S3::BucketPolicy 250 | Metadata: 251 | cdk_nag: 252 | rules_to_suppress: 253 | - id: AwsSolutions-S10 254 | reason: aws:SecureTransport policy is properly implemented in the Bucket Policy 255 | DependsOn: LoggingBucket 256 | Properties: 257 | Bucket: !Sub 258 | - public-file-browser-logging-${Unique} 259 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 260 | PolicyDocument: 261 | Version: '2012-10-17' 262 | Id: WebAccess 263 | Statement: 264 | - Sid: S3ServerAccessLogsPolicy 265 | Effect: Allow 266 | Principal: 267 | Service: logging.s3.amazonaws.com 268 | Action: 269 | - s3:PutObject 270 | Resource: !Sub 271 | - arn:${AWS::Partition}:s3:::public-file-browser-logging-${Unique}/* 272 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 273 | Condition: 274 | ArnEquals: 275 | aws:SourceArn: 276 | - !Sub 277 | - arn:${AWS::Partition}:s3:::public-file-browser-website-${Unique} 278 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 279 | - !Sub 280 | - arn:${AWS::Partition}:s3:::public-file-browser-files-${Unique} 281 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 282 | StringEquals: 283 | aws:SourceAccount: !Ref 'AWS::AccountId' 284 | - Sid: DenyPlaintextAccess 285 | Principal: '*' 286 | Effect: Deny 287 | Action: s3:* 288 | Resource: 289 | - !Sub 290 | - arn:${AWS::Partition}:s3:::public-file-browser-logging-${Unique} 291 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 292 | - !Sub 293 | - arn:${AWS::Partition}:s3:::public-file-browser-logging-${Unique}/* 294 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 295 | Condition: 296 | Bool: 297 | aws:SecureTransport: 'false' 298 | CloudFront: 299 | Type: AWS::CloudFront::Distribution 300 | DependsOn: 301 | - PublicFilesBucket 302 | - PublicWebsiteBucket 303 | Metadata: 304 | cfn_nag: 305 | rules_to_suppress: 306 | - id: W70 307 | reason: When using the default CloudFront distribution domain you cannot specify the TLS version options. Changing from the default CloudFront distribution would significantly increase the scope and cost of the project by requiring the customer to select a custom domain, deploy a Route53 Hosted Zone, and generate an ACM Certificate. 308 | cdk_nag: 309 | rules_to_suppress: 310 | - id: AwsSolutions-CFR1 311 | reason: Implementing Geo-restrictions is not a valid assumption we can make for this solution. 312 | - id: AwsSolutions-CFR2 313 | reason: aws:SecureTransport policy is properly implemented in the Bucket Policy 314 | - id: AwsSolutions-CFR4 315 | reason: When using the default CloudFront distribution domain you cannot specify the TLS version options. Changing from the default CloudFront distribution would significantly increase the scope and cost of the project by requiring the customer to select a custom domain, deploy a Route53 Hosted Zone, and generate an ACM Certificate. 316 | checkov: 317 | skip: 318 | - id: CKV_AWS_68 319 | comment: WAF is not appropriate for this workload as it is a static website and would greatly increase the cost and complexity of the solution. 320 | - id: CKV_AWS_174 321 | comment: When using the default CloudFront distribution domain you cannot specify the TLS version options. Changing from the default CloudFront distribution would significantly increase the scope and cost of the project by requiring the customer to select a custom domain, deploy a Route53 Hosted Zone, and generate an ACM Certificate. 322 | Properties: 323 | DistributionConfig: 324 | Comment: Public File Browser for Amazon S3 Static Website 325 | # Uncomment the following if you want to restrict access to the US. 326 | # Also consider changing the PriceClass option 327 | #Restrictions: 328 | # GeoRestriction: 329 | # Locations: 330 | # - US 331 | # RestrictionType: whitelist 332 | Logging: 333 | Bucket: !GetAtt LoggingBucket.DomainName 334 | Prefix: cloudfront 335 | IncludeCookies: true 336 | DefaultCacheBehavior: 337 | ForwardedValues: 338 | QueryString: true 339 | TargetOriginId: !Sub 'S3-Public-Files' 340 | ViewerProtocolPolicy: redirect-to-https 341 | Compress: true 342 | # Magic number: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html#managed-cache-policies-list 343 | CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 344 | DefaultRootObject: pfb_for_s3/index.html 345 | CacheBehaviors: 346 | - ForwardedValues: 347 | QueryString: true 348 | TargetOriginId: !Sub 'S3-Public-Website' 349 | ViewerProtocolPolicy: redirect-to-https 350 | Compress: true 351 | PathPattern: pfb_for_s3/* 352 | # Magic number: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html#managed-cache-policies-list 353 | CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 354 | Enabled: true 355 | HttpVersion: http2 356 | Origins: 357 | - DomainName: !Sub 358 | - public-file-browser-website-${Unique}.s3.${AWS::Region}.amazonaws.com 359 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 360 | Id: !Sub 'S3-Public-Website' 361 | S3OriginConfig: 362 | OriginAccessIdentity: '' 363 | OriginAccessControlId: !GetAtt CloudFrontOriginAccessControl.Id 364 | - DomainName: !Sub 365 | - public-file-browser-files-${Unique}.s3.${AWS::Region}.amazonaws.com 366 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 367 | Id: !Sub 'S3-Public-Files' 368 | S3OriginConfig: 369 | OriginAccessIdentity: '' 370 | OriginAccessControlId: !GetAtt CloudFrontOriginAccessControl.Id 371 | PriceClass: PriceClass_100 372 | IPV6Enabled: false 373 | CustomSeedS3Data: 374 | # Logging disabled on this function 375 | Type: AWS::Serverless::Function 376 | Metadata: 377 | cfn_nag: 378 | rules_to_suppress: 379 | - id: W89 380 | reason: This does not increase the security of the solutions and greatly increases the cost and scope of the deployment. 381 | - id: W92 382 | reason: This is not necessary for this function as it is only executed during CloudFormation deployment and not related to end-user requests. 383 | checkov: 384 | skip: 385 | - id: CKV_AWS_173 386 | comment: Environment variables do not contain sensitive data. 387 | - id: CKV_AWS_117 388 | comment: This does not increase the security of the solutions and greatly increases the cost and scope of the deployment. 389 | - id: CKV_AWS_115 390 | comment: Concurrency limit does not make sense for a Lambda Backed Custom Resource only used during stack deployment 391 | - id: CKV_AWS_116 392 | comment: DLQ does not make sense for a Lambda Backed Custom Resource only used during stack deployment 393 | Properties: 394 | CodeUri: seed_s3_data/ 395 | Handler: app.handler 396 | Runtime: python3.11 397 | Timeout: 30 398 | Architectures: 399 | - arm64 400 | Environment: 401 | Variables: 402 | LOGURU_LEVEL: 'DEBUG' 403 | Policies: 404 | # AWSLambdaBasicExecutionRole intentionally omitted. If allowed, it will create 405 | # a new log group during the deletion of the stack which cannot be cleaned up 406 | # automatically. No way to prevent that or specify an order, so we choose to not 407 | # allow logging at all. 408 | - S3CrudPolicy: 409 | BucketName: !Sub 410 | - public-file-browser-website-${Unique} 411 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 412 | SeedS3Data: 413 | Type: Custom::SeedS3Data 414 | Properties: 415 | ServiceToken: !GetAtt 'CustomSeedS3Data.Arn' 416 | SiteName: !Ref 'SiteName' 417 | IdentityPoolId: !Ref 'CognitoIdentityPool' 418 | PublicWebsiteBucket: !Ref PublicWebsiteBucket 419 | FilesBucketName: !Ref PublicFilesBucket 420 | FilesOpenMode: !Ref FilesOpenTabMode 421 | VisibleStorageClasses: !Ref VisibleStorageClasses 422 | CognitoIdentityPool: 423 | Type: AWS::Cognito::IdentityPool 424 | Metadata: 425 | cdk_nag: 426 | rules_to_suppress: 427 | - id: AwsSolutions-COG7 428 | reason: Unauthenticated identities are preferred for this application so users can anonymously but securely list contents of the S3 bucket. 429 | cfn_nag: 430 | rules_to_suppress: 431 | - id: W57 432 | reason: Unauthenticated identities are preferred for this application so users can anonymously but securely list contents of the S3 bucket. 433 | Properties: 434 | IdentityPoolName: !Sub 435 | - public-file-browser-Identity-Pool-${Unique} 436 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 437 | AllowUnauthenticatedIdentities: true 438 | CognitoIdentityUnauthenticatedRole: 439 | Type: AWS::IAM::Role 440 | Metadata: 441 | cfn_nag: 442 | rules_to_suppress: 443 | - id: W28 444 | reason: While not ideal, this is necessary to prevent a circular dependency. The resource uses the stack-id as part of the resource name, and the only properties of an IAM Role requiring replacement are Path and RoleName. 445 | Properties: 446 | RoleName: !Sub 447 | - public-file-browser-Cognito-Unauth-${Unique} 448 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 449 | AssumeRolePolicyDocument: 450 | Version: '2012-10-17' 451 | Statement: 452 | - Effect: Allow 453 | Principal: 454 | Federated: cognito-identity.amazonaws.com 455 | Action: sts:AssumeRoleWithWebIdentity 456 | Condition: 457 | StringEquals: 458 | cognito-identity.amazonaws.com:aud: !Ref 'CognitoIdentityPool' 459 | ForAnyValue:StringLike: 460 | cognito-identity.amazonaws.com:amr: unauthenticated 461 | Policies: 462 | - PolicyName: File-Access 463 | PolicyDocument: 464 | Version: '2012-10-17' 465 | Statement: 466 | - Effect: Allow 467 | Action: 468 | - s3:ListBucket 469 | Resource: 470 | - !Sub 471 | - arn:${AWS::Partition}:s3:::public-file-browser-files-${Unique} 472 | - Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ] 473 | CognitoIdentityPoolRoleMapping: 474 | Type: AWS::Cognito::IdentityPoolRoleAttachment 475 | Properties: 476 | IdentityPoolId: !Ref 'CognitoIdentityPool' 477 | Roles: 478 | unauthenticated: !GetAtt 'CognitoIdentityUnauthenticatedRole.Arn' 479 | Outputs: 480 | FileBrowserURL: 481 | Description: The web URL for the deployed Public File Browser for Amazon S3 solution 482 | Value: !Sub 'https://${CloudFront.DomainName}' 483 | PublicFilesBucket: 484 | Description: The name of the Amazon S3 Bucket for storing PUBLICLY ACCESSIBLE files. 485 | Value: !Ref 'PublicFilesBucket' 486 | WebInterfaceAppBucket: 487 | Description: The name of the Amazon S3 Bucket for the HTML/JS/CSS pages that make up the file browser web interface. 488 | Value: !Ref 'PublicWebsiteBucket' 489 | 490 | -------------------------------------------------------------------------------- /sam/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/public-file-browser-for-amazon-s3/9fc848a72c6f85a7cdbde7d85340c2e1bf2f5b8c/sam/tests/__init__.py -------------------------------------------------------------------------------- /sam/tests/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "public_file_browser_for_amazon_s3_tests" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Caesar Kabalan "] 6 | license = "MIT-0" 7 | 8 | [tool.poetry.dependencies] 9 | python = "~3.11" 10 | boto3 = "^1.34.23" 11 | crhelper = "^2.0.11" 12 | loguru = "^0.7.2" 13 | simplejson = "^3.19.2" 14 | pytest = "^7.4.4" 15 | moto = "^4.2.13" 16 | coverage = "^7.4.0" 17 | 18 | [tool.poetry.dev-dependencies] 19 | 20 | [build-system] 21 | requires = ["poetry-core>=1.0.0"] 22 | build-backend = "poetry.core.masonry.api" 23 | -------------------------------------------------------------------------------- /sam/tests/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.34.48 ; python_version >= "3.11" and python_version < "3.12" 2 | botocore==1.34.48 ; python_version >= "3.11" and python_version < "3.12" 3 | certifi==2024.2.2 ; python_version >= "3.11" and python_version < "3.12" 4 | cffi==1.16.0 ; python_version >= "3.11" and python_version < "3.12" and platform_python_implementation != "PyPy" 5 | charset-normalizer==3.3.2 ; python_version >= "3.11" and python_version < "3.12" 6 | colorama==0.4.6 ; python_version >= "3.11" and python_version < "3.12" and sys_platform == "win32" 7 | coverage==7.4.2 ; python_version >= "3.11" and python_version < "3.12" 8 | crhelper==2.0.11 ; python_version >= "3.11" and python_version < "3.12" 9 | cryptography==42.0.4 ; python_version >= "3.11" and python_version < "3.12" 10 | idna==3.6 ; python_version >= "3.11" and python_version < "3.12" 11 | iniconfig==2.0.0 ; python_version >= "3.11" and python_version < "3.12" 12 | jinja2==3.1.3 ; python_version >= "3.11" and python_version < "3.12" 13 | jmespath==1.0.1 ; python_version >= "3.11" and python_version < "3.12" 14 | loguru==0.7.2 ; python_version >= "3.11" and python_version < "3.12" 15 | markupsafe==2.1.5 ; python_version >= "3.11" and python_version < "3.12" 16 | moto==4.2.14 ; python_version >= "3.11" and python_version < "3.12" 17 | packaging==23.2 ; python_version >= "3.11" and python_version < "3.12" 18 | pluggy==1.4.0 ; python_version >= "3.11" and python_version < "3.12" 19 | pycparser==2.21 ; python_version >= "3.11" and python_version < "3.12" and platform_python_implementation != "PyPy" 20 | pytest==7.4.4 ; python_version >= "3.11" and python_version < "3.12" 21 | python-dateutil==2.8.2 ; python_version >= "3.11" and python_version < "3.12" 22 | pyyaml==6.0.1 ; python_version >= "3.11" and python_version < "3.12" 23 | requests==2.31.0 ; python_version >= "3.11" and python_version < "3.12" 24 | responses==0.25.0 ; python_version >= "3.11" and python_version < "3.12" 25 | s3transfer==0.10.0 ; python_version >= "3.11" and python_version < "3.12" 26 | simplejson==3.19.2 ; python_version >= "3.11" and python_version < "3.12" 27 | six==1.16.0 ; python_version >= "3.11" and python_version < "3.12" 28 | urllib3==2.0.7 ; python_version >= "3.11" and python_version < "3.12" 29 | werkzeug==3.0.1 ; python_version >= "3.11" and python_version < "3.12" 30 | win32-setctime==1.1.0 ; python_version >= "3.11" and python_version < "3.12" and sys_platform == "win32" 31 | xmltodict==0.13.0 ; python_version >= "3.11" and python_version < "3.12" 32 | -------------------------------------------------------------------------------- /sam/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/public-file-browser-for-amazon-s3/9fc848a72c6f85a7cdbde7d85340c2e1bf2f5b8c/sam/tests/unit/__init__.py -------------------------------------------------------------------------------- /sam/tests/unit/test_seed_s3_data.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest import mock 3 | 4 | import boto3 5 | import pytest 6 | from moto import mock_s3, mock_apigateway 7 | 8 | from sam.seed_s3_data import app 9 | 10 | 11 | @pytest.fixture() 12 | def cloudformation_event(): 13 | return { 14 | "RequestType": "Create", 15 | "ServiceToken": "REDACTED", 16 | "ResponseURL": "REDACTED", 17 | "StackId": "REDACTED", 18 | "RequestId": "REDACTED", 19 | "LogicalResourceId": "SeedS3Data", 20 | "ResourceType": "Custom::SeedS3Data", 21 | "ResourceProperties": { 22 | "ServiceToken": "REDACTED", 23 | "PublicWebsiteBucket": "test-bucket-static-website", 24 | "FilesOpenMode": "In New Tab", 25 | "SiteName": "TEST_SITE_NAME", 26 | "IdentityPoolId": "TEST_IDENTITY_POOL", 27 | "FilesBucketName": "test-bucket-files", 28 | "VisibleStorageClasses": "STANDARD,STANDARD_IA,ONEZONE_IA,REDUCED_REDUNDANCY" 29 | } 30 | } 31 | 32 | 33 | @mock_s3 34 | def test_seed_data(cloudformation_event): 35 | boto3.setup_default_session() 36 | s3 = boto3.client('s3') 37 | website_bucket = s3.create_bucket( 38 | Bucket='test-bucket-static-website', 39 | CreateBucketConfiguration={'LocationConstraint': 'us-west-2'} 40 | ) 41 | files_bucket = s3.create_bucket( 42 | Bucket='test-bucket-files', 43 | CreateBucketConfiguration={'LocationConstraint': 'us-west-2'} 44 | ) 45 | os.chdir('seed_s3_data') 46 | ret = app.seed_data(cloudformation_event, None) 47 | 48 | with open('/tmp/website/website/index.html', 'r') as file: # nosec hardcoded_tmp_directory 49 | config_data = file.read() 50 | assert 'test-bucket-files' in config_data # nosec assert_used 51 | assert 'TEST_SITE_NAME' in config_data # nosec assert_used 52 | assert 'TEST_IDENTITY_POOL' in config_data # nosec assert_used 53 | assert 'STANDARD,STANDARD_IA,ONEZONE_IA,REDUCED_REDUNDANCY' in config_data # nosec assert_used 54 | assert 'files_open_in_new_tab: true' in config_data # nosec assert_used 55 | response = s3.list_objects_v2( 56 | Bucket='test-bucket-static-website' 57 | ) 58 | assert response['KeyCount'] > 0 # nosec assert_used 59 | 60 | @mock_s3 61 | def test_seed_data_with_existing_data(cloudformation_event): 62 | boto3.setup_default_session() 63 | s3 = boto3.client('s3') 64 | website_bucket = s3.create_bucket( 65 | Bucket='test-bucket-static-website', 66 | CreateBucketConfiguration={'LocationConstraint': 'us-west-2'} 67 | ) 68 | images_bucket = s3.create_bucket( 69 | Bucket='test-bucket-files', 70 | CreateBucketConfiguration={'LocationConstraint': 'us-west-2'} 71 | ) 72 | s3.put_object( 73 | Bucket='test-bucket-static-website', 74 | Key='test-data-object', 75 | Body=b'test-data' 76 | ) 77 | ret = app.seed_data(cloudformation_event, None) 78 | response = s3.list_objects_v2( 79 | Bucket='test-bucket-static-website' 80 | ) 81 | assert response['KeyCount'] == 1 # nosec assert_used 82 | 83 | 84 | @mock_s3 85 | def test_delete_data_with_existing_data(cloudformation_event): 86 | boto3.setup_default_session() 87 | s3 = boto3.client('s3') 88 | website_bucket = s3.create_bucket( 89 | Bucket='test-bucket-static-website', 90 | CreateBucketConfiguration={'LocationConstraint': 'us-west-2'} 91 | ) 92 | images_bucket = s3.create_bucket( 93 | Bucket='test-bucket-files', 94 | CreateBucketConfiguration={'LocationConstraint': 'us-west-2'} 95 | ) 96 | s3.put_object( 97 | Bucket='test-bucket-static-website', 98 | Key='test-data-object', 99 | Body=b'test-data' 100 | ) 101 | ret = app.delete_data(cloudformation_event, None) 102 | response = s3.list_objects_v2( 103 | Bucket='test-bucket-static-website' 104 | ) 105 | assert response['KeyCount'] == 0 # nosec assert_used 106 | -------------------------------------------------------------------------------- /website/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.3.2 (https://getbootstrap.com/) 3 | * Copyright 2011-2023 The Bootstrap Authors 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */ 6 | :root, 7 | [data-bs-theme=light] { 8 | --bs-blue: #0d6efd; 9 | --bs-indigo: #6610f2; 10 | --bs-purple: #6f42c1; 11 | --bs-pink: #d63384; 12 | --bs-red: #dc3545; 13 | --bs-orange: #fd7e14; 14 | --bs-yellow: #ffc107; 15 | --bs-green: #198754; 16 | --bs-teal: #20c997; 17 | --bs-cyan: #0dcaf0; 18 | --bs-black: #000; 19 | --bs-white: #fff; 20 | --bs-gray: #6c757d; 21 | --bs-gray-dark: #343a40; 22 | --bs-gray-100: #f8f9fa; 23 | --bs-gray-200: #e9ecef; 24 | --bs-gray-300: #dee2e6; 25 | --bs-gray-400: #ced4da; 26 | --bs-gray-500: #adb5bd; 27 | --bs-gray-600: #6c757d; 28 | --bs-gray-700: #495057; 29 | --bs-gray-800: #343a40; 30 | --bs-gray-900: #212529; 31 | --bs-primary: #0d6efd; 32 | --bs-secondary: #6c757d; 33 | --bs-success: #198754; 34 | --bs-info: #0dcaf0; 35 | --bs-warning: #ffc107; 36 | --bs-danger: #dc3545; 37 | --bs-light: #f8f9fa; 38 | --bs-dark: #212529; 39 | --bs-primary-rgb: 13, 110, 253; 40 | --bs-secondary-rgb: 108, 117, 125; 41 | --bs-success-rgb: 25, 135, 84; 42 | --bs-info-rgb: 13, 202, 240; 43 | --bs-warning-rgb: 255, 193, 7; 44 | --bs-danger-rgb: 220, 53, 69; 45 | --bs-light-rgb: 248, 249, 250; 46 | --bs-dark-rgb: 33, 37, 41; 47 | --bs-primary-text-emphasis: #052c65; 48 | --bs-secondary-text-emphasis: #2b2f32; 49 | --bs-success-text-emphasis: #0a3622; 50 | --bs-info-text-emphasis: #055160; 51 | --bs-warning-text-emphasis: #664d03; 52 | --bs-danger-text-emphasis: #58151c; 53 | --bs-light-text-emphasis: #495057; 54 | --bs-dark-text-emphasis: #495057; 55 | --bs-primary-bg-subtle: #cfe2ff; 56 | --bs-secondary-bg-subtle: #e2e3e5; 57 | --bs-success-bg-subtle: #d1e7dd; 58 | --bs-info-bg-subtle: #cff4fc; 59 | --bs-warning-bg-subtle: #fff3cd; 60 | --bs-danger-bg-subtle: #f8d7da; 61 | --bs-light-bg-subtle: #fcfcfd; 62 | --bs-dark-bg-subtle: #ced4da; 63 | --bs-primary-border-subtle: #9ec5fe; 64 | --bs-secondary-border-subtle: #c4c8cb; 65 | --bs-success-border-subtle: #a3cfbb; 66 | --bs-info-border-subtle: #9eeaf9; 67 | --bs-warning-border-subtle: #ffe69c; 68 | --bs-danger-border-subtle: #f1aeb5; 69 | --bs-light-border-subtle: #e9ecef; 70 | --bs-dark-border-subtle: #adb5bd; 71 | --bs-white-rgb: 255, 255, 255; 72 | --bs-black-rgb: 0, 0, 0; 73 | --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 74 | --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 75 | --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); 76 | --bs-body-font-family: var(--bs-font-sans-serif); 77 | --bs-body-font-size: 1rem; 78 | --bs-body-font-weight: 400; 79 | --bs-body-line-height: 1.5; 80 | --bs-body-color: #212529; 81 | --bs-body-color-rgb: 33, 37, 41; 82 | --bs-body-bg: #fff; 83 | --bs-body-bg-rgb: 255, 255, 255; 84 | --bs-emphasis-color: #000; 85 | --bs-emphasis-color-rgb: 0, 0, 0; 86 | --bs-secondary-color: rgba(33, 37, 41, 0.75); 87 | --bs-secondary-color-rgb: 33, 37, 41; 88 | --bs-secondary-bg: #e9ecef; 89 | --bs-secondary-bg-rgb: 233, 236, 239; 90 | --bs-tertiary-color: rgba(33, 37, 41, 0.5); 91 | --bs-tertiary-color-rgb: 33, 37, 41; 92 | --bs-tertiary-bg: #f8f9fa; 93 | --bs-tertiary-bg-rgb: 248, 249, 250; 94 | --bs-heading-color: inherit; 95 | --bs-link-color: #0d6efd; 96 | --bs-link-color-rgb: 13, 110, 253; 97 | --bs-link-decoration: underline; 98 | --bs-link-hover-color: #0a58ca; 99 | --bs-link-hover-color-rgb: 10, 88, 202; 100 | --bs-code-color: #d63384; 101 | --bs-highlight-color: #212529; 102 | --bs-highlight-bg: #fff3cd; 103 | --bs-border-width: 1px; 104 | --bs-border-style: solid; 105 | --bs-border-color: #dee2e6; 106 | --bs-border-color-translucent: rgba(0, 0, 0, 0.175); 107 | --bs-border-radius: 0.375rem; 108 | --bs-border-radius-sm: 0.25rem; 109 | --bs-border-radius-lg: 0.5rem; 110 | --bs-border-radius-xl: 1rem; 111 | --bs-border-radius-xxl: 2rem; 112 | --bs-border-radius-2xl: var(--bs-border-radius-xxl); 113 | --bs-border-radius-pill: 50rem; 114 | --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); 115 | --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); 116 | --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); 117 | --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); 118 | --bs-focus-ring-width: 0.25rem; 119 | --bs-focus-ring-opacity: 0.25; 120 | --bs-focus-ring-color: rgba(13, 110, 253, 0.25); 121 | --bs-form-valid-color: #198754; 122 | --bs-form-valid-border-color: #198754; 123 | --bs-form-invalid-color: #dc3545; 124 | --bs-form-invalid-border-color: #dc3545; 125 | } 126 | 127 | [data-bs-theme=dark] { 128 | color-scheme: dark; 129 | --bs-body-color: #dee2e6; 130 | --bs-body-color-rgb: 222, 226, 230; 131 | --bs-body-bg: #212529; 132 | --bs-body-bg-rgb: 33, 37, 41; 133 | --bs-emphasis-color: #fff; 134 | --bs-emphasis-color-rgb: 255, 255, 255; 135 | --bs-secondary-color: rgba(222, 226, 230, 0.75); 136 | --bs-secondary-color-rgb: 222, 226, 230; 137 | --bs-secondary-bg: #343a40; 138 | --bs-secondary-bg-rgb: 52, 58, 64; 139 | --bs-tertiary-color: rgba(222, 226, 230, 0.5); 140 | --bs-tertiary-color-rgb: 222, 226, 230; 141 | --bs-tertiary-bg: #2b3035; 142 | --bs-tertiary-bg-rgb: 43, 48, 53; 143 | --bs-primary-text-emphasis: #6ea8fe; 144 | --bs-secondary-text-emphasis: #a7acb1; 145 | --bs-success-text-emphasis: #75b798; 146 | --bs-info-text-emphasis: #6edff6; 147 | --bs-warning-text-emphasis: #ffda6a; 148 | --bs-danger-text-emphasis: #ea868f; 149 | --bs-light-text-emphasis: #f8f9fa; 150 | --bs-dark-text-emphasis: #dee2e6; 151 | --bs-primary-bg-subtle: #031633; 152 | --bs-secondary-bg-subtle: #161719; 153 | --bs-success-bg-subtle: #051b11; 154 | --bs-info-bg-subtle: #032830; 155 | --bs-warning-bg-subtle: #332701; 156 | --bs-danger-bg-subtle: #2c0b0e; 157 | --bs-light-bg-subtle: #343a40; 158 | --bs-dark-bg-subtle: #1a1d20; 159 | --bs-primary-border-subtle: #084298; 160 | --bs-secondary-border-subtle: #41464b; 161 | --bs-success-border-subtle: #0f5132; 162 | --bs-info-border-subtle: #087990; 163 | --bs-warning-border-subtle: #997404; 164 | --bs-danger-border-subtle: #842029; 165 | --bs-light-border-subtle: #495057; 166 | --bs-dark-border-subtle: #343a40; 167 | --bs-heading-color: inherit; 168 | --bs-link-color: #6ea8fe; 169 | --bs-link-hover-color: #8bb9fe; 170 | --bs-link-color-rgb: 110, 168, 254; 171 | --bs-link-hover-color-rgb: 139, 185, 254; 172 | --bs-code-color: #e685b5; 173 | --bs-highlight-color: #dee2e6; 174 | --bs-highlight-bg: #664d03; 175 | --bs-border-color: #495057; 176 | --bs-border-color-translucent: rgba(255, 255, 255, 0.15); 177 | --bs-form-valid-color: #75b798; 178 | --bs-form-valid-border-color: #75b798; 179 | --bs-form-invalid-color: #ea868f; 180 | --bs-form-invalid-border-color: #ea868f; 181 | } 182 | 183 | *, 184 | *::before, 185 | *::after { 186 | box-sizing: border-box; 187 | } 188 | 189 | @media (prefers-reduced-motion: no-preference) { 190 | :root { 191 | scroll-behavior: smooth; 192 | } 193 | } 194 | 195 | body { 196 | margin: 0; 197 | font-family: var(--bs-body-font-family); 198 | font-size: var(--bs-body-font-size); 199 | font-weight: var(--bs-body-font-weight); 200 | line-height: var(--bs-body-line-height); 201 | color: var(--bs-body-color); 202 | text-align: var(--bs-body-text-align); 203 | background-color: var(--bs-body-bg); 204 | -webkit-text-size-adjust: 100%; 205 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 206 | } 207 | 208 | hr { 209 | margin: 1rem 0; 210 | color: inherit; 211 | border: 0; 212 | border-top: var(--bs-border-width) solid; 213 | opacity: 0.25; 214 | } 215 | 216 | h6, h5, h4, h3, h2, h1 { 217 | margin-top: 0; 218 | margin-bottom: 0.5rem; 219 | font-weight: 500; 220 | line-height: 1.2; 221 | color: var(--bs-heading-color); 222 | } 223 | 224 | h1 { 225 | font-size: calc(1.375rem + 1.5vw); 226 | } 227 | @media (min-width: 1200px) { 228 | h1 { 229 | font-size: 2.5rem; 230 | } 231 | } 232 | 233 | h2 { 234 | font-size: calc(1.325rem + 0.9vw); 235 | } 236 | @media (min-width: 1200px) { 237 | h2 { 238 | font-size: 2rem; 239 | } 240 | } 241 | 242 | h3 { 243 | font-size: calc(1.3rem + 0.6vw); 244 | } 245 | @media (min-width: 1200px) { 246 | h3 { 247 | font-size: 1.75rem; 248 | } 249 | } 250 | 251 | h4 { 252 | font-size: calc(1.275rem + 0.3vw); 253 | } 254 | @media (min-width: 1200px) { 255 | h4 { 256 | font-size: 1.5rem; 257 | } 258 | } 259 | 260 | h5 { 261 | font-size: 1.25rem; 262 | } 263 | 264 | h6 { 265 | font-size: 1rem; 266 | } 267 | 268 | p { 269 | margin-top: 0; 270 | margin-bottom: 1rem; 271 | } 272 | 273 | abbr[title] { 274 | -webkit-text-decoration: underline dotted; 275 | text-decoration: underline dotted; 276 | cursor: help; 277 | -webkit-text-decoration-skip-ink: none; 278 | text-decoration-skip-ink: none; 279 | } 280 | 281 | address { 282 | margin-bottom: 1rem; 283 | font-style: normal; 284 | line-height: inherit; 285 | } 286 | 287 | ol, 288 | ul { 289 | padding-left: 2rem; 290 | } 291 | 292 | ol, 293 | ul, 294 | dl { 295 | margin-top: 0; 296 | margin-bottom: 1rem; 297 | } 298 | 299 | ol ol, 300 | ul ul, 301 | ol ul, 302 | ul ol { 303 | margin-bottom: 0; 304 | } 305 | 306 | dt { 307 | font-weight: 700; 308 | } 309 | 310 | dd { 311 | margin-bottom: 0.5rem; 312 | margin-left: 0; 313 | } 314 | 315 | blockquote { 316 | margin: 0 0 1rem; 317 | } 318 | 319 | b, 320 | strong { 321 | font-weight: bolder; 322 | } 323 | 324 | small { 325 | font-size: 0.875em; 326 | } 327 | 328 | mark { 329 | padding: 0.1875em; 330 | color: var(--bs-highlight-color); 331 | background-color: var(--bs-highlight-bg); 332 | } 333 | 334 | sub, 335 | sup { 336 | position: relative; 337 | font-size: 0.75em; 338 | line-height: 0; 339 | vertical-align: baseline; 340 | } 341 | 342 | sub { 343 | bottom: -0.25em; 344 | } 345 | 346 | sup { 347 | top: -0.5em; 348 | } 349 | 350 | a { 351 | color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); 352 | text-decoration: underline; 353 | } 354 | a:hover { 355 | --bs-link-color-rgb: var(--bs-link-hover-color-rgb); 356 | } 357 | 358 | a:not([href]):not([class]), a:not([href]):not([class]):hover { 359 | color: inherit; 360 | text-decoration: none; 361 | } 362 | 363 | pre, 364 | code, 365 | kbd, 366 | samp { 367 | font-family: var(--bs-font-monospace); 368 | font-size: 1em; 369 | } 370 | 371 | pre { 372 | display: block; 373 | margin-top: 0; 374 | margin-bottom: 1rem; 375 | overflow: auto; 376 | font-size: 0.875em; 377 | } 378 | pre code { 379 | font-size: inherit; 380 | color: inherit; 381 | word-break: normal; 382 | } 383 | 384 | code { 385 | font-size: 0.875em; 386 | color: var(--bs-code-color); 387 | word-wrap: break-word; 388 | } 389 | a > code { 390 | color: inherit; 391 | } 392 | 393 | kbd { 394 | padding: 0.1875rem 0.375rem; 395 | font-size: 0.875em; 396 | color: var(--bs-body-bg); 397 | background-color: var(--bs-body-color); 398 | border-radius: 0.25rem; 399 | } 400 | kbd kbd { 401 | padding: 0; 402 | font-size: 1em; 403 | } 404 | 405 | figure { 406 | margin: 0 0 1rem; 407 | } 408 | 409 | img, 410 | svg { 411 | vertical-align: middle; 412 | } 413 | 414 | table { 415 | caption-side: bottom; 416 | border-collapse: collapse; 417 | } 418 | 419 | caption { 420 | padding-top: 0.5rem; 421 | padding-bottom: 0.5rem; 422 | color: var(--bs-secondary-color); 423 | text-align: left; 424 | } 425 | 426 | th { 427 | text-align: inherit; 428 | text-align: -webkit-match-parent; 429 | } 430 | 431 | thead, 432 | tbody, 433 | tfoot, 434 | tr, 435 | td, 436 | th { 437 | border-color: inherit; 438 | border-style: solid; 439 | border-width: 0; 440 | } 441 | 442 | label { 443 | display: inline-block; 444 | } 445 | 446 | button { 447 | border-radius: 0; 448 | } 449 | 450 | button:focus:not(:focus-visible) { 451 | outline: 0; 452 | } 453 | 454 | input, 455 | button, 456 | select, 457 | optgroup, 458 | textarea { 459 | margin: 0; 460 | font-family: inherit; 461 | font-size: inherit; 462 | line-height: inherit; 463 | } 464 | 465 | button, 466 | select { 467 | text-transform: none; 468 | } 469 | 470 | [role=button] { 471 | cursor: pointer; 472 | } 473 | 474 | select { 475 | word-wrap: normal; 476 | } 477 | select:disabled { 478 | opacity: 1; 479 | } 480 | 481 | [list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { 482 | display: none !important; 483 | } 484 | 485 | button, 486 | [type=button], 487 | [type=reset], 488 | [type=submit] { 489 | -webkit-appearance: button; 490 | } 491 | button:not(:disabled), 492 | [type=button]:not(:disabled), 493 | [type=reset]:not(:disabled), 494 | [type=submit]:not(:disabled) { 495 | cursor: pointer; 496 | } 497 | 498 | ::-moz-focus-inner { 499 | padding: 0; 500 | border-style: none; 501 | } 502 | 503 | textarea { 504 | resize: vertical; 505 | } 506 | 507 | fieldset { 508 | min-width: 0; 509 | padding: 0; 510 | margin: 0; 511 | border: 0; 512 | } 513 | 514 | legend { 515 | float: left; 516 | width: 100%; 517 | padding: 0; 518 | margin-bottom: 0.5rem; 519 | font-size: calc(1.275rem + 0.3vw); 520 | line-height: inherit; 521 | } 522 | @media (min-width: 1200px) { 523 | legend { 524 | font-size: 1.5rem; 525 | } 526 | } 527 | legend + * { 528 | clear: left; 529 | } 530 | 531 | ::-webkit-datetime-edit-fields-wrapper, 532 | ::-webkit-datetime-edit-text, 533 | ::-webkit-datetime-edit-minute, 534 | ::-webkit-datetime-edit-hour-field, 535 | ::-webkit-datetime-edit-day-field, 536 | ::-webkit-datetime-edit-month-field, 537 | ::-webkit-datetime-edit-year-field { 538 | padding: 0; 539 | } 540 | 541 | ::-webkit-inner-spin-button { 542 | height: auto; 543 | } 544 | 545 | [type=search] { 546 | -webkit-appearance: textfield; 547 | outline-offset: -2px; 548 | } 549 | 550 | /* rtl:raw: 551 | [type="tel"], 552 | [type="url"], 553 | [type="email"], 554 | [type="number"] { 555 | direction: ltr; 556 | } 557 | */ 558 | ::-webkit-search-decoration { 559 | -webkit-appearance: none; 560 | } 561 | 562 | ::-webkit-color-swatch-wrapper { 563 | padding: 0; 564 | } 565 | 566 | ::-webkit-file-upload-button { 567 | font: inherit; 568 | -webkit-appearance: button; 569 | } 570 | 571 | ::file-selector-button { 572 | font: inherit; 573 | -webkit-appearance: button; 574 | } 575 | 576 | output { 577 | display: inline-block; 578 | } 579 | 580 | iframe { 581 | border: 0; 582 | } 583 | 584 | summary { 585 | display: list-item; 586 | cursor: pointer; 587 | } 588 | 589 | progress { 590 | vertical-align: baseline; 591 | } 592 | 593 | [hidden] { 594 | display: none !important; 595 | } 596 | 597 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /website/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.3.2 (https://getbootstrap.com/) 3 | * Copyright 2011-2023 The Bootstrap Authors 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--bs-link-hover-color:#8bb9fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:#e685b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important} 6 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /website/css/bootstrap-reboot.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../../scss/mixins/_banner.scss","../../scss/_root.scss","dist/css/bootstrap-reboot.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_color-mode.scss","../../scss/_reboot.scss","../../scss/mixins/_border-radius.scss"],"names":[],"mappings":"AACE;;;;ACDF,MCMA,sBDGI,UAAA,QAAA,YAAA,QAAA,YAAA,QAAA,UAAA,QAAA,SAAA,QAAA,YAAA,QAAA,YAAA,QAAA,WAAA,QAAA,UAAA,QAAA,UAAA,QAAA,WAAA,KAAA,WAAA,KAAA,UAAA,QAAA,eAAA,QAIA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAAA,cAAA,QAIA,aAAA,QAAA,eAAA,QAAA,aAAA,QAAA,UAAA,QAAA,aAAA,QAAA,YAAA,QAAA,WAAA,QAAA,UAAA,QAIA,iBAAA,EAAA,CAAA,GAAA,CAAA,IAAA,mBAAA,GAAA,CAAA,GAAA,CAAA,IAAA,iBAAA,EAAA,CAAA,GAAA,CAAA,GAAA,cAAA,EAAA,CAAA,GAAA,CAAA,IAAA,iBAAA,GAAA,CAAA,GAAA,CAAA,EAAA,gBAAA,GAAA,CAAA,EAAA,CAAA,GAAA,eAAA,GAAA,CAAA,GAAA,CAAA,IAAA,cAAA,EAAA,CAAA,EAAA,CAAA,GAIA,2BAAA,QAAA,6BAAA,QAAA,2BAAA,QAAA,wBAAA,QAAA,2BAAA,QAAA,0BAAA,QAAA,yBAAA,QAAA,wBAAA,QAIA,uBAAA,QAAA,yBAAA,QAAA,uBAAA,QAAA,oBAAA,QAAA,uBAAA,QAAA,sBAAA,QAAA,qBAAA,QAAA,oBAAA,QAIA,2BAAA,QAAA,6BAAA,QAAA,2BAAA,QAAA,wBAAA,QAAA,2BAAA,QAAA,0BAAA,QAAA,yBAAA,QAAA,wBAAA,QAGF,eAAA,GAAA,CAAA,GAAA,CAAA,IACA,eAAA,CAAA,CAAA,CAAA,CAAA,EAMA,qBAAA,SAAA,CAAA,aAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,WAAA,CAAA,iBAAA,CAAA,KAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBACA,oBAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UACA,cAAA,2EAOA,sBAAA,0BE2OI,oBAAA,KFzOJ,sBAAA,IACA,sBAAA,IAKA,gBAAA,QACA,oBAAA,EAAA,CAAA,EAAA,CAAA,GACA,aAAA,KACA,iBAAA,GAAA,CAAA,GAAA,CAAA,IAEA,oBAAA,KACA,wBAAA,CAAA,CAAA,CAAA,CAAA,EAEA,qBAAA,uBACA,yBAAA,EAAA,CAAA,EAAA,CAAA,GACA,kBAAA,QACA,sBAAA,GAAA,CAAA,GAAA,CAAA,IAEA,oBAAA,sBACA,wBAAA,EAAA,CAAA,EAAA,CAAA,GACA,iBAAA,QACA,qBAAA,GAAA,CAAA,GAAA,CAAA,IAGA,mBAAA,QAEA,gBAAA,QACA,oBAAA,EAAA,CAAA,GAAA,CAAA,IACA,qBAAA,UAEA,sBAAA,QACA,0BAAA,EAAA,CAAA,EAAA,CAAA,IAMA,gBAAA,QACA,qBAAA,QACA,kBAAA,QAGA,kBAAA,IACA,kBAAA,MACA,kBAAA,QACA,8BAAA,qBAEA,mBAAA,SACA,sBAAA,QACA,sBAAA,OACA,sBAAA,KACA,uBAAA,KACA,uBAAA,4BACA,wBAAA,MAGA,gBAAA,EAAA,OAAA,KAAA,oBACA,mBAAA,EAAA,SAAA,QAAA,qBACA,mBAAA,EAAA,KAAA,KAAA,qBACA,sBAAA,MAAA,EAAA,IAAA,IAAA,qBAIA,sBAAA,QACA,wBAAA,KACA,sBAAA,yBAIA,sBAAA,QACA,6BAAA,QACA,wBAAA,QACA,+BAAA,QGhHE,qBHsHA,aAAA,KAGA,gBAAA,QACA,oBAAA,GAAA,CAAA,GAAA,CAAA,IACA,aAAA,QACA,iBAAA,EAAA,CAAA,EAAA,CAAA,GAEA,oBAAA,KACA,wBAAA,GAAA,CAAA,GAAA,CAAA,IAEA,qBAAA,0BACA,yBAAA,GAAA,CAAA,GAAA,CAAA,IACA,kBAAA,QACA,sBAAA,EAAA,CAAA,EAAA,CAAA,GAEA,oBAAA,yBACA,wBAAA,GAAA,CAAA,GAAA,CAAA,IACA,iBAAA,QACA,qBAAA,EAAA,CAAA,EAAA,CAAA,GAGE,2BAAA,QAAA,6BAAA,QAAA,2BAAA,QAAA,wBAAA,QAAA,2BAAA,QAAA,0BAAA,QAAA,yBAAA,QAAA,wBAAA,QAIA,uBAAA,QAAA,yBAAA,QAAA,uBAAA,QAAA,oBAAA,QAAA,uBAAA,QAAA,sBAAA,QAAA,qBAAA,QAAA,oBAAA,QAIA,2BAAA,QAAA,6BAAA,QAAA,2BAAA,QAAA,wBAAA,QAAA,2BAAA,QAAA,0BAAA,QAAA,yBAAA,QAAA,wBAAA,QAGF,mBAAA,QAEA,gBAAA,QACA,sBAAA,QACA,oBAAA,GAAA,CAAA,GAAA,CAAA,IACA,0BAAA,GAAA,CAAA,GAAA,CAAA,IAEA,gBAAA,QACA,qBAAA,QACA,kBAAA,QAEA,kBAAA,QACA,8BAAA,0BAEA,sBAAA,QACA,6BAAA,QACA,wBAAA,QACA,+BAAA,QIxKJ,EHyKA,QADA,SGrKE,WAAA,WAeE,8CANJ,MAOM,gBAAA,QAcN,KACE,OAAA,EACA,YAAA,2BF6OI,UAAA,yBE3OJ,YAAA,2BACA,YAAA,2BACA,MAAA,qBACA,WAAA,0BACA,iBAAA,kBACA,yBAAA,KACA,4BAAA,YASF,GACE,OAAA,KAAA,EACA,MAAA,QACA,OAAA,EACA,WAAA,uBAAA,MACA,QAAA,IAUF,GAAA,GAAA,GAAA,GAAA,GAAA,GACE,WAAA,EACA,cAAA,MAGA,YAAA,IACA,YAAA,IACA,MAAA,wBAGF,GFuMQ,UAAA,uBA5JJ,0BE3CJ,GF8MQ,UAAA,QEzMR,GFkMQ,UAAA,sBA5JJ,0BEtCJ,GFyMQ,UAAA,MEpMR,GF6LQ,UAAA,oBA5JJ,0BEjCJ,GFoMQ,UAAA,SE/LR,GFwLQ,UAAA,sBA5JJ,0BE5BJ,GF+LQ,UAAA,QE1LR,GF+KM,UAAA,QE1KN,GF0KM,UAAA,KE/JN,EACE,WAAA,EACA,cAAA,KAUF,YACE,wBAAA,UAAA,OAAA,gBAAA,UAAA,OACA,OAAA,KACA,iCAAA,KAAA,yBAAA,KAMF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QAMF,GHiIA,GG/HE,aAAA,KHqIF,GGlIA,GHiIA,GG9HE,WAAA,EACA,cAAA,KAGF,MHkIA,MACA,MAFA,MG7HE,cAAA,EAGF,GACE,YAAA,IAKF,GACE,cAAA,MACA,YAAA,EAMF,WACE,OAAA,EAAA,EAAA,KAQF,EHuHA,OGrHE,YAAA,OAQF,MF6EM,UAAA,OEtEN,KACE,QAAA,QACA,MAAA,0BACA,iBAAA,uBASF,IHyGA,IGvGE,SAAA,SFwDI,UAAA,MEtDJ,YAAA,EACA,eAAA,SAGF,IAAM,OAAA,OACN,IAAM,IAAA,MAKN,EACE,MAAA,wDACA,gBAAA,UAEA,QACE,oBAAA,+BAWF,2BAAA,iCAEE,MAAA,QACA,gBAAA,KHqGJ,KACA,IG/FA,IHgGA,KG5FE,YAAA,yBFcI,UAAA,IENN,IACE,QAAA,MACA,WAAA,EACA,cAAA,KACA,SAAA,KFEI,UAAA,OEGJ,SFHI,UAAA,QEKF,MAAA,QACA,WAAA,OAIJ,KFVM,UAAA,OEYJ,MAAA,qBACA,UAAA,WAGA,OACE,MAAA,QAIJ,IACE,QAAA,SAAA,QFtBI,UAAA,OEwBJ,MAAA,kBACA,iBAAA,qBCrSE,cAAA,ODwSF,QACE,QAAA,EF7BE,UAAA,IEwCN,OACE,OAAA,EAAA,EAAA,KAMF,IH2EA,IGzEE,eAAA,OAQF,MACE,aAAA,OACA,gBAAA,SAGF,QACE,YAAA,MACA,eAAA,MACA,MAAA,0BACA,WAAA,KAOF,GAEE,WAAA,QACA,WAAA,qBHoEF,MAGA,GAFA,MAGA,GGrEA,MHmEA,GG7DE,aAAA,QACA,aAAA,MACA,aAAA,EAQF,MACE,QAAA,aAMF,OAEE,cAAA,EAQF,iCACE,QAAA,EHsDF,OGjDA,MHmDA,SADA,OAEA,SG/CE,OAAA,EACA,YAAA,QF5HI,UAAA,QE8HJ,YAAA,QAIF,OHgDA,OG9CE,eAAA,KAKF,cACE,OAAA,QAGF,OAGE,UAAA,OAGA,gBACE,QAAA,EAOJ,0IACE,QAAA,eH0CF,cACA,aACA,cGpCA,OAIE,mBAAA,OHoCF,6BACA,4BACA,6BGnCI,sBACE,OAAA,QAON,mBACE,QAAA,EACA,aAAA,KAKF,SACE,OAAA,SAUF,SACE,UAAA,EACA,QAAA,EACA,OAAA,EACA,OAAA,EAQF,OACE,MAAA,KACA,MAAA,KACA,QAAA,EACA,cAAA,MFjNM,UAAA,sBEoNN,YAAA,QFhXE,0BEyWJ,OFtMQ,UAAA,QE+MN,SACE,MAAA,KH4BJ,kCGrBA,uCHoBA,mCADA,+BAGA,oCAJA,6BAKA,mCGhBE,QAAA,EAGF,4BACE,OAAA,KASF,cACE,mBAAA,UACA,eAAA,KAmBF,4BACE,mBAAA,KAKF,+BACE,QAAA,EAOF,6BACE,KAAA,QACA,mBAAA,OAFF,uBACE,KAAA,QACA,mBAAA,OAKF,OACE,QAAA,aAKF,OACE,OAAA,EAOF,QACE,QAAA,UACA,OAAA,QAQF,SACE,eAAA,SAQF,SACE,QAAA","sourcesContent":["@mixin bsBanner($file) {\n /*!\n * Bootstrap #{$file} v5.3.2 (https://getbootstrap.com/)\n * Copyright 2011-2023 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n}\n",":root,\n[data-bs-theme=\"light\"] {\n // Note: Custom variable values only support SassScript inside `#{}`.\n\n // Colors\n //\n // Generate palettes for full colors, grays, and theme colors.\n\n @each $color, $value in $colors {\n --#{$prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $grays {\n --#{$prefix}gray-#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors-rgb {\n --#{$prefix}#{$color}-rgb: #{$value};\n }\n\n @each $color, $value in $theme-colors-text {\n --#{$prefix}#{$color}-text-emphasis: #{$value};\n }\n\n @each $color, $value in $theme-colors-bg-subtle {\n --#{$prefix}#{$color}-bg-subtle: #{$value};\n }\n\n @each $color, $value in $theme-colors-border-subtle {\n --#{$prefix}#{$color}-border-subtle: #{$value};\n }\n\n --#{$prefix}white-rgb: #{to-rgb($white)};\n --#{$prefix}black-rgb: #{to-rgb($black)};\n\n // Fonts\n\n // Note: Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$prefix}gradient: #{$gradient};\n\n // Root and body\n // scss-docs-start root-body-variables\n @if $font-size-root != null {\n --#{$prefix}root-font-size: #{$font-size-root};\n }\n --#{$prefix}body-font-family: #{inspect($font-family-base)};\n @include rfs($font-size-base, --#{$prefix}body-font-size);\n --#{$prefix}body-font-weight: #{$font-weight-base};\n --#{$prefix}body-line-height: #{$line-height-base};\n @if $body-text-align != null {\n --#{$prefix}body-text-align: #{$body-text-align};\n }\n\n --#{$prefix}body-color: #{$body-color};\n --#{$prefix}body-color-rgb: #{to-rgb($body-color)};\n --#{$prefix}body-bg: #{$body-bg};\n --#{$prefix}body-bg-rgb: #{to-rgb($body-bg)};\n\n --#{$prefix}emphasis-color: #{$body-emphasis-color};\n --#{$prefix}emphasis-color-rgb: #{to-rgb($body-emphasis-color)};\n\n --#{$prefix}secondary-color: #{$body-secondary-color};\n --#{$prefix}secondary-color-rgb: #{to-rgb($body-secondary-color)};\n --#{$prefix}secondary-bg: #{$body-secondary-bg};\n --#{$prefix}secondary-bg-rgb: #{to-rgb($body-secondary-bg)};\n\n --#{$prefix}tertiary-color: #{$body-tertiary-color};\n --#{$prefix}tertiary-color-rgb: #{to-rgb($body-tertiary-color)};\n --#{$prefix}tertiary-bg: #{$body-tertiary-bg};\n --#{$prefix}tertiary-bg-rgb: #{to-rgb($body-tertiary-bg)};\n // scss-docs-end root-body-variables\n\n --#{$prefix}heading-color: #{$headings-color};\n\n --#{$prefix}link-color: #{$link-color};\n --#{$prefix}link-color-rgb: #{to-rgb($link-color)};\n --#{$prefix}link-decoration: #{$link-decoration};\n\n --#{$prefix}link-hover-color: #{$link-hover-color};\n --#{$prefix}link-hover-color-rgb: #{to-rgb($link-hover-color)};\n\n @if $link-hover-decoration != null {\n --#{$prefix}link-hover-decoration: #{$link-hover-decoration};\n }\n\n --#{$prefix}code-color: #{$code-color};\n --#{$prefix}highlight-color: #{$mark-color};\n --#{$prefix}highlight-bg: #{$mark-bg};\n\n // scss-docs-start root-border-var\n --#{$prefix}border-width: #{$border-width};\n --#{$prefix}border-style: #{$border-style};\n --#{$prefix}border-color: #{$border-color};\n --#{$prefix}border-color-translucent: #{$border-color-translucent};\n\n --#{$prefix}border-radius: #{$border-radius};\n --#{$prefix}border-radius-sm: #{$border-radius-sm};\n --#{$prefix}border-radius-lg: #{$border-radius-lg};\n --#{$prefix}border-radius-xl: #{$border-radius-xl};\n --#{$prefix}border-radius-xxl: #{$border-radius-xxl};\n --#{$prefix}border-radius-2xl: var(--#{$prefix}border-radius-xxl); // Deprecated in v5.3.0 for consistency\n --#{$prefix}border-radius-pill: #{$border-radius-pill};\n // scss-docs-end root-border-var\n\n --#{$prefix}box-shadow: #{$box-shadow};\n --#{$prefix}box-shadow-sm: #{$box-shadow-sm};\n --#{$prefix}box-shadow-lg: #{$box-shadow-lg};\n --#{$prefix}box-shadow-inset: #{$box-shadow-inset};\n\n // Focus styles\n // scss-docs-start root-focus-variables\n --#{$prefix}focus-ring-width: #{$focus-ring-width};\n --#{$prefix}focus-ring-opacity: #{$focus-ring-opacity};\n --#{$prefix}focus-ring-color: #{$focus-ring-color};\n // scss-docs-end root-focus-variables\n\n // scss-docs-start root-form-validation-variables\n --#{$prefix}form-valid-color: #{$form-valid-color};\n --#{$prefix}form-valid-border-color: #{$form-valid-border-color};\n --#{$prefix}form-invalid-color: #{$form-invalid-color};\n --#{$prefix}form-invalid-border-color: #{$form-invalid-border-color};\n // scss-docs-end root-form-validation-variables\n}\n\n@if $enable-dark-mode {\n @include color-mode(dark, true) {\n color-scheme: dark;\n\n // scss-docs-start root-dark-mode-vars\n --#{$prefix}body-color: #{$body-color-dark};\n --#{$prefix}body-color-rgb: #{to-rgb($body-color-dark)};\n --#{$prefix}body-bg: #{$body-bg-dark};\n --#{$prefix}body-bg-rgb: #{to-rgb($body-bg-dark)};\n\n --#{$prefix}emphasis-color: #{$body-emphasis-color-dark};\n --#{$prefix}emphasis-color-rgb: #{to-rgb($body-emphasis-color-dark)};\n\n --#{$prefix}secondary-color: #{$body-secondary-color-dark};\n --#{$prefix}secondary-color-rgb: #{to-rgb($body-secondary-color-dark)};\n --#{$prefix}secondary-bg: #{$body-secondary-bg-dark};\n --#{$prefix}secondary-bg-rgb: #{to-rgb($body-secondary-bg-dark)};\n\n --#{$prefix}tertiary-color: #{$body-tertiary-color-dark};\n --#{$prefix}tertiary-color-rgb: #{to-rgb($body-tertiary-color-dark)};\n --#{$prefix}tertiary-bg: #{$body-tertiary-bg-dark};\n --#{$prefix}tertiary-bg-rgb: #{to-rgb($body-tertiary-bg-dark)};\n\n @each $color, $value in $theme-colors-text-dark {\n --#{$prefix}#{$color}-text-emphasis: #{$value};\n }\n\n @each $color, $value in $theme-colors-bg-subtle-dark {\n --#{$prefix}#{$color}-bg-subtle: #{$value};\n }\n\n @each $color, $value in $theme-colors-border-subtle-dark {\n --#{$prefix}#{$color}-border-subtle: #{$value};\n }\n\n --#{$prefix}heading-color: #{$headings-color-dark};\n\n --#{$prefix}link-color: #{$link-color-dark};\n --#{$prefix}link-hover-color: #{$link-hover-color-dark};\n --#{$prefix}link-color-rgb: #{to-rgb($link-color-dark)};\n --#{$prefix}link-hover-color-rgb: #{to-rgb($link-hover-color-dark)};\n\n --#{$prefix}code-color: #{$code-color-dark};\n --#{$prefix}highlight-color: #{$mark-color-dark};\n --#{$prefix}highlight-bg: #{$mark-bg-dark};\n\n --#{$prefix}border-color: #{$border-color-dark};\n --#{$prefix}border-color-translucent: #{$border-color-translucent-dark};\n\n --#{$prefix}form-valid-color: #{$form-valid-color-dark};\n --#{$prefix}form-valid-border-color: #{$form-valid-border-color-dark};\n --#{$prefix}form-invalid-color: #{$form-invalid-color-dark};\n --#{$prefix}form-invalid-border-color: #{$form-invalid-border-color-dark};\n // scss-docs-end root-dark-mode-vars\n }\n}\n","/*!\n * Bootstrap Reboot v5.3.2 (https://getbootstrap.com/)\n * Copyright 2011-2023 The Bootstrap Authors\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n:root,\n[data-bs-theme=light] {\n --bs-blue: #0d6efd;\n --bs-indigo: #6610f2;\n --bs-purple: #6f42c1;\n --bs-pink: #d63384;\n --bs-red: #dc3545;\n --bs-orange: #fd7e14;\n --bs-yellow: #ffc107;\n --bs-green: #198754;\n --bs-teal: #20c997;\n --bs-cyan: #0dcaf0;\n --bs-black: #000;\n --bs-white: #fff;\n --bs-gray: #6c757d;\n --bs-gray-dark: #343a40;\n --bs-gray-100: #f8f9fa;\n --bs-gray-200: #e9ecef;\n --bs-gray-300: #dee2e6;\n --bs-gray-400: #ced4da;\n --bs-gray-500: #adb5bd;\n --bs-gray-600: #6c757d;\n --bs-gray-700: #495057;\n --bs-gray-800: #343a40;\n --bs-gray-900: #212529;\n --bs-primary: #0d6efd;\n --bs-secondary: #6c757d;\n --bs-success: #198754;\n --bs-info: #0dcaf0;\n --bs-warning: #ffc107;\n --bs-danger: #dc3545;\n --bs-light: #f8f9fa;\n --bs-dark: #212529;\n --bs-primary-rgb: 13, 110, 253;\n --bs-secondary-rgb: 108, 117, 125;\n --bs-success-rgb: 25, 135, 84;\n --bs-info-rgb: 13, 202, 240;\n --bs-warning-rgb: 255, 193, 7;\n --bs-danger-rgb: 220, 53, 69;\n --bs-light-rgb: 248, 249, 250;\n --bs-dark-rgb: 33, 37, 41;\n --bs-primary-text-emphasis: #052c65;\n --bs-secondary-text-emphasis: #2b2f32;\n --bs-success-text-emphasis: #0a3622;\n --bs-info-text-emphasis: #055160;\n --bs-warning-text-emphasis: #664d03;\n --bs-danger-text-emphasis: #58151c;\n --bs-light-text-emphasis: #495057;\n --bs-dark-text-emphasis: #495057;\n --bs-primary-bg-subtle: #cfe2ff;\n --bs-secondary-bg-subtle: #e2e3e5;\n --bs-success-bg-subtle: #d1e7dd;\n --bs-info-bg-subtle: #cff4fc;\n --bs-warning-bg-subtle: #fff3cd;\n --bs-danger-bg-subtle: #f8d7da;\n --bs-light-bg-subtle: #fcfcfd;\n --bs-dark-bg-subtle: #ced4da;\n --bs-primary-border-subtle: #9ec5fe;\n --bs-secondary-border-subtle: #c4c8cb;\n --bs-success-border-subtle: #a3cfbb;\n --bs-info-border-subtle: #9eeaf9;\n --bs-warning-border-subtle: #ffe69c;\n --bs-danger-border-subtle: #f1aeb5;\n --bs-light-border-subtle: #e9ecef;\n --bs-dark-border-subtle: #adb5bd;\n --bs-white-rgb: 255, 255, 255;\n --bs-black-rgb: 0, 0, 0;\n --bs-font-sans-serif: system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", \"Noto Sans\", \"Liberation Sans\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));\n --bs-body-font-family: var(--bs-font-sans-serif);\n --bs-body-font-size: 1rem;\n --bs-body-font-weight: 400;\n --bs-body-line-height: 1.5;\n --bs-body-color: #212529;\n --bs-body-color-rgb: 33, 37, 41;\n --bs-body-bg: #fff;\n --bs-body-bg-rgb: 255, 255, 255;\n --bs-emphasis-color: #000;\n --bs-emphasis-color-rgb: 0, 0, 0;\n --bs-secondary-color: rgba(33, 37, 41, 0.75);\n --bs-secondary-color-rgb: 33, 37, 41;\n --bs-secondary-bg: #e9ecef;\n --bs-secondary-bg-rgb: 233, 236, 239;\n --bs-tertiary-color: rgba(33, 37, 41, 0.5);\n --bs-tertiary-color-rgb: 33, 37, 41;\n --bs-tertiary-bg: #f8f9fa;\n --bs-tertiary-bg-rgb: 248, 249, 250;\n --bs-heading-color: inherit;\n --bs-link-color: #0d6efd;\n --bs-link-color-rgb: 13, 110, 253;\n --bs-link-decoration: underline;\n --bs-link-hover-color: #0a58ca;\n --bs-link-hover-color-rgb: 10, 88, 202;\n --bs-code-color: #d63384;\n --bs-highlight-color: #212529;\n --bs-highlight-bg: #fff3cd;\n --bs-border-width: 1px;\n --bs-border-style: solid;\n --bs-border-color: #dee2e6;\n --bs-border-color-translucent: rgba(0, 0, 0, 0.175);\n --bs-border-radius: 0.375rem;\n --bs-border-radius-sm: 0.25rem;\n --bs-border-radius-lg: 0.5rem;\n --bs-border-radius-xl: 1rem;\n --bs-border-radius-xxl: 2rem;\n --bs-border-radius-2xl: var(--bs-border-radius-xxl);\n --bs-border-radius-pill: 50rem;\n --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);\n --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);\n --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);\n --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);\n --bs-focus-ring-width: 0.25rem;\n --bs-focus-ring-opacity: 0.25;\n --bs-focus-ring-color: rgba(13, 110, 253, 0.25);\n --bs-form-valid-color: #198754;\n --bs-form-valid-border-color: #198754;\n --bs-form-invalid-color: #dc3545;\n --bs-form-invalid-border-color: #dc3545;\n}\n\n[data-bs-theme=dark] {\n color-scheme: dark;\n --bs-body-color: #dee2e6;\n --bs-body-color-rgb: 222, 226, 230;\n --bs-body-bg: #212529;\n --bs-body-bg-rgb: 33, 37, 41;\n --bs-emphasis-color: #fff;\n --bs-emphasis-color-rgb: 255, 255, 255;\n --bs-secondary-color: rgba(222, 226, 230, 0.75);\n --bs-secondary-color-rgb: 222, 226, 230;\n --bs-secondary-bg: #343a40;\n --bs-secondary-bg-rgb: 52, 58, 64;\n --bs-tertiary-color: rgba(222, 226, 230, 0.5);\n --bs-tertiary-color-rgb: 222, 226, 230;\n --bs-tertiary-bg: #2b3035;\n --bs-tertiary-bg-rgb: 43, 48, 53;\n --bs-primary-text-emphasis: #6ea8fe;\n --bs-secondary-text-emphasis: #a7acb1;\n --bs-success-text-emphasis: #75b798;\n --bs-info-text-emphasis: #6edff6;\n --bs-warning-text-emphasis: #ffda6a;\n --bs-danger-text-emphasis: #ea868f;\n --bs-light-text-emphasis: #f8f9fa;\n --bs-dark-text-emphasis: #dee2e6;\n --bs-primary-bg-subtle: #031633;\n --bs-secondary-bg-subtle: #161719;\n --bs-success-bg-subtle: #051b11;\n --bs-info-bg-subtle: #032830;\n --bs-warning-bg-subtle: #332701;\n --bs-danger-bg-subtle: #2c0b0e;\n --bs-light-bg-subtle: #343a40;\n --bs-dark-bg-subtle: #1a1d20;\n --bs-primary-border-subtle: #084298;\n --bs-secondary-border-subtle: #41464b;\n --bs-success-border-subtle: #0f5132;\n --bs-info-border-subtle: #087990;\n --bs-warning-border-subtle: #997404;\n --bs-danger-border-subtle: #842029;\n --bs-light-border-subtle: #495057;\n --bs-dark-border-subtle: #343a40;\n --bs-heading-color: inherit;\n --bs-link-color: #6ea8fe;\n --bs-link-hover-color: #8bb9fe;\n --bs-link-color-rgb: 110, 168, 254;\n --bs-link-hover-color-rgb: 139, 185, 254;\n --bs-code-color: #e685b5;\n --bs-highlight-color: #dee2e6;\n --bs-highlight-bg: #664d03;\n --bs-border-color: #495057;\n --bs-border-color-translucent: rgba(255, 255, 255, 0.15);\n --bs-form-valid-color: #75b798;\n --bs-form-valid-border-color: #75b798;\n --bs-form-invalid-color: #ea868f;\n --bs-form-invalid-border-color: #ea868f;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n :root {\n scroll-behavior: smooth;\n }\n}\n\nbody {\n margin: 0;\n font-family: var(--bs-body-font-family);\n font-size: var(--bs-body-font-size);\n font-weight: var(--bs-body-font-weight);\n line-height: var(--bs-body-line-height);\n color: var(--bs-body-color);\n text-align: var(--bs-body-text-align);\n background-color: var(--bs-body-bg);\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\nhr {\n margin: 1rem 0;\n color: inherit;\n border: 0;\n border-top: var(--bs-border-width) solid;\n opacity: 0.25;\n}\n\nh6, h5, h4, h3, h2, h1 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n font-weight: 500;\n line-height: 1.2;\n color: var(--bs-heading-color);\n}\n\nh1 {\n font-size: calc(1.375rem + 1.5vw);\n}\n@media (min-width: 1200px) {\n h1 {\n font-size: 2.5rem;\n }\n}\n\nh2 {\n font-size: calc(1.325rem + 0.9vw);\n}\n@media (min-width: 1200px) {\n h2 {\n font-size: 2rem;\n }\n}\n\nh3 {\n font-size: calc(1.3rem + 0.6vw);\n}\n@media (min-width: 1200px) {\n h3 {\n font-size: 1.75rem;\n }\n}\n\nh4 {\n font-size: calc(1.275rem + 0.3vw);\n}\n@media (min-width: 1200px) {\n h4 {\n font-size: 1.5rem;\n }\n}\n\nh5 {\n font-size: 1.25rem;\n}\n\nh6 {\n font-size: 1rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title] {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n cursor: help;\n -webkit-text-decoration-skip-ink: none;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: 0.5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 0.875em;\n}\n\nmark {\n padding: 0.1875em;\n color: var(--bs-highlight-color);\n background-color: var(--bs-highlight-bg);\n}\n\nsub,\nsup {\n position: relative;\n font-size: 0.75em;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\na {\n color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));\n text-decoration: underline;\n}\na:hover {\n --bs-link-color-rgb: var(--bs-link-hover-color-rgb);\n}\n\na:not([href]):not([class]), a:not([href]):not([class]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: var(--bs-font-monospace);\n font-size: 1em;\n}\n\npre {\n display: block;\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n font-size: 0.875em;\n}\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\ncode {\n font-size: 0.875em;\n color: var(--bs-code-color);\n word-wrap: break-word;\n}\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.1875rem 0.375rem;\n font-size: 0.875em;\n color: var(--bs-body-bg);\n background-color: var(--bs-body-color);\n border-radius: 0.25rem;\n}\nkbd kbd {\n padding: 0;\n font-size: 1em;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n color: var(--bs-secondary-color);\n text-align: left;\n}\n\nth {\n text-align: inherit;\n text-align: -webkit-match-parent;\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\nlabel {\n display: inline-block;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\n[role=button] {\n cursor: pointer;\n}\n\nselect {\n word-wrap: normal;\n}\nselect:disabled {\n opacity: 1;\n}\n\n[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {\n display: none !important;\n}\n\nbutton,\n[type=button],\n[type=reset],\n[type=submit] {\n -webkit-appearance: button;\n}\nbutton:not(:disabled),\n[type=button]:not(:disabled),\n[type=reset]:not(:disabled),\n[type=submit]:not(:disabled) {\n cursor: pointer;\n}\n\n::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ntextarea {\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n float: left;\n width: 100%;\n padding: 0;\n margin-bottom: 0.5rem;\n font-size: calc(1.275rem + 0.3vw);\n line-height: inherit;\n}\n@media (min-width: 1200px) {\n legend {\n font-size: 1.5rem;\n }\n}\nlegend + * {\n clear: left;\n}\n\n::-webkit-datetime-edit-fields-wrapper,\n::-webkit-datetime-edit-text,\n::-webkit-datetime-edit-minute,\n::-webkit-datetime-edit-hour-field,\n::-webkit-datetime-edit-day-field,\n::-webkit-datetime-edit-month-field,\n::-webkit-datetime-edit-year-field {\n padding: 0;\n}\n\n::-webkit-inner-spin-button {\n height: auto;\n}\n\n[type=search] {\n -webkit-appearance: textfield;\n outline-offset: -2px;\n}\n\n/* rtl:raw:\n[type=\"tel\"],\n[type=\"url\"],\n[type=\"email\"],\n[type=\"number\"] {\n direction: ltr;\n}\n*/\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-color-swatch-wrapper {\n padding: 0;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\n::file-selector-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\niframe {\n border: 0;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[hidden] {\n display: none !important;\n}\n\n/*# sourceMappingURL=bootstrap-reboot.css.map */","// stylelint-disable scss/dimension-no-non-numeric-values\n\n// SCSS RFS mixin\n//\n// Automated responsive values for font sizes, paddings, margins and much more\n//\n// Licensed under MIT (https://github.com/twbs/rfs/blob/main/LICENSE)\n\n// Configuration\n\n// Base value\n$rfs-base-value: 1.25rem !default;\n$rfs-unit: rem !default;\n\n@if $rfs-unit != rem and $rfs-unit != px {\n @error \"`#{$rfs-unit}` is not a valid unit for $rfs-unit. Use `px` or `rem`.\";\n}\n\n// Breakpoint at where values start decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n@if $rfs-breakpoint-unit != px and $rfs-breakpoint-unit != em and $rfs-breakpoint-unit != rem {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n}\n\n// Resize values based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != number or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Mode. Possibilities: \"min-media-query\", \"max-media-query\"\n$rfs-mode: min-media-query !default;\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-rfs to false\n$enable-rfs: true !default;\n\n// Cache $rfs-base-value unit\n$rfs-base-value-unit: unit($rfs-base-value);\n\n@function divide($dividend, $divisor, $precision: 10) {\n $sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1);\n $dividend: abs($dividend);\n $divisor: abs($divisor);\n @if $dividend == 0 {\n @return 0;\n }\n @if $divisor == 0 {\n @error \"Cannot divide by 0\";\n }\n $remainder: $dividend;\n $result: 0;\n $factor: 10;\n @while ($remainder > 0 and $precision >= 0) {\n $quotient: 0;\n @while ($remainder >= $divisor) {\n $remainder: $remainder - $divisor;\n $quotient: $quotient + 1;\n }\n $result: $result * 10 + $quotient;\n $factor: $factor * .1;\n $remainder: $remainder * 10;\n $precision: $precision - 1;\n @if ($precision < 0 and $remainder >= $divisor * 5) {\n $result: $result + 1;\n }\n }\n $result: $result * $factor * $sign;\n $dividend-unit: unit($dividend);\n $divisor-unit: unit($divisor);\n $unit-map: (\n \"px\": 1px,\n \"rem\": 1rem,\n \"em\": 1em,\n \"%\": 1%\n );\n @if ($dividend-unit != $divisor-unit and map-has-key($unit-map, $dividend-unit)) {\n $result: $result * map-get($unit-map, $dividend-unit);\n }\n @return $result;\n}\n\n// Remove px-unit from $rfs-base-value for calculations\n@if $rfs-base-value-unit == px {\n $rfs-base-value: divide($rfs-base-value, $rfs-base-value * 0 + 1);\n}\n@else if $rfs-base-value-unit == rem {\n $rfs-base-value: divide($rfs-base-value, divide($rfs-base-value * 0 + 1, $rfs-rem-value));\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == px {\n $rfs-breakpoint: divide($rfs-breakpoint, $rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == rem or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: divide($rfs-breakpoint, divide($rfs-breakpoint * 0 + 1, $rfs-rem-value));\n}\n\n// Calculate the media query value\n$rfs-mq-value: if($rfs-breakpoint-unit == px, #{$rfs-breakpoint}px, #{divide($rfs-breakpoint, $rfs-rem-value)}#{$rfs-breakpoint-unit});\n$rfs-mq-property-width: if($rfs-mode == max-media-query, max-width, min-width);\n$rfs-mq-property-height: if($rfs-mode == max-media-query, max-height, min-height);\n\n// Internal mixin used to determine which media query needs to be used\n@mixin _rfs-media-query {\n @if $rfs-two-dimensional {\n @if $rfs-mode == max-media-query {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}), (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) and (#{$rfs-mq-property-height}: #{$rfs-mq-value}) {\n @content;\n }\n }\n }\n @else {\n @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) {\n @content;\n }\n }\n}\n\n// Internal mixin that adds disable classes to the selector if needed.\n@mixin _rfs-rule {\n @if $rfs-class == disable and $rfs-mode == max-media-query {\n // Adding an extra class increases specificity, which prevents the media query to override the property\n &,\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @else if $rfs-class == enable and $rfs-mode == min-media-query {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Internal mixin that adds enable classes to the selector if needed.\n@mixin _rfs-media-query-rule {\n\n @if $rfs-class == enable {\n @if $rfs-mode == min-media-query {\n @content;\n }\n\n @include _rfs-media-query () {\n .enable-rfs &,\n &.enable-rfs {\n @content;\n }\n }\n }\n @else {\n @if $rfs-class == disable and $rfs-mode == min-media-query {\n .disable-rfs &,\n &.disable-rfs {\n @content;\n }\n }\n @include _rfs-media-query () {\n @content;\n }\n }\n}\n\n// Helper function to get the formatted non-responsive value\n@function rfs-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: \"\";\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + \" 0\";\n }\n @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n @if $unit == px {\n // Convert to rem if needed\n $val: $val + \" \" + if($rfs-unit == rem, #{divide($value, $value * 0 + $rfs-rem-value)}rem, $value);\n }\n @else if $unit == rem {\n // Convert to px if needed\n $val: $val + \" \" + if($rfs-unit == px, #{divide($value, $value * 0 + 1) * $rfs-rem-value}px, $value);\n } @else {\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n $val: $val + \" \" + $value;\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// Helper function to get the responsive value calculated by RFS\n@function rfs-fluid-value($values) {\n // Convert to list\n $values: if(type-of($values) != list, ($values,), $values);\n\n $val: \"\";\n\n // Loop over each value and calculate value\n @each $value in $values {\n @if $value == 0 {\n $val: $val + \" 0\";\n } @else {\n // Cache $value unit\n $unit: if(type-of($value) == \"number\", unit($value), false);\n\n // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $unit or $unit != px and $unit != rem {\n $val: $val + \" \" + $value;\n } @else {\n // Remove unit from $value for calculations\n $value: divide($value, $value * 0 + if($unit == px, 1, divide(1, $rfs-rem-value)));\n\n // Only add the media query if the value is greater than the minimum value\n @if abs($value) <= $rfs-base-value or not $enable-rfs {\n $val: $val + \" \" + if($rfs-unit == rem, #{divide($value, $rfs-rem-value)}rem, #{$value}px);\n }\n @else {\n // Calculate the minimum value\n $value-min: $rfs-base-value + divide(abs($value) - $rfs-base-value, $rfs-factor);\n\n // Calculate difference between $value and the minimum value\n $value-diff: abs($value) - $value-min;\n\n // Base value formatting\n $min-width: if($rfs-unit == rem, #{divide($value-min, $rfs-rem-value)}rem, #{$value-min}px);\n\n // Use negative value if needed\n $min-width: if($value < 0, -$min-width, $min-width);\n\n // Use `vmin` if two-dimensional is enabled\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{divide($value-diff * 100, $rfs-breakpoint)}#{$variable-unit};\n\n // Return the calculated value\n $val: $val + \" calc(\" + $min-width + if($value < 0, \" - \", \" + \") + $variable-width + \")\";\n }\n }\n }\n }\n\n // Remove first space\n @return unquote(str-slice($val, 2));\n}\n\n// RFS mixin\n@mixin rfs($values, $property: font-size) {\n @if $values != null {\n $val: rfs-value($values);\n $fluid-val: rfs-fluid-value($values);\n\n // Do not print the media query if responsive & non-responsive values are the same\n @if $val == $fluid-val {\n #{$property}: $val;\n }\n @else {\n @include _rfs-rule () {\n #{$property}: if($rfs-mode == max-media-query, $val, $fluid-val);\n\n // Include safari iframe resize fix if needed\n min-width: if($rfs-safari-iframe-resize-bug-fix, (0 * 1vw), null);\n }\n\n @include _rfs-media-query-rule () {\n #{$property}: if($rfs-mode == max-media-query, $fluid-val, $val);\n }\n }\n }\n}\n\n// Shorthand helper mixins\n@mixin font-size($value) {\n @include rfs($value);\n}\n\n@mixin padding($value) {\n @include rfs($value, padding);\n}\n\n@mixin padding-top($value) {\n @include rfs($value, padding-top);\n}\n\n@mixin padding-right($value) {\n @include rfs($value, padding-right);\n}\n\n@mixin padding-bottom($value) {\n @include rfs($value, padding-bottom);\n}\n\n@mixin padding-left($value) {\n @include rfs($value, padding-left);\n}\n\n@mixin margin($value) {\n @include rfs($value, margin);\n}\n\n@mixin margin-top($value) {\n @include rfs($value, margin-top);\n}\n\n@mixin margin-right($value) {\n @include rfs($value, margin-right);\n}\n\n@mixin margin-bottom($value) {\n @include rfs($value, margin-bottom);\n}\n\n@mixin margin-left($value) {\n @include rfs($value, margin-left);\n}\n","// scss-docs-start color-mode-mixin\n@mixin color-mode($mode: light, $root: false) {\n @if $color-mode-type == \"media-query\" {\n @if $root == true {\n @media (prefers-color-scheme: $mode) {\n :root {\n @content;\n }\n }\n } @else {\n @media (prefers-color-scheme: $mode) {\n @content;\n }\n }\n } @else {\n [data-bs-theme=\"#{$mode}\"] {\n @content;\n }\n }\n}\n// scss-docs-end color-mode-mixin\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n @include font-size(var(--#{$prefix}root-font-size));\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$prefix}body-font-family);\n @include font-size(var(--#{$prefix}body-font-size));\n font-weight: var(--#{$prefix}body-font-weight);\n line-height: var(--#{$prefix}body-line-height);\n color: var(--#{$prefix}body-color);\n text-align: var(--#{$prefix}body-text-align);\n background-color: var(--#{$prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n border: 0;\n border-top: $hr-border-width solid $hr-border-color;\n opacity: $hr-opacity;\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: var(--#{$prefix}heading-color);\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 2. Add explicit cursor to indicate changed behavior.\n// 3. Prevent the text-decoration to be skipped.\n\nabbr[title] {\n text-decoration: underline dotted; // 1\n cursor: help; // 2\n text-decoration-skip-ink: none; // 3\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n color: var(--#{$prefix}highlight-color);\n background-color: var(--#{$prefix}highlight-bg);\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: rgba(var(--#{$prefix}link-color-rgb), var(--#{$prefix}link-opacity, 1));\n text-decoration: $link-decoration;\n\n &:hover {\n --#{$prefix}link-color-rgb: var(--#{$prefix}link-hover-color-rgb);\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: var(--#{$prefix}code-color);\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`