├── .editorconfig ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .gitpod.yml ├── LICENSE ├── README.md ├── package.json ├── pnpm-lock.yaml ├── src └── index.ts ├── tsconfig.json └── wrangler.toml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | name: Deploy 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: pnpm/action-setup@v4 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: '22' 19 | cache: 'pnpm' 20 | - name: Install dependencies 21 | run: pnpm install --frozen-lockfile 22 | - name: Publish 23 | run: pnpm run deploy 24 | env: 25 | CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }} 26 | CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Logs 5 | 6 | logs 7 | _.log 8 | npm-debug.log_ 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | .pnpm-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | 16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 | 18 | # Runtime data 19 | 20 | pids 21 | _.pid 22 | _.seed 23 | \*.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | 31 | coverage 32 | \*.lcov 33 | 34 | # nyc test coverage 35 | 36 | .nyc_output 37 | 38 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 39 | 40 | .grunt 41 | 42 | # Bower dependency directory (https://bower.io/) 43 | 44 | bower_components 45 | 46 | # node-waf configuration 47 | 48 | .lock-wscript 49 | 50 | # Compiled binary addons (https://nodejs.org/api/addons.html) 51 | 52 | build/Release 53 | 54 | # Dependency directories 55 | 56 | node_modules/ 57 | jspm_packages/ 58 | 59 | # Snowpack dependency directory (https://snowpack.dev/) 60 | 61 | web_modules/ 62 | 63 | # TypeScript cache 64 | 65 | \*.tsbuildinfo 66 | 67 | # Optional npm cache directory 68 | 69 | .npm 70 | 71 | # Optional eslint cache 72 | 73 | .eslintcache 74 | 75 | # Optional stylelint cache 76 | 77 | .stylelintcache 78 | 79 | # Microbundle cache 80 | 81 | .rpt2_cache/ 82 | .rts2_cache_cjs/ 83 | .rts2_cache_es/ 84 | .rts2_cache_umd/ 85 | 86 | # Optional REPL history 87 | 88 | .node_repl_history 89 | 90 | # Output of 'npm pack' 91 | 92 | \*.tgz 93 | 94 | # Yarn Integrity file 95 | 96 | .yarn-integrity 97 | 98 | # dotenv environment variable files 99 | 100 | .env 101 | .env.development.local 102 | .env.test.local 103 | .env.production.local 104 | .env.local 105 | 106 | # parcel-bundler cache (https://parceljs.org/) 107 | 108 | .cache 109 | .parcel-cache 110 | 111 | # Next.js build output 112 | 113 | .next 114 | out 115 | 116 | # Nuxt.js build / generate output 117 | 118 | .nuxt 119 | dist 120 | 121 | # Gatsby files 122 | 123 | .cache/ 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | .cache 139 | 140 | # Docusaurus cache and generated files 141 | 142 | .docusaurus 143 | 144 | # Serverless directories 145 | 146 | .serverless/ 147 | 148 | # FuseBox cache 149 | 150 | .fusebox/ 151 | 152 | # DynamoDB Local files 153 | 154 | .dynamodb/ 155 | 156 | # TernJS port file 157 | 158 | .tern-port 159 | 160 | # Stores VSCode versions used for testing VSCode extensions 161 | 162 | .vscode-test 163 | 164 | # yarn v2 165 | 166 | .yarn/cache 167 | .yarn/unplugged 168 | .yarn/build-state.yml 169 | .yarn/install-state.gz 170 | .pnp.\* 171 | 172 | # wrangler project 173 | 174 | .dev.vars 175 | .wrangler 176 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: pnpm install 3 | command: pnpm run dev 4 | ports: 5 | - port: 8976 6 | name: Wrangler Authorization 7 | onOpen: ignore 8 | - port: 8787 9 | name: Wrangler Dev 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Render 2 | 3 | Proxies readonly requests to [Cloudflare R2](https://developers.cloudflare.com/r2) via [Cloudflare Workers](https://workers.dev). 4 | 5 | If you want an uploader, try [Aster](https://github.com/kotx/aster)! 6 | 7 | If you see a bug or something missing, please open an issue or pull request! 8 | 9 | ## Features 10 | - File listings (with optional hidden files)! 11 | 12 | ![screenshot of file listings in light mode](https://user-images.githubusercontent.com/33439542/193165135-1dd935f5-b68b-495a-97cc-9c69c3c0ce01.png) 13 | ![screenshot of file listings in dark mode](https://user-images.githubusercontent.com/33439542/193165189-3cd4b79e-27ea-4397-bb80-f3ccf31185dc.png) 14 | 15 | 16 | - Handles `HEAD`, `GET`, and `OPTIONS` requests 17 | - Forwards caching headers (`etag`, `cache-control`, `expires`, `last-modified`) 18 | - Forwards content headers (`content-type`, `content-encoding`, `content-language`, `content-disposition`) 19 | - Caches served files using the [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/) 20 | - Ranged requests (`range`, `if-range`, returns `content-range`) 21 | - Handles precondition headers (`if-modified-since`, `if-unmodified-since`, `if-match`, `if-none-match`) 22 | - Can serve an appended path if the requested url ends with / - Defaults to `index.html` in 0.5.0 23 | - Can serve custom 404 responses if a file is not found 24 | 25 | ## Setup 26 | 27 | ### Configuration 28 | 29 | Create your R2 bucket(s) if you haven't already (replace `bucket_name` and `preview_bucket_name` appropriately): 30 | ```sh 31 | pnpm install 32 | pnpm wrangler r2 bucket create bucket_name # required 33 | pnpm wrangler r2 bucket create preview_bucket_name # optional 34 | ``` 35 | You can also do this from the [Cloudflare dashboard](https://dash.cloudflare.com/?to=/:account/r2/buckets/new). 36 | 37 | Edit `wrangler.toml` to have the correct `bucket_name` and optionally, `preview_bucket_name` (you can set it to `bucket_name`) if you're going to run this locally. 38 | You can do this from a fork, if using the [GitHub Actions method](#method-2-github-actions). 39 | 40 | You may edit `CACHE_CONTROL` to the default [`cache-control` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) or remove it entirely to fall back to nothing. If you set `CACHE_CONTROL` to `"no-store"` then Cloudflare caching will not be used. 41 | 42 | ### Deploying 43 | 44 | Note: Due to how custom domains for workers work, you MUST use a route to take advantage of caching. Cloudflare may fix this soon. 45 | Also note that \*.workers.dev domains do not cache responses. You MUST use a route to your own (sub)domain. 46 | 47 | If you want to deploy render with multiple domains for one worker, check out [multi-render](https://github.com/Erisa/multi-render)! It uses render [as a package](#using-as-a-package) to serve multiple buckets to multiple domains with custom configurations. 48 | 49 | #### Method 1 (Local) 50 | ```sh 51 | pnpm wrangler publish # or `pnpm run deploy` 52 | ``` 53 | 54 | #### Method 2 (GitHub Actions) 55 | 1. Fork this repository 56 | 2. Set the secrets [`CF_API_TOKEN`](https://dash.cloudflare.com/profile/api-tokens) (with the `Edit Cloudflare Workers 57 | ` template) and `CF_ACCOUNT_ID` in the repo settings 58 | 3. Enable workflows in the Actions tab 59 | 4. Update `wrangler.toml` as needed (this will trigger the workflow) 60 | 5. (Optionally) set the worker route in the Cloudflare dashboard to use the Cache API 61 | 62 | ## Using as a package 63 | 64 | You may use this worker's functionality as a package by installing and importing [`render2`](https://www.npmjs.com/package/render2): 65 | ```sh 66 | npm install render2 67 | ``` 68 | Usage: 69 | ```js 70 | import render from "render2"; 71 | render.fetch(req, env, ctx); 72 | ``` 73 | 74 | You can see an awesome example with [Erisa](https://github.com/Erisa)'s [multi-render](https://github.com/Erisa/multi-render)! 75 | 76 | ## Development 77 | 78 | Install deps: 79 | ```sh 80 | pnpm install 81 | ``` 82 | 83 | To launch the development server: 84 | ```sh 85 | pnpm run dev 86 | ``` 87 | 88 | ## Notable Related Projects 89 | 90 | - [auravoid](https://github.com/auravoid)'s [fork](https://github.com/auravoid/render) adds [Plausible](https://plausible.io) support. 91 | - [Erisa](https://github.com/Erisa)'s project [multi-render](https://github.com/erisa/multi-render) add support for domain-specific configurations. 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "render2", 3 | "version": "1.5.0", 4 | "author": "kotx", 5 | "description": "A Cloudflare worker for proxying readonly requests to Cloudflare R2", 6 | "license": "MIT", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "dev": "wrangler dev --remote", 14 | "deploy": "wrangler deploy", 15 | "prepublishOnly": "tsc" 16 | }, 17 | "devDependencies": { 18 | "@cloudflare/workers-types": "^4.20250529.0", 19 | "@types/range-parser": "^1.2.7", 20 | "typescript": "^5.8.3", 21 | "wrangler": "^4.18.0" 22 | }, 23 | "dependencies": { 24 | "range-parser": "^1.2.1" 25 | }, 26 | "packageManager": "pnpm@8.15.9+sha512.499434c9d8fdd1a2794ebf4552b3b25c0a633abcee5bb15e7b5de90f32f47b513aca98cd5cfd001c31f0db454bc3804edccd578501e4ca293a6816166bbd9f81" 27 | } 28 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | range-parser: 9 | specifier: ^1.2.1 10 | version: 1.2.1 11 | 12 | devDependencies: 13 | '@cloudflare/workers-types': 14 | specifier: ^4.20250529.0 15 | version: 4.20250529.0 16 | '@types/range-parser': 17 | specifier: ^1.2.7 18 | version: 1.2.7 19 | typescript: 20 | specifier: ^5.8.3 21 | version: 5.8.3 22 | wrangler: 23 | specifier: ^4.18.0 24 | version: 4.18.0(@cloudflare/workers-types@4.20250529.0) 25 | 26 | packages: 27 | 28 | /@cloudflare/kv-asset-handler@0.4.0: 29 | resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} 30 | engines: {node: '>=18.0.0'} 31 | dependencies: 32 | mime: 3.0.0 33 | dev: true 34 | 35 | /@cloudflare/unenv-preset@2.3.2(unenv@2.0.0-rc.17)(workerd@1.20250525.0): 36 | resolution: {integrity: sha512-MtUgNl+QkQyhQvv5bbWP+BpBC1N0me4CHHuP2H4ktmOMKdB/6kkz/lo+zqiA4mEazb4y+1cwyNjVrQ2DWeE4mg==} 37 | peerDependencies: 38 | unenv: 2.0.0-rc.17 39 | workerd: ^1.20250508.0 40 | peerDependenciesMeta: 41 | workerd: 42 | optional: true 43 | dependencies: 44 | unenv: 2.0.0-rc.17 45 | workerd: 1.20250525.0 46 | dev: true 47 | 48 | /@cloudflare/workerd-darwin-64@1.20250525.0: 49 | resolution: {integrity: sha512-L5l+7sSJJT2+riR5rS3Q3PKNNySPjWfRIeaNGMVRi1dPO6QPi4lwuxfRUFNoeUdilZJUVPfSZvTtj9RedsKznQ==} 50 | engines: {node: '>=16'} 51 | cpu: [x64] 52 | os: [darwin] 53 | requiresBuild: true 54 | dev: true 55 | optional: true 56 | 57 | /@cloudflare/workerd-darwin-arm64@1.20250525.0: 58 | resolution: {integrity: sha512-Y3IbIdrF/vJWh/WBvshwcSyUh175VAiLRW7963S1dXChrZ1N5wuKGQm9xY69cIGVtitpMJWWW3jLq7J/Xxwm0Q==} 59 | engines: {node: '>=16'} 60 | cpu: [arm64] 61 | os: [darwin] 62 | requiresBuild: true 63 | dev: true 64 | optional: true 65 | 66 | /@cloudflare/workerd-linux-64@1.20250525.0: 67 | resolution: {integrity: sha512-KSyQPAby+c6cpENoO0ayCQlY6QIh28l/+QID7VC1SLXfiNHy+hPNsH1vVBTST6CilHVAQSsy9tCZ9O9XECB8yg==} 68 | engines: {node: '>=16'} 69 | cpu: [x64] 70 | os: [linux] 71 | requiresBuild: true 72 | dev: true 73 | optional: true 74 | 75 | /@cloudflare/workerd-linux-arm64@1.20250525.0: 76 | resolution: {integrity: sha512-Nt0FUxS2kQhJUea4hMCNPaetkrAFDhPnNX/ntwcqVlGgnGt75iaAhupWJbU0GB+gIWlKeuClUUnDZqKbicoKyg==} 77 | engines: {node: '>=16'} 78 | cpu: [arm64] 79 | os: [linux] 80 | requiresBuild: true 81 | dev: true 82 | optional: true 83 | 84 | /@cloudflare/workerd-windows-64@1.20250525.0: 85 | resolution: {integrity: sha512-mwTj+9f3uIa4NEXR1cOa82PjLa6dbrb3J+KCVJFYIaq7e63VxEzOchCXS4tublT2pmOhmFqkgBMXrxozxNkR2Q==} 86 | engines: {node: '>=16'} 87 | cpu: [x64] 88 | os: [win32] 89 | requiresBuild: true 90 | dev: true 91 | optional: true 92 | 93 | /@cloudflare/workers-types@4.20250529.0: 94 | resolution: {integrity: sha512-l6tVFpI6MUChMD0wK+Jhikb+aCbrmIR58CVpV/BhRT4THjl+nFhTT5N5ZqX42FDXdE3hCPLjueBMpPRhPUOB2A==} 95 | dev: true 96 | 97 | /@cspotcode/source-map-support@0.8.1: 98 | resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 99 | engines: {node: '>=12'} 100 | dependencies: 101 | '@jridgewell/trace-mapping': 0.3.9 102 | dev: true 103 | 104 | /@emnapi/runtime@1.4.3: 105 | resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} 106 | requiresBuild: true 107 | dependencies: 108 | tslib: 2.8.1 109 | dev: true 110 | optional: true 111 | 112 | /@esbuild/aix-ppc64@0.25.4: 113 | resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} 114 | engines: {node: '>=18'} 115 | cpu: [ppc64] 116 | os: [aix] 117 | requiresBuild: true 118 | dev: true 119 | optional: true 120 | 121 | /@esbuild/android-arm64@0.25.4: 122 | resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} 123 | engines: {node: '>=18'} 124 | cpu: [arm64] 125 | os: [android] 126 | requiresBuild: true 127 | dev: true 128 | optional: true 129 | 130 | /@esbuild/android-arm@0.25.4: 131 | resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} 132 | engines: {node: '>=18'} 133 | cpu: [arm] 134 | os: [android] 135 | requiresBuild: true 136 | dev: true 137 | optional: true 138 | 139 | /@esbuild/android-x64@0.25.4: 140 | resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} 141 | engines: {node: '>=18'} 142 | cpu: [x64] 143 | os: [android] 144 | requiresBuild: true 145 | dev: true 146 | optional: true 147 | 148 | /@esbuild/darwin-arm64@0.25.4: 149 | resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} 150 | engines: {node: '>=18'} 151 | cpu: [arm64] 152 | os: [darwin] 153 | requiresBuild: true 154 | dev: true 155 | optional: true 156 | 157 | /@esbuild/darwin-x64@0.25.4: 158 | resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} 159 | engines: {node: '>=18'} 160 | cpu: [x64] 161 | os: [darwin] 162 | requiresBuild: true 163 | dev: true 164 | optional: true 165 | 166 | /@esbuild/freebsd-arm64@0.25.4: 167 | resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} 168 | engines: {node: '>=18'} 169 | cpu: [arm64] 170 | os: [freebsd] 171 | requiresBuild: true 172 | dev: true 173 | optional: true 174 | 175 | /@esbuild/freebsd-x64@0.25.4: 176 | resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} 177 | engines: {node: '>=18'} 178 | cpu: [x64] 179 | os: [freebsd] 180 | requiresBuild: true 181 | dev: true 182 | optional: true 183 | 184 | /@esbuild/linux-arm64@0.25.4: 185 | resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} 186 | engines: {node: '>=18'} 187 | cpu: [arm64] 188 | os: [linux] 189 | requiresBuild: true 190 | dev: true 191 | optional: true 192 | 193 | /@esbuild/linux-arm@0.25.4: 194 | resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} 195 | engines: {node: '>=18'} 196 | cpu: [arm] 197 | os: [linux] 198 | requiresBuild: true 199 | dev: true 200 | optional: true 201 | 202 | /@esbuild/linux-ia32@0.25.4: 203 | resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} 204 | engines: {node: '>=18'} 205 | cpu: [ia32] 206 | os: [linux] 207 | requiresBuild: true 208 | dev: true 209 | optional: true 210 | 211 | /@esbuild/linux-loong64@0.25.4: 212 | resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} 213 | engines: {node: '>=18'} 214 | cpu: [loong64] 215 | os: [linux] 216 | requiresBuild: true 217 | dev: true 218 | optional: true 219 | 220 | /@esbuild/linux-mips64el@0.25.4: 221 | resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} 222 | engines: {node: '>=18'} 223 | cpu: [mips64el] 224 | os: [linux] 225 | requiresBuild: true 226 | dev: true 227 | optional: true 228 | 229 | /@esbuild/linux-ppc64@0.25.4: 230 | resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} 231 | engines: {node: '>=18'} 232 | cpu: [ppc64] 233 | os: [linux] 234 | requiresBuild: true 235 | dev: true 236 | optional: true 237 | 238 | /@esbuild/linux-riscv64@0.25.4: 239 | resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} 240 | engines: {node: '>=18'} 241 | cpu: [riscv64] 242 | os: [linux] 243 | requiresBuild: true 244 | dev: true 245 | optional: true 246 | 247 | /@esbuild/linux-s390x@0.25.4: 248 | resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} 249 | engines: {node: '>=18'} 250 | cpu: [s390x] 251 | os: [linux] 252 | requiresBuild: true 253 | dev: true 254 | optional: true 255 | 256 | /@esbuild/linux-x64@0.25.4: 257 | resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} 258 | engines: {node: '>=18'} 259 | cpu: [x64] 260 | os: [linux] 261 | requiresBuild: true 262 | dev: true 263 | optional: true 264 | 265 | /@esbuild/netbsd-arm64@0.25.4: 266 | resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} 267 | engines: {node: '>=18'} 268 | cpu: [arm64] 269 | os: [netbsd] 270 | requiresBuild: true 271 | dev: true 272 | optional: true 273 | 274 | /@esbuild/netbsd-x64@0.25.4: 275 | resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} 276 | engines: {node: '>=18'} 277 | cpu: [x64] 278 | os: [netbsd] 279 | requiresBuild: true 280 | dev: true 281 | optional: true 282 | 283 | /@esbuild/openbsd-arm64@0.25.4: 284 | resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} 285 | engines: {node: '>=18'} 286 | cpu: [arm64] 287 | os: [openbsd] 288 | requiresBuild: true 289 | dev: true 290 | optional: true 291 | 292 | /@esbuild/openbsd-x64@0.25.4: 293 | resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} 294 | engines: {node: '>=18'} 295 | cpu: [x64] 296 | os: [openbsd] 297 | requiresBuild: true 298 | dev: true 299 | optional: true 300 | 301 | /@esbuild/sunos-x64@0.25.4: 302 | resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} 303 | engines: {node: '>=18'} 304 | cpu: [x64] 305 | os: [sunos] 306 | requiresBuild: true 307 | dev: true 308 | optional: true 309 | 310 | /@esbuild/win32-arm64@0.25.4: 311 | resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} 312 | engines: {node: '>=18'} 313 | cpu: [arm64] 314 | os: [win32] 315 | requiresBuild: true 316 | dev: true 317 | optional: true 318 | 319 | /@esbuild/win32-ia32@0.25.4: 320 | resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} 321 | engines: {node: '>=18'} 322 | cpu: [ia32] 323 | os: [win32] 324 | requiresBuild: true 325 | dev: true 326 | optional: true 327 | 328 | /@esbuild/win32-x64@0.25.4: 329 | resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} 330 | engines: {node: '>=18'} 331 | cpu: [x64] 332 | os: [win32] 333 | requiresBuild: true 334 | dev: true 335 | optional: true 336 | 337 | /@fastify/busboy@2.1.1: 338 | resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} 339 | engines: {node: '>=14'} 340 | dev: true 341 | 342 | /@img/sharp-darwin-arm64@0.33.5: 343 | resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} 344 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 345 | cpu: [arm64] 346 | os: [darwin] 347 | requiresBuild: true 348 | optionalDependencies: 349 | '@img/sharp-libvips-darwin-arm64': 1.0.4 350 | dev: true 351 | optional: true 352 | 353 | /@img/sharp-darwin-x64@0.33.5: 354 | resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} 355 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 356 | cpu: [x64] 357 | os: [darwin] 358 | requiresBuild: true 359 | optionalDependencies: 360 | '@img/sharp-libvips-darwin-x64': 1.0.4 361 | dev: true 362 | optional: true 363 | 364 | /@img/sharp-libvips-darwin-arm64@1.0.4: 365 | resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} 366 | cpu: [arm64] 367 | os: [darwin] 368 | requiresBuild: true 369 | dev: true 370 | optional: true 371 | 372 | /@img/sharp-libvips-darwin-x64@1.0.4: 373 | resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} 374 | cpu: [x64] 375 | os: [darwin] 376 | requiresBuild: true 377 | dev: true 378 | optional: true 379 | 380 | /@img/sharp-libvips-linux-arm64@1.0.4: 381 | resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} 382 | cpu: [arm64] 383 | os: [linux] 384 | requiresBuild: true 385 | dev: true 386 | optional: true 387 | 388 | /@img/sharp-libvips-linux-arm@1.0.5: 389 | resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} 390 | cpu: [arm] 391 | os: [linux] 392 | requiresBuild: true 393 | dev: true 394 | optional: true 395 | 396 | /@img/sharp-libvips-linux-s390x@1.0.4: 397 | resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} 398 | cpu: [s390x] 399 | os: [linux] 400 | requiresBuild: true 401 | dev: true 402 | optional: true 403 | 404 | /@img/sharp-libvips-linux-x64@1.0.4: 405 | resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} 406 | cpu: [x64] 407 | os: [linux] 408 | requiresBuild: true 409 | dev: true 410 | optional: true 411 | 412 | /@img/sharp-libvips-linuxmusl-arm64@1.0.4: 413 | resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} 414 | cpu: [arm64] 415 | os: [linux] 416 | requiresBuild: true 417 | dev: true 418 | optional: true 419 | 420 | /@img/sharp-libvips-linuxmusl-x64@1.0.4: 421 | resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} 422 | cpu: [x64] 423 | os: [linux] 424 | requiresBuild: true 425 | dev: true 426 | optional: true 427 | 428 | /@img/sharp-linux-arm64@0.33.5: 429 | resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} 430 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 431 | cpu: [arm64] 432 | os: [linux] 433 | requiresBuild: true 434 | optionalDependencies: 435 | '@img/sharp-libvips-linux-arm64': 1.0.4 436 | dev: true 437 | optional: true 438 | 439 | /@img/sharp-linux-arm@0.33.5: 440 | resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} 441 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 442 | cpu: [arm] 443 | os: [linux] 444 | requiresBuild: true 445 | optionalDependencies: 446 | '@img/sharp-libvips-linux-arm': 1.0.5 447 | dev: true 448 | optional: true 449 | 450 | /@img/sharp-linux-s390x@0.33.5: 451 | resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} 452 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 453 | cpu: [s390x] 454 | os: [linux] 455 | requiresBuild: true 456 | optionalDependencies: 457 | '@img/sharp-libvips-linux-s390x': 1.0.4 458 | dev: true 459 | optional: true 460 | 461 | /@img/sharp-linux-x64@0.33.5: 462 | resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} 463 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 464 | cpu: [x64] 465 | os: [linux] 466 | requiresBuild: true 467 | optionalDependencies: 468 | '@img/sharp-libvips-linux-x64': 1.0.4 469 | dev: true 470 | optional: true 471 | 472 | /@img/sharp-linuxmusl-arm64@0.33.5: 473 | resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} 474 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 475 | cpu: [arm64] 476 | os: [linux] 477 | requiresBuild: true 478 | optionalDependencies: 479 | '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 480 | dev: true 481 | optional: true 482 | 483 | /@img/sharp-linuxmusl-x64@0.33.5: 484 | resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} 485 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 486 | cpu: [x64] 487 | os: [linux] 488 | requiresBuild: true 489 | optionalDependencies: 490 | '@img/sharp-libvips-linuxmusl-x64': 1.0.4 491 | dev: true 492 | optional: true 493 | 494 | /@img/sharp-wasm32@0.33.5: 495 | resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} 496 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 497 | cpu: [wasm32] 498 | requiresBuild: true 499 | dependencies: 500 | '@emnapi/runtime': 1.4.3 501 | dev: true 502 | optional: true 503 | 504 | /@img/sharp-win32-ia32@0.33.5: 505 | resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} 506 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 507 | cpu: [ia32] 508 | os: [win32] 509 | requiresBuild: true 510 | dev: true 511 | optional: true 512 | 513 | /@img/sharp-win32-x64@0.33.5: 514 | resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} 515 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 516 | cpu: [x64] 517 | os: [win32] 518 | requiresBuild: true 519 | dev: true 520 | optional: true 521 | 522 | /@jridgewell/resolve-uri@3.1.2: 523 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 524 | engines: {node: '>=6.0.0'} 525 | dev: true 526 | 527 | /@jridgewell/sourcemap-codec@1.5.0: 528 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 529 | dev: true 530 | 531 | /@jridgewell/trace-mapping@0.3.9: 532 | resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 533 | dependencies: 534 | '@jridgewell/resolve-uri': 3.1.2 535 | '@jridgewell/sourcemap-codec': 1.5.0 536 | dev: true 537 | 538 | /@types/range-parser@1.2.7: 539 | resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} 540 | dev: true 541 | 542 | /acorn-walk@8.3.2: 543 | resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} 544 | engines: {node: '>=0.4.0'} 545 | dev: true 546 | 547 | /acorn@8.14.0: 548 | resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} 549 | engines: {node: '>=0.4.0'} 550 | hasBin: true 551 | dev: true 552 | 553 | /as-table@1.0.55: 554 | resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} 555 | dependencies: 556 | printable-characters: 1.0.42 557 | dev: true 558 | 559 | /blake3-wasm@2.1.5: 560 | resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} 561 | dev: true 562 | 563 | /color-convert@2.0.1: 564 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 565 | engines: {node: '>=7.0.0'} 566 | requiresBuild: true 567 | dependencies: 568 | color-name: 1.1.4 569 | dev: true 570 | 571 | /color-name@1.1.4: 572 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 573 | requiresBuild: true 574 | dev: true 575 | 576 | /color-string@1.9.1: 577 | resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} 578 | requiresBuild: true 579 | dependencies: 580 | color-name: 1.1.4 581 | simple-swizzle: 0.2.2 582 | dev: true 583 | 584 | /color@4.2.3: 585 | resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} 586 | engines: {node: '>=12.5.0'} 587 | requiresBuild: true 588 | dependencies: 589 | color-convert: 2.0.1 590 | color-string: 1.9.1 591 | dev: true 592 | 593 | /cookie@0.7.2: 594 | resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} 595 | engines: {node: '>= 0.6'} 596 | dev: true 597 | 598 | /data-uri-to-buffer@2.0.2: 599 | resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} 600 | dev: true 601 | 602 | /defu@6.1.4: 603 | resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} 604 | dev: true 605 | 606 | /detect-libc@2.0.4: 607 | resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} 608 | engines: {node: '>=8'} 609 | requiresBuild: true 610 | dev: true 611 | 612 | /esbuild@0.25.4: 613 | resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} 614 | engines: {node: '>=18'} 615 | hasBin: true 616 | requiresBuild: true 617 | optionalDependencies: 618 | '@esbuild/aix-ppc64': 0.25.4 619 | '@esbuild/android-arm': 0.25.4 620 | '@esbuild/android-arm64': 0.25.4 621 | '@esbuild/android-x64': 0.25.4 622 | '@esbuild/darwin-arm64': 0.25.4 623 | '@esbuild/darwin-x64': 0.25.4 624 | '@esbuild/freebsd-arm64': 0.25.4 625 | '@esbuild/freebsd-x64': 0.25.4 626 | '@esbuild/linux-arm': 0.25.4 627 | '@esbuild/linux-arm64': 0.25.4 628 | '@esbuild/linux-ia32': 0.25.4 629 | '@esbuild/linux-loong64': 0.25.4 630 | '@esbuild/linux-mips64el': 0.25.4 631 | '@esbuild/linux-ppc64': 0.25.4 632 | '@esbuild/linux-riscv64': 0.25.4 633 | '@esbuild/linux-s390x': 0.25.4 634 | '@esbuild/linux-x64': 0.25.4 635 | '@esbuild/netbsd-arm64': 0.25.4 636 | '@esbuild/netbsd-x64': 0.25.4 637 | '@esbuild/openbsd-arm64': 0.25.4 638 | '@esbuild/openbsd-x64': 0.25.4 639 | '@esbuild/sunos-x64': 0.25.4 640 | '@esbuild/win32-arm64': 0.25.4 641 | '@esbuild/win32-ia32': 0.25.4 642 | '@esbuild/win32-x64': 0.25.4 643 | dev: true 644 | 645 | /exit-hook@2.2.1: 646 | resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} 647 | engines: {node: '>=6'} 648 | dev: true 649 | 650 | /exsolve@1.0.5: 651 | resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==} 652 | dev: true 653 | 654 | /fsevents@2.3.3: 655 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 656 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 657 | os: [darwin] 658 | requiresBuild: true 659 | dev: true 660 | optional: true 661 | 662 | /get-source@2.0.12: 663 | resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} 664 | dependencies: 665 | data-uri-to-buffer: 2.0.2 666 | source-map: 0.6.1 667 | dev: true 668 | 669 | /glob-to-regexp@0.4.1: 670 | resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} 671 | dev: true 672 | 673 | /is-arrayish@0.3.2: 674 | resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} 675 | requiresBuild: true 676 | dev: true 677 | 678 | /mime@3.0.0: 679 | resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} 680 | engines: {node: '>=10.0.0'} 681 | hasBin: true 682 | dev: true 683 | 684 | /miniflare@4.20250525.0: 685 | resolution: {integrity: sha512-F5XRDn9WqxUaHphUT8qwy5WXC/3UwbBRJTdjjP5uwHX82vypxIlHNyHziZnplPLhQa1kbSdIY7wfuP1XJyyYZw==} 686 | engines: {node: '>=18.0.0'} 687 | hasBin: true 688 | dependencies: 689 | '@cspotcode/source-map-support': 0.8.1 690 | acorn: 8.14.0 691 | acorn-walk: 8.3.2 692 | exit-hook: 2.2.1 693 | glob-to-regexp: 0.4.1 694 | sharp: 0.33.5 695 | stoppable: 1.1.0 696 | undici: 5.29.0 697 | workerd: 1.20250525.0 698 | ws: 8.18.0 699 | youch: 3.3.4 700 | zod: 3.22.3 701 | transitivePeerDependencies: 702 | - bufferutil 703 | - utf-8-validate 704 | dev: true 705 | 706 | /mustache@4.2.0: 707 | resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} 708 | hasBin: true 709 | dev: true 710 | 711 | /ohash@2.0.11: 712 | resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} 713 | dev: true 714 | 715 | /path-to-regexp@6.3.0: 716 | resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} 717 | dev: true 718 | 719 | /pathe@2.0.3: 720 | resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 721 | dev: true 722 | 723 | /printable-characters@1.0.42: 724 | resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} 725 | dev: true 726 | 727 | /range-parser@1.2.1: 728 | resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} 729 | engines: {node: '>= 0.6'} 730 | dev: false 731 | 732 | /semver@7.7.2: 733 | resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} 734 | engines: {node: '>=10'} 735 | hasBin: true 736 | dev: true 737 | 738 | /sharp@0.33.5: 739 | resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} 740 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 741 | requiresBuild: true 742 | dependencies: 743 | color: 4.2.3 744 | detect-libc: 2.0.4 745 | semver: 7.7.2 746 | optionalDependencies: 747 | '@img/sharp-darwin-arm64': 0.33.5 748 | '@img/sharp-darwin-x64': 0.33.5 749 | '@img/sharp-libvips-darwin-arm64': 1.0.4 750 | '@img/sharp-libvips-darwin-x64': 1.0.4 751 | '@img/sharp-libvips-linux-arm': 1.0.5 752 | '@img/sharp-libvips-linux-arm64': 1.0.4 753 | '@img/sharp-libvips-linux-s390x': 1.0.4 754 | '@img/sharp-libvips-linux-x64': 1.0.4 755 | '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 756 | '@img/sharp-libvips-linuxmusl-x64': 1.0.4 757 | '@img/sharp-linux-arm': 0.33.5 758 | '@img/sharp-linux-arm64': 0.33.5 759 | '@img/sharp-linux-s390x': 0.33.5 760 | '@img/sharp-linux-x64': 0.33.5 761 | '@img/sharp-linuxmusl-arm64': 0.33.5 762 | '@img/sharp-linuxmusl-x64': 0.33.5 763 | '@img/sharp-wasm32': 0.33.5 764 | '@img/sharp-win32-ia32': 0.33.5 765 | '@img/sharp-win32-x64': 0.33.5 766 | dev: true 767 | 768 | /simple-swizzle@0.2.2: 769 | resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} 770 | requiresBuild: true 771 | dependencies: 772 | is-arrayish: 0.3.2 773 | dev: true 774 | 775 | /source-map@0.6.1: 776 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 777 | engines: {node: '>=0.10.0'} 778 | dev: true 779 | 780 | /stacktracey@2.1.8: 781 | resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} 782 | dependencies: 783 | as-table: 1.0.55 784 | get-source: 2.0.12 785 | dev: true 786 | 787 | /stoppable@1.1.0: 788 | resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} 789 | engines: {node: '>=4', npm: '>=6'} 790 | dev: true 791 | 792 | /tslib@2.8.1: 793 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 794 | requiresBuild: true 795 | dev: true 796 | optional: true 797 | 798 | /typescript@5.8.3: 799 | resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} 800 | engines: {node: '>=14.17'} 801 | hasBin: true 802 | dev: true 803 | 804 | /ufo@1.6.1: 805 | resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} 806 | dev: true 807 | 808 | /undici@5.29.0: 809 | resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} 810 | engines: {node: '>=14.0'} 811 | dependencies: 812 | '@fastify/busboy': 2.1.1 813 | dev: true 814 | 815 | /unenv@2.0.0-rc.17: 816 | resolution: {integrity: sha512-B06u0wXkEd+o5gOCMl/ZHl5cfpYbDZKAT+HWTL+Hws6jWu7dCiqBBXXXzMFcFVJb8D4ytAnYmxJA83uwOQRSsg==} 817 | dependencies: 818 | defu: 6.1.4 819 | exsolve: 1.0.5 820 | ohash: 2.0.11 821 | pathe: 2.0.3 822 | ufo: 1.6.1 823 | dev: true 824 | 825 | /workerd@1.20250525.0: 826 | resolution: {integrity: sha512-SXJgLREy/Aqw2J71Oah0Pbu+SShbqbTExjVQyRBTM1r7MG7fS5NUlknhnt6sikjA/t4cO09Bi8OJqHdTkrcnYQ==} 827 | engines: {node: '>=16'} 828 | hasBin: true 829 | requiresBuild: true 830 | optionalDependencies: 831 | '@cloudflare/workerd-darwin-64': 1.20250525.0 832 | '@cloudflare/workerd-darwin-arm64': 1.20250525.0 833 | '@cloudflare/workerd-linux-64': 1.20250525.0 834 | '@cloudflare/workerd-linux-arm64': 1.20250525.0 835 | '@cloudflare/workerd-windows-64': 1.20250525.0 836 | dev: true 837 | 838 | /wrangler@4.18.0(@cloudflare/workers-types@4.20250529.0): 839 | resolution: {integrity: sha512-/ng0KI9io97SNsBU1rheADBLLTE5Djybgsi4gXuvH1RBKJGpyj1xWvZ2fuWu8vAonit3EiZkwtERTm6kESHP3A==} 840 | engines: {node: '>=18.0.0'} 841 | hasBin: true 842 | peerDependencies: 843 | '@cloudflare/workers-types': ^4.20250525.0 844 | peerDependenciesMeta: 845 | '@cloudflare/workers-types': 846 | optional: true 847 | dependencies: 848 | '@cloudflare/kv-asset-handler': 0.4.0 849 | '@cloudflare/unenv-preset': 2.3.2(unenv@2.0.0-rc.17)(workerd@1.20250525.0) 850 | '@cloudflare/workers-types': 4.20250529.0 851 | blake3-wasm: 2.1.5 852 | esbuild: 0.25.4 853 | miniflare: 4.20250525.0 854 | path-to-regexp: 6.3.0 855 | unenv: 2.0.0-rc.17 856 | workerd: 1.20250525.0 857 | optionalDependencies: 858 | fsevents: 2.3.3 859 | transitivePeerDependencies: 860 | - bufferutil 861 | - utf-8-validate 862 | dev: true 863 | 864 | /ws@8.18.0: 865 | resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} 866 | engines: {node: '>=10.0.0'} 867 | peerDependencies: 868 | bufferutil: ^4.0.1 869 | utf-8-validate: '>=5.0.2' 870 | peerDependenciesMeta: 871 | bufferutil: 872 | optional: true 873 | utf-8-validate: 874 | optional: true 875 | dev: true 876 | 877 | /youch@3.3.4: 878 | resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} 879 | dependencies: 880 | cookie: 0.7.2 881 | mustache: 4.2.0 882 | stacktracey: 2.1.8 883 | dev: true 884 | 885 | /zod@3.22.3: 886 | resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} 887 | dev: true 888 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import parseRange from "range-parser"; 2 | 3 | export interface Env { 4 | R2_BUCKET: R2Bucket; 5 | ALLOWED_ORIGINS?: string; 6 | CACHE_CONTROL?: string; 7 | PATH_PREFIX?: string; 8 | INDEX_FILE?: string; 9 | NOTFOUND_FILE?: string; 10 | DIRECTORY_LISTING?: boolean; 11 | ITEMS_PER_PAGE?: number; 12 | HIDE_HIDDEN_FILES?: boolean; 13 | DIRECTORY_CACHE_CONTROL?: string; 14 | LOGGING?: boolean; 15 | R2_RETRIES?: number; 16 | } 17 | 18 | const units = ["B", "KB", "MB", "GB", "TB"]; 19 | 20 | type ParsedRange = { offset: number; length: number } | { suffix: number }; 21 | 22 | function rangeHasLength( 23 | object: ParsedRange 24 | ): object is { offset: number; length: number } { 25 | return (<{ offset: number; length: number }>object).length !== undefined; 26 | } 27 | 28 | function hasBody(object: R2Object | R2ObjectBody): object is R2ObjectBody { 29 | return (object).body !== undefined; 30 | } 31 | 32 | function hasSuffix(range: ParsedRange): range is { suffix: number } { 33 | return (<{ suffix: number }>range).suffix !== undefined; 34 | } 35 | 36 | function getRangeHeader(range: ParsedRange, fileSize: number): string { 37 | return `bytes ${hasSuffix(range) ? fileSize - range.suffix : range.offset}-${ 38 | hasSuffix(range) ? fileSize - 1 : range.offset + range.length - 1 39 | }/${fileSize}`; 40 | } 41 | 42 | // some ideas for this were taken from / inspired by 43 | // https://github.com/cloudflare/workerd/blob/main/samples/static-files-from-disk/static.js 44 | async function makeListingResponse( 45 | path: string, 46 | env: Env, 47 | request: Request 48 | ): Promise { 49 | if (path === "/") path = ""; 50 | else if (path !== "" && !path.endsWith("/")) { 51 | path += "/"; 52 | } 53 | let cursor = new URL(request.url).searchParams.get("cursor") || undefined; 54 | let listing = await env.R2_BUCKET.list({ 55 | prefix: path, 56 | delimiter: "/", 57 | cursor, 58 | limit: env.ITEMS_PER_PAGE || 1000, 59 | }); 60 | 61 | if (listing.delimitedPrefixes.length === 0 && listing.objects.length === 0) { 62 | return null; 63 | } 64 | 65 | let html: string = ""; 66 | let lastModified: Date | null = null; 67 | 68 | if (request.method === "GET") { 69 | let htmlList = []; 70 | 71 | if (path !== "") { 72 | htmlList.push( 73 | ` ` + 74 | `../` + 75 | `--` 76 | ); 77 | } 78 | 79 | for (let dir of listing.delimitedPrefixes) { 80 | if (dir.endsWith("/")) dir = dir.substring(0, dir.length - 1); 81 | let name = dir.substring(path.length, dir.length); 82 | if (name.startsWith(".") && env.HIDE_HIDDEN_FILES) continue; 83 | htmlList.push( 84 | ` ` + 85 | `${name}/` + 86 | `--` 87 | ); 88 | } 89 | for (let file of listing.objects) { 90 | let name = file.key.substring(path.length, file.key.length); 91 | if (name.startsWith(".") && env.HIDE_HIDDEN_FILES) continue; 92 | 93 | let dateStr = file.uploaded.toISOString(); 94 | dateStr = dateStr.split(".")[0].replace("T", " "); 95 | dateStr = dateStr.slice(0, dateStr.lastIndexOf(":")) + "Z"; 96 | 97 | htmlList.push( 98 | ` ` + 99 | `${name}` + 100 | `${dateStr}${niceBytes(file.size)}` 101 | ); 102 | 103 | if (lastModified == null || file.uploaded > lastModified) { 104 | lastModified = file.uploaded; 105 | } 106 | } 107 | 108 | if (listing.truncated) { 109 | htmlList.push( 110 | ` ` + 111 | `...see more.../` + 112 | `--` 113 | ); 114 | } 115 | 116 | if (path === "") path = "/"; 117 | 118 | html = ` 119 | 120 | 121 | Index of ${path} 122 | 123 | 124 | 142 | 143 | 144 |

Index of ${path}

145 | 146 | 147 | ${htmlList.join("\n")} 148 |
FilenameModifiedSize
149 | 150 | 151 | `; 152 | } 153 | 154 | return new Response(html === "" ? null : html, { 155 | status: 200, 156 | headers: { 157 | "access-control-allow-origin": env.ALLOWED_ORIGINS || "", 158 | "last-modified": lastModified === null ? "" : lastModified.toUTCString(), 159 | "content-type": "text/html", 160 | "cache-control": env.DIRECTORY_CACHE_CONTROL || "no-store", 161 | }, 162 | }); 163 | } 164 | 165 | async function retryAsync(env: Env, fn: () => Promise): Promise { 166 | const maxAttempts = env.R2_RETRIES || 0; 167 | let attempts = 0; 168 | 169 | while (maxAttempts == -1 || attempts <= maxAttempts) { 170 | try { 171 | return await fn(); 172 | } catch (err) { 173 | attempts++; 174 | if (env.LOGGING) console.error(`Attempt ${attempts} failed:`, err); 175 | 176 | if (attempts <= maxAttempts) { 177 | const delay = Math.min(1000 * Math.pow(2, attempts - 1), 30000); 178 | await new Promise((resolve) => setTimeout(resolve, delay)); 179 | } else { 180 | throw err; 181 | } 182 | } 183 | } 184 | throw new Error("unreachable"); 185 | } 186 | 187 | export default { 188 | async fetch( 189 | request: Request, 190 | env: Env, 191 | ctx: ExecutionContext 192 | ): Promise { 193 | const allowedMethods = ["GET", "HEAD", "OPTIONS"]; 194 | if (allowedMethods.indexOf(request.method) === -1) { 195 | return new Response("Method Not Allowed", { 196 | status: 405, 197 | headers: { allow: allowedMethods.join(", ") }, 198 | }); 199 | } 200 | 201 | if (request.method === "OPTIONS") { 202 | return new Response(null, { 203 | headers: { allow: allowedMethods.join(", ") }, 204 | }); 205 | } 206 | 207 | let triedIndex = false; 208 | 209 | let response: Response | undefined; 210 | 211 | const isCachingEnabled = env.CACHE_CONTROL !== "no-store"; 212 | const cache = caches.default; 213 | if (isCachingEnabled) { 214 | response = await cache.match(request); 215 | } 216 | 217 | // Since we produce this result from the request, we don't need to strictly use an R2Range 218 | let range: ParsedRange | undefined; 219 | 220 | if (!response || !(response.ok || response.status == 304)) { 221 | if (env.LOGGING) { 222 | console.warn("Cache MISS for", request.url); 223 | } 224 | const url = new URL(request.url); 225 | let path = (env.PATH_PREFIX || "") + decodeURIComponent(url.pathname); 226 | 227 | // directory logic 228 | if (path.endsWith("/")) { 229 | // if theres an index file, try that. 404 logic down below has dir fallback. 230 | if (env.INDEX_FILE && env.INDEX_FILE !== "") { 231 | path += env.INDEX_FILE; 232 | triedIndex = true; 233 | } else if (env.DIRECTORY_LISTING) { 234 | // return the dir listing 235 | let listResponse = await makeListingResponse(path, env, request); 236 | 237 | if (listResponse !== null) { 238 | if (listResponse.headers.get("cache-control") !== "no-store") { 239 | ctx.waitUntil(cache.put(request, listResponse.clone())); 240 | } 241 | return listResponse; 242 | } 243 | } 244 | } 245 | 246 | if (path !== "/" && path.startsWith("/")) { 247 | path = path.substring(1); 248 | } 249 | 250 | let file: R2Object | R2ObjectBody | null | undefined; 251 | 252 | // Range handling 253 | if (request.method === "GET") { 254 | const rangeHeader = request.headers.get("range"); 255 | if (rangeHeader) { 256 | file = await retryAsync(env, () => env.R2_BUCKET.head(path)); 257 | if (file === null) 258 | return new Response("File Not Found", { status: 404 }); 259 | const parsedRanges = parseRange(file.size, rangeHeader); 260 | // R2 only supports 1 range at the moment, reject if there is more than one 261 | if ( 262 | parsedRanges !== -1 && 263 | parsedRanges !== -2 && 264 | parsedRanges.length === 1 && 265 | parsedRanges.type === "bytes" 266 | ) { 267 | let firstRange = parsedRanges[0]; 268 | range = 269 | file.size === firstRange.end + 1 270 | ? { suffix: file.size - firstRange.start } 271 | : { 272 | offset: firstRange.start, 273 | length: firstRange.end - firstRange.start + 1, 274 | }; 275 | } else { 276 | return new Response("Range Not Satisfiable", { status: 416 }); 277 | } 278 | } 279 | } 280 | 281 | // Etag/If-(Not)-Match handling 282 | // R2 requires that etag checks must not contain quotes, and the S3 spec only allows one etag 283 | // This silently ignores invalid or weak (W/) headers 284 | const getHeaderEtag = (header: string | null) => 285 | header?.trim().replace(/^['"]|['"]$/g, ""); 286 | const ifMatch = getHeaderEtag(request.headers.get("if-match")); 287 | const ifNoneMatch = getHeaderEtag(request.headers.get("if-none-match")); 288 | 289 | const ifModifiedSince = Date.parse( 290 | request.headers.get("if-modified-since") || "" 291 | ); 292 | const ifUnmodifiedSince = Date.parse( 293 | request.headers.get("if-unmodified-since") || "" 294 | ); 295 | 296 | const ifRange = request.headers.get("if-range"); 297 | if (range && ifRange && file) { 298 | const maybeDate = Date.parse(ifRange); 299 | 300 | if (isNaN(maybeDate) || new Date(maybeDate) > file.uploaded) { 301 | // httpEtag already has quotes, no need to use getHeaderEtag 302 | if (ifRange.startsWith("W/") || ifRange !== file.httpEtag) 303 | range = undefined; 304 | } 305 | } 306 | 307 | if (ifMatch || ifUnmodifiedSince) { 308 | file = await retryAsync(env, () => 309 | env.R2_BUCKET.get(path, { 310 | onlyIf: { 311 | etagMatches: ifMatch, 312 | uploadedBefore: ifUnmodifiedSince 313 | ? new Date(ifUnmodifiedSince) 314 | : undefined, 315 | }, 316 | range, 317 | }) 318 | ); 319 | 320 | if (file && !hasBody(file)) { 321 | return new Response("Precondition Failed", { status: 412 }); 322 | } 323 | } 324 | 325 | if (ifNoneMatch || ifModifiedSince) { 326 | // if-none-match overrides if-modified-since completely 327 | if (ifNoneMatch) { 328 | file = await retryAsync(env, () => 329 | env.R2_BUCKET.get(path, { 330 | onlyIf: { etagDoesNotMatch: ifNoneMatch }, 331 | range, 332 | }) 333 | ); 334 | } else if (ifModifiedSince) { 335 | file = await retryAsync(env, () => 336 | env.R2_BUCKET.get(path, { 337 | onlyIf: { uploadedAfter: new Date(ifModifiedSince) }, 338 | range, 339 | }) 340 | ); 341 | } 342 | if (file && !hasBody(file)) { 343 | return new Response(null, { status: 304 }); 344 | } 345 | } 346 | 347 | file = 348 | request.method === "HEAD" 349 | ? await retryAsync(env, () => env.R2_BUCKET.head(path)) 350 | : file && hasBody(file) 351 | ? file 352 | : await retryAsync(env, () => env.R2_BUCKET.get(path, { range })); 353 | 354 | let notFound: boolean = false; 355 | 356 | if (file === null) { 357 | if (env.INDEX_FILE && triedIndex) { 358 | // remove the index file since it doesn't exist 359 | path = path.substring(0, path.length - env.INDEX_FILE.length); 360 | } 361 | 362 | if (env.DIRECTORY_LISTING && (path.endsWith("/") || path === "")) { 363 | // return the dir listing 364 | let listResponse = await makeListingResponse(path, env, request); 365 | 366 | if (listResponse !== null) { 367 | if (listResponse.headers.get("cache-control") !== "no-store") { 368 | ctx.waitUntil(cache.put(request, listResponse.clone())); 369 | } 370 | return listResponse; 371 | } 372 | } 373 | 374 | if (env.NOTFOUND_FILE && env.NOTFOUND_FILE != "") { 375 | notFound = true; 376 | path = env.NOTFOUND_FILE; 377 | file = 378 | request.method === "HEAD" 379 | ? await retryAsync(env, () => env.R2_BUCKET.head(path)) 380 | : await retryAsync(env, () => env.R2_BUCKET.get(path)); 381 | } 382 | 383 | // if it's still null, either 404 is disabled or that file wasn't found either 384 | // this isn't an else because then there would have to be two of them 385 | if (file == null) { 386 | return new Response("File Not Found", { status: 404 }); 387 | } 388 | } 389 | 390 | // Content-Length handling 391 | let body; 392 | let contentLength = file.size; 393 | if (hasBody(file) && file.size !== 0) { 394 | if (range && !notFound) { 395 | contentLength = rangeHasLength(range) ? range.length : range.suffix; 396 | } 397 | let { readable, writable } = new FixedLengthStream(contentLength); 398 | file.body.pipeTo(writable); 399 | body = readable; 400 | } 401 | response = new Response(body, { 402 | status: notFound ? 404 : range ? 206 : 200, 403 | headers: { 404 | "accept-ranges": "bytes", 405 | "access-control-allow-origin": env.ALLOWED_ORIGINS || "", 406 | 407 | etag: notFound ? "" : file.httpEtag, 408 | // if the 404 file has a custom cache control, we respect it 409 | "cache-control": 410 | file.httpMetadata?.cacheControl ?? 411 | (notFound ? "" : env.CACHE_CONTROL || ""), 412 | expires: file.httpMetadata?.cacheExpiry?.toUTCString() ?? "", 413 | "last-modified": notFound ? "" : file.uploaded.toUTCString(), 414 | 415 | "content-encoding": file.httpMetadata?.contentEncoding ?? "", 416 | "content-type": 417 | file.httpMetadata?.contentType ?? "application/octet-stream", 418 | "content-language": file.httpMetadata?.contentLanguage ?? "", 419 | "content-disposition": file.httpMetadata?.contentDisposition ?? "", 420 | "content-range": 421 | range && !notFound ? getRangeHeader(range, file.size) : "", 422 | "content-length": contentLength.toString(), 423 | }, 424 | }); 425 | 426 | if (request.method === "GET" && !range && isCachingEnabled && !notFound) 427 | ctx.waitUntil(cache.put(request, response.clone())); 428 | } else { 429 | if (env.LOGGING) { 430 | console.warn("Cache HIT for", request.url); 431 | } 432 | } 433 | 434 | return response; 435 | }, 436 | }; 437 | 438 | function niceBytes(x: number) { 439 | let l = 0, 440 | n = parseInt(x.toString(), 10) || 0; 441 | 442 | while (n >= 1000 && ++l) { 443 | n = n / 1000; 444 | } 445 | 446 | return n.toFixed(n < 10 && l > 0 ? 1 : 0) + " " + units[l]; 447 | } 448 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 13 | "lib": [ 14 | "es2021" 15 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | /* Modules */ 26 | "module": "es2022" /* Specify what module code is generated. */, 27 | "rootDir": "./src" /* Specify the root folder within your source files. */, 28 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 29 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 30 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 31 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 32 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 33 | "types": [ 34 | "@cloudflare/workers-types" 35 | ] /* Specify type package names to be included without being referenced in a source file. */, 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | "resolveJsonModule": true /* Enable importing .json files */, 38 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 39 | /* JavaScript Support */ 40 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, 41 | "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | /* Emit */ 44 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 45 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 46 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 47 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 48 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 49 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 50 | // "removeComments": true, /* Disable emitting comments. */ 51 | // "noEmit": true /* Disable emitting files from a compilation. */, 52 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 53 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 54 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 55 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 56 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 57 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 58 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 59 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 60 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 61 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 62 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 63 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 64 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 65 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 66 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 67 | /* Interop Constraints */ 68 | "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, 69 | "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, 70 | // "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 71 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 72 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 73 | /* Type Checking */ 74 | "strict": true /* Enable all strict type-checking options. */, 75 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 76 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 77 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 78 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 79 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 80 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 81 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 82 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 83 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 84 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 85 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 86 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 87 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 88 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 89 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 90 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 91 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 92 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 93 | /* Completeness */ 94 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 95 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "render" 2 | main = "src/index.ts" 3 | compatibility_date = "2022-05-15" 4 | # Set this to false if you don't want to use the default *.workers.dev route. 5 | # Note that *.workers.dev routes don't support native worker-level caching: https://developers.cloudflare.com/workers/runtime-apis/cache/ 6 | workers_dev = true 7 | 8 | [vars] 9 | # The `access-control-allow-origin` header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin 10 | # Optional, the `access-control-allow-origin` header is omitted if unset, which blocks all cross-origin requests. 11 | ALLOWED_ORIGINS = "" 12 | 13 | # The `cache-control` header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control. 14 | # Optional, the `cache-control` header is omitted if unset, which would NOT disable caching: https://developers.cloudflare.com/workers/runtime-apis/cache/#headers 15 | # For example, you can disable all cache by setting this to `no-store`. 16 | CACHE_CONTROL = "max-age=86400" 17 | 18 | # The string to prepend to each file path. Optional, nothing is prepended to the path if unset. 19 | PATH_PREFIX = "" 20 | 21 | # Index file to search for on directory requests, set to "" to disable indexes 22 | # Relative to the directory of the request. 23 | #INDEX_FILE = "" 24 | INDEX_FILE = "index.html" 25 | 26 | # File to fall back to when the requested path is not found in the bucket. 27 | # Incurs an additional read operation for 404 requests. 28 | # Set to "" to disable custom 404 fallbacks. 29 | # Relative to the root of the bucket. 30 | NOTFOUND_FILE = "" 31 | #NOTFOUND_FILE = "404.html" 32 | 33 | # Enable to show a directory listing fallback on paths ending in / 34 | # If INDEX_FILE is also provided, it will be used instead if the file exists. 35 | DIRECTORY_LISTING = false 36 | 37 | # The number of items to show per page in directory listings. 38 | # Listings may also return less if the listing API call reaches a size limit. 39 | # Maximum of 1000. 40 | ITEMS_PER_PAGE = 1000 41 | 42 | # Enable to hide files or directories beginning with . from directory listings. 43 | HIDE_HIDDEN_FILES = false 44 | 45 | # Set a cache header here, e.g. "max-age=86400", if you want to cache directory listings. 46 | DIRECTORY_CACHE_CONTROL = "no-store" 47 | 48 | # Set debugging log enabled. 49 | LOGGING = true 50 | 51 | # Set the number of retries allowed for each R2 operation (-1 for unlimited). 52 | R2_RETRIES = 0 53 | 54 | [observability] 55 | enabled = true 56 | 57 | [[r2_buckets]] 58 | binding = "R2_BUCKET" 59 | bucket_name = "kot" # Set this to your R2 bucket name. Required 60 | preview_bucket_name = "kot" # Set this to your preview R2 bucket name. Can be equal to bucket_name. Optional 61 | #jurisdiction = "eu" # Set this to "eu" or "us". Optional if the bucket has no jurisdiction restrictions 62 | --------------------------------------------------------------------------------