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, "singleQuote": true,
"semi": true, "semi": true,
"useTabs": true "useTabs": true

View File

@ -4,24 +4,22 @@
Use Cloudflare Workers to provide a WebDav interface for Cloudflare R2. Use Cloudflare Workers to provide a WebDav interface for Cloudflare R2.
## Usage ## Configuration
Change wrangler.toml to your own. Change wrangler.toml to your own.
```toml ```toml
[[r2_buckets]] [[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' bucket_name = 'webdav'
[vars]
USERNAME = "USERNAME"
PASSWORD = "PASSWORD"
``` ```
Then use wrangler to deploy. * USERNAME: The username of WebDav.
* 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,24 +9,23 @@
"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.3.1", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.1.tgz", "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.2.0.tgz",
"integrity": "sha512-lKN2XCfKCmpKb86a1tl4GIwsJYDy9TGuwjhDELLmpKygQhw8X2xR4dusgpC5Tg7q1pB96Eb0rBo81kxSILQMwA==", "integrity": "sha512-MVbXLbTcAotOPUj0pAMhVtJ+3/kFkwJqc5qNOleOZTv6QkZZABDMS21dSrSlVswEHwrpWC03e4fWytjqKvuE2A==",
"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.20240223.1", "version": "1.20231030.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20240223.1.tgz", "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20231030.0.tgz",
"integrity": "sha512-GgHnvkazLFZ7bmR96+dTX0+WS13a+5CHOOP3qNUSR9oEnR4hHzpNIO75MuZsm9RPAXrvtT7nSJmYwiGCZXh6og==", "integrity": "sha512-J4PQ9utPxLya9yHdMMx3AZeC5M/6FxcoYw6jo9jbDDFTy+a4Gslqf4Im9We3aeOEdPXa3tgQHVQOSelJSZLhIw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -40,9 +39,9 @@
} }
}, },
"node_modules/@cloudflare/workerd-darwin-arm64": { "node_modules/@cloudflare/workerd-darwin-arm64": {
"version": "1.20240223.1", "version": "1.20231030.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240223.1.tgz", "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20231030.0.tgz",
"integrity": "sha512-ZF98vUmVlC0EVEd3RRuhMq4HYWFcqmPtMIMPUN2+ivEHR92TE+6E/AvdeE6wcE7fKHQ+fk3dH+ZgB0GcfptfnA==", "integrity": "sha512-WSJJjm11Del4hSneiNB7wTXGtBXI4QMCH9l5qf4iT5PAW8cESGcCmdHtWDWDtGAAGcvmLT04KNvmum92vRKKQQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -56,9 +55,9 @@
} }
}, },
"node_modules/@cloudflare/workerd-linux-64": { "node_modules/@cloudflare/workerd-linux-64": {
"version": "1.20240223.1", "version": "1.20231030.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20240223.1.tgz", "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20231030.0.tgz",
"integrity": "sha512-1kH41ewNTGMmAk2zUX0Xj9VSfidl26GQ0ZrWMdi5kwf6gAHd3oVWNigJN078Jx56SgQxNcqVGX1LunqF949asw==", "integrity": "sha512-2HUeRTvoCC17fxE0qdBeR7J9dO8j4A8ZbdcvY8pZxdk+zERU6+N03RTbk/dQMU488PwiDvcC3zZqS4gwLfVT8g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -72,9 +71,9 @@
} }
}, },
"node_modules/@cloudflare/workerd-linux-arm64": { "node_modules/@cloudflare/workerd-linux-arm64": {
"version": "1.20240223.1", "version": "1.20231030.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20240223.1.tgz", "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20231030.0.tgz",
"integrity": "sha512-Ro8Og5C4evh890JrRm0B8sHyumRtgL+mUqPeNcEsyG45jAQy5xHpapHnmJAMJV6ah+zDc1cZtQq+en39SojXvQ==", "integrity": "sha512-4/GK5zHh+9JbUI6Z5xTCM0ZmpKKHk7vu9thmHjUxtz+o8Ne9DoD7DlDvXQWgMF6XGaTubDWyp3ttn+Qv8jDFuQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -88,9 +87,9 @@
} }
}, },
"node_modules/@cloudflare/workerd-windows-64": { "node_modules/@cloudflare/workerd-windows-64": {
"version": "1.20240223.1", "version": "1.20231030.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20240223.1.tgz", "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20231030.0.tgz",
"integrity": "sha512-eNP5sfaP6WL07DaoigYou5ASPF7jHsFiNzzD2vGOI7yFd5sPlb7sJ4SpIy+BCX0LdqFnjmlUo5Xr+/I6qJ2Nww==", "integrity": "sha512-fb/Jgj8Yqy3PO1jLhk7mTrHMkR8jklpbQFud6rL/aMAn5d6MQbaSrYOCjzkKGp0Zng8D2LIzSl+Fc0C9Sggxjg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -104,9 +103,9 @@
} }
}, },
"node_modules/@cloudflare/workers-types": { "node_modules/@cloudflare/workers-types": {
"version": "4.20240222.0", "version": "4.20231121.0",
"resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20240222.0.tgz", "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20231121.0.tgz",
"integrity": "sha512-luO0BdK3rLlCv3B240+cTrfqm+XSbHtpk+88aJtGwzyVK9QF/Xz8lBgE/oZZLN8nCTmOvxAZnszyxUuZ8GP8Cg==", "integrity": "sha512-+kWfpCkqiepwAKXyHoE0gnkPgkLhz0/9HOBIGhHRsUvUKvhUtm3mbqqoGRWgF1qcjzrDUBbrrOq4MYHfFtc2RA==",
"dev": true "dev": true
}, },
"node_modules/@cspotcode/source-map-support": { "node_modules/@cspotcode/source-map-support": {
@ -496,18 +495,18 @@
} }
}, },
"node_modules/@fastify/busboy": { "node_modules/@fastify/busboy": {
"version": "2.1.1", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz",
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@jridgewell/resolve-uri": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@ -530,27 +529,27 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.11.24", "version": "20.10.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz",
"integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==",
"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.11", "version": "1.3.10",
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.10.tgz",
"integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", "integrity": "sha512-y6PJDYN4xYBxwd22l+OVH35N+1fCYWiuC3aiP2SlXVE6Lo7SS+rSx9r89hLxrP4pn6n1lBGhHJ12pj3F3Mpttw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.11.3", "version": "8.11.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
"dev": true, "dev": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@ -560,9 +559,9 @@
} }
}, },
"node_modules/acorn-walk": { "node_modules/acorn-walk": {
"version": "8.3.2", "version": "8.3.1",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz",
"integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
@ -606,17 +605,23 @@
"dev": true "dev": true
}, },
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.3", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"fill-range": "^7.1.1" "fill-range": "^7.0.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",
@ -628,10 +633,16 @@
} }
}, },
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "3.6.0", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"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",
@ -644,9 +655,6 @@
"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"
} }
@ -751,9 +759,9 @@
} }
}, },
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
@ -776,15 +784,6 @@
"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",
@ -813,18 +812,6 @@
"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",
@ -837,18 +824,6 @@
"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",
@ -901,20 +876,20 @@
} }
}, },
"node_modules/miniflare": { "node_modules/miniflare": {
"version": "3.20240223.0", "version": "3.20231030.4",
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240223.0.tgz", "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20231030.4.tgz",
"integrity": "sha512-8T/36FEfvsL4aMF7SLZ28v+PQL0jsUlVw/u114GYcdobkyPax9E6Ahn0XePOHEqLxQSndwPee+eS1phHANFePA==", "integrity": "sha512-7MBz0ArLuDop1WJGZC6tFgN6c5MRyDOIlxbm3yp0TRBpvDS/KsTuWCQcCjsxN4QQ5zvL3JTkuIZbQzRRw/j6ow==",
"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.28.2", "undici": "^5.22.1",
"workerd": "1.20240223.1", "workerd": "1.20231030.0",
"ws": "^8.11.0", "ws": "^8.11.0",
"youch": "^3.2.2", "youch": "^3.2.2",
"zod": "^3.20.6" "zod": "^3.20.6"
@ -977,12 +952,6 @@
"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",
@ -1001,21 +970,6 @@
"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",
@ -1034,23 +988,6 @@
"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",
@ -1112,6 +1049,16 @@
"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",
@ -1139,18 +1086,6 @@
"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",
@ -1183,9 +1118,9 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "5.28.4", "version": "5.28.2",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.2.tgz",
"integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", "integrity": "sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@fastify/busboy": "^2.0.0" "@fastify/busboy": "^2.0.0"
@ -1201,9 +1136,9 @@
"dev": true "dev": true
}, },
"node_modules/workerd": { "node_modules/workerd": {
"version": "1.20240223.1", "version": "1.20231030.0",
"resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20240223.1.tgz", "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20231030.0.tgz",
"integrity": "sha512-Mo1fwdp6DLva4/fWdL09ZdYllkO45I4YpWG5PbF/YUGFlu2aMk24fmU6Pd6fo5/cWek4F+n3LmYEKKHfqjiJIA==", "integrity": "sha512-+FSW+d31f8RrjHanFf/R9A+Z0csf3OtsvzdPmAKuwuZm/5HrBv83cvG9fFeTxl7/nI6irUUXIRF9xcj/NomQzQ==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"bin": { "bin": {
@ -1213,29 +1148,29 @@
"node": ">=16" "node": ">=16"
}, },
"optionalDependencies": { "optionalDependencies": {
"@cloudflare/workerd-darwin-64": "1.20240223.1", "@cloudflare/workerd-darwin-64": "1.20231030.0",
"@cloudflare/workerd-darwin-arm64": "1.20240223.1", "@cloudflare/workerd-darwin-arm64": "1.20231030.0",
"@cloudflare/workerd-linux-64": "1.20240223.1", "@cloudflare/workerd-linux-64": "1.20231030.0",
"@cloudflare/workerd-linux-arm64": "1.20240223.1", "@cloudflare/workerd-linux-arm64": "1.20231030.0",
"@cloudflare/workerd-windows-64": "1.20240223.1" "@cloudflare/workerd-windows-64": "1.20231030.0"
} }
}, },
"node_modules/wrangler": { "node_modules/wrangler": {
"version": "3.30.1", "version": "3.21.0",
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.30.1.tgz", "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.21.0.tgz",
"integrity": "sha512-cT6Ezx8h2v5QiI0HWhnHVy32ng4omdMVdhaMQLuMnyMIHmyDoRg7pmrbhtZfj0663gExLdVtE4ucK//yncVTwg==", "integrity": "sha512-DLoo4XfjeyuGRAVWZFHmU1jWnZIfyLGDm6Ika9oy/CLCPfJzVJvf2jI70EU5BlEHWDZXMSJKw7FDdgSqwhaQXg==",
"dev": true, "dev": true,
"dependencies": { "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-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.20240223.0", "miniflare": "3.20231030.4",
"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",
@ -1250,20 +1185,12 @@
}, },
"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.17.1", "version": "8.15.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.15.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "integrity": "sha512-W5OZiCjXEmk0yZ66ZN82beM5Sz7l7coYxpRkzS+p9PP+ToQry8szKh+61eNktr7EA9DOwvFGhfC605jDHbP6QQ==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"

View File

@ -5,13 +5,10 @@
"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,9 +46,22 @@ 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;
@ -58,7 +71,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) {
@ -66,11 +79,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 />",
}; };
} }
@ -86,12 +99,23 @@ 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, {
@ -109,15 +133,12 @@ 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, { return new Response(page, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
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,
@ -126,70 +147,40 @@ 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 && contentLength !== object.size) ? 206 : 200, status: object.range ? 206 : 200,
headers: { headers: {
'Content-Type': object.httpMetadata?.contentType ?? 'application/octet-stream', 'Content-Type': object.httpMetadata?.contentType ?? 'application/octet-stream',
'Content-Length': contentLength.toString(), // TODO: Content-Length, Content-Range
...({ '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 ? {
...(object.httpMetadata?.contentEncoding
? {
'Content-Encoding': object.httpMetadata.contentEncoding, 'Content-Encoding': object.httpMetadata.contentEncoding,
} } : {}),
: {}), ...(object.httpMetadata?.contentLanguage ? {
...(object.httpMetadata?.contentLanguage
? {
'Content-Language': object.httpMetadata.contentLanguage, 'Content-Language': object.httpMetadata.contentLanguage,
} } : {}),
: {}), ...(object.httpMetadata?.cacheControl ? {
...(object.httpMetadata?.cacheControl
? {
'Cache-Control': object.httpMetadata.cacheControl, 'Cache-Control': object.httpMetadata.cacheControl,
} } : {}),
: {}), ...(object.httpMetadata?.cacheExpiry ? {
...(object.httpMetadata?.cacheExpiry
? {
'Cache-Expiry': object.httpMetadata.cacheExpiry.toISOString(), '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 });
@ -218,11 +209,10 @@ 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, let r2_objects, cursor: string | undefined = undefined;
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);
} }
@ -244,14 +234,13 @@ 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, let r2_objects, cursor: string | undefined = undefined;
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);
} }
@ -278,22 +267,28 @@ 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 });
} }
function generate_propfind_response(object: R2Object | null): string { async function handle_propfind(request: Request, bucket: R2Bucket): Promise<Response> {
if (object === null) { let resource_path = make_resource_path(request);
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>
@ -306,10 +301,38 @@ function generate_propfind_response(object: R2Object | null): string {
<status>HTTP/1.1 200 OK</status> <status>HTTP/1.1 200 OK</status>
</propstat> </propstat>
</response>`; </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 />' ? '/' : '')}`; 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> <response>
<href>${href}</href> <href>${href}</href>
<propstat> <propstat>
@ -322,45 +345,27 @@ function generate_propfind_response(object: R2Object | null): string {
<status>HTTP/1.1 200 OK</status> <status>HTTP/1.1 200 OK</status>
</propstat> </propstat>
</response>`; </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; 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)) {
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; break;
@ -390,11 +395,8 @@ 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 let destination_parent = destination.split('/').slice(0, destination.endsWith('/') ? -2 : -1).join('/');
.split('/') if (destination_parent !== '' && !await bucket.head(destination_parent)) {
.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 });
} }
@ -415,10 +417,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, {
@ -485,11 +487,8 @@ 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 let destination_parent = destination.split('/').slice(0, destination.endsWith('/') ? -2 : -1).join('/');
.split('/') if (destination_parent !== '' && !await bucket.head(destination_parent)) {
.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 });
} }
@ -507,8 +506,7 @@ 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) { if (destination_exists) { // Delete the destination first
// 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);
} }
@ -518,10 +516,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 move = 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, {
@ -531,9 +529,9 @@ async function handle_move(request: Request, bucket: R2Bucket): Promise<Response
await bucket.delete(object.key); await bucket.delete(object.key);
} }
}; };
let promise_array = [move(resource)]; let promise_array = [copy(resource)];
for await (let object of listAll(bucket, prefix, true)) { for await (let object of listAll(bucket, prefix, true)) {
promise_array.push(move(object)); promise_array.push(copy(object));
} }
await Promise.all(promise_array); await Promise.all(promise_array);
if (destination_exists) { 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> { async function dispatch_handler(request: Request, bucket: R2Bucket): Promise<Response> {
switch (request.method) { switch (request.method) {
case 'OPTIONS': { case 'OPTIONS': {
return new Response(null, { return await handle_options(request, bucket);
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);
@ -622,9 +611,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,
}, }
}); });
} }
} }
@ -634,15 +623,11 @@ 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 ( if (request.headers.get('Authorization') !== `Basic ${btoa(`${env.USERNAME}:${env.PASSWORD}`)}`) {
request.method !== 'OPTIONS' &&
request.headers.get('Authorization') !== `Basic ${btoa(`${env.USERNAME}:${env.PASSWORD}`)}`
) {
return new Response('Unauthorized', { return new Response('Unauthorized', {
status: 401, status: 401, headers: {
headers: {
'WWW-Authenticate': 'Basic realm="webdav"', 'WWW-Authenticate': 'Basic realm="webdav"',
}, }
}); });
} }
@ -651,19 +636,15 @@ 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( response.headers.set('Access-Control-Allow-Headers',
'Access-Control-Allow-Headers', ["authorization", "content-type", "depth", "overwrite", "destination", "range"].join(', ')
['authorization', 'content-type', 'depth', 'overwrite', 'destination', 'range'].join(', '),
); );
response.headers.set( response.headers.set('Access-Control-Expose-Headers',
'Access-Control-Expose-Headers', ["content-type", "content-length", "dav", "etag", "last-modified", "location", "date", "content-range"].join(', ')
['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,9 +12,7 @@
/* 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": [ "lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"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. */
@ -33,9 +31,7 @@
// "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": [ "types": ["@cloudflare/workers-types"] /* Specify type package names to be included without being referenced in a source file. */,
"@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,8 +7,9 @@ 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]
# MY_VARIABLE = "my-value" USERNAME = "USERNAME"
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
@ -20,7 +21,7 @@ node_compat = true
# 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 = "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. # 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