Compare commits

...

16 Commits
ci ... worker

Author SHA1 Message Date
5fae701f01 Specify R2 binding 2024-10-09 17:31:56 +08:00
Velih Dzen
5f272ec046
Implement partial content support for GET: Add Content-Length and Content-Range headers (#9) 2024-09-15 00:31:07 +08:00
dependabot[bot]
7503d6115b
Bump braces from 3.0.2 to 3.0.3 (#8)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-19 09:47:53 +08:00
dependabot[bot]
3afcdcfe46
Bump ws from 8.16.0 to 8.17.1 (#7)
Bumps [ws](https://github.com/websockets/ws) from 8.16.0 to 8.17.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.16.0...8.17.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-19 09:46:59 +08:00
dependabot[bot]
ab7622cc5b
Bump undici from 5.28.3 to 5.28.4 (#6)
Bumps [undici](https://github.com/nodejs/undici) from 5.28.3 to 5.28.4.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v5.28.3...v5.28.4)

---
updated-dependencies:
- dependency-name: undici
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-05 17:38:46 +08:00
abersheeran
2e6d001c0d Update lock 2024-03-02 14:01:25 +08:00
abersheeran
b3c8753308 prettierrc printWidth 120 2024-03-02 13:54:52 +08:00
Aber
11d1825dae
Close #3 2024-03-02 13:51:34 +08:00
Aber
9450ba3577
Merge pull request #2 from abersheeran/dependabot/npm_and_yarn/undici-5.28.3
Bump undici from 5.28.2 to 5.28.3
2024-02-18 09:34:53 +08:00
dependabot[bot]
237ff01c22
Bump undici from 5.28.2 to 5.28.3
Bumps [undici](https://github.com/nodejs/undici) from 5.28.2 to 5.28.3.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v5.28.2...v5.28.3)

---
updated-dependencies:
- dependency-name: undici
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-16 18:36:26 +00:00
abersheeran
c104601ab1 Refactor content headers and handle move operation 2024-01-06 11:28:32 +08:00
abersheeran
f81cee4647 Refactor DAV handling and remove unused code 2024-01-06 10:10:37 +08:00
abersheeran
4788813c39 Refactor content metadata handling in index.ts 2024-01-06 10:08:16 +08:00
abersheeran
a534702d24 Refactor handle_propfind function to use a helper function for generating propfind response 2024-01-06 10:04:51 +08:00
abersheeran
e740a54af9 Update README.md and wrangler.toml 2023-12-26 19:44:27 +08:00
abersheeran
988fc29dda pettier . --write 2023-12-24 13:51:38 +08:00
7 changed files with 357 additions and 257 deletions

View File

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

View File

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

267
package-lock.json generated
View File

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

View File

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

View File

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

View File

@ -12,7 +12,9 @@
/* Language and Environment */ /* Language and Environment */
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, "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. */, "jsx": "react" /* Specify what JSX code is generated. */,
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
@ -31,7 +33,9 @@
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "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. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ // "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. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
"resolveJsonModule": true /* Enable importing .json files */, "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. */ // "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,9 +7,8 @@ node_compat = true
# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) # Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)
# Note: Use secrets to store sensitive data. # Note: Use secrets to store sensitive data.
# Docs: https://developers.cloudflare.com/workers/platform/environment-variables # Docs: https://developers.cloudflare.com/workers/platform/environment-variables
[vars] # [vars]
USERNAME = "USERNAME" # MY_VARIABLE = "my-value"
PASSWORD = "PASSWORD"
# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs. # Bind a KV Namespace. Use KV as persistent storage for small key-value pairs.
# Docs: https://developers.cloudflare.com/workers/runtime-apis/kv # Docs: https://developers.cloudflare.com/workers/runtime-apis/kv
@ -21,7 +20,7 @@ PASSWORD = "PASSWORD"
# Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/ # Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/
[[r2_buckets]] [[r2_buckets]]
binding = "bucket" # <~ valid JavaScript variable name binding = "bucket" # <~ valid JavaScript variable name
bucket_name = "webdav" bucket_name = "zotero"
# Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. # 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 # Docs: https://developers.cloudflare.com/queues/get-started