Compare commits

..

1 Commits
worker ... ci

Author SHA1 Message Date
abersheeran
7c715682f7 Add CI workflow for Node.js 2023-12-18 16:26:52 +08:00
8 changed files with 271 additions and 356 deletions

15
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,15 @@
name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "20.x"
- run: npm install

View File

@ -1,5 +1,5 @@
{
"printWidth": 120,
"printWidth": 140,
"singleQuote": true,
"semi": true,
"useTabs": true

View File

@ -4,24 +4,22 @@
Use Cloudflare Workers to provide a WebDav interface for Cloudflare R2.
## Usage
## Configuration
Change wrangler.toml to your own.
```toml
[[r2_buckets]]
binding = 'bucket' # <~ valid JavaScript variable name, don't change this
binding = 'webdav' # <~ valid JavaScript variable name, don't change this
bucket_name = 'webdav'
[vars]
USERNAME = "USERNAME"
PASSWORD = "PASSWORD"
```
Then use wrangler to deploy.
```bash
wrangler deploy
wrangler secret put USERNAME
wrangler secret put PASSWORD
```
* USERNAME: The username of WebDav.
* PASSWORD: The password of WebDav.
## Development

267
package-lock.json generated
View File

@ -9,24 +9,23 @@
"version": "0.0.0",
"devDependencies": {
"@cloudflare/workers-types": "^4.20231121.0",
"prettier": "3.1.1",
"typescript": "^5.0.4",
"wrangler": "^3.0.0"
}
},
"node_modules/@cloudflare/kv-asset-handler": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.1.tgz",
"integrity": "sha512-lKN2XCfKCmpKb86a1tl4GIwsJYDy9TGuwjhDELLmpKygQhw8X2xR4dusgpC5Tg7q1pB96Eb0rBo81kxSILQMwA==",
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.2.0.tgz",
"integrity": "sha512-MVbXLbTcAotOPUj0pAMhVtJ+3/kFkwJqc5qNOleOZTv6QkZZABDMS21dSrSlVswEHwrpWC03e4fWytjqKvuE2A==",
"dev": true,
"dependencies": {
"mime": "^3.0.0"
}
},
"node_modules/@cloudflare/workerd-darwin-64": {
"version": "1.20240223.1",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20240223.1.tgz",
"integrity": "sha512-GgHnvkazLFZ7bmR96+dTX0+WS13a+5CHOOP3qNUSR9oEnR4hHzpNIO75MuZsm9RPAXrvtT7nSJmYwiGCZXh6og==",
"version": "1.20231030.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20231030.0.tgz",
"integrity": "sha512-J4PQ9utPxLya9yHdMMx3AZeC5M/6FxcoYw6jo9jbDDFTy+a4Gslqf4Im9We3aeOEdPXa3tgQHVQOSelJSZLhIw==",
"cpu": [
"x64"
],
@ -40,9 +39,9 @@
}
},
"node_modules/@cloudflare/workerd-darwin-arm64": {
"version": "1.20240223.1",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240223.1.tgz",
"integrity": "sha512-ZF98vUmVlC0EVEd3RRuhMq4HYWFcqmPtMIMPUN2+ivEHR92TE+6E/AvdeE6wcE7fKHQ+fk3dH+ZgB0GcfptfnA==",
"version": "1.20231030.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20231030.0.tgz",
"integrity": "sha512-WSJJjm11Del4hSneiNB7wTXGtBXI4QMCH9l5qf4iT5PAW8cESGcCmdHtWDWDtGAAGcvmLT04KNvmum92vRKKQQ==",
"cpu": [
"arm64"
],
@ -56,9 +55,9 @@
}
},
"node_modules/@cloudflare/workerd-linux-64": {
"version": "1.20240223.1",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20240223.1.tgz",
"integrity": "sha512-1kH41ewNTGMmAk2zUX0Xj9VSfidl26GQ0ZrWMdi5kwf6gAHd3oVWNigJN078Jx56SgQxNcqVGX1LunqF949asw==",
"version": "1.20231030.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20231030.0.tgz",
"integrity": "sha512-2HUeRTvoCC17fxE0qdBeR7J9dO8j4A8ZbdcvY8pZxdk+zERU6+N03RTbk/dQMU488PwiDvcC3zZqS4gwLfVT8g==",
"cpu": [
"x64"
],
@ -72,9 +71,9 @@
}
},
"node_modules/@cloudflare/workerd-linux-arm64": {
"version": "1.20240223.1",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20240223.1.tgz",
"integrity": "sha512-Ro8Og5C4evh890JrRm0B8sHyumRtgL+mUqPeNcEsyG45jAQy5xHpapHnmJAMJV6ah+zDc1cZtQq+en39SojXvQ==",
"version": "1.20231030.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20231030.0.tgz",
"integrity": "sha512-4/GK5zHh+9JbUI6Z5xTCM0ZmpKKHk7vu9thmHjUxtz+o8Ne9DoD7DlDvXQWgMF6XGaTubDWyp3ttn+Qv8jDFuQ==",
"cpu": [
"arm64"
],
@ -88,9 +87,9 @@
}
},
"node_modules/@cloudflare/workerd-windows-64": {
"version": "1.20240223.1",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20240223.1.tgz",
"integrity": "sha512-eNP5sfaP6WL07DaoigYou5ASPF7jHsFiNzzD2vGOI7yFd5sPlb7sJ4SpIy+BCX0LdqFnjmlUo5Xr+/I6qJ2Nww==",
"version": "1.20231030.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20231030.0.tgz",
"integrity": "sha512-fb/Jgj8Yqy3PO1jLhk7mTrHMkR8jklpbQFud6rL/aMAn5d6MQbaSrYOCjzkKGp0Zng8D2LIzSl+Fc0C9Sggxjg==",
"cpu": [
"x64"
],
@ -104,9 +103,9 @@
}
},
"node_modules/@cloudflare/workers-types": {
"version": "4.20240222.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20240222.0.tgz",
"integrity": "sha512-luO0BdK3rLlCv3B240+cTrfqm+XSbHtpk+88aJtGwzyVK9QF/Xz8lBgE/oZZLN8nCTmOvxAZnszyxUuZ8GP8Cg==",
"version": "4.20231121.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20231121.0.tgz",
"integrity": "sha512-+kWfpCkqiepwAKXyHoE0gnkPgkLhz0/9HOBIGhHRsUvUKvhUtm3mbqqoGRWgF1qcjzrDUBbrrOq4MYHfFtc2RA==",
"dev": true
},
"node_modules/@cspotcode/source-map-support": {
@ -496,18 +495,18 @@
}
},
"node_modules/@fastify/busboy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz",
"integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==",
"dev": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
"dev": true,
"engines": {
"node": ">=6.0.0"
@ -530,27 +529,27 @@
}
},
"node_modules/@types/node": {
"version": "20.11.24",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz",
"integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==",
"version": "20.10.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz",
"integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/node-forge": {
"version": "1.3.11",
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz",
"integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==",
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.10.tgz",
"integrity": "sha512-y6PJDYN4xYBxwd22l+OVH35N+1fCYWiuC3aiP2SlXVE6Lo7SS+rSx9r89hLxrP4pn6n1lBGhHJ12pj3F3Mpttw==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"version": "8.11.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
"integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
@ -560,9 +559,9 @@
}
},
"node_modules/acorn-walk": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
"integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz",
"integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==",
"dev": true,
"engines": {
"node": ">=0.4.0"
@ -606,17 +605,23 @@
"dev": true
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"dependencies": {
"fill-range": "^7.1.1"
"fill-range": "^7.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"node_modules/capnp-ts": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/capnp-ts/-/capnp-ts-0.7.0.tgz",
@ -628,10 +633,16 @@
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@ -644,9 +655,6 @@
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
@ -751,9 +759,9 @@
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
@ -776,15 +784,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-source": {
"version": "2.0.12",
"resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz",
@ -813,18 +812,6 @@
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"dev": true
},
"node_modules/hasown": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
"integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -837,18 +824,6 @@
"node": ">=8"
}
},
"node_modules/is-core-module": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
"dev": true,
"dependencies": {
"hasown": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -901,20 +876,20 @@
}
},
"node_modules/miniflare": {
"version": "3.20240223.0",
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240223.0.tgz",
"integrity": "sha512-8T/36FEfvsL4aMF7SLZ28v+PQL0jsUlVw/u114GYcdobkyPax9E6Ahn0XePOHEqLxQSndwPee+eS1phHANFePA==",
"version": "3.20231030.4",
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20231030.4.tgz",
"integrity": "sha512-7MBz0ArLuDop1WJGZC6tFgN6c5MRyDOIlxbm3yp0TRBpvDS/KsTuWCQcCjsxN4QQ5zvL3JTkuIZbQzRRw/j6ow==",
"dev": true,
"dependencies": {
"@cspotcode/source-map-support": "0.8.1",
"acorn": "^8.8.0",
"acorn-walk": "^8.2.0",
"capnp-ts": "^0.7.0",
"exit-hook": "^2.2.1",
"glob-to-regexp": "^0.4.1",
"source-map-support": "0.5.21",
"stoppable": "^1.1.0",
"undici": "^5.28.2",
"workerd": "1.20240223.1",
"undici": "^5.22.1",
"workerd": "1.20231030.0",
"ws": "^8.11.0",
"youch": "^3.2.2",
"zod": "^3.20.6"
@ -977,12 +952,6 @@
"node": ">=0.10.0"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/path-to-regexp": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
@ -1001,21 +970,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/prettier": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz",
"integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/printable-characters": {
"version": "1.0.42",
"resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz",
@ -1034,23 +988,6 @@
"node": ">=8.10.0"
}
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"dev": true,
"dependencies": {
"is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve.exports": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz",
@ -1112,6 +1049,16 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
@ -1139,18 +1086,6 @@
"npm": ">=6"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -1183,9 +1118,9 @@
}
},
"node_modules/undici": {
"version": "5.28.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
"integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
"version": "5.28.2",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.2.tgz",
"integrity": "sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==",
"dev": true,
"dependencies": {
"@fastify/busboy": "^2.0.0"
@ -1201,9 +1136,9 @@
"dev": true
},
"node_modules/workerd": {
"version": "1.20240223.1",
"resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20240223.1.tgz",
"integrity": "sha512-Mo1fwdp6DLva4/fWdL09ZdYllkO45I4YpWG5PbF/YUGFlu2aMk24fmU6Pd6fo5/cWek4F+n3LmYEKKHfqjiJIA==",
"version": "1.20231030.0",
"resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20231030.0.tgz",
"integrity": "sha512-+FSW+d31f8RrjHanFf/R9A+Z0csf3OtsvzdPmAKuwuZm/5HrBv83cvG9fFeTxl7/nI6irUUXIRF9xcj/NomQzQ==",
"dev": true,
"hasInstallScript": true,
"bin": {
@ -1213,29 +1148,29 @@
"node": ">=16"
},
"optionalDependencies": {
"@cloudflare/workerd-darwin-64": "1.20240223.1",
"@cloudflare/workerd-darwin-arm64": "1.20240223.1",
"@cloudflare/workerd-linux-64": "1.20240223.1",
"@cloudflare/workerd-linux-arm64": "1.20240223.1",
"@cloudflare/workerd-windows-64": "1.20240223.1"
"@cloudflare/workerd-darwin-64": "1.20231030.0",
"@cloudflare/workerd-darwin-arm64": "1.20231030.0",
"@cloudflare/workerd-linux-64": "1.20231030.0",
"@cloudflare/workerd-linux-arm64": "1.20231030.0",
"@cloudflare/workerd-windows-64": "1.20231030.0"
}
},
"node_modules/wrangler": {
"version": "3.30.1",
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.30.1.tgz",
"integrity": "sha512-cT6Ezx8h2v5QiI0HWhnHVy32ng4omdMVdhaMQLuMnyMIHmyDoRg7pmrbhtZfj0663gExLdVtE4ucK//yncVTwg==",
"version": "3.21.0",
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.21.0.tgz",
"integrity": "sha512-DLoo4XfjeyuGRAVWZFHmU1jWnZIfyLGDm6Ika9oy/CLCPfJzVJvf2jI70EU5BlEHWDZXMSJKw7FDdgSqwhaQXg==",
"dev": true,
"dependencies": {
"@cloudflare/kv-asset-handler": "0.3.1",
"@cloudflare/kv-asset-handler": "^0.2.0",
"@cspotcode/source-map-support": "0.8.1",
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"blake3-wasm": "^2.1.5",
"chokidar": "^3.5.3",
"esbuild": "0.17.19",
"miniflare": "3.20240223.0",
"miniflare": "3.20231030.4",
"nanoid": "^3.3.3",
"path-to-regexp": "^6.2.0",
"resolve": "^1.22.8",
"resolve.exports": "^2.0.2",
"selfsigned": "^2.0.1",
"source-map": "0.6.1",
@ -1250,20 +1185,12 @@
},
"optionalDependencies": {
"fsevents": "~2.3.2"
},
"peerDependencies": {
"@cloudflare/workers-types": "^4.20230914.0"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
"optional": true
}
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"version": "8.15.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.15.1.tgz",
"integrity": "sha512-W5OZiCjXEmk0yZ66ZN82beM5Sz7l7coYxpRkzS+p9PP+ToQry8szKh+61eNktr7EA9DOwvFGhfC605jDHbP6QQ==",
"dev": true,
"engines": {
"node": ">=10.0.0"

View File

@ -5,13 +5,10 @@
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"start": "wrangler dev",
"lint-perttier": "prettier . --write",
"check-perttier": "prettier . --check"
"start": "wrangler dev"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20231121.0",
"prettier": "3.1.1",
"typescript": "^5.0.4",
"wrangler": "^3.0.0"
},

View File

@ -46,9 +46,22 @@ async function* listAll(bucket: R2Bucket, prefix: string, isRecursive: boolean =
if (r2_objects.truncated) {
cursor = r2_objects.cursor;
}
} while (r2_objects.truncated);
} while (r2_objects.truncated)
}
const DAV_CLASS = "1";
const SUPPORT_METHODS = [
"OPTIONS",
"PROPFIND",
"MKCOL",
"GET",
"HEAD",
"PUT",
"COPY",
"MOVE",
];
type DavProperties = {
creationdate: string | undefined;
displayname: string | undefined;
@ -58,7 +71,7 @@ type DavProperties = {
getetag: string | undefined;
getlastmodified: string | undefined;
resourcetype: string;
};
}
function fromR2Object(object: R2Object | null | undefined): DavProperties {
if (object === null || object === undefined) {
@ -66,11 +79,11 @@ function fromR2Object(object: R2Object | null | undefined): DavProperties {
creationdate: new Date().toUTCString(),
displayname: undefined,
getcontentlanguage: undefined,
getcontentlength: '0',
getcontentlength: "0",
getcontenttype: undefined,
getetag: undefined,
getlastmodified: new Date().toUTCString(),
resourcetype: '<collection />',
resourcetype: "<collection />",
};
}
@ -86,12 +99,23 @@ function fromR2Object(object: R2Object | null | undefined): DavProperties {
};
}
function make_resource_path(request: Request): string {
let path = new URL(request.url).pathname.slice(1);
path = path.endsWith('/') ? path.slice(0, -1) : path;
return path;
}
async function handle_options(request: Request, bucket: R2Bucket): Promise<Response> {
return new Response(null, {
status: 204,
headers: {
'DAV': DAV_CLASS,
'Allow': SUPPORT_METHODS.join(', '),
}
});
}
async function handle_head(request: Request, bucket: R2Bucket): Promise<Response> {
let response = await handle_get(request, bucket);
return new Response(null, {
@ -109,15 +133,12 @@ async function handle_get(request: Request, bucket: R2Bucket): Promise<Response>
if (resource_path !== '') page += `<a href="../">..</a><br>`;
for await (const object of listAll(bucket, resource_path)) {
if (object.key === resource_path) {
continue;
continue
}
let href = `/${object.key + (object.customMetadata?.resourcetype === '<collection />' ? '/' : '')}`;
page += `<a href="${href}">${object.httpMetadata?.contentDisposition ?? object.key}</a><br>`;
}
return new Response(page, {
status: 200,
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
return new Response(page, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
} else {
let object = await bucket.get(resource_path, {
onlyIf: request.headers,
@ -126,70 +147,40 @@ async function handle_get(request: Request, bucket: R2Bucket): Promise<Response>
let isR2ObjectBody = (object: R2Object | R2ObjectBody): object is R2ObjectBody => {
return 'body' in object;
};
}
if (object === null) {
return new Response('Not Found', { status: 404 });
} else if (!isR2ObjectBody(object)) {
return new Response('Precondition Failed', { status: 412 });
return new Response("Precondition Failed", { status: 412 });
} else {
const { rangeOffset, rangeEnd } = calcContentRange(object);
const contentLength = rangeEnd - rangeOffset + 1;
return new Response(object.body, {
status: (object.range && contentLength !== object.size) ? 206 : 200,
status: object.range ? 206 : 200,
headers: {
'Content-Type': object.httpMetadata?.contentType ?? 'application/octet-stream',
'Content-Length': contentLength.toString(),
...({ 'Content-Range': `bytes ${rangeOffset}-${rangeEnd}/${object.size}` }),
...(object.httpMetadata?.contentDisposition
? {
// TODO: Content-Length, Content-Range
...(object.httpMetadata?.contentDisposition ? {
'Content-Disposition': object.httpMetadata.contentDisposition,
}
: {}),
...(object.httpMetadata?.contentEncoding
? {
} : {}),
...(object.httpMetadata?.contentEncoding ? {
'Content-Encoding': object.httpMetadata.contentEncoding,
}
: {}),
...(object.httpMetadata?.contentLanguage
? {
} : {}),
...(object.httpMetadata?.contentLanguage ? {
'Content-Language': object.httpMetadata.contentLanguage,
}
: {}),
...(object.httpMetadata?.cacheControl
? {
} : {}),
...(object.httpMetadata?.cacheControl ? {
'Cache-Control': object.httpMetadata.cacheControl,
}
: {}),
...(object.httpMetadata?.cacheExpiry
? {
} : {}),
...(object.httpMetadata?.cacheExpiry ? {
'Cache-Expiry': object.httpMetadata.cacheExpiry.toISOString(),
} : {}),
}
: {}),
},
});
}
}
}
function calcContentRange(object: R2ObjectBody) {
let rangeOffset = 0;
let rangeEnd = object.size - 1;
if (object.range) {
if ('suffix' in object.range) {
// Case 3: {suffix: number}
rangeOffset = object.size - object.range.suffix;
} else {
// Case 1: {offset: number, length?: number}
// Case 2: {offset?: number, length: number}
rangeOffset = object.range.offset ?? 0;
let length = object.range.length ?? (object.size - rangeOffset);
rangeEnd = Math.min(rangeOffset + length - 1, object.size - 1);
}
}
return { rangeOffset, rangeEnd };
}
async function handle_put(request: Request, bucket: R2Bucket): Promise<Response> {
if (request.url.endsWith('/')) {
return new Response('Method Not Allowed', { status: 405 });
@ -218,11 +209,10 @@ async function handle_delete(request: Request, bucket: R2Bucket): Promise<Respon
let resource_path = make_resource_path(request);
if (resource_path === '') {
let r2_objects,
cursor: string | undefined = undefined;
let r2_objects, cursor: string | undefined = undefined;
do {
r2_objects = await bucket.list({ cursor: cursor });
let keys = r2_objects.objects.map((object) => object.key);
let keys = r2_objects.objects.map(object => object.key);
if (keys.length > 0) {
await bucket.delete(keys);
}
@ -244,14 +234,13 @@ async function handle_delete(request: Request, bucket: R2Bucket): Promise<Respon
return new Response(null, { status: 204 });
}
let r2_objects,
cursor: string | undefined = undefined;
let r2_objects, cursor: string | undefined = undefined;
do {
r2_objects = await bucket.list({
prefix: resource_path + '/',
prefix: resource_path + "/",
cursor: cursor,
});
let keys = r2_objects.objects.map((object) => object.key);
let keys = r2_objects.objects.map(object => object.key);
if (keys.length > 0) {
await bucket.delete(keys);
}
@ -278,22 +267,28 @@ async function handle_mkcol(request: Request, bucket: R2Bucket): Promise<Respons
}
// Check if the parent directory exists
let parent_dir = resource_path.split('/').slice(0, -1).join('/');
let parent_dir = resource_path.split('/').slice(0, -1).join("/");
if (parent_dir !== '' && !(await bucket.head(parent_dir))) {
if (parent_dir !== '' && !await bucket.head(parent_dir)) {
return new Response('Conflict', { status: 409 });
}
await bucket.put(resource_path, new Uint8Array(), {
httpMetadata: request.headers,
customMetadata: { resourcetype: '<collection />' },
customMetadata: { resourcetype: '<collection />' }
});
return new Response('', { status: 201 });
}
function generate_propfind_response(object: R2Object | null): string {
if (object === null) {
return `
async function handle_propfind(request: Request, bucket: R2Bucket): Promise<Response> {
let resource_path = make_resource_path(request);
let is_collection: boolean;
let page = `<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:">`;
if (resource_path === "") {
page += `
<response>
<href>/</href>
<propstat>
@ -306,10 +301,38 @@ function generate_propfind_response(object: R2Object | null): string {
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>`;
is_collection = true;
} else {
let object = await bucket.head(resource_path);
if (object === null) {
return new Response('Not Found', { status: 404 });
}
is_collection = object.customMetadata?.resourcetype === '<collection />';
let href = `/${object.key + (object.customMetadata?.resourcetype === '<collection />' ? '/' : '')}`;
return `
page += `
<response>
<href>${href}</href>
<propstat>
<prop>
${Object.entries(fromR2Object(object))
.filter(([_, value]) => value !== undefined)
.map(([key, value]) => `<${key}>${value}</${key}>`)
.join('\n ')}
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>`
};
if (is_collection) {
let depth = request.headers.get('Depth') ?? 'infinity';
switch (depth) {
case '0': break;
case '1': {
let prefix = resource_path === "" ? resource_path : resource_path + '/';
for await (let object of listAll(bucket, prefix)) {
let href = `/${object.key + (object.customMetadata?.resourcetype === '<collection />' ? '/' : '')}`;
page += `
<response>
<href>${href}</href>
<propstat>
@ -322,45 +345,27 @@ function generate_propfind_response(object: R2Object | null): string {
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>`;
}
async function handle_propfind(request: Request, bucket: R2Bucket): Promise<Response> {
let resource_path = make_resource_path(request);
let is_collection: boolean;
let page = `<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:">`;
if (resource_path === '') {
page += generate_propfind_response(null);
is_collection = true;
} else {
let object = await bucket.head(resource_path);
if (object === null) {
return new Response('Not Found', { status: 404 });
}
is_collection = object.customMetadata?.resourcetype === '<collection />';
page += generate_propfind_response(object);
}
if (is_collection) {
let depth = request.headers.get('Depth') ?? 'infinity';
switch (depth) {
case '0':
break;
case '1':
{
let prefix = resource_path === '' ? resource_path : resource_path + '/';
for await (let object of listAll(bucket, prefix)) {
page += generate_propfind_response(object);
}
}
break;
case 'infinity':
{
let prefix = resource_path === '' ? resource_path : resource_path + '/';
case 'infinity': {
let prefix = resource_path === "" ? resource_path : resource_path + '/';
for await (let object of listAll(bucket, prefix, true)) {
page += generate_propfind_response(object);
let href = `/${object.key + (object.customMetadata?.resourcetype === '<collection />' ? '/' : '')}`;
page += `
<response>
<href>${href}</href>
<propstat>
<prop>
${Object.entries(fromR2Object(object))
.filter(([_, value]) => value !== undefined)
.map(([key, value]) => `<${key}>${value}</${key}>`)
.join('\n ')
}
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>`;
}
}
break;
@ -390,11 +395,8 @@ async function handle_copy(request: Request, bucket: R2Bucket): Promise<Response
destination = destination.endsWith('/') ? destination.slice(0, -1) : destination;
// Check if the parent directory exists
let destination_parent = destination
.split('/')
.slice(0, destination.endsWith('/') ? -2 : -1)
.join('/');
if (destination_parent !== '' && !(await bucket.head(destination_parent))) {
let destination_parent = destination.split('/').slice(0, destination.endsWith('/') ? -2 : -1).join('/');
if (destination_parent !== '' && !await bucket.head(destination_parent)) {
return new Response('Conflict', { status: 409 });
}
@ -415,10 +417,10 @@ async function handle_copy(request: Request, bucket: R2Bucket): Promise<Response
let depth = request.headers.get('Depth') ?? 'infinity';
switch (depth) {
case 'infinity': {
let prefix = resource_path + '/';
let prefix = resource_path + "/";
const copy = async (object: R2Object) => {
let target = destination + '/' + object.key.slice(prefix.length);
target = target.endsWith('/') ? target.slice(0, -1) : target;
let target = destination + "/" + object.key.slice(prefix.length);
target = target.endsWith("/") ? target.slice(0, -1) : target;
let src = await bucket.get(object.key);
if (src !== null) {
await bucket.put(target, src.body, {
@ -485,11 +487,8 @@ async function handle_move(request: Request, bucket: R2Bucket): Promise<Response
destination = destination.endsWith('/') ? destination.slice(0, -1) : destination;
// Check if the parent directory exists
let destination_parent = destination
.split('/')
.slice(0, destination.endsWith('/') ? -2 : -1)
.join('/');
if (destination_parent !== '' && !(await bucket.head(destination_parent))) {
let destination_parent = destination.split('/').slice(0, destination.endsWith('/') ? -2 : -1).join('/');
if (destination_parent !== '' && !await bucket.head(destination_parent)) {
return new Response('Conflict', { status: 409 });
}
@ -507,8 +506,7 @@ async function handle_move(request: Request, bucket: R2Bucket): Promise<Response
return new Response('Bad Request', { status: 400 });
}
if (destination_exists) {
// Delete the destination first
if (destination_exists) { // Delete the destination first
await handle_delete(new Request(new URL(destination_header), request), bucket);
}
@ -518,10 +516,10 @@ async function handle_move(request: Request, bucket: R2Bucket): Promise<Response
let depth = request.headers.get('Depth') ?? 'infinity';
switch (depth) {
case 'infinity': {
let prefix = resource_path + '/';
const move = async (object: R2Object) => {
let target = destination + '/' + object.key.slice(prefix.length);
target = target.endsWith('/') ? target.slice(0, -1) : target;
let prefix = resource_path + "/";
const copy = async (object: R2Object) => {
let target = destination + "/" + object.key.slice(prefix.length);
target = target.endsWith("/") ? target.slice(0, -1) : target;
let src = await bucket.get(object.key);
if (src !== null) {
await bucket.put(target, src.body, {
@ -531,9 +529,9 @@ async function handle_move(request: Request, bucket: R2Bucket): Promise<Response
await bucket.delete(object.key);
}
};
let promise_array = [move(resource)];
let promise_array = [copy(resource)];
for await (let object of listAll(bucket, prefix, true)) {
promise_array.push(move(object));
promise_array.push(copy(object));
}
await Promise.all(promise_array);
if (destination_exists) {
@ -580,19 +578,10 @@ async function handle_move(request: Request, bucket: R2Bucket): Promise<Response
}
}
const DAV_CLASS = '1';
const SUPPORT_METHODS = ['OPTIONS', 'PROPFIND', 'MKCOL', 'GET', 'HEAD', 'PUT', 'COPY', 'MOVE'];
async function dispatch_handler(request: Request, bucket: R2Bucket): Promise<Response> {
switch (request.method) {
case 'OPTIONS': {
return new Response(null, {
status: 204,
headers: {
Allow: SUPPORT_METHODS.join(', '),
DAV: DAV_CLASS,
},
});
return await handle_options(request, bucket);
}
case 'HEAD': {
return await handle_head(request, bucket);
@ -622,9 +611,9 @@ async function dispatch_handler(request: Request, bucket: R2Bucket): Promise<Res
return new Response('Method Not Allowed', {
status: 405,
headers: {
Allow: SUPPORT_METHODS.join(', '),
DAV: DAV_CLASS,
},
'Allow': SUPPORT_METHODS.join(', '),
'DAV': DAV_CLASS,
}
});
}
}
@ -634,15 +623,11 @@ export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const { bucket } = env;
if (
request.method !== 'OPTIONS' &&
request.headers.get('Authorization') !== `Basic ${btoa(`${env.USERNAME}:${env.PASSWORD}`)}`
) {
if (request.headers.get('Authorization') !== `Basic ${btoa(`${env.USERNAME}:${env.PASSWORD}`)}`) {
return new Response('Unauthorized', {
status: 401,
headers: {
status: 401, headers: {
'WWW-Authenticate': 'Basic realm="webdav"',
},
}
});
}
@ -651,19 +636,15 @@ export default {
// Set CORS headers
response.headers.set('Access-Control-Allow-Origin', request.headers.get('Origin') ?? '*');
response.headers.set('Access-Control-Allow-Methods', SUPPORT_METHODS.join(', '));
response.headers.set(
'Access-Control-Allow-Headers',
['authorization', 'content-type', 'depth', 'overwrite', 'destination', 'range'].join(', '),
response.headers.set('Access-Control-Allow-Headers',
["authorization", "content-type", "depth", "overwrite", "destination", "range"].join(', ')
);
response.headers.set(
'Access-Control-Expose-Headers',
['content-type', 'content-length', 'dav', 'etag', 'last-modified', 'location', 'date', 'content-range'].join(
', ',
),
response.headers.set('Access-Control-Expose-Headers',
["content-type", "content-length", "dav", "etag", "last-modified", "location", "date", "content-range"].join(', ')
);
response.headers.set('Access-Control-Allow-Credentials', 'false');
response.headers.set('Access-Control-Max-Age', '86400');
return response;
return response
},
};

View File

@ -12,9 +12,7 @@
/* Language and Environment */
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": [
"es2021"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"jsx": "react" /* Specify what JSX code is generated. */,
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
@ -33,9 +31,7 @@
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
"types": [
"@cloudflare/workers-types"
] /* Specify type package names to be included without being referenced in a source file. */,
"types": ["@cloudflare/workers-types"] /* Specify type package names to be included without being referenced in a source file. */,
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
"resolveJsonModule": true /* Enable importing .json files */,
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */

View File

@ -7,8 +7,9 @@ node_compat = true
# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)
# Note: Use secrets to store sensitive data.
# Docs: https://developers.cloudflare.com/workers/platform/environment-variables
# [vars]
# MY_VARIABLE = "my-value"
[vars]
USERNAME = "USERNAME"
PASSWORD = "PASSWORD"
# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs.
# Docs: https://developers.cloudflare.com/workers/runtime-apis/kv
@ -20,7 +21,7 @@ node_compat = true
# Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/
[[r2_buckets]]
binding = "bucket" # <~ valid JavaScript variable name
bucket_name = "zotero"
bucket_name = "webdav"
# Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer.
# Docs: https://developers.cloudflare.com/queues/get-started